/* ---------- DESIGN TOKENS (2026-08-27) ----------
   13 values replace 41 of the 92 colours style.css used to hold, for 0 variables.

   2 colours changed value on purpose; the other 11 are exactly what was already there:
     --text-muted  #9a9ab0 -> #cacae1   APCA Lc 48 -> 75
     --text-faint  #7a7a95 -> #b0b0cd   APCA Lc 32 -> 60

   Everything else that moved is a near-duplicate merging into the colour it was already imitating,
   and every one of those merges came out of a measurement (CIE Lab dE inside its role's limit),
   never a list somebody typed. scripts/palette/tokens.json holds the map and the dE for each.

   Contrast is APCA (Lc), not WCAG: WCAG 2.x overstates contrast for dark pairs badly enough that a
   "passing" ratio can be unreadable. Body text sits at Lc 93, read-me text at 75, labels at 60.

   Labels (.meta-label, .sa-detail, .etab .cnt) deliberately take --text-faint, NOT --text-muted:
   lifting them to the same step as the value made "Migratory Echoes" sink into its own annotations.
   A label is meant to be skipped; an explanation is meant to be read.

   Add a colour here, not inline. That is the whole point.
   ------------------------------------------------------------ */
:root {
  --bg: #090810;
  --bg-lift: #1c1a31;   /* the far end of the ground wash, at the 1.23:1 measured off his reference */
  --surface: #282731;
  --surface-raised: #35343f;
  --border: #70707e;
  --border-subtle: #474754;
  --text: #d8d8e1;
  --text-muted: #9d9ca5;
  --text-faint: #8e8e96;
  /* ⛔ SHAPE: SQUARE. His call 2026-09-08 — "so maybe try the square and some pattern like the
     lobby one first", with /lobby-design.html as the reference for the whole system.
     ⚠ ONE LEVER ON PURPOSE: he said "try", so every border-radius on the site reads this token and
     going back to soft is one line, not a second sweep. The --r-4/--r-8 scale in the generated
     block below is kept because scripts/geom/build-geometry.mjs still names it. */
  --r: 0;

  /* ⛔ ELEVATION IS LIGHTNESS, NOT SHADOW — measured, not stylistic. On a near-black ground a shadow
     is a dark thing on a dark thing: he could not see the four-step shadow scale at all, and he was
     right. Each step is WHITE OVER THE GROUND at a fixed percentage (Material's dark-theme rule,
     and what /lobby-design.html was built on):
       --l0  #090810  0%   the page itself (identical to --bg; named so the ladder reads as one)
       --l1  #15141c  5%   a resting block
       --l2  #1d1c23  8%   a raised block — ready, focused
       --l3  #26262d  12%  a FIELD. ⚠ A field is LIGHTER than what holds it, never sunk: "sunk" is a
                           light-mode idea and inverts on a dark ground.
     ⚠ --rule is a HAIRLINE, not a surface. Area is not in the contrast formula, so a value solved
     to be seen as a 1px line is not the same value as a plane. */
  --l0: #090810;
  --l1: #15141c;
  --l2: #1d1c23;
  --l3: #26262d;
  --rule: #24232c;

  /* ⛔ THESE LIVE IN THE HAND-WRITTEN :root, NOT IN THE GENERATED BLOCK BELOW, AND THAT COST AN HOUR.
     They were first written beside --r-4, which is INSIDE the block scripts/geom/apply-geometry.mjs
     replaces on every run — so the next run deleted all six. Nothing errored: an undefined var()
     makes its declaration invalid at computed-value time, so `background: var(--l2)` silently
     became transparent and the settings panel rendered with no surface at all. ⚠ CSS HAS NO SYNTAX
     ERRORS; only the screenshot showed it. */

  --accent: #6364fe;
  --special-fill: #9460b8;
  --bad-fill: #c44f55;
  --warn-fill: #ab6602;
  --good-fill: #1d8742;
  /* NAMED, NOT NEW (2026-09-25 cleanup): values the sheet already typed by hand in several places, each given one name.
     Same values — nothing on screen changes. */
  --hover-fill: #3b3a47;   /* a control's hover plane */
  --accent-hover: #8c8dff; /* an openable name under the pointer */
  --on-tile: #0b0a12;      /* the letter on an avatar tile */
  --crown: #d9a63c;        /* the host crown */
  --accent-check: #52598f; /* native checkbox / range accent-color */
  --accent-fill: #6265ed;   /* the accent as a PLANE — white on it is 4.5:1 */
  /* RESULT TINTS — one strength everywhere (his colour pass 4b, 2026-09-25): a row or seat washed with a result.
     The history rows, the round's artist rows and the ready lobby seat all read these; hover is one fixed step up. */
  --good-tint: rgba(29, 135, 66, .20);
  --bad-tint: rgba(196, 79, 85, .20);
  --good-tint-hover: rgba(29, 135, 66, .36);
  --bad-tint-hover: rgba(196, 79, 85, .36);
  --good: #06ff79;
  --warn: #fc9800;
  --bad: #fd0444;
  --special: #b728fe;
}

/* ── The geometry scale ──────────────────────────────────────────────────────────────────────
   ONE VALUE PER AXIS, and it is a standing rule, not a refactor: a page that invents its own
   length is a bug. Generated by scripts/geom/build-geometry.mjs and applied by
   scripts/geom/apply-geometry.mjs, which reverts rather than warns when the file disagrees
   with the map. Before: 31 font sizes · 12 radii · 11 tracking values · 28 spacing values.
   Type is a 1.2 scale anchored at 15px (fs-1=12px · fs-2=15px · fs-3=18px · fs-4=22px · fs-5=26px · fs-6=31px · fs-7=37px · fs-8=45px · fs-9=54px) — anchored there, not at 16,
   because the mass of the app sits at 14.4px and anchoring at 16 would have shrunk it to 13px,
   against his own report that 0.9rem reads too small.
   ⚠ Four em font-sizes and one glyph tracking value are deliberately NOT on the scale; they are
   sized against their parent, not the root, and geometry.json names each one. */
:root {
  /* type — ratio 1.2, anchored at 15px */
  --fs-1: 0.75rem;
  --fs-2: 0.9375rem;
  --fs-3: 1.125rem;
  --fs-4: 1.375rem;
  --fs-5: 1.625rem;
  --fs-6: 1.9375rem;
  --fs-7: 2.3125rem;
  --fs-8: 2.8125rem;
  --fs-9: 3.375rem;
  /* radius — one corner style, repeated */
  --r-4: 4px;
  --r-8: 8px;
  --r-pill: 999px;
  /* space — the 8-point grid, its half and its quarter */
  --sp-2: 2px;
  --sp-4: 4px;
  --sp-8: 8px;
  --sp-12: 12px;
  --sp-16: 16px;
  --sp-24: 24px;
  --sp-32: 32px;
  --sp-48: 48px;
  --sp-64: 64px;
  /* line-height — solid is a single-line control whose height comes from padding */
  --lh-solid: 1;
  --lh-ui: 1.35;
  --lh-prose: 1.5;
  /* tracking — four steps, clustered from what the approved lobby actually uses */
  --track-tight: 0.02em;
  --track-caps: 0.06em;
  --track-wide: 0.1em;
  --track-wider: 0.18em;
}

/* ---------- THE SITE FONTS ----------
   Loaded from Google Fonts by a <link> in every page's <head> — `test/sitefont.test.js` fails if a
   page forgets it, and a page that forgets it renders in the fallback and looks ALMOST right.

   ⛔ `font-synthesis: none` SHIPS WITH THIS AND IS NOT DECORATION. Without it the browser FAKES a
   weight it was not given by smearing the nearest one, which is the distortion he was shown by
   accident while comparing faces. Synthesis off means a missing weight renders as a real weight.
   ⚠ `system-ui` stays behind every stack, so a page renders in the OS font rather than in Times if
   Google Fonts is unreachable.
   ⚠ Superseded 2026-09-08: this file used to carry Mitr as the single site font, plus a pinned
   @font-face that remapped its 200 file onto weight 400 for the Thai unicode-range. Both are gone —
   see the role block below for why Prompt makes that machinery unnecessary. */
/* ⛔ FOUR TYPE ROLES, PROVISIONAL — decided by him on /game-face.html over 2026-09-08, and marked
   provisional because the last of them was chosen without a use pass. His words per role:
     --font-display  Bebas Neue  "Display is also bebas"   — buttons, labels, chrome
     --font-numbers  Bebas Neue  "if tighter works then bebas" — scores, the 5/8 count, the countdown
     --font-names    Teko        "Teko wins"              — song title, card and scoreboard names
     --font-body     Prompt      "let's try going with prompt" — credits, artists, everything read
   ⚠ THREE FAMILIES, THREE JOBS. One face doing every job is the clearest slop signal in type; the
   separation IS the design. Verified on the real screen: exactly Bebas Neue, Teko and Prompt appear.

   ✅ THE MITR @font-face HACK IS GONE, and that is the point of Prompt. It used to remap Mitr's 200
   file onto weight 400 for the Thai unicode-range so English and Thai matched in weight. Prompt
   draws BOTH scripts itself, so there is no handover to patch — no unicode-range, no remap, no
   per-character fallback anywhere in the body stack.
   ⚠ Prompt stays last in the display and names stacks, where it IS the Thai fallback: Bebas Neue and
   Teko have no Thai glyphs, so a Thai title falls through to Prompt per character. Drop it from
   those stacks and every Thai character in a name loses its face.
   ⚠ ITS WEIGHT THERE IS 500, MEASURED: stem width over letter height is the only cross-script measure
   of thickness that works — Teko 600 is 22.7%, Prompt 500 is 23.6%, Prompt 400 is 19.1% and reads
   visibly lighter than the Latin beside it.

   ⛔ NEITHER BEBAS NOR TEKO HAS FIXED-WIDTH DIGITS. Bebas advances `1` at 10.58 against `8` at 15.81
   (34.2% spread). Any number that changes IN PLACE — the countdown, a live score, the 5/8 count —
   must be laid out one digit per fixed-width cell, or the line shifts as it ticks.
   ⛔ AND NUMBERS TAKE PROPORTIONAL FIGURES: `font-variant-numeric: proportional-nums` is what closed
   the gap he reported in Bebas `11` (80px -> 58.2px). ⚠ Set BOTH that and `font-feature-settings`
   — `font-variant-numeric` BEATS `font-feature-settings` for the same feature, so setting only the
   low-level property does nothing wherever `tabular-nums` is already declared. */
:root { color-scheme: dark; font-family: Mitr, system-ui, sans-serif; font-synthesis: none;
        /* ⛔ TYPE FACES SETTLED 2026-09-18/19 (his calls): labels/tags = Bebas, big titles = Kanit
           (dual-script — draws Thai where Teko couldn't), text = Mitr, numbers = Bebas. Sizes are a
           later pass. Song NAMES are text, so those elements use --font-body — see the overrides below. */
        --font-display: 'Bebas Neue', 'Arial Narrow', Mitr, sans-serif;
        --font-numbers: 'Bebas Neue', 'Arial Narrow', Mitr, sans-serif;
        --font-names: Kanit, system-ui, sans-serif;
        --font-body: Mitr, system-ui, sans-serif; }
/* The `hidden` attribute is only a UA rule (display:none), so ANY author `display:` on the same
   element silently beats it and the element stays on screen while the code believes it is gone.
   That is exactly what happened to the /add YouTube preview card (.yt-preview is display:flex).
   One defensive rule so `el.hidden = true` means hidden everywhere. */
[hidden] { display: none !important; }
body { margin: 0; background: var(--bg); color: var(--text); }
/* ── THE GROUND WASH ─────────────────────────────────────────────────────────────────────────────
   Aur, 2026-09-04, pointing at a game panel he likes: "maybe try doing the main colors/gradiant
   based on this?" MEASURED off that image rather than eyeballed (scripts/palette/reference.json):
   its four corners span 1.232:1 — brightest bottom-left, near-black bottom-right. That is a
   vignette you feel, not a gradient you see, and its smallness is what keeps it clear of the
   refusal list. `--bg-lift` is solved to that exact ratio, and carries more chroma than `--bg`
   because the reference does too: its lit corner is chroma 0.055 against the dark corner 0.022.
   ⚠ A FIXED PSEUDO-ELEMENT, not `background-attachment: fixed`. That property renders against the
   DOCUMENT box on iOS Safari rather than the viewport, so on a long /list it would stretch the
   wash over thousands of pixels and read as a flat tint. A fixed layer is viewport-sized
   everywhere and costs no scroll repaint.
   ⚠ `z-index: -1` puts it behind the page content but still ABOVE body own background, which is
   why `body` keeps its flat `--bg` — that is the fallback if this layer is ever suppressed. */
/* ⛔ TWO SOURCES, NOT ONE, AND HIS REFERENCE HAS BOTH (Aur, 2026-09-04: "the gradiant only grouped
   in the bottom left ... idk how to make the top right doesn't feels plain"). One radial left the
   whole top-right at flat --bg. His four measured corners are NOT one glow — bottom-left Y 0.0121,
   top-left 0.0088, top-right 0.0027, bottom-right 0.0004 — a bright bottom-left AND a lit top-left,
   falling away to the right. So: the source he has, plus the top-left one he also has, weaker.
   ⚠ AND BOTH REACH FURTHER THAN HIS DO. His reference is a 1012x716 panel; this is a 1920x1080
   screen, so the same relative geometry leaves a far larger flat region. The radii are widened
   until nothing is more than one source away — which is what stops the top-right reading as plain
   without lifting the whole page, the failure he named in the same breath ("making the screen too
   bright and bland").
   ⚠ `color-mix` for the weaker source rather than a second token: it is the SAME colour at less
   strength, and a token would invite it to drift into being a different one. */
/* ⛔ DIMMED ~45% — his 06:44 on the round ("wash. but still a little too bright"), and DESIGN.md makes the
   round's ground every page's ground: the lit corner is --bg-lift mixed 55% back toward --bg. */
body::before { content: ''; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(150% 120% at 12% 112%, color-mix(in oklab, var(--bg-lift) 55%, var(--bg)) 0%, transparent 58%),
    radial-gradient(130% 110% at 2% -12%, color-mix(in oklab, color-mix(in oklab, var(--bg-lift) 55%, var(--bg)) 62%, var(--bg)) 0%, transparent 55%),
    var(--bg); }
#app { max-width: 560px; margin: 0 auto; padding: 16px; }
h1 { font-size: var(--fs-4); letter-spacing: var(--track-caps); }
/* Headings default to 700 in every UA and the display faces are single-weight — so without this the browser
   would ask for a weight that is not there. `font-synthesis: none` above makes that fall back to the
   real 600 rather than a smeared fake, and this line makes it deliberate instead of a fallback. */
h1, h2, h3 { font-weight: 600; }
/* ⛔ A FORM CONTROL DOES NOT INHERIT `font-family` — the UA stylesheet sets its own and that beats
   anything the page inherits. This block set a `font-size` on `button` and never a family, so before
   the site-font change EVERY button in RMQ rendered in the browser's default form font (measured: the
   line-up buttons computed to `Arial` on Windows) while the text beside it was system-ui. Without
   these `inherit`s the site font simply would not reach any button, input, select or textarea. */
button, input, select, textarea { font-family: inherit; }
button { background: var(--surface-raised); color: inherit; border: 1px solid var(--border); border-radius: var(--r); padding: var(--sp-12) var(--sp-16); font-size: var(--fs-2); cursor: pointer; }
/* ⛔ A FILLED BUTTON TAKES --accent-fill, NEVER --accent, AND IT NEEDS ITS OWN TEXT COLOUR.
   `--accent` is solved to be READ ON the dark ground; as a plane it is bright, and `color: inherit`
   then puts near-white text on it — "+ Create profile" came out pale-on-pale in the first screenshot
   of the new palette. `--accent-fill` is the other half of the pair, solved so #fff on it is exactly
   4.5:1. Two tokens, because one colour genuinely cannot do both jobs. */
button.primary { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--text); }
.players { list-style: none; padding: 0; }
.players li { display: flex; justify-content: space-between; gap: 8px; padding: 4px 0; border-bottom: 1px solid var(--border-subtle); }
.players li.dc { opacity: 0.4; }

/* ── The lobby / loading seat CARDS (his call 2026-09-06: "try making it a card") ──────────────────
   Still ul.players > li, because main.js patchPlayers replaces the board BY THAT SELECTOR. `.pcards`
   is what turns the rows into cards, so the old flat list is one class away if it ever has to come
   back. The grid is auto-fill rather than a fixed count: four friends and ten both have to look
   deliberate, and at 375px it collapses to one column on its own. */
.players.pcards { display: grid; grid-template-columns: repeat(auto-fill, minmax(215px, 1fr));
                  gap: var(--sp-8); margin: 12px 0; }
.players.pcards li { display: grid; grid-template-columns: auto 1fr auto; grid-template-rows: auto auto;
                     column-gap: var(--sp-8); align-items: center; padding: var(--sp-8); border: 1px solid var(--surface-raised);
                     border-bottom: 1px solid var(--surface-raised); border-radius: var(--r);
                     background: var(--surface); position: relative; }
.players.pcards .pav { grid-row: 1 / span 2; width: 44px; height: 44px; border-radius: var(--r);
                       position: relative; overflow: hidden; display: flex; align-items: center;
                       justify-content: center; font-size: var(--fs-3); font-weight: 500; color: var(--bg); }
.players.pcards .pav > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.players.pcards .pav.haspic { color: transparent; }
.players.pcards .sname { grid-column: 2; grid-row: 1; font-weight: 500; white-space: nowrap;
                         overflow: hidden; text-overflow: ellipsis; }
.players.pcards .shost { color: var(--text-muted); margin-right: 4px; }
.players.pcards .ssub { grid-column: 2; grid-row: 2; font-size: var(--fs-1); color: var(--text-muted); }
/* ⛔ THE EMPTY-LIST WARNING KEEPS ITS OWN COLOUR. It used to be the words "no songs yet" in a plain
   row; on a card it still has to be the thing you notice BEFORE starting, or a friend contributes
   nothing to the draw and nobody finds out until the songs are all his. */
.players.pcards .ssub.none { color: var(--warn); }
.players.pcards .badge { grid-column: 3; grid-row: 1; color: var(--accent); }
.players.pcards .score { grid-column: 3; grid-row: 2; font-variant-numeric: tabular-nums; }
.players.pcards li.isready { border-color: var(--accent); }
.players.pcards li.spect { opacity: 0.62; }
/* host controls stay out of the way until wanted. ⚠ On touch there is no hover, so they are always
   shown there — the same rule .artist-fix-hint had to learn, and add-check A10 is its guard. */
.players.pcards .sacts { grid-column: 3; grid-row: 1 / span 2; display: flex; gap: 4px; align-self: center; }
@media (hover: hover) { .players.pcards .sacts { visibility: hidden; }
                        .players.pcards li:hover .sacts { visibility: visible; } }
.players.pcards .sacts button { padding: var(--sp-2) var(--sp-8); font-size: var(--fs-1); }
.badge { color: var(--good); }
.kick { background: none; border: 0; color: #ed898e; cursor: pointer; font-size: 0.95em; padding: 0 4px; margin-left: var(--sp-8); line-height: 1; }
.kick:hover { color: #ee8a80; }
/* give-host ★ — sits left of the kick ✕, muted until hovered so the row stays calm (it is rarer
   than kick and far less destructive, so it must not shout) */
.givehost { background: none; border: 0; color: var(--text-faint); cursor: pointer; font-size: 0.95em; padding: 0 4px; margin-left: var(--sp-8); line-height: 1; }
.givehost:hover { color: #c9a043; }
.settings { display: grid; gap: 8px; margin-top: 12px; }
.settings label { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
/* ⛔ ONE WIDTH FOR EVERYTHING WAS WRONG IN BOTH DIRECTIONS (Aur, 2026-08-29, with a screenshot:
   "number boxes are too long. why texts don't even have enough space to show full text").
   90px is far more than a two-digit number needs and far less than `Random start point` or
   `Song / Artist (S/A)` needs, so the numbers looked like text fields and the menus were clipped
   mid-word. They are different controls answering different questions and they get different room:
   a number box sized to its digits, a menu sized to its longest option and allowed to grow. */
.settings input, .settings select { background: var(--surface); color: inherit; border: 1px solid var(--border); border-radius: var(--r); padding: var(--sp-8); }
.settings input[type=number] { width: 5.5ch; text-align: center; font-variant-numeric: tabular-nums; }
.settings select { min-width: 12ch; max-width: 60%; }
.settings .hint { color: var(--text-muted); font-size: var(--fs-1); margin: -4px 0 0; }
.guessbox { position: relative; margin-top: 12px; }
.guessbox input { width: 100%; box-sizing: border-box; font-size: var(--fs-3); padding: var(--sp-12); background: var(--surface); color: inherit; border: 1px solid var(--border); border-radius: var(--r); }
/* z-index above the edge vote/settings tabs (40) so the open dropdown is never covered by them while
   typing — the tabs sit at mid-screen on mobile and used to overlap the suggestion list. */
.dropdown { list-style: none; margin: 4px 0 0; padding: 0; position: absolute; width: 100%; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); max-height: 40vh; overflow-y: auto; z-index: 45; }
/* Closing a dropdown empties the <ul> but leaves it in the DOM — and a bordered box with no rows
   still paints its 2px border, a hairline hanging under the search box (Aur's use-pass, /add).
   :empty is exact here: replaceChildren() leaves no child nodes at all, not even whitespace. */
.dropdown:empty { display: none; }
.dropdown li { padding: var(--sp-8) var(--sp-12); cursor: pointer; }
.dropdown li:hover { background: #95a0f222; } /* faint: just "mouse is over this" */
/* the arrow-key selection is stronger + has a left accent bar, so it can't be mistaken for hover (d1) */
.dropdown li.active { background: #95a0f255; box-shadow: inset 3px 0 0 #646cad; }
.picked { color: var(--good); }
/* ADR-0006 S/A readout: the two verdict lines under the box. Never counts/totals. The confirmed
   chips that used to sit here are gone (use-pass 2026-07-20) — each line names its own confirm. */
.sa-readout { margin-top: var(--sp-12); }
.sa-readout .sa-line { margin: 2px 0; font-size: var(--fs-3); }
.sa-readout .sa-line.done { color: var(--good); }
.answer-meta p { margin: 2px 0; }
.right { color: var(--good); } .wrong { color: var(--bad); }
/* S/A half-answer: amber, between the green tick and the red miss — it scored 0.5, and it must not
   read as either extreme. The verdict breakdown beside it is quieter than the name it follows. */
.partial { color: #c9a043; }
.sa-detail { color: var(--text-faint); font-size: 0.85em; }
#volume { position: fixed; top: 12px; right: 12px; display: flex; align-items: center; gap: var(--sp-8); z-index: 3; opacity: 0.85; }
#mute { background: none; border: none; padding: 2px 4px; font-size: var(--fs-2); cursor: pointer; }
#conn { position: fixed; top: 0; left: 0; right: 0; background: #852030; color: var(--text); text-align: center; padding: var(--sp-8); z-index: 4; }
.warn { color: var(--warn); font-size: var(--fs-2); }
button.minor { margin-top: var(--sp-12); padding: var(--sp-8) var(--sp-16); font-size: var(--fs-2); }
.row.danger { margin-top: var(--sp-32); border-top: 1px solid #7e6d6d; padding-top: 12px; opacity: 0.8; }
button.minor { margin-left: 8px; }

/* ---------- LEFT-EDGE VOTE TABS (spec C) ---------- */
/* Skip / Return sit fixed to the left screen edge (vertically centred) so they show identically in
   guessing AND reveal regardless of the guess box; collapsed = count pill only, hover/focus (desktop)
   or touch (see the (hover:none) rule below) reveals the label. z-index below the settings drawer/
   modals (spec A) but above the game. */
/* --edge-y is set by main.js from visualViewport = the centre of what is NOT covered by the on-screen
   keyboard, in layout-viewport px (position:fixed measures against the layout viewport, which the
   keyboard does not shrink). Unset — no visualViewport, or a page that doesn't set it — falls back to
   plain 50%, i.e. exactly the old behaviour. */
.edge-left { position: fixed; left: 0; top: var(--edge-y, 50%); transform: translateY(-50%); z-index: 40;
  display: flex; flex-direction: column; gap: var(--sp-12); transition: top .18s ease; }
/* ⛔ ON THE NEW GAME SCREEN, SKIP/RETURN ARE A ROW ON EVERY POINTER — his call 2026-09-06: "idc about
   mobile … and it's skip has its own row fine? how would that overlap with anything?" The touch
   layout below already did exactly this; this is the same treatment given to the desktop, which is
   what the game-ui mockup drew and what stops there being two Skips.
   ⚠ It REVERSES the desktop half of "Skip is a fixed left-edge tab", and it is safe for the reason
   the touch fix was: a control in normal flow cannot cover the answer box, so the whole overlay
   problem (pressable vs covering, three bugs in a week) simply does not arise. "Never restore the
   fixed overlay" is honoured, not bent — this moves FURTHER from the overlay, not back toward it.
   ⚠ Scoped to #app.gameflow. Any screen still drawing these tabs outside the new round keeps today's
   behaviour, so nothing untested changes. */
#app.gameflow .edge-left { position: static; transform: none; flex-direction: row;
  justify-content: center; gap: 0; margin: 0 0 var(--sp-12); border: 1px solid var(--border);
  border-radius: var(--r); overflow: hidden; }
#app.gameflow .edge-left .etab { flex: 1 1 0; justify-content: center; border: 0; border-radius: var(--r);
  box-shadow: none; }
#app.gameflow .edge-left .etab + .etab { border-left: 1px solid var(--border); }
/* ⛔ THE SHORT LABEL, and G8 is why. It measures `.labs` and refuses a strip whose words are
   suppressed — "a segmented strip with the labels still suppressed is the old icon-nub row wearing a
   border". My first attempt showed the LONG label instead (there is room on a desktop row) and G8 went
   red for exactly the right reason. Two arguments settle it the same way: the mockup draws "» Skip",
   and variant B is his pick with these words. **The test was left alone; the change was wrong.** */
#app.gameflow .edge-left .etab .lab { display: none; }
#app.gameflow .edge-left .etab .labs { display: inline; }
.etab { display: flex; align-items: center; gap: var(--sp-8); background: var(--surface); border: 1px solid var(--border);
  border-left: 0; border-radius: var(--r); padding: var(--sp-12) var(--sp-16) var(--sp-12) var(--sp-12); margin: 0; cursor: pointer;
  color: var(--text); box-shadow: 2px 2px 12px rgba(0,0,0,.35); font-size: var(--fs-2);
  transition: border-color .18s, background .18s; }
.etab .ico { font-size: var(--fs-3); line-height: 1; }
.etab .lab { max-width: 0; overflow: hidden; white-space: nowrap; opacity: 0;
  transition: max-width .22s ease, opacity .18s; }
.etab:focus-visible .lab { max-width: 160px; opacity: 1; }
/* the TOUCH label (variant B) — hidden everywhere except the touch block below, where it replaces
   `.lab`. Desktop never shows both; see edgeTab() in render.js for why there are two. */
.etab .labs { display: none; white-space: nowrap; }
.etab .cnt { font-variant-numeric: tabular-nums; color: var(--text-faint); font-size: var(--fs-1);
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--r); padding: var(--sp-2) var(--sp-8); }
.etab.skip.voted { border-color: var(--good); background: #1d3a25; }        /* green once you've voted (#6) */
.etab.skip.voted .cnt { color: #90ad90; }
.etab.ret.voted { border-color: #926667; background: #271d1d; }         /* reddish voted feedback */
.etab.ret.voted .cnt { color: #c99a9a; }
.etab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Hover ONLY where a pointer can really hover. On a touch screen :hover latches after a tap and
   stays until you tap somewhere else — so un-voting left the skip button still green, and the label
   stayed expanded, until you tapped elsewhere (Aur's iPad, 2026-07-27). The voted/unvoted colours
   below are class-driven and unaffected; this is purely the pointer-feedback layer. */
@media (hover: hover) {
  .etab:hover .lab { max-width: 160px; opacity: 1; }
  .etab.skip:hover { border-color: var(--good); background: #16241a; }
  .etab.ret:hover { border-color: #926667; background: #271d1d; }
}
/* touch devices have no hover → keep the tabs as compact icon (+count) nubs, NOT expanded labels:
   the labels sat at mid-screen and overlapped the guess dropdown on a phone. A tap still votes/opens
   (the whole nub is the button); the icon + count carry the meaning. */
@media (hover: none) {
  .etab { padding: var(--sp-12) var(--sp-12); }
  .etab .lab { max-width: 0; opacity: 0; }

  /* ON TOUCH, SKIP/RETURN LEAVE THE FIXED OVERLAY AND JOIN THE PAGE (2026-07-27, the end of a
     week-long series). A fixed mid-screen overlay cannot satisfy both of Aur's requirements at once:
     it must be pressable, and it must not cover the answer box ("player can't see their typed
     answers"). Every previous fix traded one for the other — hide it and you can't press it, show it
     and it covers the box, and iPadOS gives no visualViewport signal to move it by. In normal flow
     both are true unconditionally: the tabs occupy their own row ABOVE the guess box (that is their
     DOM order in render()), so they cover nothing, they scroll into view with the focused input, and
     they are always pressable. No focus events, no viewport maths, no device-specific behaviour.
     Desktop keeps the fixed edge tabs — there is room there, and hover reveals the labels. */
  .edge-left { position: static; transform: none; flex-direction: row; justify-content: center;
    margin: 12px 0 4px; }

  /* VARIANT B — ONE SEGMENTED CONTROL (Aur's pick, 2026-08-25, from /game-mockup.html?v=b). The two
     votes are one decision with two halves — end this round early, end this game early — so they read
     as one control rather than two loose chips floating above the answer box. Full width makes both
     halves thumb-sized. He rejected A (the same row as two separate labelled chips) as looking buggy:
     two differently-sized pills, centred, with a gap between them.
     ⚠ The strip owns the border; the segments must not, or the shared edge doubles. The voted colours
     (.etab.skip.voted etc.) still apply — on touch they read as a FILLED HALF rather than a coloured
     outline, which is the one thing this changes about the vote feedback. */
  .edge-left { gap: 0; border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; }
  .edge-left .etab { flex: 1 1 0; justify-content: center; border: 0; border-radius: var(--r);
    box-shadow: none; padding: var(--sp-12) var(--sp-12); }
  .edge-left .etab + .etab { border-left: 1px solid var(--border); }
  /* the words come back. They were hidden ONLY because the row was a fixed overlay covering the guess
     dropdown; in normal flow they cover nothing, and "Skip · 2/4" needs no learning where a bare ⏭
     nub does. `.lab` stays hidden — `.labs` carries the short form that fits a half-width segment. */
  .edge-left .etab .labs { display: inline; }
}

/* ---------- RIGHT-EDGE TABS (📜 history above ⚙ settings; mirror of the left vote tabs) ---------- */
.edge-right { position: fixed; right: 0; top: var(--edge-y, 50%); transform: translateY(-50%); z-index: 40;
  display: flex; flex-direction: column; align-items: flex-end; gap: var(--sp-12); transition: top .18s ease; }
/* mirror of .etab: flush to the RIGHT edge, rounded on the left, label expands leftward (row-reverse) */
.edge-right .etab { border-left: 1px solid var(--border); border-right: 0; border-radius: var(--r);
  padding: var(--sp-12) var(--sp-12) var(--sp-12) var(--sp-16); box-shadow: -2px 2px 12px rgba(0,0,0,.35); flex-direction: row-reverse; }
.edge-right .etab.open { border-color: #646cad; background: var(--bg); } /* accent when its panel is open */

/* MOBILE overlap: the keyboard used to cover these tabs, and the fix was to HIDE them while the guess
   box had focus (`#app.typing`, and `:focus-within` before that). Deleted 2026-07-27 — three separate
   bugs came out of that one idea, all of the same shape: a control that hides itself cannot be
   pressed, and every version found a new way to hide at the wrong moment (the tap-eating
   :focus-within, then auto-focus hiding them from the first frame of a round, then iPadOS's "hide
   keyboard" key not blurring, which stuck them off for good). They now MOVE instead — see --edge-y
   above and edgeCenterY in render.js. Nothing about them is focus-driven any more. */

/* ---------- SETTINGS DRAWER (spec A) ---------- */
/* right-side drawer over a dim (not blurred) backdrop, so the game stays legible while you flip a
   toggle mid-round. z-index above the edge tabs (40) and game chrome. */
.drawer-back { position: fixed; inset: 0; z-index: 60; background: rgba(6,6,12,.5); }
.drawer { position: fixed; top: 0; right: 0; height: 100%; width: min(370px, 90vw); z-index: 61;
  background: var(--surface); border-left: 1px solid var(--border); box-shadow: -18px 0 50px rgba(0,0,0,.45);
  padding: var(--sp-24) var(--sp-24); overflow-y: auto; box-sizing: border-box; }
/* animate ONLY on open (.opening), never on the re-renders a toggle triggers — else the panel
   re-slides in on every interaction ("recalls" itself) */
.drawer-back.opening { animation: drawer-fade .15s ease; }
.drawer.opening { animation: drawer-slide .2s ease; }
@keyframes drawer-fade { from { opacity: 0; } }
@keyframes drawer-slide { from { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) { .drawer.opening, .drawer-back.opening { animation: none; } }

.set-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 2px; }
.set-head h2 { margin: 0; font-size: var(--fs-3); letter-spacing: var(--track-caps); text-transform: uppercase; }
.set-head-actions { display: flex; align-items: center; gap: 4px; }
.set-x { background: none; border: 0; color: var(--text-muted); font-size: var(--fs-5); line-height: 1; cursor: pointer; padding: 0 4px; }
.set-x:hover { color: var(--text); }
/* jump to the sibling right-drawer (settings ↔ history) without closing first */
.set-swap { background: none; border: 1px solid var(--border); border-radius: var(--r); color: var(--text-muted);
  font-size: var(--fs-2); line-height: 1; cursor: pointer; padding: var(--sp-4) var(--sp-8); }
.set-sub { color: var(--text-faint); font-size: var(--fs-1); margin: 0 0 var(--sp-8); }
.set-group { font-size: var(--fs-1); letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--text-faint);
  margin: var(--sp-24) 0 var(--sp-2); display: flex; align-items: center; gap: 8px; }
.set-group::after { content: ''; flex: 1; height: 1px; background: var(--surface-raised); }
.set-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-16); padding: 12px 0; }
.set-row + .set-row { border-top: 1px solid var(--border-subtle); }
.set-row .lbl { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }
.set-row .lbl b { font-weight: 600; font-size: var(--fs-2); }
.set-row .lbl small { color: var(--text-muted); font-size: var(--fs-1); line-height: var(--lh-ui); }

/* pill toggle — green "on" matches RMQ's ready/skip-voted green.
   ⛔ THE KNOB AND THE TRACK MUST NEVER BE THE SAME COLOUR, and this is a fix, not a preference
   (Aur, 2026-09-05: "the button has no knob/indicator which side it's on"). It shipped correct in
   4d6ce82 — a light grey knob #8a8aa3 on a dark #2b2b3d track — and the 13-token palette sweep
   (f1409ca) mapped BOTH of them to var(--surface-raised). Two colours that were deliberately
   different became one token, so the knob was invisible whenever the switch was off: the only thing
   saying on-vs-off was the track going green, and a switch with no knob does not read as a switch.
   ⚠ THIS IS A DIFFERENT FAILURE FROM "a token solved for one role is wrong in another" — nothing
   here is wrong for its role. The sweep MERGED two roles that had to stay apart, and a merge is
   invisible in the diff (both lines just read `var(--surface-raised)`). --text-muted is the nearest
   token to the original #8a8aa3 and keeps the palette closed. test/switchknob.test.js pins it. */
.sw { width: 46px; height: 27px; border-radius: var(--r); background: var(--surface-raised); border: 1px solid var(--border);
  position: relative; cursor: pointer; flex: none; padding: 0; margin: 0; transition: background .18s, border-color .18s; }
.sw::after { content: ''; position: absolute; top: 2px; left: 2px; width: 21px; height: 21px;
  border-radius: var(--r); background: var(--text-muted); transition: transform .18s, background .18s; }
.sw.on { background: #1d3a25; border-color: var(--good); }
.sw.on::after { transform: translateX(19px); background: var(--good); }
.sw:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .sw, .sw::after { transition: none; } }

.vol { display: flex; align-items: center; gap: var(--sp-12); flex: 1; justify-content: flex-end; }
.vol input[type=range] { width: 130px; accent-color: var(--accent); }

/* the 📜 history drawer reuses the settings drawer shell but is WIDER — it holds a table you read.
   The table keeps its own scroll region (sticky headers), sized to fill the drawer under the header. */
/* ── stickers: the picker drawer, the carried sticker, and the green/red zones ─────────────────────
   The drawer reuses the settings shell, so a third right panel needed no new furniture.
   ⛔ NARROWER THAN THE HISTORY DRAWER, and that is his phone answer applied everywhere: "just make it
   have less column". Fewer columns was never about bigger buttons — it was so the panel stops
   covering the cards you are aiming at, which is the whole reason a picker on a phone was in doubt.
   The cell stays put and the count follows the width, so the panel is narrow on every screen. */
.stk-drawer { width: min(320px, 92vw); }
.stk-head { margin: var(--sp-12) 0 var(--sp-8); }
.stk-q { width: 100%; }
/* ⛔ THE TILE IS DOUBLED — his call 2026-09-07: "make the emoji x4 (x2 both w and h)". 44 -> 88
   is exactly twice in each direction, so the tile is four times the area.
   ⚠ THE GLYPH ITSELF SITS AT THE TOP OF THE TYPE SCALE (--fs-6, 31px) rather than at a literal 44px:
   the scale is a closed set of six steps and test/visualsystem.test.js refuses any font-size that is
   not one of them. Doubling the glyph exactly would mean ADDING a seventh step, which is a change to
   the design system rather than to this panel — flagged to him rather than done quietly. */
.stk-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
  gap: var(--sp-4); max-height: calc(100dvh - 220px); overflow-y: auto; }
/* ⛔ EVERY TILE IS THE SAME SQUARE, WHATEVER IS IN IT. His report 2026-09-07: the `+` tile "isn't the
   emoji square size yet — oh it's only when no emoji there yet", which is the whole diagnosis in his
   own words. The tile had no height of its own, so the grid ROW was sized by its tallest item: beside
   emoji at --fs-4 the `+` inherited a correct-looking row, and alone in the grid it set its own,
   smaller one at --fs-3. `aspect-ratio` makes the tile square against the column instead, so nothing
   depends on what happens to be next to it. */
.stk-item { font-size: var(--fs-6); line-height: var(--lh-ui); padding: var(--sp-4) 0; background: transparent;
  border: 1px solid transparent; border-radius: var(--r); cursor: pointer; color: var(--text);
  aspect-ratio: 1; display: flex; align-items: center; justify-content: center; }
.stk-item:hover:not(:disabled) { background: var(--surface-raised); }
.stk-item:disabled { opacity: 0.35; cursor: default; }
.stk-cust img { width: 52px; height: 52px; object-fit: cover; border-radius: var(--r); display: block; margin: 0 auto; }
.stk-empty { color: var(--text-muted); font-size: var(--fs-1); margin: var(--sp-8) 0; }
.stk-bal { color: var(--accent); }
/* the upload tile — dashed, so it reads as "add" rather than as a sticker you could throw */
.stk-up { border: 1px dashed var(--border); color: var(--text-muted); font-size: var(--fs-3); }
/* The lobby's manage tile. ⛔ NOT A BUTTON — nothing is picked up outside a round, so the tile itself
   does nothing and the only action is the ✕. `position: relative` is what the ✕ hangs off. */
.stk-manage { position: relative; cursor: default; }
.stk-del { position: absolute; top: 0; right: 0; margin: 0; padding: 0 var(--sp-4); line-height: var(--lh-ui);
  background: var(--surface-raised); color: var(--text-muted); border: 1px solid var(--border);
  border-radius: var(--r); cursor: pointer; font-size: var(--fs-1); }
.stk-del:hover { color: var(--bad); border-color: var(--bad); }
/* the balance pill on the tab, in the same slot the vote tabs put their count */
.etab.stk .cnt { color: var(--accent); }

/* the sticker riding the cursor. `position: fixed` on <body>, so a rerender mid-carry cannot take it
   away, and pointer-events: none so it never eats the click that drops it. */
#stk-carry { position: fixed; display: none; pointer-events: none; z-index: 1000;
  font-size: var(--fs-8); transform: translate(-50%, -50%);
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.6)); }
#stk-carry img { width: 54px; height: 54px; object-fit: contain; border-radius: var(--r); display: block; }
#stk-carry.no { opacity: 0.4; }

/* ⛔ HIS RULE MADE VISIBLE: green where it can land, red where it cannot. The alternative was the
   clamp doing it silently, which reads to the player as "I missed" — the outline teaches instead. */
/* ⛔ RED ONLY — his call 2026-09-07: "make it consistent … are we gonna make it no feedback(not
   showing green) or showing red when not placeable?" Green on every card was noise, because almost
   everywhere IS placeable; the only thing worth saying is where it will NOT go. So: no mark at rest,
   red over the answer box. */
body.carrying .pcard { cursor: none; }
body.carrying .pcard.nozone { outline: 2px solid var(--bad); outline-offset: 1px; }

/* landed stickers. The layer is absolute inside the card, which is why .pcard is position: relative —
   and it takes no pointer events, so a sticker can never block the card's own controls. */
.pcard { position: relative; }
/* ⛔ ABOVE EVERYTHING ON THE CARD, INCLUDING THE PICTURE. A sticker dropped on somebody's face
   vanished — the avatar is a positioned, overflow-hidden box, so it painted over the layer. He hit
   it immediately: "placing on pfp make it disappear (it went under it)". */
.pstk { position: absolute; inset: 0; pointer-events: none; overflow: hidden;
        border-radius: var(--r); z-index: 5; }
.pstk-one { position: absolute; transform: translate(-50%, -50%); font-size: var(--fs-5); line-height: 1; }
.pstk-one img { width: 28px; height: 28px; object-fit: cover; border-radius: var(--r); display: block; }

.history-drawer { width: min(560px, 96vw); }
/* the resize grip on the drawer's LEFT edge — its only free edge, since it is docked right at full
   height. ⚠ Wider than it looks: 4px of visible line inside a 10px hit area, because a 4px target is
   a miss on a trackpad. `touch-action: none` or a drag on a touchscreen scrolls the panel instead. */
.drawer-grip { position: absolute; left: 0; top: 0; width: 10px; height: 100%; cursor: ew-resize;
  touch-action: none; z-index: 2; }
.drawer-grip::after { content: ''; position: absolute; left: 3px; top: 0; width: 2px; height: 100%;
  background: transparent; transition: background .15s; }
.drawer-grip:hover::after, body.resizing-drawer .drawer-grip::after { background: var(--accent); }
/* while dragging, nothing else on the page should select or hover */
.history-drawer { position: fixed; }
.history-drawer .history { margin: 12px 0 0; border: 0; border-radius: var(--r); }
.history-drawer .history-scroll { max-height: calc(100dvh - 150px); }
#volume input { width: 110px; accent-color: var(--accent); }
/* ⛔ HIDDEN BY DEFAULT, SHOWN ONLY IN A ROOM. It used to be visible in the markup and hidden by a
   class that JS adds, so every refresh FLASHED it before the script ran — his report: "the volume
   button is still appears during refresh". A control that must not appear yet cannot start visible;
   the default has to be the safe state, and `body.inroom` is added by connect(). */
#volume { display: none; }
body.inroom #volume { display: flex; }
/* ⛔ AND NOT IN THE LOBBY — his call, carried by /lobby-design.html: nothing plays there, so the
   slider asks for a level you cannot hear. It belongs to the round, where sound exists.
   ⚠ `body.inroom` is the wrong axis for this on its own: you are in the room the whole time. The
   question is whether anything is AUDIBLE, and only the phase answers that. */
body.inroom:has(#app.lobby) #volume { display: none; }
/* ⛔ AND NOT ON THE END SCREEN — his spec for it is one sentence: "only the count down and no other
   button" (2026-09-06). The song is over, so there is nothing left to set a level for.
   ⚠ IT WAS ALSO A COLLISION, not only a rule: `#volume` is fixed at top-right and the count tab is
   in normal flow, so on a 391px phone the two overlapped by 85 x 22 px and the countdown read
   through the word "Mute" (measured 2026-09-09). `#app.lobby` is NOT set on this screen — the end
   screen is `#app.gameflow` with `.endview` inside it, which is why the lobby rule above misses it. */
body.inroom:has(#app.gameflow .gs.endview) #volume { display: none; }
/* the room-password gate as a full-screen takeover (index/list/library replaceChildren #app then
   mount the box) — a centered, readable card for PC; /add's inline re-auth box keeps the base look */
/* ⛔ SURFACE 0 of the visual-system conversion (2026-09-08) — the first screen anyone ever sees,
   and the cheapest possible proof the system works before a game screen depends on it.
   ⚠ FOUR ELEMENTS, and it stays four. "Plain" is fixed by the type system, not by adding to it. */
/* ═══ ENTRY SCREENS — the name box and the password gate share one shape ══════════════════════
   ⛔ THE NAME BOX IS THE ONE ALMOST EVERYONE SEES. A device that already has the room password
   skips the gate, so `.passbox` is a first-visit-only screen and `#app.entry` is the real front
   door. It carried NO class until 2026-09-08 and inherited generic styles.
   ⚠ His report on the old one: "the screen is way too small". #app is a 560px column, so a short
   form sat marooned in a corner of a 1600px window. The entry screens leave that column: they
   centre in the viewport and set their own width. */
#app.entry { max-width: none; width: 100%; box-sizing: border-box; min-height: 78vh; display: flex; flex-direction: column;
             justify-content: center; align-items: center; padding: 0 var(--sp-24); }
#app.entry > * { width: min(520px, 100%); }
#app.entry h1 { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-8);
                letter-spacing: var(--track-caps); text-transform: uppercase;
                margin: 0 0 var(--sp-24); line-height: var(--lh-solid); }
#app.entry .row { flex-wrap: nowrap; gap: var(--sp-8); align-items: stretch; }
/* a field is LIGHTER than its ground and carries no permanent outline — the edge appears on focus */
#app.entry input { flex: 1; min-width: 0; font-size: var(--fs-3); padding: var(--sp-12) var(--sp-16);
                   box-sizing: border-box; background: var(--l3); color: inherit;
                   border: 0; border-bottom: 2px solid var(--text-faint); border-radius: var(--r); }
/* the round's field: an underline, and the accent on focus (a box outline is a second edge) */
#app.entry input:focus, #app.entry input:focus-visible { outline: none; border-bottom-color: var(--accent); }
/* the one primary action on the screen, and the only accent fill on it */
#app.entry button { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-3);
                    letter-spacing: var(--track-caps); text-transform: uppercase; border-radius: var(--r);
                    border: 0; padding: var(--sp-12) var(--sp-24); margin: 0; display: flex; align-items: center; }
#app.entry .warn { font-size: var(--fs-2); margin-top: var(--sp-16); }
/* the escape hatch is a tertiary action: text, not a second button competing with Play */
#app.entry a { color: var(--text-muted); font-size: var(--fs-2); text-decoration: none; }
#app.entry a:hover { color: var(--text); text-decoration: underline; }
#app.entry > p { margin: var(--sp-24) 0 0; }
/* the entry is a fixed frame like every other page: no reserved scrollbar lane (it put the box 7.5px left of centre) */
html:has(#app.entry) { scrollbar-gutter: auto; overflow: clip; }
/* a message never pushes the screen: its line is always there (type-your-name entry, 2026-09-24) */
#app.entry .whomsg { min-height: 1.5em; }
/* the suggestions (his pick "3", 2026-09-24): a list that FLOATS under the name box — solid, never pushing the screen —
   holding only the players whose names match what is typed, then "New player" for a name nobody has */
#app .enbox { position: relative; flex: 1; min-width: 0; display: flex; }
#app .enbox > input { width: 100%; }
#app .ensug { position: absolute; top: 100%; left: 0; right: 0; z-index: 20; margin: var(--sp-4) 0 0; padding: 0; list-style: none;
  background: var(--l3); box-shadow: 0 8px 24px rgba(0, 0, 0, .45); }
#app .ensug:empty { display: none; }
#app .ensug li { display: flex; align-items: center; gap: var(--sp-12); height: 48px; padding: 0 var(--sp-12) 0 var(--sp-8); cursor: pointer; }
#app .ensug li + li { box-shadow: inset 0 1px 0 var(--rule); }
#app .ensug li.on { background: color-mix(in srgb, var(--l3) 80%, white 20%); }
#app .ensug .entile { width: 32px; height: 32px; flex: none; display: flex; align-items: center; justify-content: center; position: relative;
  overflow: hidden; font-family: var(--font-display); font-size: var(--fs-3); color: #fff; }
#app .ensug .entile img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
#app .ensug .enname { font-family: var(--font-names); font-weight: 500; font-size: var(--fs-3); min-width: 0; overflow-wrap: anywhere; }
#app .ensug .enlab { font-family: var(--font-display); font-size: var(--fs-2); letter-spacing: var(--track-wider); text-transform: uppercase;
  color: var(--text-faint); width: 96px; flex: none; padding-left: var(--sp-4); }

#app > .passbox { margin-top: 16vh; text-align: left; max-width: 520px; margin-left: auto;   /* the name box's width: one shape */
                  margin-right: auto; }
/* the heading is the DISPLAY face — this is the one line on the screen that is a title, not language */
#app > .passbox h1 { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-7);
                     letter-spacing: var(--track-caps); text-transform: uppercase;
                     margin: 0 0 var(--sp-24); line-height: var(--lh-solid); }
#app > .passbox .row { flex-wrap: nowrap; gap: var(--sp-8); align-items: stretch; }
/* ⛔ A FIELD IS LIGHTER THAN ITS GROUND AND CARRIES NO PERMANENT OUTLINE. On a dark ground "sunk"
   is the direction that makes a control vanish; the edge appears on focus, where it does a job. */
#app > .passbox input { flex: 1; font-size: var(--fs-3); padding: var(--sp-12) var(--sp-16);
                        box-sizing: border-box; background: var(--l3); color: inherit;
                        border: 0; border-bottom: 2px solid var(--text-faint); border-radius: var(--r); }
/* the round's FIELD (the port, 2026-09-23): an underline, the accent on focus — never a second box edge */
#app > .passbox input:focus, #app > .passbox input:focus-visible { outline: none; border-bottom-color: var(--accent); }
/* the one primary action on the screen, so it is the only accent fill on it */
#app > .passbox button { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-3);
                         letter-spacing: var(--track-caps); text-transform: uppercase;
                         padding: var(--sp-12) var(--sp-24); border-radius: var(--r); border: 0; margin: 0;
                         display: flex; align-items: center; }
#app > .passbox .warn { font-size: var(--fs-2); margin-top: var(--sp-16); }
.timerbar { height: 6px; background: var(--surface-raised); border-radius: var(--r); overflow: hidden; margin: 2px 0; }
/* #3 Extra Guess Time: the extra window's own bar (amber, audio off) + its label */
.timerbar.extra .fill { background: var(--warn); }
.timer-label { font-size: var(--fs-1); margin: var(--sp-4) 0 var(--sp-2); }
.timer-label.muted { color: var(--text-muted); }
.timer-label.extra { color: var(--warn); font-weight: 600; }
a { color: #95a0f2; }
/* ⛔ `min-width: 0` IS LOAD-BEARING, NOT TIDINESS. The UA stylesheet gives every <fieldset>
   `min-inline-size: min-content`, so a fieldset REFUSES to be narrower than its widest unbreakable
   row — it does not clip and it does not wrap, it pushes the whole page wider. Adding the
   Type · No. · Cut row (2026-09-04) did exactly that: `.add-song` measured 465px inside a 375px
   phone, the page scrolled sideways, and Chromium's mobile emulation zoomed the whole page out to
   fit. That is the friends' page, so it is the one width that must never do this. */
.addpage fieldset { border: 1px solid var(--border-subtle); border-radius: var(--r); margin: 0 0 var(--sp-16); padding: 12px; display: grid; gap: var(--sp-12); min-width: 0; }
/* the same reason one row down: a <select> is as wide as its longest option ("CUSTOM") unless told
   otherwise, and three of them side by side is what overflowed. /library's panel already carries
   this exact pair of rules for its own editor — see `.ldetail input, .ldetail select`. */
.addpage .lib-triple > label { min-width: 0; }
.addpage .lib-triple input, .addpage .lib-triple select { min-width: 0; width: 100%; box-sizing: border-box; }
.addpage legend { color: var(--text-muted); font-size: var(--fs-1); text-transform: uppercase; letter-spacing: var(--track-caps); padding: 0 var(--sp-8); }
.addpage label { display: flex; flex-direction: column; gap: 4px; font-size: var(--fs-2); color: var(--text-muted); }
.addpage label.radio { flex-direction: row; align-items: center; gap: 8px; color: var(--text); }
/* ⚠ FLEX BASIS FOLLOWS THE CONTAINER'S DIRECTION, and on /add the container is a COLUMN.
   `.artist-box` is `flex: 1 1 200px` because on /library it sits in `.credit-row`, a flex ROW, where
   200px is the WIDTH it was written to be. `.addpage label` is `flex-direction: column`, so the same
   basis became a 200px HEIGHT — reserved twice (Credit, and Franchise tags) for content that needs
   36px. Measured 2026-08-20 at 375px: 328px of the page's 1802px was this, and /add is the page
   friends use from their phones. The trap is already written down in CLAUDE.md; this is it happening.
   Scoped to .addpage so /library's row keeps the basis it actually wants. */
.addpage .artist-box { flex: 0 0 auto; }
.addpage input, .addpage select { background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: var(--r); padding: 8px; font-size: var(--fs-2); }
.addpage .hint { color: var(--text-muted); font-size: var(--fs-2); }
.addpage #status.ok { color: var(--good); } .addpage #status.err { color: var(--bad); }
/* the picked-anime confirmation is a SUCCESS line, not a hint — it carries the mal_id the whole
   feature exists to get right, so it reads green like every other confirmation. Needs the id to
   outrank `.addpage .hint` (0,2,0), which a bare `.ok` would lose to. */
.addpage #anime-picked { color: var(--good); }
/* #11: the YouTube preview card — thumbnail beside the title, so you confirm the right video */
.yt-preview { display: flex; gap: var(--sp-12); align-items: center; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); padding: 8px; }
.yt-preview img { width: 120px; max-width: 40%; border-radius: var(--r); flex: none; }
.yt-preview-body { min-width: 0; }
.yt-preview-title { margin: 0 0 4px; color: var(--text); font-size: var(--fs-2); word-break: break-word; }
.yt-preview .hint { margin: 0; }
/* The preview card left the Audio-source fieldset when /add gained its wide arrangement (it has to
   be a direct child of the form to be placed by the grid). On a phone it must still read as part of
   that fieldset rather than as a third box, so it is pulled up against it. */
#addform > .yt-preview { margin: -8px 0 var(--sp-16); }
/* ⛔ ON A PHONE AN EMPTY BOX IS JUST A WASTED ROW, so the resting state is not drawn there. The wide
   arrangement re-shows it (below), because there the whole point is that the top-right cell is
   already built and waiting rather than appearing under your hands. */
#addform > .yt-preview.empty { display: none; }

/* ⛔ /add IS MOBILE ON PURPOSE and this does not change that. Two friends are phone-only and the
   whole point of the page is adding from your own device — the single column is the BASE and this is
   an ARRANGEMENT laid over it on a wide screen, never a replacement (his sketch, 2026-08-28,
   confirmed 2026-08-30: "that should be for the pc resolution. not a phone one").
   His layout, in his words: "link on top left, preview on top right, anime on bottom left, song
   detail on bottom right … and download in that area" — so the song block, the button and the status
   line share the right column, under the preview they line up with.
   ⚠ 900px, not the ledger's 1101px: two ~480px columns is what this form needs, and his iPad in
   landscape (1194) should get the arrangement while the same iPad in portrait (834) keeps the phone
   column. The two pages answer different questions, so they do not share a breakpoint. */
@media (min-width: 900px) {
  /* ⛔ THE SAME WINDOW AS /list AND /library (Aur, 2026-09-04: "it's the same size on list and
     library now. but not /add yet"). It was capped at 1000px, so its nav row stopped short of the
     window edge while the other two ran the full width — the exact inconsistency he had just had
     fixed on /list. `box-sizing` rides along for the same reason it does there: without it the
     content box starts 16px further left and the three pages' left edges disagree as you switch. */
  #app.addflow { max-width: min(1720px, calc(100vw - 32px)); box-sizing: border-box; }
  #app.addflow #addform {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas:
      "source preview"
      "anime  song"
      "anime  tags"
      "anime  go"
      "anime  status";
    /* the anime block spans three rows and is TALLER than song+button+status, and a spanned track
       shares its slack out among the rows it covers — which floated the button ~80px below the song
       details it belongs to. Sending the slack to the last row instead keeps the button under the
       block it goes with. */
    grid-template-rows: auto auto auto auto 1fr;
    column-gap: var(--sp-16);
    align-items: start;
  }
  #app.addflow #addform > .source { grid-area: source; }
  /* ⛔ THE BOX MATCHES THE UPLOAD BOX BESIDE IT (Aur: "make the thumbnail/title of yt video fits the
     box with same size as the the upload box"). `align-self: stretch` is what makes that true by
     construction — the row's height is the Audio-source fieldset's, and the preview simply takes it —
     rather than by a hard-coded height that would be wrong the next time that fieldset gains a line. */
  #app.addflow #addform > .yt-preview { grid-area: preview; margin: 0 0 var(--sp-16); align-self: stretch; }
  #app.addflow #addform > .yt-preview.empty { display: flex; justify-content: center; text-align: center; }
  /* the thumbnail fills the box's height instead of sitting at a fixed 120px in the middle of it.
     `object-fit: cover` keeps 16:9 from distorting while it does. */
  #app.addflow #addform > .yt-preview img { align-self: stretch; width: 42%; max-width: 220px; height: auto; object-fit: cover; }
  #app.addflow #addform > .add-tags { grid-area: tags; }
  /* ⚠ `add-song` / `add-anime`, not `song` / `anime`: `.song` is a LIVE class on /list's rows
     (font-size, padding, flex), and hanging it on a fieldset here would have dressed the whole
     block in another page's row styling. Grep the name before reusing one. */
  #app.addflow #addform > .add-song { grid-area: song; }
  #app.addflow #addform > .add-anime { grid-area: anime; }
  #app.addflow #addform > #go { grid-area: go; justify-self: start; }
  #app.addflow #addform > #status { grid-area: status; }
}
.timerbar .fill { height: 100%; background: var(--accent); width: 100%; }
.row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 8px 0; }
/* ⚠ THE LOBBY'S THREE BUTTONS MAY NOT WRAP (Aur, 2026-08-29: "the three button on mobile is bad. it
   get pushed to 2 on one row and 1 below"). Measured at 375px: Ready 107 + Spectate 105 + Change name
   144, plus `button.minor`'s 8px left margins, is 372px against 343px of usable width — so the last
   one dropped to a line of its own at 375 AND 390, i.e. on both of his phones.
   Three things buy the room back, in order of how much they buy: the margins go (the flex gap owns
   the spacing now, and those margins were also what put Ready and Spectate on two different baselines
   — 208 against 213), the padding is trimmed for this row only, and `nowrap` makes the break
   impossible rather than unlikely. Ellipsis is the last resort, never reached at his widths — but a
   longer label in some future state truncates instead of silently re-breaking the row.
   ⛔ NOT A REDESIGN. Real buttons or a menu, and dropping the emoji, are his (use-pass report §3) and
   are deferred; this only makes the three fit. */
.lobby-acts { flex-wrap: nowrap; gap: 4px; align-items: stretch; }
/* `List: …` — the lobby's list switcher (2026-08-29; the caption was `Playing from` until
   2026-08-30). It sits above the seat board and belongs
   to no seat, which is his call: no list name appears on any seat, only inside this control. */
.lobby-lists { gap: var(--sp-8); align-items: center; margin: 8px 0 4px; }
/* ⛔ THE CONTROL DOES NOT RESIZE ITSELF (Aur, 2026-08-29, with three screenshots: "currently the
   dropdown resize based on the current list name length … and idk when i try adding longer name and
   retry it now extends").
   It is the same rule this whole batch is built on — content that changes size must not move what is
   around it — one level down: the CAPTION is content, so a button sized to it jumps every time you
   switch, and the panel underneath jumped with it. A fixed width for the button and a fixed width
   for the panel; a long name truncates in the button (the panel spells it out in full) and wraps in
   the panel rather than stretching it.
   ⚠ 13ch, not px: it has to hold a list name at whatever font the device picks, and `My list` /
   `Second list` are the shapes it is sized for. */
/* ⛔ THE CARET HOLDS THE RIGHT EDGE (Aur, 2026-08-29: "isn't the triangle stick on the right and not
   move with text?"). The fixed 13ch above stops the BUTTON moving; this stops the arrow moving inside
   it. The truncation had to move with it: `text-overflow` clips the end of the box's own text, so
   while the caret was part of that text a long name ellipsised the arrow away rather than the name.
   ⚠ `.pick-cap` / `.pick-caret` exist on EVERY toggle in the app now (checkfilter.js), but only this
   one is a flex box — everywhere else the two spans flow inline exactly as the old single text node
   did, which is why the Columns menu and the /library pager needed no change and show none. */
.lobby-lists .checkfilter-toggle { font-size: var(--fs-2); width: 13ch; text-align: left;
  display: flex; align-items: center; gap: 4px; white-space: nowrap; }
.lobby-lists .checkfilter-toggle .pick-cap { flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; }
.lobby-lists .checkfilter-toggle .pick-caret { flex: 0 0 auto; }
.lobby-lists .pickmenu-panel { width: 15ch; min-width: 15ch; }
.lobby-lists .pickmenu-row { white-space: normal; overflow-wrap: anywhere; }

/* ⚠ ONE FONT SIZE FOR THE THREE, and it is not decoration: Ready is `button.primary` at 1rem while
   the other two are `button.minor` at 0.9rem, so the widest button was also the one carrying the
   biggest text. Levelling it is what turns "just fits at 375" into ~16px of slack — measured 359px
   of buttons in 343px of usable width before, 327px after — and his own note that "the buttons are
   not the same size" points the same way. Ready keeps its accent fill, which is what marks it. */
.lobby-acts button { margin: 0; padding-left: var(--sp-12); padding-right: var(--sp-12); font-size: var(--fs-2);
  min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ⛔ THE SLACK THIS ROW HAD WAS 16px, AND MITR ATE ALL OF IT (2026-08-30). The levelling above got
   the three buttons to 327px inside 343px of usable width at 375 — and the site font then drew the
   same three labels ~10% wider (measured at 0.9rem: Ready 62→69, Spectate 73→84, Change name
   112→122), which is 348px in 343px. G18 still passed, because `nowrap` holds the row together by
   CLIPPING — so all three came out as "✏️ Change nam…". G18b is the half that saw it.
   ⚠ THE PADDING GIVES WAY, NOT THE TYPE. 0.9rem is already the smaller of the two sizes this row
   used to mix, and shrinking text to fit a font change is how a phone label becomes unreadable; 6px
   of padding buys 24px, which puts the slack back at ~19px — where it was before the font moved.
   ⚠ 430px, not 375px: it has to cover both of his phones (375 and 390) with room to spare rather
   than sit one pixel from the edge of the breakpoint it was derived at.
   ⛔ AND IT GAVE WAY A SECOND TIME, on 2026-09-04: the geometry sweep snapped this 6px up to 8px
   (nearest step, ties up) while the type scale moved 0.9rem to 0.9375rem, so the row clipped again.
   It is now the grid's 4px step — the smallest legal value, which is the right answer for the one
   place in the app where the padding is what has to yield. */
@media (max-width: 430px) {
  .lobby-acts button { padding-left: var(--sp-4); padding-right: var(--sp-4); }
}
/* min-width:0 for the same reason .listpick needs it, one line down: a flex item will not shrink
   below its intrinsic width, and an <input>'s is ~20 characters. Without it the rename/create row
   on a phone pushed Cancel onto a second line while Save stayed up top (Aur's use-pass). */
.row input { flex: 1; min-width: 0; }
/* ADR-0011 slice 3: the list switcher is the row's main control, so it takes the free space the way
   a text input does. min-width:0 is what stops a long list name from pushing ✏️ ＋ ✕ off a phone —
   a flex item refuses to shrink below its content width without it. */
.listpick { flex: 1; min-width: 0; font-size: var(--fs-2); }
.listpick[disabled], .row button[disabled] { opacity: 0.4; cursor: not-allowed; }
/* a checkbox must NOT stretch like a text input (flex:1 pushed it far from its label, making the
   whole row read as clickable). Keep it compact next to its label, and give it a clear accent so its
   CHECKED state looks intentional, not like a stray focus/hover ring. */
.row input[type="checkbox"] { flex: none; width: 16px; height: 16px; accent-color: var(--accent-check); cursor: pointer; }
.muted { color: var(--text-muted); font-size: var(--fs-2); }
.entries details { border-bottom: 1px solid var(--border-subtle); padding: 4px 0; }
.entries summary { display: flex; justify-content: space-between; gap: 8px; cursor: pointer; }
.song { display: flex; justify-content: space-between; gap: 8px; align-items: center; padding: var(--sp-4) 0 var(--sp-4) var(--sp-16); font-size: var(--fs-2); }
.song.off { opacity: 0.45; }
.tag { font-size: var(--fs-1); text-transform: uppercase; letter-spacing: 0.06em; color: var(--special); border: 1px solid #79689c; border-radius: var(--r); padding: var(--sp-2) var(--sp-8); margin-left: 8px; align-self: center; }

/* ── ADR-0011 slice 4: ONE row shape for every line of /list ──────────────────────────────────
   An anime on your list, a song inside it and a search hit are the same row: a 22px tick (the old
   16px boxes were too small for a thumb — measured on a 390px screen with Aur), an optional caret,
   a two-line body, and a count. `.song`/`.entries details` above belong to /library and the viewer;
   these classes are deliberately separate names so the two pages cannot drift into each other. */
.pick { width: 22px; height: 22px; accent-color: var(--accent-check); flex: none; cursor: pointer; margin: 0; }
.srow { display: flex; gap: var(--sp-12); align-items: center; padding: var(--sp-8) 0; border-bottom: 1px solid var(--border-subtle); }
.srow .body { min-width: 0; flex: 1; }
.srow .t { display: block; color: var(--text); font-size: var(--fs-2); }
.srow .sub { display: block; color: var(--text-muted); font-size: var(--fs-1); }
.srow .num { flex: none; color: var(--text-muted); font-size: var(--fs-1); width: 34px; }
.srow .count { flex: none; color: var(--text-muted); font-size: var(--fs-1); }
.srow.anime .t { font-size: var(--fs-2); }
.srow.anime { cursor: pointer; }
.srow.song { padding-left: var(--sp-24); border-bottom: none; }   /* nested under its anime */
.srow.off .t, .srow.off .sub, .srow.off .num { opacity: 0.45; } /* turned off: still listed, greyed */
/* ADR-0011 slice 5: the sample button, on the far edge of a song row. 34px square because /list is a
   phone page and 22px was already judged too small for a thumb on the tick beside it — this one is a
   real target too. It is deliberately quiet at rest (a muted glyph on the row's own ground): a page of
   sixty rows must not read as a page of sixty buttons, and only the one that is SOUNDING earns the
   accent. `flex: none` keeps it off the title's width — .body is the only track that flexes. */
.sbtn {
  flex: none; width: 34px; height: 34px; padding: 0;
  background: none; border: 1px solid var(--border-subtle); border-radius: var(--r);
  color: var(--text-muted); font-size: var(--fs-1); line-height: var(--lh-solid); cursor: pointer;
}
.sbtn:hover { color: var(--text); border-color: var(--border); }
.sbtn.on { color: var(--accent); border-color: var(--accent); }
.srow.off .sbtn { opacity: 0.45; } /* a song turned off is still playable, and still greyed with its row */

/* The scrub bar, on the row's own bottom edge and OUT OF THE LAYOUT (his idea, 2026-09-05).
   ⛔ It must never change the row's height: a bar that takes part in layout shoves the rest of a
   60-row list down on every play and back up on every stop, which is worse than having no bar. Hence
   absolute + a `position: relative` row, and hence `bottom: -1px` — it sits ON the row's border rather
   than above it, so the border becomes the bar's own unfilled track and nothing moves by even a pixel.
   The 4px stripe is the visible part; `padding` gives the finger a 14px target without the stripe
   growing (a 4px drag target is unusable on a phone, and this page is a phone page). */
.srow { position: relative; }
.sbar { position: absolute; left: 0; right: 0; bottom: 0; height: 12px;
  cursor: pointer; touch-action: none; z-index: 1; }   /* the hand, like the volume slider — not a resize arrow (his, 2026-09-24) */
.sbar::before { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 4px; background: var(--border-subtle); }
.sbar-fill { position: absolute; left: 0; bottom: 0; height: 4px; width: 0; background: var(--accent); }
/* current / total, beside the button (his idea, 2026-09-05). `flex: none` and a tabular figure width
   so the digits changing every second cannot nudge the button sideways — a readout that jitters is
   worse than none, and this one updates four times a second. */
.stime { flex: none; color: var(--text-muted); font-size: var(--fs-1); font-variant-numeric: tabular-nums; }

/* the sample volume, in the control strip (his ask, 2026-09-05). `margin-left: auto` puts it on the
   far right of the row it shares with the kind buttons, so it never competes with them for the left
   edge; it wraps below on a narrow phone rather than squeezing the three buttons that already fought
   for that row's width once. */
/* the ▶ beside the panel's anime heading (his call, 2026-09-05: "in the editor we still have the
   space beside the anime name"). It is NOT on the ledger row on purpose — those columns are
   draggable, so a row button has no stable position to reach for. */
/* ⛔ THE ROW IS THE SCRUB BAR'S POSITIONING CONTEXT (2026-09-05). `.sbar` is `position: absolute`
   with `bottom: 0`, so without `position: relative` here it would hang off `.ldetail` instead and sit
   at the bottom of the whole panel — under the delete button, nowhere near the ▶ it belongs to.
   ⚠ The padding is what the bar occupies. It is reserved ALWAYS, not only while sounding, because
   the bar takes part in no layout: reserve it once and the panel is the same height whether a sample
   is playing or not, which is the same rule /list's rows keep for the same reason. */
.panel-anime-row { display: flex; align-items: center; gap: var(--sp-8); position: relative; padding-bottom: 8px; }
.panel-anime-row .panel-anime { margin: 0; min-width: 0; }
.psample { flex: none; display: inline-flex; align-items: center; gap: var(--sp-4);
  background: none; border: 1px solid var(--border-subtle); border-radius: var(--r);
  color: var(--text-muted); font: inherit; font-size: var(--fs-1); line-height: var(--lh-solid);
  padding: var(--sp-4) var(--sp-8); cursor: pointer; }
.psample:hover { color: var(--text); border-color: var(--border); }
.psample.on { color: var(--accent); border-color: var(--accent); }
.psample-time { font-variant-numeric: tabular-nums; color: var(--text-muted); }

.svol { display: inline-flex; align-items: center; gap: var(--sp-8); margin-left: auto; }
.svol-label { color: var(--text-muted); font-size: var(--fs-1); }
.svol-range { width: 104px; accent-color: var(--accent-check); }
@media (max-width: 430px) { .svol { margin-left: 0; width: 100%; } .svol-range { flex: 1; width: auto; } }
.caret { flex: none; color: var(--text-muted); width: 14px; text-align: center; }
.kids.shut { display: none; }
.kids .kidnote { padding-left: var(--sp-24); font-size: var(--fs-2); }
.sect { font-size: var(--fs-1); text-transform: uppercase; letter-spacing: var(--track-caps); color: var(--text-muted); margin: var(--sp-16) 0 var(--sp-4); }
 /* a heading is a small target — give it a row's worth of height */
.more { font-size: var(--fs-1); padding: 4px 0; }
 /* it loads more now, so it has to look tappable and be thumb-sized */
button.seg { padding: 4px 12px; font-size: var(--fs-2); background: var(--surface); border: 1px solid var(--border); }
button.seg.on { background: var(--accent-fill); color: var(--text); border-color: var(--accent-fill); }
.ok { color: var(--good); font-size: var(--fs-2); }

/* #7 song-history panel */
.history { margin: 8px 0 4px; border: 1px solid var(--border-subtle); border-radius: var(--r); overflow: hidden; }
.history-tabs { display: flex; gap: 0; background: var(--surface); }
button.htab { flex: 1; margin: 0; border: 0; border-bottom: 2px solid transparent; border-radius: var(--r); background: none; padding: var(--sp-8) var(--sp-8); font-size: var(--fs-2); color: var(--text-muted); }
button.htab.active { color: var(--text); border-bottom-color: var(--accent); background: var(--surface); }
.history > .muted { margin: 0; padding: var(--sp-16); }
/* scroll the 25-row list inside its own region instead of growing the page; header stays visible */
.history-scroll { max-height: 55vh; overflow-y: auto; }
/* #4 Past Games: a collapsible per-game header inside the Past tab */
.history-table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: var(--fs-2); }
.history-table th { position: sticky; top: 0; background: var(--surface); color: var(--text-muted); font-size: var(--fs-1); text-transform: uppercase; letter-spacing: 0.06em; text-align: left; padding: var(--sp-8) var(--sp-8); font-weight: 600; }
/* one-line rows: every cell truncates with … ; the full text lives in the tap-to-expand panel */
.history-table td { padding: var(--sp-8) var(--sp-8); border-top: 1px solid var(--border-subtle); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.h-num { width: 2.2em; color: var(--text-muted); }
.h-type { width: 3.2em; color: var(--special); }
.h-guesses { width: 3em; text-align: right; color: var(--text-muted); }
.h-row { cursor: pointer; }
.h-row:hover td { filter: brightness(1.25); }
.h-info td { padding: 0; background: var(--bg); border-top: 0; white-space: normal; overflow: visible; }
.song-info { padding: var(--sp-12) var(--sp-12); }
.song-info p { margin: var(--sp-4) 0; }
.h-guess-list { list-style: none; margin: 8px 0 0; padding: 0; font-size: var(--fs-2); }
.h-guess-list li { padding: 2px 0; }
/* S/A rows name the two halves separately (2026-08-17) — the song and the artist score
   independently, so one joined string could not say which half was missed. The halves indent under
   the name rather than sitting on one line, so a miss is findable by scanning down the dashes.
   ⚠ NOT scoped under .h-guess-list: the REVEAL renders the same row shape through the same helper
   (render.js saGuessRow) and its <ul> carries no class, so a scoped rule styled the history panel
   and silently left the reveal as plain text. */
.h-guess-who { display: block; }
.h-guess-half { display: block; padding-left: var(--sp-16); color: var(--text-muted); font-size: var(--fs-2); }
/* a half that LANDED is green, the same #7CE38B every other "you got it" on the page uses. It was a
   desaturated #b9d0b9, which read as "noted" rather than "correct" next to the green name line —
   Aur, 2026-08-17: "the song/artist should shows green too when it's correct". */
.h-guess-half.got { color: var(--good); }
/* Names you typed correctly that did not complete the credit (Aur, 2026-09-05). It reuses .partial's
   amber deliberately: that colour already means "some of this counted", and this half-line is the
   place that is actually true of. Green = the point was earned, amber = earned something, muted = the
   half is empty — three states where there used to be two, and a half-right artist had no colour. */
.h-guess-half.part { color: #c9a043; }

/* A band's members, reachable from the game (a friend's request, 2026-09-05).
   ⛔ IT IS A BUTTON, NOT A HOVER. AMQ is desktop; the RMQ game is mobile on purpose and a phone has no
   hover at all, so tap/click/keyboard is the real control and the hover rule below is a desktop extra
   guarded by `(hover: hover)` — the same guard `.edge-left` uses for the touch Skip/Return row. Ship
   it hover-only and it works for him and is dead for the friends who asked. */
/* one line per credit (Aur, 2026-09-05: "maybe separate one line per artist"). A joined
   `artist_display` run cannot be tapped per-name, so the structured credits are the display. */
.artlines { display: inline-block; vertical-align: top; }
/* the game's song box draws the credit as ONE RUN — 306px for 36 names against 1,225 as lines */
#app.gameflow .artrun { display: inline; }
#app.gameflow .artname, #app.gameflow .artsep { display: inline; }
/* Extra Info's artists: one name per row, and the row with members is the tappable one */
#app.gameflow .ar-list { display: flex; flex-direction: column; gap: 2px; margin-top: var(--sp-4);
                         flex: 1 1 auto; min-height: 0; overflow-y: auto; }
#app.gameflow .ar-row { display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-4) var(--sp-8);
                        border-radius: var(--r); font-size: var(--fs-2); text-align: left;
                        white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#app.gameflow .ar-row.memhost { cursor: pointer; }
#app.gameflow .ar-row:hover { background: var(--surface-raised); }
.artline { display: block; }
.memhost { position: relative; display: inline-block; }
/* ⛔ THE AFFORDANCE IS THE POINT, not decoration ("have them underlined/highlighted to kinda tell that
   it could be tapped/hovered"). Without it the credits you CAN open look identical to the ones you
   cannot, and you learn the control by failing at it — which is how AMQ's blue names read as links.
   `--accent` is the token solved to be READ as text (visual-system §16.2), so this is its own role. */
.membtn { background: none; border: 0; padding: 0; margin: 0; font: inherit; color: var(--accent);
  cursor: pointer; border-bottom: 1px dashed var(--accent); }
.membtn:hover { border-bottom-style: solid; }
.memcaret { font-size: var(--fs-1); margin-left: var(--sp-4); color: var(--text-muted); }
.mempop { display: none; position: absolute; left: 0; top: calc(100% + 6px); z-index: 20;
  min-width: 160px; max-width: min(320px, 80vw); padding: var(--sp-8) var(--sp-12);
  background: var(--surface-raised); border: 1px solid var(--border); border-radius: var(--r);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45); white-space: normal; text-align: left; }
.memhost.open .mempop { display: block; }
@media (hover: hover) { .memhost:hover .mempop, .membtn:focus-visible + .mempop { display: block; } }
.memname { display: block; color: var(--text-muted); font-size: var(--fs-1); }
/* ⛔ STACKED, ONE NAME PER ROW (Aur, 2026-09-05: "stacking to the right will make it harder to read").
   A line-up is a SET of proper nouns; wrapped across a line the eye scans sideways past ragged word
   boundaries, stacked it goes straight down. It was a comma-joined run before.
   ⚠ `.memchip` is deliberately SHARED with /library's read-only member list — his point, and it is
   the same content in both places: "it's still the same as hovering on artist on result page".
   ⚠ NOT a pill. He chose the chip and then trimmed it: "maybe not pills like that, or maybe less
   round on the edges." --r-4, so it reads as a tile rather than a tag. */
.memlist { display: flex; flex-direction: column; gap: var(--sp-4); margin-top: var(--sp-4); }
.memchip { color: var(--text); font-size: var(--fs-2); background: var(--surface);
  border: 1px solid var(--border-subtle); border-radius: var(--r); padding: var(--sp-2) var(--sp-8); }
/* .h-guess-typed is gone: what they typed moved onto the name line (2026-08-17), where it reads as
   one sentence — who, what they said, whether it counted. */

/* task 4: an AniList add-search hit not yet in the library — a "＋ new / ＋ year" tag on the right */
.dropdown li.add-remote { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.dropdown li.add-tagged { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.add-rowtags { flex: none; font-size: var(--fs-1); color: var(--text-muted); max-width: 45%; text-align: right; }
.add-tag { font-size: var(--fs-1); color: var(--good); border: 1px solid #57795d; border-radius: var(--r); padding: var(--sp-2) var(--sp-8); flex: none; }

/* task 5: the /library page — read-only view (sub-step 1) */
.library details { margin: 0; }
.library summary { justify-content: space-between; }
.song .tags { display: flex; gap: var(--sp-8); align-items: center; flex: none; }
.song.off { opacity: 0.5; }

/* task 5 sub-step 2: admin inline edit + sticky Save bar */
button.lib-edit-btn { margin: 0 0 0 8px; padding: 2px 8px; font-size: var(--fs-1); line-height: var(--lh-ui); }
.lib-row { display: flex; justify-content: space-between; align-items: center; padding: var(--sp-4) 0 var(--sp-4) var(--sp-16); font-size: var(--fs-2); }
.lib-edit { display: flex; flex-wrap: wrap; gap: 8px 12px; padding: var(--sp-8) var(--sp-8) var(--sp-12) var(--sp-16); margin: var(--sp-2) 0 var(--sp-8); background: var(--surface); border-left: 2px solid var(--accent); border-radius: var(--r); }
.lib-edit.dirty { border-left-color: var(--warn); }
/* yellow bar on the anime line when it (or any song inside it) has an unsaved edit — visible even
   when the anime is collapsed. inset shadow, so no layout shift. */
.library details.dirty > summary { box-shadow: inset 3px 0 0 var(--warn); padding-left: 8px; }
.lib-edit label { display: flex; flex-direction: column; gap: 2px; font-size: var(--fs-1); color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--track-caps); }
.lib-edit input, .lib-edit select { font-size: var(--fs-2); padding: var(--sp-4) var(--sp-8); background: var(--surface); color: inherit; border: 1px solid var(--border); border-radius: var(--r); }
.lib-edit input[type=text] { min-width: 180px; }
.lib-edit input[type=number] { width: 72px; }
/* slice 3: the Credit box is GENERATED from the credit rows below it, so it reads as output rather
   than as a field you failed to fill. Specificity note — this is (0,2,0), the same as
   `.lib-edit input`, so it must come AFTER it to win; `.lib-edit .hint`-style surprises are on
   record in this file. It stays a real input (readOnly, not disabled) so form reads still see it. */
.lib-edit input.generated { background: var(--surface); color: var(--text-muted); border-style: dashed; cursor: default; }
.lib-edit input.generated:focus { outline: none; border-color: var(--border); }

/* ── THE THREE STATES OF A FIELD, AS THREE MEASURED STEPS (Aur, 2026-09-05) ──────────────────────
   His report, with a screenshot: "these can barly notice the difference between the typable and the
   hint", and "i think white texts aren't that bad".
   ⛔ MEASURED, NOT EYEBALLED, because the cause was not the one the report names. Every field in this
   panel was drawing at `--text-muted` — `.lib-edit input { color: inherit }` inherits it — so:
       a value you TYPED        #9d9ca5   5.43:1 on the field
       a GENERATED, uneditable  #9d9ca5   5.43:1  ← the same colour, separated only by a 1px dash
       a PLACEHOLDER            #757575   3.2:1   ← Chrome's UA default, not a token at all
   So it was not "the hint looks like a value"; it was that the panel had ONE colour for two states
   and let the browser pick the third. The ladder is now:
       value      var(--text)         10.41:1   the data is the brightest thing on the panel
       read-only  var(--text-muted)    5.43:1   where a value used to sit
       hint       var(--text-faint)    4.53:1   a token, and still over the 4.5 floor
   ⚠ NO NEW COLOURS. Three tokens he already settled; the palette stays closed. What changed is
   WHICH state gets which — the fix was an assignment, not a hue.
   ⚠ `opacity: 1` on the placeholder is not decoration: Firefox applies its own default opacity to
   ::placeholder, so without it the measured colour is not the drawn one.
   ⛔ CUTTING THE READ-ONLY CREDIT TO PLAIN TEXT IS **NOT** PART OF THIS, and that is his call: he
   meant the credit-line joins (`,` `feat.` `×`) by "we still need the linking word to make the
   uneditable credit box makes sense", so plain text waits on that deferred design pass. */
.lib-edit input, .lib-edit select, .lib-edit textarea,
.ldetail .credit-row input[type=text], .ldetail .credit-members input,
.ldetail .lineup-col input, .ldetail .lineup-col select { color: var(--text); }
.lib-edit ::placeholder, .ldetail ::placeholder { color: var(--text-faint); opacity: 1; }
/* stated again after the ladder so the read-only state cannot be swept into it by a later edit —
   this is the pair the whole rule exists to keep APART, and a merge is invisible in a diff (the
   ⚙ switch knob, 2026-09-05, cost exactly that). */
.lib-edit input.generated { color: var(--text-muted); }
/* the offer sits under the typed line it would split, and only on a song with no credit rows */
.lib-edit .credit-offer { margin-top: var(--sp-4); padding: var(--sp-2) var(--sp-8); font-size: var(--fs-1); text-transform: none; letter-spacing: 0; }
/* the anime's two SOURCE titles, shown read-only above the fields (D2: they are copied onto every
   song row in the manifest, so editing one is a bulk rewrite the next download would undo anyway).
   flex-basis 100% so it owns the first line of the form. */
/* ⛔ `.fixed-names` (the read-only "romaji X · english Y" line) was DELETED from the anime panel on
   2026-08-28 — his call, "we can't edit those right?". The rules go with it rather than lingering as
   dead selectors; git has them if the line ever comes back. */
.lib-edit textarea { font: inherit; font-size: var(--fs-2); min-width: 260px; padding: var(--sp-4) var(--sp-8); background: var(--surface); color: inherit; border: 1px solid var(--border); border-radius: var(--r); resize: vertical; }
.lib-edit textarea.field-dirty { border-color: var(--warn); box-shadow: 0 0 0 2px rgba(255, 180, 84, 0.35); }
button.lib-del-btn { margin: 0 0 0 var(--sp-8); padding: var(--sp-2) var(--sp-8); font-size: var(--fs-1); line-height: var(--lh-ui); border-color: #8b6868; }
button.lib-del-btn:hover { background: #352626; }
.song.staged-del > span:first-child { text-decoration: line-through; opacity: 0.6; }
.tag.del { color: var(--bad); border-color: #8b6868; }
.song.edited { box-shadow: inset 3px 0 0 var(--warn); }
/* a staged DELETE is red, not the amber of an edit (Aur, 2026-07-22) — after .song.edited so it wins
   on a row that is both edited and deleted, which is the more serious of the two */
.song.staged-del, .library details.dirty.del-staged > summary { box-shadow: inset 3px 0 0 var(--bad); }
.lib-edit.dirty.del-staged { border-left-color: var(--bad); }
.tag.edit { color: var(--warn); border-color: #79689c; }
/* per-field glow: the input/select whose value differs from the saved value lights up too — BOTH the
   row's left bar and the changed box glow (Aur's model). */
.lib-edit input.field-dirty, .lib-edit select.field-dirty { border-color: var(--warn); box-shadow: 0 0 0 2px rgba(255, 180, 84, 0.35); }
.deleted-box { margin-top: var(--sp-16); border-top: 1px solid #7e6d6d; padding-top: var(--sp-8); }
.deleted-box > summary { color: #c99a9a; }
.savebar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; display: none; align-items: center; justify-content: flex-end; gap: 12px; padding: var(--sp-12) var(--sp-16); background: var(--bg); border-top: 1px solid var(--accent); box-shadow: 0 -4px 16px rgba(0,0,0,0.4); }
/* while the fixed bar is up it covers the end of the page — which is exactly where the paging
   footer lives. Reserve its height so First/Prev/Next/Last stay reachable with an edit staged. */
body.saving { padding-bottom: var(--sp-64); }
.savebar-msg { margin-right: auto; color: var(--warn); font-size: var(--fs-2); }
.savebar button { padding: 8px 16px; font-size: var(--fs-2); margin: 0; }
.library { padding-bottom: var(--sp-64); } /* room so the Save bar never covers the last rows */

/* sub-step 4: request/log tab + friend request flow */
.lib-tabs { display: flex; gap: var(--sp-8); margin: 12px 0 4px; border-bottom: 1px solid var(--border-subtle); }
.lib-tabs .tab { margin: 0; padding: 8px 16px; background: none; border: none; border-bottom: 2px solid transparent; color: var(--text-muted); border-radius: var(--r); font-size: var(--fs-2); }
.lib-tabs .tab.active { color: var(--text); border-bottom-color: var(--accent); }
.lib-tabs .tab:hover { color: var(--text); }
.req-card { background: var(--surface); border: 1px solid var(--border-subtle); border-left: 3px solid var(--accent); border-radius: var(--r); padding: var(--sp-12) var(--sp-12); margin: var(--sp-12) 0; }
.req-card.stale { border-left-color: var(--warn); }
.req-head { display: flex; justify-content: space-between; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.req-target { font-weight: 600; }
.req-meta { font-size: var(--fs-1); }
.req-diff { list-style: none; padding: 4px 0 0; margin: 4px 0 8px; font-size: var(--fs-2); }
.req-diff li { padding: var(--sp-2) 0; }
.req-field { color: var(--text-muted); }
.req-change.del { color: var(--bad); margin: var(--sp-8) 0 var(--sp-8); }
.req-actions { display: flex; gap: 8px; margin-top: var(--sp-8); }
.req-actions button { margin: 0; padding: var(--sp-8) var(--sp-16); font-size: var(--fs-2); }
.three-way { display: grid; gap: 2px 0; margin: 8px 0; font-size: var(--fs-2); }
.tw-row { display: grid; grid-template-columns: 70px 1fr 1fr 1fr; gap: 8px; padding: 2px 0; }
.tw-head { color: var(--text-muted); font-size: var(--fs-1); text-transform: uppercase; letter-spacing: var(--track-caps); border-bottom: 1px solid var(--border-subtle); }
.tw-label { color: var(--text-muted); }
.tw-live { color: #cb9e59; }
.req-status { font-size: var(--fs-1); text-transform: uppercase; letter-spacing: 0.06em; border: 1px solid #79689c; border-radius: var(--r); padding: var(--sp-2) var(--sp-8); }
.req-status.pending { color: #cb9e59; border-color: #806f4e; }
.req-status.approved { color: #53b97e; border-color: #4b7b5a; }
.req-status.rejected { color: #ed8a8a; border-color: #8b6868; }
.log-title { margin-top: var(--sp-24); }
/* a log row opens to show its diff (2026-07-31). `display: flex` on the <summary> kills the default
   triangle in WebKit, so the marker is drawn by hand — and `cursor: pointer` is what tells a phone
   user the row is tappable at all. */
.log-entry > summary::-webkit-details-marker { display: none; }
.log-row { display: flex; gap: var(--sp-12); align-items: baseline; flex-wrap: wrap; padding: var(--sp-4) 0; border-bottom: 1px solid var(--border-subtle); font-size: var(--fs-2); cursor: pointer; list-style: none; }
.log-row::before { content: '▸'; flex: none; color: var(--text-faint); }
.log-entry[open] > .log-row::before { content: '▾'; }
.log-entry[open] > .log-row { border-bottom: none; }
.log-target { flex: 1 1 auto; }
.log-when { flex: none; font-variant-numeric: tabular-nums; }
.log-note { font-style: italic; }
.log-detail { padding: var(--sp-2) 0 var(--sp-12) var(--sp-16); border-bottom: 1px solid var(--border-subtle); font-size: var(--fs-2); }
.log-detail .req-diff { margin: 0; }
/* Aur's diff colours (2026-07-31): unchanged plain, added green, removed red and struck, swapped
   yellow. Per PART — a credit that only lost a member keeps its name plain and reddens the member,
   instead of the whole old value going red and the whole new one green. */
.d-same { color: var(--text); }
.d-add { color: var(--good); }
.d-del { color: var(--bad); text-decoration: line-through; }
.d-edit { color: var(--warn); }
.role-note { font-size: 0.85em; }

/* sub-step 5: credits editor inside the song form */
.lib-credits { display: flex; flex-direction: column; gap: 4px; flex-basis: 100%; margin-top: 2px; }
.credit-label { font-size: var(--fs-1); color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--track-caps); }
.credit-rows { display: flex; flex-direction: column; gap: var(--sp-12); }
/* ⛔ ONE ARTIST IS ONE CARD (Aur, 2026-08-29: "the members doesn't feel like it's member of
   something yet", with a screenshot showing four member boxes, a hint line, a LINE-UP row and then
   the NEXT artist's boxes running on with no break between them).
   ⚠ THE FIX IS NOT AN INDENT, and that matters: `margin-left: 12px` on MEMBERS and LINE-UP is
   exactly what he made me delete on 2026-08-28 (use pass 4 item 10, his THIRD report on it — "LINE-UP
   and MEMBERS still are not lined up to ARTIST(S)"). Indenting says "belongs to" by shifting one
   edge and breaking another. A card says it by drawing the boundary, and everything inside keeps the
   single left edge he asked for — the artist box, its members and its line-up all still start at the
   same x. The ARTIST(S) section label stays outside, naming the stack of cards.
   ⚠ The trailing empty row is a card too. It is the "add another artist" slot, and giving it a
   different shape would make the page look like it had two kinds of artist row. */
/* ⛔ THE NEGATIVE MARGIN IS LOAD-BEARING, NOT A NUDGE. `padding: 8px 10px` plus a 1px border pushes
   everything inside the card 11px right — which breaks the ONE alignment he has reported three times
   ("LINE-UP and MEMBERS still are not lined up to ARTIST(S)", use pass 4 item 10), because the
   ARTIST(S) label sits outside the card. Pulling the card back by exactly that 11px puts its CONTENT
   edge where the label already is, so the card bleeds into `.ldetail`'s own 14px padding instead of
   indenting its contents. browse-check R4 is the guard and it failed on the padded version. */
.credit-block { border: 1px solid var(--border-subtle); border-radius: var(--r); padding: var(--sp-8) var(--sp-12);
  /* ⚠ THE NEGATIVE MARGIN IS NOT A NUMBER, IT IS A RELATIONSHIP: it must cancel the padding AND the
     border exactly, or the card indents its own contents — the alignment he reported three times.
     Written as a calc so the geometry sweep cannot break the pair; snapping 10px->12px while -11px
     went to -12px left 1px of indent, and browse-check R7 caught it. */
  margin: 0 calc(-1 * (var(--sp-12) + 1px)); background: var(--bg); }
.credit-row { display: flex; gap: var(--sp-8); align-items: center; }
.credit-row input[type=text] { flex: 1 1 200px; min-width: 0; font-size: var(--fs-2); padding: var(--sp-4) var(--sp-8); background: var(--surface); color: inherit; border: 1px solid var(--border); border-radius: var(--r); }
/* ⚠ WIDE ENOUGH FOR ITS OWN LONGEST OPTION (Aur, use pass 4 retest: "this has not enough space … we
   have to do something or like shorten it"). It was shrink-to-fit inside a flex row that gives the
   NAME box `flex: 1 1 200px`, so the select was squeezed until `performer` read as `perform` — the
   value you cannot afford to misread, since it is the one that decides S/A scoring. `flex: none`
   stops the row taking room from it; `ch` units size it to the text rather than to a guessed pixel
   count, so `lyricist`/`arranger` fit at any font. */
.credit-row select { flex: none; min-width: 11ch; font-size: var(--fs-2); padding: var(--sp-4) var(--sp-8); background: var(--surface); color: inherit; border: 1px solid var(--border); border-radius: var(--r); }
/* task 8 (ADR-0006): the Members line under a performer credit row */
/* the swappable tick sits on its own line under the credit, checkbox and label on ONE baseline —
   unstyled it stacked the box above the text (use-pass 2026-07-20) */
/* `label.credit-swap`, not `.credit-swap`: `.lib-edit label` (0,1,1) outranks a bare class (0,1,0),
   so the plain-class version silently lost and the label kept .lib-edit's flex-direction:column —
   which is what stacked the checkbox ABOVE its text instead of beside it (use-pass 2026-07-20). */
label.credit-swap { display: flex; flex-direction: row; align-items: center; gap: var(--sp-8); margin: 2px 0 2px 12px; cursor: pointer;
               font-size: var(--fs-1); color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--track-caps); }
label.credit-swap input[type=checkbox] { flex: none; align-self: center; width: 14px; height: 14px; margin: 0; accent-color: var(--accent); }
label.credit-swap span { line-height: var(--lh-ui); }
.credit-members, .credit-alts { display: flex; gap: var(--sp-8); align-items: center; margin: 2px 0 4px 12px; }
/* one member per row (2026-07-30): the label sits beside a COLUMN of rows now, so this one starts its
   items at the top instead of centring the label against a stack of boxes */
.credit-members { align-items: flex-start; }
.credit-members .credit-label { padding-top: var(--sp-8); }
.member-rows { display: flex; flex-direction: column; gap: var(--sp-8); flex: 1 1 200px; min-width: 0; }
/* rows + the line-up note stack. .credit-members is a flex ROW, so a note appended beside the
   boxes prints straight over them (found in Aur's 2026-08-07 use pass). */
.member-col { display: flex; flex-direction: column; flex: 1 1 200px; min-width: 0; }
/* .member-rows carries `flex: 1 1 200px` for the ROW it used to live in, where 200px is a WIDTH.
   In this column the same basis becomes a 200px HEIGHT and opened a huge gap under the members
   (Aur spotted it immediately). Basis follows the container's direction. */
.member-col > .member-rows { flex: 0 0 auto; }
/* the line-up picker: below the artist box, above the members. Full width because a label is free
   text (`pre-Karen`), not a number — Aur, 2026-08-07. */
.credit-lineup { display: flex; gap: var(--sp-8); align-items: flex-start; margin: 2px 0 4px 12px; }
.credit-lineup .credit-label { padding-top: var(--sp-8); }
/* ⛔ `.lineup-pending` IS DELETED (2026-08-28, his D1 call). It reserved this row's height and blanked
   its contents while `/api/lineups` was in flight, so the panel stopped jumping but the row still
   visibly filled in — he reported that as "the only line left that's still flashing".
   The round trip is gone: `/api/library` carries every roster and every pin, so the row is correct on
   the first frame. There is nothing to reserve and nothing to guess, which also retired the whole
   `— none —`-or-blank argument — it was a question about what to paint during a wait that no longer
   happens. browse-check R1 watches the network so the wait cannot come back unnoticed. */
.lineup-col { display: flex; flex-direction: column; gap: var(--sp-8); flex: 1 1 200px; min-width: 0; }
/* ⛔ `stretch`, NOT `center` (Aur, 2026-08-30: "the button looks weird, and doesn't line up").
   Centring three boxes of one height against a taller select leaves four different edges in a row of
   four controls. Stretch makes every child exactly as tall as the row, so the tops and the bottoms
   line up by construction rather than by a height I would have to keep in step with the select's. */
.lineup-pick-row { display: flex; gap: 4px; align-items: stretch; }
.lineup-pick { flex: 1 1 auto; min-width: 0; }
/* box-sizing: width:100% plus padding and a border overflowed the column, so the label box hung
   wider than the row above it (Aur: "the box is broken"). */
.lineup-col select, .lineup-col input { box-sizing: border-box; font-size: var(--fs-2); padding: 4px 8px; background: var(--surface); color: inherit; border: 1px solid var(--border); border-radius: var(--r); width: 100%; }
/* + is a toggle: pressed means the next save mints a new version rather than editing this one */
/* ⛔ ONE SIZE FOR THE THREE (Aur, 2026-08-29: "the sizes aren't equal too"). They shared a padding,
   which is NOT the same as sharing a width: `✎`, `+` and `✕` are three different glyph widths, so
   equal padding produced three different buttons. A fixed box and a centred glyph is what makes a
   row of controls read as a row rather than as three unrelated things — the same argument the Type /
   Flags / Cut chips settled on 2026-08-28. */
/* ⛔ `button.` PREFIXED, AND THAT IS THE WHOLE FIX (Aur, 2026-08-30). `button.minor` (0,1,1) sets
   `margin-top: 10px`, `margin-left: 8px`, `padding: 6px 14px` and `font-size: 0.9rem`, and it beat a
   bare `.lineup-add` (0,1,0) — so the version that was supposed to make these three equal only ever
   won on `width`, and the 10px top margin pushed all three BELOW the dropdown they sit beside.
   ⚠ THIRD TIME THIS EXACT TRAP HAS BITTEN IN ONE DAY: `.lanime-open`'s `font: inherit` lost to
   `.ledger .lrow button`, and `.credit-swap` / `.member-swap` are on record losing to `.lib-edit
   label`. **A rule aimed at a control needs the element in the selector, because the rule it has to
   beat almost always has one.**
   ⚠ The glyph is centred by flex rather than by padding, so the box stays exactly 32px wide however
   wide the character is — `✎`, `+` and `✕` are three different widths. */
button.lineup-add, button.lineup-edit, button.lineup-del { flex: 0 0 auto; width: 32px; margin: 0;
  padding: 0; font-size: var(--fs-2); line-height: 1;
  /* ⚠ AIMED AT THE OTHER HALF OF "the button looks weird", AND NOT PROVEN TO LAND. `✎` is not an
     emoji in Unicode's own data, but `system-ui` has no glyph for it and Windows fills the gap from
     Segoe UI Emoji — a colour pencil between two flat text glyphs. Naming Segoe UI Symbol asks for
     the monochrome glyph, and the U+FE0E in library.js asks again.
     ⛔ MEASURED IN HEADLESS CHROMIUM AT 4× AND THE PENCIL STILL CAME BACK IN COLOUR — so this is an
     attempt, not a fix, and it must not be described as one. If it still reads wrong on his machine,
     the remaining lever is the GLYPH: `✎` is the app's edit mark (`.gedit` on a grouped band uses it
     too), so changing it is an app-wide call rather than a tweak to this row. */
  font-family: "Segoe UI Symbol", "Segoe UI", system-ui, sans-serif;
  display: flex; align-items: center; justify-content: center; }
.lineup-del:disabled { opacity: 0.35; cursor: default; }
.lineup-new-row { display: flex; gap: 4px; align-items: center; }
.lineup-new-row input { flex: 1 1 auto; min-width: 0; }
.lineup-new-save { flex: 0 0 auto; padding: var(--sp-4) var(--sp-12); font-size: var(--fs-2); }
.lineup-add.on { background: #215e43; border-color: #2d7f5a; color: #97aaa0; }
/* ⛔ ✕ GOES RED THE MOMENT IT IS ARMED (Aur, 2026-09-05: "make delete button has the red outline
   when about to confirm"). ＋ pressed means "you are making one" and is green; ✕ pressed means
   "press again and it is gone", and those two states must not read the same colour.
   ⚠ AN OUTLINE, NOT A FILL — his word, and it is also the quieter change: the button is already
   growing from `✕` to `✕ sure?` at that moment, and a filled red block that changes width at the
   same time is a bigger movement than one click deserves.
   ⛔ `width: auto` IS A BUG FIX RIDING ALONG, not decoration. These three buttons are pinned to
   32px so `✎`, `+` and `✕` draw the same box — which meant the armed label `✕ sure?` was being
   drawn into a 32px box. Nothing measured it because no assertion read the armed state's WIDTH. */
.lineup-del.on { width: auto; padding: 0 var(--sp-8); border-color: var(--bad); color: var(--bad); }
.lineup-note { margin: var(--sp-4) 0 0; font-size: var(--fs-1); color: var(--text-muted); }
.lineup-note:empty { display: none; }
.member-row { display: flex; gap: var(--sp-8); align-items: center; }
/* 2026-07-31: each member row carries the artist picker, so its near-dup note needs to sit BELOW the
   row rather than beside the box — hence the column wrapper. An empty note is display:none so it
   costs no gap. */
.member-row-wrap { display: flex; flex-direction: column; min-width: 0; }
.member-row-wrap .artist-note { margin: 2px 0 0; font-size: var(--fs-1); }
.member-row-wrap .artist-note:empty { display: none; }
/* ...but NOT the "✓ known artist · on 4 songs" line: the picker renders it on attach, so a unit with
   five saved members would open behind five lines of text nobody asked for. The warnings that DO
   matter here (near-dup, new artist) stay. */
.member-row-wrap .artist-note.known { display: none; }
/* the person tick on a member row. `label.member-swap`, not `.member-swap`: `.lib-edit label` (0,1,1)
   outranks a bare class and would stack the box above its text — the same specificity bite that hit
   .credit-swap in the 2026-07-20 use pass. */
label.member-swap { display: flex; flex: none; flex-direction: row; align-items: center; gap: 4px; margin: 0; cursor: pointer;
                    font-size: var(--fs-1); color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--track-caps); }
label.member-swap input[type=checkbox] { flex: none; width: 14px; height: 14px; margin: 0; accent-color: var(--accent); }
/* alt names and members both sit on every credit row (members stopped being performer-only on
   2026-07-31) — same look, same indent */
.credit-members input, .credit-alts input { flex: 1 1 200px; min-width: 0; font-size: var(--fs-2); padding: 4px 8px; background: var(--surface); color: inherit; border: 1px solid var(--border); border-radius: var(--r); }
button.credit-rm { margin: 0; padding: 2px 8px; font-size: var(--fs-1); border-color: #8b6868; }
button.credit-add { margin: 2px 0 0; padding: var(--sp-4) var(--sp-12); font-size: var(--fs-1); align-self: flex-start; }
.req-credits { margin: 4px 0 8px; font-size: var(--fs-2); }

/* The artist box wraps its input so the suggestion list anchors to the BOX, not the page — the
   native datalist it replaced could not be positioned at all and covered half the screen when the
   box was focused empty (use-pass 2026-07-20). Takes the input's flex slot inside .credit-row. */
.artist-box { position: relative; display: block; flex: 1 1 200px; min-width: 0; }
/* ⚠ AND THE PANEL HAS TO SAY IT AGAIN (Aur, 2026-08-29: "the artist row takes too much space and
   cause hori scroll to appear. there shouldn't be one").
   `#app` carries `class="addpage"` on library.html and list.html as well as add.html — it is "the
   three form pages", not "/add" — so `.addpage .artist-box { flex: 0 0 auto }` (added 2026-08-20 to
   stop the 200px basis becoming a 200px HEIGHT in /add's COLUMN) reaches this panel too, and its own
   comment claiming it is "scoped to .addpage so /library's row keeps the basis it actually wants" has
   been wrong since the day it was written. Here the box sits in a flex ROW, so `0 0 auto` means it
   can neither grow NOR shrink: measured, the credit row's floor is 424px inside a 410px panel, and
   the ✕ ends up 29px into the padding with the row overflowing behind it.
   ⚠ THE `.addpage` RULE IS NOT NARROWED — that class is on three pages and 12 rules, and renaming a
   shared identifier without grepping every user of it is a documented way to lose a session. This
   re-asserts the basis exactly where a flex ROW holds the box, which is what `.lib-tagfield .tagadd
   .artist-box` already does for the tag field. */
.credit-row .artist-box, .member-row .artist-box { flex: 1 1 200px; }
.artist-box > input { width: 100%; box-sizing: border-box; }
.artist-box .dropdown { z-index: 60; max-height: 220px; box-sizing: border-box; } /* above the sticky save bar; border-box so it matches the input's width EXACTLY */

/* artist picker note under a name box: quiet when the artist is known, loud when the typed name is
   a near-duplicate of one that already exists (2026-07-20). */
/* The Save bar is FIXED over the bottom 55px, which scrollIntoView cannot see — it counted a note
   sitting under the bar as "fully visible" and never scrolled. scroll-margin-bottom reserves the bar's
   height (plus a little air) so revealing the near-dup choices actually clears them. */
.artist-note { margin: var(--sp-4) 0 var(--sp-4); font-size: var(--fs-2); line-height: var(--lh-ui); color: var(--text-muted); scroll-margin-bottom: 84px; }
/* ⛔ A CONFIRMATION SHOULD BE QUIET (Aur, use pass 3, item 7: it "cause space and looks weird").
   `✓ known · 🎵12` is the state where there is NOTHING to do, and it was taking a full-size line
   next to the two states that matter — the near-dup warning and "+ new artist — will be created".
   Green stays (it is the ✓), the SPACE goes: it hugs its box now instead of being a paragraph.
   ⚠ Written HERE, not earlier in the file: an `.artist-note.known` block above the base rule loses
   on source order for every property the base rule also sets. That trap has bitten this stylesheet
   before — check order before adding a competing block. */
.artist-note.known { color: var(--good); font-size: var(--fs-1); margin: var(--sp-2) 0 0; }
.artist-note.new { color: var(--text-muted); }
.artist-note.near { color: var(--warn); }
/* ⛔ THE RESTING `✓ 🎵7` LIVES ON THE BOX (Aur, use pass 4 item 11: "it's not about the telling. but
   it's where it is"). The line under the box is where this page says something HAPPENED — a
   near-duplicate, a new artist, a `5 → 6` count — and a confirmation of what was already true was
   using it to say nothing had. On the field it is the ordinary validation-tick pattern: attached to
   the thing it is about, costing no line.
   ⚠ `pointer-events: none` — it sits over the input and must never eat a click on it.
   ⚠ It clears out of the way ON FOCUS. Reserving 48px of a ~200px credit box would clip the tail of
   a long name for the whole time you are typing; the moment the box has focus you are CHANGING the
   name, and the note slot below is doing the talking again. */
.artist-known { position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
  font-size: var(--fs-1); color: var(--good); white-space: nowrap; pointer-events: none; }
.artist-box.has-known > input { padding-right: 48px; }
.artist-box.has-known > input:focus { padding-right: 8px; }
.artist-box.has-known > input:focus ~ .artist-known { display: none; }
/* an empty message slot must not still cost its margin — the panel is the one place that is measured
   in rows of editor you cannot see (2026-08-27) */
.ldetail .artist-note:empty { display: none; }
/* ⛔ AND THE BLOCK BELOW HAS TO CARRY ITS OWN GAP NOW. Aur, use pass 4 retest: "why is line up text
   got stuck up there like that?" — LINE-UP was sitting hard against the artist box. It had been
   spaced by the EMPTY note's margin, which is exactly the dead space the rule above deletes, so
   hiding it took a real gap with it. A layout must not depend on an empty element's margin: that is
   the same class of accident as `.lib-anime` looking styled because a class name was present. */
.ldetail .credit-lineup.lib-stack { margin-top: 8px; }
.artist-note .artist-fix { margin-left: 8px; padding: var(--sp-4) var(--sp-12); font-size: var(--fs-2); }
/* Two ways out of a near-dup, wrapping onto their own line so a long pair of names never squeezes the
   buttons off a phone. "add as new" stays quiet so it is never the accidental click. Each button
   carries its cost as a second line: the hint used to live in title= only, and a phone has no hover,
   so long-pressing showed nothing (Aur's use-pass, 2026-07-22). */
/* ⛔ THE TWO BUTTONS ARE ONE PAIR, SO THEY ARE ONE SIZE (Aur, use pass 4 item 12: they "look bad" and
   "must hold up for both short and long names. Today a long name makes them ragged").
   They were shrink-to-fit, and the two labels are never the same length — `use “X”` against
   `add “X” as new` — so the pair was always mismatched, and a long artist name blew one of them out
   while the other stayed small. `flex: 1 1 0` makes them split the row evenly whatever the names are,
   `align-items: stretch` makes them the same HEIGHT when one hint wraps and the other does not, and
   `overflow-wrap: anywhere` keeps a long unbroken name inside its own button instead of overflowing
   it — an artist name has no spaces to break at (`SawanoHiroyuki[nZk]:Tielle&Gemie`). */
.artist-note .artist-choices { display: flex; flex-wrap: wrap; align-items: stretch; gap: 8px; margin-top: var(--sp-8); }
.artist-note .artist-choices .artist-fix {
  margin-left: 0; flex: 1 1 0; min-width: 150px; max-width: 100%; box-sizing: border-box; padding: var(--sp-8) var(--sp-12);
  display: flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: 2px; text-align: left;
}
.artist-note .artist-fix.new { color: var(--text-muted); }
.artist-note .artist-fix-label, .artist-note .artist-fix-hint { max-width: 100%; overflow-wrap: anywhere; }
.artist-note .artist-fix-hint { font-size: var(--fs-1); color: var(--text-muted); font-weight: normal; }
/* ⛔ NO HINT LINE IN THE PANEL (Aur, use pass 4 retest: "remove the hint on the two buttons").
   ⚠ SCOPED TO `.ldetail`, AND THAT SCOPE IS THE WHOLE POINT. The hint became VISIBLE text on
   2026-07-22 for a stated reason: a title= tooltip needs hover and a phone has none, so long-pressing
   the buttons on /add showed nothing at all. /library is a desktop page by his own 2026-08-09 call,
   so the tooltip is legitimate HERE and is not a pattern for the friends' page. Removing it globally
   would silently re-break the report it was built for. `title=` still carries the words on hover. */
.ldetail .artist-note .artist-fix-hint { display: none; }
/* "on 5 → 6 songs": what the count is now, and what this pick makes it */
.artist-note .uses-was { color: var(--bad); }
.artist-note .uses-now { color: var(--good); }
/* Artists tab header while a rename is staged: "LiSA → Lisa".
   ⚠ WAS `.library summary .renaming` — the roster left `.library` on 2026-08-28 (use pass 4 item 14),
   and a rule keyed to the old ancestor would have gone quietly dead. The rename would still have
   happened; it just would have stopped being amber, which nothing would have failed on. */
.libroster .renaming { color: var(--warn); }

/* ── THE ROSTER TABS: Artists and Tags (Aur, use pass 4 items 14 + 15) ────────────────────────────
   ⛔ WHAT WAS WRONG, and the two halves have one cause. Item 15: the Tags tab drew `gakumas106 anime`
   — the name and its count with nothing between them — because `.lib-anime` / `.lib-anime-title`
   were styled NOWHERE. Item 14: the Artists tab put its class on the `<details>` itself, so
   `.entries summary` styled the row and `.entries details` (the rule with the border) could not, and
   /library's 1720px width left the two spans at opposite ends of an empty line.
   Both tabs are ROSTERS — a list you occasionally tidy — so they get ONE shape, and it is the
   ledger's: a column header strip, real tracks, a 44px row, a hover, a separator.
   ⚠ Deliberately NOT a restyle of `.entries`: that class also draws /list's view-only page. */
.libroster { margin-top: var(--sp-8); }
.rcount { margin: 8px 0 2px; font-size: var(--fs-2); }
.rrow, .rhead { display: grid; align-items: center; column-gap: var(--sp-16); }
/* the two rosters differ only in their tracks. Artists: caret · name · songs · person.
   ⚠ THE ALT-NAMES TRACK IS GONE (his retest): a wide column of free text earns the ledger's whole
   column system — drag to reorder, resize, hide — or it does not belong. That system for this tab is
   captured and not built. */
/* ⛔ FIVE TRACKS SINCE 2026-09-05 — Line-ups joined between Songs and Person (his ask, "maybe lineup
   could also have it's own column? like person?"). It takes the same 70px Songs has: both hold a
   short count, and giving them different widths would say they are different KINDS of thing. */
.rrow-artist { grid-template-columns: 18px minmax(220px, 3fr) 70px 70px 80px; }
/* ⛔ A STAGED CHANGE IS A COLOURED EDGE, NOT A COLUMN (Aur, 2026-08-30: "just change it to the
   boarder color around the row like in library then remove the staged row"). It used to be a fifth
   track carrying an `edited` / `will delete` chip under a `STAGED` heading — a permanent column
   that is blank on every row you have not touched, and a word that appears nowhere else in the UI.
   The ledger already answers this the right way, so the roster copies it exactly: amber edge for an
   edit, red edge for a staged delete, and the name struck through on a delete. Same rules as
   `.ledger .lrow.edited` / `.lrow.staged-del`, one screen up. */
.rentry.edited > summary { box-shadow: inset 3px 0 0 var(--warn); }
.rentry.staged-del > summary { box-shadow: inset 3px 0 0 var(--bad); }
.rentry.staged-del > summary .rname { text-decoration: line-through; opacity: 0.6; }
/* Tags: name · how many anime carry it · the admin's ✕. The action track may grow, because the
   confirm state replaces "✕" with a whole sentence naming the tag and its reach. */
.rrow-tag { grid-template-columns: minmax(200px, 2fr) 130px minmax(44px, auto); }
.rhead { padding: var(--sp-8) var(--sp-12); border-bottom: 1px solid var(--border-subtle);
  color: var(--text-faint); font-size: var(--fs-1); text-transform: uppercase; letter-spacing: var(--track-caps); }
/* the ledger's row, to the pixel — same padding, same separator, same 44px floor, same hover */
.rrow { padding: var(--sp-8) var(--sp-12); border-bottom: 1px solid var(--border-subtle); border-radius: var(--r);
  cursor: pointer; min-height: 44px; box-sizing: border-box; }
.rrow:hover { background: var(--surface); }
.rentry { margin: 0; }
/* `display: grid` on a <summary> drops the UA disclosure triangle, which is why the caret is drawn */
.rentry > summary::-webkit-details-marker { display: none; }
.rentry[open] > summary { background: var(--bg); }
/* one glyph, rotated — swapping the character would mean the JS re-rendering the row to open it */
/* ⚠ "the texts currently are too small" (item 14), and this is the trap in answering it: the LEDGER's
   own secondary text is `.t2` at 0.8rem, which is SMALLER than the 0.9rem `.muted` this tab used. So
   "make it match the library page" and "make the text bigger" pull opposite ways, and copying the
   ledger's sizes would have shipped a shrink against his report. Structure comes from the ledger;
   the SIZES go up from where they were — every number here is strictly larger than what it replaced
   (name 1rem→1.06, secondary 0.9→0.92), which is what V6 asserts rather than "matches the ledger". */
.rname { font-size: var(--fs-3); color: var(--text); min-width: 0; }
.rcell { font-size: var(--fs-2); color: var(--text-muted); min-width: 0; }
/* the roster's own editor gets the same step up — its boxes were 0.9rem inherited from `.lib-edit` */
.rform input, .rform select { font-size: var(--fs-2); }
.rform > label { font-size: var(--fs-1); }
.ract { display: flex; align-items: center; gap: var(--sp-8); justify-content: flex-start; min-width: 0; }
.ract button { margin: 0; }
/* ⚠ NOT DIMMED (Aur, use pass 4 retest: "current emoji got lost into the background color").
   0.75 opacity over `--surface` on a hovered row left the glyph reading as part of the background.
   It is the only thing in this column, so it is either legible or the column is empty. */
.rswap { font-size: var(--fs-2); opacity: 1; }
.ract .tag { margin-left: 0; }
/* the open editor takes the SONG PANEL's card, not the old accordion's accent bar */
.rform { border-left: 0; border: 1px solid var(--border-subtle); border-radius: var(--r);
  padding: var(--sp-16); margin: var(--sp-8) 0 var(--sp-12); }
.rform.dirty { border-left: 0; border-color: var(--warn); }

/* /library non-anime filter: three small mode buttons under the search box (2026-07-20) */
.lib-filter { display: flex; flex-wrap: wrap; gap: var(--sp-8); margin: 8px 0 4px; }
.lib-filter button { margin: 0; padding: 4px 12px; font-size: var(--fs-2); }
.lib-filter button.on { background: var(--accent-fill); color: var(--text); border-color: var(--accent-fill); }
/* ⛔ ONE CONTROL, DRAWN AS ONE (use pass 4, item 1). The three exclusive modes join into a single
   segmented strip — touching, sharing one outline, only the ends rounded — while the two independent
   toggles stay as separate pills with a gap. The difference a reader needs is "pick one of these"
   versus "switch these on and off", and shape carries that where colour cannot: both states are
   already `.on` in the same accent. */
/* ⚠ `.segstrip` ON THE WRAPPER, and it is a different thing from `button.seg` (2026-08-30). /list
   has always put `seg` on each BUTTON — a chip style — while /library put `seg` on the CONTAINER.
   Two meanings, one word, and the strip rules were scoped to `.lib-filter` so nothing collided until
   /list wanted the same shape (Aur: "make the all/anime/non a whole three button (like in library)").
   The container class is `segstrip` now, on both pages; `.lib-filter .seg` keeps working because that
   wrapper carries both. */
/* the filled segment needs its own edge back, or it loses its left border to the neighbour above */
/* the count rides at the left end of the strip instead of owning a line (item 3) */
.lib-filter .lcount { align-self: center; margin: 0 0 0 4px; font-size: var(--fs-1); white-space: nowrap; }

/* franchise tag filter on /list. Unlike the OP/ED segs it shows with an empty box too, because a
   tag is a filter in its own right rather than a refinement of a search. */
/* `Filter tags…` at the far end of the Show row (Aur, 2026-08-29: "Move Filter tags to the right of
   the OP/ED row" — the top row, which the same use pass reordered to Show All/Anime/Non-anime).
   ⚠ `margin-left: auto` on the ITEM, not `justify-content: space-between` on the row: the row wraps
   on a phone, and space-between would then push the segments apart on every line it wraps to. */
.kindrow .tagright { margin-left: auto; }

/* (2026-09-23) The pinned `.kindrow` strip is RETIRED with the old whole-page-scrolling /list: the
   redesigned page is a fixed frame and only its rows pane scrolls, so nothing needs to pin. The
   reasoning is in git history (the 2026-09-05 option-A block) and CLAUDE.md § /list. */

/* the artist suggestions drop into the flow while a near-duplicate warning is up, so they can be
   read without covering the use / add-as-new buttons underneath (2026-08-08). */
.dropdown.inflow { position: static; box-shadow: none; margin-top: 4px; max-height: 9rem; }

/* a member row staged for removal: struck through and dimmed until you save, so a delete is
   something you can SEE rather than a row that quietly disappeared (2026-08-08). */
.member-row.removed input { text-decoration: line-through; opacity: 0.55; border-color: #a05d6c; }
.member-row.removed .member-swap { opacity: 0.45; }

/* the multi-select tag filter: a summary button that opens a searchable checkbox panel */
.checkfilter { position: relative; display: inline-block; }
.checkfilter-toggle { font-size: var(--fs-2); padding: var(--sp-4) var(--sp-12); background: var(--surface); color: inherit;
  border: 1px solid var(--border); border-radius: var(--r); cursor: pointer; }
.checkfilter-toggle.on { border-color: #646cad; color: var(--text); }
.checkfilter-panel { position: absolute; z-index: 30; top: calc(100% + 4px); left: 0; min-width: 200px;
  max-width: min(280px, 90vw); background: var(--bg); border: 1px solid var(--border); border-radius: var(--r); padding: var(--sp-8);
  box-shadow: 0 8px 24px rgba(0,0,0,0.5); }
/* set by checkfilter.js when the panel would open past the right edge — see the comment there. The
   page scrolling sideways is the failure; hanging the panel off the other corner is the fix. */
.checkfilter-panel.flip { left: auto; right: 0; }
.checkfilter-search { width: 100%; box-sizing: border-box; font-size: var(--fs-2); padding: var(--sp-4) var(--sp-8);
  background: var(--surface); color: inherit; border: 1px solid var(--border); border-radius: var(--r); }
.checkfilter-rows { max-height: 40vh; overflow-y: auto; margin: var(--sp-8) 0; }
.checkfilter-row { display: flex; gap: 8px; align-items: center; padding: 4px 2px; font-size: var(--fs-2); cursor: pointer; }
.checkfilter-row:hover { background: var(--surface); border-radius: var(--r); }
.checkfilter-clear { width: 100%; }
/* /library carries .addpage, and `.addpage label` (0,2,0) beats `.checkfilter-row` (0,1,0) — its
   flex-direction: column would stack every tick box ABOVE its own name, and `.addpage input` would
   give each checkbox 8px of padding and a border. Scope the correction to .addpage rather than
   weakening either rule; /list is unaffected because it is not an .addpage. */
.addpage .checkfilter-row { flex-direction: row; color: inherit; }
.addpage .checkfilter-row input[type="checkbox"] { width: auto; padding: 0; margin: 0; }
.addpage .checkfilter-search { font-size: var(--fs-2); padding: var(--sp-4) var(--sp-8); }

/* Background-download popup (2026-08-08). Bottom-right, above everything, and NEVER over the form's
   controls on a phone: it is width-capped and sits clear of the right edge. A ✓ row stays until
   dismissed — auto-hiding it would defeat the one question the popup exists to answer. */
.jobs { position: fixed; right: 12px; bottom: 12px; z-index: 60; display: flex; flex-direction: column;
  gap: var(--sp-8); max-width: min(340px, calc(100vw - 24px)); }
.job { display: flex; align-items: center; gap: 8px; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r); padding: var(--sp-8) var(--sp-12); font-size: var(--fs-2); color: var(--text);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45); }
/* the title takes the slack and truncates; the state never wraps, so the row height stays stable
   as a percentage climbs (a re-flowing popup reads as flicker) */
.job-title { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* shrinkable, or a long failure message shoves the Retry button off the screen entirely
   (Aur, 2026-08-08). Only the buttons are allowed to hold their size. */
.job-state { flex: 0 1 auto; min-width: 0; color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* a failure is the one state worth reading in full, so it wraps instead of truncating; the row is
   already width-capped by .jobs, and long yt-dlp errors have no spaces to break on */
.job-failed .job-state { white-space: normal; overflow-wrap: anywhere; }
.job-failed { align-items: flex-start; }
.job-done { border-color: #417d56; }
.job-done .job-state { color: #60b783; }
.job-failed { border-color: #a65b66; }
.job-failed .job-state { color: #ec8995; }
.job-stalled .job-state { color: #c2a341; }
.job-retry, .job-x { flex: 0 0 auto; background: var(--border-subtle); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r); padding: var(--sp-4) var(--sp-8); font-size: var(--fs-1); cursor: pointer; }
.job-x { padding: var(--sp-4) var(--sp-8); color: var(--text-muted); }
/* the two icon buttons want the same footprint so a failed row and a done row line up */
.job-retry { padding: var(--sp-4) var(--sp-8); font-size: var(--fs-2); line-height: 1; }
.job-retry:hover, .job-x:hover { background: var(--surface); }

/* ---------- PAGE NAV (2026-08-09) ---------- */
/* One row across /list, /library and /add. Aur: "the navigation between sections still badly design.
   that might be the cause I wanna just merge them. since it's opening a new tab everytime." These
   pages hold no socket, so they hop in the SAME tab; only the game tab still spawns new ones.
   Sized for a thumb (44px targets) and it wraps rather than scrolling sideways on a phone. */
.pagenav { gap: var(--sp-4); margin: 0 0 var(--sp-16); padding-bottom: var(--sp-12); border-bottom: 1px solid var(--border-subtle); flex-wrap: nowrap; }
/* ONE row at 390px (Aur: "currently it doesn't fit and make it two rows"). 44px tall for a thumb, but
   the horizontal padding shrinks and the label may ellipsis rather than wrap the row. */
/* ⚠ `.nav-back` IS A <button> NOW, NOT AN <a> — a link's href is followable by ctrl+click and
   middle-click however the click handler behaves, and following it means a second game tab. It has
   to keep the links' look, so every selector here names it explicitly. */
.pagenav a, .pagenav .nav-here, .pagenav .nav-back {
  display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 0 var(--sp-8);
  border-radius: var(--r); text-decoration: none; font-size: var(--fs-2); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
.pagenav a, .pagenav .nav-back { color: var(--text); background: var(--bg); border: 1px solid var(--border-subtle); font-family: inherit; cursor: pointer; }
.pagenav a:hover, .pagenav .nav-back:hover { background: var(--surface); }
/* the page you are on is stated, not linked — a link to where you already are costs a tap to learn */
.pagenav .nav-here { color: var(--text); background: #95a0f222; border: 1px solid #95a0f288; font-weight: 600; }
.pagenav .nav-back { margin-right: auto; }
/* the back link gives up its space first — the three destinations are the row's job */
.pagenav .nav-back { flex: 0 1 auto; }

/* ── /library LEDGER (desktop redesign slice 1, 2026-08-09) ───────────────────────────────────
   Rows are SONGS, columns line up from ONE grid template shared by the header strip and every row.
   Deliberately NEW class names: `.song` / `.entries details` above belong to the old accordion and
   the viewer, and the two must not drift into each other.
   ⚠ /library is a DESKTOP page (Aur, 2026-08-09) — and ONLY /library. The game, /add and /list stay
   mobile, which is why the width lives on a body class this page sets rather than on #app. */
/* ⛔ RESERVE THE SCROLLBAR'S LANE ON EVERY TAB — HIS FINDING, 2026-08-29, and it is the whole answer
   to "the ui or boarder or something aren't the same across each page".
   The LIBRARY tab is a fixed frame (`body.libframe { overflow: hidden }`) and never shows a window
   scrollbar. The other four are ordinary pages: TALL ones (Artists, Requests) scroll and get one,
   SHORT ones (Tags, Deleted) do not — and a classic scrollbar takes ~15px out of the viewport, so the
   whole page jumps sideways as you switch tabs. That is exactly the grouping he reported twice and
   could not name: "artist and request has the same size, and also tags and deleted. but nothing same
   as library." He worked it out himself — I had measured `#app` on tabs that all happened to be
   short, so my numbers agreed and the difference was invisible to me.
   `scrollbar-gutter: stable` keeps the lane reserved whether or not anything scrolls, so all five
   tabs are the same width and nothing moves.
   ⚠ IT IS EVERY PAGE NOW (Aur, 2026-08-29: "ship it everywhere now"), over the safer "wait for their
   use pass". It was scoped to /library by `:has()` because /add and /list are the friends' pages and
   had no browser cover at all; he took that cost knowingly and named it, so the note stays as the
   record rather than as a reason to hold. `/add` and `/list` each have an assertion now — the
   cheapest possible answer to the thing that made scoping it feel necessary.
   ⚠ The class is on `<body>` in library.html STATICALLY. Set from JS it would arrive after the first
   paint, and the gutter appearing late is the same jump this rule exists to remove. */
html { scrollbar-gutter: stable; }
/* ⛔ THE SCROLLBAR IS PART OF THE PALETTE (Aur, 2026-09-04: "weird color on the right").
   Nothing set `scrollbar-color`, so the page and the editor panel used the UA default for
   `color-scheme: dark` — a mid grey, unremarkable against the old #14141b ground and a pale stripe
   against this one.
   ⛔ AND THE FIRST FIX PUT `--border` THERE, WHICH HE REPORTED AGAIN — "but on the right nothing
   was fixed". The rule WAS applying; the colour was wrong, and wrong in a way worth naming: a
   scrollbar thumb is a PLANE, and `--border` is a LINE token, solved to 3:1 against --surface
   because that is what a 1px boundary needs. The same ratio spread over a 15px bar running the
   full height of the panel reads far louder than it does on a hairline — area is not in the
   contrast formula at all.
   ⚠ THIS IS THE SAME MISTAKE AS `--accent` USED AS A FILL, one section up: a colour solved for one
   role, reused in another. The pair is the fix in both places.
   So: the thumb rests at `--border-subtle` (1.61:1 on the panel — a shape you can see without a
   stripe down the page) and lifts to `--border` on hover, which is when you are reaching for it and
   want it obvious. */
html, .ldetail, .lscroll, .drawer { scrollbar-color: var(--border-subtle) transparent; }
html:hover, .ldetail:hover, .lscroll:hover, .drawer:hover { scrollbar-color: var(--border) transparent; }
/* ⛔ /list SHARES /library's WINDOW (Aur, 2026-09-04: "make the switching tab, the back to game
   button and basically the whole window, the same as the /library one"). It was penned into the
   560px `#app` column, so on a 1440px screen the nav row and its ← Game button sat marooned in the
   middle with ~440px of empty margin on each side while /library's ran edge to edge.
   ⚠ THIS DOES NOT TOUCH THE PHONE. `calc(100vw - 32px)` is the phone's own width there, so /list
   stays exactly the page that passed its phone use pass — it only stops refusing a wide screen. */
body.libpage #app, body.listpage #app { max-width: min(1720px, calc(100vw - 32px)); }
/* ⚠ MINE, NOT HIS — say so rather than filing it as part of the instruction. Widening the frame let
   two controls grow with it that have no business being 1200px wide: the "which list" picker and the
   MyAnimeList username box. A cap is invisible on a phone (they are already narrower than this) and
   stops the wide page reading as a stretched phone page. The search box is deliberately NOT capped —
   /library's runs the full width too, and that is the thing he asked these pages to share. */
.listpick, .malname { max-width: 420px; }

/* ⛔ AN EMPTY FLASH TAKES NO SPACE, ON EVERY TAB (2026-08-28 retest). This was scoped to
   `body.libframe`, i.e. to the Library tab alone — but `#lib-flash` is appended on ALL of them, so an
   empty <p>'s margin pushed the Artists search box 8px below the Library one, which is exactly what he
   measured by eye: "make like the search box line up (be as the same spot) as the library's search
   box". Scoped by id, so only /library has the element at all. */
#lib-flash:empty { display: none; }
/* ⛔ AND WHEN IT IS NOT EMPTY IT STILL TAKES NO SPACE (Aur, 2026-08-29: "and saved changes still is on
   top. and that pushes the search box. so maybe make it the same pop up at bottom right like queueing
   download").
   ⚠ THE ELEMENT HE MEANT IS THIS ONE, not the Save bar — the bar has been `position: fixed; bottom: 0`
   since it was written and pushes nothing. "Saved 3 changes." is this <p>'s own text, and it is
   appended ABOVE the tabs, so every save shoved the whole page down a line for five seconds and let it
   spring back. That is the layout rule this batch is built on: content that changes size must not sit
   BEFORE controls.
   The shape is the one he named — the download queue's toast (`.jobs`), same corner, same box, same
   width cap. Nothing else moves when it appears.
   ⚠ NO `color` HERE. paintFlash() sets `ok` / `warn` on the node and an id (1,0,0) would outrank both
   (0,1,0), so a failure would print in the success colour.
   ⚠ The Save bar owns the bottom of the viewport when it is up, so the toast steps above it — the same
   collision the pinned panel already documents. */
/* ⚠ SHARED WITH THE GAME'S "sound is off here" HINT (2026-08-29). Two pages, one shape, because he
   named the same reference for both — the download queue's toast — and a second copy of these values
   is a second thing to keep in step. */
/* ⚠ 0.95rem AND ROOMIER PADDING (Aur, 2026-08-29: "make it a little bigger"). It started at the
   download toast's 0.85rem, which is right for a row in a stack of progress lines and too small for
   the one line telling you a save landed. */
#lib-flash, .audiohint { position: fixed; right: 12px; bottom: 12px; z-index: 61; margin: 0;
  max-width: min(360px, calc(100vw - 24px)); background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--r); padding: var(--sp-12) var(--sp-16); font-size: var(--fs-2);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45); }
body.saving #lib-flash { bottom: 84px; }
/* ⛔ IT CANNOT BE IN THE WAY, WHICH IS HALF OF WHAT HE ASKED FOR ("put it bottom right or anywhere
   that doesn't disturb"). `pointer-events: none` means a tap aimed at whatever is underneath still
   lands there — and since ANY tap unlocks audio (main.js), that same tap also dismisses this. A
   toast you have to tap around would be a worse version of the thing it is warning about. */
.audiohint { pointer-events: none; }

.ledger { margin-top: var(--sp-12); }
/* ONE template for the header and every row, written once on .llist as --ltpl (slice 2). A resize
   drag then costs one style write per frame instead of 50, and no row can drift out of alignment by
   missing an update. */
.ledger .lhead, .ledger .lrow { display: grid; align-items: center; column-gap: var(--sp-16);
  grid-template-columns: var(--ltpl); }
.ledger .lhead {
  position: sticky; top: 0; z-index: 5; background: var(--bg);
  padding: var(--sp-8) var(--sp-12); border-bottom: 1px solid var(--border-subtle);
  color: var(--text-faint); font-size: var(--fs-1); text-transform: uppercase; letter-spacing: var(--track-caps);
}
/* AMQ's box sizing: the whole row is the target, not a link inside it */
.ledger .lrow { padding: var(--sp-8) var(--sp-12); border-radius: var(--r); position: relative;
  cursor: pointer; min-height: 44px; box-sizing: border-box; }
/* ⛔ THE SEPARATOR IS A PSEUDO-ELEMENT, NOT `border-bottom`, AND THAT IS THE WHOLE FIX FOR THE
   HOOKS AT EACH END OF EVERY ROW (Aur, 2026-09-04: "why is the top boarder like these?").
   A border on a box with `border-radius` FOLLOWS the radius — so a bottom border alone renders as
   a line that curls upward through 8px at both ends, and what he was seeing above a row was the
   curl of the row before it. The bug is old; the palette sweep is what made it visible, because
   --border-subtle went from 1.35:1 against the ground to 3:1 and the hooks came with it.
   ⚠ Inset by the radius at both ends, so the line stops where the corner starts instead of
   bending around it. `left`/`right` rather than a width, so it survives any row width. */
.ledger .lrow::after { content: ''; position: absolute; left: var(--r-8); right: var(--r-8);
  bottom: 0; height: 1px; background: var(--border-subtle); pointer-events: none; }
.ledger .lrow:last-child::after { display: none; }
.ledger .lrow:hover { background: var(--surface); }
.ledger .lrow:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* ⛔ SELECTION IS THE FILL, AND NOTHING ELSE. Aur, 2026-09-04, asked directly: "build it".
   It was `background: var(--bg)` + an accent bar. Two problems, fixed in two steps the same day:
   the ground colour punched a flat rectangle into the wash and made a selected row DARKER than a
   hovered one; and the left bar carried THREE meanings in three colours at once — selected, staged
   edit, staged delete — which is the pattern spec §7 exists to break.
   ⚠ THE COLOURED LEFT EDGE IS NOT GONE, and his §1 call is intact: it still marks a row with
   unsaved work, in amber for an edit and red for a delete. It now means exactly ONE thing, which is
   what makes it readable at a glance down a 50-row page.
   ⚠ This was only possible once the surfaces separated. At the old 1.10:1 a fill was invisible, so
   the bar had to do every job; at 1.2:1 between surface and raised a fill actually reads. The
   ordering was not a preference — the earlier palette could not have carried this.
   ⚠ HOVER AND SELECTED ARE ONE STEP APART (--surface to --surface-raised, 1.2:1) and that is
   deliberate: hover is transient and selection persists, so they are almost never compared side by
   side. If they turn out to be too close in use, selection is the one to strengthen. */
.ledger .lrow.sel { background: var(--surface-raised); }
.ledger .lrow.off { opacity: 0.45; }
.lcell { min-width: 0; }
.lcell.ell, .lcell .ell { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* one size for Song, Credit and Anime (use pass 4, item 6). Credit carried no size class, so it
   sat at the row's base while the other two were at 0.97rem — that gap is what he saw. */
.lcell .t1 { font-size: var(--fs-2); color: var(--text); }
.lcell .t2 { font-size: var(--fs-1); color: var(--text-muted); }

/* ⚠ ONE CHIP SHAPE for Type, Flags and Cut (Aur, 2026-08-28: "type and flags box has different sizes
   and different background color. also the cut should have the same box style too").
   They had grown as three unrelated boxes: `.ltype` was 0.72rem on `--surface`, centred and stretched
   across its whole 58px column; `.lflag` was 0.66rem, transparent and shrink-to-fit; `.lcut` was not
   a box at all, just muted text. On one row that reads as three different KINDS of thing when they
   are the same kind — a short status word about the song. Same shape, and the COLOUR carries the
   meaning; nothing else does. */
.ltype, .lflag, .lcut { font-size: var(--fs-1); letter-spacing: 0.06em; text-transform: uppercase;
  border: 1px solid var(--border); border-radius: var(--r); padding: var(--sp-2) var(--sp-8); white-space: nowrap;
  color: var(--text-muted); background: var(--surface); justify-self: start; }
.ltype.op { color: var(--good); border-color: #55795f; }
.ltype.ed { color: #95a0f2; border-color: #676e9e; }
.ltype.ins { color: var(--warn); border-color: #816e4f; }
.ltype.custom { color: var(--special); border-color: #79689c; }

/* the state the page exists to fix: a flat credit line from /add with no artist rows behind it */
.lcredit.flat .ell { color: var(--warn); }
/* ⛔ `.why` (the `N artists linked` sub-line) is DELETED — the Needs-artists FILTER answers that
   question, and the amber `.flat` colour still marks the row in place (use pass 4, item 8). */

.lflags, .ltags { display: flex; gap: var(--sp-4); flex-wrap: wrap; }
.lflag.warn { color: var(--warn); border-color: #816e4f; }
.lflag.tag { color: var(--special); border-color: #79689c; }
.lflag.del { color: var(--bad); border-color: #8b6868; }
/* The VERSION chip (2026-08-09). One class for every surface that shows a song name — the /library
   ledger, the panel heading, a /list row and the reveal — so the version can never drift into looking
   like part of the title on one page and a chip on another. `.vchip` is deliberately NOT uppercased
   like .lflag: a version is typed prose ("live in Budokan"), not a status word. */
.vchip { font-size: var(--fs-1); color: #6dacdb; border: 1px solid var(--border); border-radius: var(--r);
  padding: 0 var(--sp-4); margin-left: var(--sp-8); white-space: nowrap; vertical-align: baseline; }

/* THE PROVENANCE SLOT — a real column on every row, empty when the row has nothing to say, so
   "edited by DesZ" costs no layout when the friend-edit review feed lands. */
/* the two optional anime/audio columns (2026-08-17) — secondary information, so they read at the
   weight the Anime column's own second line does rather than competing with the song title */
/* ⚠ `.lcut` LEFT THIS PAIR on 2026-08-28 — it is a chip now, with Type and Flags. Season/Year stays
   plain text: it is a VALUE you read, not a status word, and boxing it would say otherwise. */
.lvintage { color: var(--text-muted); font-size: var(--fs-1); }
/* ⛔ `justify-content: flex-end` IS GONE with the header's right-align (his retest, 2026-08-28): a
   column that can be dragged anywhere has no right edge to hug. */
.lprov { display: flex; align-items: center; gap: var(--sp-8); }
.lprov .who { font-size: var(--fs-1); color: var(--text-muted); white-space: nowrap; }
.lprov .who b { color: var(--special); font-weight: 600; }

/* ⛔ THE LEFT BAR MEANS "UNSAVED WORK" AND NOTHING ELSE NOW (2026-09-04). Selection stopped using
   it the same day, so amber and red are the only two bars on the page and they are one idea. */
.ledger .lrow.edited { box-shadow: inset 3px 0 0 var(--warn); }
/* a staged DELETE is red, not the amber of an edit (Aur, 2026-07-22) — after .edited so it wins */
.ledger .lrow.staged-del { box-shadow: inset 3px 0 0 var(--bad); }
.ledger .lrow.staged-del .lsong .t1 { text-decoration: line-through; opacity: 0.6; }
.ledger .lrow button { margin: 0; padding: 2px 8px; font-size: var(--fs-1); line-height: var(--lh-ui); }
/* ⛔ THE ANIME NAME IS NOT ONE OF THOSE BUTTONS (Aur, 2026-08-29: "anime title size should be the
   same as song and credit"). It IS a button — that is how it opens the anime — so the rule above
   (0,2,1) was beating `.lanime-open { font: inherit }` (0,1,0) and printing the anime at 0.78rem
   beside a 1rem song and a 1rem credit. The `.t1` wrapper was already right; nothing inherited it.
   ⚠ THIS IS THE THIRD TIME SPECIFICITY HAS BITTEN THIS FILE THROUGH A `font: inherit` ON A CONTROL
   (.credit-swap and .member-swap are the other two, both stacking a checkbox above its label). When
   a control has to look like the text around it, check what generic rule already claims it. */
.ledger .lrow button.lanime-open { font: inherit; }

/* ── GROUP BY (slice 2) — amqbot's drag-a-column-into-the-strip ───────────────────────────────
   Ported from the mockup he compared the variants in, so the built page looks like the one he
   picked from. The strip is a dashed drop target so it reads as somewhere to put something. */
.gby { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: var(--sp-12) 0 var(--sp-2);
  padding: var(--sp-8) var(--sp-12); border: 1px dashed var(--border); border-radius: var(--r); min-height: 34px; }
.gby.over { border-color: var(--accent); background: var(--surface); }
.gby.on { border-style: solid; }
.gby .lbl { font-size: var(--fs-1); text-transform: uppercase; letter-spacing: var(--track-caps); color: var(--text-faint); }
.gby .gchip { display: inline-flex; align-items: center; gap: var(--sp-8); font-size: var(--fs-1);
  background: var(--bg); border: 1px solid #95a0f266; color: var(--text); border-radius: var(--r);
  padding: var(--sp-4) var(--sp-8) var(--sp-4) var(--sp-12); }
.gby .gchip .gx { background: none; border: 0; color: var(--text-muted); margin: 0; padding: 0 4px; font-size: var(--fs-2); }
.gby .gchip .gx:hover { color: var(--bad); }
/* ⚠ `.gby .gopt` is DELETED, not orphaned — the pill row it styled was removed on 2026-08-27 (his
   call: grouping is the drag, and one gesture is the whole control). M1 asserts the row is gone. */
/* the column labels you can drag. The ⋮⋮ is the affordance: a label that does nothing must not
   look like one that does, which is why a column with no grouper stays a plain <span>. */
.ledger .lhead .hdrag { cursor: grab; border-bottom: 1px dotted var(--border);
  /* ⚠ THE BROWSER MUST NOT CLAIM THE GESTURE (2026-08-29). Without this a finger on the label scrolls
     the page and the pointer events stop arriving, so the touch drag could never start — the same
     reason the resize grip is on pointer events rather than on a drag. */
  touch-action: none; }
.ledger .lhead .hdrag.dragging { opacity: 0.6; cursor: grabbing; }
.ledger .lhead .hdrag:hover { color: var(--special); border-bottom-color: #79689c; }
/* ⚠ The grab dots were half-transparent, tiny and butted straight against the label, so they read as
   part of the word — Aur, 2026-08-17: "the 6 dot are too small and got hide in the text. so I didn't
   notice it." He had been using /library for a week without knowing a column could be dragged into a
   grouping at all. A control nobody can see is a control that does not exist: they are now separated
   from the label, larger and properly opaque. */
.ledger .lhead .hdrag::before { content: '⋮⋮'; letter-spacing: -3px; margin-right: 8px;
  font-size: 1.15em; color: var(--text-faint); opacity: 0.9; vertical-align: -1px; }
.ledger .lhead .hdrag:hover::before { color: var(--special); opacity: 1; }
.ledger .lhead .hdrag.grouping { color: var(--special); }
/* STRETCHABLE COLUMNS (his request). The grip is its own element on the cell's right edge, on
   pointer events — it cannot be the label, which is already an HTML5 drag handle for group-by. */
/* ⚠ the CELL must not clip: the grip hangs off its right edge, and `overflow: hidden` here deleted
   the whole hit area (caught by N2/N5, which measured a drag that moved nothing). The ellipsis lives
   on the LABEL inside instead, which is what needed it anyway. */
.ledger .lhead .hcell { position: relative; min-width: 0; }
/* the drop target for MOVING a column (2026-08-28) — a left edge, because that is where the dragged
   column lands: it takes this one's place and pushes it right. A whole-cell highlight would say
   "swap with me", which is not what happens. */
.ledger .lhead .hcell.hover-drop { box-shadow: inset 3px 0 0 var(--accent); }
.ledger .lhead .hcell > span:not(.hgrip) { display: inline-block; max-width: 100%; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
.ledger .lhead .hgrip { position: absolute; top: -6px; bottom: -6px; right: -8px; width: 12px;
  cursor: col-resize; touch-action: none; }
/* ⚠ This used to be `opacity: 0` at rest — "a permanent line between every column is noise on a page
   of 7 of them". Aur's use overruled that on 2026-08-17: "also no dimmed line so I know I can resize
   the column yet." Invisible-until-hover only works for a control you already know is there; for
   discovering one it is exactly wrong. It rests faint and lights up on approach. */
.ledger .lhead .hgrip::after { content: ''; position: absolute; top: 2px; bottom: 2px; left: 5px;
  width: 2px; background: var(--accent); opacity: 0.22; border-radius: var(--r); transition: opacity 0.12s; }
.ledger .lhead .hgrip:hover::after, .ledger .lhead .hgrip:active::after { opacity: 0.85; }

/* a group separator inside the ledger — a band across the whole grid, not a row of its own */
.gsep { display: flex; align-items: baseline; gap: var(--sp-12); padding: var(--sp-12) var(--sp-12) var(--sp-8);
  border-bottom: 1px solid var(--border-subtle); }
.gsep .gname { font-size: var(--fs-2); color: var(--text); }
.gsep .gcount { font-size: var(--fs-1); color: var(--text-muted); }
.gsep .gmeta { font-size: var(--fs-1); color: var(--text-muted); }
/* ⚠ THE BAND IS NOT ONE BUTTON ANY MORE (2026-08-27). It carries two controls that mean different
   things — the chevron folds the band, the NAME opens the anime — and one element cannot mean both.
   Grouped by anime the name is the button (his 2026-08-16 fix for reaching an anime from the header
   of its own songs); grouped by anything else it is plain text and only the chevron is live. */
.gsep .gfold { margin: 0; padding: 0 var(--sp-8); background: none; border: 0; border-radius: var(--r);
  color: var(--text-faint); font-size: var(--fs-1); line-height: var(--lh-ui); cursor: pointer; align-self: center; }
.gsep .gfold:hover { color: var(--text); background: var(--surface-raised); }
.gsep.folded { opacity: 0.82; }
button.gname.ganime { margin: 0; padding: 0; background: none; border: 0; border-radius: var(--r);
  text-align: left; cursor: pointer; font: inherit; font-size: var(--fs-2); color: var(--text); }
button.gname.ganime:hover { color: var(--special); text-decoration: underline; }
.gsep .gedit { margin-left: auto; color: var(--text-faint); font-size: var(--fs-2); }
.gsep:hover .gedit { color: var(--special); }

/* SORTING (2026-08-27): a header click sorts, asc → desc → the order the server sent. The arrow is
   the only thing on screen that says which of the three you are in — without it the third click
   reads as a click that did nothing. */
.ledger .lhead .hsort { cursor: pointer; }
.ledger .lhead .hsort:hover { color: var(--text); }
.ledger .lhead .sorted { color: var(--special); }
.ledger .lhead .sarrow { margin-left: var(--sp-4); font-size: 0.72em; vertical-align: 1px; }

/* paging footer — amqbot's shape: page N of M plus the per-page picker */
.lpage { display: flex; align-items: center; gap: var(--sp-12); padding: 12px 4px 4px; flex-wrap: wrap; }
/* ⛔ THE PAGER MAY NOT RE-MEASURE AS YOU PAGE (Aur, 2026-08-29: "digits make the buttons move"). The
   label used to sit BETWEEN Prev and Next, so `Page 9 of 88 · 201–250 of 4394` and `Page 1 of 88 ·
   1–50 of 4394` were different widths and both buttons shifted on every tap. Three rules hold it
   still now, and all three are load-bearing:
     · the steps and the number window are ONE group at the left, so nothing variable is inside it;
     · every `.lpage-num` is the same width — a min-width that fits three digits plus tabular figures,
       because `1` and `88` are otherwise different widths and would move Next by a few px each page;
     · the count comes after them, and `.per` is pinned right by its own margin, so the digits eat the
       slack between the two instead of pushing anything. */
.lpage .lpage-steps { display: flex; align-items: center; gap: 4px; }
.lpage .lpage-nums { display: flex; align-items: center; gap: 4px; margin: 0 4px; }
.lpage .lpage-num { min-width: 34px; text-align: center; font-variant-numeric: tabular-nums; padding: var(--sp-4) var(--sp-8); }
/* the current page is a MARKER, not a button — his call. It has to line up with the buttons beside it,
   so it copies their box and drops only the affordance. */
.lpage span.lpage-num { display: inline-block; box-sizing: border-box; border: 1px solid var(--accent);
  border-radius: var(--r); background: var(--accent-fill); color: var(--text); font-size: var(--fs-2); font-weight: 600;
  line-height: 1.35; padding: var(--sp-4) var(--sp-8); }
.lpage .lpage-step { min-width: 34px; text-align: center; }
.lpage .lpage-at { color: var(--text-muted); font-size: var(--fs-2); display: inline-flex; align-items: center; gap: var(--sp-8); }
/* wide enough for four digits (a 25-per-page library passes 176 pages) and no wider: it is a jump box,
   not a field */
.lpage .lpage-box { width: 4.5ch; text-align: center; font-variant-numeric: tabular-nums;
  background: var(--surface); color: inherit; border: 1px solid var(--border); border-radius: var(--r);
  padding: var(--sp-4) var(--sp-4); font-size: var(--fs-2); }
.pickmenu-panel { min-width: 0; padding: 4px; }
/* ⚠ THE PAGER'S COPY OPENS UPWARD AND RIGHT-ALIGNED, and only the pager's. The shared panel is
   written for a control near the TOP LEFT of the page (`top: 100%; left: 0`), which is right for the
   lobby's `List: …` switcher and wrong here: this one lives in the bottom-right corner, where
   the default would put the list off the bottom edge and off the right one. Scoped to `.lpage` so
   the next place that reuses pickMenu gets the sane default rather than this exception. */
.lpage .pickmenu-panel { top: auto; bottom: calc(100% + 4px); left: auto; right: 0; }
.pickmenu-row { display: block; width: 100%; text-align: left; margin: 0; padding: var(--sp-8) var(--sp-12);
  font-size: var(--fs-2); background: none; border: 0; border-radius: var(--r); color: var(--text); cursor: pointer; }
.pickmenu-row:hover { background: var(--surface-raised); }
.pickmenu-row.on { background: var(--accent-fill); color: var(--text); }
/* ⛔ EVERY SINGLE-PICK <select> OPENS THIS LIST, NOT THE BROWSER'S (selectmenu.js; his look pass 2026-10-23, 1b:
   the browser's list hovered light blue with dark text, ours only brightens the row). It wears the /list switcher's
   and the profile menu's shape: solid --l3 (anything that floats is opaque), rule lines between rows, the hover is
   --hover-fill and the TEXT DOES NOT CHANGE. The select's own value carries the /list `.here` bar; the brighter
   fill goes only where the pointer or the arrow keys are, so one row is lit at a time. The face and size are copied
   from the select inline. */
.selmenu { position: fixed; z-index: 1100; box-sizing: border-box; overflow-y: auto; overscroll-behavior: contain;
  max-width: min(480px, calc(100vw - 16px)); background: var(--l3); border-radius: var(--r); box-shadow: 0 12px 32px rgba(0, 0, 0, .5); }
.selmenu-row { padding: var(--sp-8) var(--sp-12); color: var(--text); cursor: pointer; overflow-wrap: anywhere; user-select: none; }
.selmenu-row + .selmenu-row { box-shadow: inset 0 1px 0 var(--rule); }
.selmenu-row.act { background: var(--hover-fill); }
.selmenu-row.here { box-shadow: inset 3px 0 0 var(--text); }
.selmenu-row + .selmenu-row.here { box-shadow: inset 3px 0 0 var(--text), inset 0 1px 0 var(--rule); }
.selmenu-row[aria-disabled="true"] { opacity: .45; cursor: default; }
/* a finger needs the 44px row; a mouse keeps the browser list's density */
@media (pointer: coarse) { .selmenu-row { min-height: 44px; display: flex; align-items: center; box-sizing: border-box; } }
.lpage .per-label { color: var(--text-muted); font-size: var(--fs-2); }
.lpage .per { margin-left: auto; display: flex; align-items: center; gap: var(--sp-8);
  color: var(--text-faint); font-size: var(--fs-1); text-transform: uppercase; letter-spacing: var(--track-caps); }
.lpage button { margin: 0; padding: 4px 12px; font-size: var(--fs-2); }
.lpage button.on { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--text); }
.lpage button[disabled] { opacity: 0.35; cursor: not-allowed; }

/* ── the docked editor (slice 1 task 6) ───────────────────────────────────────────────────────
   The panel's lane is RESERVED, never animated in: appearing on the first click would re-measure
   every column at the exact moment you start editing, and the row would move under the cursor. */
.lpane { display: grid; grid-template-columns: minmax(0, 1fr) 440px; gap: var(--sp-16); align-items: start; }
/* ⚠ A FLOOR, NOT ZERO (Aur, 2026-08-28: "no horizontal scrollbar yet"). `min-width: 0` let the grid
   shrink below its own tracks, so `fr` columns CRUSHED instead of overflowing and `.lscroll` never
   had anything to scroll — the Credit column narrowed to nothing rather than the list moving
   sideways. `--lmin` is the sum of the showing columns' floors, written by renderResults, so the
   scrollbar appears exactly when the box can no longer hold them at a readable width. */
.llist { min-width: var(--lmin, 0); }
/* the list + its pager, as one column. ⚠ `min-width: 0` outside the frame's media query too: a grid
   item defaults to `min-width: auto` and refuses to shrink below its content, so without this the
   ledger pushes the panel off the right edge on the narrow (no-frame) layout. */
.lmain { min-width: 0; }
/* ⚠ THE LIST OWNS ITS SIDEWAYS SCROLL IN EVERY LAYOUT, NOT ONLY INSIDE THE FRAME (Aur, 2026-08-29:
   "the rows just go through the editor box (it's in front of the rows)", measured on his phone at
   708px). `--lmin` (2026-08-28) is a real floor on `.llist` — 1256px with the default columns — and
   `.lpane`'s first track is `minmax(0, 1fr)`, whose 0 min means the track NEVER grows to hold it. So
   wherever the list has no scroll box of its own the overflow simply spills across the panel's
   column, and `.ldetail` being `position: sticky` makes it a positioned box that paints ON TOP of
   the (unpositioned) rows — the editor in front of the rows, exactly as reported.
   ⚠ IT IS NOT A NARROW-SCREEN BUG. Measured overlap: 440px at 375/708/1100 wide AND 143px at
   1600x240 — every layout where the frame's `(min-width: 1101px) and (min-height: 480px)` does not
   match. That is why this is a BASE rule instead of one inside a media query: the frame's own
   `overflow: auto` (below) is the same mechanism, and this makes the fallback use it too.
   ⚠ `overflow-x` forces the other axis to `auto`, so `.lscroll` is a scroll container here as well —
   harmless, because in the fallback it is content-height and never scrolls vertically. */
.lscroll { overflow-x: auto; }
/* ⚠ THE 2026-08-09 "no scrollbar of its own" RULE WAS REVERSED BY AUR ON 2026-08-17, deliberately —
   do not restore it from the old note. Its cost had become invisible: on his real songs the panel
   (form + credit rows + line-ups + tags + history + delete) is taller than the screen almost always,
   so the .tall escape hatch meant `position: static` and the panel NEVER followed him — "nothing
   ever goes with the scroll". Offered pin-with-inner-scroll / shrink-the-panel / follow-on-scroll,
   he picked the first: the panel is ALWAYS pinned, and scrolls inside itself only when it cannot
   fit. A short panel still shows no scrollbar at all, so the box-in-a-box he objected to appears
   exactly in the case where the alternative was an unreachable bottom.
   ⚠ overflow-y makes this a scroll container, which CLIPS the absolutely-positioned artist
   `.dropdown` at the panel edge — the panel's most-used control. O7 measures it. */
/* ⛔ `box-sizing: border-box` IS LOAD-BEARING, NOT TIDINESS (2026-08-30). Without it `max-height`
   clamps the CONTENT box, so the panel's own 14px padding and 1px border made it 30px TALLER than
   every calc() below assumes — the un-saving clamp overran the viewport by 6px, and the saving one
   left only 3px between the panel and the save bar. That 3px is why O9 passed for two weeks and then
   failed the moment the site font changed: Mitr draws the bar's buttons 7px taller (55px -> 62px),
   the bar rose 7px, and 3px of air is not air. Border-box makes the arithmetic in the calc() true. */
.ldetail { position: sticky; top: 12px; max-height: calc(100vh - 24px); overflow-y: auto;
  box-sizing: border-box;
  min-width: 0; background: var(--surface); border: 1px solid var(--border-subtle);
  border-radius: var(--r); padding: var(--sp-16); display: grid; gap: var(--sp-12); align-content: start; }
/* ⚠ THE FIXED SAVE BAR AND THE PINNED PANEL BOTH OWN THE BOTTOM OF THE VIEWPORT (2026-08-17).
   `body.saving { padding-bottom: 72px }` reserves the bar's height in the PAGE FLOW — which does
   nothing for a box clamped to the viewport, so with an edit staged the bar covered the panel's last
   ~43px, and since the panel was already scrolled to its end that content could not be brought above
   it. What lives down there is History and 🗑 Delete this song. Same failure the paging footer had
   (7f52c87); the flow reserve fixed that one and could not fix this one.
   Shrinking the clamp by the same 72px also retires the z-index half of the problem: `position:
   sticky` makes .ldetail a stacking context at z-index auto, so the artist `.dropdown`'s z-index 60
   is trapped inside it and cannot beat the bar's 20 — but once the two boxes never overlap, nothing
   inside the panel can be painted under the bar in the first place. */
body.saving .ldetail { max-height: calc(100vh - 24px - 76px); }
.ldetail h3 { margin: 0; font-size: var(--fs-3); letter-spacing: 0; }
.ldetail .sub { color: var(--text-muted); font-size: var(--fs-2); margin: 0; }
.ldetail > button, .ldbody > button { justify-self: start; margin: 0; }
/* the editor's own box when a player sits under it (library.js panel()); outside the frame it is no box at all */
.ldbody { display: contents; }
/* a fixed-width lane holds fixed-width children: without this the reused form's inputs keep their
   default size and spill straight out of the panel */
.ldetail input, .ldetail select, .ldetail textarea { min-width: 0; max-width: 100%; box-sizing: border-box; }
/* the reused songForm/animeForm is already inside a card here — drop its own card chrome */
.ldetail .lib-edit { background: none; border-left: 0; border-radius: var(--r); padding: 0; margin: 0; }
.ldetail .lib-edit.dirty { border-left: 0; }
.ldetail .lib-edit > label { flex: 1 1 100%; }
.lblock { border-top: 1px solid var(--border-subtle); padding-top: var(--sp-12); display: grid; gap: 8px; justify-items: start; }
/* Tags moved into the song panel (2026-08-09) — the most-used control on the page, so its box takes
   the panel's whole width instead of the shrink-to-content that .lblock's justify-items: start gives
   every other block. The wrapper attachSuggestBox mints (span.artist-box) has to stretch too, or the
   input inside it has nothing to be 100% of. */
.lblock > .lib-tagfield { justify-self: stretch; width: 100%; display: grid; gap: var(--sp-8); min-width: 0; }
.lib-tagfield .artist-box { display: block; width: 100%; min-width: 0; }
.lib-tagfield input { width: 100%; min-width: 0; }
/* TAG PILLS (Aur, 2026-08-28) — deliberately the same shape as the Group by chips, because they are
   the same idea: a thing that is ON, with its own ✕ to take it off. Borrowing the look is the point;
   a second visual language for "applied item" would make the page harder to read, not richer. */
.tpills { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-8); }
/* ⛔ THE ＋ IS A checkFilter TOGGLE NOW (2026-08-28) — his call: click away closes it, a tick applies
   at once, and several tags go on in one visit. It has to keep looking like the ＋ button it replaced,
   so the toggle borrows `.tadd`'s metrics rather than the filter-strip button's.
   ⚠ `.tagpick` must stay `position: relative`: `.checkfilter-panel` is absolutely positioned against
   its `.checkfilter` wrapper, and inside the pill row a static wrapper would hang the panel off the
   whole panel box instead of off the ＋. */
.tagpick { position: relative; display: inline-flex; }
.tagpick .checkfilter-toggle { margin: 0; padding: var(--sp-2) var(--sp-8); font-size: var(--fs-2); line-height: var(--lh-ui); }
.tagpick .checkfilter-panel { min-width: 220px; }
/* the create row is a full-width action, not a tick — it reads as the last resort it is */
.checkfilter-create { display: block; width: 100%; margin: 2px 0 0; text-align: left; font-size: var(--fs-2); }
.tpill { display: inline-flex; align-items: center; gap: var(--sp-8); font-size: var(--fs-1);
  background: var(--bg); border: 1px solid #95a0f266; color: var(--text); border-radius: var(--r);
  padding: var(--sp-4) var(--sp-8) var(--sp-4) var(--sp-12); }
.tpill .tx { background: none; border: 0; color: var(--text-muted); margin: 0; padding: 0 4px; font-size: var(--fs-2); }
.tpill .tx:hover { color: var(--bad); }
.lib-tagfield .hint.quiet { color: var(--text-faint); }
/* Season + Year share a line (Aur, use pass 3). Each half keeps its own label above its control,
   which is what the stacked `<label>`s already did — they just stop owning a whole row each. */
/* Type · No. · Cut on one row (his item 5). No. is the narrow one — it holds "1", not a word. */
.lib-triple { display: flex; gap: var(--sp-12); }
/* ⛔ THEY SPREAD TO FILL THE ROW (Aur, use pass 4 item 9: "spread the type,no,cut. to fill the row").
   No. was pinned to 72px because it holds a number — which left the three bunched at the left with
   dead space after them. Equal thirds instead: the ROW is the unit here, not the field. */
.lib-triple > label { flex: 1 1 0; min-width: 0; margin: 0; }
/* the panel's one remaining header line: the ANIME name, the only thing there that the form below
   does not already carry as an editable field (his item 4). */
.ldetail h3.panel-anime { margin: 0 0 8px; font-size: var(--fs-1); font-weight: 500; color: var(--text-muted); }
/* ⛔ THE PERSON TICK IS A GLYPH (his items 9-11, and his measurement: labels were eating half the
   row). It sits INLINE in both the credit row and the member row now — one shape, and the ✕ buttons
   line up down the whole block as a result. The words live in `title`; /library is desktop-only by
   his own 2026-08-09 call, so a hover tooltip is legitimate HERE and is not a pattern for /add. */
label.credit-swap, label.member-swap { flex: 0 0 auto; margin: 0; gap: 4px; padding: 0 2px; }
.swap-mark { font-size: var(--fs-2); line-height: 1; opacity: 0.75; }
label.credit-swap:hover .swap-mark, label.member-swap:hover .swap-mark { opacity: 1; }
.lib-pair { display: flex; gap: var(--sp-12); }
.lib-pair > label { flex: 1 1 0; min-width: 0; }
/* a label ABOVE its control, full width beneath it — `Other names` (his call: the names are the
   widest thing on this panel and were getting the least room of anything on it). `.credit-members`
   is the other shape, a label in a column to the LEFT, and it stays right for a credit row. */
.lib-stack { display: flex; flex-direction: column; gap: var(--sp-8); margin: 2px 0 4px; }
.lib-stack > .credit-label { padding: 0; }
/* ⚠ THE BLOCKS KEEP THEIR OWN CLASS AND *ADD* THIS ONE. `.credit-members input` (further down) is
   what styles every member box, and `.credit-members` is what browse-check's panel-flash probe looks
   for — swapping the class out instead of adding to it unstyled the boxes AND blinded the probe,
   which is how R1/R2 came back with 0 samples. Add layout, do not replace identity.
   ⚠ `align-items` must be reset: `.credit-members` sets `flex-start`, which in a ROW meant "top" and
   in this COLUMN means "don't stretch" — the boxes would collapse to their content width. */
/* ⛔ NO LEFT INDENT (Aur, use pass 4 item 10 — his THIRD report on this alignment: "LINE-UP and
   MEMBERS still are not lined up to ARTIST(S)"). `margin-left: 12px` is what the two earlier passes
   never touched: they aligned LINE-UP to MEMBERS, which fixed those two against each other and left
   both sitting 12px right of the ARTIST(S) label and its credit rows. The indent was inherited from
   `.credit-members`'s original shape, where it read as "this belongs to the row above" — but in a
   column stack the label is already doing that job, so the only thing left was a 12px lie. */
/* ⚠ AND THEN 8px WAS TOO MUCH (Aur, same evening, on the built version: "the space is too much").
   Read together, the two reports say something narrower than either alone: what was too close was the
   ROWS — member box against member box, 4px, now 6px — and the block-to-block gap was mine, not
   something he asked for. It is back to 3px, which is a hair over where it started. */
.credit-members.lib-stack, .credit-lineup.lib-stack { align-items: stretch; margin-left: 0;
  margin-top: var(--sp-4); }
.credit-members.lib-stack > .credit-label, .credit-lineup.lib-stack > .credit-label { padding-top: 0; }
/* ⛔ FLEX BASIS FOLLOWS THE CONTAINER'S DIRECTION, AND THIS IS THE SECOND TIME IT HAS BITTEN HERE.
   `.member-col` and `.lineup-col` carry `flex: 1 1 200px`, written when their parent was a flex ROW
   where 200px is a WIDTH. Turning the parent into a COLUMN (2026-08-28, so the label could sit above
   the boxes) turned that basis into a 200px HEIGHT and opened two enormous holes in the credit
   editor — Aur saw it on his first reload. `.member-col > .member-rows` a few lines up carries a
   comment about the identical failure from 2026-07-30.
   ⚠ The children must be `flex: 0 0 auto` in the stack: they should be exactly as tall as they are. */
.credit-members.lib-stack > .member-col, .credit-lineup.lib-stack > .lineup-col { flex: 0 0 auto; }

/* ── the Artists tab's line-up block (2026-09-05, the line-ups-move-to-Artists spec) ─────────────
   ⛔ ALIGNMENT A — HIS CALL, AND IT IS THE WHOLE REASON THIS RULE EXISTS. The boxes are CAPPED, not
   flexed. On the SONG panel `.lpane`'s second track is a fixed 440px, so `width: 100%` on a picker
   means 440px and reads fine; the Artists tab is the whole page, so the same declaration draws a
   1700px dropdown holding the word `current` on his 1920 screen and pushes every control after it
   to the far right. That is precisely the complaint he opened this design with — "everything is on
   right side" — and it is why the mockup's side-by-side proved nothing: both of its panels were too
   narrow for the difference to appear.
   ⚠ A CAP, NOT A WIDTH. `max-width` still lets the block shrink with a narrow window; a fixed width
   would overflow the frame that /library falls back to below 1101px. */
/* ⛔ ONE ROW WIDTH FOR THE WHOLE FORM. `.rform` is `.lib-edit`, a WRAPPING FLEX ROW, and every box
   in it carried `flex: 1 1 200px` — written when this editor lived in a 440px panel. On the Artists
   tab, which is the whole page, that draws an alt-name box 1540px wide with its `swaps` tick and its
   ✕ parked against the right edge of a 1920px screen. Capping the ROW (not the input) makes every
   row start at the left and end where its content ends, which is alignment A in one line.
   ⚠ ONE VARIABLE, not four numbers: NAME, ALT NAMES, the line-up picker and the member rows have to
   agree or the form gains a fourth ragged right edge instead of losing three. */
.rform { --row-w: 320px; }
.rform .credit-row, .rform > label { max-width: var(--row-w); }
.rform .credit-row input[type=text], .rform > label input { min-width: 0; }
/* ⛔ `flex-basis: 100%` OR THE DELETE BUTTON MOVES. `.lib-edit` wraps its children, so a block that
   does not claim the whole line becomes a flex ITEM beside the next one — the first build of this
   put `Delete artist` in the middle of the panel, level with the member rows. `.lib-credits` above
   carries the same declaration for the same reason. Caught by looking at a screenshot; no assertion
   on this page would have said a word. */
.artist-lineups { flex-basis: 100%; margin-top: var(--sp-16); }
.artist-lineups > .lineup-col, .artist-lineups .member-col { max-width: var(--row-w); }
/* the read-only roster a FRIEND sees — `.memlist`/`.memchip` are the shipped treatment, shared with
   the reveal's popover and the song panel, so nothing here restyles them; it only stops the column
   they sit in from stretching. */
/* ⛔ EACH CHIP HUGS ITS OWN NAME. `.memlist` is a COLUMN flex box, so the default `stretch` makes
   every chip exactly as wide as its container — and a full-width bordered rectangle is visually a
   BOX, which is the one thing he ruled out for read-only members ("they shouldn't have box right?
   since they're not editable"). The mockup he chose from had `align-items: flex-start` and that is
   what he was looking at.
   ⚠ SCOPED TO /library, NOT ADDED TO `.memlist` ITSELF. The reveal's popover ships the stretched
   version and he has approved it; its container is content-sized so the difference barely shows
   there, and changing a screen he signed off is not part of this. */
.ldetail .memlist, .artist-lineups .memlist { align-items: flex-start; }
.artist-lineups .memlist { max-width: var(--row-w); }
.lblabel { font-size: var(--fs-1); text-transform: uppercase; letter-spacing: var(--track-caps); color: var(--text-faint); }
/* an unsaved change somewhere in this row's ANIME — the row itself only carries the dot */
.anime-dot { color: var(--warn); }
/* ⛔ `button.anime-dirty` is DELETED, not orphaned — it styled the panel's "✎ <anime>" button, and
   that button went on 2026-08-28 when the anime NAME on the row became the way in. The unsaved-change
   signal survives as `.anime-dot`, which is on the row where the name now is. */
/* The anime name in a row: a button, because it opens the anime — but it must not READ as one. The
   row is already a click target for the song, so this stays plain text until you are on it, and the
   underline is what says "this one does something else". */
.lanime-open { background: none; border: 0; padding: 0; margin: 0; font: inherit; color: inherit;
  text-align: left; cursor: pointer; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.lanime-open:hover, .lanime-open:focus-visible { color: var(--accent); text-decoration: underline; }
/* Below its natural width the ledger SCROLLS rather than squashing — honestly desktop-shaped, the
   settled 2026-08-09 decision. Only the ledger scrolls, so the nav, tabs, search and filters above
   it stay usable on a phone (a friend who taps /library from the nav row should still be able to
   get back out). The page body itself never scrolls sideways. */
/* ⚠ SCOPED TO NARROW SCREENS ON PURPOSE (2026-08-17). `overflow-x: auto` cannot stand alone: CSS
   forces the OTHER axis to `auto` as well, so this one line silently made .ledger a scroll container
   — and a `position: sticky` child pins to its nearest scroll container. .ledger is 3000px tall and
   never scrolls itself (the PAGE does), so the editor panel pinned to a scrollport that never moved
   and simply rode off the top. Aur reported it twice; O1–O4 read the computed `position` and stayed
   green, which is why it survived a fix. O5 scrolls and measures instead.
   The 2026-08-09 decision this came from only ever applied to a phone, so it now applies only there:
   wide enough for the pane to fit = no scroll container = the panel sticks. */
@media (max-width: 1100px) { .ledger { overflow-x: auto; } }
.lpane { min-width: 1040px; }

/* ── THE FIXED FRAME (Aur, 2026-08-27: "amqbot style") ────────────────────────────────────────
   The page fills the window and NOTHING scrolls except the row list. The header strip, the filters,
   the group-by strip, the pager and the editor panel all stay where they are while 4,195 rows move
   underneath — which is the whole reason the ledger is paged rather than one long page.

   The height has to reach the scroll box through every wrapper, so each link in the chain gets
   `min-height: 0`: a flex item's default `min-height: auto` refuses to shrink below its content, and
   ONE missing line makes the whole frame grow past the viewport instead of scrolling inside it. The
   chain is  #app → .lbody → .ledger → .lpane → .lmain → .lscroll.

   ⚠ SCOPED TO WIDE SCREENS AND TO THE LIBRARY TAB. Under 1100px the pane is wider than the window,
   and the panel lives OUTSIDE the scroll box — so freezing the body there would put the editor
   off-screen with no way to reach it; narrow screens keep the old page-scrolls behaviour. The tab
   scoping is `body.libframe`, set only while the ledger is showing (Requests / Artists / Tags are
   ordinary long pages).
   ⚠ THE SCROLL BOX IS A SIBLING OF THE PANEL, NEVER ITS ANCESTOR. A `position: sticky` box pins to
   its nearest scroll container, and wrapping the panel in one is exactly the bug that shipped on
   2026-08-17 — the panel pinned to a scrollport that never moved and rode off the top. Here the
   panel does not need sticky at all: it is a full-height grid cell that scrolls inside itself.
   ⚠ AND TO WINDOWS TALL ENOUGH TO HOLD ONE. Under 480px of height the chrome above the rows (nav,
   filters, the group strip) is most of the window, so the frame would leave a pane of nothing and
   push the editor panel off the bottom — measured at 240px, where the panel landed at y=460. Below
   that height the page keeps its old behaviour: the window scrolls and the panel is `position:
   sticky`. That fallback is still live code, and O2/O3 still test it. */
@media (min-width: 1101px) and (min-height: 480px) {
  body.libframe { height: 100vh; overflow: hidden; }
  body.libframe #app { height: 100vh; box-sizing: border-box; display: flex; flex-direction: column; min-height: 0; }
  body.libframe .lbody { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
  /* ⚠ `min-height` IS A FLOOR HERE, NOT ZERO — and the difference is the whole bug of 2026-08-27.
     `.ledger` is not the only child of `.lbody`: `details.deleted-box` ("Recently deleted") is a
     SIBLING after it, and a flex item whose content is 628px tall refuses to shrink below it. The
     ledger, being the one item with `min-height: 0`, absorbed the entire deficit and collapsed to
     **h=0** — then its children (group strip, header, rows, pager, editor panel) spilled out of the
     zero-height box and drew on top of the page. Measured on a fixture carrying Aur's twelve deleted
     songs: ledger 0px, pager overlapping the rows by 88px, panel 30px tall. That is exactly what he
     saw. The floor makes the collapse impossible; the cap below makes it unnecessary. */
  body.libframe .ledger { flex: 1 1 auto; min-height: 260px; display: flex; flex-direction: column; margin-top: var(--sp-8); }
  /* Recently deleted takes what is left, up to a quarter of the window, and scrolls inside itself.
     ⚠ `min-height: 0` is what lets it shrink at all — without it a flex item's min-height is its
     content, which is how it pushed the ledger out of existence. */
  /* ⚠ Recently deleted is a TAB now (2026-08-27) and no longer a sibling of the ledger, so nothing
     here can squeeze it. The rule stays as a guard: if that box — or anything else — is ever put
     back beside `.ledger`, it caps itself instead of collapsing the list to zero height again. */
  body.libframe details.deleted-box { flex: 0 1 auto; min-height: 0; max-height: 26vh; overflow: auto; }
  /* ⚠ EVERY LINE ABOVE THE ROWS IS A ROW HE CANNOT SEE. In a fixed frame the chrome is not "space
     above the content", it is content deleted from the list and the editor — his words: "you give
     the space to unnecessary thing than just the editor". These are the trims, measured. */
  /* ⛔ THE CHROME ABOVE THE CONTENT IS THE SAME ON EVERY TAB (Aur, use pass 4 retest: "i feel like the
     ui or boarder or something aren't the same across each page … and make like the search box line
     up (be as the same spot) as the library's search box").
     ⚠ MEASURED, and it was two separate offsets. `body.libframe #app` sets `box-sizing: border-box`,
     so the Library tab's content box was 1536px starting at x=32 while every other tab was 1568px
     starting at x=16 — a 16px shift of the whole page as you switch tabs. And these two margins were
     scoped to `.libframe`, which put the Artists search box at y=139 against the Library one's y=131.
     Both are now `body.libpage`, so all five tabs share one frame. The rules BELOW stay `.libframe`:
     they trim controls that only the ledger has. */
  body.libpage #app, body.listpage #app { box-sizing: border-box; }
  body.libpage .lib-tabs { margin: 0 0 8px; }
  body.libpage .guessbox { margin: 0 0 8px; }
  body.libframe .lib-filter { margin: 0 0 var(--sp-8); }
    body.libframe .ledger > p.muted { margin: 0; }
  body.libframe .gby { margin: 4px 0 2px; }
  /* stretch, not start: the panel is a full-height cell now, so its own scrollbar has a box to be in */
  body.libframe .lpane { flex: 1 1 auto; min-height: 0; align-items: stretch; }
  body.libframe .lmain { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
  /* BOTH axes: long credit lines and many columns are the sideways case he asked for, and the page
     body must never be the thing that scrolls sideways. */
  body.libframe .lscroll { flex: 1 1 auto; min-height: 0; overflow: auto; }
  /* the column headers ride the scroll box, so they are still there on row 300 */
  body.libframe .lhead { position: sticky; top: 0; z-index: 2; background: var(--bg); }
  /* the panel stops being sticky BECAUSE the frame makes it unnecessary — nothing under it moves */
  body.libframe .ldetail { position: static; max-height: 100%; }
  /* ⚠ THE SAVE BAR OWNS THE BOTTOM OF THE WINDOW, and in a fixed frame `body { padding-bottom }`
     cannot answer it — the frame is 100vh tall regardless, so the bar simply lands ON the pager and
     the panel's last credit. The frame gives the bar its own 76px by being that much shorter. The
     body padding stays (Q0 reads it, and the fallback layout below 480px still needs it). */
  body.libframe.saving #app { height: calc(100vh - 76px); }
}
/* ⚠ ONE CONTROL ROW (Aur, 2026-08-28: "these now take the editor space"). The three rows — modes,
   the two narrowings, the tag filter — are now one line: exclusive modes, then the narrowings, then
   a growing spacer, then the two MENUS at the right end. Columns has sat right since his 2026-08-09
   use pass because it changes what the table shows rather than which rows match; Filter tags… joins
   it for the same reason. The push is the spacer, NOT `margin-left: auto` on `.checkfilter` — with
   two of them in the row that rule would have pushed them apart instead of grouping them. */
/* Nothing inside the panel may be wider than the panel. `min-width: auto` is a flex/grid item's
   default, so the reused form's blocks (a 180px input, a hint paragraph, a credit row) each refused
   to shrink and pushed a sideways scrollbar under the whole thing — with the sentence that names
   the anime cut off mid-word, which is the one sentence that has to be readable. */
.ldetail .lib-edit > *, .ldetail .lib-credits > *, .ldetail .row { min-width: 0; max-width: 100%; }
.ldetail p, .ldetail .hint, .ldetail .sub, .ldetail label { overflow-wrap: anywhere; }
.ldetail .lib-edit textarea { min-width: 0; }

/* ── Your picture: the square on the /list identity row, and the flow that sets it ───────────────
   ⛔ THE SQUARE IS THE AFFORDANCE. There is no separate "change picture" button — the picture is the
   control, which is what every app he already uses does. It sits on the NAME row and not on the
   `Link a device` / `⇄ Not you?` row: those two are about which PERSON this device is, and this is
   about who that person is.
   ⚠ The generated square stays UNDERNEATH the <img> rather than being replaced by it, so a picture
   that fails to load (a file deleted by hand, a half-finished restore) falls back to the coloured
   letter instead of a broken-image glyph. */
/* ⛔ A GREY WASH OVER THE WHOLE SQUARE WITH THE PEN IN THE MIDDLE — his call 2026-09-07, refining
   the corner mark I built first: "what i actually want is a filter(normally gray) even hovers on it
   and the pen in the middle." The wash is what makes it read as "this is editable" rather than as a
   decoration stuck to one corner.
   ⚠ Hover is the desktop half only. On touch it sits at a low opacity permanently, because a phone
   has no hover and would otherwise get no hint the square does anything — the rule
   .artist-fix-hint had to learn, and add-check A10 guards its original. */
@media (hover: hover) {
      }

.picflow { position: fixed; inset: 0; z-index: 70; background: rgba(9, 8, 16, 0.72);
           display: flex; align-items: center; justify-content: center; padding: var(--sp-16); }
.picbox { width: 100%; max-width: 340px; box-sizing: border-box; background: var(--surface);
          border: 1px solid var(--border); border-radius: var(--r); padding: var(--sp-16);
          max-height: calc(100vh - 32px); overflow-y: auto; }
/* ⛔ THE WHOLE PANEL IS CENTRED — his call: "move everything center aligned(header,choosefile+name,
   save+cancel)". The stage was already centred and everything around it was not, which is what made
   it look misaligned rather than simply left-aligned. */
.picbox { text-align: center; }
.pictitle { font-size: var(--fs-4); margin: 0 0 var(--sp-12); text-align: center; }
/* ⛔ THE STAGE IS THE SQUARE, AT REST AND FULL. His call 2026-09-07: "make the profile edit box
   starts at the same size as when uploaded (the square)". It used to be a 96px-tall strip that jumped
   to a 256px square the moment a file was chosen, so the panel resized under his cursor.
   ⚠ 256px is the STORED size, so what he approves is drawn at the size it is kept at — no scaling
   between the preview and the file. */
.picstage { display: flex; align-items: center; justify-content: center;
            width: 256px; height: 256px; max-width: 100%; box-sizing: border-box; margin: 0 auto var(--sp-12);
            background: var(--bg); border: 1px solid var(--border-subtle);
            border-radius: var(--r); padding: var(--sp-8); }
/* ⛔ NO WIDTH AND NO HEIGHT ON THE PREVIEW, and that is the assertion browse-check Z4 rests on. If
   CSS forced the box square, a preview built from the wrong bytes would still LOOK square and the
   check would pass while showing him something the upload does not match. The shape comes from the
   blob — which is the same blob that gets posted. */
.picstage img { display: block; max-width: 100%; height: auto; border-radius: var(--r); }
.picempty { color: var(--text-faint); font-size: var(--fs-2); }
/* ⛔ THE NATIVE "Choose File" BUTTON IS UNREADABLE ON THIS GROUND and he screenshotted it. Chrome
   paints that button with the UA's own light chrome, which on a #090810 page is grey-on-grey. The
   ::file-selector-button pseudo-element is the only way to reach it, so it gets the same treatment as
   any other minor button here. */
/* `width: auto` + inline-block so the control is only as wide as its content and can be centred by
   the parent's text-align — a 100%-wide file input pins its button to the left edge whatever the
   alignment says. */
/* ⛔ THE WHOLE CONTROL CENTRES, BUTTON AND FILENAME TOGETHER. `display: inline-block` centred the
   ELEMENT, but a file input lays its button and its "No file chosen" text out inside itself and they
   kept sitting at the left of that box — which is what he screenshotted. A flex wrapper around it is
   the only way to centre the pair, since the text is UA shadow content and cannot be styled directly. */
.picfilewrap { display: flex; justify-content: center; margin: 0 0 var(--sp-8); }
/* ⛔ AND IT WAS STILL OFF-CENTRE — his report 2026-09-07, "the 'choose file' here is also not middle
   aligned", against a rule already claiming to have fixed it. The flex wrapper centred the INPUT, and
   a file input lays out as [button][No file chosen], so the PAIR was centred and the button sat left
   of centre by half the width of that text. Centring the element was never the same as centring the
   thing you can see, and the text is UA shadow content — it cannot be selected, sized or removed.
   ⛔ SO THE NATIVE CONTROL IS GONE FROM THE PAGE and a real <label> opens it. A label styled as a
   button is centred by the same rule as every other button here, which is the actual fix; collapsing
   the filename with `font-size: 0` also worked and put an off-scale type size in the stylesheet,
   which test/visualsystem.test.js refuses — correctly, since that guard is what keeps the type ladder
   meaning anything.
   ⚠ `display: none` and not an off-screen box: Playwright drives a hidden input by `set_input_files`
   without needing it visible, and an off-screen 1px input is two more off-scale lengths. */
.picfile { display: none; }
.picfilebtn { display: inline-block; padding: var(--sp-4) var(--sp-12); cursor: pointer;
  background: var(--surface-raised); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r); font-size: var(--fs-2); }
.picfilebtn:hover { border-color: var(--accent); }
.picmsg { margin: var(--sp-8) 0 0; font-size: var(--fs-2); }
/* ⛔ EVERY BUTTON IN THIS ROW IS ONE SIZE. His question was fair — "what makes you always create
   diffrent sized buttons?" — and the answer is that `.primary` and `.minor` carry different padding,
   so Save came out narrower than Cancel purely by label length. A shared floor fixes it without
   touching either class anywhere else. */
.picacts { justify-content: center; gap: var(--sp-8); }
.picacts button { min-width: 96px; margin: 0; }
/* ══ THE ROUND SCREEN ═══════════════════════════════════════════════════════════════════════════════
   docs/superpowers/specs/2026-09-06-game-screen-design.md §2, and the shape in
   public/game-ui-mockup.html. Every number below is measured on a 1920x900 target — a MAXIMISED
   browser with a bookmarks bar, never F11 fullscreen, because only Aur plays fullscreen.

   ⛔ EVERY RULE IS SCOPED TO `#app.gameflow`, AND THAT IS NOT BELT-AND-BRACES — a bare `.gs` collides
   with two pages that already serve this stylesheet. `library-mockup.html` emits `class="gs ell"` on
   a small grey line (`.ghead .gs`), and `game-ui-mockup.html` builds its whole round out of `.gs`
   with its own complete copy of these rules in a <style> block. A bare `.gs` would have reached both.
   ⚠ `#app.gameflow` is set by render.js only for the round and the end screen, so /list, /library and
   /add cannot reach any of this — and neither can either mockup, which is what keeps the mockup an
   independent record rather than something this file quietly restyles.
   ⛔ THE 40px CARD GAP AND THE 10px ABOVE THE ANSWER ARE WRITTEN AS calc() OF SCALE STEPS ON PURPOSE.
   Neither is on the spacing scale, and inventing a 40px step would make a one-off measurement look
   like a system step. The arithmetic is the honest form: 40 = 16 + 24, 10 = 12 - 2.
   ⛔ THE SONG BOX STAYS FIXED AT ITS WORST CASE AND THE VOID IS INTENDED — his call. A normal song
   needs 179 of the 451; he looked at the empty 270 and kept it ("the void is fine … else cutting the
   height would make it looks short"). Do not make it content-sized. */
#app.gameflow { max-width: min(1720px, calc(100vw - 32px)); box-sizing: border-box; }

/* The lobby is a full-width screen like the round, not a 560px column — the seats are a grid, and a
   narrow container collapses it to one card per row on a desktop. Same mechanism as .gameflow, so
   there is one way a screen goes wide rather than two. */
#app.lobby { max-width: min(1720px, calc(100vw - 32px)); box-sizing: border-box;
             padding: var(--sp-8) var(--sp-16); }
/* ⛔ RESERVE THE EDGE RAIL. `.edge-right` (📜 ⚙ ☺) is `position: fixed` against the right of the
   VIEWPORT, so it does not participate in layout and nothing pushes away from it — measured at
   375px it sat from 305 to 360 straight over the seat card's song COUNT, which is the one number
   that card exists to show. ⚠ It overlapped the old 560px lobby too and was simply less obvious
   there; a full-width screen makes it unmissable. 55px of rail plus a gap, on the lobby only —
   the round has its own layout and its own answer to this. */
/* ⛔ SUPERSEDED BY THE ROUND PORT: the rail moved into the lobby's top row, so there is nothing to reserve.
   And the lobby runs out to the SAME EDGE as the round's ⌂ and volume slider (his use pass 2026-09-23: "other
   pages need extending to the same edge as the home and volume slider") — the round's 24px screen edge. */
#app.lobby { --edge: clamp(10px, 1.25vw, 24px); max-width: none; padding: var(--sp-8) var(--edge); }

/* the narrow shape, and the base every wide rule is written over: one column, boxes full width. The
   phone and the iPad are NOT targets for this screen (his call), so this is an honest fallback rather
   than a second design. */
#app.gameflow .gs { --songbox-h: 451px; --midw: 824px; position: relative; display: flex; flex-direction: column;
      gap: var(--sp-8); }

/* ── the title block: the count tab welded to the plate, no seam ────────────────────────────────── */
#app.gameflow .gs .gs-plateblock { display: flex; flex-direction: column; align-items: center; gap: 0; }
/* the tab overlaps the plate's top border by exactly one pixel and carries no bottom edge of its own,
   so the two read as ONE shape. Without the pull the plate's border draws a line under the tab. */
#app.gameflow .gs .gs-tabwrap { display: flex; flex-direction: column; align-items: stretch; width: 206px;
                  max-width: 100%; margin-bottom: calc(-1px); position: relative; z-index: 1; }
/* the tab is always a NUMBER — `5/8` in a round, the countdown at the end — so it takes the numbers
   face at every phase. ⚠ Bebas is tabular (all ten digits 40px at 100px, measured 2026-09-09), which
   is what stops a count changing in place from shivering. */
#app.gameflow .gs .gs-tab { background: var(--l2); border: 1px solid var(--rule); border-bottom: 0;
              border-radius: var(--r); padding: 0 var(--sp-24); text-align: center;
              height: 40px; display: flex; align-items: center; justify-content: center;
              font-family: var(--font-numbers); font-weight: 400; letter-spacing: var(--track-tight);
              font-size: var(--fs-4); color: var(--text-muted); font-variant-numeric: tabular-nums; }
#app.gameflow .gs .gs-tab.counting { color: var(--text); }
/* the MATCH bar belongs to the COUNT — it is the same number — so it lives under the tab and never
   beside the song's timer. Two bars that are two different numbers can then never read as one. */
#app.gameflow .gs .gs-progress { height: 6px; background: var(--surface-raised); overflow: hidden; box-sizing: border-box;
                   border-left: 1px solid var(--border-subtle); border-right: 1px solid var(--border-subtle); }
#app.gameflow .gs .gs-progress i { display: block; height: 100%; background: var(--text-faint); opacity: .85; }
/* FIXED BOX, SHRINKING TYPE: a 100-character anime title must not resize the middle of the screen.
   fitPlate() sets the real size (28px down to 11px); this is only the resting value. */
/* ⛔ THE PLATE TAKES THE NAMES FACE, and this is the decision the end-screen conversion deliberately
   deferred to this surface: the box draws the ANIME TITLE, which is typed data, so the display face
   may never touch it (Bebas has no lowercase — the artist-name rule). Teko has real lowercase and is
   the face his own use-pass put on every name. The one chrome string it ever holds ("Returning to
   lobby", at the end) rides along, which is the right way round: the face follows the box's job, and
   this box's job is names. */
#app.gameflow .gs .gs-plate { background: var(--l2); border: 1px solid var(--rule);
                border-radius: var(--r); width: var(--midw); max-width: 100%; height: 76px;
                padding: var(--sp-4) var(--sp-16); box-sizing: border-box; display: flex;
                align-items: center; justify-content: center; text-align: center;
                font-family: var(--font-names); font-weight: 500;
                font-size: var(--fs-6); line-height: var(--lh-solid); overflow: hidden; }
#app.gameflow .gs .gs-plate.unknown { color: var(--text-faint); }

/* the ladder: the song box and the extra box are FIELDS the round reads from, so they sit one step
   above the ground with a hairline rather than a pale border on nothing. */
#app.gameflow .gs .song-info { background: var(--l1); border: 1px solid var(--rule); border-radius: var(--r);
                 box-sizing: border-box; display: flex; flex-direction: column; overflow: hidden; }

/* ⛔ A LABEL IS CHROME AND A VALUE IS DATA, AND THAT IS THE WHOLE TYPE SPLIT ON THIS BOX. `SONG NAME`
   and `CREDITS` are words we wrote, so they take the display face — already uppercase, so Bebas
   having no lowercase costs nothing. What sits under them was typed by a person. */
/* a song title is a NAME → the names face. ⚠ The credit line under it is NOT: his role table puts
   "credits, artists, everything read" on the body face, and a credit line is the one place on this
   screen that runs to several names, punctuation and a `feat.` — it is read, not glanced at. */

/* what you have already earned this round reads as EARNED, not as the reveal */

/* the roles are TEXT, never boxes (his screenshot), and they stand in the label row's place — same
   size, same caps, same tracking — because the row they replace IS a label row */
/* ⚠ `font: inherit` RESETS THE FAMILY TOO, so the display face has to be named after it or these
   would inherit the box's names face and read as data rather than as the controls they are. */

/* ── the song timer, welded to the box's bottom edge ───────────────────────────────────────────────
   ONE bar in TWO colours (his call): the blue runs the audio window, the amber is the extra typing
   time. "You are into extra time" is the bar changing colour, not a second bar appearing.
   ⚠ 25px of air above it = the 25 above the Song Name label; the negative margins are what pull it
   out through the box's own padding so it spans the full width and touches the bottom edge. */

#app.gameflow .gs .timerbar.gs-bar { display: flex; height: 8px; margin: 0; border-radius: var(--r);
                       background: var(--surface-raised); }
/* ⛔ LAYERED, NOT SIDE BY SIDE (his call 2026-09-07, the boss-health-bar model). Both fills are
   absolutely positioned at the SAME origin and each is full width, so orange sits behind at 100%
   while blue drains over it. `display: flex` would put them next to each other again — which is
   exactly the bar he saw and did not recognise — so the bar is a positioning context instead. */
#app.gameflow .gs .timerbar.gs-bar { position: relative; overflow: hidden; }
#app.gameflow .gs .timerbar.gs-bar .fill { position: absolute; left: 0; top: 0; height: 100%; }
#app.gameflow .gs .timerbar.gs-bar .fill.extra { background: var(--warn); opacity: .85; z-index: 1; }
#app.gameflow .gs .timerbar.gs-bar .fill.main { background: var(--accent-fill); z-index: 2; }

/* ⛔ THE BOX A PERSON TYPES INTO TAKES THE NAMES FACE, NEVER THE DISPLAY ONE. What goes in it is an
   anime or a song title, and Bebas would uppercase it silently as he types — the artist-name rule,
   at the one keystroke where it would be most obvious. Teko has real lowercase; Thai falls through
   to Prompt per character, which needs no rule of its own. */
#app.gameflow .gs .guessbox input { font-family: var(--font-names); font-weight: 400;
                      background: var(--l2); border-color: var(--rule); }
#app.gameflow .gs .dropdown { background: var(--l3); border-color: var(--rule); }
#app.gameflow .gs .dropdown li { font-family: var(--font-names); }
/* the count on Skip is a number; the word beside it is chrome */
/* Skip beside the box, on its left — game-ui-mockup.html:977, "Skip sits to the LEFT of the box". */
#app.gameflow .gs .gs-side { display: flex; align-items: stretch; flex: 0 0 auto; }
/* ⛔ THE SKIP BUTTON DOES NOT CHANGE SIZE WHEN PRESSED — his call, and he worked out himself why a
   text swap cannot do it: the count has to stay visible so you can see other people's votes. So the
   count's slot is always there and only its CONTENT appears, which keeps the width fixed either way. */

/* the anime's other names, hanging off the title plate (AMQ-style). Hover on a desktop, tap anywhere
   — the plate is a real button, because a phone has no hover at all.

   ⛔ A PANEL TO THE SIDE, AND IT MOVES NOTHING — his call 2026-09-07, from a screenshot: "a panel to
   the SIDE of the title with two headed sections, and it must not push anything else on the screen
   around." It was a block UNDER the plate, in normal flow, inside a `flex-direction: column` — so
   opening it pushed the song box, the answer box and the cards down the screen, and in the wide grid
   it GREW the `auto` row track the plate sits in, shifting the whole layout.
   ⚠ `position: absolute` is what enforces that, not a promise in a comment: an absolutely-positioned
   box is out of flow and sizes no grid track at all. Same mechanism `.gs-extra` uses ten rules down,
   and for the same reason.
   ⚠ IT NEEDS `position: relative` ON `.gs-plateblock`, which it did not have — the nearest positioned
   ancestor was `.gs`, so an absolute panel would have hung off the whole screen instead of the title.
   ⚠ AND THE PLATE'S WIDTH GOES BACK TO `--midw`. `.hasalso` was overriding it to `width: 100%`
   (5 classes beating 4), so the title plate silently got WIDER on any song that had other names —
   the layout moving with the DATA, which is the same complaint one level down. */
#app.gameflow .gs .gs-plateblock { position: relative; }
/* ⛔ `font: inherit` IS A SHORTHAND AND IT RESETS THE FAMILY, THE SIZE AND THE WEIGHT. It is here to
   kill the UA button font, because the plate becomes a <button> when the anime has other names — and
   it silently took the names face straight back off the title again, on exactly the anime that have
   alt names. Measured: the plate read Prompt while every other element on the screen had converted.
   The three properties the plate actually wants are restored after it. (fitPlate sets the real size
   inline; this is only the resting value, same as the base rule.) */
#app.gameflow .gs .gs-plate.hasalso { cursor: help; border: 0; font: inherit; width: var(--midw);
                font-family: var(--font-names); font-weight: 500; font-size: var(--fs-6); }
/* ⛔ IT EXTENDS, IT DOES NOT SCROLL — his call 2026-09-07: "the box should extend instead of making it
   a breakdown. overlapping on top of other boxes is fine for this one. since it's only when you
   hover." A capped box turns a long list into a scroller, which is the breakdown he meant.
   ⚠ AND THE CAP WAS EATING THE PAGE SCROLL. `max-height: 100%` + `overflow-y: auto` made the panel its
   own scroll container, so a wheel with the cursor over it moved the PANEL and the page stayed still —
   his report: "you can't scroll when you cursor is hovering on the title". One line fixed the shape
   and the scroll, which is how you can tell they were the same bug.
   ⚠ Overlap is fine HERE and only here: it appears on hover and vanishes again, so it is never on top
   of something you were trying to use. */
#app.gameflow .gs .gs-also { position: absolute; top: 0; left: calc(100% + var(--sp-16)); z-index: 3;
  width: 300px; padding: var(--sp-12);
  background: var(--surface-raised); border: 1px solid var(--border); border-radius: var(--r);
  font-size: var(--fs-2); text-align: left; box-sizing: border-box; }
#app.gameflow .gs .gs-also-sec + .gs-also-sec { margin-top: var(--sp-12); }
#app.gameflow .gs .gs-also-h { margin: 0 0 var(--sp-4); font-size: var(--fs-1); font-weight: 500;
  color: var(--text-faint); text-transform: uppercase; letter-spacing: var(--track-caps); }
#app.gameflow .gs .gs-also-list { margin: 0; padding: 0; list-style: none; }
#app.gameflow .gs .gs-also-list li { padding: var(--sp-2) 0; }
/* ⚠ BELOW THE WIDE GRID THERE IS NO SIDE TO SIT ON — the plate is centred in a single column and a
   panel 300px to its right would be off the phone entirely. It centres OVER what follows instead:
   still absolute, so it still pushes nothing, which is the half of his rule that has to survive. */
@media (max-width: 1499px) {
  #app.gameflow .gs .gs-also { top: calc(100% + var(--sp-4)); left: 50%; transform: translateX(-50%);
    width: min(300px, 100%); max-height: none; }
}
@media (hover: hover) {
  #app.gameflow .gs .gs-plateblock:hover .gs-also[hidden] { display: block !important; }
}

/* Return is a vote strip UNDER the scoreboard — the mockup's .gs-vote, designed and never built. */
/* ⛔ THE VOTE SITS IN THE LEFT COLUMN, IN THE ANSWER'S ROW — i.e. UNDER the scoreboard, which is
   what he asked for and what the mockup's own grid says ("vote answer extra"). Appending it after
   the scoreboard in the DOM was NOT enough: this is a named-area grid, so DOM order says nothing
   about where a child lands, and it drew ABOVE the board while my assertion — which compared DOM
   rectangles — passed. The screenshot is what caught it. */
#app.gameflow .gs .gs-vote { grid-area: vote; align-self: start;
  display: flex; align-items: stretch; margin: var(--sp-8) 0 0;
  border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; font-size: var(--fs-2); }
#app.gameflow .gs .gs-vote.voted { border-color: var(--good); }
#app.gameflow .gs .gs-vote .cast { padding: 0 var(--sp-8); color: var(--text-muted);
  font-variant-numeric: tabular-nums; }

#app.gameflow .gs .sa-readout { margin-top: var(--sp-8); }
#app.gameflow .gs .picked { margin: var(--sp-8) 0 0; }

/* ── the player cards ──────────────────────────────────────────────────────────────────────────────
   The card is a media object: [avatar] [name · score · +score] over [the answer]. His edit: the three
   text lines are taller than a square hanging from the top, so the square STRETCHES to the block's
   height and the two end together. */
#app.gameflow .gs .gs-cards { display: flex; flex-direction: column; }
/* ⛔ THE CARD'S SURFACE AND FACES ARE THE SAME ON EVERY PHASE, and that is why they moved out of the
   `.endview` block the end-screen conversion put them in: the card is one object the round, the
   reveal and the end screen all draw. A field is lighter than what holds it; the 1px box stays a box
   because the medal rules colour that border. */
#app.gameflow .gs .pcard { flex: 1 1 46%; min-width: 0; box-sizing: border-box; min-height: 86px; position: relative;
             background: var(--l2); border: 1px solid var(--rule); border-radius: var(--r);
             padding: var(--sp-4) var(--sp-12); display: grid; align-content: center;
             grid-template-columns: auto minmax(0, 1fr) auto auto; grid-template-rows: auto auto;
             gap: 0 var(--sp-8); }
#app.gameflow .gs .pcard .pav { grid-column: 1; grid-row: 1 / span 2; align-self: stretch; width: auto; height: auto;
                  aspect-ratio: 1; border-radius: var(--r); display: flex; align-items: center;
                  justify-content: center; font-size: var(--fs-5); font-weight: 500;
                  color: var(--bg); margin-right: var(--sp-8); }
/* The uploaded picture. It fills .pav rather than replacing it, so the box keeps the geometry the
   round and the end screen were measured at. `overflow: hidden` is what makes the radius apply to the
   image and not just to the coloured square underneath, and `position: relative` gives the img
   something to be absolute against so the letter keeps centring the square when there is no picture.
   ⚠ object-fit: cover even though the stored file is already 256x256 square — a hand-placed file or a
   future size change must never stretch a face. */
#app.gameflow .gs .pcard .pav { position: relative; overflow: hidden; }
/* the letter goes away only while a picture is actually showing; render.js drops .haspic on an
   image error, so a broken src falls back to the square and its initial rather than an empty box */
#app.gameflow .gs .pcard .pav.haspic { color: transparent; }
#app.gameflow .gs .pcard .pav > img { position: absolute; inset: 0; width: 100%; height: 100%;
                  object-fit: cover; display: block; }
#app.gameflow .gs .pcard .pname { grid-column: 2; grid-row: 1; font-family: var(--font-names);
                    font-weight: 500; font-size: var(--fs-3);
                    align-self: baseline; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#app.gameflow .gs .pcard .prank { font-family: var(--font-numbers); letter-spacing: var(--track-tight);
                    font-variant-numeric: tabular-nums; color: var(--text-faint); margin-right: var(--sp-8); }
#app.gameflow .gs .pcard .plp { font-family: var(--font-numbers); letter-spacing: var(--track-tight);
                  color: var(--text-faint); font-size: var(--fs-1); font-weight: 400;
                  font-variant-numeric: tabular-nums; }
#app.gameflow .gs .pcard .pscore { grid-column: 3; grid-row: 1; font-family: var(--font-numbers);
                     letter-spacing: var(--track-tight);
                     font-size: var(--fs-3); text-align: right;
                     font-variant-numeric: tabular-nums; align-self: baseline; }
/* the delta sits AGAINST the score, not a column away — his edit, "8+1" rather than "8      +1" */
#app.gameflow .gs .pcard .pdelta { grid-column: 4; grid-row: 1; font-family: var(--font-numbers);
                     letter-spacing: var(--track-tight);
                     font-size: var(--fs-1); color: var(--good);
                     text-align: right; align-self: baseline; margin-left: calc(-1 * var(--sp-4)); }
#app.gameflow .gs .pcard.half .pdelta { color: var(--warn); }
/* ⚠ THE RESERVE IS 47 PIXELS AND IT IS FIXED: fitGuess() shrinks the type 18px -> 12px to fit it, and
   what still does not fit is cut at the card's edge. A long song answer must never push the artist
   half out of the card — that failure showed no name, no tick, no cross and nothing saying so. */
/* ⚠ THE ANSWER LINE IS TYPED DATA — it is literally what a player wrote — so it takes the names face
   and can never take the display one. `fitGuess()` sets an inline px size on this element, so the
   family is the only thing this rule contributes to its type. */
#app.gameflow .gs .pcard .pguess { grid-column: 2 / -1; grid-row: 2; height: 47px; overflow: hidden;
                     font-family: var(--font-names);
                     font-size: var(--fs-3); line-height: var(--lh-ui);
                     display: flex; flex-direction: column; }
#app.gameflow .gs .pcard .pguess.none { color: var(--text-faint); }
/* ⛔ THE TWO S/A HALVES SPLIT THE RESERVE EVENLY, and that is the rule "never let a long answer hide
   the artist half" made structural rather than hoped for. Each half is clipped inside its own share,
   so a three-line song answer runs out of ITS half instead of eating the artist's. */
#app.gameflow .gs .pcard .pguess > * { min-height: 0; overflow: hidden; }
#app.gameflow .gs .pcard .pguess > .h-guess-half { flex: 1 1 0; }
#app.gameflow .gs .pcard .pguess > .pline { flex: 1 1 auto; }
/* the half lines are plain blocks inside the card, NOT the indented rows the Song Info panel draws.
   ⚠ TWO THINGS HERE ARE DELIBERATE AND BOTH WERE WRONG ONCE. Their size stays RELATIVE (`em`), or
   fitGuess's inline px on `.pguess` never reaches them and nothing shrinks. And they must be allowed
   to WRAP: `white-space: nowrap` kept each half to one line, which meant the reserve was never
   overrun, fitGuess never fired, and a 78-character answer read as 30 characters and an ellipsis at
   full size. Wrapping is what gives the fitter something to fit. */
#app.gameflow .gs .pcard .pguess .h-guess-half { display: block; padding-left: 0; font-size: 0.85em;
                                   color: var(--text-muted); overflow-wrap: anywhere; }
#app.gameflow .gs .pcard .pguess .h-guess-half.got { color: var(--good); }
#app.gameflow .gs .pcard .pguess .h-guess-half.part { color: var(--warn); }
#app.gameflow .gs .pcard .pguess .hlab { color: var(--text-faint); text-transform: uppercase;
                           letter-spacing: var(--track-caps); }
#app.gameflow .gs .pcard .pguess .mark { color: var(--text-faint); }
#app.gameflow .gs .pcard .pguess .mark.ok { color: var(--good); }
#app.gameflow .gs .pcard .pguess .mark.no { color: var(--bad); }
/* THE CARD IS THE RECEIPT: accent = locked in · amber dashed = an S/A half is in · nothing = not
   answered. At the reveal the border becomes the verdict instead. */
#app.gameflow .gs .pcard.sent { border-color: var(--accent); }
#app.gameflow .gs .pcard.partial { border-color: var(--warn); border-style: dashed; }
#app.gameflow .gs .pcard.hit { border-color: var(--good); }
#app.gameflow .gs .pcard.half { border-color: var(--warn); }
/* ⛔ WRONG IS RED, and grey is reserved for "did not answer" — his call 2026-09-07. A miss was only
   dimmed, so a wrong answer and a silent player looked the same at a glance, which is the one
   distinction the card exists to make. */
#app.gameflow .gs .pcard.miss { border-color: var(--bad); }
/* a seat that has left the room. Dimmed and desaturated, so it reads as absent rather than as a
   verdict — the verdict colours are all on the border. */
#app.gameflow .gs .pcard.dc { opacity: 0.45; filter: grayscale(0.7); }

/* ── the end of a game ─────────────────────────────────────────────────────────────────────────────
   Two ranked columns of picture-led cards, 1-4 left and 5-8 right. A podium was built and rejected in
   favour of this; columns also need no special case for a tie, where the podium needed an empty-slot
   placeholder to stay centred.

   ⛔ CONVERTED TO THE VISUAL SYSTEM 2026-09-09 — surface 2 of the conversion plan, and EVERY RULE IS
   SCOPED TO `.endview`. The plate, the count tab and the card itself are SHARED with the reveal and
   the round, which are surfaces 3 and 4 and have not been converted: an unscoped rule here would
   silently convert two screens Aur has not looked at yet. When those surfaces land, the scoping
   dissolves into them rather than being deleted.
   ⚠ THE FACE ON THE PLATE IS DELIBERATELY NOT TOUCHED. During a round that same box draws the ANIME
   TITLE — typed data — so which face it takes is the reveal's decision, not this screen's. Only its
   surface is brought onto the ladder here, because a #35343f plate over #1d1c23 cards reads as two
   different systems on one screen. */
/* ⛔ THE SCREEN HAS TO HAVE A HEIGHT OR NOTHING CAN BE CENTRED IN IT. `.gs` is a flex column at every
   width, but its height came from its content everywhere below 1500px — so `.end-body`'s `flex: 1 1
   auto` had no slack to take and the phone drew the whole end screen clumped into its top 410px with
   430px of nothing under it. The wide layout already carried this line inside its own media query;
   the narrow one never got it. ⚠ `dvh`, not `vh` — on a phone the address bar is part of the
   viewport `vh` reports and the screen would jump as it retracts. */
#app.gameflow .gs.endview { min-height: calc(100dvh - 32px); }
/* ⚠ NO GUTTER OF ITS OWN. One was added here and measured: it put the cards at x = 48 on a 390px
   phone while the title plate above them sat at x = 32, because the screen's own padding was already
   there and the two stacked. The cards line up with the plate now — the gutter belongs to the
   CONTAINER, which is the same lesson the lobby port paid for with `100vw`. */
#app.gameflow .gs.endview .end-body { flex: 1 1 auto; min-height: 0; width: 100%; display: flex;
                        align-items: center; justify-content: center; }
/* ⚠ `max-width: 100%` on the CARD could never bite, and that is why the phone shipped with cards
   running off both edges (measured 2026-09-09: x = -1, width 392, in a 391px viewport). Percentage
   max-widths resolve against the parent, and a nowrap flex column sized by its content IS 392 wide —
   so the card was asking permission from a box it had itself set. The clamp has to be here, and
   `min-width: 0` is what lets the column shrink below its content at all. */
/* ⚠ AND THE CLAMP HAS TO BE ON EVERY BOX IN THE CHAIN, not only the outer one — a flex item's
   `min-width` defaults to AUTO, which means "never smaller than my content", so a single unconstrained
   wrapper hands the whole 392px straight back down. `.gs-cards` is that wrapper here, and with it
   left out the cards still measured x = -1 after the other two clamps were in place. */
#app.gameflow .gs.endview .gs-cards { min-width: 0; max-width: 100%; }
#app.gameflow .gs.endview .end-cols { display: flex; gap: calc(var(--sp-32) + var(--sp-24));
                        align-items: flex-start; max-width: 100%; }
#app.gameflow .gs.endview .end-col { display: flex; flex-direction: column; gap: var(--sp-24);
                       min-width: 0; }
/* ⚠ THE CARD'S SURFACE, ITS FACES AND THE COUNT TAB'S FACE USED TO BE DUPLICATED HERE. They moved to
   the round's own rules when surface 3/4 landed, because the card and the tab are ONE object drawn on
   three phases — two copies of the same decision is how the ⚙ knob and its track got merged without
   anything noticing. What stays below is only what is true of the END and nowhere else.
   ⚠ The end card's name is weight 600 against the round's 500: at --fs-4 with no answer line under
   it, the name is the card's subject rather than its label. */
#app.gameflow .gs.endview .pcard .pname { font-weight: 600; }
/* ⛔ THE UNIT IS NOT IN THE NUMBERS FACE, AND IT CANNOT BE: Bebas has no lowercase glyphs and does
   not fall back for them (the codepoints are mapped to capital forms), so `8s` drew as `8S` the
   moment the tab took the numbers face — the artist-name rule again, in miniature. The digits keep
   Bebas because they count down in place and Bebas is tabular; the `s` takes the names face, which
   has real lowercase. */
#app.gameflow .gs.endview .gs-tab .unit { font-family: var(--font-names); font-weight: 500; }
#app.gameflow .gs.endview .gs-tab > i { font-style: normal; }
/* ⚠ THE PLATE'S SURFACE ALSO MOVED OUT OF HERE. The plate and the tab are welded into one shape (see
   the title-block section above), so they must sit at the SAME level of the ladder — a lighter tab
   over a darker plate breaks the weld the seam was removed to create. Both are `--l2` on every phase
   now, so the end screen needs no rule of its own. */
/* ⚠ flex basis follows the container's DIRECTION: a card's row-layout basis becomes a HEIGHT the
   moment it sits in a column, which is why this is spelled out rather than inherited. */
#app.gameflow .gs.endview .end-col .pcard { flex: 0 0 auto; width: 392px; max-width: 100%; min-height: 124px; }
/* his call: at the end the PICTURE is the card. The answer line is gone by then, so the space goes to
   the face instead of to empty rows. */
#app.gameflow .gs.endview .pcard .pav { width: 100px; height: 100px; aspect-ratio: auto; align-self: center;
                          font-size: var(--fs-6); }
#app.gameflow .gs.endview .pcard .pname { font-size: var(--fs-4); }
#app.gameflow .gs.endview .pcard .pscore { font-size: var(--fs-5); }
#app.gameflow .gs.endview .pcard .prank { font-size: var(--fs-4); } /* = the name, as on the scoreboard (his, 2026-09-25) */
/* with no answer row the name and the score span both rows and sit level with the middle of the
   picture, instead of clinging to the top of the card */
#app.gameflow .gs.endview .pcard .pname, #app.gameflow .gs.endview .pcard .pscore { grid-row: 1 / -1; align-self: center; }
/* ⚠ THE MEDALS ARE THE ONLY COLOURS OUTSIDE THE CLOSED PALETTE, and they are his ask. Gold, silver
   and bronze have no token because nothing else on any page is ranked. */
#app.gameflow .gs .pcard.medal1 .pname { color: #e8c15a; }
#app.gameflow .gs .pcard.medal2 .pname { color: #c9ccd6; }
#app.gameflow .gs .pcard.medal3 .pname { color: #cd8f5e; }
#app.gameflow .gs .pcard.medal1 { border-color: #e8c15a; }
#app.gameflow .gs .pcard.medal2 { border-color: #9aa0ad; }
#app.gameflow .gs .pcard.medal3 { border-color: #a3703f; }

/* ── THE END SCREEN AT >= 1500px ──────────────────────────────────────────────────────────────────
   ⛔ `.gs.gsround` IS NOT DEAD: the end screen's root is `gs gsround endview` (render.js). The old
   round's wide GRID that used to sit here (and its .gs-plateblock / .gs-cards / .pcard children) was
   cut 2026-09-25 — measured on the end screen at 1920, 1600 and 1500: removing those four rules moved
   0 elements. THIS rule is still live (removing it opens an 8px gap and shifts the body 8px). */
@media (min-width: 1500px) {
  #app.gameflow .gs.gsround.endview { display: flex; justify-content: flex-start; gap: 0;
                        min-height: calc(100dvh - 32px); }
}

/* ================================================================================================
   THE LOBBY — ported from /lobby-design.html, the mockup Aur settled on 2026-09-07.
   ⛔ THE MOCKUP IS THE RECORD AND IT IS NOT MAINTAINED. It still stands in the old Anton/Manrope/
   Mitr stack, deliberately: it is what he actually LOOKED AT when he approved this layout, and
   rewriting it would destroy the thing it exists to prove. When the two disagree, THIS is the
   truth — the same relationship DESIGN.md has to this file.
   ⚠ EVERY SELECTOR IS SCOPED UNDER `#app .lb`, and not for tidiness: `.who`, `.cnt`, `.lab`, `.n`,
   `.srow` and `.gname` all already exist on other surfaces of this stylesheet. Unscoped, the port
   would have silently restyled the game screen.
   ⚠ THE ONE DELIBERATE DEPARTURE FROM THE MOCKUP IS THE TYPE, because the mockup predates the four
   roles he picked the next day. The mapping, and each one is his decision applied:
     Anton   → var(--font-display)   nav, region titles, menu items, buttons
     .n      → var(--font-numbers)   the digit runs (still one digit per fixed cell — see below)
     Manrope → var(--font-body)      everything read as language
     .lab    → var(--font-display)   ⚠ REVERSES the mockup, where a label was language. Labels are
                                     chrome now — his call, from the boxes on /game-face.html.
     .who    → var(--font-names)     the person's name. The mockup drew it in the body face because
                                     no names role existed yet; Teko is what he picked for exactly
                                     this slot ("Teko wins").
   ⛔ NO FACE HERE IS TABULAR, so `.n` lays each digit in its own fixed cell. A live count set as a
   plain string visibly shivers as it ticks.
   ================================================================================================ */
#app .lb button, #app .lb input, #app .lb select { border-radius: var(--r); }
#app .lb { font-family: var(--font-body); color: var(--text);
        width: 100%; margin: 0 auto; padding: var(--sp-24) 0 var(--sp-48); }
/* ⛔ THE MOCKUP SIZED ITSELF AGAINST `100vw` BECAUSE IT WAS A WHOLE PAGE; INSIDE THE APP IT IS NOT.
   `#app` is 560px wide, so a `calc(100vw - 64px)` child overflowed it by 289px at 1280 and the
   whole page scrolled sideways. The gutter belongs to the CONTAINER — `#app.lobby` widens the way
   `#app.gameflow` already does — and the lobby simply fills it. Measured, not guessed: scrollWidth
   1569 against a 1280 viewport. */

#app .lb .d { font-family: var(--font-display); font-weight: 400; letter-spacing: var(--track-tight); }
#app .lb .n { font-family: var(--font-numbers); display: inline-flex; letter-spacing: var(--track-tight); }
/* ⛔ NO FIXED CELL — AND THE RULE IT USED TO SERVE WAS MEASURED ON THE WRONG FACE. His report: "why
   are the numbers so separated?". The cell was `.60em` while a Bebas digit is `.40em`, so every
   number carried 50% of its own width in air.
   ⛔ AND THE DEEPER CORRECTION: **BEBAS NEUE IS TABULAR.** Measured at 100px, all ten digits are
   exactly 40px — 0% spread. The "no face in this system is tabular" note came from ANTON (`1` 10.58
   against `8` 15.81) and was carried onto Bebas when the face changed, without re-measuring. A
   Bebas count cannot shiver as it ticks, so it never needed the cell.
   ⚠ TEKO IS THE OPPOSITE and the note is true of it: `1` is 26 against `0` at 42.3 — a 62.7% spread.
   The `<i>` per digit stays in the markup for that reason: if the numbers role ever moves to Teko,
   re-arming this is one value (`width: .43em`), and a live countdown in a non-tabular face is a
   real defect. */
#app .lb .n > i { font-style: normal; }
#app .lb .lab { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-1);
         letter-spacing: var(--track-wider); text-transform: uppercase; color: var(--text-faint); }
/* ⛔ OPEN — the round's rule (DESIGN.md): a panel that only holds other panels sits on the ground; the seat
   cards are the filled objects. The padding stays so nothing moves. */
#app .lb .rg { background: transparent; padding: var(--sp-16) 0; }   /* open, so no side padding: the cards meet the mast's edge */
#app .lb .rg + .rg { margin-top: 2px; }
#app .lb .mast { border-bottom: 2px solid var(--text); padding-bottom: 12px; }
#app .lb .mrow { display: flex; align-items: center; gap: var(--sp-16); flex-wrap: wrap; }
/* ⚠ CENTRED, NOT BASELINE. On a baseline the biggest item drags the row's baseline down as it
   grows, so scaling the mark moved the account chip too. Centred, each item grows about its own
   middle inside a row whose height is set by the tallest FIXED thing in it. */
#app .lb .mrow.top { align-items: center; }
#app .lb .mrow + .mrow { margin-top: 12px; }
#app .lb .mast .room { font-size: var(--fs-1); letter-spacing: var(--track-wider); text-transform: uppercase;
                color: var(--text-muted); }
#app .lb .mast .room b { font-family: var(--font-names); font-weight: 500; color: var(--text);
                  font-size: var(--fs-2); letter-spacing: var(--track-caps); }
#app .lb .mast .sp { flex: 1 1 auto; }
#app .lb .mast nav { display: flex; gap: var(--sp-24); }
#app .lb .mast nav a { color: var(--text-muted); text-decoration: none; font-size: var(--fs-1); font-weight: 500; }
#app .lb .mast nav a:hover { color: var(--text); }
#app .lb .sp { flex: 1 1 auto; }
#app .lb .mid { display: flex; align-items: center; gap: var(--sp-12); }
#app .lb .mid .nm { font-weight: 600; font-size: var(--fs-2); }
#app .lb .dot { color: var(--rule); }
#app .lb .home { display: flex; align-items: center; text-decoration: none; color: var(--text);
          padding: 0 var(--sp-24) 0 var(--sp-24); flex: 0 0 auto; }
/* ⛔ `line-height: 1` SO THE MARK CANNOT PUSH THE ROOM DOWN — his report: "the rmq move it higher
   so it doesn't push the boxes below when scaled up". Measured: at 37px the wordmark's LINE BOX is
   48px, not 37, because the row inherits a 1.35 line-height; step it to 54 and the box is 70, the
   masthead grows 60 → 70 and everything below it moves 10px. ⚠ THE INK WAS NEVER THE PROBLEM — the
   leading was. With the box equal to the ink, the row's height comes from the account chip's fixed
   60px, and the mark can take any step up to 54 without moving a single thing beneath it. */
#app .lb .home .d { font-size: var(--fs-9); letter-spacing: var(--track-tight);
          line-height: var(--lh-solid); display: block; }
#app .lb .home:hover { color: var(--accent); }
#app .lb .navb { display: flex; gap: 0; }
/* ⚠ A STEP UP FROM THE MOCKUP ON PURPOSE — his call, 2026-09-09: "it doesn't look the same bc we
   didn't use anton right? then make those toprow, text on button a little larger". Bebas Neue is
   lighter and narrower than the Anton the mockup was drawn in, so the same nominal size reads
   smaller on screen. The nav, the account name and every button move one scale step. */
#app .lb .navb a { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-5);
            letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--text-muted);
            background: none; padding: 0 24px; text-decoration: none;
            display: flex; align-items: center; border-bottom: 2px solid transparent; }
#app .lb .navb a:hover, #app .lb .navb a.on { color: var(--text); border-bottom-color: var(--text); }
#app .lb .mrow.top { align-items: stretch; }
#app .lb .mrow.top > * { min-height: 60px; }
#app .lb .navb a { display: flex; align-items: center; }
/* ⚠ `position: relative` IS LOAD-BEARING AND THE MOCKUP SET IT INLINE. `.amenu` is absolutely
   positioned against this box; without it the menu resolves against the nearest positioned
   ancestor — or the viewport — and lands somewhere else entirely. Inline styles do not survive a
   port, so it belongs here. */
#app .lb .idg { position: relative; display: flex; align-items: center; gap: var(--sp-12); background: var(--l2);
         padding: 0 var(--sp-16) 0 0; flex: 0 0 300px; width: 300px; }
#app .lb .idg > * { flex: 0 0 auto; }
#app .lb .idg .nm { flex: 1 1 auto; min-width: 0; justify-content: space-between; }
#app .lb .idg .nm > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#app .lb .idg .tile { width: 60px; height: 60px; font-size: var(--fs-6); }
#app .lb .idg .pick { padding-top: 0; padding-bottom: 0; height: 60px; }
/* ⛔ THE NAMES FACE, NOT THE DISPLAY ONE — his call: "the profile the name font and list the same
   font as name". Your own name in the masthead is the same kind of thing as a name on a seat card,
   so it takes the same role. ⚠ It is also TYPED DATA, which the display face may never draw. */
#app .lb .idg .nm { font-weight: 600; font-size: var(--fs-6); background: none; border: 0; color: var(--text);
             /* ⚠ THIS SLOT HELD `font-family: inherit` AND THAT IS WHY THE FIRST FIX DID NOTHING.
                Adding the names face at the TOP of this block left the later `inherit` standing, and
                the last declaration in a block wins — so the size changed and the face did not, which
                reads exactly like "the token is broken". Replace the declaration, never prepend one. */
             font-family: var(--font-names); cursor: pointer; display: flex; align-items: center; gap: var(--sp-8);
             padding: 0; height: 60px; }
#app .lb .idg .nm .car { font-size: var(--fs-1); color: var(--text-faint); }
#app .lb .idg .nm:hover { color: var(--accent); }
#app .lb .amenu { position: absolute; top: 100%; left: 0; right: 0; background: var(--l3);
           z-index: 60; display: flex; flex-direction: column; padding: var(--sp-8) 0; }
#app .lb .amenu button, #app .lb .amenu a { background: none; border: 0; color: var(--text);
           font-family: var(--font-display); font-size: var(--fs-6); letter-spacing: var(--track-caps);
           text-transform: uppercase; text-align: left; padding: var(--sp-8) var(--sp-16); cursor: pointer;
           text-decoration: none; display: block; width: 100%; }
#app .lb .amenu button:hover, #app .lb .amenu a:hover { background: var(--hover-fill); }
#app .lb .amenu .sep { height: 1px; background: var(--rule); margin: var(--sp-4) 0; }
/* ⛔ NOT DIMMED — his report: "the not you is barely visible". It was `--text-muted`, which measures
   5.53:1 on the menu's `--l3` and passes on paper; what it does not survive is BEBAS AT 12px, a
   thin condensed face whose strokes half-disappear against a dark ground at that size. ⚠ THE
   RATIO WAS NEVER THE PROBLEM — the same value reads fine at 22px on a seat card. It keeps its
   rank through SIZE instead of colour, which is the honest way to rank two things anyway. */
#app .lb .amenu .quiet { color: var(--text-muted); font-size: var(--fs-3); }
#app .lb .pen { background: none; border: 0; color: var(--text-muted); cursor: pointer; font-size: var(--fs-2);
         padding: var(--sp-4) var(--sp-8); line-height: 1; }
#app .lb .pen:hover { color: var(--text); }
#app .lb .idg:hover .pen { color: var(--text); }
#app .lb .pick { display: inline-flex; align-items: center; gap: var(--sp-12); background: var(--l3);
          border: 0; color: var(--text); font: inherit; font-size: var(--fs-2); font-weight: 600;
          padding: var(--sp-12) var(--sp-16); cursor: pointer;
          width: max-content; flex: 0 0 max-content; white-space: nowrap; }
#app .lb .pick .cap { color: var(--text-faint); font-weight: 400; font-size: var(--fs-1); letter-spacing: var(--track-wider);
               text-transform: uppercase; }
#app .lb .pick .val { color: var(--text); white-space: nowrap; }
#app .lb .pick .car { color: var(--text-faint); font-size: var(--fs-1); }
#app .lb .pick:hover { background: #33323d; }
#app .lb .sec { padding: var(--sp-24) 0 0; }
#app .lb .sec > .lab { display: block; margin-bottom: var(--sp-16); }
/* ⛔ THE BORDER STAYS AND ONLY ITS COLOUR MOVED — his correction, 2026-09-12: "having the border
   isn't the problem. it might be the color you chose." My diagnosis (drop one of the two separation
   techniques) was wrong and his was right.
   #1d8742 is `--good-fill`, a token solved to be a PLANE with white text on it, and it measured
   3.6:1 against the card's own fill — for a ONE PIXEL line that is a smudge, not an edge. Area is
   not in the contrast formula, which this project already recorded in reverse when `--border`, a
   line colour, was used as a 15px scrollbar thumb. `--good` is 12.0:1 against the same fill.
   ⛔ AND THE MIDDLE DOES NOT EXIST: a hand-mixed #27a855 (5.3:1) was tried and
   `test/paletteidempotent.test.js` caught it — the palette sweep rewrote it to #04833c and then
   #03833c, drifting further on every pass, because an off-palette colour has no token to snap to.
   The palette is CLOSED (his call, 2026-09-04), so the choice here is exactly two values.
   ⚠ ONE LINE TO REVERSE: put #1d8742 back. */
/* 2px since the round port: the edge is the card's one signal, the round's verdict-edge width (his 08:03,
   "try 1 px thicker on the card border"). ⚠ One number to reverse. A waiting card is FILL ONLY — its hairline
   meant nothing, and the round rule is that a border exists only when it means something. */
#app .lb .seat.ready { background: var(--good-tint); box-shadow: inset 0 0 0 2px var(--good); }
#app .lb .seat.waiting { background: var(--l1); box-shadow: none; }
#app .lb .seat.waiting .who { color: var(--text-muted); }
#app .lb .seat, #app .lb .tile { transition: background .18s ease, filter .18s ease, color .18s ease; }
/* ⛔ TWO PER ROW, FIXED — his call, 2026-09-09: "maybe fix to the size for two card per row. and
   just keep the blank area?" It was `auto-fit, minmax(300px, 1fr)`, so a card's WIDTH depended on
   how many people were in the room: two players got two ~600px cards and the list picker inside one
   looked lost in it, four players got three narrower ones. A seat card should be the same object
   whichever it is. ⚠ The cost he accepted up front is the blank beside an odd last card.
   ⚠ Going back to a filling grid is this one line. */
/* ⛔ 12px, NOT 4 — his pick (variant B, 2026-09-12), and the 4px was measurably wrong rather than a
   taste call: four cards 802 × 133 separated by 4px are 0.5% of a card's width apart, so four
   separate objects read as four rows of one table. That table shape is most of what he was calling
   "still looks made by ai".
   ⚠ HE REJECTED THE REST OF MY DIAGNOSIS AND WAS RIGHT TO: "having the border isn't the problem. it
   might be the color you chose." The border stays; only its colour is open. */
#app .lb .seats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-12); }
#app .lb .seat { background: var(--l2); padding: 24px; display: grid;
          grid-template-columns: 84px 1fr auto; gap: var(--sp-16); align-items: center; position: relative; }
#app .lb .seat.ready .who { color: var(--text); }
/* ⛔ THE ONE PLACE THE PORT DEPARTS FROM THE MOCKUP, AND IT IS A DECISION, NOT A DRIFT. The mockup
   drew a player's name in the BODY face because no names role existed when it was built. Teko is
   what he picked for exactly this slot the next day — "Teko wins" — and the seat card is the
   scoreboard name it was picked against. ⚠ Prompt stays behind it: Teko has no Thai glyphs, and a
   Thai player name has to fall through per character. */
#app .lb .seat .who { font-family: var(--font-names); font-weight: 600; font-size: var(--fs-7); color: var(--text-muted);
               display: flex; align-items: center; gap: var(--sp-8); min-width: 0; }
#app .lb .crown { width: 19px; height: 16px; flex: 0 0 auto; color: var(--crown); }
#app .lb .tile { width: 84px; height: 84px; display: flex; align-items: center; justify-content: center;
          font-family: var(--font-display); font-size: var(--fs-7); color: var(--on-tile); overflow: hidden;
          position: relative; }
#app .lb .you .tile { width: 44px; height: 44px; font-size: var(--fs-4); }
#app .lb .cnt.spectl .n { font-family: var(--font-display); font-weight: 600; font-size: var(--fs-1);
                   letter-spacing: var(--track-wider); color: var(--text-faint); }
#app .lb .tile img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
#app .lb .cnt { text-align: right; line-height: var(--lh-ui); }
/* ⛔ `--fs-6` (31px), NOT `--fs-7`, AND THAT IS RESTORING THE MOCKUP RATHER THAN OVERRIDING THE
   SCALE. The mockup drew the seat tile at 40 and this count at 34, so the initial read clearly
   bigger than the number. Snapping sent the tile 40 → 37 and the count 34 → 37 — 34 is EXACTLY
   equidistant from 31 and 37, and the scale's tie rule rounds up — so both landed on one step and
   the hierarchy he approved disappeared. ⚠ THE SNAP DESTROYED A RELATIONSHIP, NOT JUST A VALUE, and
   that is the failure mode to watch for in every later surface: check what a value is bigger THAN,
   not only whether it moved. The other side of the tie is still on the scale. */
/* ⚠ BIGGER AND TIGHTER TO ITS LABEL — his call: "the list count bigger and closure to the songs
   text". It went 34 (mockup, Anton) → 37 → 31 → 37: I dropped it to 31 to keep the tile reading
   bigger than it, and he wants the number to lead instead. The `SONGS` label loses most of its gap
   so the two read as one block rather than two lines. */
/* ⛔ `line-height: 1`, AND IT IS WHAT MAKES THE GAP CONSTANT — his report: "make the songs and the
   count closer. and fix that gap range whether how size is changed". The `.lab` margin was already
   2px; what he was seeing was the LEADING inside the number's own line box. Measured at 37px: a
   49.95px line box around 26px of ink, so ~12px of empty space sat under the digits and GREW with
   every size step (73px box at 54px type). ⚠ With the box equal to the ink the gap is the margin
   alone — the same at 12px as at 54px, which is what "fix that gap range" asks for. */
#app .lb .cnt .n { font-size: var(--fs-7); color: var(--text); line-height: var(--lh-solid);
          display: inline-flex; }
#app .lb .cnt .lab { display: block; margin-top: var(--sp-4); font-size: var(--fs-2); line-height: var(--lh-solid); }
/* ⛔ THE NUMBERS FACE — his call: "make the empty the same font as the number". It occupies the
   slot a count occupies, so it is read as one, and it is a word WE wrote rather than typed data. */
#app .lb .cnt.none .n { font-family: var(--font-numbers); font-weight: 400; font-size: var(--fs-6);
                 letter-spacing: var(--track-wide); color: var(--warn); }
#app .lb .cnt.none .lab { color: var(--warn); opacity: .72; }
#app .lb .seat.spect { background: transparent; box-shadow: inset 0 0 0 1px var(--rule); }
#app .lb .seat.spect .who, #app .lb .seat.spect .cnt .n { color: var(--text-faint); }
#app .lb .seat.spect .tile { filter: grayscale(1); opacity: .5; }
#app .lb .seat .cnt { grid-column: 3; grid-row: 1; }
#app .lb .acts { grid-column: 3; grid-row: 1; display: flex; gap: 4px; justify-self: end;
          visibility: hidden; }
#app .lb .seat:hover .acts { visibility: visible; }
#app .lb .seat:hover:has(.acts) .cnt { visibility: hidden; }
/* ✅ THE LIST PICKER IS BACK ON YOUR OWN SEAT CARD — HIS CALL, 2026-09-09: "the list still isn't on
   card". That REVERSES his 2026-08-29 rule ("no need list name beside name"), which game-check G20b
   used to enforce; his newer word wins and G20b is rewritten to say so.
   ⛔ ONE THING FROM THE MOCKUP IS NOT COPIED, AND IT IS NOT A STYLE PREFERENCE: the mockup drew this
   control in the display face with `text-transform: uppercase`. A LIST NAME IS TYPED DATA, and
   BEBAS NEUE HAS NO LOWERCASE GLYPHS — a list named `no yuka zzz` would render `NO YUKA ZZZ` and
   dropping the text-transform would not fix it, because the face has no lowercase to fall back to.
   Same rule as artist names ("artist is case-sensitive. You shouldn't make it all caps"). It takes
   the BODY face here; the tracking and the box are the mockup's.
   ⚠ ON HOVER IT SWAPS WITH THE COUNT — the card has one slot on the right and the mockup gives it
   to whichever you are reaching for. On touch there is no hover, so it takes its own row below. */
#app .lb .seat .mine { grid-column: 3; grid-row: 1; justify-self: end; display: flex;
                       visibility: hidden; }
#app .lb .seat:hover .mine { visibility: visible; }
#app .lb .seat:hover:has(.mine) .cnt { visibility: hidden; }
#app .lb .seat .mine .lobby-lists { margin: 0; }
/* ⚠ `ch` IS A FONT-RELATIVE UNIT AND THE FONT JUST CHANGED. `.lobby-lists .checkfilter-toggle` is
   `width: 13ch`, sized when this control drew in the body face; Teko is condensed, so its `0` is far
   narrower and the same 13ch clipped `My list` to `My…`. On the card the button sizes to its
   content instead — the mockup's own button did too. */
#app .lb .seat .mine .checkfilter-toggle { width: auto; min-width: 0; max-width: 100%; }
#app .lb .seat .mine .muted { display: none; }
/* ⛔ MATCHED TO THE SETTINGS CHOICE CONTROL — his call: "make it the same as in settings". It
   already shared its background (`--l3`) and its ink (`--text`); what differed was weight, size and
   tracking, which is exactly what he named. Now: 18px, weight 500, tight tracking, same padding.
   ⚠ ONE THING IS DELIBERATELY NOT MATCHED — the FACE. A settings choice reads `ANIME TITLES`, words
   we wrote; this reads a list name HE typed. Bebas Neue has no lowercase glyphs at all, so putting
   the display face here would render `imas` as `IMAS` with no way to undo it — the artist-name rule.
   Teko has the same condensed, flat-sided build and does draw lowercase. */
#app .lb .seat .mine button { background: var(--l3); border: 0; color: var(--text);
                       font-family: var(--font-names); font-size: var(--fs-4); font-weight: 500;
                       letter-spacing: var(--track-tight);
                       padding: var(--sp-8) var(--sp-12); cursor: pointer;
                       display: flex; align-items: center; gap: var(--sp-8); }
#app .lb .seat .mine button:hover { background: #33323d; }
#app .lb .seat .mine .car,
#app .lb .seat .mine .pick-caret { font-size: var(--fs-1); color: var(--text-faint); }
/* the seat's list caret is /list's picker caret (his, 2026-09-24: "make this triangle the same size as in list") — /list draws
   '▼' in the text face at 12px; checkfilter writes ' ▾' (a smaller glyph, and caption tests read that text), so the span's
   own glyph is hidden and the toggle draws /list's after its caption */
#app .lb .seat .mine .pick-caret { display: none; }
#app .lb .seat .mine .checkfilter-toggle::after { content: '▼'; margin-left: var(--sp-4); font-family: var(--font-body); font-size: var(--fs-1); color: var(--text-faint); }
#app .lb .acts button { background: var(--l3); border: 0; color: var(--text-muted); width: 44px;
                 height: 44px; cursor: pointer; font-size: var(--fs-2); line-height: 1; padding: 0; }
#app .lb .acts button:hover { color: var(--text); }
#app .lb .acts .givehost:hover { color: var(--crown); }
#app .lb .acts .kick:hover { color: #ed898e; }
#app .lb .acts button { display: flex; align-items: center; justify-content: center; }

#app .lb .setup { display: flex; align-items: flex-end; gap: var(--sp-24); flex-wrap: wrap; }
#app .lb .roomtop { display: flex; align-items: center; justify-content: flex-end; gap: var(--sp-12);
             min-height: 40px; margin-bottom: 12px; }
#app .lb .eye { width: 20px; height: 20px; flex: 0 0 auto; color: var(--text-faint); }
#app .lb .wfaces { display: flex; gap: 4px; }
#app .lb .wface { width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
           font-family: var(--font-display); font-size: var(--fs-3); color: var(--on-tile);
           filter: saturate(.55) brightness(.8); }
/* a watcher's PICTURE fills the 34px face like a seat tile's (his 2026-09-25 screenshot: an unsized <img> drew at its full
   size over the seats) */
#app .lb .wface { position: relative; overflow: hidden; }
#app .lb .wface img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
#app .lb .wface.haspic { color: transparent; }
#app .lb .launch { display: flex; align-items: center; gap: var(--sp-16); margin-top: var(--sp-16);
            padding-top: var(--sp-16); border-top: 1px solid var(--rule); flex-wrap: wrap; }
#app .lb .scrim { position: fixed; inset: 0; background: rgba(0,0,0,.6); opacity: 0;
           pointer-events: none; transition: opacity .14s; z-index: 40; }
#app .lb .scrim.on { opacity: 1; pointer-events: auto; }
/* ⚠ A DROPDOWN STILL NEEDS A CLOSER, AND IT MUST NOT DIM THE PAGE. Without this the account menu
   had no way to close except tapping its own button again — every click outside it landed on #app,
   because an `.on`-less scrim is `pointer-events: none`. A full dark scrim behind a three-item menu
   reads as a modal, which it is not, so this catches the click and paints nothing. */
#app .lb .scrim.bare { opacity: 0; pointer-events: auto; }
#app .lb .panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
           background: var(--l2); padding: var(--sp-24) var(--sp-24) var(--sp-24); z-index: 50;
           width: min(920px, calc(100vw - 48px)); max-height: calc(100vh - 48px); overflow: auto; }
#app .lb .panel.confirm { width: min(420px, calc(100vw - 48px)); }
#app .lb .panelhead { display: flex; align-items: center; margin-bottom: var(--sp-24); }
/* ⛔ THE WHOLE PANEL IS ONE STEP UP — his call: "setting page is still small. enlarge the whole".
   Not the labels alone: the heading, the group names, the values, the number boxes and the select
   boxes all move together, or lifting one just flattens the ranking between them. The panel gets
   wider with them so nothing wraps. */
#app .lb .panelhead .t { font-family: var(--font-display); font-size: var(--fs-6); letter-spacing: var(--track-wide);
                  text-transform: uppercase; flex: 1 1 auto; }
#app .lb .panelhead .x { background: none; border: 0; color: var(--text-muted); font-size: var(--fs-2);
                  cursor: pointer; padding: 4px 8px; }
#app .lb .panelhead .x:hover { color: var(--text); }
#app .lb .setcols { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; }

#app .lb .gname { display: block; font-family: var(--font-display); font-size: var(--fs-5); letter-spacing: var(--track-wide);
           text-transform: uppercase; color: var(--text); margin-bottom: 16px;
           padding-bottom: var(--sp-12); border-bottom: 1px solid var(--rule); }
#app .lb .srow, #app .lb .setup .f { display: flex; flex-direction: row; align-items: center;
                     justify-content: space-between;
                     gap: var(--sp-16); padding: var(--sp-12) 0; border-bottom: 1px solid var(--rule); }
#app .lb .srow .lab, #app .lb .setup .f > .lab { margin: 0; flex: 0 0 auto; }
/* ⛔ THE PANEL'S LABELS ARE NOT FAINT — his report: "same as the setting texts. which the
   choices/numbers are much more visible". Measured on `--l2`: the label was 5.20:1 and its value
   11.93:1, so the two sat more than two steps apart and the label read as decoration beside the
   thing it names. ⚠ A LABEL IS WHAT YOU SCAN FOR — you already know the value, you are hunting the
   row — so it may sit below its value but not this far below. One colour step and one size step;
   the value keeps `--text` and its own weight, so the ranking survives.
   ⚠ SCOPED TO THE PANEL on purpose: the seat card's `SONGS` sits under a 37px number and reads
   fine at 12px faint, and he has not asked for it. */
#app .lb .panel .lab { color: var(--text-muted); font-size: var(--fs-4); }
#app .lb .setup { display: block; }
#app .lb .setup .f select { min-width: 174px; } /* fits INSTRUMENTAL (the widest option) so every select lines up */
#app .lb .sctl { display: flex; align-items: center; gap: var(--sp-16); }
/* ⛔ NOT `ch`, AND THIS IS THE SECOND TIME THAT UNIT HAS CLIPPED SOMETHING TODAY. `1ch` is the width
   of `0`, which only represents the other digits in a TABULAR face — and no face in this system is
   tabular, which is already written down. Measured at 22px: the box came out 39px wide with 24px of
   padding, so 15px of content, while `45` in Bebas is 17.6px and `88` in Teko is 18.5px. It
   overflowed by 3px and he saw it.
   ⚠ 64px is derived, not picked: three digits of the widest face (Teko `8`, 9.3px) is 27.9px, plus
   16px of padding, plus room for the value to grow if the settings caps ever go — which is a change
   he has already decided on. The list picker was clipped by the same unit an hour earlier. */
#app .lb .sctl input[type=number] { width: 64px; text-align: center; background: var(--l3); border: 0;
    color: var(--text); font-family: var(--font-display); font-size: var(--fs-5); padding: var(--sp-12) var(--sp-8);
    appearance: textfield; -moz-appearance: textfield; }
#app .lb .sctl input[type=number]::-webkit-outer-spin-button, #app .lb .sctl input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
#app .lb .setfoot { display: flex; align-items: center; gap: var(--sp-12); margin-top: var(--sp-24); padding-top: var(--sp-16);
             border-top: 1px solid var(--rule); }
#app .lb .go.small { font-size: var(--fs-2); padding: var(--sp-12) var(--sp-24); }
#app .lb .ctext { margin: 0 0 var(--sp-24); color: var(--text); font-size: var(--fs-2); }
#app .lb .crow { display: flex; gap: var(--sp-12); justify-content: flex-end; }

#app .lb .f { display: flex; flex-direction: column; gap: var(--sp-8); }
#app .lb .f input, #app .lb .f select {
    
    background: var(--l3); border: 0; border-radius: var(--r); color: var(--text);
    font-family: var(--font-display); font-size: var(--fs-3); letter-spacing: var(--track-tight);
    padding: 8px 12px; appearance: textfield; -moz-appearance: textfield; }
/* ⚠ SAME UNIT, SAME TRAP — nothing renders through this rule today (the panel's numbers all go
   through `.sctl input`), but it is one markup change away from clipping in exactly the same way. */
#app .lb .f input { width: 56px; text-align: center; }
#app .lb .f input::-webkit-outer-spin-button, #app .lb .f input::-webkit-inner-spin-button {
    -webkit-appearance: none; margin: 0; }
/* ⛔ THE NAMES FACE — his call, 2026-09-09, off /lobby-face.html: "only thing changed is settings:
   values to teko … but text is confirmed as teko". This is the choice TEXT (`ANIME TITLES`,
   `RANDOM`, `RANDOM POINT`). ⚠ THE NUMBERS ARE NOT THIS ROW and are still open — they sit in
   `.sctl input` and keep the display face until he settles them; the picker has a row of their own
   for exactly that. */
#app .lb .f select { font-family: var(--font-names); font-size: var(--fs-4); font-weight: 500;
              padding-right: var(--sp-24); }
#app .lb .f input:focus-visible, #app .lb .f select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
#app .lb .setup .sp { flex: 1 1 auto; }
#app .lb .go { font-family: var(--font-display); font-size: var(--fs-6); letter-spacing: var(--track-caps); border: 0;
        border-radius: var(--r); background: var(--accent-fill); color: #fff;
        padding: var(--sp-12) var(--sp-32); cursor: pointer; }
#app .lb .go:hover { background: #7375ff; }
#app .lb .go:active { transform: translateY(1px); }
#app .lb .go:disabled { background: var(--l3); color: var(--text-faint); cursor: default; }
#app .lb .go:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
#app .lb .btn2 { font-family: var(--font-display); font-size: var(--fs-4); font-weight: 400; letter-spacing: var(--track-caps);
          text-transform: uppercase; border: 0;
          border-radius: var(--r); background: var(--l2); color: var(--text);
          padding: var(--sp-12) var(--sp-16); cursor: pointer; }
#app .lb .btn2:hover { background: var(--l3); }
#app .lb .btn3 { background: none; border: 0; color: var(--text-muted); font: inherit; font-size: var(--fs-1);
          font-weight: 500; cursor: pointer; padding: var(--sp-8) var(--sp-2); text-decoration: none; }
#app .lb .btn3:hover { color: var(--text); }
#app .lb .spectbtn { font-family: var(--font-display); font-size: var(--fs-4); letter-spacing: var(--track-caps);
              text-transform: uppercase; padding: 12px 16px; }
#app .lb .you { display: flex; align-items: center; gap: var(--sp-16); padding: var(--sp-16); background: var(--l1);
         flex-wrap: wrap; }
#app .lb .you .nm { font-weight: 600; font-size: var(--fs-2); }
#app .lb .you .sp { flex: 1 1 auto; }
#app .lb .picker { display: flex; align-items: center; gap: var(--sp-8); }
#app .lb .picker select { background: var(--l3); border: 0; color: var(--text); font: inherit;
                   font-size: var(--fs-2); font-weight: 500; padding: var(--sp-8) var(--sp-12); }
#app .lb .foot { margin-top: var(--sp-32); padding-top: 16px; border-top: 1px solid var(--rule);
          display: flex; gap: var(--sp-24); align-items: center; }

/* ⛔ EVERY RESPONSIVE BLOCK SITS AT THE END, AND THAT IS A RULE THIS PORT PAID FOR TWICE. The mockup
   wrote them near the top; a media query carries NO extra specificity, so `@media(max-width:700px)
   #app .lb .seats { grid-template-columns: 1fr }` sitting 90 lines above the base `#app .lb .seats` lost to it
   on source order alone — the phone kept a 300px minimum track inside a 227px box and every seat
   card overflowed under the edge rail. The mockup's own comments record the same failure against
   `.idg`, and it still travelled with the port. ⚠ Measured, not eyeballed: seat right edge 341
   against a room right edge of 288. */

@media (max-width: 700px) {
  /* ⚠ THE WIDTH IS THE CONTAINER'S JOB, NOT THIS RULE'S — same reason as the base `#app .lb`. Left as
     a `100vw` override it beat #app.lobby's padding and put the seat cards straight back under the
     edge rail on a phone, which is the exact overlap that padding exists to prevent. */
  #app .lb { width: 100%; padding-top: var(--sp-16); }
  #app .lb .seats { grid-template-columns: 1fr; }
  /* ⛔ A GRID ITEM WILL NOT SHRINK BELOW ITS CONTENT — `min-width` is `auto` on one, not 0. So a
     `1fr` track did not stop the card overflowing: the 84px tile column plus 24px of padding each
     side plus the count's own words held it at 313px inside a 288px box, and it ran under the edge
     rail again. The tile and the padding come down on a phone; `min-width: 0` is what lets the
     name column actually give. */
  #app .lb .seat { grid-template-columns: 60px 1fr auto; padding: var(--sp-16); gap: var(--sp-12);
            min-width: 0; }
  #app .lb .seat .tile { width: 60px; height: 60px; font-size: var(--fs-4); }
  /* ⛔ THE BUTTON STEP-UP IS DESKTOP ONLY. He asked for larger button text looking at a 1280px
     screen; at 375px the same step breaks the launch bar onto two rows — game-check G18 measured
     Spectate at y 777-824 and UNREADY at 840-893. A phone keeps the smaller step, which is the rule
     that row has always had: one line, nothing clipped. */
  #app .lb .btn2, #app .lb .spectbtn { font-size: var(--fs-2); }
  #app .lb .go { font-size: var(--fs-3); }
  /* ⛔ THE BAR PAYS ITS GAP TWICE AND THAT IS WHY IT WRAPPED. `.launch` holds Spectate, a flex
     SPACER, then the primary button — so a 16px gap costs 32px, not 16. Measured at 375px:
     83 + 16 + 16 + 122 = 237 against 231 of usable width, and the two buttons landed on separate
     rows while each one was comfortably narrow enough. ⚠ An empty flex child is invisible in a
     screenshot and free-looking in a diff; it is not free in the gap. */
  #app .lb .launch { gap: var(--sp-8); }
  #app .lb .mrow.top > * { min-height: 0; }
  #app .lb .idg > * { min-height: 44px; }
}

@media (hover: none) {
  /* no hover on a phone, so the count/control swap cannot happen — both are shown and the picker
     takes its own row below, the same compromise the host controls make here */
  #app .lb .seat .mine { visibility: visible; grid-column: 1 / -1; grid-row: 2; justify-self: end;
                         margin-top: var(--sp-12); }
  #app .lb .seat:hover:has(.mine) .cnt { visibility: visible; }
  #app .lb .acts { visibility: visible; grid-column: 1 / -1; grid-row: 2;
            justify-content: flex-end; margin-top: var(--sp-12); gap: var(--sp-8); }
  #app .lb .seat:hover .cnt { visibility: visible; }
}

@media (max-width: 760px) {
  #app .lb .setcols { grid-template-columns: 1fr; gap: 24px; }
}

@media (max-width: 700px) {
  #app .lb .idg { flex: 1 1 auto; min-width: 0; flex-wrap: wrap; row-gap: 8px;
           justify-content: flex-start; padding: 0 8px 8px 0; }
  #app .lb .idg > * { min-height: 44px; }
}

/* ════════════════════════════════════════════════════════════════════════════════════════════════
   THE ROUND — ported 2026-09-23 from public/round-final.html (render.js `rq*`). The mockup is the
   spec; DESIGN.md § "What the round settled" is the system it teaches every other page.
   ⛔ NO transform: scale (council verdict: iPad 12px → 7.5px, and it breaks getBoundingClientRect).
   The mockup's 1920 geometry is the maximised case; every other size RE-FLOWS from the same numbers:
     side inset 84 · card gap 40 · card = (content − 3 gaps) / 4 → 408 · song box = two middle cards
     + their gap → 856 · side columns = 5/6 of a card → 340 · title plate = song box − 160 → 696.
   ⛔ IT MUST HOLD ALL NINE WINDOWS SNAP LAYOUTS of a 1920×1080 screen (his 08:12): full, halves
   (960), two-thirds (1280), thirds (640), quarters (960 × ~450). Horizontal matters more than vertical.
   Three branches: WIDE (three columns, four cards in a row), NARROW-TALL (≤1000 wide and ≥700 tall:
   board | middle, the right box under the middle, cards 2×2), and everything else keeps WIDE with
   every height scaled down.
   ⚠ Every class is `rq-` prefixed and every rule is under `#app .rqr` — the bare names belong to the
   old screen and to style.css's globals (`button { padding… }` among them).
   ════════════════════════════════════════════════════════════════════════════════════════════════ */
html:has(#app .rqr) { scrollbar-gutter: auto; overflow: clip; }   /* the lane was 15px of nothing that made 1920 overflow */
body:has(#app .rqr) { overflow: clip; }
#app.gameflow:has(> .rqr) { max-width: none; margin: 0; padding: 0; }
/* the empty band under the room controls — never over the cards */
body:has(#app .rqr) .audiohint { position: fixed; left: clamp(10px, 1.25vw, 24px); top: 64px; right: auto; bottom: auto; max-width: min(300px, 20vw); margin: 0; z-index: 4; }

#app .rqr {
  /* the ladder on the ground: white over the wash, so the light carries through the panels.
     ⛔ ANYTHING THAT FLOATS OVER CONTENT RE-DECLARES THE SOLID --l1..--l3 (his 06:49). */
  --q1: rgba(255, 255, 255, .05); --q2: rgba(255, 255, 255, .08); --q3: rgba(255, 255, 255, .12);
  --side: clamp(16px, 4.375vw, 84px);
  --edge: clamp(10px, 1.25vw, 24px);
  --cg: clamp(12px, 2.0834vw, 40px);
  --cw: calc((100vw - 2 * var(--side) - 3 * var(--cg)) / 4);
  --scol: calc(var(--cw) * 5 / 6);
  --songw: calc(2 * var(--cw) + var(--cg));
  --platew: calc(var(--songw) - 160px);
  --pad-top: clamp(8px, 1.7dvh, 16px);
  --tab-h: clamp(28px, 4.3dvh, 41px);
  --plate-h: clamp(56px, 11dvh, 104px);
  --ans-h: clamp(40px, 5.6dvh, 53px);
  --cards-gap: clamp(12px, 3.8dvh, 36px);
  --np-h: clamp(30px, 4.2dvh, 40px);
  /* the card is 168 tall at 1920; shorter screens shrink it, and a narrow card caps it so the face
     (square, = the row under the nameplate) never takes more than ~a third of the width */
  --card-h: min(168px, max(96px, 17.7dvh), calc(var(--cw) * 0.35 + var(--np-h)));
  position: fixed; inset: 0; box-sizing: border-box;
  padding: var(--pad-top) var(--side) var(--sp-8);
  display: grid; column-gap: 0; row-gap: 0;
  grid-template-columns: var(--scol) minmax(0, 1fr) var(--songw) minmax(0, 1fr) var(--scol);
  grid-template-rows: auto minmax(0, 1fr) calc(var(--ans-h) + 10px) var(--cards-gap) auto;
  grid-template-areas:
    "acts  . plate . mine"
    "stand . info  . extra"
    "vote  . answer . ."
    ".     . .      . ."
    "cards cards cards cards cards";
  color: var(--text); font-family: var(--font-body);
}
:where(#app .rqr button) { font: inherit; color: inherit; margin: 0; border-radius: var(--r); cursor: pointer; }
#app .rqr :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── row 1: room controls · title · your controls ──────────────────────────────────────────── */
/* ⛔ THE TOP ROW STAYS ON THE 24px EDGE (his "don't move the buttons on the top") — the clusters are
   pulled back out of the 84px inset by the difference */
#app .rqr .rq-acts { grid-area: acts; align-self: start; display: flex; gap: var(--sp-8); margin-left: calc(var(--edge) - var(--side)); margin-top: calc(16px - var(--pad-top)); }
#app .rqr .rq-mine { grid-area: mine; justify-self: end; align-self: start; display: flex; align-items: center; gap: var(--sp-8);
                     margin-right: calc(var(--edge) - var(--side)); margin-top: calc(16px - var(--pad-top)); }
/* ⛔ NO BOX (his 2026-09-22): a plain glyph in a 44×40 hit area; hover lifts it to white on a faint plate */
#app .rqr .rq-act { width: 44px; height: 40px; display: grid; place-items: center; padding: 0;
                    background: none; border: 0; color: var(--text-muted); font-size: var(--fs-4); line-height: 1; }
#app .rqr .rq-act:hover { color: #fff; background: var(--q2); }
/* the icon glyphs are not drawn centred in their own em box — each corrected by its measured offset (the mockup's) */
#app .rqr .rq-act[data-d="hist"] { padding-bottom: calc(var(--sp-4) + .5px); }
#app .rqr .rq-act.rq-home { padding-bottom: var(--sp-4); }
#app .rqr .rq-act[aria-pressed="true"], #app .rqr .rq-act.open { color: var(--text); } /* an open panel's icon is ink, not blue (his colour pass 1b, 2026-09-25) */
/* ⛔ THE BALANCE READS LIKE THE WATCHERS COUNT: icon then number, at the count's size */
#app .rqr .rq-act.stk { width: auto; padding: 0 calc(var(--sp-8) + 2px) calc(var(--sp-4) + 1px); display: flex; align-items: center; gap: calc(var(--sp-4) + 2px); }
#app .rqr .rq-act.stk .cnt { font-family: var(--font-numbers); font-size: var(--fs-4); line-height: 1; color: var(--text-muted); translate: -3px 4.25px; }
#app .rqr .rq-act.stk:hover .cnt { color: #fff; }
#app .rqr .rq-watchers { display: flex; align-items: center; gap: calc(var(--sp-4) + 2px); height: 40px; padding: 0 calc(var(--sp-8) + 2px); margin-left: var(--sp-4);
                         background: none; border: 1px solid transparent; color: var(--text-muted); font-family: var(--font-numbers); font-size: var(--fs-4); }
#app .rqr .rq-watchers svg { width: 20px; height: 14px; }
#app .rqr .rq-watchers:hover { border-color: var(--border-subtle); color: var(--text); }
/* the page's own #volume sits over this slot while the round is up (it lives outside #app so a
   repaint never interrupts a drag) */
#app .rqr .rq-volslot { width: 162px; height: 40px; margin-left: var(--sp-8); }   /* = #volume's measured width (MUTE at 22 + 96px track), so ☺ ≡ ⚙ sit 8px from it as in the mockup */
body:has(#app .rqr) #volume { top: 16px; right: clamp(10px, 1.25vw, 24px); height: 40px; opacity: 1; gap: var(--sp-8); z-index: 5; }
body:has(#app .rqr) #volume button { background: none; border: 0; padding: 0 var(--sp-2); font-family: var(--font-display); font-size: var(--fs-2);
                                    letter-spacing: var(--track-caps); color: var(--text-muted); padding-top: calc(var(--sp-2) + 1px); }
body:has(#app .rqr) #volume input { width: 96px; margin: 0; }
/* ⛔ MUTE DOES NOT MOVE THE SLIDER — the level you set is still there when you unmute; it greys instead */
body.muted #volume input { accent-color: var(--border-subtle); opacity: .55; }
body.muted:has(#app .rqr) #volume button { color: var(--text-faint); } /* muted is an off state, not a warning (colour pass 3b) */

#app .rqr .rq-plateblock { grid-area: plate; position: relative; justify-self: center; width: var(--platew); max-width: 100%;
                           display: flex; flex-direction: column; align-items: center; }
/* the count tab is WELDED: it overlaps the plate's top edge and has no bottom edge */
#app .rqr .rq-tabwrap { width: min(206px, 40%); margin-bottom: calc(-1 * (var(--sp-2) + -1px)); position: relative; z-index: 1; }
#app .rqr .rq-tab { box-sizing: border-box; height: var(--tab-h); padding-top: calc(var(--sp-2) + 1px); background: var(--q2);
                    display: flex; align-items: center; justify-content: center; font-family: var(--font-numbers);
                    font-size: var(--fs-5); letter-spacing: var(--track-tight); color: var(--text-muted); white-space: nowrap; }
#app .rqr .rq-tab b { font-weight: 400; color: var(--text); }
#app .rqr .rq-progress { height: 5px; background: var(--q1); }
#app .rqr .rq-progress i { display: block; height: 100%; background: var(--text-faint); }
#app .rqr .rq-plate { width: 100%; height: var(--plate-h); box-sizing: border-box; padding: var(--sp-4) var(--sp-16); position: relative;
                      background: var(--q2); border: 0; display: flex; align-items: center; justify-content: center; text-align: center;
                      font-family: var(--font-names); font-weight: 500; line-height: 1.12; color: var(--text); overflow: hidden; }
#app .rqr .rq-plate.unknown { color: var(--text-faint); }
#app .rqr button.rq-plate { cursor: help; }
#app .rqr .rq-also { position: absolute; top: calc(var(--tab-h) + 6px); left: calc(100% + 16px); width: 300px; z-index: 5;
                     background: var(--l3); border: 1px solid var(--border); padding: var(--sp-12) calc(var(--sp-12) + 2px); box-sizing: border-box; display: none; }
#app .rqr .rq-plateblock:hover .rq-also, #app .rqr .rq-plateblock:focus-within .rq-also, #app .rqr .rq-plateblock.pinned .rq-also { display: block; }
#app .rqr .rq-also h4, .rq-pop h4 { margin: 0 0 calc(var(--sp-4) + 2px); font-family: var(--font-display); font-weight: 400; font-size: var(--fs-2); letter-spacing: var(--track-caps); color: var(--text-faint); }
#app .rqr .rq-also section + section, .rq-pop section + section { margin-top: var(--sp-12); }
#app .rqr .rq-also ul, .rq-pop ul { margin: 0; padding: 0; }
/* ⛔ ONE ROW PER NAME, A RULE BETWEEN — a long name wraps inside its own row */
#app .rqr .rq-also li, .rq-pop li { list-style: none; padding: calc(var(--sp-4) + 2px) 0; font-family: var(--font-names); font-size: var(--fs-3); line-height: 1.35; }
#app .rqr .rq-also li + li, .rq-pop li + li { border-top: 1px solid var(--border-subtle); }
/* the one floating box for members and watchers — appended to <body>, fixed, SOLID */
.rq-pop { position: fixed; z-index: 30; width: 300px; box-sizing: border-box; padding: var(--sp-12) calc(var(--sp-12) + 2px); color: var(--text);
          background: var(--l3); border: 1px solid var(--border); display: none; }
.rq-pop.on { display: block; }
.rq-pop.watch { width: auto; min-width: 160px; max-height: 196px; overflow-y: auto; }   /* four names fit, then it scrolls */

/* ── the scoreboard ───────────────────────────────────────────────────────────────────────── */
#app .rqr .rq-stand { grid-area: stand; align-self: stretch; margin: var(--sp-8) 0 0; padding: 0 var(--sp-12); list-style: none;
                      overflow-y: auto; overflow-x: clip; scrollbar-width: thin; scrollbar-color: var(--border-subtle) transparent;
                      box-sizing: border-box; background: transparent; min-height: 0; }
#app .rqr .rq-srow { --rowfs: clamp(12px, calc(var(--rowh, 56px) * 0.4), 22px);
                     height: var(--rowh, 56px); display: grid; grid-template-columns: max-content max-content minmax(0, 1fr) max-content;
                     align-items: center; column-gap: var(--sp-8); border-bottom: 1px solid var(--rule); box-sizing: border-box; }
#app .rqr .rq-srow:last-child { border-bottom: 0; }
#app .rqr .rq-srow .rk { font-family: var(--font-numbers); font-size: var(--rowfs); line-height: 1; color: var(--text-muted); min-width: 20px; translate: 0 1.5px; }
#app .rqr .rq-srow .lp { font-family: var(--font-numbers); font-size: var(--fs-2); letter-spacing: var(--track-tight); color: var(--text-faint); text-align: right; translate: 0 1.5px; }
#app .rqr .rq-srow .who { min-width: 0; white-space: nowrap; overflow: hidden; font-family: var(--font-names); font-weight: 500;
                          font-size: var(--rowfs); line-height: 1.3; }
#app .rqr .rq-srow .who .crown { width: 17px; height: 14px; color: var(--crown); vertical-align: -1px; margin-right: calc(var(--sp-4) + 1px); }
/* the score takes the name's blue — BLUE = a name you can open, and the score */
#app .rqr .rq-srow .sc { font-family: var(--font-numbers); font-size: min(var(--fs-5), calc(var(--rowfs) * 1.2)); line-height: 1; text-align: right; color: var(--accent); translate: 0 1.5px; }
#app .rqr .rq-srow.rq-pick { cursor: pointer; }
#app .rqr .rq-srow.rq-pick:hover .who { color: #fff; }
/* the shown group: the raised fill, bled to the board's edge (the bleed MUST equal the padding) */
#app .rqr .rq-srow.on { background: var(--q3); margin: 0 calc(-1 * var(--sp-12)); padding: 0 var(--sp-12); }
#app .rqr .rq-srow.dc { opacity: .45; }
#app .rqr .rq-sdiv { height: 0; border-top: 1px solid var(--border); margin: 0 calc(-1 * var(--sp-12)); list-style: none; }

/* ── the middle: song NAME box, ARTIST box, the timer welded under it ───────────────────────── */
#app .rqr .rq-song { grid-area: info; justify-self: center; align-self: stretch; width: var(--songw); max-width: 100%; margin: var(--sp-8) 0 0;
                     box-sizing: border-box; display: flex; flex-direction: column; gap: var(--sp-8); min-height: 0; overflow: hidden; }
/* ⛔ THE SONG NAME IS 22 AND ITS BOX MUST NOT OUT-SHOUT THE TITLE PLATE */
#app .rqr .rq-songname { flex: none; margin: 0; padding: calc(var(--sp-12) + 1px) var(--sp-24) calc(var(--sp-12) + 3px); background: var(--q1); font-family: var(--font-body); font-weight: 500;
                         font-size: var(--fs-4); line-height: 1.3; color: var(--text); overflow-wrap: anywhere; }
#app .rqr .rq-songname.unknown { color: var(--text-faint); }
/* the version sits BESIDE the name, never inside it; plain muted text, not a chip.
   ⛔ THE TEXT FACE, NEVER BEBAS (his, 2026-09-27: "version shouldn't be shown as the blocky font. the caps matters") — a
   version is data and its capitals are part of it (Bebas has no lowercase). The reveal's label and the one you typed alike. */
#app .rqr .rq-songname .rq-ver { font-family: var(--font-body); font-weight: 400; font-size: var(--fs-3); letter-spacing: 0;
                                 color: var(--text-muted); margin-left: calc(var(--sp-8) + 2px); white-space: nowrap; }
/* who a voice actor PLAYS (2026-09-27): after the name, in the name's own row — ONLY the characters YOU typed (live and at the
   reveal), typed data so the text face. A character typed before its VA is a row of its own with the name blank. */
#app .rqr .rq-ar.rq-charrow .nm:empty { display: none; }
#app .rqr .rq-ar.rq-charrow .nm:empty + .rq-char { margin-left: 0; }
#app .rqr .rq-ar .nmc .rq-char, .rq-pop .rq-char { font-family: var(--font-body); font-weight: 400; font-size: var(--fs-2); letter-spacing: 0;
                                                  color: var(--text-muted); margin-left: var(--sp-8); text-transform: none; }
/* /library Artists tab: who each member plays — the field right beside the member's name, then ⇄ and ✕ (2026-09-27) */
.artist-lineups .member-row:has(.member-char) { width: max-content; max-width: none; }
.artist-lineups .member-row:has(.member-char) .artist-box { flex: 0 0 380px; width: 380px; }
.artist-lineups .member-row .member-char { flex: 0 0 240px; width: 240px; min-width: 0; }
.artist-lineups .member-row.ro { width: max-content; align-items: center; gap: var(--sp-8); }
.artist-lineups .member-row.ro .memchip { flex: 0 0 380px; width: 380px; height: 40px; box-sizing: border-box; display: flex; align-items: center;
                                         font-size: var(--fs-2); }
#app .rqr .rq-abox { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; background: var(--q1); padding: var(--sp-8) var(--sp-24) 0; position: relative; }
#app .rqr .rq-alist { order: 1; flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: clip; margin: 0 calc(-1 * var(--sp-24)); padding: 0 var(--sp-24);
                      scrollbar-width: thin; scrollbar-color: var(--border-subtle) transparent; }
/* ⚠ the separator is a SHADOW, not a border: a tinted row paints over a border and the rows ran together */
#app .rqr .rq-ar { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: baseline; column-gap: var(--sp-12);
                   padding: var(--sp-8) 0 calc(var(--sp-8) + 2px);   /* 9 / 9 measured the row 2.2px low; 8 / 10 lifts name and role together */ font-size: var(--fs-4); box-shadow: inset 0 -1px 0 var(--rule); }
#app .rqr .rq-ar > :first-child { translate: 0 -1px; }
#app .rqr .rq-ar.rq-got { background: var(--good-tint); margin: 0 calc(-1 * var(--sp-24)); padding-left: var(--sp-24); padding-right: var(--sp-24); }
#app .rqr .rq-ar.rq-lost { background: var(--bad-tint); margin: 0 calc(-1 * var(--sp-24)); padding-left: var(--sp-24); padding-right: var(--sp-24); }
#app .rqr .rq-ar.rq-none { color: var(--text-faint); box-shadow: none; }
#app .rqr .rq-ar .nmc { min-width: 0; display: flex; align-items: baseline; column-gap: var(--sp-12); }
#app .rqr .rq-ar .nm { font-family: var(--font-body); overflow-wrap: anywhere; }
/* ⛔ BLUE = A NAME YOU CAN OPEN, and no ▾ after it */
#app .rqr .rq-ar button.nm { background: none; border: 0; padding: 0; text-align: left; color: var(--accent); font-size: inherit; }
#app .rqr .rq-ar button.nm:hover, #app .rqr .rq-ar button.nm[aria-expanded="true"] { color: var(--accent-hover); text-decoration: underline; text-underline-offset: 3px; }
#app .rqr .rq-ar .role { font-family: var(--font-display); font-size: var(--fs-2); letter-spacing: var(--track-caps); color: var(--text-faint); }
/* the count in the FOOTER: a fixed 40, the count centred in it and right-aligned on the role labels.
   The rule is an INSET SHADOW (a border pushed the centred count 0.5px down). */
#app .rqr .rq-ahd { flex: none; order: 2; display: flex; align-items: center; justify-content: flex-end; height: 40px;
                    box-shadow: inset 0 1px 0 var(--rule); margin: 0 calc(-1 * var(--sp-24)); padding: 0 var(--sp-24) 0 0; }
#app .rqr .rq-ahd .rq-cnum { height: 20px; font-family: var(--font-numbers); font-weight: 400; font-size: var(--fs-5); line-height: 1; translate: 0 -2px; }
#app .rqr .rq-cnum.rq-short { color: var(--warn); }
#app .rqr .rq-cnum.rq-full { color: var(--good); }
#app .rqr .rq-cnum.rq-missed { color: var(--bad); }
/* the timer: ONE bar, blue over amber, welded to the artist box's bottom */
#app .rqr .rq-timer { flex: none; height: 8px; margin: -8px 0 0; position: relative; background: var(--q3); overflow: hidden; }
#app .rqr .rq-timer i { position: absolute; left: 0; top: 0; height: 100%; }
#app .rqr .rq-timer .extra { background: var(--warn); opacity: .85; }
#app .rqr .rq-timer .main { background: var(--accent-fill); }

/* ── the right box: TYPE · CREDITS · FROM — OPEN (on the ground), from the song box's top line to the
   answer row. Text by INK: TYPE's caps 15px under the top line, the lowest ink 5px above the bottom. */
#app .rqr .rq-extra { grid-area: extra; margin-top: var(--sp-8); align-self: stretch; min-height: 0; box-sizing: border-box; background: transparent;
                      padding: calc(var(--sp-12) + 1px) var(--sp-16) var(--sp-4); display: flex; flex-direction: column; grid-row: 2 / 4; }
#app .rqr .rq-lab { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-2); letter-spacing: var(--track-caps); color: var(--text-faint); line-height: 1; }
#app .rqr .rq-extra .rq-val { margin: calc(var(--sp-4) + 3px) 0 calc(var(--sp-16) + 2px); font-family: var(--font-body); font-size: var(--fs-3); line-height: 1.3; color: var(--text); overflow-wrap: anywhere; }
#app .rqr .rq-extra .rq-val.unknown { color: var(--text-faint); }
/* ADR-0007: an instrumental round's reveal — the word beside the type, and the ghost-vocals flag (a control, so boxed) */
#app .rqr .rq-extra .rq-inst { color: var(--text-muted); }
#app .rqr .rq-extra .rq-flag { margin-left: var(--sp-8); padding: 2px var(--sp-8) 0; border: 0; border-radius: var(--r);
  background: var(--l3); color: var(--text); font: 400 var(--fs-2)/1.4 var(--font-display); letter-spacing: var(--track-caps); cursor: pointer; }
#app .rqr .rq-extra .rq-flag:hover { background: var(--hover-fill); }
#app .rqr .rq-extra .rq-flag.done { color: var(--text-faint); background: transparent; cursor: default; }
/* ⚠ it SCROLLS and never pushes FROM away (his catch: a 37-name credit shoved FROM out of the box).
   TYPE and CREDITS are white (07:01). */
#app .rqr .rq-extra .rq-credit2 { line-height: 1.45; flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: clip; margin: calc(var(--sp-4) + 3px) 0 0;
                                  scrollbar-width: thin; scrollbar-color: var(--border-subtle) transparent; }
#app .rqr .rq-extra .rq-credit2 .sep { color: var(--border-subtle); padding: 0 var(--sp-4); }
#app .rqr .rq-extra .rq-from { margin-top: auto; padding-top: calc(var(--sp-12) + 2px); border-top: 1px solid var(--rule); }
#app .rqr .rq-extra .rq-from .rq-val { margin-bottom: 0; }

/* ── the vote and the answer row ─────────────────────────────────────────────────────────── */
#app .rqr .rq-vote { grid-area: vote; align-self: start; margin-top: calc(var(--sp-8) + 2px); height: var(--ans-h); box-sizing: border-box;
                     display: flex; align-items: stretch; background: var(--q2); }
#app .rqr .rq-vote .q { flex: 1 1 auto; display: flex; align-items: center; padding: calc(var(--sp-2) + 1px) calc(var(--sp-12) + 2px) 0; font-family: var(--font-display);
                        font-size: var(--fs-4); letter-spacing: var(--track-caps); color: var(--text); white-space: nowrap; overflow: hidden; }
#app .rqr .rq-vote button { background: none; border: 0; border-left: 1px solid var(--rule); padding: calc(var(--sp-2) + 1px) var(--sp-16) 0;
                            font-family: var(--font-display); font-size: var(--fs-4); letter-spacing: var(--track-caps); }
#app .rqr .rq-vote button .c { font-family: var(--font-numbers); color: var(--text-muted); margin-left: var(--sp-8); }
#app .rqr .rq-vote button[aria-pressed="true"] { color: var(--good); }
/* 684 inside the 856 song box at 1920; the inset closes as the screen narrows and is gone by 1100,
   so the field is never squeezed to a stub (960 wide left it 256px) */
#app .rqr .rq-answer { grid-area: answer; justify-self: center; align-self: start; margin-top: calc(var(--sp-8) + 2px);
                       width: min(calc(var(--songw) - clamp(0px, (100vw - 1100px) * 0.21, 172px)), 100%);
                       height: var(--ans-h); display: flex; gap: var(--sp-8); }
#app .rqr .rq-skip { flex: 0 0 auto; display: flex; align-items: center; gap: calc(var(--sp-8) + 2px); padding: calc(var(--sp-2) + 1px) var(--sp-16) 0;
                     background: var(--q2); border: 1px solid transparent; font-family: var(--font-display); font-size: var(--fs-4); letter-spacing: var(--track-caps); }
#app .rqr .rq-skip:hover { border-color: var(--border); }
#app .rqr .rq-skip .n { font-family: var(--font-numbers); font-size: var(--fs-4); color: var(--text); min-width: 2.2em; text-align: right; letter-spacing: var(--track-tight); }
#app .rqr .rq-skip[aria-pressed="true"] { border-color: var(--good); color: var(--good); }
/* the vote is settled and the skip is under way: grey, done, not pressable (his, 2026-09-25) */
#app .rqr .rq-skip.passed, #app .rqr .rq-skip.passed:hover { border-color: transparent; color: var(--text-faint); cursor: default; }
#app .rqr .rq-skip.passed .n { color: var(--text-faint); }
/* ⛔ A FIELD, NOT A CARD: the lightest step on the ladder with an underline — SOLID, so the
   dropdown under it is readable too */
#app .rqr .rq-field { flex: 1 1 auto; min-width: 0; position: relative; }
#app .rqr .rq-field input { width: 100%; height: 100%; box-sizing: border-box; background: var(--l3); border: 0; border-radius: var(--r);
                            border-bottom: 2px solid var(--text-faint); padding: 0 var(--sp-16); margin: 0; color: var(--text);
                            font-family: var(--font-names); font-size: var(--fs-4); font-weight: 400; outline: none; }
#app .rqr .rq-field input:focus { border-bottom-color: var(--accent); }
#app .rqr .rq-field input::placeholder { color: var(--text-faint); }
#app .rqr .rq-field input:disabled { border-bottom-color: var(--border-subtle); }
#app .rqr .rq-drop { position: absolute; left: 0; right: 0; top: 100%; z-index: 16; /* over the thrown stickers (15), his 2026-09-25 */ margin: 0; padding: 0; list-style: none;
                     background: var(--l3); border: 1px solid var(--border); border-top: 0; max-height: 230px; overflow-y: auto; }
#app .rqr .rq-drop li { padding: calc(var(--sp-8) + 1px) var(--sp-16); font-family: var(--font-names); font-size: var(--fs-3); cursor: pointer; }
#app .rqr .rq-drop li.active, #app .rqr .rq-drop li.on, #app .rqr .rq-drop li:hover { background: var(--l2); color: #fff; }
#app .rqr .rq-drop:empty { display: none; }
#app .rqr .rq-spec { flex: 1 1 auto; display: flex; align-items: center; padding: 0 var(--sp-16); font-family: var(--font-body);
                     font-size: var(--fs-3); color: var(--text-muted); background: var(--q1); }
#app .rqr .rq-warn { position: fixed; left: 50%; top: 4px; translate: -50% 0; margin: 0; color: var(--warn); font-size: var(--fs-2); z-index: 4; }

/* ── the cards: card G ──────────────────────────────────────────────────────────────────── */
#app .rqr .rq-cards { grid-area: cards; display: flex; flex-direction: column; align-items: center; }
#app .rqr .rq-crow { display: flex; flex-wrap: wrap; gap: var(--cg); justify-content: center; }
#app .rqr .rq-card { width: var(--cw); height: var(--card-h); box-sizing: border-box; position: relative; overflow: hidden;
                     display: grid; grid-template-rows: var(--np-h) minmax(0, 1fr);
                     background: var(--q2); border: 2px solid transparent; }
#app .rqr .rq-plate2 { background: var(--q3); display: flex; align-items: center; gap: calc(var(--sp-8) + 2px); min-width: 0; padding: 0 var(--sp-12); }
#app .rqr .rq-plate2 > * { translate: 0 3px; }
#app .rqr .rq-name { flex: 1 1 auto; min-width: 0; font-family: var(--font-names); font-weight: 500; font-size: var(--fs-4); line-height: 1.15;
                     white-space: nowrap; overflow: hidden; color: var(--text); }
#app .rqr .rq-scl { flex: none; display: flex; align-items: baseline; gap: calc(var(--sp-4) + 2px); }
#app .rqr .rq-scl b { font-family: var(--font-numbers); font-weight: 400; font-size: min(var(--fs-6), calc(var(--np-h) * 0.78)); line-height: .9; color: var(--text); }
#app .rqr .rq-scl i { font-style: normal; font-family: var(--font-numbers); font-size: var(--fs-2); color: var(--good); }
#app .rqr .rq-card.half .rq-scl i { color: var(--warn); }
#app .rqr .rq-grow { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 0; }
/* ⛔ THE FACE IS SQUARE BY CONSTRUCTION — its width is taken from the row's height (his 07:17) */
#app .rqr .rq-av { height: 100%; aspect-ratio: 1; box-sizing: border-box; padding-bottom: var(--sp-2); position: relative; overflow: hidden;
                   display: grid; place-items: center; font-family: var(--font-names); font-weight: 500;
                   font-size: min(var(--fs-8), calc((var(--card-h) - var(--np-h)) * 0.36)); color: var(--bg); }
#app .rqr .rq-av img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
#app .rqr .rq-slot { min-width: 0; min-height: 0; display: flex; flex-direction: column; justify-content: center; padding: var(--sp-8) calc(var(--sp-8) + 3px); }
#app .rqr .rq-slot:has(.sa-strips) { padding: 0; }
#app .rqr .rq-ans { flex: 1 1 auto; min-height: 0; overflow: hidden; margin: 0; font-family: var(--font-body); font-size: var(--fs-3);
                    line-height: 1.28; color: var(--text-muted); display: flex; flex-direction: column; justify-content: flex-start; }
#app .rqr .rq-ans.one:not(.sa) { justify-content: center; }
#app .rqr .rq-ln { display: block; overflow-wrap: anywhere; }
#app .rqr .rq-ans:not(.sa) .rq-ln { translate: 0 -2px; }
#app .rqr .rq-ans.one .rq-ln { white-space: nowrap; overflow-wrap: normal; }
#app .rqr .rq-ans.none { color: var(--text-faint); justify-content: center; }
#app .rqr .rq-ln .rq-mark { display: inline-block; width: 1em; margin-right: var(--sp-4); font-style: normal; font-weight: 500; }
#app .rqr .rq-mark.rq-ok { color: var(--good); }
#app .rqr .rq-mark.rq-no { color: var(--bad); }
/* ⛔ WRAP IS AT MOST 3 ROWS, THEN IT SCROLLS (his 06:50) */
#app .rqr .rq-ans:not(.one):not(.sa):not(.none) { line-height: 1.25; max-height: 3.75em; overflow-y: auto; flex: 0 1 auto; }   /* sized to its rows, so a short answer sits centred like the 1-line one did */
#app .rqr .rq-ans.sa:not(.one) .rq-tx { display: block; line-height: 1.2; max-height: 3.6em; overflow-y: auto; }
#app .rqr .rq-ans:not(.one), #app .rqr .rq-ans.sa:not(.one) .rq-tx { scrollbar-width: thin; scrollbar-color: var(--border-subtle) transparent; }
/* S/A: a two-row table — a fixed mark column, the text beside it. ⛔ THE MARK NEVER RESIZES WITH THE
   ANSWER (fixed 18) and its two gaps are EQUAL BY CONSTRUCTION (slot padding − column gap = 0).
   ⚠ `clip` WITH AN INK MARGIN, NOT `hidden`: Mitr paints ~3px above its line box. */
#app .rqr .rq-ans.sa { gap: 0; }
#app .rqr .rq-ans.sa .rq-ln { flex: 1 1 0; min-height: 0; overflow: clip; overflow-clip-margin: 4px;
                              display: grid; grid-template-columns: 38px minmax(0, 1fr); align-items: center; column-gap: calc(var(--sp-8) + 3px); padding: 0 calc(var(--sp-8) + 3px); }
#app .rqr .rq-ans.sa .rq-ln + .rq-ln { border-top: 1px solid var(--rule); }
#app .rqr .rq-ans.sa .rq-ln:first-child { background: rgba(255, 255, 255, .03); }
#app .rqr .rq-ans.sa .rq-ln > * { translate: 0 -1px; }
#app .rqr .rq-ans.sa .rq-ln { padding-bottom: calc(var(--sp-2) + 1px); }   /* the pair, lifted together: measured +1.5–2px low */
#app .rqr .rq-ans.sa .rq-ln .rq-mark { width: auto; margin: 0; text-align: center; justify-self: center; align-self: center; font-size: var(--fs-3); translate: 0 0; }
#app .rqr .rq-ans.sa .rq-mark.rq-cnt { font-family: var(--font-numbers); font-weight: 400; font-size: var(--fs-3); letter-spacing: var(--track-tight); line-height: 1; translate: 0 1px; }
#app .rqr .rq-ans.sa .rq-ln > .rq-mark.rq-no:not(.rq-cnt) { translate: 0 -1px; }   /* the ✗ draws lower than the ✓ */
#app .rqr .rq-ans.sa .rq-tx { min-width: 0; overflow: hidden; }
#app .rqr .rq-ans.one.sa .rq-tx { white-space: nowrap; }
/* THE CARD IS THE RECEIPT — the 2px edge is its one signal (his 08:03). ⛔ ANSWERED = BLUE — his use pass
   2026-09-23 ("isn't the border when submitted the answer should be blue?"). The mockup had turned it faint on
   my reading of "blue = a name you can open, and the score"; on the real screen the faint edge was invisible. */
#app .rqr .rq-card.sent { border-color: var(--accent); }
#app .rqr .rq-card.rq-partial { border-color: var(--warn); border-style: dashed; }
#app .rqr .rq-card.hit { border-color: var(--good); }
#app .rqr .rq-card.halfv { border-color: var(--warn); }
#app .rqr .rq-card.miss { border-color: var(--bad); }
#app .rqr .rq-card.dc { opacity: .45; filter: grayscale(.7); }
/* stickers: one song's worth, drawn on the card, never catching a click */
#app .rqr .rq-card .pstk { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
/* ⚠ NO `translate` here: the global `.pstk-one` already centres it with `transform`, and the two STACK — the
   sticker landed a whole sticker-size up and left of the click (his use pass, "the placing still bugged"). */
/* 1.5x both ways (his, 2026-09-25: the Slap 'em UP! stickers "are meant to use at larger size") — emoji 31 -> 45, pictures 36 -> 54 */
#app .rqr .rq-card .pstk-one { position: absolute; font-size: var(--fs-8); line-height: 1; filter: drop-shadow(0 1px 0 rgba(0, 0, 0, .4)); }
#app .rqr .rq-card .pstk-one img { width: 54px; height: 54px; object-fit: contain; display: block; }

/* ── NARROW AND TALL (a half or a third of the screen, full height): board | middle, the right box
   under the middle, the cards 2 × 2. Horizontal matters more than vertical (his 08:12). ─────── */
@media (max-width: 1000px) and (min-height: 700px) {
  #app .rqr {
    --cw: calc((100vw - 2 * var(--side) - var(--cg)) / 2);
    --card-h: min(168px, max(96px, 14dvh), calc(var(--cw) * 0.35 + var(--np-h)));
    --plate-h: clamp(56px, 9dvh, 104px);
    grid-template-columns: minmax(0, 0.8fr) 16px minmax(0, 1.4fr);
    grid-template-rows: auto auto minmax(0, 1fr) auto calc(var(--ans-h) + 10px) auto var(--cards-gap) auto;
    grid-template-areas:
      "acts  .  mine"
      "plate plate plate"
      "stand .  info"
      "stand .  extra"
      "answer answer answer"
      "vote vote vote"
      ".     .  ."
      "cards cards cards";
  }
  #app .rqr .rq-plateblock { width: 100%; margin-top: var(--sp-8); }
  #app .rqr .rq-song { width: 100%; }
  /* ⛔ his use pass: at half / two-thirds width the right box got a 0.55fr sliver — CREDITS grew a scrollbar and
     FROM was cut off. Here it sizes to its CONTENT (the artist box above takes the flex), TYPE and FROM share a
     row, and CREDITS runs under them, capped at 3 lines then scrolling. */
  #app .rqr .rq-extra { grid-area: extra; background: var(--q1); margin-top: var(--sp-8); padding: calc(var(--sp-12) + 1px) var(--sp-16) var(--sp-8);
                        display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-auto-flow: row; column-gap: var(--sp-16); align-content: start; }
  #app .rqr .rq-extra > :nth-child(1) { grid-column: 1; grid-row: 1; }
  #app .rqr .rq-extra > :nth-child(2) { grid-column: 1; grid-row: 2; margin-bottom: var(--sp-8); }
  #app .rqr .rq-extra > .rq-from { grid-column: 2; grid-row: 1 / 3; margin: 0; padding: 0; border: 0; }
  #app .rqr .rq-extra > :nth-child(3) { grid-column: 1 / -1; grid-row: 3; }
  #app .rqr .rq-extra .rq-credit2 { grid-column: 1 / -1; grid-row: 4; max-height: 4.35em; flex: none; }
  #app .rqr .rq-answer { width: 100%; }
  #app .rqr .rq-vote { margin-top: 0; margin-bottom: var(--sp-8); }
  #app .rqr .rq-acts, #app .rqr .rq-mine { margin-top: 0; }
  #app .rqr .rq-crow { row-gap: var(--sp-12); }
  #app .rqr .rq-also { left: auto; right: 0; top: calc(100% + 6px); }
}

/* ── SHORT (a quarter of the screen, ~420–600 tall): the middle stack tightens so the artist box keeps
   whole rows — the song name and the rows step down one size (22 → 18), the footer 40 → 28. ──── */
@media (max-height: 600px) {
  #app .rqr .rq-songname { padding: calc(var(--sp-4) + 3px) var(--sp-24) calc(var(--sp-8) + 1px); font-size: var(--fs-3); }
  #app .rqr .rq-abox { padding-top: var(--sp-2); }
  #app .rqr .rq-ar { padding: calc(var(--sp-4) + 1px) 0; font-size: var(--fs-3); }
  #app .rqr .rq-ahd { height: 28px; }
  #app .rqr .rq-ahd .rq-cnum { font-size: var(--fs-4); height: 16px; }
  #app .rqr .rq-extra .rq-val { margin-bottom: calc(var(--sp-8) + 2px); }
}

/* ── THE SONG HISTORY WINDOW — ported from round-final.html. SOLID (it floats over content). Fixed to the
   viewport, dragged by its title bar, resized from the corner; the round stays live behind it. ──── */
#app .rq-win { position: fixed; z-index: 22; display: flex; flex-direction: column; gap: var(--sp-12); box-sizing: border-box;
               min-width: 460px; min-height: 260px; max-width: calc(100vw - 8px); max-height: calc(100dvh - 8px); padding: 0 calc(var(--sp-16) + 2px) var(--sp-16);
               background: var(--l2); border: 1px solid var(--border); resize: both; overflow: hidden; color: var(--text); font-family: var(--font-body); }
#app .rq-win .rq-dh { cursor: move; user-select: none; touch-action: none; margin: 0 calc(-1 * (var(--sp-16) + 2px)); padding: calc(var(--sp-8) + 2px) var(--sp-12) calc(var(--sp-8) + 2px) calc(var(--sp-16) + 2px);
                      background: var(--l3); display: flex; align-items: center; justify-content: space-between; }
#app .rq-win .rq-dh h2 { margin: 0; font-family: var(--font-display); font-weight: 400; font-size: var(--fs-4); letter-spacing: var(--track-caps); }
#app .rq-win .rq-x { background: none; border: 1px solid var(--border-subtle); width: 36px; height: 36px; padding: 0; font-size: var(--fs-3); border-radius: var(--r); color: var(--text); }
#app .rq-win .rq-x:hover { border-color: var(--border); }
#app .rq-win .rq-htabs { display: flex; gap: 0; border-bottom: 1px solid var(--rule); }
#app .rq-win .rq-htabs button { background: none; border: 0; border-bottom: 2px solid transparent; border-radius: var(--r); padding: var(--sp-8) calc(var(--sp-12) + 2px); margin: 0;
                                font-family: var(--font-display); font-size: var(--fs-2); letter-spacing: var(--track-caps); color: var(--text-muted); }
#app .rq-win .rq-htabs button.on { border-bottom-color: var(--text); color: var(--text); background: var(--l3); }
#app .rq-win .rq-hscroll { overflow-y: auto; min-height: 0; flex: 1 1 auto; scrollbar-width: thin; scrollbar-color: var(--border-subtle) transparent; }
#app .rq-win .rq-hempty { margin: calc(var(--sp-16) + 2px) var(--sp-8); color: var(--text-faint); font-size: var(--fs-3); }
/* ⚠ ONE COLUMN GRID FOR EVERY TABLE: past games stack several, and auto layout gave each its own widths */
#app .rq-win .rq-htable { width: 100%; border-collapse: collapse; font-size: var(--fs-3); table-layout: fixed; }
#app .rq-win .rq-htable col.c-n { width: 52px; } #app .rq-win .rq-htable col.c-ty { width: 64px; } #app .rq-win .rq-htable col.c-xy { width: 56px; }
#app .rq-win .rq-htable col.c-an { width: 22%; } #app .rq-win .rq-htable col.c-sg { width: 18%; } #app .rq-win .rq-htable col.c-ar { width: 15%; }
#app .rq-win .rq-htable col.c-fr { width: 11%; } #app .rq-win .rq-htable col.c-ya { width: 17%; }
#app .rq-win .rq-htable th { text-align: left; font-family: var(--font-display); font-weight: 400; font-size: var(--fs-2); letter-spacing: var(--track-caps);
                             color: var(--text-faint); padding: calc(var(--sp-4) + 2px) var(--sp-8); border-bottom: 1px solid var(--rule); }
#app .rq-win .rq-htable td { padding: calc(var(--sp-8) + 1px) var(--sp-8); border-bottom: 1px solid var(--rule); vertical-align: top; overflow-wrap: break-word; }
#app .rq-win .rq-htable td.n, #app .rq-win .rq-htable td.xy { font-family: var(--font-numbers); font-size: var(--fs-3); color: var(--text-muted); }
#app .rq-win .rq-htable td.ty { font-family: var(--font-display); letter-spacing: var(--track-caps); color: var(--text-muted); white-space: nowrap; }
#app .rq-win .rq-htable td.ar, #app .rq-win .rq-htable td.fr, #app .rq-win .rq-htable td.ya { color: var(--text-muted); }
#app .rq-win .rq-htable td.fr { font-family: var(--font-names); }
#app .rq-win .rq-htable td.ya .ln { display: block; }
/* ⛔ BLUE = A NAME YOU CAN OPEN — the MAL title and a band with members. The title is typed data, so it
   takes the names face, never Bebas (`BOCCHI THE ROCK!` in caps was the screenshot's catch) */
#app .rq-win .rq-htable td.an a.rq-mal { color: var(--accent); text-decoration: none; font-family: var(--font-names); }
#app .rq-win .rq-htable td.an a.rq-mal:hover { text-decoration: underline; text-underline-offset: 3px; color: var(--accent-hover); }
#app .rq-win .rq-htable td.ar button { background: none; border: 0; padding: 0; margin: 0; text-align: left; color: var(--accent); font: inherit; cursor: pointer; border-radius: var(--r); }
#app .rq-win .rq-htable td.ar button:hover { color: var(--accent-hover); text-decoration: underline; text-underline-offset: 3px; }
/* ✅ THE WHOLE ROW WEARS YOUR RESULT (AMQ's red/green rows), tinted not filled; hover BRIGHTENS it —
   his note: AMQ "doesn't go grey, they go brighter red/green" */
#app .rq-win .rq-htable tr.rq-hrow { cursor: pointer; }
#app .rq-win .rq-htable tr.rq-hrow.rq-hok td { background: var(--good-tint); }
#app .rq-win .rq-htable tr.rq-hrow.rq-hno td { background: var(--bad-tint); }
#app .rq-win .rq-htable tr.rq-hrow.rq-hok:hover td { background: var(--good-tint-hover); }
#app .rq-win .rq-htable tr.rq-hrow.rq-hno:hover td { background: var(--bad-tint-hover); }
#app .rq-win .rq-htable tr.rq-hrow.rq-hnone:hover td { background: var(--l3); }
#app .rq-win .rq-htable tr.rq-hdetrow td { background: var(--l1); padding: calc(var(--sp-8) + 2px) calc(var(--sp-12) + 2px) calc(var(--sp-12) + 2px) calc(var(--sp-24) + 6px); }
#app .rq-win .rq-hdet { display: grid; grid-template-columns: max-content 1fr; column-gap: var(--sp-16); row-gap: var(--sp-4); font-size: var(--fs-2); }
#app .rq-win .rq-hdet .who { font-family: var(--font-names); font-weight: 500; color: var(--text); }
#app .rq-win .rq-hdet .a { color: var(--text-muted); }
#app .rq-win .rq-hdet .a .ln { display: block; }
#app .rq-win .rq-hdet .a i { font-style: normal; margin-right: var(--sp-4); }
#app .rq-win .rq-hdet .a i.rq-ok { color: var(--good); } #app .rq-win .rq-hdet .a i.rq-no { color: var(--bad); }
/* ✅ AMQ's PAST GAMES: every game is a CENTRED DIVIDER — a rule, WHEN, a rule — its table under it */
#app .rq-win button.rq-past { width: 100%; display: flex; justify-content: center; align-items: center; gap: calc(var(--sp-12) + 2px);
                              padding: calc(var(--sp-12) + 2px) var(--sp-8) calc(var(--sp-8) + 2px); margin: 0; background: none; border: 0; border-radius: var(--r); cursor: pointer; color: var(--text); }
#app .rq-win .rq-past::before, #app .rq-win .rq-past::after { content: ''; flex: 1 1 0; height: 1px; background: var(--border-subtle); align-self: center; }
#app .rq-win .rq-past .when { font-family: var(--font-display); font-size: var(--fs-5); letter-spacing: var(--track-caps); line-height: 1; }
#app .rq-win .rq-past .n { font-family: var(--font-display); font-size: var(--fs-3); letter-spacing: var(--track-caps); color: var(--text-muted); }
#app .rq-win button.rq-past:not(.open) .when { color: var(--text-muted); }
#app .rq-win .rq-pastbody { margin-bottom: var(--sp-8); }

/* ── THE ROUND'S DRAWERS (settings, stickers) — ported from round-final.html: SOLID, 460 wide, a head row
   (the title, or the ☺ balance), group labels in the caps voice, one plain row per setting. ─────── */
#app .drawer.rqd { width: min(460px, 100vw); box-sizing: border-box; background: var(--l2); border-left: 1px solid var(--border);
                   padding: calc(var(--sp-16) + 4px) calc(var(--sp-16) + 6px); display: flex; flex-direction: column; gap: var(--sp-12); overflow-y: auto; }
#app .drawer.rqd .set-head { display: flex; align-items: center; justify-content: space-between; margin: 0; }
#app .drawer.rqd .set-head h2 { margin: 0; font-family: var(--font-display); font-weight: 400; font-size: var(--fs-5); letter-spacing: var(--track-caps); }
#app .drawer.rqd .set-x { background: none; border: 1px solid var(--border-subtle); width: 36px; height: 36px; padding: 0; font-size: var(--fs-3); border-radius: var(--r); }
#app .drawer.rqd .set-x:hover { border-color: var(--border); }
#app .drawer.rqd .set-group { margin: var(--sp-4) 0 0; padding: 0; border: 0; font-family: var(--font-display); font-size: var(--fs-2); letter-spacing: var(--track-caps); color: var(--text-faint); text-transform: none; }
#app .drawer.rqd .set-group::after { display: none; }
#app .drawer.rqd .set-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-12); padding: calc(var(--sp-8) + 2px) 0; margin: 0;
                            border-bottom: 1px solid var(--rule); font-size: var(--fs-3); }
#app .drawer.rqd .set-row .lbl { font-weight: 400; }
#app .drawer.rqd .set-row .vol { display: flex; align-items: center; gap: var(--sp-12); }
#app .drawer.rqd .set-row .vol input { width: 180px; accent-color: var(--accent); margin: 0; }
#app .drawer.rqd .set-val { font-family: var(--font-numbers); font-weight: 400; font-size: var(--fs-4); min-width: 1.5em; text-align: right; }
/* the sticker balance: the ☺ matched to the number's INK height (08:04 his "same size as 3") */
#app .drawer.rqd .stk-charges { display: flex; align-items: center; gap: calc(var(--sp-8) + 2px); }
#app .drawer.rqd .stk-charges .ico { font-size: var(--fs-7); line-height: 1; color: var(--text-muted); display: inline-block; transform: scale(1.15); translate: 0 -7.5px; }
#app .drawer.rqd .stk-charges .stk-bal { font-family: var(--font-numbers); font-weight: 400; font-size: var(--fs-7); line-height: 1; color: var(--text); }
#app .drawer.rqd .stk-head { margin: 0; padding: 0; }
#app .drawer.rqd .stk-q { width: 100%; height: 44px; box-sizing: border-box; background: var(--l3); border: 0; border-radius: var(--r); border-bottom: 2px solid var(--text-faint);
                          padding: 0 calc(var(--sp-12) + 2px); margin: 0; color: var(--text); font-family: var(--font-body); font-size: var(--fs-3); outline: none; }
#app .drawer.rqd .stk-q:focus { border-bottom-color: var(--accent); }
#app .drawer.rqd .stk-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: calc(var(--sp-4) + 2px); overflow-y: auto; padding: 0; }
#app .drawer.rqd .stk-item { aspect-ratio: 1; background: var(--l1); border: 1px solid var(--rule); border-radius: var(--r); font-size: var(--fs-6); padding: 0;
                             display: grid; place-items: center; line-height: 1; }
#app .drawer.rqd .stk-item:hover:not(:disabled) { border-color: var(--border); }
#app .drawer.rqd .stk-item img { width: 70%; height: 70%; object-fit: contain; }
/* the picker's Emoji | Stickers tabs (2026-09-25): INK = where you are, /list's segment — a lighter fill over a 2px
   --text rule; Bebas sits ~1.4px high in a flex box, so 4px on top re-centres it by ink */
#app .drawer.rqd .stk-tabs { display: flex; gap: 2px; }
#app .drawer.rqd .stk-tab { flex: 1; min-height: 44px; margin: 0; padding: var(--sp-4) var(--sp-16) 0; border: 0; border-radius: var(--r);
  background: var(--l1); color: var(--text-muted); font-family: var(--font-display); font-weight: 400; font-size: var(--fs-3);
  letter-spacing: var(--track-caps); line-height: 1; cursor: pointer; }
#app .drawer.rqd .stk-tab:hover { color: var(--text); }
#app .drawer.rqd .stk-tab.on { background: var(--l3); color: var(--text); box-shadow: inset 0 -2px 0 var(--text); }
/* the tabs + search stay put; only the stickers scroll */
#app .drawer.stk-drawer.rqd { overflow: hidden; }
#app .drawer.rqd .stk-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--sp-8); }
#app .drawer.rqd .stk-body .stk-grid { overflow: visible; flex: none; }
#app .drawer.rqd .stk-sec { margin: var(--sp-4) 0 0; font-family: var(--font-display); font-weight: 400; font-size: var(--fs-2);
  letter-spacing: var(--track-caps); color: var(--text-faint); line-height: 1; }
#app .drawer.rqd .stk-sec:first-child { margin-top: 0; }
/* ★ pin: top-right of the tile, shown on hover; a pinned one always shows it, and a touch screen always does */
/* ⚠ content-visibility: the full emoji set is ~1,870 tiles, and laying out + shaping every glyph made the picker take
   2 s to open; off-screen tiles now cost nothing until scrolled to (the size hint keeps the scrollbar honest) */
#app .drawer.rqd .stk-cell { position: relative; min-width: 0; content-visibility: auto; contain-intrinsic-size: auto 64px; }
#app .drawer.rqd .stk-cell .stk-item { width: 100%; }
#app .drawer.rqd .stk-pin { position: absolute; top: 1px; right: 1px; width: 22px; height: 22px; margin: 0; padding: 0; border: 0;
  background: none; color: var(--text-muted); font-size: var(--fs-2); line-height: 1; cursor: pointer; opacity: 0; display: grid; place-items: center; }
#app .drawer.rqd .stk-cell:hover .stk-pin, #app .drawer.rqd .stk-pin:focus-visible { opacity: 1; }
#app .drawer.rqd .stk-pin:hover { color: var(--text); }
#app .drawer.rqd .stk-pin.on { opacity: 1; color: var(--text); }
@media (hover: none) { #app .drawer.rqd .stk-pin { opacity: 1; } }
/* "Nothing matches": one line, the search box's size (his, 2026-09-25: bigger, one line) */
#app .drawer.rqd .stk-grid .stk-empty { grid-column: 1 / -1; white-space: nowrap; margin: var(--sp-8) 0; font-size: var(--fs-3); color: var(--text-muted); }

/* ── the lobby's top-row tools: the round's glyph buttons (no box; hover lifts to white on a faint plate) ── */
#app .lb .lbtools { display: flex; align-items: center; gap: var(--sp-8); }
/* the tool pages' stand-in for it: the same width (3 × 44 + 2 gaps), nothing in it — see pagenav.js */
#app .lb.tmast .lbtools-slot { flex: none; width: calc(3 * 44px + 2 * var(--sp-8)); height: 40px; }
#app .lb .lbtools .rq-act { width: 44px; height: 40px; display: grid; place-items: center; padding: 0; margin: 0;
                            background: none; border: 0; border-radius: var(--r); color: var(--text-muted); font-size: var(--fs-4); line-height: 1; }
#app .lb .lbtools .rq-act:hover { color: #fff; background: var(--l2); }
#app .lb .lbtools .rq-act.open { color: var(--text); } /* colour pass 1b */
#app .lb .lbtools .rq-act[data-d="hist"] { padding-bottom: calc(var(--sp-4) + .5px); }
/* the seat cards are still `ul.players > li` (patchPlayers finds them by it), so the old list row's hairline
   (`.players li { border-bottom }`) drew under every card — a structural line on a fill-only object */
#app .lb .seat { border-bottom: 0; }
/* ⛔ THE LOBBY IS A FIXED FRAME LIKE THE ROUND (his rule: no page scrolls), so the reserved scrollbar lane is 15px
   of nothing that pulled its right edge in from the round's (1881 against 1896, measured). And the RMQ mark sits ON
   the 24px edge, as the round's ⌂ does — its hit area extends inward instead. */
html:has(#app.lobby) { scrollbar-gutter: auto; }
#app .lb .home { padding-left: 0; }
#app .lb .lbtools .rq-act[data-d="stk"] { padding-bottom: var(--sp-4); }   /* the ☺ glyph draws 2.5px low in its em box (measured by ink) */
/* the round's FIELD language in the room settings panel too: a field carries a 2px underline, the accent on focus */
#app .lb .panel input, #app .lb .panel select { border: 0; border-bottom: 2px solid var(--text-faint); border-radius: var(--r); }
#app .lb .panel input:focus, #app .lb .panel select:focus { outline: none; border-bottom-color: var(--accent); }

/* ── THE END SCREEN, restyled in place with the round's language (the port, 2026-09-23). Its layout is his (two
   ranked columns, the countdown in the tab); what changes is the box language: the tab and the plate at the
   round's sizes so the last reveal does not jump, cards FILL ONLY with a 2px edge that MEANS something — a
   placing, 1st–3rd — and no "…" on a name (render.js shrinks it instead). ── */
#app.gameflow .gs.endview .gs-tab { box-sizing: border-box; height: 41px; min-width: 206px; padding-top: calc(var(--sp-2) + 1px); border: 0;
                                    background: rgba(255, 255, 255, .08); font-size: var(--fs-5); }
#app.gameflow .gs.endview .gs-plate { width: min(696px, 100%); height: 104px; border: 0; background: rgba(255, 255, 255, .08); }
#app.gameflow .gs.endview .pcard { border-width: 2px; border-style: solid; background: rgba(255, 255, 255, .08); }
#app.gameflow .gs.endview .pcard:not(.medal1):not(.medal2):not(.medal3) { border-color: transparent; }
#app.gameflow .gs.endview .pcard .pname { text-overflow: clip; }
/* his use pass 2026-09-23 ("skip is a little weird when it has no 1/1"): the empty count held its 2.2em, so the
   word sat left in a box too wide for it. The button keeps ONE width (the width it has with a count, so nothing
   jumps when a vote lands) and centres whatever it holds; an empty count takes no room. */
#app .rqr .rq-skip { min-width: 142px; justify-content: center; }
#app .rqr .rq-skip .n:empty { display: none; }
/* his use pass 2026-09-23: "the card on lobby is too long. try make it the size for 3 card per row" — the grid above
   is 3 now (was 2, his 2026-09-09 call); one number to reverse. */
/* ⏳ TRIAL, his "i'm not sure yet": the room-settings VALUES (Anime titles / S/A, Random / Equal, From start …) in the
   same face as the setting labels. They are words we wrote, so the display face may draw them. One rule to reverse. */
#app .lb .panel select { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-4); letter-spacing: var(--track-caps); }
#app .lb .panel select option { font-family: var(--font-display); font-size: var(--fs-4); letter-spacing: var(--track-caps); }   /* his "the dropdown is still the same font" */
/* the round's ONE sticker layer (his use pass: a sticker on a card's edge was cropped by the card). Fixed over the
   round, above its boxes and below every drawer / window (z 20+); it never catches a click. */
#app .rqr .rq-stickers { position: fixed; inset: 0; pointer-events: none; z-index: 15; }
#app .rqr .rq-stickers .pstk-one { position: fixed; font-size: var(--fs-8); line-height: 1; filter: drop-shadow(0 1px 0 rgba(0, 0, 0, .4)); }
#app .rqr .rq-stickers .pstk-one img { width: 54px; height: 54px; object-fit: contain; display: block; }
/* the carried sticker over the typing box: red, and the drop is refused (never moved) */
#stk-carry.no { filter: drop-shadow(0 0 0 var(--bad)) drop-shadow(0 0 3px var(--bad)); opacity: .7; }
#app .lb .seat .who { overflow: hidden; }   /* the fitter measures the overflow; a name never draws over the count */

/* ── THE TOP-ROW ICONS (render.js iconSvg): a 3px stroke that never scales — the nav's Bebas stem, measured — so an
   icon weighs what the words beside it weigh (his use pass 2026-09-23). The glyph offsets that corrected the old
   font glyphs are cancelled: an SVG sits where its box puts it. ── */
#app .rq-act .ico { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 3px; vector-effect: non-scaling-stroke;
                    stroke-linecap: square; stroke-linejoin: miter; display: block; overflow: visible; }
#app .rq-act .ico * { vector-effect: non-scaling-stroke; }
#app .rqr .rq-act[data-d="hist"], #app .rqr .rq-act.rq-home, #app .lb .lbtools .rq-act[data-d="hist"], #app .lb .lbtools .rq-act[data-d="stk"] { padding-bottom: 0; }
#app .rqr .rq-mine .rq-act.stk { padding-bottom: 0; }
#app .rqr .rq-act.stk .cnt { translate: 0 1.5px; }   /* Bebas digits draw ~1.4px high in a centred row */
/* ── the round's volume at the icons' weight (his use pass 2026-09-23: "make the slider and muted to match the button
   too"): the word in the nav's Bebas at 22 with a fixed width (MUTE ↔ MUTED must not shove the slider), a 4px track
   with a square thumb (the round is square), the filled part in the accent drawn from --v (main.js applyVolume). ── */
body:has(#app .rqr) #volume button { font-size: var(--fs-4); min-width: 58px; text-align: right; }   /* = MUTED (53, measured) + its padding */
body:has(#app .rqr) #volume input { -webkit-appearance: none; appearance: none; width: 96px; height: 16px; background: transparent; cursor: pointer; }
body:has(#app .rqr) #volume input::-webkit-slider-runnable-track { height: 4px; background: linear-gradient(to right, var(--accent) var(--v, 70%), var(--l3) var(--v, 70%)); }
body:has(#app .rqr) #volume input::-moz-range-track { height: 4px; background: var(--l3); }
body:has(#app .rqr) #volume input::-moz-range-progress { height: 4px; background: var(--accent); }
body:has(#app .rqr) #volume input::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; margin-top: calc(-1 * (var(--sp-4) + 1px)); background: var(--text); border: 0; border-radius: var(--r); }
body:has(#app .rqr) #volume input::-moz-range-thumb { width: 14px; height: 14px; background: var(--text); border: 0; border-radius: var(--r); }
/* muted keeps the level (his rule) and loses its colour */
body.muted:has(#app .rqr) #volume input::-webkit-slider-runnable-track { background: linear-gradient(to right, var(--border-subtle) var(--v, 70%), var(--l3) var(--v, 70%)); }
body.muted:has(#app .rqr) #volume input::-webkit-slider-thumb { background: var(--text-faint); }
body.muted:has(#app .rqr) #volume input { opacity: 1; }
/* his use pass 2026-09-23: "the slider could be longer. the sticker just shouldn't exceed the 'type'". The right cluster
   now runs from the right box's TYPE (☺'s glyph lands on TYPE's left edge: the box's 16px inset minus ☺'s 10px pad) to
   the screen edge, and the SLIDER takes the spare room — capped at 320 so a narrow layout does not grow a runway. The
   #volume overlay is outside #app, so rqFit hands it the slot's measured width (--volw). */
#app .rqr .rq-mine { justify-self: stretch; justify-content: flex-end; margin-left: calc(var(--sp-4) + 2px); }
#app .rqr .rq-volslot { flex: 1 1 auto; min-width: 126px; max-width: 320px; }   /* 126 = a 60px slider at the least; below ~1500 wide the cluster may pass TYPE by that much */
body:has(#app .rqr) #volume input { width: calc(var(--volw, 162px) - 66px); }
/* ⛔ A MESSAGE NEVER PUSHES THE SCREEN — his use pass 2026-09-23 ("when there's a error feedback or smth. it shouldn't
   push the top. make it a pop up, a toast on bottom right"). The lobby's top-level notices (the refused start, a
   rename error) float bottom-right, solid (it floats over content), with the warn colour on its edge. */
#app.lobby > p.warn { position: fixed; right: clamp(10px, 1.25vw, 24px); bottom: clamp(10px, 1.25vw, 24px); z-index: 40; margin: 0;
                      max-width: min(420px, calc(100vw - 32px)); box-sizing: border-box; padding: var(--sp-12) var(--sp-16);
                      background: var(--l3); border-left: 2px solid var(--warn); color: var(--text); font-size: var(--fs-2); line-height: var(--lh-ui); }
/* his use pass 2026-09-23 ("why the allignment of the name feels weird?"): the mockup's 3px push, measured on the REAL page,
   put the name 3.9px LOW (ink, bottom-edge row excluded). Unpushed it sits +0.9; the Bebas score needs half the push. */
#app .rqr .rq-plate2 > .rq-name { translate: 0 0; }
#app .rqr .rq-plate2 > .rq-scl { translate: 0 1.5px; }

/* ════════════════════════════════════════════════════════════════════════════════════════════════
   THE TOOL PAGES' MASTHEAD — the lobby's own row (pagenav.js wraps it in `.lb.tmast`), so the tools
   tab and the game tab read as one place (list-design.html §1). ← GAME sits in the wordmark's slot.
   ════════════════════════════════════════════════════════════════════════════════════════════════ */
#app .lb.tmast { padding: var(--sp-32) 0 0; width: 100%; flex: 0 0 auto; }   /* the lobby's 8 + 24 */
/* ⛔ THE TOP ROW MUST NOT MOVE WHEN YOU SWITCH TABS (his, 2026-09-23), AND THE LOBBY IS THE REFERENCE — never
   changed to meet a tool page ("i never meant to change lobby"). The first slot is the lobby's own RMQ wordmark
   (pagenav.js), so every x matches by construction; the account block keeps the lobby's content-box (the tool
   pages set `border-box` on everything, which made it 16px narrower). */
#app .lb.tmast .idg { box-sizing: content-box; }
/* ⛔ OPTICAL CENTRING OF THE TOP ROW — VERTICAL ONLY, lobby and tool pages alike, so no x moves anywhere.
   Measured by ink in the 60px row (his, 2026-09-23: "the add,lib,list and the text in pfp top right isn't
   allign … it isn't middle"): the Bebas nav drew 2.25px HIGH, the name 0.75px LOW, the tile letter 2px HIGH.
   Padding on one side of a flex-centred box moves its ink half that much. */
#app .lb .mrow.top .navb a { padding-top: var(--sp-4); }
/* the nav sits one step further from the wordmark (his, 2026-09-23: "move these on every page to the right a
   little") — 12px puts ADD's ink on x 176, the seat card's name line below it (measured) */
/* ⛔ RMQ IS CENTRED between the card border and ADD's box (his, 2026-09-23: "keep it in the middle between the
   left card border and before the 'add' invisible box"). The slot is FIXED so the nav never moves with it: it runs
   from the edge to ADD's box (128 = 112 + the row's 16 gap, which the nav gives back), and the word centres in it. */
#app .lb .mrow.top > .home { flex: 0 0 128px; width: 128px; padding: 0; justify-content: center; box-sizing: border-box; }
#app .lb .mrow.top > .navb { margin-left: calc(var(--sp-16) * -1); }
#app .lb .idg .nm { padding-bottom: var(--sp-2); }
#app .lb .idg .tile { padding-top: var(--sp-4); box-sizing: border-box; }
/* RMQ on a tool page is a BUTTON wearing the lobby's wordmark (it closes this tab; there is no href to fork) */
#app .lb.tmast .home.nav-back { background: none; border: 0; border-radius: var(--r); margin: 0; font: inherit; color: var(--text);
  cursor: pointer; min-height: 0; box-sizing: content-box; }
#app .lb.tmast .home.nav-back:hover { background: none; color: var(--accent); }
/* the old `.pagenav` row drew bordered boxes; inside the masthead the lobby's underline nav wins */
#app .lb.tmast .navb.pagenav { margin: 0 0 0 calc(var(--sp-16) * -1); padding: 0; border: 0; gap: 0; flex-wrap: nowrap; }
#app .lb.tmast .navb.pagenav a { background: none; border: 0; border-bottom: 2px solid transparent; border-radius: var(--r);
  min-height: 0; padding: var(--sp-4) var(--sp-24) 0; overflow: visible; font-weight: 400; }
#app .lb.tmast .navb.pagenav a:hover { background: none; color: var(--text); border-bottom-color: var(--text); }
#app .lb.tmast .navb a.nav-here { color: var(--text); border-bottom-color: var(--text); cursor: default; }
#app .lb.tmast .idg .tile { padding: var(--sp-4) 0 0; border: 0; border-radius: var(--r); cursor: pointer; }
#app .lb.tmast .idg button.tile .pen { position: absolute; inset: 0; display: grid; place-items: center; opacity: 0;
  background: rgba(9, 8, 16, .66); color: var(--text); font-size: var(--fs-4); z-index: 1; padding: 0; }
#app .lb.tmast .idg button.tile:hover .pen, #app .lb.tmast .idg button.tile:focus-visible .pen { opacity: 1; }
#app .lb.tmast .amenu .quiet.danger { color: var(--bad); }
/* your own tile in the LOBBY (the masthead and your seat card) is the picture control too (his, 2026-09-25): the tool
   pages' ✎-on-hover, on a button that must look exactly like the span it replaces */
#app .lb button.picbtn { border: 0; cursor: pointer; }
/* a button CENTRES its content where the span top-aligned it: measured, the masthead letter rose 6px — 12px more top
   padding (a centred box moves half of it) puts it back on the span's pixel */
#app .lb .idg > button.picbtn { padding-top: calc(var(--sp-4) + 12px); }
#app .lb button.picbtn .pen { position: absolute; inset: 0; display: grid; place-items: center; opacity: 0;
  background: rgba(9, 8, 16, .66); color: var(--text); font-size: var(--fs-4); z-index: 1; padding: 0; }
#app .lb button.picbtn:hover .pen, #app .lb button.picbtn:focus-visible .pen { opacity: 1; }

/* ════════════════════════════════════════════════════════════════════════════════════════════════
   /list — built 2026-09-23 from public/list-design.html + DESIGN.md § "What the round settled".
   ⛔ A FIXED FRAME: the screen never scrolls; only the rows pane does (his rule, said many times).
   ⛔ Every rule is under `body.listpage #app` — the bare names (.srow .pick .t .sub .count .row) are
   the old page's and style.css's globals, and they still exist for other surfaces.
   Type: anime TITLES are names (Kanit) · song names and artists are text (Mitr) · labels, buttons and
   numbers are Bebas, one step up from the mockup (it was drawn in Anton, which is heavier).
   ════════════════════════════════════════════════════════════════════════════════════════════════ */
html:has(body.listpage) { scrollbar-gutter: auto; overflow: clip; }
body.listpage { overflow: clip; }
/* the lobby's own side edge, so nothing on the top row moves between the two tabs at any width */
body.listpage #app { --edge: clamp(10px, 1.25vw, 24px); max-width: none; width: 100%; margin: 0; padding: 0 var(--edge); box-sizing: border-box;
  height: 100vh; height: 100dvh; display: flex; flex-direction: column; min-height: 0; }
body.listpage #app * { box-sizing: border-box; }
body.listpage #app .lab { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-2);
  letter-spacing: var(--track-wider); text-transform: uppercase; color: var(--text-faint); }
:is(body.listpage, body.libpage) #app .sp { flex: 1 1 auto; }

/* ── buttons: three ranks, told apart by FILL, never by a border ─────────────────────────────── */
body.listpage #app :where(.lbtn, .lbtn2, .lbtn3, .lcta, .lcta2, .lpage, .lseg button, .listpick, .slot, .slotmore), body.libpage #app :where(.lbtn, .lbtn2, .lbtn3, .lpage, .lseg button) {
  font-family: var(--font-display); font-weight: 400; letter-spacing: var(--track-caps); text-transform: uppercase;
  border: 0; border-radius: var(--r); margin: 0; cursor: pointer; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center; }
body.listpage #app :is(button, a, summary):focus-visible, #app.addflow :is(button, summary):focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
:is(body.listpage, body.libpage) #app .lbtn { font-size: var(--fs-4); background: var(--accent-fill); color: #fff; padding: 0 var(--sp-24); min-height: 44px; }
:is(body.listpage, body.libpage) #app .lbtn:disabled { opacity: .6; cursor: default; }
:is(body.listpage, body.libpage) #app .lbtn2 { font-size: var(--fs-4); background: var(--l2); color: var(--text); padding: 0 var(--sp-16); min-height: 44px; }
:is(body.listpage, body.libpage) #app .lbtn2:hover { background: var(--l3); }
:is(body.listpage, body.libpage) #app .lbtn3 { font-size: var(--fs-3); background: none; color: var(--text-muted); padding: var(--sp-8) var(--sp-12); }
:is(body.listpage, body.libpage) #app .lbtn3:hover { color: var(--text); }
:is(body.listpage, body.libpage) #app .lbtn3.danger:hover { color: var(--bad); }

/* ── the field: the round's, an underline that takes the accent on focus ─────────────────────── */
:is(body.listpage, body.libpage) #app .lfield, :is(body.listpage, body.libpage) #app .lsearch { background: var(--l3); color: var(--text); border: 0;
  border-bottom: 2px solid var(--text-faint); border-radius: var(--r); font-family: var(--font-body);
  font-size: var(--fs-3); padding: 0 var(--sp-16); min-height: 44px; margin: 0; min-width: 0; }
:is(body.listpage, body.libpage) #app :is(.lfield, .lsearch):focus { outline: none; border-bottom-color: var(--accent); }
:is(body.listpage, body.libpage) #app :is(.lfield, .lsearch)::placeholder { color: var(--text-faint); }

/* ── the band: the list's name as the page's title, and its numbers the lobby's way ──────────── */
body.listpage #app .lband { position: relative; flex: 0 0 auto; display: flex; align-items: flex-end;
  gap: var(--sp-24); padding: var(--sp-24) 0 var(--sp-16); min-width: 0; }
body.listpage #app .lpickwrap { position: relative; min-width: 0; }
body.listpage #app .listpick { display: flex; align-items: baseline; gap: var(--sp-12); background: none;
  color: var(--text); padding: 0; text-transform: none; letter-spacing: 0; max-width: 100%; }
body.listpage #app .listpick .val { font-family: var(--font-names); font-weight: 600; font-size: var(--fs-7);
  letter-spacing: var(--track-tight); line-height: var(--lh-solid); overflow-wrap: anywhere; text-align: left; }
body.listpage #app .listpick .car { font-family: var(--font-body); font-size: var(--fs-2); color: var(--text-faint); }
body.listpage #app .listpick:hover .val { color: var(--accent); }
body.listpage #app .lwho { font-family: var(--font-names); font-weight: 600; font-size: var(--fs-7);
  letter-spacing: var(--track-tight); line-height: var(--lh-solid); }
:is(body.listpage, body.libpage) #app .ltot { display: flex; align-items: baseline; gap: 0 var(--sp-8); flex-wrap: wrap; white-space: nowrap; }
:is(body.listpage, body.libpage) #app .ltot .big { font-family: var(--font-numbers); font-size: var(--fs-7); line-height: var(--lh-solid); color: var(--text); }
:is(body.listpage, body.libpage) #app .ltot .sm { font-family: var(--font-numbers); font-size: var(--fs-5); line-height: var(--lh-solid);
  color: var(--text-muted); margin-left: var(--sp-16); }
:is(body.listpage, body.libpage) #app .ltot .sm.off { color: var(--text-faint); }
body.listpage #app .lfetch { margin: 0; font-size: var(--fs-2); color: var(--text-muted); max-width: 40%; text-align: right; }
body.listpage #app .lfetch:empty { display: none; }

/* ⛔ ANYTHING THAT FLOATS OVER CONTENT IS SOLID (the round's rule) — the list menu and the forms */
body.listpage #app .lmenu { position: absolute; top: calc(100% + var(--sp-8)); left: 0; min-width: 300px; max-width: 480px;
  max-height: 60vh; overflow-y: auto; background: var(--l3); z-index: 60; display: flex; flex-direction: column;
  padding: var(--sp-8) 0; box-shadow: 0 12px 32px rgba(0, 0, 0, .5); }
body.listpage #app .lmenu button { background: none; border: 0; color: var(--text); text-align: left; cursor: pointer;
  font-family: var(--font-names); font-weight: 500; font-size: var(--fs-4); padding: var(--sp-8) var(--sp-16);
  border-radius: var(--r); margin: 0; overflow-wrap: anywhere; }
body.listpage #app .lmenu button.here { box-shadow: inset 3px 0 0 var(--text); background: var(--hover-fill); }
body.listpage #app .lmenu button.verb { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-5);
  letter-spacing: var(--track-caps); text-transform: uppercase; }
body.listpage #app .lmenu button.quiet { font-size: var(--fs-4); color: var(--text-muted); }
body.listpage #app .lmenu button.danger { color: var(--bad); }
body.listpage #app .lmenu button:hover:not(:disabled) { background: var(--hover-fill); }
body.listpage #app .lmenu button:disabled { opacity: .45; cursor: default; }
body.listpage #app .lmenu .sep { height: 1px; background: var(--rule); margin: var(--sp-4) 0; }
body.listpage #app .lpop { position: absolute; top: calc(100% + var(--sp-8)); z-index: 60; width: min(480px, calc(100vw - 48px));
  background: var(--l3); padding: var(--sp-16); display: flex; flex-direction: column; gap: var(--sp-12);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .5); }
body.listpage #app .lpop.left { left: 0; }
body.listpage #app .lpop.right { right: 0; }
body.listpage #app .lpop .lfield { background: var(--l2); flex: 1 1 auto; }
body.listpage #app .lpop-row { display: flex; gap: var(--sp-8); align-items: stretch; }
body.listpage #app .lpop-note { margin: 0; font-size: var(--fs-2); color: var(--text-muted); }
body.listpage #app .lpop .lbtn3 { align-self: flex-start; padding-left: 0; }

/* ── the strip: a mode, a search, a volume; then the filters ─────────────────────────────────── */
:is(body.listpage, body.libpage) #app .lstrip { flex: 0 0 auto; display: flex; flex-direction: column; gap: var(--sp-8);
  padding-bottom: var(--sp-12); border-bottom: 1px solid var(--rule); }
:is(body.listpage, body.libpage) #app .lsrow { display: flex; align-items: center; gap: var(--sp-12); flex-wrap: wrap; min-width: 0; }
:is(body.listpage, body.libpage) #app .lsearch { flex: 1 1 320px; }
:is(body.listpage, body.libpage) #app .lseg { display: inline-flex; gap: 2px; }
/* ⛔ INK = WHERE YOU ARE: a lighter fill under a 2px --text rule. The accent is kept for the ticks. */
:is(body.listpage, body.libpage) #app .lseg button { font-size: var(--fs-3); background: var(--l1); color: var(--text-muted);
  padding: 0 var(--sp-16); min-height: 44px; }
:is(body.listpage, body.libpage) #app .lseg button:hover { color: var(--text); }
:is(body.listpage, body.libpage) #app .lseg button.on { background: var(--l3); color: var(--text); box-shadow: inset 0 -2px 0 var(--text); }
body.listpage #app .lseg.mode button { font-size: var(--fs-4); padding: 0 var(--sp-24); }
/* OP · ED · INS · CUS: the row's main filter, so wider and a step bigger than the two toggles beside it (his, 2026-09-24:
   "make the op/ed/ins/cus wider. and stood out more") — the mode segment's size, one width for all four */
:is(body.listpage, body.libpage) #app .lseg.types button { font-size: var(--fs-4); min-width: 72px; padding: 0 var(--sp-16); }
:is(body.listpage, body.libpage) #app .svol { margin-left: 0; gap: var(--sp-12); }
:is(body.listpage, body.libpage) #app .svol-label { font-family: var(--font-display); font-size: var(--fs-2); letter-spacing: var(--track-wider);
  text-transform: uppercase; color: var(--text-faint); }
:is(body.listpage, body.libpage) #app .svol-range { width: 140px; accent-color: var(--accent); }
:is(body.listpage, body.libpage) #app .tagright { margin-left: auto; }
:is(body.listpage, body.libpage) #app .checkfilter-toggle { font-family: var(--font-display); font-size: var(--fs-3); letter-spacing: var(--track-caps);
  text-transform: uppercase; background: var(--l1); color: var(--text-muted); border: 0; border-radius: var(--r);
  min-height: 44px; padding: 0 var(--sp-16); display: inline-flex; align-items: center; gap: var(--sp-8); }
:is(body.listpage, body.libpage) #app .checkfilter-toggle.on { background: var(--l3); color: var(--text); box-shadow: inset 0 -2px 0 var(--text); }
:is(body.listpage, body.libpage) #app .checkfilter-panel { background: var(--l3); border: 0; border-radius: var(--r); box-shadow: 0 12px 32px rgba(0, 0, 0, .5); }

/* ── the body: a pane of rows that scrolls inside itself, and the pager under it ──────────────── */
body.listpage #app .lbody { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
body.listpage #app .lrows { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }

/* the tick: a 22px square — the accent when on (what you chose to include), a hairline when off */
body.listpage #app input.pick { appearance: none; -webkit-appearance: none; width: 22px; height: 22px; margin: 0;
  flex: 0 0 22px; border: 1px solid var(--border-subtle); border-radius: var(--r); background: var(--l1) center / 16px no-repeat;
  cursor: pointer; padding: 0; min-height: 0; }
body.listpage #app input.pick:checked { background-color: var(--accent-fill); border-color: var(--accent-fill);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 7l3.2 3L13 3' fill='none' stroke='%23fff' stroke-width='2.2'/%3E%3C/svg%3E"); }
body.listpage #app input.pick:indeterminate { background-color: var(--l3); border-color: var(--accent);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8h9' fill='none' stroke='%236364fe' stroke-width='2.2'/%3E%3C/svg%3E"); }
body.listpage #app input.pick:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* the anime row: caret · tick · title (+ year) · slot chips · count */
body.listpage #app .srow.anime { display: grid; grid-template-columns: 20px 28px minmax(0, 1fr) auto 72px;
  align-items: center; gap: var(--sp-12); padding: var(--sp-8) var(--sp-12); min-height: 52px;
  border-bottom: 1px solid var(--rule); cursor: pointer; }
body.listpage #app .srow.anime:hover { background: var(--l1); }
body.listpage #app .srow .caret { color: var(--text-faint); font-size: var(--fs-2); text-align: center; }
body.listpage #app .srow.anime:hover .caret { color: var(--text); }
body.listpage #app .srow.anime .body { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 var(--sp-12); min-width: 0; }
/* ⛔ NEVER "…" — a long title wraps (the round's rule: he wants to see what does not fit) */
body.listpage #app .srow.anime .t { display: inline; font-family: var(--font-names); font-weight: 500; font-size: var(--fs-4);
  letter-spacing: var(--track-tight); color: var(--text); overflow-wrap: anywhere; line-height: var(--lh-ui); }
body.listpage #app .srow.anime .sub { display: inline-flex; align-items: baseline; gap: var(--sp-8); font-family: var(--font-body);
  font-size: var(--fs-2); color: var(--text-faint); white-space: nowrap; }
body.listpage #app .srow .tag { font-family: var(--font-display); font-size: var(--fs-2); letter-spacing: var(--track-wide);
  text-transform: uppercase; color: var(--text-muted); background: var(--l2); padding: var(--sp-4) var(--sp-8); border: 0; border-radius: var(--r); }
body.listpage #app .srow.anime .count { font-family: var(--font-numbers); font-size: var(--fs-4); color: var(--text);
  text-align: right; line-height: var(--lh-solid); }
body.listpage #app .srow.anime.off .t, body.listpage #app .srow.anime.off .count { color: var(--text-muted); opacity: 1; }
/* the slot chips: ON = full contrast on a raised fill, OFF = faint on the ground. Hierarchy by
   treatment, not by hue — fifty rows of green and grey would be a traffic light. */
body.listpage #app .slots { display: flex; gap: var(--sp-4); flex-wrap: wrap; justify-content: flex-end; }
/* text-indent = the tracking: the last letter's trailing space otherwise pushes the ink 1.2px left */
body.listpage #app .slot { font-size: var(--fs-2); letter-spacing: var(--track-wide); text-indent: var(--track-wide); background: var(--l3); color: var(--text);
  padding: 0 var(--sp-12); min-height: 30px; min-width: 48px; }
body.listpage #app .slot.off { background: transparent; color: var(--text-faint); box-shadow: inset 0 0 0 1px var(--rule); }
body.listpage #app .slot:hover { color: var(--text); box-shadow: inset 0 0 0 1px var(--border-subtle); }
body.listpage #app .slot.warn { color: var(--warn); }
body.listpage #app .slot.warn.off { color: color-mix(in oklch, var(--warn) 55%, var(--l0)); }
body.listpage #app .slotmore { font-size: var(--fs-2); letter-spacing: var(--track-wide); background: none; color: var(--text-muted);
  padding: 0 var(--sp-8); min-height: 30px; }
body.listpage #app .slotmore:hover { color: var(--text); }

/* the opened anime: its songs as COLUMNS — slot · ▶ · song · artist · flags (the ledger's shape) */
body.listpage #app .kids { background: var(--l1); border-bottom: 1px solid var(--rule); }
body.listpage #app .kids.shut { display: none; }
body.listpage #app .srow.song { display: grid;
  grid-template-columns: 20px 28px 56px 36px minmax(0, 560px) minmax(0, 420px) max-content max-content 1fr;
  align-items: center; gap: var(--sp-12); padding: var(--sp-8) var(--sp-12); min-height: 48px; border: 0; cursor: default; }
body.listpage #app .srow.song .body { display: contents; }
/* OP1 / ED2 one step bigger (his, 2026-09-24: "the op/ed should be a little bigger") */
body.listpage #app .srow.song .num { width: auto; font-family: var(--font-display); font-size: var(--fs-3);
  letter-spacing: var(--track-wide); color: var(--text-muted); text-transform: uppercase; }
body.listpage #app .srow.song .t { font-family: var(--font-body); font-size: var(--fs-3); color: var(--text); overflow-wrap: anywhere;
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 var(--sp-8); }
body.listpage #app .srow.song .sub { font-family: var(--font-body); font-size: var(--fs-2); color: var(--text-muted); overflow-wrap: anywhere; }
/* TV/FULL at OP1's size (his, 2026-09-24) */
body.listpage #app .srow.song .kflag { font-family: var(--font-display); font-size: var(--fs-3); letter-spacing: var(--track-wide);
  text-transform: uppercase; color: var(--text-faint); white-space: nowrap; display: inline-flex; gap: var(--sp-8); }
body.listpage #app .srow.song.off :is(.t, .sub, .num, .kflag) { opacity: .45; }
body.listpage #app .srow.song .sbtn { width: 36px; height: 36px; min-height: 0; padding: 0; margin: 0; display: grid; place-items: center;
  background: var(--l3); border: 0; border-radius: var(--r); color: var(--text); font-size: var(--fs-2); }
body.listpage #app .srow.song .sbtn:hover { background: var(--hover-fill); }
body.listpage #app .srow.song .sbtn.on { color: var(--accent); }
body.listpage #app .srow.song .stime { font-family: var(--font-numbers); font-size: var(--fs-3); color: var(--text-muted);
  grid-column: -2 / -1; grid-row: 1; justify-self: end; }   /* the time at the row's RIGHT end (his, 2026-09-24) */
/* the scrub bar's knob at the play head — the same knob /add's has (his, 2026-09-24: "i forgot the knob … in /list too") */
body.listpage #app .srow .sbar-fill::after { content: ''; position: absolute; right: -7px; top: 50%; width: 14px; height: 14px; border-radius: var(--r-pill);
  background: var(--accent); transform: translateY(-50%); }
body.listpage #app .kids .kidnote { color: var(--text-muted); font-size: var(--fs-2); padding: var(--sp-12) 0 var(--sp-12) var(--sp-48); margin: 0; }
body.listpage #app .vchip { font-family: var(--font-body); font-size: var(--fs-1); color: var(--text-muted); background: var(--l3);
  padding: 2px var(--sp-8); border-radius: var(--r); }

/* the pager — ink marks the page you are on */
:is(body.listpage, body.libpage) #app .lpager { flex: 0 0 auto; display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-12) 0 var(--sp-24);
  border-top: 1px solid var(--rule); }
body.listpage #app .lfound { font-family: var(--font-display); font-size: var(--fs-2); letter-spacing: var(--track-wider);
  text-transform: uppercase; color: var(--text-faint); margin-right: auto; }
body.listpage #app .lfound b { font-family: var(--font-numbers); font-weight: 400; font-size: var(--fs-4); color: var(--text-muted);
  letter-spacing: var(--track-tight); }
:is(body.listpage, body.libpage) #app .lpage { font-family: var(--font-numbers); font-size: var(--fs-4); background: var(--l1); color: var(--text-muted);
  min-width: 44px; min-height: 44px; padding: 0 var(--sp-12); }
:is(body.listpage, body.libpage) #app .lpage:hover:not(:disabled) { color: var(--text); }
:is(body.listpage, body.libpage) #app .lpage:disabled { opacity: .35; cursor: default; }
/* ‹ › are not in Bebas's set — they fell back to a thin system glyph half the digits' size */
:is(body.listpage, body.libpage) #app .lpage[aria-label] { font-family: var(--font-body); font-size: var(--fs-5); }
:is(body.listpage, body.libpage) #app .lpage.on { background: var(--l3); color: var(--text); box-shadow: inset 0 -2px 0 var(--text); }
:is(body.listpage, body.libpage) #app .lgap { color: var(--text-faint); padding: 0 var(--sp-4); font-family: var(--font-numbers); font-size: var(--fs-4); }

/* ⛔ AN EMPTY STATE IS A ROUTE, NOT A PARAGRAPH */
body.listpage #app .lempty { padding: var(--sp-64) var(--sp-12); display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-24); }
body.listpage #app .lempty .big { font-family: var(--font-display); font-size: var(--fs-6); letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--text-muted); }
body.listpage #app .lempty-acts { display: flex; gap: var(--sp-8); flex-wrap: wrap; }
body.listpage #app .lcta { font-size: var(--fs-4); background: var(--accent-fill); color: #fff; padding: 0 var(--sp-24); min-height: 48px; }
body.listpage #app .lcta2 { font-size: var(--fs-4); background: var(--l2); color: var(--text-muted); padding: 0 var(--sp-24); min-height: 48px; }
body.listpage #app .lcta2:hover { color: var(--text); background: var(--l3); }

/* a notice never pushes the screen: a fixed toast, bottom right, tap to put it away */
body.listpage #app .ltoast { position: fixed; right: var(--sp-24); bottom: var(--sp-24); z-index: 80; max-width: min(520px, calc(100vw - 48px));
  background: var(--l3); color: var(--text); padding: var(--sp-12) var(--sp-16); font-size: var(--fs-3); cursor: pointer;
  box-shadow: 0 12px 32px rgba(0, 0, 0, .5); }
body.listpage #app .ltoast.warn { color: var(--warn); }

/* ── the profile picker, the pair prompt and the read-only viewer ────────────────────────────── */
body.listpage #app .lscroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-bottom: var(--sp-24); }
body.listpage #app .lprofiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: var(--sp-8); align-items: start; }
body.listpage #app .lprof { display: flex; align-items: flex-start; gap: var(--sp-12); background: var(--l1); padding: var(--sp-12); min-height: 76px; }
body.listpage #app .ptile { position: relative; overflow: hidden; width: 52px; height: 52px; flex: 0 0 52px; display: grid; place-items: center;
  font-family: var(--font-display); font-size: var(--fs-5); color: var(--on-tile); }
body.listpage #app .ptile img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
body.listpage #app .lprof.new .ptile { background: var(--accent-fill) !important; color: #fff; }
body.listpage #app .pcol { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
body.listpage #app .pname { font-family: var(--font-names); font-weight: 500; font-size: var(--fs-4); overflow-wrap: anywhere; }
body.listpage #app .pacts { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; margin-top: var(--sp-8); }
body.listpage #app .lprof.new { align-items: center; }
body.listpage #app .lprof.new .pcol { flex-direction: row; gap: var(--sp-8); }
:is(body.listpage, body.libpage) #app .lprof.new .pcol .lfield { flex: 1 1 auto; }
body.listpage #app .lprof.new .pacts { margin-top: 0; }
:is(body.listpage, body.libpage) #app .lpair .lbtn3 { align-self: flex-start; padding-left: 0; }
body.listpage #app .lview details { border-bottom: 1px solid var(--rule); padding: var(--sp-8) var(--sp-12); }
body.listpage #app .lview summary { font-family: var(--font-names); font-weight: 500; font-size: var(--fs-4); cursor: pointer; }
body.listpage #app .lview .song { font-size: var(--fs-2); color: var(--text-muted); padding: var(--sp-4) 0 var(--sp-4) var(--sp-24); }
body.listpage #app .lview .song.off { opacity: .45; }

/* ⛔ OPTICAL CENTRING (DESIGN.md): Bebas draws ~2px HIGH in a flex-centred box — measured on this page by
   scripts/geom/list-align.py. 4px of top padding against 0 below moves the ink down 2px. ⚠ AFTER every
   base rule on purpose: those set `padding` as a shorthand, and a correction placed above them is reset. */
body.listpage #app :is(.lbtn, .lbtn2, .lcta, .lcta2, .lseg button, .lseg.mode button, .lpage) { padding-top: var(--sp-4); }
/* the masthead's Bebas too (its row is the lobby's; the lobby's own nav gets the same fix below) */

/* ⛔ LAST IN THE BLOCK, ALWAYS — a media query carries no extra specificity. Judged at PC width (his
   scope rule); this exists so the narrow snaps do not get WORSE, not to chase phones. */
@media (max-width: 1100px) {
  body.listpage #app .srow.song { grid-template-columns: 20px 28px 56px 36px minmax(0, 1fr) minmax(0, 1fr) max-content max-content; }
  :is(body.listpage, body.libpage) #app .lsearch { flex-basis: 220px; }
}
@media (max-width: 760px) {
  body.listpage #app .srow.anime { grid-template-columns: 20px 28px minmax(0, 1fr) 56px; row-gap: var(--sp-8); }
  body.listpage #app .srow.anime .slots { grid-column: 3 / -1; grid-row: 2; justify-content: flex-start; }
  body.listpage #app .srow.anime .count { grid-column: 4; grid-row: 1; }
  body.listpage #app .srow.song { grid-template-columns: 20px 28px 56px 36px minmax(0, 1fr) auto; row-gap: var(--sp-4); }
  body.listpage #app .srow.song .sub { grid-column: 5; grid-row: 2; }
  body.listpage #app .srow.song .kflag { grid-column: 6; grid-row: 2; }
  body.listpage #app .srow.song .stime { grid-column: auto; grid-row: auto; justify-self: auto; }   /* a phone keeps the old flow placement: pinning it made a third row (W5) */
  body.listpage #app .lband { flex-wrap: wrap; gap: var(--sp-12); }
  :is(body.listpage, body.libpage) #app .lpager { flex-wrap: wrap; }
  body.listpage #app .lfound { flex: 1 1 100%; margin: 0 0 var(--sp-8); }
}

/* ════════════════════════════════════════════════════════════════════════════════════════════════
   /add — built 2026-09-23 from public/add-design.html + DESIGN.md § "What the round settled".
   ⛔ STILL A MOBILE PAGE (two friends are phone-only): one column is the BASE, the 2×2 is laid over it
   from 900px (his layout: link top-left, preview top-right, anime bottom-left, song bottom-right).
   ⛔ A FIXED FRAME: the masthead stays, the form scrolls inside itself when it must.
   Every rule is under `#app.addflow` — `.addpage` is also the class /list and /library's #app carry.
   ════════════════════════════════════════════════════════════════════════════════════════════════ */
html:has(#app.addflow) { scrollbar-gutter: auto; overflow: clip; }
body:has(#app.addflow) { overflow: clip; }
#app.addflow { --edge: clamp(10px, 1.25vw, 24px); max-width: none; width: 100%; margin: 0; padding: 0 var(--edge); box-sizing: border-box;
  height: 100vh; height: 100dvh; display: flex; flex-direction: column; }
#app.addflow * { box-sizing: border-box; }
#app.addflow #addform { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding: var(--sp-24) 0 var(--sp-24); display: flex; flex-direction: column; gap: var(--sp-12); }

/* ── a block: the lobby's resting surface and a small tracked label, in place of a <fieldset> ── */
#app.addflow .ablk { background: var(--l1); padding: var(--sp-16); min-width: 0; display: flex; flex-direction: column;
  gap: var(--sp-16); margin: 0; border: 0; }
/* a field's label: the display face, a step above the block's; the placeholder is body text, faint,
   and an EXAMPLE of the value — a label and its placeholder must never render the same */
#app.addflow label { display: flex; flex-direction: column; gap: var(--sp-8); font-family: var(--font-display); font-weight: 400;
  font-size: var(--fs-3); letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--text-muted); margin: 0; }
#app.addflow .lib-triple > label > span { font: inherit; }
/* ⛔ THE ROUND'S FIELD: lighter than its ground, an underline, the accent on focus — never a box edge */
#app.addflow :is(input:not([type=radio]):not([type=checkbox]):not([type=file]), select) { width: 100%; min-width: 0;
  background: var(--l3); color: var(--text); border: 0; border-bottom: 2px solid var(--text-faint); border-radius: var(--r);
  font-family: var(--font-body); font-size: var(--fs-3); font-weight: 400; letter-spacing: 0; text-transform: none;
  padding: 0 var(--sp-16); min-height: 48px; margin: 0; }
#app.addflow :is(input, select):focus { outline: none; border-bottom-color: var(--accent); }
#app.addflow input::placeholder { color: var(--text-faint); }
/* Mitr draws ~1.5px LOW in a centred field (measured, list-align.py add) — the names face does not */
#app.addflow input:not(.aname):not([type=radio]):not([type=checkbox]):not([type=file]) { padding-bottom: var(--sp-2); }
/* ⛔ A FIELD THAT HOLDS A NAME TAKES THE NAMES FACE (his, 2026-09-12: "those font on these text
   (kimetsu, lisa, etc.) doesn't fits other fonts"). The link keeps the body face: a URL is read
   character by character, and it is not a name. */
#app.addflow input.aname { font-family: var(--font-names); font-weight: 500; font-size: var(--fs-4); letter-spacing: var(--track-tight); }
#app.addflow select { font-family: var(--font-names); font-weight: 500; font-size: var(--fs-4); cursor: pointer; }
#app.addflow #file-input { font-family: var(--font-body); font-size: var(--fs-3); color: var(--text-muted); text-transform: none; letter-spacing: 0;
  border: 0; background: none; padding: 0; width: 100%; }
/* the browser's "Choose File" is the one control on the page nobody styled (his, 2026-09-24): the page's own button — the
   source segment's face and height — and room between it and the file name */
#app.addflow #file-input::file-selector-button { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-3); letter-spacing: var(--track-caps);
  text-transform: uppercase; background: var(--l3); color: var(--text); border: 0; border-radius: var(--r); min-height: 44px;
  padding: var(--sp-2) var(--sp-16) 0; margin: 0 var(--sp-16) 0 0; cursor: pointer; }
#app.addflow #file-input::file-selector-button:hover { background: var(--hover-fill); }
/* the file zone: the field's own shape (l3 fill, underline), the button and the name centred in it; a file held over it
   takes the focus line */
#app.addflow .fzone { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: var(--sp-16); min-height: 48px;
  background: var(--l3); border-bottom: 2px solid var(--text-faint); padding: 0 var(--sp-16); cursor: pointer; text-transform: none; letter-spacing: 0; }
#app.addflow .fzone[hidden] { display: none; }
#app.addflow .fzone.over { border-bottom-color: var(--accent); background: var(--hover-fill); }
#app.addflow .fzone .fbtn { font-family: var(--font-display); font-size: var(--fs-3); letter-spacing: var(--track-caps); text-transform: uppercase;
  color: var(--text); background: var(--hover-fill); min-height: 44px; display: inline-flex; align-items: center; padding: var(--sp-2) var(--sp-16) 0; }
#app.addflow .fzone:hover .fbtn { background: #45445a; }
#app.addflow .fzone #file-name { font-family: var(--font-body); font-size: var(--fs-4); color: var(--text-muted); overflow-wrap: anywhere; min-width: 0; }
/* Type · No. · Cut sized to what they hold (his, 2026-09-24: "narrow type, no., cut boxes"); Version left this row for
   its own under the title */
/* Type 4px narrower, Cut 4px wider (his, 2026-09-24): CUS and FULL leave the same room before their arrows — 14.5 and 14px, measured by ink */
#app.addflow .lib-triple { display: grid; grid-template-columns: minmax(0, 76px) minmax(0, 56px) minmax(0, 84px) minmax(0, 1fr); gap: var(--sp-12); }
/* Type · No. · Cut hold a SLOT, not a name (his, 2026-09-24: "change the font of data inside three of them"): the
   display face, which is also the numbers face. The boxes are sized to OP/INS/CUS, a two-digit number and FULL; CUSTOM
   reads CUS in the box (the value sent is still CUSTOM). The number box has no spinner. */
/* ⚠ (1,3,1) on purpose: the page's own field rule is (1,3,1) and sets the body face — this one has to come later AND match it */
#app.addflow .add-song .lib-triple :is(select, input[type=number]) { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-5);
  letter-spacing: var(--track-caps); text-transform: uppercase; padding: var(--sp-4) var(--sp-8) 0 var(--sp-12); }
#app.addflow .lib-triple input[type=number] { -moz-appearance: textfield; appearance: textfield; }
#app.addflow .lib-triple input[type=number]::-webkit-inner-spin-button,
#app.addflow .lib-triple input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
#app.addflow .hint { color: var(--text-faint); font-family: var(--font-body); font-size: var(--fs-2); text-transform: none; letter-spacing: 0; margin: 0; }

/* the source switch: two states, so a segmented control — the radios stay underneath as the value */
#app.addflow .aseg { display: inline-flex; gap: 2px; align-self: flex-start; }
#app.addflow .aseg label.radio { position: relative; flex-direction: row; gap: 0; cursor: pointer; }
#app.addflow .aseg label.radio input { position: absolute; opacity: 0; pointer-events: none; width: 1px; height: 1px; }
#app.addflow .aseg label.radio span { display: inline-flex; align-items: center; min-height: 44px; padding: var(--sp-2) var(--sp-16) 0;
  background: var(--l2); color: var(--text-muted); font-size: var(--fs-3); }
#app.addflow .aseg label.radio:hover span { color: var(--text); }
/* INK = where you are (the /list rule); the accent is the one primary action's */
#app.addflow .aseg label.radio:has(input:checked) span { background: var(--l3); color: var(--text); box-shadow: inset 0 -2px 0 var(--text); }
#app.addflow .aseg label.radio:has(input:focus-visible) span { outline: 2px solid var(--accent); outline-offset: -2px; }

/* the non-anime tick — /list's 22px square */
#app.addflow label.acheck { flex-direction: row; align-items: center; gap: var(--sp-12); color: var(--text); cursor: pointer; min-height: 44px; }
#app.addflow label.acheck .hint { margin-left: var(--sp-4); }
#app.addflow label.acheck input { appearance: none; -webkit-appearance: none; width: 22px; height: 22px; flex: 0 0 22px; margin: 0; padding: 0;
  border: 1px solid var(--border-subtle); border-radius: var(--r); background: var(--l1) center / 16px no-repeat; cursor: pointer; }
#app.addflow label.acheck input:checked { background-color: var(--accent-fill); border-color: var(--accent-fill);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 7l3.2 3L13 3' fill='none' stroke='%23fff' stroke-width='2.2'/%3E%3C/svg%3E"); }
#app.addflow label.acheck input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* the anime search and its drop — ⛔ SOLID, it floats over the form */
#app.addflow #anime-fields { display: flex; flex-direction: column; gap: var(--sp-12); }
#app.addflow #anime-fields.off { visibility: hidden; }
#app.addflow .guessbox { position: relative; margin: 0; }
#app.addflow #anime-drop { background: var(--l3); border: 0; border-radius: var(--r); box-shadow: 0 12px 32px rgba(0, 0, 0, .5); z-index: 30; }
#app.addflow #anime-drop li { font-family: var(--font-names); font-weight: 500; font-size: var(--fs-4); padding: var(--sp-12) var(--sp-16); }
#app.addflow #anime-picked { font-family: var(--font-body); font-size: var(--fs-3); }
/* the picked anime: cover + title + MAL link (his, 2026-09-24: "show the cover … would be more useful") */
#app.addflow .apicked { display: flex; gap: var(--sp-16); align-items: flex-start; background: var(--l2); padding: var(--sp-12); }
#app.addflow .apicked[hidden] { display: none; }
/* ⛔ THE COVER FILLS THE HEIGHT THE ROW GIVES IT (his, 2026-09-24: "anime could still go bigger", twice): the anime block
   stretches to the song column's height, the card takes what is left, and the cover is the card's height at 2:3 — so it
   ends level with the Tags box instead of stopping at a number. 192px tall is its floor. */
/* ⚠ the cover's BOX has `contain: size`, or the image's own 230×345 becomes the row's height and squeezes the top row */
#app.addflow .apcovbox { contain: size; height: 100%; min-height: 192px; aspect-ratio: 2 / 3; flex: 0 0 auto; background: var(--l3); }
#app.addflow .apcover { display: block; width: 100%; height: 100%; object-fit: cover; }
#app.addflow .aptext { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }
#app.addflow .aptitle { font-family: var(--font-names); font-weight: 500; font-size: var(--fs-4); color: var(--text); overflow-wrap: anywhere; }
#app.addflow .apsub { font-family: var(--font-body); font-size: var(--fs-3); color: var(--text-muted); overflow-wrap: anywhere; }
#app.addflow .amanual { display: flex; flex-direction: column; gap: var(--sp-16); }

/* the preview: the empty frame IS the instruction — a sunk well, no sentence */
#app.addflow #addform > .yt-preview { margin: 0; background: var(--l1); border: 0; border-radius: var(--r); padding: var(--sp-16); min-height: 132px; }
#app.addflow #addform > .yt-preview.empty { display: none; }
#app.addflow .yt-preview-title { font-family: var(--font-names); font-weight: 500; font-size: var(--fs-4); color: var(--text); margin: 0 0 var(--sp-4); }
#app.addflow .yt-preview .hint { font-size: var(--fs-2); color: var(--text-muted); }

/* one column below 900px: the right column's wrapper steps aside and `order` keeps the old order (W5) */
#app.addflow .acol { display: contents; }
#app.addflow #addform > .source { order: 1; } #app.addflow #addform > .yt-preview { order: 2; } #app.addflow .add-song { order: 3; }
#app.addflow #addform > .add-anime { order: 4; } #app.addflow .add-tags { order: 5; } #app.addflow #addform > .abar { order: 6; }
/* the tag box sits in the song's column */
/* a field label that is not a <label> (the anime search, the tags): the same face as TITLE / CREDIT */
#app.addflow .flab { display: block; font-family: var(--font-display); font-weight: 400; font-size: var(--fs-3); letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--text-muted); line-height: var(--lh-solid); }
#app.addflow .add-tags { gap: var(--sp-8); }
/* the preview's status line ("Looking up the video…", "No preview for a file") is the whole content of the box, so it
   reads at body size, not as a caption (his, 2026-09-24: "bigger size") */
#app.addflow .yt-preview:has(#yt-title:empty) #yt-sub { font-size: var(--fs-4); color: var(--text-muted); }
#app.addflow #addform > .yt-preview:has(#yt-title:empty) { justify-content: center; align-items: center; text-align: center; }
#app.addflow .yt-preview:has(#yt-title:empty) #yt-title { display: none; }
/* the uploaded file's player (his, 2026-09-24): /list's ▶ and clock, and the scrub bar along the box's bottom edge */
#app.addflow #addform > .yt-preview { position: relative; overflow: hidden; }
/* ⛔ A PLAYING FILE READS TOP TO BOTTOM (his, 2026-09-24): the name + size at the top of the box, the ▶ + clock + volume
   at the bottom, right over the scrub bar — the box is tall now, and centring all of it left the player floating */
#app.addflow #addform > .yt-preview:has(.fplay) { align-items: stretch; padding-bottom: var(--sp-24); }
#app.addflow .yt-preview:has(.fplay) .yt-preview-body { flex: 1 1 auto; display: flex; flex-direction: column; min-width: 0; }
#app.addflow .fplay { display: flex; align-items: center; gap: var(--sp-12); margin-top: auto; padding-top: var(--sp-12); }
#app.addflow .fplay .svol { margin-left: auto; display: inline-flex; align-items: center; gap: var(--sp-12); }
#app.addflow .fplay .svol-mute { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-3); letter-spacing: var(--track-caps);
  text-transform: uppercase; background: none; border: 0; color: var(--text-muted); padding: var(--sp-4) var(--sp-4) 0; margin: 0; min-width: 58px; cursor: pointer; }
#app.addflow .fplay .svol-mute:hover { color: var(--text); }
#app.addflow .fplay .svol.muted .svol-mute { color: var(--text-faint); } /* colour pass 3b */
#app.addflow .fplay .svol input.svol-range { width: 140px; accent-color: var(--accent); margin: 0; padding: 0; min-height: 0; border: 0; background: none; }
#app.addflow .fplay .svol.muted input.svol-range { accent-color: var(--border-subtle); opacity: .55; }
#app.addflow .fplay .sbtn { width: 44px; height: 44px; min-height: 0; padding: 0; margin: 0; display: grid; place-items: center; background: var(--l3);
  color: var(--text); border: 0; border-radius: var(--r); font-size: var(--fs-3); cursor: pointer; }
#app.addflow .fplay .sbtn:hover { background: var(--hover-fill); }
#app.addflow .fplay .sbtn.on { color: var(--accent); }
/* the scrub bar gets a KNOB at the play head (his, 2026-09-24: "like the volume slider") — round like that thumb */
/* the bar sits INSIDE the box's padding so the knob is never cut by the box edge */
#app.addflow .yt-preview .sbar { left: var(--sp-16); right: var(--sp-16); bottom: var(--sp-12); }

#app.addflow .yt-preview .sbar-fill::after { content: ''; position: absolute; right: -7px; top: 50%; width: 14px; height: 14px; border-radius: var(--r-pill);
  background: var(--accent); transform: translateY(-50%); }
#app.addflow .fplay .stime { font-family: var(--font-numbers); font-size: var(--fs-4); color: var(--text-muted); }

/* ⛔ THE TAG CONTROL, /library's shape (his, 2026-09-24): ＋ and one chip per tag. The panel is SOLID and opens UPWARD —
   downward it overflowed the form and scrolled the screen, which is the thing he reported. */
#app.addflow .tpills { display: flex; flex-wrap: nowrap; align-items: center; gap: var(--sp-8); min-width: 0; }   /* ⚠ NOT overflow: hidden — the tag panel lives in here and would be clipped (measured: its Clear was unclickable) */
#app.addflow .tpill { flex: 0 0 auto; }
#app.addflow .tpill[hidden] { display: none; }
/* +N: the chips that did not fit; it opens the tag panel (his, 2026-09-24) — a count in the numbers face, a chip's height */
#app.addflow .tmore { flex: 0 0 auto; margin: 0 0 0 auto; min-height: 44px; min-width: 44px; padding: var(--sp-4) var(--sp-12) 0; border: 0;
  border-radius: var(--r); background: var(--l3); color: var(--text); font-family: var(--font-numbers); font-size: var(--fs-4); cursor: pointer; }
#app.addflow .tmore:hover { background: var(--hover-fill); }
#app.addflow .tagpick { position: relative; display: inline-flex; }
#app.addflow .tagpick .checkfilter-toggle { font-family: var(--font-display); font-size: var(--fs-5); line-height: 1; background: var(--l3); color: var(--text);
  border: 0; border-radius: var(--r); width: 44px; height: 44px; padding: 0; margin: 0; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
#app.addflow .tagpick .checkfilter-toggle:hover { background: var(--hover-fill); }
#app.addflow .tpill { display: inline-flex; align-items: center; gap: var(--sp-4); min-height: 44px; background: var(--l3); border: 0; border-radius: var(--r);
  padding: 0 var(--sp-4) 0 var(--sp-12); font-family: var(--font-names); font-weight: 500; font-size: var(--fs-3); color: var(--text); }
#app.addflow .tpill .tx { background: none; border: 0; color: var(--text-muted); margin: 0; padding: 0; width: 32px; height: 32px; font-size: var(--fs-2); cursor: pointer; }
#app.addflow .tpill .tx:hover { color: var(--bad); }
/* Kanit's Latin draws 2.7px LOW in the chip (measured, list-align.py add) */
#app.addflow .tpill > span { position: relative; top: -3px; }
#app.addflow .tagpick .checkfilter-panel { top: auto; bottom: calc(100% + var(--sp-4)); left: 0; width: 300px; background: var(--l3); border: 0; border-radius: var(--r);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .5); padding: var(--sp-12); z-index: 40; }
#app.addflow .tagpick .checkfilter-panel input.checkfilter-search { width: 100%; box-sizing: border-box; background: var(--l2); color: var(--text); border: 0; border-bottom: 2px solid var(--text-faint);
  border-radius: var(--r); font-family: var(--font-body); font-size: var(--fs-3); min-height: 44px; padding: 0 var(--sp-12); margin: 0 0 var(--sp-8); text-transform: none; letter-spacing: 0; }
/* ⚠ the selectors are this long on purpose: the page's own field rule is (1,3,1) and paints every input l3 — the same
   fill as the panel, so the search box vanished into it (his, 2026-09-24: "follow the tag window in /list") */
#app.addflow .tagpick .checkfilter-panel input.checkfilter-search:focus { outline: none; border-bottom-color: var(--accent); }
#app.addflow .checkfilter-rows { max-height: calc(6 * 40px); overflow-y: auto; overscroll-behavior: contain; margin: 0; }
#app.addflow .checkfilter-row { display: flex; flex-direction: row; align-items: center; gap: var(--sp-12); min-height: 40px; padding: 0 var(--sp-4); cursor: pointer;
  border-radius: var(--r); font-family: var(--font-names); font-weight: 500; font-size: var(--fs-3); color: var(--text); text-transform: none; letter-spacing: 0; }
#app.addflow .checkfilter-row:hover { background: var(--hover-fill); }
#app.addflow .checkfilter-row input { appearance: none; -webkit-appearance: none; width: 20px; height: 20px; flex: 0 0 20px; margin: 0; padding: 0; min-height: 0;
  border: 1px solid var(--text-faint); background: var(--l2) center / 14px no-repeat; border-radius: var(--r); cursor: pointer; }
#app.addflow .checkfilter-row input:checked { background-color: var(--accent-fill); border-color: var(--accent-fill);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 7l3.2 3L13 3' fill='none' stroke='%23fff' stroke-width='2.2'/%3E%3C/svg%3E"); }
#app.addflow .checkfilter-name { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
#app.addflow .checkfilter-n { font-family: var(--font-numbers); font-size: var(--fs-4); color: var(--text-muted); }
#app.addflow .checkfilter-create { display: block; width: 100%; margin: var(--sp-4) 0 0; min-height: 40px; background: var(--l2); border: 0; border-radius: var(--r);
  color: var(--text); font-family: var(--font-names); font-weight: 500; font-size: var(--fs-3); text-align: left; padding: 0 var(--sp-12); cursor: pointer; }
#app.addflow .checkfilter-create:hover { background: var(--hover-fill); }
#app.addflow .checkfilter-clear { width: 100%; margin: var(--sp-8) 0 0; background: var(--l2); border: 0; border-radius: var(--r); color: var(--text-muted);
  font-family: var(--font-display); font-size: var(--fs-3); letter-spacing: var(--track-caps); text-transform: uppercase; min-height: 40px; cursor: pointer; }
#app.addflow .checkfilter-clear:hover { color: var(--text); }
#app.addflow .checkfilter-panel .muted { font-family: var(--font-body); font-size: var(--fs-3); color: var(--text-faint); margin: var(--sp-8) var(--sp-4); }

/* ── the action bar: one primary action, its own row ─────────────────────────────────────────── */
#app.addflow .abar { display: flex; align-items: center; gap: var(--sp-16); background: var(--l1); padding: var(--sp-16); }
#app.addflow .abar #status { margin: 0 auto 0 0; font-size: var(--fs-3); color: var(--text-muted); }
#app.addflow .abar #status.ok { color: var(--good); }
#app.addflow .abar #status.err { color: var(--bad); }
#app.addflow #go { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-5); letter-spacing: var(--track-caps);
  text-transform: uppercase; background: var(--accent-fill); color: #fff; border: 0; border-radius: var(--r); margin: 0;
  padding: var(--sp-4) var(--sp-32) 0; min-height: 56px; display: inline-flex; align-items: center; cursor: pointer; }
#app.addflow #go:disabled { opacity: .6; cursor: default; }

/* ⛔ THE 2×2 IS FOR A WIDE SCREEN ONLY (his; add-check W5 fails if it reaches a phone) — LAST, always */
@media (min-width: 900px) {
  #app.addflow #addform { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    /* ⚠ NOTHING SPANS A ROW (his, 2026-09-24: "why is the anime box connected to the download bar"). Anime used to span
       the Song row and a 1fr row, and a grid item spanning a FLEXIBLE track does not size it — with the manual fields
       open it ran 30px under the bar. Song + Tags are one column box (`.acol`) now; the bar's 1fr row keeps it low. */
    grid-template-areas: "source preview" "anime col" "bar bar";
    /* ⛔ THE TOP ROW TAKES THE SPARE HEIGHT (his, 2026-09-24: "extend the video preview box + the link box … until the
       tags box almost hit the download"): the link and the preview grow, everything under them rides down, and the bar
       stays last. `minmax(auto, 1fr)`, so on a short window the row is its content and the form scrolls as before. */
    grid-template-rows: minmax(auto, 1fr) auto auto; column-gap: var(--sp-12); row-gap: var(--sp-12); align-items: start; }
  /* ⛔ THE FIELD FILLS THE BOX (his, 2026-09-24: "it still feel float" — centring the tabs + field did not cure it): a
     small control in a big empty box floats wherever it sits. So the tabs stay on top and the link field / file picker
     TAKES the rest of the box's height: the box is the drop target it looks like, its text centred in it. */
  #app.addflow #addform > .source { grid-area: source; align-self: stretch; }
  #app.addflow #addform > .source > #yt-url { flex: 1 1 auto; text-align: center; font-size: var(--fs-4); }
  #app.addflow #addform > .source > .fzone { flex: 1 1 auto; }
  #app.addflow #addform > .yt-preview { grid-area: preview; align-self: stretch; }
  #app.addflow #addform > .yt-preview.empty { display: flex; }
  #app.addflow #addform > .acol { grid-area: col; display: flex; flex-direction: column; gap: var(--sp-12); min-width: 0; }
  #app.addflow #addform > .add-anime { grid-area: anime; align-self: stretch; }
  /* the anime fields take the block's height, so the tick sits at the BOTTOM whether or not an anime is picked (his,
     2026-09-24: "the bottom is better. fix the not from at the bottom") */
  #app.addflow .add-anime #anime-fields { flex: 1 1 auto; min-height: 0; }
  #app.addflow .add-anime .apicked { flex: 1 1 auto; min-height: 0; }
  #app.addflow #addform > .abar { grid-area: bar; }
  /* the thumbnail grows with the box: its height is the box's, its width follows at 16:9 */
  #app.addflow #addform > .yt-preview img { align-self: stretch; width: auto; height: auto; aspect-ratio: 16 / 9; max-width: 55%; object-fit: cover; }
}
@media (max-width: 600px) {
  #app.addflow .abar { flex-wrap: wrap; }
  #app.addflow #go { width: 100%; justify-content: center; }
}

/* ═══ /list, HIS FIRST USE PASS (2026-09-23 23:12) — overrides, kept together so each is one line to reverse ═══ */
/* the list's name is the strip's first control now (no band above the strip) */
:is(body.listpage, body.libpage) #app .lstrip { padding-top: var(--sp-24); }
body.listpage #app .listpick { background: var(--l2); padding: 0 var(--sp-16); min-height: 44px; align-items: center; gap: var(--sp-12); }
body.listpage #app .listpick:hover { background: var(--l3); }
body.listpage #app .listpick .val { font-size: var(--fs-4); font-weight: 500; line-height: var(--lh-solid); }
body.listpage #app .listpick .car { font-size: var(--fs-1); }
body.listpage #app .lstrip .lpop.left, body.listpage #app .lstrip .lmenu { top: calc(100% + var(--sp-4)); }
/* the page bar: page buttons LEFT (where /library keeps them), the list's numbers right */
body.listpage #app .lpager .lfound { margin-right: 0; }
:is(body.listpage, body.libpage) #app .lpager .ltot .big { font-size: var(--fs-5); }
:is(body.listpage, body.libpage) #app .lpager .ltot .sm { font-size: var(--fs-4); margin-left: var(--sp-12); }
body.listpage #app .lpager > * + .ltot, body.listpage #app .lpager .lfound + .ltot { margin-left: var(--sp-24); }
/* the caret is gone (his: "the triangle on the left can just be removed") — the row itself opens */
body.listpage #app .srow .caret, body.listpage #app .srow.song .ind { display: none; }
body.listpage #app .srow.anime { grid-template-columns: 28px minmax(0, 1fr) auto 72px; }
/* ⛔ COLUMNS THAT FILL THE ROW (his, 2026-09-24: "there's still no feel of they have they own column"): song and artist
   share the row 2:3 instead of stopping at 560/420 and leaving the right third empty, and the flags get a FIXED track so
   TV and ⚠ line up down the list whatever the name beside them. The last track is the play time (right end), FIXED
   too: every row is its own grid, so a max-content time track shifted only the playing row's columns (measured 25px).
   ⚠ THIS is the rule that wins at every width (it is later than the @media blocks above, same specificity). */
body.listpage #app .srow.song { grid-template-columns: 28px 64px 36px minmax(0, 2fr) minmax(0, 3fr) 120px 96px; }
/* TALKING / FADE in TV's own voice — no colour, no symbol (his, 2026-09-24) */
body.listpage #app .srow.song .kflag .flagword { color: inherit; font-weight: 400; }
/* an OFF song dims its words, never its controls — the tick and ▶ must still read as pressable (his) */
body.listpage #app .srow.song.off :is(input.pick, .sbtn) { opacity: 1; }
body.listpage #app .srow.song.off :is(.t, .sub, .num, .kflag) { opacity: .55; }

/* Mute, like the game: the word says what it does, and the slider greys instead of moving */
:is(body.listpage, body.libpage) #app .svol-mute, body.libpage #app .svol-mute { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-3);
  letter-spacing: var(--track-caps); text-transform: uppercase; background: none; border: 0; color: var(--text-muted);
  padding: var(--sp-4) var(--sp-4) 0; margin: 0; min-width: 58px; cursor: pointer; }
:is(body.listpage, body.libpage) #app .svol-mute:hover { color: var(--text); }
:is(body.listpage, body.libpage) #app .svol.muted .svol-mute { color: var(--text-faint); } /* colour pass 3b */
:is(body.listpage, body.libpage) #app .svol.muted .svol-range { accent-color: var(--border-subtle); opacity: .55; }

/* ── the account menu on the tool pages: four options must READ as four (his) ────────────────── */
#app .lb.tmast .amenu { padding: 0; }
#app .lb.tmast .amenu button { font-size: var(--fs-4); padding: var(--sp-12) var(--sp-16); }
#app .lb.tmast .amenu button + button { box-shadow: inset 0 1px 0 var(--rule); }
#app .lb.tmast .amenu .sep { margin: 0; height: var(--sp-8); background: var(--l2); }
/* the two quiet ones are text, not buttons that shout — delete goes red only when you reach for it */
#app .lb.tmast .amenu .quiet { font-family: var(--font-body); text-transform: none; letter-spacing: 0; font-size: var(--fs-3);
  color: var(--text-muted); padding-top: var(--sp-8); padding-bottom: var(--sp-8); }
#app .lb.tmast .amenu .quiet.danger { color: var(--text-faint); }
#app .lb.tmast .amenu .quiet.danger:hover { color: var(--bad); }
/* a form IN the menu: same box, same width */
#app .lb.tmast .amenu.haspanel { padding: var(--sp-16); }
body.listpage #app .lpop.inmenu { position: static; width: auto; background: none; box-shadow: none; padding: 0; }
body.listpage #app .lpop.inmenu .lpop-row { flex-direction: column; }
/* the form's own buttons inside the menu keep their shapes — the menu-row rule above must not reach them */
#app .lb.tmast .amenu.haspanel .lbtn { display: inline-flex; justify-content: center; text-align: center; width: 100%;
  font-size: var(--fs-4); background: var(--accent-fill); color: #fff; padding: var(--sp-4) var(--sp-24) 0; min-height: 44px; box-shadow: none; }
#app .lb.tmast .amenu.haspanel .lbtn3 { width: auto; font-size: var(--fs-3); background: none; color: var(--text-muted);
  padding: var(--sp-8) 0 0; box-shadow: none; }
#app .lb.tmast .amenu.haspanel .lbtn3:hover { color: var(--text); background: none; }
/* a long-runner's per-type count — the count's own face, no box: a box on this page means "toggle" */
body.listpage #app .slotsum { font-family: var(--font-numbers); font-size: var(--fs-3); letter-spacing: var(--track-caps);
  color: var(--text-muted); white-space: nowrap; padding-top: var(--sp-2); }

/* ═══ HIS SECOND /list PASS (2026-09-24 00:31) ═════════════════════════════════════════════════════ */
/* RMQ's own dialog (dialog.js) — replaces the browser's confirm(); appended to <body>, so NOT under #app */
.rdlg-back { position: fixed; inset: 0; z-index: 90; background: rgba(9, 8, 16, .72); display: grid; place-items: center; padding: var(--sp-16); }
.rdlg { width: min(480px, 100%); background: var(--l3); color: var(--text); font-family: var(--font-body);
  box-shadow: 0 16px 48px rgba(0, 0, 0, .6); padding: var(--sp-24); display: flex; flex-direction: column; gap: var(--sp-24); }
.rdlg-head { display: flex; align-items: center; gap: var(--sp-12); }
.rdlg-title { flex: 1 1 auto; font-family: var(--font-display); font-weight: 400; font-size: var(--fs-6);
  letter-spacing: var(--track-caps); text-transform: uppercase; line-height: var(--lh-solid); padding-top: var(--sp-4); }
/* a typed name inside the title keeps its own case and the names face (the display face has no lowercase) */
.rdlg-title .rdlg-name { font-family: var(--font-names); font-weight: 500; letter-spacing: 0; text-transform: none; }
.rdlg-x { background: none; border: 0; color: var(--text-muted); font-family: var(--font-body); font-size: var(--fs-6);
  line-height: 1; width: 44px; height: 44px; margin: 0 calc(var(--sp-12) * -1) 0 0; padding: 0; cursor: pointer; border-radius: var(--r); }
.rdlg-x:hover { color: var(--text); background: var(--l2); }
.rdlg-body { display: flex; flex-direction: column; gap: var(--sp-12); }
.rdlg-text { margin: 0; font-size: var(--fs-3); line-height: var(--lh-prose); color: var(--text); }
.rdlg-err { margin: 0; font-size: var(--fs-2); color: var(--bad); }
.rdlg-err:empty { display: none; }
.rdlg-field { background: var(--l2); color: var(--text); border: 0; border-bottom: 2px solid var(--text-faint); border-radius: var(--r);
  font-family: var(--font-names); font-weight: 500; font-size: var(--fs-4); padding: 0 var(--sp-16); min-height: 48px; width: 100%; box-sizing: border-box; margin: 0; }
.rdlg-field:focus { outline: none; border-bottom-color: var(--accent); }
.rdlg-field::placeholder { color: var(--text-faint); }
/* the tag/list picker search fell back to Chrome's own #757575 on /list, /library and the lobby (rendered-colour pass
   2026-10-01) — the same faint every other placeholder here wears */
.checkfilter-search::placeholder { color: var(--text-faint); opacity: 1; }
/* two equal buttons, the safe one left, a clear gap between */
.rdlg-acts { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-16); }
.rdlg-acts > :only-child { grid-column: 2; }
.rdlg-btn { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-4); letter-spacing: var(--track-caps); text-transform: uppercase;
  background: var(--l2); color: var(--text); border: 0; border-radius: var(--r); min-height: 48px; padding: var(--sp-4) var(--sp-16) 0; margin: 0; cursor: pointer; }
.rdlg-btn:hover { background: var(--hover-fill); }
.rdlg-btn.go { background: var(--accent-fill); color: #fff; }
.rdlg-btn.go:hover { background: var(--accent); }
.rdlg-btn.go.danger { background: var(--bad); }
.rdlg-btn:disabled { opacity: .6; cursor: default; }
.rdlg-btn:focus-visible, .rdlg-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* the import's answer: two numbers you can read at a glance */
.rdlg-stats { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-16); }
.rdlg-stat { background: var(--l2); padding: var(--sp-16); display: flex; flex-direction: column; gap: var(--sp-4); }
.rdlg-stat b { font-family: var(--font-numbers); font-weight: 400; font-size: var(--fs-8); line-height: var(--lh-solid); }
.rdlg-stat span { font-family: var(--font-display); font-size: var(--fs-2); letter-spacing: var(--track-wider); text-transform: uppercase; color: var(--text-muted); }

/* the picture window, in the same language: bigger, equal buttons with room between, the display face on
   everything but "No picture yet" and "Choose a picture" (his) */
.picbox { max-width: 440px; background: var(--l3); border: 0; padding: var(--sp-24); }
.pictitle { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-6); letter-spacing: var(--track-caps); text-transform: uppercase; margin: 0 0 var(--sp-16); }
.picacts { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-16); margin-top: var(--sp-16); }
.picacts button { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-4); letter-spacing: var(--track-caps); text-transform: uppercase;
  min-height: 48px; min-width: 0; margin: 0; border: 0; border-radius: var(--r); padding: var(--sp-4) var(--sp-16) 0; background: var(--l2); color: var(--text); }
.picacts .piccancel { order: -1; }
.picacts .picsave { background: var(--accent-fill); color: #fff; }
.picacts .picclear { grid-column: 1 / -1; background: none; color: var(--text-muted); }

/* the forms in a menu: Cancel and Save on one row, the same size */
body.listpage #app .lpop-acts { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-12); }
body.listpage #app .lpop-acts button { width: 100%; justify-content: center; }
#app .lb.tmast .amenu.haspanel .lbtn2 { display: inline-flex; justify-content: center; font-size: var(--fs-4); background: var(--l2); color: var(--text);
  padding: var(--sp-4) var(--sp-16) 0; min-height: 44px; box-shadow: none; }
#app .lb.tmast .amenu.haspanel .lbtn2:hover { background: var(--hover-fill); }
#app .lb.tmast .amenu.haspanel .lbtn { width: auto; }
/* the account menu's two quiet rows sit closer (his "make the gap of not you and delete smaller") */
#app .lb.tmast .amenu .quiet { padding-top: var(--sp-4); padding-bottom: var(--sp-4); }
#app .lb.tmast .amenu .sep + .quiet { padding-top: var(--sp-8); }
#app .lb.tmast .amenu .quiet:last-child { padding-bottom: var(--sp-8); }
#app .lb.tmast .amenu .quiet + .quiet { box-shadow: none; }

/* the list menu: the LISTS are the content, the verbs are quiet text under them (his: "the options stood out too much") */
body.listpage #app .lmenu button.verb { font-family: var(--font-body); font-weight: 400; font-size: var(--fs-3); text-transform: none;
  letter-spacing: 0; color: var(--text-muted); padding-top: var(--sp-4); padding-bottom: var(--sp-4); }
body.listpage #app .lmenu button.verb:hover { color: var(--text); }
body.listpage #app .lmenu button.verb.danger { color: var(--text-faint); }
body.listpage #app .lmenu button.verb.danger:hover { color: var(--bad); }
body.listpage #app .lmenu .sep + .verb { padding-top: var(--sp-8); }
body.listpage #app .lmenu button:not(.verb) { font-size: var(--fs-4); padding-top: var(--sp-12); padding-bottom: var(--sp-12); }

/* the list picker: text, no box (his), and a long name steps down instead of pushing the strip */
body.listpage #app .listpick { background: none; padding: 0 var(--sp-8) 0 0; max-width: 320px; }
body.listpage #app .listpick:hover { background: none; }
body.listpage #app .listpick .val { overflow-wrap: anywhere; }
body.listpage #app .listpick.long .val { font-size: var(--fs-3); }

/* an unticked box must still read as a box you can tick (his: "too dark … feel it's nothing pressable") */
body.listpage #app input.pick:not(:checked):not(:indeterminate) { border-color: var(--text-faint); background-color: var(--l2); }

/* the pager's "…" is a door: click it, type a page */
:is(body.listpage, body.libpage) #app button.lgap { background: none; border: 0; cursor: pointer; min-width: 32px; min-height: 44px; margin: 0; padding: 0; }
:is(body.listpage, body.libpage) #app button.lgap:hover { color: var(--text); background: var(--l1); }
body.listpage #app .lgoto { width: 64px; min-height: 44px; background: var(--l3); color: var(--text); border: 0; border-bottom: 2px solid var(--accent);
  border-radius: var(--r); font-family: var(--font-numbers); font-size: var(--fs-4); text-align: center; margin: 0; padding: var(--sp-4) 0 0; }
body.listpage #app .lgoto:focus { outline: none; }

/* the tag filter's panel, in the page's language: underline search, the tick squares, names left and counts
   right, room between rows, Clear as a quiet word (his) */
:is(body.listpage, body.libpage) #app .checkfilter-panel { background: var(--l3); padding: var(--sp-12); min-width: 280px; }
:is(body.listpage, body.libpage) #app .checkfilter-search { width: 100%; box-sizing: border-box; background: var(--l2); color: var(--text); border: 0;
  border-bottom: 2px solid var(--text-faint); border-radius: var(--r); font-family: var(--font-body); font-size: var(--fs-3); min-height: 44px; padding: 0 var(--sp-12); margin: 0 0 var(--sp-8); }
:is(body.listpage, body.libpage) #app .checkfilter-search:focus { outline: none; border-bottom-color: var(--accent); }
:is(body.listpage, body.libpage) #app .checkfilter-row { display: flex; align-items: center; gap: var(--sp-12); min-height: 40px; padding: 0 var(--sp-4); cursor: pointer; border-radius: var(--r); }
:is(body.listpage, body.libpage) #app .checkfilter-row:hover { background: var(--hover-fill); }
:is(body.listpage, body.libpage) #app .checkfilter-row input { appearance: none; -webkit-appearance: none; width: 20px; height: 20px; flex: 0 0 20px; margin: 0;
  border: 1px solid var(--text-faint); background: var(--l2) center / 14px no-repeat; border-radius: var(--r); cursor: pointer; }
:is(body.listpage, body.libpage) #app .checkfilter-row input:checked { background-color: var(--accent-fill); border-color: var(--accent-fill);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 7l3.2 3L13 3' fill='none' stroke='%23fff' stroke-width='2.2'/%3E%3C/svg%3E"); }
:is(body.listpage, body.libpage) #app .checkfilter-name { flex: 1 1 auto; min-width: 0; font-family: var(--font-names); font-weight: 500; font-size: var(--fs-3); color: var(--text); overflow-wrap: anywhere; }
:is(body.listpage, body.libpage) #app .checkfilter-n { font-family: var(--font-numbers); font-size: var(--fs-4); color: var(--text-muted); }
:is(body.listpage, body.libpage) #app .checkfilter-clear { width: 100%; margin: var(--sp-8) 0 0; background: none; border: 0; color: var(--text-muted);
  font-family: var(--font-display); font-size: var(--fs-3); letter-spacing: var(--track-caps); text-transform: uppercase; min-height: 40px; cursor: pointer; }
:is(body.listpage, body.libpage) #app .checkfilter-clear:hover { color: var(--text); }

/* ═══ HIS THIRD /list PASS (2026-09-24 01:01) ══════════════════════════════════════════════════════ */
/* an OFF song keeps its tick and ▶ exactly like an included one — a global `.song.off { opacity: .5 }` was
   dimming the WHOLE row, controls included (measured: the row's own opacity, not the children's) */
body.listpage #app .srow.song.off { opacity: 1; }
/* a song the OP/ED/INS or ⚠ toggles leave out stays in the opened row, faded right down (no sentence) */
body.listpage #app .srow.song.filtered { opacity: .3; }

/* MENUS: every option is its OWN LINE — a hairline between rows (his: "it's still doesn't feel it's a separate
   lines"). The verbs keep the display face, a size down and muted (his: "keep the options the blocky font").
   Delete fills red when you reach for it, in both menus. */
body.listpage #app .lmenu { padding: 0; }
body.listpage #app .lmenu > button + button { box-shadow: inset 0 1px 0 var(--rule); }
body.listpage #app .lmenu .sep { height: var(--sp-8); margin: 0; background: var(--l2); }
body.listpage #app .lmenu button.verb { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-3); letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--text-muted); padding: var(--sp-12) var(--sp-16) var(--sp-8); }
body.listpage #app .lmenu .sep + .verb { padding-top: var(--sp-12); }
body.listpage #app .lmenu button.verb.danger { color: var(--text-muted); }
body.listpage #app .lmenu button.verb.danger:hover:not(:disabled) { background: var(--bad); color: #fff; }
/* ⛔ MANY LISTS / MANY TAGS (his, 2026-09-24: "the tags should be fixed at x rows … and scrollable after that").
   The lists get their OWN scroll box, whole rows tall, and the verbs under it stay put; the tag panel's rows
   likewise, with the search on top and Clear under them fixed. Row counts: 8 lists, 10 tags (mine, his "idk
   how much yet"). A name long enough to wrap makes its row taller, so the cut is exact for one-line names. */
body.listpage #app .lmenu { overflow: hidden; max-height: none; }
body.listpage #app .lmenu-lists { display: flex; flex-direction: column; overflow-y: auto; overscroll-behavior: contain;
  max-height: max(114px, min(calc(8 * 57px), round(down, calc(100dvh - 520px), 57px))); }   /* 57 = one list row, measured; a short window takes rows off */
body.listpage #app .lmenu-lists > button { flex: 0 0 auto; }
body.listpage #app .lmenu-lists > button + button { box-shadow: inset 0 1px 0 var(--rule); }
body.listpage #app .lmenu-lists > button[hidden] { display: none; }
body.listpage #app .lmenu-find { margin: var(--sp-12); width: calc(100% - 2 * var(--sp-12)); box-sizing: border-box; background: var(--l2); color: var(--text);
  border: 0; border-bottom: 2px solid var(--text-faint); border-radius: var(--r); font-family: var(--font-body); font-size: var(--fs-3);
  min-height: 44px; padding: 0 var(--sp-12); }
body.listpage #app .lmenu-find:focus { outline: none; border-bottom-color: var(--accent); }
:is(body.listpage, body.libpage) #app .checkfilter-rows { max-height: max(120px, min(calc(10 * 40px), round(down, calc(100dvh - 380px), 40px))); overscroll-behavior: contain; }
:is(body.listpage, body.libpage) #app .checkfilter-row { min-height: 40px; }
#app .lb.tmast .amenu .quiet { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-3); letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--text-muted); padding: var(--sp-12) var(--sp-16) var(--sp-8); }
#app .lb.tmast .amenu .sep + .quiet { padding-top: var(--sp-12); }
#app .lb.tmast .amenu .quiet:last-child { padding-bottom: var(--sp-8); }
#app .lb.tmast .amenu .quiet + .quiet { box-shadow: inset 0 1px 0 var(--rule); }
#app .lb.tmast .amenu .quiet.danger { color: var(--text-muted); }
#app .lb.tmast .amenu .quiet.danger:hover { background: var(--bad); color: #fff; }

/* Link a device: the code IS the message */
.rdlg-code { font-family: var(--font-numbers); font-size: var(--fs-9); letter-spacing: var(--track-wider); text-align: center;
  line-height: var(--lh-solid); background: var(--l2); padding: var(--sp-24) 0 var(--sp-16); }
.rdlg-note { margin: 0; text-align: center; font-size: var(--fs-2); color: var(--text-muted); }

/* the tag panel's Clear gets a box (his) */
:is(body.listpage, body.libpage) #app .checkfilter-clear { background: var(--l2); border-radius: var(--r); padding-top: var(--sp-4); }
:is(body.listpage, body.libpage) #app .checkfilter-clear:hover { background: var(--hover-fill); }
/* the page box types digits only — no spinner arrows (his) */
body.listpage #app .lgoto { -moz-appearance: textfield; appearance: textfield; }
body.listpage #app .lgoto::-webkit-outer-spin-button, body.listpage #app .lgoto::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
/* "Hidden by filters" / "No songs yet" — the label face, quiet, where the songs would start */
body.listpage #app .kids .kidnote { font-family: var(--font-display); font-size: var(--fs-3); letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--text-faint); padding: var(--sp-12) var(--sp-16); margin: 0; }
/* the empty page speaks where the first row would have been, right under the controls that emptied it */
body.listpage #app .lempty { padding: var(--sp-24) var(--sp-12); gap: var(--sp-16); }
/* a dialog with ONE button gives it the whole row (his: "try making the box full. not only on the right") */
.rdlg-acts > :only-child { grid-column: 1 / -1; }
/* the strip's first row: the search text at the size of the words beside it, the row 48 tall */
:is(body.listpage, body.libpage) #app .lstrip .lsrow:first-child > * { min-height: 48px; }
:is(body.listpage, body.libpage) #app .lsearch { font-size: var(--fs-4); min-height: 48px; }
:is(body.listpage, body.libpage) #app .lstrip .lsrow:first-child .lseg button { min-height: 48px; }
/* ink, measured (list-align.py): the Mitr arrows took the Bebas padding and sat 3.8px low; the search text 1.2px low */
:is(body.listpage, body.libpage) #app .lpage[aria-label] { padding-top: 0; padding-bottom: var(--sp-8); }
:is(body.listpage, body.libpage) #app .lsearch { padding-bottom: var(--sp-2); }

/* ═══ /library, CONVERTED IN PLACE (2026-09-24) — what /list and /add became ═══════════════════════════════════
   The lobby's masthead (pagenav.js mountToolMast), /list's strip, segments, underline fields, pager and totals
   (their rules above were widened to `:is(body.listpage, body.libpage)` — one rule, two pages). Structure is
   unchanged: the five /library calls in STILL HIS are his. Everything here is under `body.libpage`. */
html:has(body.libpage) { scrollbar-gutter: auto; }
body.libpage #app { --edge: clamp(10px, 1.25vw, 24px); max-width: none; width: 100%; box-sizing: border-box; margin: 0; padding: 0 var(--edge); }   /* border-box: below the 1101 frame #app was 100% + 2 edges wide, the page scrolled sideways and the top row sat off the lobby (PL6) */
body.libpage #app .lab { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-2);
  letter-spacing: var(--track-wider); text-transform: uppercase; color: var(--text-faint); }
body.libpage #app > p.muted { margin: var(--sp-8) 0 0; font-size: var(--fs-2); color: var(--text-muted); }
body.libpage #app #lib-flash:empty { display: none; }
/* the strip: tabs · search · volume, then the filters */
body.libpage #app .lstrip { margin: 0 0 var(--sp-8); }
body.libpage #app .lib-tabs { margin: 0; flex-wrap: nowrap; border: 0; }
body.libpage #app .lib-tabs .tab { border-bottom: 0; }
body.libpage #app .lsrow .guessbox { flex: 1 1 320px; display: flex; margin: 0; min-width: 0; }
body.libpage #app .lsrow .guessbox .lsearch { width: 100%; }
body.libpage #app .lib-filter { margin: 0; }
body.libpage #app .lib-filter .tagright { display: flex; gap: var(--sp-8); }
body.libpage #app .lib-filter .checkfilter-toggle { min-height: 44px; }
/* GROUP BY — a drop target, drawn as a quiet filled band; it lights only while a header is carried over it */
body.libpage #app .gby { background: var(--l1); border: 0; border-radius: var(--r); min-height: 40px; padding: 0 var(--sp-12); margin: 0 0 var(--sp-4); }
body.libpage #app .gby.over { background: var(--l3); box-shadow: inset 0 -2px 0 var(--text); }
body.libpage #app .gby .lbl { font-family: var(--font-display); font-size: var(--fs-2); letter-spacing: var(--track-wider); color: var(--text-faint); }
body.libpage #app .gby .gchip { font-family: var(--font-display); font-size: var(--fs-3); letter-spacing: var(--track-caps); text-transform: uppercase;
  background: var(--l3); border: 0; color: var(--text); padding: var(--sp-4) var(--sp-4) var(--sp-4) var(--sp-12); }
/* the header labels: the lobby's tracked label */
body.libpage #app .ledger .lhead { font-family: var(--font-display); font-size: var(--fs-2); letter-spacing: var(--track-wider); color: var(--text-faint);
  border-bottom: 1px solid var(--rule); background: var(--bg); }
body.libpage #app .ledger .lhead .hdrag { border-bottom: 0; }
/* rows: fill only; the type in TV's voice, no colour and no box (/list's OP1) */
body.libpage #app .ledger .lrow:hover { background: var(--l1); }
body.libpage #app .ledger .lrow.sel { background: var(--l2); }
body.libpage #app .ltype { font-family: var(--font-display); font-size: var(--fs-4); letter-spacing: var(--track-caps); color: var(--text-muted);
  background: none; border: 0; padding: 0; text-align: left; }
body.libpage #app :is(.lflag.warn, .lcut) { font-family: var(--font-display); font-size: var(--fs-3); letter-spacing: var(--track-caps);
  color: var(--text-muted); background: none; border: 0; padding: 0; }
body.libpage #app .lflag.del { font-family: var(--font-display); font-size: var(--fs-3); letter-spacing: var(--track-caps); color: var(--bad);
  background: none; border: 0; padding: 0; }
/* a TAG is typed data: never the display face (Bebas has no lowercase) */
/* …and a READOUT, so plain text, never a chip (on these pages a box means a control: his /list rule, 2026-09-24) */
body.libpage #app .lflag.tag { font-family: var(--font-body); font-size: var(--fs-2); letter-spacing: 0; text-transform: none; color: var(--text-muted);
  background: none; border: 0; padding: 0; }
body.libpage #app .lflags { gap: var(--sp-12); }
body.libpage #app .vchip { font-family: var(--font-body); font-size: var(--fs-1); color: var(--text-muted); background: var(--l3); border: 0;
  padding: 2px var(--sp-8); border-radius: var(--r); }
/* the panel: a filled pane, the lobby's labels, the page's underline fields */
/* the panel is OPEN (on the ground, like the round's side panels) so each credit card reads as its own box — on an l1 pane
   the l2 cards were 3% apart and the empty "Artist name" card sat on the same background as Tags (his, 2026-09-24) */
body.libpage #app .ldetail { background: none; border: 0; }
body.libpage #app :is(.ldetail, .lbody .lib-edit) :is(.lblabel, .credit-label, label > span:first-child) { font-family: var(--font-display); font-size: var(--fs-2);
  letter-spacing: var(--track-wider); text-transform: uppercase; color: var(--text-faint); font-weight: 400; }
body.libpage #app .ldetail h3.panel-anime { font-family: var(--font-names); font-weight: 600; font-size: var(--fs-4); color: var(--text);
  letter-spacing: 0; text-transform: none; }
body.libpage #app :is(.ldetail, .lbody .lib-edit) :is(input:not([type=checkbox]):not([type=radio]):not(.checkfilter-search), select) { background: var(--l3);
  border: 0; border-bottom: 2px solid var(--text-faint); border-radius: var(--r); font-family: var(--font-body); font-size: var(--fs-3);
  min-height: 40px; padding: 0 var(--sp-12); }
body.libpage #app :is(.ldetail, .lbody .lib-edit) :is(input, select):focus { outline: none; border-bottom-color: var(--accent); }
body.libpage #app .ldetail input.generated { color: var(--text-muted); }
body.libpage #app .ldetail .lib-triple select { font-family: var(--font-display); font-size: var(--fs-4); letter-spacing: var(--track-caps); }
body.libpage #app :is(.ldetail, .lbody) button.minor { font-family: var(--font-display); font-size: var(--fs-3); letter-spacing: var(--track-caps);
  text-transform: uppercase; background: var(--l2); border: 0; color: var(--text-muted); }
body.libpage #app :is(.ldetail, .lbody) button.minor:hover { background: var(--l3); color: var(--text); }
body.libpage #app .ldetail button.credit-rm { font-family: var(--font-body); background: none; }
body.libpage #app .ldetail button.credit-rm:hover { background: var(--l2); color: var(--bad); }
/* the scrub bar's knob at the play head (/list's and /add's) */
body.libpage #app .ldetail .sbar-fill::after { content: ''; position: absolute; right: -7px; top: 50%; width: 14px; height: 14px; border-radius: var(--r-pill);
  background: var(--accent); transform: translateY(-50%); }
/* the pager: /list's, with /library's per-page and the totals at the right end */
body.libpage #app .lpager .per { display: flex; align-items: center; gap: var(--sp-8); margin: 0 var(--sp-24) 0 0; }
body.libpage #app .lpager .per .checkfilter-toggle { font-family: var(--font-numbers); font-size: var(--fs-4); min-height: 44px; background: var(--l1); }
body.libpage #app .lpager .pickmenu-panel { top: auto; bottom: calc(100% + 4px); left: auto; right: 0; }
body.libpage #app .lpager .lnone { margin-right: var(--sp-24); }
body.libpage #app .lpager .ltot { flex: none; flex-wrap: nowrap; }
body.libpage #app .lpager .ltot .lab + .sm { margin-left: var(--sp-12); }
body.libpage #app .lpager input.lgoto { width: 72px; min-height: 44px; background: var(--l3); color: var(--text); border: 0;
  border-bottom: 2px solid var(--accent); font-family: var(--font-numbers); font-size: var(--fs-4); text-align: center; -moz-appearance: textfield; appearance: textfield; }
body.libpage #app .lpager input.lgoto::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
/* the save bar: the page's buttons */
body.libpage .savebar { background: var(--l2); border-top: 0; padding: var(--sp-12) 24px; }
body.libpage .savebar-msg { font-size: var(--fs-3); color: var(--text-muted); }
body.libpage .savebar button { font-family: var(--font-display); font-size: var(--fs-4); letter-spacing: var(--track-caps); text-transform: uppercase;
  border: 0; border-radius: var(--r); min-height: 44px; padding: 0 var(--sp-24); }
body.libpage .savebar button.primary { background: var(--accent-fill); color: #fff; }
body.libpage .savebar button.minor { background: var(--l3); color: var(--text); }
.rdlg .rdlg-text { white-space: pre-line; }
.rdlg .lfield { width: 100%; box-sizing: border-box; background: var(--l2); color: var(--text); border: 0; border-bottom: 2px solid var(--text-faint);
  border-radius: var(--r); font-family: var(--font-body); font-size: var(--fs-3); padding: 0 var(--sp-16); min-height: 44px; }
.rdlg .lfield:focus { outline: none; border-bottom-color: var(--accent); }
/* one credit = one filled block (was a hairline box); the sample button is the page's small fill button */
body.libpage #app .ldetail .credit-block { border-color: transparent; background: var(--l2); }   /* the 1px stays: R7 (its margin cancels padding + border) */
body.libpage #app .ldetail .psample { background: var(--l2); border: 0; min-height: 32px; padding: 0 var(--sp-8); }
body.libpage #app .ldetail .psample:hover { background: var(--l3); }
body.libpage #app .ldetail .psample.on { color: var(--accent); }
/* the pane ends 24px above the screen's edge, the pager with it (/list's bottom) */
body.libframe #app .lpane { padding-bottom: var(--sp-24); }
body.libpage #app .lpager { padding-bottom: 0; flex-wrap: nowrap; white-space: nowrap; }
/* ≤1500px the ledger shares the width with the panel: the per-page picker keeps its number, drops its word */
@media (max-width: 1500px) { body.libpage #app .lpager .per > .lab { display: none; } body.libpage #app .lpager .per { margin-right: var(--sp-12); }
  body.libpage #app .lpager :is(.lpage, .lgap) { min-width: 36px; padding-left: var(--sp-4); padding-right: var(--sp-4); } }
/* the other four tabs: the lobby's label on every heading and column head, status words in TV's voice */
body.libpage #app .lbody h2 { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-5); letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--text-muted); margin: var(--sp-24) 0 var(--sp-8); }
body.libpage #app .lbody > h2:first-child { margin-top: var(--sp-12); }
body.libpage #app .ldetail h3 { font-family: var(--font-display); font-weight: 400; letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--text-muted); }
body.libpage #app .req-status { font-family: var(--font-display); font-size: var(--fs-3); letter-spacing: var(--track-caps); text-transform: uppercase;
  background: none; border: 0; padding: 0; min-width: 80px; }
body.libpage #app .rcount { font-family: var(--font-display); font-size: var(--fs-2); letter-spacing: var(--track-wider); text-transform: uppercase;
  color: var(--text-faint); margin: var(--sp-12) 0 var(--sp-4); }
body.libpage #app .rhead { font-family: var(--font-display); font-size: var(--fs-2); letter-spacing: var(--track-wider); color: var(--text-faint);
  border-bottom: 1px solid var(--rule); }
body.libpage #app :is(.rrow, .log-row):hover { background: var(--l1); }
/* a lone ✕ is a quiet mark, never a boxed button (the Tags tab's delete, like a credit's ✕) */
body.libpage #app .lbody button.minor:is([title*="Delete" i], [aria-label*="Delete" i]), body.libpage #app .lbody .tagrow button.minor { background: none; min-width: 40px; }
/* ⛔ OPTICAL CENTRING (DESIGN.md), measured by scripts/geom/list-align.py library: Bebas draws ~1.8px HIGH in a centred
   box — the tabs, OP·ED·INS·CUS, the toggles, the pager and the type cell. /list's 4px-top correction, after the base rules. */
body.libpage #app :is(.lseg button, .lpage) { padding-top: var(--sp-4); }
body.libpage #app .ledger .lrow .ltype { padding-top: var(--sp-4); }
body.libpage #app .lseg.types button, body.libpage #app .lpage[aria-label] { padding-top: var(--sp-4); }   /* their own rules set padding at (1,3,1) */
/* ⛔ NO NUDGE ON THE TOOL PAGES' RMQ (his, 2026-09-25: "'rmq' on add/lib/list aren't the same position as in lobby"). A
   4px top padding here (2026-09-24, "1.5px high") was measured against the tool row itself, not the lobby, and put the
   wordmark's ink 2px BELOW the lobby's at every width. The lobby is the reference; browse-check J24b compares against it. */
/* ── /library, HIS FIRST USE PASS (2026-09-24 12:28) ─────────────────────────────────────────────────────────── */
/* DELETE HAS TWO SHAPES: a row's trash icon, an editor's boxed button that says it */
body.libpage #app .ltrash { display: inline-grid; place-items: center; width: 40px; height: 40px; padding: 0; margin: 0; background: none;
  border: 0; border-radius: var(--r); color: var(--text-muted); cursor: pointer; font-family: var(--font-body); font-size: var(--fs-3); }
body.libpage #app .ltrash:hover { background: var(--l2); color: var(--bad); }
body.libpage #app :is(.ldetail, .lbody) button.ldelbox { font-family: var(--font-display); font-size: var(--fs-4); letter-spacing: var(--track-caps);
  text-transform: uppercase; background: var(--l2); color: var(--text); border: 0; border-radius: var(--r); min-height: 44px;
  padding: var(--sp-4) var(--sp-24) 0; margin: 0; justify-self: start; }
body.libpage #app :is(.ldetail, .lbody) button.ldelbox:hover { background: var(--bad); color: #fff; }
/* the tag cell: the panel's own chip, minus its ✕, on ONE line */
body.libpage #app .ltags { display: flex; flex-wrap: nowrap; gap: var(--sp-8); overflow: hidden; min-width: 0; }
/* what does not fit becomes one +N chip at the right end (his, 2026-09-24 — /add's tag row does the same) */
body.libpage #app .ltags .tmore { font-family: var(--font-numbers); font-size: var(--fs-3); color: var(--text); background: var(--bg);
  border: 1px solid #95a0f266; border-radius: var(--r); padding: var(--sp-2) var(--sp-8) 0; flex: none; }   /* the chips' own box */
body.libpage #app .lflag.tag.tpill { font-family: var(--font-body); font-size: var(--fs-1); letter-spacing: 0; text-transform: none; color: var(--text);
  background: var(--bg); border: 1px solid #95a0f266; border-radius: var(--r); padding: var(--sp-4) var(--sp-8); white-space: nowrap; flex: none; }
/* the header and GROUP BY one step up, and the six dots centred on the word */
body.libpage #app .ledger .lhead { font-size: var(--fs-3); }
body.libpage #app .gby .lbl { font-size: var(--fs-3); }
/* the label ends at a rule (his, 2026-09-24: 'add a "|" … to show that group by isn't just a plain text') — what follows it is
   the drop zone, not more of the label */
body.libpage #app .gby .lbl { display: inline-flex; align-items: center; align-self: stretch; padding-right: var(--sp-12);
  margin-right: var(--sp-4); box-shadow: inset -1px 0 0 var(--text-faint); }
body.libpage #app .ledger .lhead .hdrag { display: inline-flex; align-items: center; }
/* ⛔ THE SIX DOTS ARE DRAWN, NOT A GLYPH (his, 2026-09-24: "those 6 dots aren't align to the header yet"). '⋮⋮' comes from
   whichever fallback font the MACHINE has — centred in headless Chromium, low on his Windows Chrome. Two columns of three
   1.5px dots, 12px tall = the word's cap height, centred by flex: the same on every machine. */
body.libpage #app .ledger .lhead .hdrag::before { content: ''; flex: none; width: 5px; height: 12px; margin: 0 var(--sp-8) 0 0; letter-spacing: 0;
  background: radial-gradient(circle, currentColor 0.9px, transparent 1.2px) 0 0 / 2.5px 4px; opacity: .85; position: relative; top: -1px; }   /* ink: dots 11.0 vs word 10.5 (-1.5px lands 10.0; half a pixel either way) */
/* Type · No. · Cut at their own widths, Version the rest (/add's row); their values and the role in the display face */
/* Type · No. · Cut at /add's own widths (his, 2026-09-24: "make these three boxes the same size as in /add") */
body.libpage #app .ldetail .lib-triple { display: grid; grid-template-columns: 76px 56px 84px; gap: var(--sp-12); width: 100%; justify-self: stretch; }
body.libpage #app .ldetail .lib-triple > label { flex: none; min-width: 0; }
body.libpage #app .ldetail .lib-triple :is(input, select) { width: 100%; min-width: 0; box-sizing: border-box; }
/* ⚠ (1,6,2): the page's field rule is (1,5,2) and set the body face over these (his "why aren't op,1,tv,composer changed") */
body.libpage #app .ldetail .lib-edit.lib-edit :is(.lib-triple select, .lib-triple input[type=number], .credit-row select) { font-family: var(--font-display);
  font-size: var(--fs-4); letter-spacing: var(--track-caps); text-transform: uppercase; }
/* Type · No. · Cut (his 2026-09-24, second look: "no. isn't the same size as type and cut … maybe the box is also too big
   compared to title, version, credit"). The 26px face had grown the two SELECTS to 42 while the number box stayed 40, and read
   big beside the 40px Title/Version/Credit. So: all three a fixed 40 like every field in this panel, and 22px — /add's
   face-to-box proportion (26 in 48 = .54, 22 in 40 = .55). Bebas draws high in a centred box, so 2px goes on top. */
body.libpage #app .ldetail .lib-edit.lib-edit :is(.lib-triple select, .lib-triple input[type=number]) { font-size: var(--fs-4); height: 40px; padding-top: 2px; padding-bottom: 0; }
body.libpage #app .ldetail .lib-triple input[type=number] { -moz-appearance: textfield; appearance: textfield; }
body.libpage #app .ldetail .lib-triple input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
/* the credits end, then air before Tags — the blank "add an artist" row belongs to the credits, not to what follows */
body.libpage #app .ldetail .lib-credits { margin-bottom: var(--sp-16); }
body.libpage #app .ldetail .lblock { padding-top: var(--sp-16); margin-top: var(--sp-8); }
/* the player: a FIXED ROW at the panel's foot (his, 2026-09-24, "like 'from' in game screen") — ONE opaque box the panel's full
   width: ▶ on the left, what is playing beside it, the clock on the right, the scrub bar along its bottom edge. Sticky at
   bottom 0 with no panel padding under it, so it sits on the real bottom and does not move when the editor reaches its end. */
body.libpage #app .ldetail:has(> .lplayer) { padding-bottom: 0; }
body.libpage #app .ldetail > .lplayer.lplayer { position: sticky; bottom: 0; z-index: 3; display: block; padding: 0; background: none;
  margin: var(--sp-16) calc(-1 * (var(--sp-12) + 1px)) 0; }   /* the artist cards' own bleed (.credit-block) — edge to edge, no band */
body.libpage #app .ldetail .lplayer .psample { position: relative; display: flex; align-items: center; width: 100%; min-height: 56px; margin: 0;
  padding: var(--sp-4) var(--sp-16) 0; gap: var(--sp-12); border: 0; border-radius: var(--r); cursor: pointer;
  background: color-mix(in srgb, var(--bg) 88%, white 12%); font-family: var(--font-numbers); font-size: var(--fs-4); color: var(--text-muted); }
body.libpage #app .ldetail .lplayer .psample:hover { background: color-mix(in srgb, var(--bg) 84%, white 16%); }
body.libpage #app .ldetail .lplayer .psample-glyph { font-family: var(--font-body); font-size: var(--fs-3); line-height: 1; width: 20px; flex: none;
  display: inline-grid; place-items: center; margin-top: calc(var(--sp-4) * -1); color: var(--text); }
body.libpage #app .ldetail .lplayer .psample-what { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left;
  font-family: var(--font-body); font-size: var(--fs-3); color: var(--text); margin-top: calc(var(--sp-4) * -1); }
body.libpage #app .ldetail .lplayer .psample-time { flex: none; margin-left: auto; }
body.libpage #app .ldetail .lplayer .sbar { left: 0; right: 0; bottom: 0; }
/* ⛔ IN THE FRAME THE PLAYER NEVER MOVES (his, 2026-10-24): the panel stops scrolling, the editor scrolls inside `.ldbody`, and
   the player is the panel's last row OUTSIDE that scroll — the same place it sat, 3px in from the panel's edges as before. */
@media (min-width: 1101px) and (min-height: 480px) {
  body.libpage.libframe #app .ldetail:has(> .ldbody) { display: flex; flex-direction: column; overflow: hidden; padding: 0; gap: 0; }
  body.libpage.libframe #app .ldetail > .ldbody { display: grid; gap: var(--sp-12); align-content: start; flex: 1 1 auto; min-height: 0;
    overflow-y: auto; padding: var(--sp-16); }
  body.libpage.libframe #app .ldetail > .lplayer.lplayer { position: relative; flex: none; margin: 0 calc(var(--sp-16) - var(--sp-12) - 1px) 0; }
}
body.libpage #app .ldetail .panel-anime-name { font-family: var(--font-names); font-weight: 600; font-size: var(--fs-4); color: var(--text); margin: 0 0 var(--sp-8); }
.sr-only { position: absolute !important; width: 1px; height: 1px; margin: 0; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
body.libpage #app .ldetail .panel-anime-row { padding-bottom: var(--sp-16); margin-bottom: var(--sp-8); }
body.libpage #app .ldetail .psample { font-family: var(--font-numbers); font-size: var(--fs-4); color: var(--text-muted); min-height: 36px;
  padding: var(--sp-4) var(--sp-12) 0; gap: var(--sp-8); }
body.libpage #app .ldetail .psample-glyph { font-family: var(--font-body); font-size: var(--fs-2); }
/* the Artists and Tags rosters: no caret, numbers in the numbers face, the header stays while the list scrolls */
body.libpage #app .rrow-artist { grid-template-columns: minmax(220px, 3fr) 96px 96px 96px; }
body.libpage #app .rentry > summary { list-style: none; }
body.libpage #app .rentry > summary::-webkit-details-marker { display: none; }
body.libpage #app :is(.rrow, .rhead) .rcell { font-family: var(--font-numbers); font-size: var(--fs-4); color: var(--text-muted); }
body.libpage #app .libroster .rhead, body.libpage #app .deleted-box .rhead { position: sticky; top: 0; z-index: 2; background: var(--bg);
  font-size: var(--fs-3); padding-top: var(--sp-8); padding-bottom: var(--sp-8); }
body.libpage #app .rfoot { border-top: 1px solid var(--rule); padding: var(--sp-12) 0 var(--sp-24); }
body.libpage #app .rfoot .ltot .big { font-size: var(--fs-5); }
/* the artist editor: the panel's pane (fill l1, fields l3 — they read as fields again), its ticks /list's squares */
body.libpage #app .lbody .rform { background: var(--l1); border: 0; }
body.libpage #app .lbody .rform input[type=checkbox], body.libpage #app .ldetail input[type=checkbox] { appearance: none; -webkit-appearance: none;
  width: 20px; height: 20px; margin: 0; background: var(--l3); border: 1px solid var(--text-faint); border-radius: var(--r); cursor: pointer; }
/* ⚠ background-COLOR only: the `background` shorthand wiped the white check (his, 2026-09-24: "why the checkbox here doesn't have
   white checkmark?") — the tick is the tag picker's and /list's own */
body.libpage #app :is(.lbody .rform, .ldetail) input[type=checkbox]:checked { background-color: var(--accent-fill); border-color: var(--accent-fill);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 7l3.2 3L13 3' fill='none' stroke='%23fff' stroke-width='2.2'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; background-size: 80%; }
body.libpage #app .lbody .rform button.minor:not(.ldelbox) { min-width: 40px; min-height: 40px; }
/* the change log: WHEN · WHO · WHAT · the target, in fixed columns; no caret */
body.libpage #app .log-row { display: grid; grid-template-columns: 104px 360px 120px 72px minmax(0, 1fr); align-items: baseline; column-gap: var(--sp-16);
  padding: var(--sp-8) var(--sp-12); list-style: none; cursor: pointer; }
body.libpage #app .log-row::before, body.libpage #app .log-entry[open] > .log-row::before { content: none; }
body.libpage #app .log-row::-webkit-details-marker { display: none; }
body.libpage #app .log-row :is(.log-when, .log-kind) { font-family: var(--font-display); font-size: var(--fs-4); letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--text-muted); white-space: nowrap; }
body.libpage #app .log-row .log-who { color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
body.libpage #app .log-row .log-target { min-width: 0; }
body.libpage #app .log-detail { padding: var(--sp-4) var(--sp-12) var(--sp-12) calc(104px + 360px + 120px + 72px + 4 * var(--sp-16) + var(--sp-12)); }
/* the Deleted tab: the ledger's columns, read-only */
body.libpage #app .ldelrow { display: grid; grid-template-columns: var(--dcols, 58px 220px 250px minmax(160px, 1fr) 120px); column-gap: var(--sp-16); align-items: center;
  padding: var(--sp-8) var(--sp-12); }
body.libpage #app .ldelrow.song.off { opacity: 1; color: var(--text); }
body.libpage #app .ldelrow .tags { justify-self: end; display: flex; align-items: center; margin: 0; }
body.libpage #app .ldelrow :is(.tags button, span) { margin: 0; }
body.libpage #app .ldelrow.song { display: grid; align-items: center; }
body.libpage #app .ldelrow > .ell { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
body.libpage #app .deleted-box { border: 0; margin: 0; padding: 0; max-height: none; }
body.libpage #app :is(.ldetail, .rform) :is(.lblock, .lib-row):has(> .ldelbox) { padding-left: 0; margin-left: 0; }
/* ── /library, HIS SECOND USE PASS (2026-09-24 15:16) ────────────────────────────────────────────────────────── */
/* the artist editor: longer boxes, the person tick a 22px square CENTRED on its field, every small control a 40px box */
body.libpage #app .lbody .rform { --row-w: 480px; }
body.libpage #app .lbody .rform :is(.credit-row, .lineup-pick-row) { align-items: center; }
body.libpage #app .lbody .rform :is(label > input[type=text], .credit-row input[type=text], .lineup-pick) { width: var(--row-w); max-width: 100%; min-height: 44px; }
body.libpage #app .lbody .rform .credit-swap { display: inline-flex; align-items: center; gap: var(--sp-4); min-height: 44px; }
/* the Name row: the person tick is the label's SIBLING in a centred flex row, so it centred on label + field; it sits on the
   field's bottom line at the field's height instead — centred on the field (his #19: "align the checkboxes to be at the middle") */
body.libpage #app .lbody .rform > .credit-swap { align-self: flex-end; min-height: 40px; }
body.libpage #app .lbody .rform { column-gap: var(--sp-12); }
body.libpage #app .lbody .rform .credit-rows .credit-row { gap: var(--sp-12); }
body.libpage #app .lbody .rform :is(input[type=text], select) { box-sizing: border-box; }
body.libpage #app .lbody .rform .credit-rows .credit-row input[type=text] { width: var(--row-w); flex: 0 0 var(--row-w); }
body.libpage #app .lbody .rform .credit-swap > input { align-self: center; margin: 0; }
body.libpage #app .lbody .rform input[type=checkbox] { width: 22px; height: 22px; }
body.libpage #app .lbody .rform :is(.lineup-edit, .lineup-add, .lineup-del, .credit-rm) { min-width: 44px; min-height: 44px; padding: 0;
  background: var(--l2); font-family: var(--font-body); font-size: var(--fs-4); display: inline-grid; place-items: center; }
/* no line-up yet: "+ New line-up" is a FIELD of the Line-ups row (the width of the fields, text in the fields' face, left),
   not a boxed button — it read as the same level as Delete artist (his, 2026-09-24) */
body.libpage #app .lbody .rform .lineup-add.lnew { font-family: var(--font-body); font-size: var(--fs-3); letter-spacing: 0; text-transform: none;
  width: var(--row-w); max-width: 100%; justify-content: start; place-items: center start; padding: 0 var(--sp-12); background: var(--l2);
  color: var(--text-muted); border-bottom: 2px solid var(--text-faint); white-space: nowrap; }
body.libpage #app .lbody .rform .lineup-add.lnew:hover { background: var(--l3); color: var(--text); }
/* Delete artist is the editor's own last word: a rule and air above it, like the song panel's */
body.libpage #app .lbody .rform .lib-row:has(> .ldelbox) { border-top: 1px solid var(--rule); padding-top: var(--sp-16); margin-top: var(--sp-16); width: 100%; }
body.libpage #app .lbody .rform :is(.lineup-edit, .lineup-add, .lineup-del, .credit-rm):disabled { opacity: .4; cursor: default; }
body.libpage #app .lbody .rform .lineup-pick-row { display: flex; gap: var(--sp-8); }
/* the roster: the header sits where it will stick — no margin above it for the sticky header to jump over (his "the line
   moved after scroll"), and the list starts right under the strip (his "so much space between library and artist") */
body.libpage #app .lbody:has(> .libroster) { padding-top: 0; }
body.libpage #app .libroster { margin: 0; padding: 0; }
body.libpage #app .libroster > .rhead { margin: 0; }
/* the tag picker's list never scrolls sideways (his #22: "+ CREATE “…”" gave it a horizontal bar) */
body.libpage #app .checkfilter-rows { overflow-x: hidden; }
body.libpage #app .checkfilter-panel :is(.checkfilter-name, .checkfilter-create, button) { overflow-wrap: anywhere; white-space: normal; min-width: 0; }
/* the change log: which anime a song edit sits under, on the opened row (the row itself names what changed) */
body.libpage #app .log-anime { margin: 0 0 var(--sp-4); }
/* an EDITED field: a solid 2px ring in the warn colour (was a 35%-alpha 2px glow, too faint to see — his, 2026-09-24: "make
   the color brighter or make the border thicker"; the palette is closed, so the ring got solid and the fill stays) */
body.libpage #app .lib-edit :is(input, select, textarea).field-dirty { box-shadow: inset 0 0 0 2px var(--warn); border-bottom-color: var(--warn); }
/* the Deleted tab's column grips: the ledger's own resize line, on the header cell's right edge */
body.libpage #app .ldelrow .dcell { position: relative; min-width: 0; }
body.libpage #app .ldelrow .dgrip { position: absolute; top: calc(var(--sp-8) * -1); bottom: calc(var(--sp-8) * -1); right: calc(var(--sp-8) * -1); width: 12px;
  cursor: col-resize; touch-action: none; }
body.libpage #app .ldelrow .dgrip::after { content: ''; position: absolute; top: 2px; bottom: 2px; left: 5px; width: 2px; background: var(--rule); }
body.libpage #app .ldelrow .dgrip:hover::after { background: var(--text-muted); }
body.libpage #app .deleted-box { overflow-x: hidden; }
/* Type · No. · Cut: /add's tight side padding, so CUS and FULL clear their arrows in 76 / 84 */
body.libpage #app .ldetail .lib-edit.lib-edit .lib-triple :is(select, input[type=number]) { padding-left: var(--sp-8); padding-right: var(--sp-4); }
/* members: the game screen's artist box, not a pile of chips (his, 2026-09-24: "try removing the box of members and make it
   similar to what's in the middle artist box in the game screen") — one sunk box, a row per name, a thin dimmed line
   between rows. Readouts, so no chip shape: a box on these pages means a control. */
body.libpage #app .ldetail .memlist { display: flex; flex-direction: column; gap: 0; width: 100%; padding: 0; border-radius: var(--r);
  background: color-mix(in srgb, var(--bg) 70%, transparent); box-shadow: inset 0 1px 4px rgba(0, 0, 0, .45); }
body.libpage #app .ldetail .memlist .memchip { display: block; align-self: stretch; margin: 0; padding: var(--sp-8) var(--sp-12); border: 0; border-radius: var(--r); background: none;
  font-family: var(--font-names); font-weight: 500; font-size: var(--fs-3); color: var(--text); border-bottom: 1px solid rgba(255, 255, 255, .06); }
body.libpage #app .ldetail .memlist .memchip:last-child { border-bottom: 0; }
/* the near-match choices are BUTTONS, so they wear the button box (his, 2026-09-24: "should the use/add be a box instead?
   currently it feels like just a random text") */
body.libpage #app .ldetail .artist-note .artist-fix { margin-top: 0; background: var(--l3); box-shadow: inset 0 0 0 1px var(--rule); border: 0; border-radius: var(--r); min-height: 40px;
  padding: var(--sp-4) var(--sp-12) 0; color: var(--text); }
/* ⛔ THE LABEL CARRIES A TYPED ARTIST NAME, so it is never the display face (his, 2026-09-24: "the problem is that font makes
   everything capitalized" — Bebas has no lowercase, so YOASOBi and YOASOBI drew the same, which is the very difference the
   near-match is about). Mitr as typed; only the verb's first letter goes up. /library AND /add. */
#app .artist-note .artist-fix .artist-fix-label { font-family: var(--font-body); font-size: var(--fs-2); font-weight: 400;
  text-transform: none; letter-spacing: normal; }
#app .artist-note .artist-fix .artist-fix-label::first-letter { text-transform: uppercase; }
body.libpage #app .ldetail .artist-note .artist-fix:hover { background: var(--l3); box-shadow: inset 0 0 0 1px var(--text-muted); }
/* his "the use and add is too far from the hint" */
body.libpage #app .ldetail .artist-note .artist-choices { margin-top: var(--sp-4); gap: var(--sp-8); }
body.libpage #app .ldetail .artist-note.near { color: var(--warn); }
/* the Tags tab: the count sits under its header, and ✎ / 🗑 show on the row you point at (a column of 14 identical icons
   was the "weird far right" — his, 2026-09-24); keyboard focus shows them too */
/* name + its ✎/🗑 · COUNT at the far right (his "count isn't stay at the far right"; the actions sit right after the name) */
body.libpage #app .rrow-tag { grid-template-columns: minmax(200px, 1fr) 96px; }
body.libpage #app .rrow-tag > .rcell, body.libpage #app .rhead.rrow-tag > span:nth-child(2) { text-align: right; }
body.libpage #app .rrow-tag .rnamecell { display: flex; align-items: center; gap: var(--sp-16); min-width: 0; }
body.libpage #app .rrow-tag .rnamecell > .rname { min-width: 0; }
body.libpage #app .rrow.rrow-tag .ract { display: flex; gap: var(--sp-8); justify-content: flex-end; opacity: 0; }
body.libpage #app .rrow.rrow-tag:is(:hover, :focus-within) .ract { opacity: 1; }
/* EVERY TAB IS A FIXED FRAME (his rule: "every page shouldn't be scrollable"): Artists, Tags, Requests and Deleted
   used to scroll the window; now the strip stays put and only their body scrolls. The Library tab keeps its own
   pane chain (.lscroll). Same size threshold as the ledger's frame. */
@media (min-width: 1101px) and (min-height: 480px) {
  body.libpage { height: 100vh; overflow: hidden; }
  body.libpage #app { height: 100vh; height: 100dvh; display: flex; flex-direction: column; min-height: 0; }
  body.libpage #app > .lb.tmast, body.libpage #app > .lstrip { flex: 0 0 auto; }
  body.libpage:not(.libframe) #app .lbody { flex: 1 1 auto; min-height: 0; overflow: auto; padding-bottom: var(--sp-24); }
  body.libpage.saving #app { height: calc(100vh - 68px); }
}
/* /list's Switch for a friend = the entry's name box, at the entry's width (one shape) — full width it read as a search bar */
body.listpage #app .lwhobox { width: min(520px, 100%); }
body.listpage #app .lwhobox .enrow { display: flex; gap: var(--sp-8); align-items: stretch; margin: 0; }
/* the known-artist line (his, 2026-09-25: "try way to make it clearer … the number couldn't be seen at all"): the TEXT face at
   15px wherever it sits — inside a <label> it had been picking up the label face (uppercase, 12px, and ♪ drew as a speck) —
   the state in green, the count quiet */
#app .artist-note, #app .artist-note span { font-family: var(--font-body); text-transform: none; letter-spacing: normal; }
#app .artist-note.known { font-size: var(--fs-2); }
#app .artist-note.known .kc { color: var(--text-muted); }
/* end screen (his, 2026-09-25: "why isn't the ranking number here colored like the rank? and the owned song should be
   bigger"): a medal card's rank wears its medal colour with the name, and the list pair reads at 18, not 12 */
#app.gameflow .gs .pcard:is(.medal1, .medal2, .medal3) .prank { color: inherit; }
#app.gameflow .gs.endview .pcard .plp { font-size: var(--fs-3); margin-left: var(--sp-4); }
#app .rqr .rq-field input:disabled { color: var(--text-muted); -webkit-text-fill-color: var(--text-muted); opacity: 1; }

/* 2026-09-26 THE LINKING WORDS on a performer card (docs/superpowers/specs/2026-09-26-linking-words-design.md): the name
   the character · the join phrase printed after this artist. One row under the name, the panel's own field voice. */
.ldetail .credit-extras { display: grid; grid-template-columns: minmax(0, 1fr) 136px; gap: var(--sp-8); margin-top: var(--sp-8); align-items: center;
  grid-template-areas: 'character join';   /* the name column and the role column of the row above (136 = the role select, measured) */
  margin-right: calc(32px + var(--sp-8)); }   /* ends on the role select's right edge: the ✕ beside it is 32px + the row's 8px gap (measured) */
.ldetail .credit-extras[hidden] { display: none; }
.ldetail .credit-extras input[type=text] { color: var(--text); width: 100%; min-width: 0; box-sizing: border-box; }
.ldetail .credit-extras .char-field { grid-area: character; display: flex; align-items: center; gap: var(--sp-8); min-width: 0; }
.ldetail .credit-extras .char-field .credit-character { flex: 1 1 auto; }   /* the character, then ITS Person tick (2026-09-27) */
.ldetail .credit-extras .credit-join { grid-area: join; }

/* ── TEAMS (ADR-0009) — my design, uncommitted until his look ─────────────────────────────────────────────────────────
   The mark `T1` is chrome we write (the numbers face may draw it); it sits before the name on the lobby seat, the round
   card, the end card, the board's row (plain rows — no team grouping on the board, his 2026-10-01). A teammate's live answer on their card is plain answer text, muted — it is not yet the reveal. */
#app .teammark, #app .rqr .rq-tmark { display: inline-block; margin-right: var(--sp-8); padding: 0 var(--sp-4);
  font-family: var(--font-numbers); font-weight: 400; letter-spacing: var(--track-caps); line-height: 1.15;
  color: var(--text-muted); background: var(--l3); } /* his 2026-10-01: no border, a filled box like the others */
#app .lb .seat .teammark { font-size: inherit; line-height: 1.35; padding: 0 var(--sp-4); vertical-align: baseline; } /* the same box as your own seat's dropdown */
#app .lb .seat .teampick { margin-left: var(--sp-8); position: relative; display: inline-block; }
/* ⛔ appearance: none — his 2026-10-01 screenshot ("it still doesn't go away"): on his Chrome the NATIVE select paints its own
   grey frame whatever border we set, and a headless Chrome here never draws it, so every screenshot I took read clean. With
   the native control off, the box is only what this rule paints, and the caret is ours (the list picker's ▼, faint). */
#app .lb .seat .teampick select { appearance: none; -webkit-appearance: none; cursor: pointer; }
#app .lb .seat .teampick::after { content: '▼'; position: absolute; right: var(--sp-8); top: 50%; translate: 0 calc(-50% - 2px);
  font-family: var(--font-body); font-size: var(--fs-1); color: var(--text-faint); pointer-events: none; } /* his 2026-10-23: the ▼ sat 4px off the edge — 8px, the same as the text's left */
/* his 2026-10-01: - / 1 / 2 / 3 in the numbers face, the box the name's size (inherit = the name inside .who) */
#app .lb .seat .teampick select { font-family: var(--font-numbers); font-weight: 400; font-size: inherit; line-height: 1; background: var(--l3); color: var(--text);
  border: 0; padding: 0 calc(var(--sp-24) + var(--sp-4)) 0 var(--sp-8); height: 1.35em; vertical-align: baseline; translate: 0 -2px; } /* measured 2px low against the name's centre */
/* his 2026-10-01 ("it still has boarder"): the ring after a click was Chrome's FOCUS OUTLINE, not a border — gone, and focus
   reads as the accent underline the other lobby selects use (#app .lb .panel select:focus) */
#app .lb .seat .teampick select:focus { outline: none; box-shadow: inset 0 -2px 0 var(--accent); }
#app .rqr .rq-ans.mate .rq-ln { color: var(--text-muted); }
/* MULTIPLE CHOICE (his 2026-10-01 "build it"; my design, uncommitted until his look): the four answers fill the artist box
   while guessing, one row each in the box's own row shape. An anime title is a NAME (Kanit). The pick = INK ("where you
   are"): a light fill under a 2px text rule. Rows split the box's height equally so four always fit without a scroll. */
#app .rqr .rq-choices .rq-alist { display: grid; grid-template-rows: repeat(4, 1fr); height: 100%; }
#app .rqr .rq-choice { all: unset; box-sizing: border-box; display: flex; align-items: center; width: 100%; cursor: pointer;
  padding: 0 var(--sp-16); font-family: var(--font-names); font-weight: 500; font-size: var(--fs-5); color: var(--text);
  border-bottom: 1px solid var(--rule); }
#app .rqr .rq-choice .nm { min-width: 0; overflow-wrap: anywhere; }
#app .rqr .rq-choice:hover { background: var(--l2); }
#app .rqr .rq-choice.on { background: var(--l3); box-shadow: inset 0 -2px 0 var(--text); }
#app .rqr .rq-choice:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* ════════════════════════════════════════════════════════════════════════════════════════════════
   THE PHONE (his real-site pass, 2026-09-25: "no need to adjust ui much. just make them not stuck").
   Three narrow-screen rules, and NOTHING here reaches a window wider than 700px — the PC and all nine
   snap layouts stay exactly as they were (a before/after rect fingerprint of every element at 1920×1080,
   1280×800 and 960×1080 is identical). ⚠ It must stay the LAST block: a media query adds no specificity,
   so each rule below wins only by coming later than the one it overrides.
   ⚠ His iPhone X shows ~375×635 in Safari, so the round's "narrow and tall" block (min-height: 700px)
   never applied there and the phone got the 5-column PC grid — the reason the cards were a sliver. The
   round below therefore restates everything it needs instead of leaning on that block.
   phone-check.py (the `phone-*` entries of the browser suite) proves each rule at 375×635 and 375×450.
   ════════════════════════════════════════════════════════════════════════════════════════════════ */
@media (max-width: 700px) {
  /* 1 · THE PAGE SCROLLS. The fixed frame is a PC rule; on a phone the /list strip alone is taller than
     half the screen, and the frame left the rows 0 tall under it. */
  html:has(body.listpage), html:has(#app.addflow), html:has(#app.entry), html:has(#app .rqr) { overflow: visible; }
  body.listpage, body:has(#app.addflow), body:has(#app .rqr) { overflow: visible; }
  body.listpage #app { height: auto; min-height: 100dvh; }
  body.listpage #app .lbody { flex: none; }
  body.listpage #app .lrows { flex: none; overflow: visible; }
  #app.addflow { height: auto; min-height: 100dvh; }
  #app.addflow #addform { flex: none; overflow: visible; }

  /* 2 · EVERY FLOATING WINDOW FITS THE SCREEN, ITS × ALWAYS ON IT. `100vh` is the page with Safari's bars
     hidden, so a box sized by it and centred in the real (shorter) screen put its own top — and the × —
     above the glass; `dvh` is the screen as it stands. */
  #app .lb .panel { top: 0; left: 0; transform: none; box-sizing: border-box; width: 100%; height: 100dvh; max-height: none; padding-top: 0; }
  #app .lb .panel.confirm { top: 50%; left: 50%; transform: translate(-50%, -50%); box-sizing: border-box; width: calc(100% - 32px); height: auto; max-height: calc(100dvh - 32px); padding-top: var(--sp-24); }
  #app .lb .panel:not(.confirm) .panelhead { margin: 0 0 var(--sp-16); padding-top: var(--sp-16); }
  /* no × (his: Close is the one way out), so the foot with Close is pinned to the bottom of the screen */
  #app .lb .panel:not(.confirm) { display: flex; flex-direction: column; padding-bottom: 0; }
  #app .lb .panel:not(.confirm) .setfoot { position: sticky; bottom: 0; z-index: 2; background: var(--l2); margin: auto calc(-1 * var(--sp-24)) 0; padding: var(--sp-16) var(--sp-24); }
  /* the history window: the whole screen, no drag, no corner resize (the inline geometry is a PC thing) */
  #app .rq-win { left: 0 !important; top: 0 !important; width: 100% !important; height: 100dvh !important;
                 min-width: 0; min-height: 0; max-width: none; max-height: none; resize: none; border: 0; }
  #app .rq-win .rq-dh { cursor: default; }
  #app .rq-win .rq-hscroll { overflow: auto; }
  #app .rq-win .rq-htable { min-width: 640px; }
  /* the drawers: the screen's height as it stands, and the head (with its ×) pinned while the body scrolls */
  .drawer { height: 100dvh; }
  #app .drawer.rqd .set-head { position: sticky; top: calc(-1 * (var(--sp-16) + 4px)); z-index: 2; background: var(--l2);
                               margin: calc(-1 * (var(--sp-16) + 4px)) calc(-1 * (var(--sp-16) + 6px)) 0; padding: calc(var(--sp-16) + 4px) calc(var(--sp-16) + 6px) var(--sp-8); }
  .rdlg { max-height: 100%; overflow-y: auto; box-sizing: border-box; }

  /* 3 · THE ROUND IS ONE COLUMN: title, song, the artist box under the song (his, 2026-09-25: "move artist to below
     song name"), the answer row, the players, then the rest. The artist box sizes to what it holds instead of filling
     the middle — at rest it is one `?` row — and scrolls inside past 3 rows, so naming performers never shoves the
     answer box down under the keyboard while you type. */
  #app .rqr {
    --cw: calc((100vw - 2 * var(--side) - var(--cg)) / 2);
    --card-h: min(168px, max(96px, 14dvh), calc(var(--cw) * 0.35 + var(--np-h)));
    --plate-h: 64px;
    position: relative; inset: auto; min-height: 100dvh; padding-bottom: var(--sp-24);
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: none; grid-auto-rows: auto;
    grid-template-areas:
      "acts   mine"
      "plate  plate"
      "song   song"
      "timer  timer"
      "abox   abox"
      "answer answer"
      "vote   vote"
      "cards  cards"
      "extra  extra"
      "stand  stand";
  }
  #app .rqr .rq-acts, #app .rqr .rq-mine { margin-top: 0; }
  #app .rqr .rq-plateblock { width: 100%; margin-top: var(--sp-8); }
  #app .rqr .rq-also { left: auto; right: 0; top: calc(100% + 6px); width: min(300px, calc(100vw - 32px)); }
  #app .rqr .rq-song { display: contents; }
  #app .rqr .rq-songname { grid-area: song; margin-top: var(--sp-8); }
  #app .rqr .rq-timer { grid-area: timer; margin: 0; }
  #app .rqr .rq-answer { width: 100%; }
  #app .rqr .rq-vote { margin-top: var(--sp-8); }
  #app .rqr .rq-cards { margin-top: var(--sp-16); }
  #app .rqr .rq-crow { row-gap: var(--sp-12); }
  #app .rqr .rq-abox { grid-area: abox; flex: none; margin-top: var(--sp-8); }
  #app .rqr .rq-alist { flex: none; max-height: 156px; }   /* 3 rows of 52 */
  #app .rqr .rq-answer { margin-top: var(--sp-8); }
  #app .rqr .rq-extra { margin-top: var(--sp-16); }
  #app .rqr .rq-extra { grid-area: extra; background: var(--q1); margin-top: var(--sp-8); padding: calc(var(--sp-12) + 1px) var(--sp-16) var(--sp-8);
                        display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); column-gap: var(--sp-16); align-content: start; }
  #app .rqr .rq-extra > :nth-child(1) { grid-column: 1; grid-row: 1; }
  #app .rqr .rq-extra > :nth-child(2) { grid-column: 1; grid-row: 2; margin-bottom: var(--sp-8); }
  #app .rqr .rq-extra > .rq-from { grid-column: 2; grid-row: 1 / 3; margin: 0; padding: 0; border: 0; }
  #app .rqr .rq-extra > :nth-child(3) { grid-column: 1 / -1; grid-row: 3; }
  #app .rqr .rq-extra .rq-credit2 { grid-column: 1 / -1; grid-row: 4; max-height: none; overflow: visible; flex: none; }
  #app .rqr .rq-stand { margin-top: var(--sp-16); overflow: visible; }
  #app .rqr .rq-srow { --rowh: 44px; --rowfs: 18px; height: 44px; }
  body:has(#app .rqr) .audiohint { max-width: calc(100vw - 20px); }
  /* the volume rides the top row and scrolls away with it — fixed, it sat over the count tab once scrolled */
  body:has(#app .rqr) #volume { position: absolute; }
  #app .rqr .rq-volslot { width: 126px; }
  /* the tool pages' top row on the lobby's pixels on a phone too: the lobby is #app's 8 + .lb's 16 there, the tool
     masthead kept its PC 32 and sat 12px lower (his, 2026-09-25) */
  #app .lb.tmast { padding-top: calc(var(--sp-8) + var(--sp-16)); }

  /* /list's song rows (his, 2026-09-25: the names letter-wrapped — the PC's seven fixed tracks left the name 0px wide):
     tick · number · ▶ · the name over its artist · the flags over the time */
  body.listpage #app .srow.song { grid-template-columns: 28px 48px 36px minmax(0, 1fr) auto; row-gap: var(--sp-4); }
  body.listpage #app .srow.song > :is(input.pick, .num, .sbtn) { grid-row: 1 / 3; }
  body.listpage #app .srow.song .t { grid-column: 4; grid-row: 1; }
  body.listpage #app .srow.song .sub { grid-column: 4; grid-row: 2; }
  body.listpage #app .srow.song .kflag { grid-column: 5; grid-row: 1; justify-self: end; }
  body.listpage #app .srow.song .stime { grid-column: 5; grid-row: 2; justify-self: end; }
}
/* his 2026-10-23: /mashup joined the row as a 4th word, and at 375px ADD LIBRARY LIST MASHUP ran the row 38px past the screen
   (PH1/PL1/PA1). A phone gets 16px either side of each word instead of 24 — the lobby and every tool page alike, so PL6 holds. */
@media (max-width: 430px) {
  #app .lb .navb a, #app .lb.tmast .navb.pagenav a { padding-left: var(--sp-16); padding-right: var(--sp-16); }
}

/* /add "did you mean": the list's first row says what the rows are; it is a label, not a choice */
.dropdown.similar .similar-head { font-family: var(--font-display); letter-spacing: var(--track-caps); color: var(--text-muted); cursor: default; }
.dropdown.similar .similar-head:hover { background: none; }
/* the rows are typed names (song, anime, artist): the text face in their own case. The list sits inside the Title <label>,
   whose display face would otherwise uppercase them */
.dropdown.similar li { font-family: var(--font-body); text-transform: none; letter-spacing: normal; font-size: var(--fs-2); }
.dropdown.similar li .muted { font-size: var(--fs-1); }
.dropdown.similar li.similar-head { font-family: var(--font-display); letter-spacing: var(--track-caps); }
