/**
 * party-ui.css — the single-source stylesheet for the party surfaces:
 * the extension's host console (console.html) and the web remote
 * (relay/public/index.html, guests' phones).
 *
 * Canonical copy lives in extension/ because the extension cannot load files
 * from outside its own package; the relay serves this same file to the web
 * page (SHARED_FILES in relay/server.js) and the packaged relay embeds it
 * (relay/build.mjs). Do not fork styles per surface — page-specific chrome
 * belongs in that page's own <style>, and there should be almost none.
 *
 * The tokens, the reset and the QR tile moved to ui-shared.css, which the
 * popup loads too — see the note at the top of that file for why they travel
 * by @import rather than by a second <link> on each page.
 */
@import url('ui-shared.css');

body {
  background: var(--bg); color: var(--text); min-height: 100vh;
  font: 16px/1.5 system-ui, sans-serif; padding: 20px; max-width: 480px; margin: 0 auto;
}
h1 { font-size: 18px; }
.sub { color: var(--dim); font-size: 13px; }
/* A card note describing something that has not happened yet. Brighter than
   `.sub`'s dim, because it is the answer to a click the host just made and
   must not read as the same static help text it replaces. */
.sub.pending { color: var(--accent-2); }
/* Links inside a hint line. Default UA blue/purple is unreadable on the dark
   card and marks the link as visited after one tap; the accent keeps it
   legible and the same colour every party. Vertical padding buys a bigger
   touch target on a phone without changing the line box. */
.sub a { color: var(--accent-2); text-decoration: underline; text-underline-offset: 2px; padding: 6px 0; }
.sub a:hover { color: var(--text); }
.sub a:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; border-radius: 4px; }
.card { background: var(--card); border-radius: 14px; padding: 16px; margin-top: 14px; }
label { color: var(--dim); font-size: 13px; display: block; margin-bottom: 10px; }
.val { color: var(--text); float: right; font-variant-numeric: tabular-nums; }
.row { display: flex; align-items: center; justify-content: space-between; }
/* !important: this utility must always win over a component's own
   `display` rule (e.g. .nowplaying sets display:flex) regardless of
   which is declared later in the stylesheet. */
.hidden { display: none !important; }
.anchors { display: flex; justify-content: space-between; font-size: 12px; color: var(--dim); margin-top: 6px; }

/* --- segmented tiers / preset chips --------------------------------------- */
.seg { display: flex; gap: 6px; }
.seg button {
  flex: 1; padding: 12px 0; border: none; border-radius: 9px; background: #3a3748;
  color: var(--text); font-size: 14px; cursor: pointer;
}
.seg button.on { background: var(--accent); font-weight: 600; }
.seg button:disabled { opacity: .35; cursor: default; }
/* Quality auto-reduced because the GPU couldn't keep up — the active tier
   turns red so a lowered tier reads as forced, not chosen. The popup has had
   this since the tier could move on its own; the host console renders the same
   card now (host-cards.js) and was setting the class with nothing to draw. */
.seg.degraded button.on { background: var(--danger); }
/* ASKED FOR, NOT YET IN FORCE. The fill says what the room is getting; a box
   says what is queued behind it. Two settings defer by design — a quality
   RAISE waits for the next song, a precision change for the next session — and
   while the request was the thing that lit, both could claim a state the room
   was not in.

   Box rather than fill, in the same accent: the choice is registered, and the
   missing fill is the whole message. Danger for the one that is never coming,
   so the two are told apart without reading the sub-line. Neither can collide
   with `.on` — a value in force is not outstanding. */
.seg button.want { box-shadow: inset 0 0 0 2px var(--accent); }
.seg button.refused { box-shadow: inset 0 0 0 2px var(--danger); }
/* Lead vocal volume. A RAMP, like Master Volume's blocks: each preset's
   height carries how much of the original singer it leaves, so the row draws
   the level it sets. Bottom-aligned, so they sit on a common floor and the
   tops draw the slope; the heights themselves are inline, computed from the
   level (GUIDE_H_MIN/MAX in party-ui.js) rather than written per button.

   The vertical padding goes with it — the height is stated now, so padding
   would fight it — and the label centres inside whatever height it gets. */
/* No bottom margin: the card's own 16px padding is what every other card
   closes with. The 12px here dated from when the presets sat above
   something else, and all it did was make this one card deeper at the foot
   than its neighbours. The meaning line below the row (issue #24) carries
   its own top margin instead. */
.presets { align-items: flex-end; }
/* The endpoint meaning line ("Sing solo" / "Sing along with the original
   singer", issue #24). min-height RESERVES the line for the middle presets,
   which print nothing — a collapsed line would bounce every card below
   this one as a guest cycles the presets. Same voice as .anchors: the
   quiet caption register of this surface. */
.guidemeaning {
  min-height: 15px; font-size: 12px; color: var(--dim);
  text-align: center; margin-top: 6px;
}
.presets button {
  font-size: 12.5px; padding: 0 4px; line-height: 1.15;
  display: flex; align-items: center; justify-content: center; text-align: center;
}
/* The reading joins the name instead of floating to the far edge. Everything
   else on this surface says "<name> <value>" as one centred phrase — the
   sandwich cards do it in their stack, and this card has no strip to hang a
   right-aligned figure over. */
/* AND IT SITS IN THE NOTCH, not on a line of its own.
   The presets are bottom-aligned, so the shorter three leave a wedge of empty
   card above them and only the LAST one — the tallest, at full level —
   reaches the top. The label drops into that wedge: a negative margin, which
   pulls the row up under it, so the card loses a whole line of height without
   any button changing size.

   -10px is where it stops, and the number is a COLLISION, not a taste: the
   name is centred, so it clears the first and last buttons either side and
   the first thing beneath it is the third. It lands a few pixels above that
   top edge, and dropping further would put the words on the button.

   THE CLEARANCE IS WHY THE FIGURE WENT. Carrying a per cent beside the name
   made the line wide enough to reach the last button — which, being the tall
   one, is the only one the label cannot pass over — and it did so below about
   340px, so the narrowest phones printed the reading on top of a button. The
   name alone is short enough to sit in the notch at every width the surface
   supports. The level is not lost: the lit preset names it and the ramp draws
   it.

   Written by POSITION rather than by button name, because the names are
   wording and this is geometry: it held for "Solo / A little help / Duet /
   Original" and holds for "Off / Low / Medium / Full".

   Note the box, unlike the text, is full width and does overlap that last
   button — harmless, since a label paints no background. */
#guideCard > label { text-align: center; margin-bottom: -10px; }

/* --- steppers -------------------------------------------------------------- */
/* Every stepper is now label-and-reading on one line, strip below, with the
   buttons at the outer edges — see .steprow. The old .tonerow/.tval layout
   (value centred between its two buttons) has no users left. */
.pbtn {
  width: 54px; height: 54px; border-radius: 14px; border: none; background: #3a3748;
  color: var(--text); font-size: 26px; cursor: pointer; flex: none;
  display: flex; align-items: center; justify-content: center; line-height: 1;
}
.pbtn:disabled { opacity: .35; cursor: default; }
/* Drawn glyphs on a stepper button (the peek clock arrows) — sized here
   because an SVG has no font-size to inherit. */
.pbtn svg { width: 22px; height: 22px; }
/* ♭/♮/♯ glyphs sit low within their own font metrics even when the button
   box is centered — nudge these up a touch. The nudge is a share of the
   button, not a fixed 8px for all three: ♮ is a 28px centre mark now and 8px
   inside it would push the glyph into the top edge. */
#pitchDown, #pitchUp { padding-bottom: 8px; }
#pitchNatural { padding-bottom: 4px; }
/* ♮ acknowledges the press by flashing, since returning to natural otherwise
   empties the strip — or, from natural, changes nothing at all. Cool: it is a
   return to neutral, not a move above it. Colour lands instantly (no
   transition while .flash is on) and fades back once the class is dropped. */
#pitchNatural { transition: background .35s ease-out, color .35s ease-out; }
/* At the original key ♮ takes the accent, the same "you have not moved this"
   the picture-priority floors and the marquee reading carry. Same idiom as
   any other lit control, so it is `.on` rather than a rule of its own. */
#pitchNatural.on { background: var(--accent); }
/* AFTER `.on`, and it has to stay there: the two selectors weigh the same, so
   the later one wins, and the flash must beat the lit state or pressing ♮
   while already at the original key would acknowledge nothing. */
#pitchNatural.flash {
  background: var(--accent-2); color: #fff; transition: none;
}
.steprow { display: flex; align-items: center; gap: 10px; }
/* --- the sandwich: two buttons around a two-line stack --------------------- */
/* Peek, Key, Bass and Master Volume were three lines each — label, then a row
   of [− strip +] — and four of them in a column is most of a phone screen
   before the host cards even start. The buttons now SANDWICH a stack whose
   first line is the label and reading and whose second is the strip, which
   costs a card one line and costs the controls nothing: the strip keeps its
   full width, and the buttons keep their size.
   `flex-end` is what makes it read as one control rather than a label with a
   row under it — the strip's baseline and the buttons' bottom edge are the
   same line, so the eye takes the three as one object. */
.steprow.sandwich { align-items: flex-end; }
.sandwich .stack { flex: 1; min-width: 0; }
/* A CONTROL THAT CANNOT BE USED IS DRAWN THAT WAY WHOLE (issue #250). A
   disabled button dims itself (`.pbtn:disabled`), but the strip between two of
   them is a plain element and never did — so while a session was starting,
   the Transition card's lit 15 s block and the Lyrics timing card's accented
   centre dash stood at full colour between two greyed buttons. When no button
   in the row can be pressed, the stack — label, reading and strip — dims to
   the same .35. Keyed on "no enabled button" rather than "a disabled one", so
   a stepper that one day disables only at the end of its range does not grey
   a strip that is merely at its maximum. */
.steprow.sandwich:not(:has(.pbtn:not(:disabled))) .stack { opacity: .35; }
/* Tighter than the standalone label's 10px: the strip below it belongs to it
   now, and the old gap was sized to separate two things rather than join them.

   CENTRED, WITH THE READING BESIDE THE NAME. On a full-width label a floated
   value sits at the far right, which was the right answer when the label owned
   its own line — the eye had the whole width to sweep and the two ends framed
   it. Inside the stack it is wrong twice over: the value ends up above the far
   end of the strip, pointing at a bar it has nothing to do with, and the pair
   reads as two separate captions rather than one reading of one control.
   Together and centred, "Bass −4 dB" is a single phrase sitting over the thing
   it describes. */
.sandwich .stack > label { margin-bottom: 6px; text-align: center; }
/* The reading is never split: "2.0s" over "late" is a number without its
   meaning. On the narrowest phones "Lyrics timing 2.0s late" does not fit
   beside the buttons (issue #248, measured at 320 px), and there the whole
   reading moves under the name instead — "Lyrics timing" / "2.0s late". */
.sandwich .stack > label .val { float: none; margin-left: 5px; white-space: nowrap; }
/* The strip line inside the stack. Centred, not end-aligned: every child is
   either the same height as the bars (the blocks, Key's ♮) or a mark that has
   to sit level with their middle (Bass and Peek's dash). */
.bars { display: flex; align-items: center; gap: 10px; }
.steprow.fine .bars { gap: 8px; }
.blocks { flex: 1; display: flex; align-items: center; gap: 4px; }
.blocks .block { flex: 1; height: 28px; border-radius: 4px; background: #3a3748; }
.blocks .block.on { background: var(--accent); }
/* Above 100% is boost — warm-coloured so being over unity is visible at a
   glance rather than implied by the number. The dash at unity is what
   separates the two runs; it replaced a margin that did the same job. */
.blocks .block.boost.on { background: var(--warn); }
/* The marquee strip on the Transition announcement card: 15, 20, 25 and 30 s,
   each block as wide as the time it adds (uiPolicy.TRANSITION, issue #249;
   the widths are inline, from the table), none lit meaning no marquee. */
.blocks.warm .block.on { background: var(--warn); }
/* THE FIRST TWO BLOCKS ARE ACCENT, THE LAST TWO WARM.
   The strip used to be warm end to end, on the reasoning that all of it is
   time spent ON TOP of the song. True, but it made the longest setting and
   the shortest look like the same kind of choice. Fifteen and twenty are the
   ordinary range — fifteen is the shipped default and sits in the accent the
   reading beside it uses — while twenty-five and thirty keep the warning
   colour, because that is an announcement long enough to be felt between
   every pair of songs. Same idiom as Master Volume, where warm begins where
   boost does. */
#trBlocks .block:nth-child(-n+2).on { background: var(--accent); }
/* Master Volume only: blocks carry their step's loudness in their height, so
   they sit on a common floor and their tops draw the slope. Heights are set
   per block in the markup (see VOL_BLOCKS) — an inline height beats the flat
   28px above, which the other strips keep. */
.blocks.ramp { align-items: flex-end; }
/* Two-sided strips (Key, Bass): which side is lit is the sign, so the sides
   are coloured apart. Warm above neutral matches the volume card's boost
   blocks; cool below it. Unlit bars stay the same grey on both sides, so at
   rest the control reads as one strip rather than two things. */
.blocks.minus .block.on { background: var(--accent-2); }
.blocks.plus .block.on { background: var(--warn); }
/* The centre a two-sided strip is measured from — the ♮ button on Key, a
   dash on Bass and Peek. Always drawn: it marks where neutral *is*, so the
   bars either side can be read against it, rather than reporting a state the
   bars already show. Master Volume has none; its ramp is one continuous
   slope, and a marker cut through it. */
.blocks.minus + .blocks.plus { margin-left: 7px; }
/* Bass is asymmetric — four bars down, five up — and the two strips split the
   row by bar count so every bar keeps the same width. The weights are inline
   now, from BASS_CUT_BARS/BASS_BOOST_BARS, because they ARE the bar counts:
   written here they were a second copy of the range that had to be remembered
   whenever it moved. Key stays symmetric and needs no weighting. */
.centermark {
  flex: none; width: 14px; text-align: center; color: var(--dim);
  font-size: 18px; line-height: 1;
}
/* At the centre the dash is not just the origin the bars are measured from,
   it IS the value in force — and it is the only thing left saying so, since
   the strip is empty and the reading blank there. So it takes the accent the
   way every other current setting does (Key's ♮, the picture floors, the
   marquee's ten seconds), and drops back to the dim once the value moves. */
.centermark.on { color: var(--accent); }
/* Five controls in one row needs smaller buttons than the two-button rows,
   or the bars are squeezed to slivers on a phone. */
/* Fourteen bars in one row: tighten the gaps rather than let each bar thin to
   a hairline on a phone. */
.steprow.fine { gap: 8px; }
/* Picture priority: each preference button shares its row with the floor it
   protects (Sharpness · min res, Smoothness · min fps). The seg wrapper keeps
   the segmented-button look; flex lets the button fill the row's left so the
   two steppers align down the right edge. */
#viewPolicyCard .steprow .seg { flex: 1; min-width: 0; }
#viewPolicyCard .steprow + .steprow { margin-top: 10px; }
/* The keys-row button size: the full 54px steppers plus "Smoothness" do not
   fit a phone's 375px side by side. */
#viewPolicyCard .steprow .pbtn { width: 44px; height: 44px; font-size: 22px; border-radius: 12px; }
/* Sized to the widest reading ("720p") so the two rows' buttons and steppers
   stay aligned, and stepping a value never reflows its row. Accent-coloured
   because these two readings are the setting itself — every other card on this
   surface says its value in lit blocks, and these have none to light. */
#viewPolicyCard .steprow .val { min-width: 48px; text-align: center; }
#viewPolicyCard .steprow .floorlbl { min-width: 52px; text-align: right; }
/* The coming-next card's toggle, on the Transition announcement card. A square
   stepper-sized button that wears the MASCOT when it is on — what it switches
   on is the mascot's card, so the picture is the state, and the word is only
   what it has to say when the picture is absent. Sized like the picture
   card's buttons so a phone fits button + stepper + four blocks in one row. */
#trCard { width: 44px; height: 44px; border-radius: 12px; padding: 0; overflow: hidden; }
#trCard .cardmascot { display: none; width: 100%; height: 100%; object-fit: cover; }
#trCard .cardword { font-size: 12px; }
#trCard.on { background: var(--accent); }
/* The marquee's duration, beside the word it labels. Its colour is set by
   paintTransition, not here: the accent marks the SHIPPED ten seconds only,
   the same rule the picture-priority floors follow. */
#trCard.on .cardmascot { display: block; }
#trCard.on .cardword { display: none; }
#transitionCard .steprow .pbtn { width: 44px; height: 44px; font-size: 22px; border-radius: 12px; }
.steprow.fine .blocks { gap: 3px; }
/* Floor labels on the picture-priority card: quiet captions between the
   steppers, sized like the .anchors rail text. */
.floorlbl { font-size: 12px; color: var(--dim); white-space: nowrap; }
.steprow.keys { gap: 7px; }
/* ♭ and ♯ are FULL SIZE, like every other stepper on the surface. They were
   44px because this row once carried five controls side by side and the bars
   would have been slivers; the sandwich moved ♮ and both strips down into the
   stack, so the row is two buttons again and there is nothing left to shrink
   for. A control that does the same job as its neighbours and is smaller than
   them reads as a different, lesser control. */
.steprow.keys .blocks { gap: 3px; }
/* ♮ is the centre MARK, not a stepper — Bass and Peek draw a dash in this
   spot. Sized to the bars it sits among so the strip stays one line, and wide
   enough to stay a target. */
.natbtn { width: 44px; height: 28px; font-size: 17px; border-radius: 7px; }

/* --- toggles ---------------------------------------------------------------- */
.toggle {
  position: relative; width: 56px; height: 32px; border-radius: 16px; background: #3a3748;
  border: none; cursor: pointer; flex: none;
}
.toggle::after {
  content: ""; position: absolute; top: 4px; left: 4px; width: 24px; height: 24px;
  border-radius: 50%; background: #fff; transition: transform .15s;
}
.toggle.on { background: var(--accent); }
.toggle.on::after { transform: translateX(24px); }
.toggle:disabled { opacity: .35; cursor: default; }

/* --- status / big buttons --------------------------------------------------- */
.status { margin-top: 14px; font-size: 13px; color: var(--dim); text-align: center; }
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--danger); margin-right: 6px; vertical-align: 1px; }
.dot.ok { background: var(--ok); }
.wide {
  width: 100%; padding: 14px 0; border: none; border-radius: 10px; background: var(--accent);
  color: #fff; font-size: 16px; font-weight: 600; cursor: pointer;
  display: flex; align-items: center; justify-content: center; gap: 8px;
}
/* The glyph is drawn, so it needs sizing up to sit level with 16px text —
   the 13px default is tuned for the 30px round buttons. */
.wide svg { width: 15px; height: 15px; }
button.wide:disabled { opacity: .35; cursor: default; }
.stopbtn { background: #3a3748 !important; color: var(--danger) !important; margin-top: 14px; }

/* --- pairing (web remote splash) -------------------------------------------- */
#pair { text-align: center; }
#pair input {
  width: 220px; text-align: center; font: 600 16px/1.4 system-ui;
  background: #17161d; border: 1px solid #3a3748; color: var(--text);
  border-radius: 10px; padding: 10px; margin: 12px 0;
}
#pair button {
  width: 100%; padding: 14px 0; border: none; border-radius: 10px; background: var(--accent);
  color: #fff; font-size: 16px; font-weight: 600; cursor: pointer;
}

/* The QR tile lived here. It is in ui-shared.css now, because the popup draws
   the join QR too and a second copy of the rules is what host-cards.js warned
   against. */

/* --- DJ code (host reveal + guest claim) ------------------------------------- */
/* Quiet by default on both surfaces: this is a once-a-night control sitting
   under the QR, and it must not read as a primary action next to Stop. */
.ghost {
  width: 100%; padding: 11px 0; border: 1px solid #3a3748; border-radius: 10px;
  background: transparent; color: var(--dim); font-size: 13px; cursor: pointer;
  margin-top: 12px;
}
.ghost:hover:not(:disabled) { color: var(--text); border-color: var(--accent); }
.card > .ghost:first-child { margin-top: 0; } /* card padding already spaces it */
.ghost:disabled { opacity: .4; cursor: default; }
/* Sized to be read across a room, since it is read across a room. */
#djNum {
  font-size: 52px; font-weight: 700; line-height: 1.15; color: var(--accent-2);
  letter-spacing: 2px;
}
/* Guest picker: deliberately as large as the transport controls — this is
   tapped once, in the dark, by someone who is not looking carefully. */
#djPick button { padding: 18px 0; font-size: 22px; font-weight: 700; }
#djPick button.wrong { background: #3a3748; color: var(--danger); }
.djnote { margin-top: 10px; text-align: center; }
.djnote.bad { color: var(--danger); }
.djnote.good { color: var(--ok); }

/* --- sing next / queue ------------------------------------------------------- */
.addrow { display: flex; gap: 8px; }
.addrow input {
  flex: 1; background: #17161d; border: 1px solid #3a3748; color: var(--text);
  border-radius: 9px; padding: 12px; font-size: 14px; min-width: 0;
}
.addrow button {
  padding: 0 20px; border: none; border-radius: 9px; background: var(--accent);
  color: #fff; font-size: 15px; font-weight: 600; cursor: pointer;
}
.cardlabel { display: flex; align-items: center; gap: 10px; }
.cardlabel input {
  width: 110px; min-width: 0; background: #17161d; border: 1px solid #3a3748; color: var(--text);
  border-radius: 8px; padding: 6px 10px; font-size: 13px; text-transform: none; letter-spacing: normal;
}
.nowplaying {
  background: var(--card); border-radius: 14px; padding: 16px; margin-top: 14px;
  border-left: 3px solid var(--ok);
}
/* Status label left, transport controls right-aligned, title below. */
.nowplaying .nphead { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
/* Three names won't fit beside the transport buttons on a phone, so the line
   clips rather than wrapping the row to two lines or squeezing the controls.
   The "NOW SINGING · " prefix survives and the names are what get cut, which
   is the right way round — the label says what the row is. */
.nowplaying .nphead .s {
  flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nowplaying .t { font-size: 14px; }
/* Title left, fill-the-screen bottom-right — the corner a video player puts
   it in. flex-end keeps the button in that corner rather than floating beside
   the first line when the title wraps. */
.nowplaying .npfoot { display: flex; align-items: flex-end; gap: 8px; }
.nowplaying .npfoot .t { flex: 1; min-width: 0; }
/* Wider than the round transport buttons because it carries a word: an ad is
   the one thing on this card people actively want gone, and a bare glyph
   would leave them guessing. Warm, matching the AD label above it. */
.nowplaying button.skipad {
  width: auto; height: 30px; padding: 0 12px; gap: 6px; font-size: 12px;
  background: var(--warn); color: #2b1d05; font-weight: 600;
}
.nowplaying button.skipad svg { width: 12px; height: 12px; }
/* Captions toggle. The lit state must be readable from a phone at arm's
   length, so it takes the accent fill rather than a subtle outline. */
.nowplaying button.ccon { background: var(--accent); color: #fff; }
/* Row buttons — transport on the Now singing card, move/remove on a queue
   entry. One rule for both: they sit on near-identical rows a few pixels
   apart on screen, and as two rules they had already drifted to 28px vs 30px
   with only one of them centring its glyph. The controls with their own
   sizing (the mic, the claim yes/no, the playlist peek) all outrank this on
   specificity and are unaffected. */
.nowplaying button,
.qitem button {
  width: 30px; height: 30px; border: none; border-radius: 7px; background: #3a3748;
  color: var(--text); cursor: pointer; flex: none; font-size: 13px;
  display: flex; align-items: center; justify-content: center; line-height: 1;
}
.queuelist { display: flex; flex-direction: column; gap: 6px; }
.qitem {
  display: flex; align-items: center; gap: 8px; background: #17161d;
  border-radius: 9px; padding: 8px 10px;
}
.qitem .info { flex: 1; min-width: 0; }
/* Two lines, then ellipsis: YouTube titles carry the distinguishing part late
   ("... (Official Video)", "... Karaoke Version"), so one line clipped the
   very thing that tells two entries apart. */
.qitem .t {
  font-size: 13.5px; line-height: 1.35;
  display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden;
}
/* Flex, so the 22px mic button and the 11.5px names centre against each other
   — on a text baseline a button that tall can only be nudged with
   vertical-align, which never truly centres and drifts with the font.
   margin-top clears the button from the title above it. */
.qitem .s {
  font-size: 11.5px; color: var(--accent-2); margin-top: 2px;
  display: flex; align-items: center; gap: 6px;
}
/* Names shrink and ellipsise rather than pushing the mic out of the row:
   three names on a narrow phone is the case that would otherwise overflow. */
.s .who { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The mic. Filled with the accent and a white glyph so it reads as the same
   kind of thing as every other action in the UI — a transparent one sat in
   the row looking like punctuation. The non-interactive variant drops the
   fill entirely rather than grey-ing it: a dimmed button still invites a tap,
   a plain glyph doesn't. */
   Every selector here is scoped under .s deliberately: the shared row-button
   rule above sets a 30px grey square for the move/remove controls, and an
   unscoped .micbtn would lose to it on specificity — coming out grey and
   oversized — regardless of which is declared first. */
.s .micbtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; flex: none; padding: 0; border: none;
  border-radius: 7px; background: var(--accent); color: #fff;
  cursor: pointer; font-size: 0;
}
.s .micbtn:hover { background: var(--accent-2); }
.s .micbtn:active { opacity: .7; }
.s .micbtn:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; }
.s .miclabel { display: inline-flex; align-items: center; flex: none; color: inherit; opacity: .75; }
/* The two-step. Yes/no sit right where the mic was, so the answer is where the
   question was asked and the row doesn't reflow under a moving thumb. Yes is
   filled like the mic it replaces; no stays an outline, so the destructive-
   looking option is never the loud one. */
.s .claimask { display: inline-flex; align-items: center; gap: 4px; color: var(--text); }
.s .claimask button {
  background: none; border: 1px solid #3a3748; border-radius: 7px;
  width: 22px; height: 22px; font-size: 12px; line-height: 1; cursor: pointer;
  color: inherit; flex: none; padding: 0;
}
.s .claimask button[data-act="claimYes"] { background: var(--accent); border-color: var(--accent); color: #fff; }
.s .claimask button[data-act="claimYes"]:hover { background: var(--accent-2); border-color: var(--accent-2); }
.s .claimask button[data-act="claimNo"]:hover { background: #3a3748; }
.qitem.asking { outline: 1px solid var(--accent-2); }
.claimnote { color: var(--warn); }
/* Not a queue entry — a preview of what YouTube plays next if nobody queues
   anything. Dashed and dimmed so it never reads as somebody's turn. */
.qitem.qnext { background: transparent; border: 1px dashed #3a3748; opacity: .8; }
.qitem.qnext .s { color: var(--dim); } /* flex/centring now comes from .qitem .s */
.qitem.qnext .s .pl { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Inline affordance on a dimmed preview row — sized for a fingertip via
   padding rather than a box, so it reads as part of the line, not chrome.
   Scoped to .peek deliberately: the mics and the yes/no buttons that also
   live on this row bring their own sizing, and a bare `button` selector here
   outranks theirs and would flatten it. */
.qitem.qnext .s button.peek {
  background: none; border: none; padding: 2px 4px; font-size: 13px;
  line-height: 1; cursor: pointer; color: inherit; flex: none;
  width: auto; height: auto; border-radius: 0;
}
/* Claiming is the row's point — let the mic sit at full strength against the
   .8 the preview card is dimmed to, pushed right away from the label. */
.qitem.qnext .s .micbtn,
.qitem.qnext .s .miclabel { margin-left: auto; margin-right: 6px; opacity: 1; }
.qitem.qnext .s .claimask { margin-left: auto; }
/* Read-only peek further down the playlist. Indented under the preview card
   and dimmer than it, so it never competes with the claimable song above. */
.peeklist {
  margin: 4px 0 0; padding: 0 0 0 30px; list-style: decimal;
  color: var(--dim); font-size: 12px; opacity: .75;
}
/* Numbered from 2: the card above is the playlist's next song, i.e. #1 of the
   five previewable. Spacing and clamping deliberately on different elements:
   -webkit-line-clamp sizes the content box only, so vertical padding on the
   same element would let a third line bleed in sliced in half. */
.peeklist li { padding: 7px 0; }
.peeklist li + li { border-top: 1px solid #2a2833; }
.peeklist li span {
  line-height: 1.35;
  display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden;
}
.peeklist li::marker { color: #3a3748; }
/* The nudge points at the Sing next card above the queue. Its copy promises an
   interaction ("boop me"), so it's a real button — keyboard-reachable and
   announced as such.
   It is drawn as its own card so the whole panel reads as the target: mascot
   and copy sit side by side, because stacking them made the mascot look like
   the button and the line under it look like a caption. It sits at page level
   above the queue now, so it takes --card like every other card; the inset
   treatment it had before was for when it nested inside the Queue card. The
   border is transparent rather than absent so the hover state doesn't resize
   it by two pixels. Mascot and copy are centred as a group rather than
   flushed left: the button spans the whole card, and against that width a
   left-flushed pair read as an icon with a stray caption beside it. */
button.qnudge {
  display: flex; align-items: center; justify-content: center; gap: 12px;
  width: 100%; text-align: center;
  background: var(--card); border: 1px solid transparent; border-radius: 14px;
  padding: 12px 16px; margin-top: 14px; font: inherit; color: var(--dim);
  cursor: pointer; transition: border-color .15s, color .15s;
}
button.qnudge .mascot-sm { width: 72px; margin: 0; flex: none; }
button.qnudge:hover { border-color: var(--accent-2); color: var(--text); }
button.qnudge:active { opacity: .6; }
button.qnudge:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; }
/* Centred, like the nudge directly above it: this label names the whole
   card rather than heading a left-aligned control, and it is the one that
   carries the empty state, so it reads as a heading for the list below. */
#queueCard > label { text-align: center; }
.qbtns { margin-bottom: 10px; }
/* Folds the queue back to QUEUE_VISIBLE rows. Full-width and quiet: it is a
   way to see more, not an action competing with the rows above it. */
.showall {
  display: block; width: 100%; margin-top: 6px; padding: 9px 0;
  background: none; border: 1px dashed #3a3748; border-radius: 9px;
  font: inherit; font-size: 12px; color: var(--dim); cursor: pointer;
}
.showall:hover { color: var(--text); border-color: var(--accent-2); }
.showall:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; }

/* --- startup banner ----------------------------------------------------------- */
.prep {
  background: var(--card); border-radius: 14px; padding: 14px 16px; margin-top: 14px;
  border-left: 3px solid var(--accent-2);
}
.prep .h { font-size: 14px; font-weight: 600; color: var(--text); }
.prep .sub { margin-top: 4px; }
.prep.err { border-left-color: var(--danger); }
.prep.err .h { color: var(--danger); }
.prepbar { height: 4px; border-radius: 2px; background: #3a3748; margin-top: 10px; overflow: hidden; }
.prepbar i { display: block; height: 100%; width: 0; background: var(--accent); border-radius: 2px; transition: width .25s; }
/* No percentage available (benchmarking, warm-up): sweep instead of sitting
   at zero, which reads as stuck. */
.prepbar.indet i { width: 40%; animation: prepsweep 1.2s ease-in-out infinite; }
@keyframes prepsweep { from { margin-left: -40%; } to { margin-left: 100%; } }

/* --- brand bits ---------------------------------------------------------------- */
.tagline {
  font-size: 15px; font-weight: 600; color: var(--text);
  text-align: center; margin-bottom: 8px;
}
.micbang {
  height: .95em; width: auto; margin-left: 4px; vertical-align: -0.12em;
  fill: var(--accent-2); color: var(--accent-2);
}
.mascot { display: block; width: 150px; margin: 4px auto 10px; }
/* Only ever appears inside the nudge button, which sets its own size. */
.mascot-sm { display: block; opacity: .9; }

/* --- host-only divider (web remote) --------------------------------------------- */
.hostdiv {
  margin-top: 22px; padding-top: 10px; border-top: 1px solid #3a3748;
  color: var(--accent-2); font-size: 13px; font-weight: 600;
  letter-spacing: .4px; text-transform: uppercase;
}
.hostdiv .sub { text-transform: none; letter-spacing: 0; font-weight: 400; margin-left: 6px; }
