/*
 * Shared styling for all four screens.
 *
 * The big screen is projected into a dark room and read from the back, so:
 * type is enormous and sized in viewport units, contrast is high, and nothing
 * important is ever thin, grey or small. The phone screens are the opposite
 * problem — big thumb targets, one decision per screen, no scrolling needed.
 */

/*
 * `hidden` has to actually hide.
 *
 * The browser's own rule is `[hidden] { display: none }`, which any rule of
 * ours setting `display` beats on specificity — so `.minor { display:
 * inline-block }` quietly made every `hidden` button on the console visible.
 * "File the rest away" and "Clear all" sat there on a night with no photos at
 * all, and both would have failed if pressed. One line here rather than
 * remembering it at every call site.
 */
[hidden] { display: none !important; }

:root {
  --ink: #f7f7fb;
  --ink-dim: #a7a7c2;
  --bg: #07070e;
  --bg-2: #10101f;
  --panel: rgba(255, 255, 255, 0.06);
  --panel-line: rgba(255, 255, 255, 0.14);

  /* The two corner washes and the two slowly drifting blobs. Named so a look
     can change the whole mood of the room rather than just the buttons. */
  --glow-1: #241348;
  --glow-2: #07304a;
  --drift-1: rgba(255, 46, 136, 0.16);
  --drift-2: rgba(75, 216, 255, 0.14);

  --hot: #ff2e88;
  --hot-2: #ff8a3d;

  /* ---- ONE SURFACE, and it carries the account's own colour.
   *
   * Every card and panel on the console was some shade of
   * `rgba(255,255,255,0.04–0.06)` on near-black — four slightly different
   * greys that nobody decided, plus the generator panel's blue tint. Grey on
   * black is also the one combination that cannot pick up a scheme, so an
   * account's colours reached exactly one button a screen and the loudest
   * thing on most pages was the destructive red.
   *
   * These are the fill's two stops and its edge, tinted with `--hot` at low
   * strength so a pink-and-orange account and a lime one are different rooms
   * rather than the same grey room with different buttons. It is a TINT and
   * not a wash: at this strength it reads as depth, and the scheme still
   * means personality rather than meaning, exactly as the GUI rules say.
   *
   * OPAQUE, and that is load-bearing rather than incidental. Translucent
   * cards over the page's washes render a different colour depending on where
   * they happen to sit — the same pack card pink in the middle of the grid and
   * grey at the edge of it, which is the "all over the place" fault wearing a
   * coat. Opaque, the background can be as alive as it likes.
   *
   * The rgba fallbacks come FIRST, because a browser too old for `color-mix`
   * would otherwise drop the declaration and leave a transparent card. */
  --surf-1: rgba(28, 24, 48, 1);
  --surf-1: color-mix(in srgb, var(--hot) 9%, #15152b);
  --surf-2: rgba(18, 16, 32, 1);
  --surf-2: color-mix(in srgb, var(--hot) 3%, #0e0e1c);
  --surf-line: rgba(255, 255, 255, 0.16);
  --surf-line: color-mix(in srgb, var(--hot) 20%, rgba(255, 255, 255, 0.12));
  --surface: linear-gradient(180deg, var(--surf-1), var(--surf-2));
  /* The far end of the logo's gradient. Its own variable rather than reusing
     --gold, because gold is the right third colour for a pink-and-orange
     record and quite wrong for a turquoise one. */
  --mark-3: #ffd23f;
  --cool: #4bd8ff;
  --gold: #ffd23f;

  /* ---- THE THREE METALS, and the sheen that makes them look like metal.
   *
   * Bronze/Silver/Gold mean a TIER (and second and third on the podium).
   * `--gold` above is a different job — the TROPHY colour, points and winning
   * — and the two must not be merged however close the hex is.
   *
   * They were written out in SIX places with SEVEN different values: bronze
   * was #cd7f32, #cd895a, #e0a066 and #cd8c58 depending which rule you landed
   * in. That is drift rather than a system, and it is why the sheen is a token
   * too — one edit, not six.
   *
   * `--metal-sheen` is a highlight in the top-left corner plus a shadow in the
   * far one, so a filled pill reads as a curved surface catching a light
   * rather than a flat swatch. Asked for as *"a little highlight in the top
   * corner to give it a bit of pop"*, and rendered against a plain 120deg
   * sweep before choosing: the sweep is louder and says nothing extra, and the
   * corner pair still reads at the 24px chips in the owner's hat switch, where
   * a sweep is only a lighter dot.
   *
   * It is a POSITION rather than an angle, so it does not break `120deg for
   * every gradient` — and it is ONE position everywhere, which is the same
   * discipline that rule exists for. It layers OVER the flat colour, so the
   * metal underneath stays a single token and can still be read as a `color`
   * by anything that needs one. */
  --metal-bronze: #cd7f32;
  --metal-silver: #c0c7d0;
  /* The same value as `--gold` deliberately: the tier gold and the trophy gold
     sit near each other and a shade between them would read as a fault. They
     stay two TOKENS because they are two jobs, not because they differ. */
  --metal-gold: #ffd23f;
  --metal-bronze-ink: #1a0e02;
  --metal-silver-ink: #10131a;
  --metal-gold-ink: #1a1200;
  --metal-sheen:
    radial-gradient(circle at 20% 16%, rgba(255, 255, 255, 0.62), rgba(255, 255, 255, 0) 50%),
    radial-gradient(circle at 88% 92%, rgba(0, 0, 0, 0.28), rgba(0, 0, 0, 0) 55%);
  --good: #2fe07a;
  --bad: #ff4d5e;

  --a: #4bd8ff;
  --b: #ffd23f;
  --c: #ff6bd6;
  --d: #7cf76b;
  /* Two more for the pick-them-all round, which shows six. Same spacing round
     the wheel so no two read as the same colour from the back of a room. */
  --e: #ff9d5c;
  --f: #b48cff;

  /* Every native control the browser draws for us — tickboxes, scrollbars,
     the open list of a dropdown — takes its colours from this. Without it the
     OS paints them for a white page and they arrive as white slabs on a dark
     one, which is exactly how the console's tickboxes and dropdowns looked. */
  color-scheme: dark;
  /* Three radii and no more: a field, a card, a pill. See the GUI rules. */
  --r-field: 10px;
  --r-card: 14px;
  --r-pill: 999px;

  --radius: 18px;
  --shadow: 0 18px 60px rgba(0, 0, 0, 0.55);

  /* ---- The type ladder.
   *
   * Ten steps, and every size on a PAGE comes from one of them. It was
   * twenty-eight different numbers, six of them halves — 12.5 next to 13, 13.5
   * next to 14, 11.5 next to 11 — which is not a scale, it is the same
   * decision taken separately twenty-eight times. The tell was the On|Off
   * switch: the one in the top right was 11px and the identical one on each
   * feature was 11.5px, on two controls this file says are meant to be the
   * same shape so they are recognised rather than read.
   *
   * The names are the JOB, not the pixels, so moving a step is one edit here
   * rather than a search for a number that also means something else.
   *
   * TWO THINGS ARE DELIBERATELY NOT ON IT, and both would be a bug to fold in:
   *
   * - The BINGO CELL ramp (`.bingo-cell .bt` / `.ba` down through cols-3, 4
   *   and 5). Those halves are not drift — they are tuned so 25 squares still
   *   fit a 320px phone with the words readable, checked by hand, and a square
   *   you cannot read is somebody missing a full house. Same for
   *   `.cam-prop-name`, which has to fit inside a 66px tile.
   * - The PROJECTOR, which is sized in `vh` throughout. A big screen's type
   *   has to grow with the screen; pinning it to pixels would make the
   *   question small on a hall projector and huge on a laptop. */
  --fs-tag: 11px;     /* over-line labels, badges, type tags */
  --fs-mini: 12px;    /* dense UI — switches, chips, small buttons */
  --fs-note: 13px;    /* secondary lines. This is what `.tiny` means */
  --fs-body: 14px;    /* body copy in a panel, and a warning that must not go quiet */
  --fs-ctl: 15px;     /* buttons and selects */
  --fs-field: 16px;   /* text inputs — the floor that stops iOS zooming on focus */
  --fs-lead: 17px;    /* a card's title, the lead line of a panel */
  --fs-head: 19px;    /* a panel heading */
  --fs-sub: 20px;
  --fs-title: 22px;
  /* Above 22 the sizes are display type — a result, a room code, a countdown —
     one per surface and chosen for that surface. They stay written out. */
}

* { box-sizing: border-box; }

/* =====================================================================
 * THE FIELD — one look for a text box and a dropdown.
 *
 * There was no rule for a plain input at all outside the phone and the
 * control view, which is why the venue box on a pack card came out as a
 * browser default sitting beside three carefully styled selects. A dropdown
 * and a box you type in are the same KIND of control — "choose something" —
 * and the GUI rules in CLAUDE.md say they look identical.
 *
 * Deliberately quiet, and deliberately NOT carrying the brand gradient. It is
 * furniture around the decision rather than the decision, and the one filled
 * gradient on a screen has to stay the thing you press.
 *
 * Placed high in the file on purpose: `.phone input[type="text"]` and the
 * editor's own fields have the same specificity, so source order is what lets
 * them keep winning where they have already been tuned.
 * ===================================================================== */
/* `input:not([type])` IS IN THIS LIST BECAUSE AN INPUT WITHOUT A `type`
 * ATTRIBUTE IS A TEXT BOX AND `input[type="text"]` DOES NOT MATCH IT.
 * The attribute selector reads the ATTRIBUTE, not the behaviour — so
 * `<input class="ow-name" placeholder="Your name">` behaved as a text field
 * and was styled as a browser default: square corners, the wrong font, a
 * white slab. It sat in the first-run "Create the owner account" panel next
 * to `.ow-email` and `.ow-pass`, which DO carry a type and were styled
 * properly — three boxes in one row, one of them visibly not part of the
 * app, on the very first screen anybody sees. Matching bare inputs here
 * fixes every one of them, including the next one somebody writes. */
input[type="text"], input[type="search"], input[type="email"],
input[type="password"], input[type="number"], input[type="url"],
input:not([type]), textarea {
  font-family: inherit;
  font-size: var(--fs-note);
  font-weight: 700;
  color: var(--ink);
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid var(--panel-line);
  border-radius: var(--r-field);
  padding: 8px 11px;
  min-width: 0;
  max-width: 100%;
}
input:focus, textarea:focus {
  outline: none;
  /* Their own colour, because a focus ring is a highlight and highlights are
     personality. See "a scheme changes personality, never meaning". */
  border-color: var(--hot);
}
input::placeholder, textarea::placeholder { color: var(--ink-dim); font-weight: 600; }
/*
 * A NUMBER BOX IS NARROW BY NATURE, so it gets less padding than a box you
 * type words into. The shared 11px each side left 30 of the 52 pixels those
 * round-count fields have, which clipped "10" to "1" and a sliver — found by
 * the host looking at the generator panel, not by any test.
 */
input[type="number"] { padding-left: 6px; padding-right: 6px; }


html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--bg);
  color: var(--ink);
  font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

body {
  /* ---- A PAGE SCROLLS. THE PROJECTOR IS THE ONE THAT DOES NOT.
   *
   * This said `hidden` for two years, written for the big screen — and `body`'s
   * overflow PROPAGATES TO THE VIEWPORT when `html` is `visible`, so it was not
   * clipping the body box, it was switching off scrolling for the whole
   * document. Every page then had to opt back OUT of it, and four of the eight
   * did: console, host, editor, phone.
   *
   * The four that did not were public. Measured with a real wheel: Terms was
   * 2,528px tall in a 900px window and moved ZERO — the same for Privacy,
   * Refunds and the sign-up form, and for a gallery of fourteen photographs on
   * a phone. Nobody could read the legal pages this app SELLS against.
   *
   * A programmatic `scrollTo()` works fine on a hidden viewport, which is why
   * this survived being looked at: it has to be driven with a real gesture.
   *
   * So the default is the ordinary web one and the exception is named — the
   * projector, on `body.screen`. A default that six pages have to escape is
   * how two of them came to be missed. */
  overflow: auto;
  /* Through variables so a look can repaint the whole room. These were written
     out as hex here, which meant changing --bg for Halloween moved almost
     nothing — the two corner glows were still the ordinary purple and teal. */
  background:
    radial-gradient(120% 90% at 12% 0%, var(--glow-1) 0%, transparent 55%),
    radial-gradient(120% 90% at 88% 100%, var(--glow-2) 0%, transparent 55%),
    var(--bg);
}

/* A slow drift so a static screen never looks like it has frozen. */
body::after {
  content: '';
  position: fixed;
  inset: -30%;
  pointer-events: none;
  background:
    radial-gradient(38% 38% at 30% 30%, var(--drift-1), transparent 70%),
    radial-gradient(38% 38% at 72% 66%, var(--drift-2), transparent 70%);
  animation: drift 26s ease-in-out infinite alternate;
  z-index: 0;
}
@keyframes drift {
  from { transform: translate3d(-3%, -2%, 0) scale(1); }
  to   { transform: translate3d(3%, 3%, 0) scale(1.12); }
}

.stage {
  position: relative;
  z-index: 1;
  height: 100%;
  display: flex;
  flex-direction: column;
}

/* ------------------------------------------------------------------ shared */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  padding: 0.35em 0.9em;
  border-radius: var(--r-pill);
  background: var(--panel);
  border: 1px solid var(--panel-line);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.muted { color: var(--ink-dim); }
.hidden { display: none !important; }

.grad-text {
  background: linear-gradient(120deg, var(--hot) 0%, var(--hot-2) 45%, var(--gold) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  /*
   * AND THE DESCENDERS NEED A BOX TO BE PAINTED IN.
   *
   * `background-clip: text` paints the gradient across the element's own box
   * and clips it to the glyph shapes — so anything hanging BELOW that box is
   * simply not painted. Every display heading on the projector sets a
   * `line-height` under 1 (0.95 on the winner and the round intro, 0.98 on the
   * lobby), which is exactly what makes the box shorter than the type, so
   * **every g, y, p, q and j was sliced off at the baseline**: reported off
   * the DJ screen's own heading, and true since gradient text was introduced.
   * The worst case is the one nobody had looked at — the WINNER'S NAME, 13vh,
   * in gold, at the moment the night is built towards.
   *
   * **THE PADDING AND THE MARGIN CANCEL, so not one pixel moves.** The box
   * grows downward by enough to hold a descender and the negative margin takes
   * the same amount back off the flow, which is what lets one rule serve the
   * projector, the phones and the public pages without re-measuring six
   * layouts. `final-fits.mjs` is the check that says so.
   *
   * **DO NOT "TIDY" THIS TO A `line-height: 1`** — the tight leading on big
   * display type is a design decision, and this pays for it rather than
   * undoing it.
   */
  padding-bottom: 0.18em;
  margin-bottom: -0.18em;
}

/* =================================================================== SCREEN
   The projected view. Everything here is sized in vw/vh so it fills whatever
   television or projector it lands on without anyone touching a setting. */

.screen .topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5vw;
  padding: 1.6vh 2.2vw;
  font-size: 1.5vh;
  border-bottom: 1px solid var(--panel-line);
  flex: 0 0 auto;
}
/* THE HEADER IS A BAR AND MUST NOT GROW. A long pack name stacked it to four
   lines on a narrow stage, which pushed it past the `top: 8.5vh` the join
   corner is pinned at — so the QR card painted over the round and player
   pills. One line, ellipsised, and the collision cannot happen. */
.screen .topbar > * { min-width: 0; }
.screen .topbar .title { font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The pills SHRINK rather than push the row off the stage. Bingo's status pill
   is a sentence — "Playing for 2 lines — prize 2 of 3" — and beside the player
   count it ran 11px past the edge of a narrow stage, where `overflow: hidden`
   means gone rather than scrolled to. */
.screen .topbar .right { flex: 0 1 auto; min-width: 0; overflow: hidden; }
.screen .topbar .right > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.screen .topbar .right { display: flex; gap: 0.8vw; align-items: center; }

.screen .card {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 2.5vh 3vw;
  min-height: 0;
  animation: cardIn 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
@keyframes cardIn {
  from { opacity: 0; transform: translateY(1.2vh) scale(0.995); }
  to   { opacity: 1; transform: none; }
}

/* ---- lobby */
.lobby-grid {
  display: grid;
  grid-template-columns: 1.15fr 0.85fr;
  gap: 3vw;
  align-items: center;
  height: 100%;
}
.lobby h1 {
  font-size: 7.4vh;
  line-height: 0.98;
  margin: 0 0 1.6vh;
  font-weight: 900;
  letter-spacing: -0.02em;
}
.lobby .sub { font-size: 2.5vh; color: var(--ink-dim); margin-bottom: 3.5vh; }
.lobby .join-steps { display: grid; gap: 1.6vh; font-size: 2.6vh; }
.lobby .join-steps li { display: flex; gap: 1.2vw; align-items: baseline; }
.lobby .join-steps .n {
  flex: 0 0 auto;
  width: 1.9em; height: 1.9em;
  display: grid; place-items: center;
  border-radius: 50%;
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  color: #fff; font-weight: 900; font-size: 0.8em;
}
.lobby ol { list-style: none; padding: 0; margin: 0; }

/*
 * THE JOIN PANEL IS CAPPED BY THE SCREEN'S HEIGHT, not only by its column.
 *
 * A QR code is square and this panel is `width: 100%` of a grid column, so on
 * a WIDE, SHORT projector the column is generous and the panel grows taller
 * than the screen. Measured on the lobby with nothing else changed: at 21:9
 * (2560x1080) the typed-URL fallback is **26px off the bottom**, and at 3:1
 * (2160x720) the CODE ITSELF is **166px off** — on a page that deliberately
 * does not scroll, so there is no way to reach it. The room can neither scan
 * nor type.
 *
 * 21:9 is an ordinary pub projector and 32:9 monitors exist; this is not an
 * exotic size. **The rules slide's identical panel was capped all along**
 * (`min(44vw, 66vh)`), which is why that one fits everywhere — so this is the
 * answer the app already had, applied to the screen it was missing from.
 *
 * `min(100%, …)` so nothing changes at any ordinary ratio: the column is still
 * the limit at 16:9 (495px against a 72vh cap of 518) and at 4:3 (396px), and
 * the height only becomes the limit once it is genuinely the smaller of the
 * two. **72vh rather than something rounder** — the panel runs about its own
 * width plus 4vh of padding, gap and typed URL, and it starts a little way
 * down the column, so 72 is the largest number that still clears the bottom at
 * 3:1 with the arcade board in it. Measured, not chosen.
 */
.qr-panel {
  background: #fff;
  border-radius: var(--r-card);
  padding: 2.2vh;
  box-shadow: var(--shadow);
  display: grid;
  justify-items: center;
  gap: 1.2vh;
  max-width: min(100%, 72vh);
}
.qr-panel svg, .qr-panel img { width: 100%; height: auto; display: block; }
.qr-panel .url {
  color: #12121c;
  font-weight: 800;
  font-size: 2.1vh;
  word-break: break-all;
  text-align: center;
}

/*
 * THE LOBBY GAME'S TOP SCORES, inside the white QR panel and under the code.
 *
 * Dark on white, because it is a child of `.qr-panel` and that panel is white
 * so a phone camera can read the code off it — a board in the app's own dark
 * colours would be invisible in there.
 *
 * Sized in `vh` like everything else on the projector: a big screen's type has
 * to grow with the screen, and pinning this to pixels would make it a squint
 * in a hall and enormous on a laptop.
 */
.arcade-board {
  width: 100%;
  border-top: 1px solid rgba(18, 18, 28, 0.14);
  padding-top: 1.2vh;
}
.arcade-board .ab-head {
  color: #4a4a5e;
  font-size: 1.6vh;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: 0.6vh;
}
.arcade-board .ab-row {
  display: grid;
  grid-template-columns: 2.4vh 1fr auto;
  align-items: baseline;
  gap: 0.8vh;
  color: #12121c;
  font-size: 1.9vh;
  line-height: 1.35;
}
/* A MIXED BOARD HAS FOUR THINGS IN A ROW, so it needs a fourth column.
   The game icon is only drawn when the room chose its own game — and with
   three columns declared, that fourth child dropped onto an implicit second
   row: the score on a line of its own, every row twice as tall, and three of
   five rows below the fold on a projector that cannot scroll. "Let them
   choose" is the selected default, so the mixed board is the ordinary case.
   A class rather than `:has(.ab-game)`, so a projector too old for it is not
   the one left with the fault. */
.arcade-board .ab-row.ab-mixed { grid-template-columns: 2.4vh 1fr auto auto; }
.arcade-board .ab-pos { color: #7a7a92; font-weight: 800; }
/* A team name has no spaces in it if somebody decides it does not — there is
   deliberately no filtering and 28 characters is the only limit, so a solid
   one has to break rather than push the score off the panel. */
.arcade-board .ab-name { font-weight: 700; overflow-wrap: anywhere; min-width: 0; }
.arcade-board .ab-score { font-weight: 800; font-variant-numeric: tabular-nums; }
/* Which game that score was set on, when the room picked its own. Drawn only
   on a mixed board — see lobby-board.js — so a pinned night's rows are the two
   columns they always were. */
.arcade-board .ab-game { flex: 0 0 auto; line-height: 1; }

.player-strip {
  flex: 0 0 auto;
  padding: 1.4vh 2.2vw 2.4vh;
  display: flex;
  flex-wrap: wrap;
  gap: 0.8vh 0.8vw;
  align-content: flex-start;
  max-height: 26vh;
  overflow: hidden;
}
.player-chip {
  padding: 0.6vh 1.1vw;
  border-radius: var(--r-pill);
  background: var(--panel);
  border: 1px solid var(--panel-line);
  font-size: 2.1vh;
  font-weight: 700;
  animation: pop 380ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
@keyframes pop {
  from { opacity: 0; transform: scale(0.7); }
  to   { opacity: 1; transform: scale(1); }
}

/* ---- round intro */
.round-intro { text-align: center; }
.round-intro .kicker { font-size: 2.4vh; letter-spacing: 0.4em; text-transform: uppercase; color: var(--ink-dim); }
.round-intro h1 { font-size: 11vh; margin: 2vh 0; font-weight: 900; letter-spacing: -0.03em; line-height: 0.95; }
.round-intro .blurb { font-size: 3.4vh; color: var(--ink-dim); }
/* THEY WRAP. Two pills side by side ran 19px off a narrow stage, which on a
   `overflow: hidden` projector means the round's own description is cut. */
.round-intro .facts { margin-top: 5vh; display: flex; flex-wrap: wrap; gap: 1.4vw;
  justify-content: center; font-size: 2vh; }

/* ---- question */
.q-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 2vw;
  margin-bottom: 2vh;
}
.q-counter { font-size: 2vh; }

.q-prompt {
  font-size: 5.6vh;
  line-height: 1.08;
  font-weight: 800;
  letter-spacing: -0.015em;
  margin: 0 0 3vh;
  flex: 0 0 auto;
}
.q-prompt.small { font-size: 4.4vh; }

/* The options stretch to fill whatever height is left, so the screen is used
   right down to the bottom edge and the text is as big as it can be. */
.options {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-auto-rows: 1fr;
  gap: 1.8vh 1.8vw;
  flex: 1 1 auto;
  min-height: 0;
}
.option {
  display: flex;
  align-items: center;
  gap: 1.4vw;
  padding: 2.2vh 2vw;
  border-radius: var(--radius);
  background: var(--panel);
  border: 2px solid var(--panel-line);
  font-size: 3.9vh;
  font-weight: 700;
  transition: transform 260ms, background 260ms, border-color 260ms, opacity 260ms;
}
/* A SOLID WORD BREAKS RATHER THAN RUNNING OFF THE STAGE.
   The same rule `.ab-name` already carries, and it belongs here far more:
   `body.screen` is `overflow: hidden`, so an option wider than its column is
   not scrolled to, it is GONE — measured at 328px off a 1440 stage and 1300px
   off an 820 one, with the room unable to read two of the four answers. A flex
   item defaults to `min-width: auto`, so the box refuses to shrink below its
   longest word; `min-width: 0` is what lets the wrap happen at all. */
.option { min-width: 0; }
.option .text { min-width: 0; overflow-wrap: anywhere; }
.option .letter {
  flex: 0 0 auto;
  width: 2em; height: 2em;
  display: grid; place-items: center;
  border-radius: var(--r-card);
  font-weight: 900;
  color: #0a0a12;
  font-size: 0.78em;
}
.option:nth-child(1) .letter { background: var(--a); }
.option:nth-child(2) .letter { background: var(--b); }
.option:nth-child(3) .letter { background: var(--c); }
.option:nth-child(4) .letter { background: var(--d); }
.option:nth-child(5) .letter { background: var(--e); }
.option:nth-child(6) .letter { background: var(--f); }

.option.correct {
  background: rgba(47, 224, 122, 0.18);
  border-color: var(--good);
  transform: scale(1.02);
}
.option.dimmed { opacity: 0.32; }

.option .tally {
  margin-left: auto;
  font-size: 0.7em;
  color: var(--ink-dim);
  font-weight: 800;
}

/* ---- timer */
.timer-wrap { flex: 0 0 auto; display: flex; align-items: center; gap: 1.5vw; margin-bottom: 2.4vh; }
.timer-bar {
  flex: 1 1 auto;
  height: 1.8vh;
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.1);
  overflow: hidden;
}
.timer-bar span {
  display: block;
  height: 100%;
  width: 100%;
  border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--good), var(--gold) 55%, var(--hot));
  transform-origin: left center;
}
.timer-num {
  flex: 0 0 auto;
  font-size: 6vh;
  font-weight: 900;
  font-variant-numeric: tabular-nums;
  min-width: 2.2em;
  text-align: right;
  letter-spacing: -0.03em;
}
.timer-num.urgent { color: var(--bad); animation: pulse 700ms ease-in-out infinite; }
@keyframes pulse { 50% { transform: scale(1.08); } }

.answered-count { flex: 0 0 auto; font-size: 2.2vh; color: var(--ink-dim); font-weight: 700; }

/* ---- reveal */
.reveal-banner {
  flex: 0 0 auto;
  margin-top: 2.6vh;
  display: flex;
  align-items: center;
  gap: 1.6vw;
  padding: 2vh 2vw;
  border-radius: var(--radius);
  background: linear-gradient(120deg, rgba(255, 46, 136, 0.22), rgba(255, 210, 63, 0.14));
  border: 2px solid rgba(255, 210, 63, 0.4);
  font-size: 3vh;
  animation: cardIn 340ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.reveal-banner .label { font-size: 0.62em; letter-spacing: 0.24em; text-transform: uppercase; color: var(--gold); font-weight: 800; }
/* THE ONE I MISSED FIRST TIME ROUND. Options, the phone's card and the host's
   key were all fixed and this still pushed the banner 319px off a narrow stage
   — same solid word, same rule, a different box. */
.reveal-banner { min-width: 0; }
.reveal-banner > * { min-width: 0; }
.reveal-banner .who { font-weight: 900; font-size: 1.15em; overflow-wrap: anywhere; }
.reveal-banner .pts { margin-left: auto; font-weight: 900; color: var(--gold); font-size: 1.5em; }
/* The fastest finger's own face. Sized in vh like everything else on the
   projector so it holds its proportion from a laptop to a 4K screen, and
   square-cropped because a photo from a phone is portrait and a drawn avatar
   is square — one shape, or the banner changes height depending on who won. */
.reveal-banner .fastest-face {
  flex: 0 0 auto;
  width: 11vh;
  height: 11vh;
  border-radius: var(--r-card);
  object-fit: cover;
  background: var(--panel);
  border: 2px solid rgba(255, 210, 63, 0.55);
  animation: cardIn 420ms cubic-bezier(0.2, 0.8, 0.2, 1) 120ms backwards;
}

/* ---- leaderboard */
/* Centred vertically, so four teams look deliberate rather than stranded at
   the top, and ten still fit comfortably. */
.board { display: flex; flex-direction: column; height: 100%; justify-content: center; }
/*
 * A BREAK THAT ROTATES — the wrapper the scores and this venue's slides take
 * turns inside. It has to fill the card exactly as either of them would on
 * its own, or a slide drawn inside it would sit in a shorter box than the
 * same slide drawn by the host pressing the button. `min-height: 0` because
 * the advert's own image and QR are flex children further down and would
 * otherwise refuse to shrink on a 720p projector.
 */
.board-cycle { display: flex; flex-direction: column; height: 100%; min-height: 0; }
.board-cycle > * { flex: 1 1 auto; min-height: 0; }
/* NOTHING ON THE SCREEN, asked for in those words — the round still names
   itself, because a projector with literally nothing on it reads as broken
   from the back of a room rather than as deliberate. */
.board-quiet { justify-content: center; text-align: center; }
.board h1 { font-size: 6.4vh; margin: 0 0 2.5vh; font-weight: 900; letter-spacing: -0.02em; }
.board-rows { display: grid; gap: 1.1vh; align-content: start; overflow: hidden; }
.board-row {
  display: flex;
  align-items: center;
  gap: 1.6vw;
  padding: 1.5vh 1.8vw;
  border-radius: var(--r-card);
  background: var(--panel);
  border: 1px solid var(--panel-line);
  font-size: 3.2vh;
  font-weight: 700;
  animation: slideIn 420ms cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
}
@keyframes slideIn {
  from { opacity: 0; transform: translateX(-2vw); }
  to   { opacity: 1; transform: none; }
}
.board-row .pos {
  flex: 0 0 auto; width: 2em; text-align: center;
  font-weight: 900; color: var(--ink-dim); font-variant-numeric: tabular-nums;
}
.board-row .score { margin-left: auto; font-weight: 900; font-variant-numeric: tabular-nums; }
.board-row.top1 { background: linear-gradient(120deg, rgba(255, 210, 63, 0.26), rgba(255, 138, 61, 0.12)); border-color: var(--gold); }
.board-row.top1 .pos, .board-row.top1 .score { color: var(--gold); }
.board-row.top2 { border-color: rgba(255, 255, 255, 0.4); }
.board-row.top3 { border-color: rgba(255, 138, 61, 0.5); }

/* ---- winner */
/* ---- THE FINAL FITS, ALWAYS — see `fitWinner()` in `screen.js`.
 *
 * `place-content: center` in a fixed-height card with the projector's overflow
 * hidden means content that is too tall is cut at BOTH ends — which it was, by
 * 142px each way on a night with a draw, a league and a comeback. `--fit` is
 * measured after the card draws and is 1 on any night that already fitted, so
 * this changes nothing about a slide that was never in trouble. */
.winner {
  text-align: center; display: grid; place-content: center; gap: 2vh; height: 100%;
  transform: scale(var(--fit, 1));
  transform-origin: center center;
}
/* THE DRAW AND THE COMEBACK SIT SIDE BY SIDE, which is the half of the fix
   that costs nothing: there is plenty of width going spare — both bands are
   centred and narrow — and stacking them was most of the overflow. They wrap
   on a narrow screen rather than being squeezed, and their own `margin-top` is
   dropped because the row carries it once for both. */
.winner .endband {
  display: flex; flex-wrap: wrap; gap: 2vw;
  justify-content: center; align-items: stretch;
  margin-top: 3.5vh;
}
.winner .endband:empty { display: none; margin-top: 0; }
.winner .endband > * { margin-top: 0; }
.winner .kicker { font-size: 3vh; letter-spacing: 0.5em; text-transform: uppercase; color: var(--gold); }
/* `overflow-wrap: anywhere` on both the winner and the podium, because a team
   name has no spaces in it if somebody decides it does not — there is
   deliberately no filtering on what gets typed, and the 28-character cap is
   the only limit. Without this a solid 28-character name runs straight out of
   its card and across the one beside it. Seen at exactly that width. */
.winner h1 {
  font-size: 13vh; margin: 0; font-weight: 900; letter-spacing: -0.04em; line-height: 0.95;
  overflow-wrap: anywhere;
}
.winner .score { font-size: 7vh; font-weight: 900; }
/* SECOND AND THIRD ARE A PODIUM, not a caption under the winner.
   They were one dim 2.6vh line of "2. Name — 1,240", which from the back of a
   pub is unreadable — so the only result the room could actually see was
   first place, and everybody who came second went unnamed. Being on the
   podium is most of what a quiz night gives the people who did not win it.

   Gold stays first place's alone (it is the trophy colour everywhere in this
   app); the medal tints below are what tells 2nd from 3rd, which is the one
   place a metal genuinely carries the meaning. A fourth row, if the board has
   one, keeps the plain treatment — it is context, not a placing. */
/* SECOND AND THIRD SHARE THE ROW — half each, and that is the point.
   They were three equal cards in a line, 2nd, 3rd and 4th, so the podium read
   as three also-rans of the same weight and coming second looked like coming
   fourth. Given the width between them they are big enough to read from the
   back of a pub, and the winner is untouched at 13vh — first place stays the
   result the night is about.

   `grid-auto-flow: column` rather than a fixed pair, so a tie for second gets
   three equal cells instead of one wrapping onto a line of its own. */
.winner .runners {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: 1.6vw; margin-top: 3.5vh;
}
.winner .runner {
  display: grid; gap: 0.5vh; justify-items: center; align-content: center;
  padding: 2.2vh 1.6vw;
  border-radius: var(--r-card);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.1);
  min-width: 0;
}
.winner .rplace {
  font-size: 2.8vh; font-weight: 900; line-height: 1;
  width: 5.2vh; height: 5.2vh; border-radius: var(--r-pill);
  display: grid; place-items: center;
  color: #17131b; background: rgba(255, 255, 255, 0.55);
}
.winner .rname {
  font-size: 5.4vh; font-weight: 800; color: var(--ink); line-height: 1.1;
  overflow-wrap: anywhere;
}
.winner .rscore { font-size: 3.2vh; font-weight: 700; color: var(--ink-dim); }
/* THE DRAW, under the podium and deliberately not on it.
   It is a real prize and a real moment, but it is not a placing — somebody
   who came eleventh has not beaten anybody, and putting them in the medal row
   would say they had. So: its own band, below, in the account's own colours
   rather than a metal, because the metals mean position and this does not.

   The count is on screen for a reason the host says on the mic — "drawn from
   nine still playing" is what makes it obviously fair to the eight who did
   not win it, and it is the sentence that gets people to stay in next week. */
.winner .dip {
  margin-top: 4.5vh;
  padding: 2vh 3vw;
  border-radius: var(--r-card);
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.04);
  display: inline-grid; gap: 0.5vh; justify-items: center;
}
.winner .dip-label {
  font-size: 2.2vh; font-weight: 800; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-dim);
}
.winner .dip-name {
  font-size: 5vh; font-weight: 900; line-height: 1.1;
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.winner .dip-note { font-size: 1.9vh; font-weight: 600; color: var(--ink-dim); }

/* ---- THE LAST SLIDE OF THE NIGHT — "Back here Thursday 20th"
   Under the winner and the draw, never over them: somebody has just won a quiz
   in front of a room, and next week's date has no business being the biggest
   thing on that screen. Sized in vh like everything else on the projector, so
   it grows with the hall rather than being pinned to a laptop's pixels.

   The QR is white-on-white with a real quiet zone, because the one thing that
   must work here is a camera pointed at it from six feet away in a dark pub —
   the same reasoning as the join corner, which is the other scannable thing
   this app puts on a projector. */
.winner .comeback {
  margin-top: 4.5vh;
  padding: 2vh 3vw;
  border-radius: var(--r-card);
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.04);
  /* Centred as a PAIR. The winner's own name sets how wide this column is, so
     the box stretches like the draw above it — but packed left the words and
     the QR ended up in the corner of a very wide band with nothing beside
     them. */
  display: flex; align-items: center; justify-content: center; gap: 3vw;
}
.winner .cb-words { display: grid; gap: 0.6vh; justify-items: center; }
.winner .comeback.has-qr .cb-words { justify-items: start; text-align: left; }
.winner .cb-line { font-size: 4.4vh; font-weight: 900; line-height: 1.1; }
.winner .cb-note {
  font-size: 2vh; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink-dim);
}
.winner .cb-qr {
  flex: 0 0 auto;
  background: #fff; border-radius: var(--r-field); padding: 1vh;
  width: 14vh; height: 14vh;
}
.winner .cb-qr img { display: block; width: 100%; height: 100%; }

/* ---- TONIGHT'S PHOTOGRAPHS — a slide of its own, and the code is the point.
 *
 * The comeback band's 14vh QR is sized to sit BESIDE a line of text under a
 * podium, where it is the second thing on the slide. Here it is the only
 * thing, and the job is harder than the join code's at the start of the night:
 * that one is read by people sitting down at a table, this one by people
 * standing up with their coats on. Measured at 1280x720 the band's code came
 * out 86px across, which is not a code anybody scans from the back of a pub.
 *
 * 34vh, and the words go ABOVE it rather than beside it, so the code gets the
 * full width of the card rather than what is left of it. */
.photos-slide .comeback {
  display: grid; justify-items: center; gap: 1.5vh;
  grid-template-columns: none;
}
.photos-slide .cb-qr { width: 34vh; height: 34vh; padding: 1.4vh; }
.photos-slide .cb-line { font-size: 4vh; }

/* Silver and bronze. Only these two — a fourth place has no medal. */
/* The medal disc takes the sheen too — it is the one metal the ROOM sees, at
   the biggest size any of them is ever drawn, so it is where a flat swatch
   showed most. The card's own tint behind it stays as it was: that is a wash,
   not a medal. */
.winner .place-2 { border-color: rgba(214, 218, 226, 0.5); background: rgba(214, 218, 226, 0.09); }
.winner .place-2 .rplace { background: var(--metal-sheen), var(--metal-silver); }
.winner .place-3 { border-color: rgba(205, 140, 88, 0.5); background: rgba(205, 140, 88, 0.09); }
.winner .place-3 .rplace { background: var(--metal-sheen), var(--metal-bronze); }

/* ---- DRAG AND DROP in the editor: rounds and questions.
   A grip rather than a draggable card, because a card is full of text boxes
   and `draggable` on their container stops you selecting a word to retype it.
   The arrow buttons stay: drag is the faster way, not the only way, and HTML5
   drag events do not fire on touch at all. */
.drag-grip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; flex: 0 0 auto;
  color: var(--ink-dim); cursor: grab; border-radius: var(--r-field);
}
.drag-grip:hover { color: var(--ink); background: rgba(255, 255, 255, 0.08); }
.drag-grip:active { cursor: grabbing; }
.is-dragging { opacity: 0.45; }
/* WHERE IT WOULD LAND, drawn on the row under the cursor — above or below,
   decided by which half of it you are over. Without the two, a list can only
   be reordered in one direction and the last position is unreachable. */
/* A pack being dragged up to Tonight: the section says it will take it. */
.launchbar.drop-here { border-color: var(--hot); background: rgba(255, 46, 136, 0.08); }
.pack-card[draggable="true"] { cursor: grab; }
.pack-card.is-dragging { opacity: 0.45; }
/* A venue dragged up to the same place, from the Venues tab. Only a SHUT card
   is draggable — an open one is full of prize boxes, and `draggable` on their
   container stops you selecting a word to retype it. */
.venue-card[draggable="true"] { cursor: grab; }
.venue-card.is-dragging { opacity: 0.45; }

/* TONIGHT PINS ITSELF WHILE SOMETHING IS BEING DRAGGED.

   The section is at the top of the tab and a venue card can be most of a page
   below it. HTML5 drag has no dependable auto-scroll, so without this the
   gesture is "pick the card up, find the target off-screen, give up" — worse
   again on a trackpad. Pinned for the length of the drag, the target is always
   under the cursor's reach.

   Only while dragging: sticky the rest of the time would cost the top of every
   tab all night, which is the same reason the tab bar scrolls sideways on a
   phone rather than growing a second row. */
/* AND THE TAB COLUMN STANDS DOWN WHILE SOMETHING IS BEING DRAGGED.
   Two sticky things pinned to the same place is what produced the mess in the
   post-drag screenshot: the tabs painted straight through the middle of the
   panel somebody was dragging INTO — over the tiles and over the Launch
   button. Un-pinning for the drag is better than out-ranking it: during a
   drag the tabs are not the thing being used, and one pinned object cannot
   fight another that is not there.
   Still true with the tabs in a left-hand COLUMN, which is why this survived
   the change rather than being deleted with the rest of the bar: Tonight
   spans the full width, the sidebar's lane included, so a pinned sidebar and
   a pinned Tonight are still two things claiming the same top-left. */
body.is-dragging-card .tabbar { position: static; }
/* `.launchbar.panel`, NOT `.launchbar` ALONE — `body.console .panel` sets its
   own `position: relative` for the scroll rods, at EQUAL specificity (two
   classes on body's element each), so whichever rule is LATER in the sheet
   wins the `position` property regardless of what this comment says it
   should be. It was: `position: relative` silently beat `position: sticky`
   on every drag, so the pin never engaged at all — but `top: var(--lb-pin)`
   below has no competing declaration and applied anyway, as a plain
   RELATIVE offset with nothing reserving the space it shifted the panel
   into. Stuck `is-dragging-card` (a drag that never reaches its own
   `dragend`) then froze that shift on indefinitely, and the panel sat down
   over whatever followed it in the document — the pack shelf. Naming
   `.panel` here too costs nothing (the markup always carries both) and
   makes this the more specific selector, so `position: sticky` wins outright
   rather than by source order — the same trap this file already has a note
   about for `border` lower down the sheet. */
body.is-dragging-card .launchbar.panel {
  position: sticky;
  /* WHERE IT ALREADY IS, worked out at the moment the drag starts — see
     `pinTonightWhereItIs()`. Pinning at `--topbar-h` flat only ever moves a
     panel that has scrolled past that line, and it always has, because you
     scroll DOWN to reach the library you are dragging from. So the panel
     lurched ninety pixels the instant a card was picked up and the drop tiles
     slid out from under the cursor. A sticky top may be NEGATIVE: pinning at
     the current offset freezes it exactly where the eye last saw it, which is
     all the pinning was ever for.

     `--topbar-h` is the fallback and stays correct on its own — it is the old
     behaviour, for a drag that somehow starts before the measurement runs. */
  top: var(--lb-pin, var(--topbar-h));
  /* Above the tab bar as well as above the page. Belt and braces: the rule
     above takes the bar out of the way, and this makes the drop target win
     even if something is pinned that we did not think of. */
  z-index: 12;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
}
.drop-above { box-shadow: inset 0 3px 0 0 var(--hot); }
.drop-below { box-shadow: inset 0 -3px 0 0 var(--hot); }

/* YOUR PINNED SIX, IN ORDER — the Workshop's arranger, console-packs.js. Its
   own compact list rather than the shelf's cards made draggable: a pack card
   is already a drag source to Tonight, and a second drag purpose on the same
   element is two operations fighting over one dragstart. */
.pin-arranger { margin: 4px 0 14px; }
.pin-arranger-tag { margin-bottom: 6px; }
.pin-arranger-rows { display: flex; flex-direction: column; gap: 2px; }
.pin-row {
  display: flex; align-items: center; gap: 8px;
  padding: 4px 6px; border-radius: var(--r-field);
}
.pin-row-title { flex: 1 1 auto; font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pin-row-off { flex: 0 0 auto; line-height: 1; padding: 2px 8px; }

/* WHAT THEY ARE PLAYING FOR, on the lobby. Big enough to read from the back
   while people are still finding a table, and quieter than the join steps —
   the prize is the reason to stay, joining is the thing to do first. */
.lobby-prize {
  margin-top: 2.4vh; display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.6em;
  font-size: 2.6vh; font-weight: 700;
}
.lobby-prize .lp-label {
  font-size: 2vh; font-weight: 800; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--ink-dim);
}
.lobby-prize .lp-one { white-space: nowrap; }

/* WHAT THE ROOM ASKED FOR, on the console. Yes and No on every row, because
   the triage IS the feature: a queue that shrinks as you work it costs a
   fraction of one that only grows. */
.ask-rows { display: grid; gap: 8px; margin-top: 12px; }
.ask-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  border: 1px solid var(--panel-line); border-radius: var(--r-card);
  background: rgba(255, 255, 255, 0.04); padding: 10px 12px;
}
.ask-row > * { min-width: 0; }
.ask-what { flex: 1 1 200px; }
.ask-do { display: flex; gap: 8px; flex: 0 0 auto; }
.ask-count {
  margin-left: 8px; padding: 2px 8px; border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.1); font-size: var(--fs-tag); font-weight: 800;
}
.asks-kept-head {
  margin-top: 14px; font-weight: 800; letter-spacing: 0.02em;
  color: var(--ink);
}
.ask-rows.kept .ask-row { border-color: rgba(63, 219, 133, 0.35); }

/* ASK FOR A ROUND, on the phone at the end of the night. One line and one
   button: it is a theme, not a comment box, and the field is the guard rail —
   sixty characters holds "a reggae round" and will not hold a grievance. */
.ask-card {
  display: grid; gap: 8px; padding: 14px;
  border: 1px solid var(--panel-line); border-radius: var(--r-card);
  background: rgba(255, 255, 255, 0.04);
}
.ask-card .sub { font-size: var(--fs-note); font-weight: 800; color: var(--ink-dim); }
/* THREE BUTTONS RATHER THAN A BOX. Nobody types on a phone in a pub, and a
   tap is a vote that can be counted — where a text box collects opinions of
   wildly varying use from whoever could be bothered. It also means nothing a
   stranger writes ever reaches the quizmaster, which is what removes the
   moderation question rather than managing it. */
.ask-ideas { display: grid; gap: 8px; }
.ask-idea {
  padding: 14px; font: inherit; font-size: var(--fs-field); font-weight: 800;
  text-align: left; cursor: pointer; color: var(--ink);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--panel-line); border-radius: var(--r-field);
}
.ask-idea:hover { border-color: var(--hot); }
/* The one they chose, in the account's own colours — this is a highlight,
   which is the half of the scheme that follows the quizmaster. */
.ask-idea.on {
  color: #12000c; border-color: transparent;
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
}
.ask-card.ask-done { border-color: rgba(63, 219, 133, 0.4); }

/* ---- connection warning */
.conn-warn {
  position: fixed;
  bottom: 2vh; left: 50%;
  transform: translateX(-50%);
  z-index: 50;
  padding: 1vh 2vw;
  border-radius: var(--r-pill);
  background: var(--bad);
  color: #fff;
  font-weight: 800;
  font-size: 2vh;
  box-shadow: var(--shadow);
}

/* ==================================================================== PHONE
   The player's own phone. One decision per screen, thumb-sized targets, and
   no scrolling during a question — twenty seconds is not long enough to be
   hunting for a button. */

body.phone {
  overflow: auto;
  /* Through the same variables as the projector, or a themed night leaves the
     phones looking like an ordinary one — and the two screens being the same
     night is the whole point. */
  background:
    radial-gradient(110% 60% at 20% 0%, var(--glow-1) 0%, transparent 60%),
    radial-gradient(110% 60% at 80% 100%, var(--glow-2) 0%, transparent 60%),
    var(--bg);
  padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}

.phone .wrap {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  padding: 16px;
  gap: 14px;
  max-width: 560px;
  margin: 0 auto;
}

.phone .head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  font-size: var(--fs-body);
}
.phone .head .team { font-weight: 800; font-size: var(--fs-lead); }
.phone .head .score { font-weight: 900; font-size: var(--fs-sub); color: var(--gold); font-variant-numeric: tabular-nums; }

.phone .body { flex: 1 1 auto; display: flex; flex-direction: column; justify-content: center; gap: 16px; }

.phone h1 { font-size: 30px; line-height: 1.05; margin: 0; font-weight: 900; letter-spacing: -0.02em; }
.phone h2 { font-size: var(--fs-title); margin: 0; font-weight: 800; }
.phone p { font-size: var(--fs-lead); line-height: 1.45; margin: 0; color: var(--ink-dim); }
/* `.tiny` HAD NO SIZE ON THE PHONE AT ALL, and it is the one page where the
   class is used and never defined — the control view says 12px, the console
   12.5px, and here it fell through to body copy. So "spelling does not count"
   under the alphabet keyboard, and the camera's own notes, were coming out as
   big as the question. 13px rather than the console's 12.5: this is read in a
   dark room on a phone held at arm's length, not at a laptop. */
.phone .tiny { font-size: var(--fs-note); line-height: 1.45; color: var(--ink-dim); }
/* Except the camera warning, which is the house exception — warnings and money
   are read once, at a moment that matters, and being quiet there costs
   somebody something real. */
.phone .cam-warn { font-size: var(--fs-body); }

.phone input[type="text"] {
  width: 100%;
  padding: 18px 16px;
  font-size: var(--fs-sub);
  font-weight: 700;
  border-radius: var(--r-card);
  border: 2px solid var(--panel-line);
  background: rgba(255, 255, 255, 0.08);
  color: var(--ink);
}
.phone input[type="text"]:focus { outline: none; border-color: var(--hot); }

.phone .btn {
  display: block;
  width: 100%;
  padding: 18px 16px;
  font-size: var(--fs-head);
  font-weight: 800;
  border-radius: var(--r-card);
  border: none;
  color: #12000c;
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.phone .btn:disabled { opacity: 0.5; }
.phone .btn.ghost { background: var(--panel); border: 2px solid var(--panel-line); color: var(--ink); }

/* ---- answer buttons */
/* The four buttons share whatever height is going, so they are enormous thumb
   targets and nobody has to aim in a dark pub. */
/* Two columns, in the same order and the same colours as the projector.
   A player looks up at the big screen, decides "the pink one, bottom left",
   and looks down at their phone — so the phone has to be the same picture, or
   they have to re-read four options against the clock. It was a single column,
   which meant the same four answers in a different arrangement on each screen. */
.answers {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  flex: 1 1 auto;
  grid-auto-rows: 1fr;
  min-height: 0;
}
.answer-btn {
  /* The letter above the text rather than beside it. Beside it costs 42 of the
     ~140 pixels a half-width cell has on the smallest phone, which was enough
     to break "Christmas" across two lines mid-word. The letter stays top left,
     the same letter and the same colour as the projector. */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 12px;
  border-radius: var(--r-card);
  border: 2px solid var(--panel-line);
  background: var(--panel);
  color: var(--ink);
  /* Smaller than the single column was, because half the width has to hold
     "I Wish It Could Be Christmas Everyday" without turning it into a column
     of single words. Still the biggest thing on the phone. */
  font-size: var(--fs-field);
  font-weight: 800;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 120ms, opacity 200ms, border-color 200ms, background 200ms;
  min-height: 72px;
  overflow: hidden;
}
/* break-word, not anywhere: only split a word that cannot fit a line on its
   own, so "Christmas" wraps whole instead of coming apart. */
.answer-btn .text { width: 100%; overflow-wrap: break-word; line-height: 1.22; }
.answer-btn:active { transform: scale(0.98); }
.answer-btn .letter {
  flex: 0 0 auto;
  width: 32px; height: 32px;
  display: grid; place-items: center;
  border-radius: var(--r-field);
  font-weight: 900;
  color: #0a0a12;
  font-size: var(--fs-field);
}
.answer-btn:nth-child(1) .letter { background: var(--a); }
.answer-btn:nth-child(2) .letter { background: var(--b); }
.answer-btn:nth-child(3) .letter { background: var(--c); }
.answer-btn:nth-child(4) .letter { background: var(--d); }
.answer-btn:nth-child(5) .letter { background: var(--e); }
.answer-btn:nth-child(6) .letter { background: var(--f); }

.answer-btn.chosen { border-color: var(--ink); background: rgba(255, 255, 255, 0.16); }
.answer-btn.faded { opacity: 0.35; }
.answer-btn.right { border-color: var(--good); background: rgba(47, 224, 122, 0.22); }
.answer-btn.wrong { border-color: var(--bad); background: rgba(255, 77, 94, 0.2); }
.answer-btn[disabled] { cursor: default; }

/* ---- phone timer */
.phone .timer {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 12px;
}
.phone .timer .bar { flex: 1 1 auto; height: 10px; border-radius: var(--r-pill); background: rgba(255,255,255,0.12); overflow: hidden; }
.phone .timer .bar span { display: block; height: 100%; width: 100%; transform-origin: left center; background: linear-gradient(90deg, var(--good), var(--gold) 55%, var(--hot)); }
.phone .timer .num { font-size: 26px; font-weight: 900; font-variant-numeric: tabular-nums; min-width: 1.8em; text-align: right; }
.phone .timer .num.urgent { color: var(--bad); }

/* ---- the question, on the phone, ONLINE ONLY
 *
 * In a pub this does not exist: rule 8 keeps the words off the phone so the
 * room looks up at the projector. Online there is no projector, so this is the
 * only place the question is — which is why it is the biggest type on the
 * screen after the clock, and why it is centred rather than tucked under a
 * label like the counter it replaced.
 *
 * It is capped and scrollable rather than allowed to grow: a long question
 * must never push the OPTIONS below the fold on a 320px phone, because a
 * question you can read and cannot answer is worse than the other way round.
 */
.phone .online-q { flex: 0 0 auto; display: grid; gap: 4px; text-align: center; min-width: 0; }
.phone .online-q-num { font-size: var(--fs-tag); letter-spacing: 0.06em; text-transform: uppercase; }
.phone .online-q-text {
  font-size: var(--fs-lead);
  font-weight: 800;
  line-height: 1.3;
  max-height: 34vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Long words — a band name, a URL — must wrap rather than widen the page.
     A grid child's default minimum is its min-content, which is how one long
     word drags a whole phone sideways. */
  overflow-wrap: anywhere;
}

/* ---- result panel */
.result {
  border-radius: var(--r-card);
  padding: 22px 18px;
  text-align: center;
  border: 2px solid var(--panel-line);
  background: var(--panel);
}
.result.good { border-color: var(--good); background: rgba(47, 224, 122, 0.16); }
.result.bad { border-color: var(--bad); background: rgba(255, 77, 94, 0.14); }
.result .big { font-size: 34px; font-weight: 900; margin-bottom: 6px; }
.result .pts { font-size: 46px; font-weight: 900; color: var(--gold); font-variant-numeric: tabular-nums; }
.result .sub { color: var(--ink-dim); font-size: var(--fs-field); margin-top: 6px; }
/* An answer nobody can read is the same fault as an option nobody can read —
   a solid word pushed this card 44px wider than a 390px phone and took the
   answer off the right-hand edge. */
.result, .result .big, .result .sub, .result .sub strong { overflow-wrap: anywhere; min-width: 0; }
.result .bonus { display: inline-block; margin-top: 10px; padding: 6px 14px; border-radius: var(--r-pill); background: var(--gold); color: #1a1200; font-weight: 900; font-size: var(--fs-body); }

.mini-board { display: grid; gap: 8px; }
.mini-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px; border-radius: var(--r-card);
  background: var(--panel); border: 1px solid var(--panel-line);
  font-weight: 700; font-size: var(--fs-field);
}
.mini-row.you { border-color: var(--gold); background: rgba(255, 210, 63, 0.16); }
.mini-row .pos { width: 1.6em; color: var(--ink-dim); font-weight: 900; }
.mini-row .score { margin-left: auto; font-weight: 900; font-variant-numeric: tabular-nums; }

.phone .foot { flex: 0 0 auto; text-align: center; font-size: var(--fs-note); color: var(--ink-dim); padding-top: 8px; }
.phone .status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--good); margin-right: 6px; }
.phone .status-dot.off { background: var(--bad); }

/* ===================================================================== HOST
   Your own phone or laptop. Used one-handed, in the dark, while talking to a
   room — so: the primary action is always the same big button in the same
   place, and anything destructive is deliberately smaller and out of the way.

   Everything in here is private. The big screen never receives this data at
   all, so there is nothing to accidentally reveal. */

body.host { overflow: auto; background: #0a0a13; }
body.host::after { display: none; }

/* THE BAR IS FULL WIDTH; THE PANELS STAY A NARROW COLUMN.
   The wrap used to be 720px and centred with the bar inside it, so on a laptop
   the logo and the menu sat halfway across the screen while every other page
   had them hard left — which is the "the top section changes every time"
   complaint, and on the page you flip to most. The measure that mattered was
   never the bar's: it is that a cue you read at a glance should not be a metre
   wide, and that belongs to the panels. On a phone nothing moves at all. */
.host .wrap { max-width: 1000px; margin: 0 auto; padding: 12px 16px 128px; display: grid; gap: 12px; }
.host .wrap > main { max-width: 720px; width: 100%; margin: 0 auto; }
.host.bingo .wrap > main { max-width: 1280px; }
/* THE CONTROL VIEW SCROLLED SIDEWAYS ON A NARROW PHONE, and had done all along.
   A grid item's default minimum is its MIN-CONTENT, so one child that cannot
   shrink drags the whole column with it — here the Setup panel's pack dropdown,
   which sizes itself to its longest pack name. On a 320px iPhone SE that made
   every panel 342px wide and the sticky bar 366px, so the right-hand edge of
   the host's own screen was off the side of it: the clock, the end of each
   answer row, and the "..." that opens a player. `min-width: 0` lets the column
   be as narrow as the phone; the dropdown then has to be told it may shrink
   too, because a select will not go below its longest option on its own. */
.host .wrap > * { min-width: 0; }
.host select { max-width: 100%; }
/* Bingo's call sheet is scanned, not read, so it gets the whole screen. The
   quiz's panels keep the narrower measure — a cue you read at a glance should
   not be a metre wide. */


.host .bar {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  /* The console's exact numbers, so the logo and the menu begin at the same
     place on both — the row moving as you walk between pages is the whole
     complaint a menu exists to answer. */
  padding: 14px 22px; margin: -12px -16px 0;
  background: rgba(10, 10, 19, 0.94);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--panel-line);
  font-size: var(--fs-note);
}
.host .bar .where { font-weight: 800; }
.host .bar .clock { font-weight: 900; font-size: var(--fs-title); font-variant-numeric: tabular-nums; }
.host .bar .clock.urgent { color: var(--bad); }

.panel {
  border-radius: var(--r-card);
  background: var(--panel);
  border: 1px solid var(--panel-line);
  padding: 14px;
}
/* HEADINGS ARE NOT SHOUTED — a heading is told apart by BRIGHTNESS, not by
   being in capitals.

   The host's own line, and it is a brand decision rather than a taste one:
   *"the way I'm building this app, it needs to be as soft and friendly as
   possible… some people interpret capital letters as shouting."* He is right,
   and the tell is what these headings actually SAY: "Nothing here is being
   saved permanently" and "Nothing here for the night you have booked?" are
   whole sentences, and a sentence in capitals is somebody raising their voice
   at you about your own backups.

   So the differentiator moves from CASE to COLOUR, which is what he asked
   for and is the stronger signal anyway: the heading is full `--ink` against
   `--ink-dim` body text, one rung up the ladder, and heavy. Capitals plus dim
   grey was doing the job with the two weakest tools available.

   WHAT KEEPS ITS CAPITALS, deliberately, so this is a rule rather than a
   sweep: the PROJECTOR (every `vh`-sized rule — "WINNER" across a dark room
   is a title card, and at that size and tracking nobody reads it as a raised
   voice), the option letters A–F, the alphabet keyboard, and the small
   one-word BADGES — GOLD, PAID, NEW. A four-letter pill is a shape you
   recognise rather than a word you read, and those already carry their
   meaning in colour. */
.panel h3 {
  margin: 0 0 10px;
  font-size: var(--fs-note); letter-spacing: 0; color: var(--ink);
  font-weight: 800;
}
.panel.secret { border-color: rgba(255, 210, 63, 0.5); background: rgba(255, 210, 63, 0.09); }
.panel.secret h3 { color: var(--gold); }

.host .prompt { font-size: var(--fs-sub); font-weight: 800; line-height: 1.25; margin: 0 0 12px; }

.keylist { display: grid; gap: 7px; }
/* A key row is a button now: tapping it shows who picked that answer. It still
   has to look like a row rather than a control, because it is a list you read
   far more often than you tap. */
.keyrow {
  display: flex; align-items: center; gap: 10px;
  width: 100%; text-align: left;
  padding: 10px 12px; border-radius: var(--r-field);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid transparent;
  color: var(--ink); font-family: inherit;
  font-size: var(--fs-ctl); font-weight: 600;
  cursor: default;
}
/* AND THE HOST HAS TO BE ABLE TO READ THEIR OWN KEY. Same solid-word fault:
   the answer ran off the panel on a phone, which is the one screen they are
   holding while the room waits for them to say it. */
.keyrow { min-width: 0; overflow-wrap: anywhere; }
.keyrow > * { min-width: 0; }
.keyrow.has-who { cursor: pointer; }
.keyrow.has-who:hover { background: rgba(255, 255, 255, 0.09); }
.keyrow .caret {
  flex: 0 0 auto; margin-left: 4px;
  color: var(--ink-dim); font-size: var(--fs-note);
  transition: transform 140ms;
}
.keyrow .caret.open { transform: rotate(180deg); }
.keyrow .letter {
  width: 26px; height: 26px; flex: 0 0 auto;
  display: grid; place-items: center; border-radius: var(--r-field);
  font-weight: 900; font-size: var(--fs-note); color: #0a0a12; background: rgba(255,255,255,0.5);
}
.keyrow.is-correct { background: rgba(47, 224, 122, 0.2); border-color: var(--good); font-weight: 800; }
.keyrow.is-correct .letter { background: var(--good); }
.keyrow .n { margin-left: auto; color: var(--ink-dim); font-weight: 800; font-size: var(--fs-note); }

.cue { display: grid; gap: 4px; }
.cue .track { font-size: 26px; font-weight: 900; line-height: 1.1; }
.cue .artist { font-size: var(--fs-head); font-weight: 700; color: var(--gold); }
/* Auto-play did not start it. Gold rather than red: the question is up, the
   night is fine, and all this means is "use your thumb". */
.cue-failed {
  margin-top: 8px; padding: 8px 10px; border-radius: var(--r-field);
  background: rgba(255, 210, 63, 0.14);
  border: 1px solid rgba(255, 210, 63, 0.4);
  color: var(--gold); font-size: var(--fs-note); font-weight: 700;
}
.cue-failed .tiny { display: block; color: var(--ink-dim); font-weight: 400; margin-top: 2px; }
.cue .from { font-size: var(--fs-body); color: var(--ink-dim); }

.actions {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
  background: rgba(10, 10, 19, 0.96);
  backdrop-filter: blur(8px);
  border-top: 1px solid var(--panel-line);
  display: grid; gap: 8px;
  grid-template-columns: repeat(4, 1fr);
  max-width: 720px; margin: 0 auto;
}
.actions .primary {
  grid-column: 1 / -1;
  padding: 20px; font-size: var(--fs-sub); font-weight: 900;
  border: none; border-radius: var(--r-card); cursor: pointer;
  color: #12000c; background: linear-gradient(120deg, var(--hot), var(--hot-2));
  -webkit-tap-highlight-color: transparent;
}
.actions .primary:disabled { opacity: 0.45; }
/* 44px, THE TOUCH FLOOR — the launch bar's own rule, applied to the row that is
   actually pressed all night. These came out at 36px and the row menu at 27px. */
.actions .minor { min-height: 44px; }
/* AND EVERY OTHER PRESSABLE THING ON THIS PAGE, not just the bar at the bottom.
   The first pass scoped this to `.actions` and left twelve controls at 34-41px
   — Load, Reset scores, Clear everything, the three timer buttons, Switch off,
   the pack picker and the brand. Scoped to `body.host` so the console, which
   has its own 44px rule on the launch bar, is untouched. */
.host .minor, .host select, .host .brand { min-height: 44px; }
.host .brand { display: inline-flex; align-items: center; justify-content: center; min-width: 44px; }
.host select { padding-top: 0; padding-bottom: 0; }
/* The row menu is not in the action bar — it is on each player's line, and at
   31x27 it was the smallest control on the page. */
.host [data-act="menu"] { min-width: 44px; min-height: 44px; }
/* And the two full-width minors that came out at 35-36px. */
.host .minor.tidy, .host .report-q { min-height: 44px; }
.actions .minor {
  padding: 13px 6px; font-size: var(--fs-note); font-weight: 800;
  border-radius: var(--r-field); cursor: pointer;
  /* NEVER A FLAT GREY BOX, and never a wall of colour either — the host's two
     constraints, in his own words. So the face is DEPTH rather than a swatch:
     a top-lit surface, so a button reads as an object you press. The account's
     own colour arrives as the bottom EDGE only, which is enough to say the
     control belongs to this quizmaster without six of them in a row shouting.

     The underline is deliberately the bottom BORDER of a boxed control, not a
     bar under a bare label — which is how the tab bar marks the tab you are
     on. Same colour, different object: one has a box round it and one does
     not. Worth watching if the two ever end up side by side. */
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.04));
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-bottom: 2px solid var(--hot);
  color: var(--ink);
}
/* Destructive keeps RED on the edge, in every scheme. A quizmaster's colours
   change the personality of the app; they never change what red means. */
.actions .minor.danger { color: var(--bad); border-color: rgba(255, 77, 94, 0.4);
  border-bottom-color: var(--bad); }
/* …AND THE SAME SHAPE INSIDE THE CONTROL VIEW'S OWN PANELS.
   `.minor` only ever had a rule under `.actions` (the button bar) and under
   `.console`, so the Setup panel's Load, Download results and the two red
   ones were drawn by the BROWSER: a default grey button and a blue underlined
   link, on a page that is otherwise entirely this app's. Nothing was broken,
   which is why it survived — it just looked like somebody else's page had
   leaked in halfway down.
   Padding is the console's rather than the bar's: these sit in a panel with
   text around them, not in a row of thumb targets. */
.host .panel a.minor, .host .panel button.minor {
  text-decoration: none; display: inline-block;
  padding: 9px 13px; border-radius: var(--r-field); font-size: var(--fs-note); font-weight: 800;
  font-family: inherit; cursor: pointer;
  /* NEVER A FLAT GREY BOX, and never a wall of colour either — the host's two
     constraints, in his own words. So the face is DEPTH rather than a swatch:
     a top-lit surface, so a button reads as an object you press. The account's
     own colour arrives as the bottom EDGE only, which is enough to say the
     control belongs to this quizmaster without six of them in a row shouting.

     The underline is deliberately the bottom BORDER of a boxed control, not a
     bar under a bare label — which is how the tab bar marks the tab you are
     on. Same colour, different object: one has a box round it and one does
     not. Worth watching if the two ever end up side by side. */
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.04));
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-bottom: 2px solid var(--hot);
  color: var(--ink);
}
.host .panel button.minor.danger, .host .panel a.minor.danger {
  /* `border-color` FIRST and `border-bottom-color` after it — the shorthand
     overwrites all four sides, so written the other way round it wiped the
     full-strength red edge and left the four red buttons on the control view
     a fifth as loud as the identical ones in the console. Same trap as the
     pack tile's border and the one `.danger` rule at the foot of this sheet;
     third sighting. `.console button.minor.danger` twelve hundred lines below
     has always had this order — they read identically now. */
  color: var(--bad); border-color: rgba(255, 77, 94, 0.4);
  border-bottom-color: var(--bad);
}
.host .panel button.minor:disabled { opacity: 0.6; cursor: default; }
/* Back, as an arrow. It keeps a WHOLE CELL rather than being shrunk to fit
   the glyph: this is the undo, pressed in the dark with a mic in the other
   hand, and a small target is the wrong economy on the one control that
   fixes a mis-tap. The icon is centred in it and nothing else changes. */
.actions .back-btn { display: flex; align-items: center; justify-content: center; }

/* ---- "Your group" panel, My account — see console-account.js groupPanel() ---- */
.grp-seats { display: grid; gap: 6px; margin-top: 10px; }
.grp-seat-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 10px 12px; border-radius: var(--r-field);
  background: rgba(255, 255, 255, 0.05);
}
.grp-seat-remove { flex-shrink: 0; }

/* ---- "Your public page", My account — see console-account.js pagePanel().

   LABELS ABOVE THEIR CONTROLS, which is the launch bar's own rule: side by
   side, one of these two labels wraps and the other does not. One column on a
   phone, two from 560px, and `minmax(0, 1fr)` because a grid child defaults to
   `min-width: auto` and a text input's own width would push the panel wide. */
.book-fields { display: grid; gap: 10px; margin-top: 12px; }
.book-fields label { display: grid; gap: 4px; }
.book-fields input { width: 100%; }
@media (min-width: 560px) {
  .book-fields { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}

.plist { display: grid; gap: 6px; }
.prow {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 10px; border-radius: var(--r-field);
  background: rgba(255, 255, 255, 0.05);
  font-size: var(--fs-ctl);
}
.prow .pos { width: 1.8em; color: var(--ink-dim); font-weight: 900; font-size: var(--fs-note); }
.prow .nm { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.prow .sc { margin-left: auto; font-weight: 900; font-variant-numeric: tabular-nums; }
.prow .tick { color: var(--good); font-weight: 900; }
.prow .off { color: var(--bad); font-size: var(--fs-tag); }
/* The team a phone is sitting with, on a team night — the name the ROOM knows,
   under the name somebody typed into a handset. Dim and small: the row is
   still about the phone, this is only how to find it in a pub. */
.prow .ptm { display: block; font-weight: 600; font-size: var(--fs-tag); color: var(--ink-dim); }
/* A note, not an accusation — so it is gold rather than red. Red beside a name
   reads as "this one is cheating" and the app does not know that; it knows the
   phone went dark, which a call coming in also does. */
.prow .wandered-count {
  color: var(--gold);
  font-size: var(--fs-tag);
  font-weight: 700;
  white-space: nowrap;
}
.keywho.wandered b { color: var(--gold); }
.prow button {
  border: 1px solid var(--panel-line); background: rgba(255,255,255,0.06);
  color: var(--ink); border-radius: var(--r-field); padding: 5px 8px; font-size: var(--fs-note); font-weight: 800; cursor: pointer;
}

.host .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.host select, .host input[type="text"], .host input[type="number"] {
  padding: 10px; border-radius: var(--r-field); font-size: var(--fs-ctl);
  background: rgba(255,255,255,0.08); color: var(--ink); border: 1px solid var(--panel-line);
}
.host .tiny { font-size: var(--fs-mini); color: var(--ink-dim); }
.host a { color: var(--cool); }

.toast {
  position: fixed; left: 50%; bottom: 150px; transform: translateX(-50%);
  background: var(--ink); color: #0a0a12; font-weight: 800;
  padding: 10px 18px; border-radius: var(--r-pill); z-index: 60; font-size: var(--fs-body);
  box-shadow: var(--shadow);
}

/* ============================================================= ROUND TYPES
   A round is a plugin: it brings its own media block and its own option
   layout, and nothing else on the screen has to know about it. */

/* ---- round 2: whose face is this?
   The image starts hard in on the face and pulls back over the twenty
   seconds, so an early guess is worth more. The caption stays put the whole
   time: these are illustrations, and the screen says so. */
/* A 4:3 window in the middle of the screen rather than a full-width letterbox,
   so a portrait is framed properly instead of being cropped to a strip of
   cheek when it pulls back. */
.zoom-stage {
  flex: 1 1 auto;
  min-height: 0;
  margin-bottom: 2vh;
  display: flex;
  justify-content: center;
}
.zoom-frame {
  position: relative;
  height: 100%;
  aspect-ratio: 4 / 3;
  max-width: 100%;
  border-radius: var(--radius);
  overflow: hidden;
  background: #05050a;
  border: 2px solid var(--panel-line);
  box-shadow: var(--shadow);
}
.zoom-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  will-change: transform;
  /* No CSS transition: the zoom is redrawn every frame from the server's
     clock, so it stays in step with the countdown rather than drifting. */
}
.zoom-caption {
  position: absolute;
  left: 1vw;
  bottom: 1.2vh;
  padding: 0.6vh 1.1vw;
  border-radius: var(--r-pill);
  background: rgba(0, 0, 0, 0.66);
  border: 1px solid rgba(255, 255, 255, 0.28);
  font-size: 1.7vh;
  font-weight: 700;
  letter-spacing: 0.03em;
  color: #fff;
  backdrop-filter: blur(4px);
}

/* ---- round 3: name that intro
   Nothing on this screen names the track — the host is reading the same
   screen the room is. It is a visual only. */
.intro-visual {
  flex: 1 1 auto;
  min-height: 0;
  margin-bottom: 2vh;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.7vw;
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.03);
  border: 2px solid var(--panel-line);
}
.intro-visual i {
  display: block;
  width: 1vw;
  height: 20%;
  border-radius: var(--r-pill);
  background: linear-gradient(120deg, var(--hot), var(--cool));
  animation: eq 1.15s ease-in-out infinite alternate;
  animation-delay: calc(var(--i) * -70ms);
}
@keyframes eq {
  from { height: 7%; opacity: 0.55; }
  to   { height: 72%; opacity: 1; }
}

/* With a big picture or visual above them, the options go across in one row
   so the media keeps the height it needs. */
.type-image .options,
.type-intro .options {
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: auto;
  flex: 0 0 auto;
}
.type-image .option,
.type-intro .option {
  font-size: 2.7vh;
  padding: 1.8vh 1vw;
  gap: 0.8vw;
}
.type-image .q-prompt,
.type-intro .q-prompt {
  font-size: 3.6vh;
  margin-bottom: 1.6vh;
}
.type-image .option .tally,
.type-intro .option .tally { display: none; }

/* No options at all on a breakout question — the grid would otherwise sit
   there empty, still claiming the flexible space below the prompt. */
.type-breakout .options { display: none; }

/* =================================================================== EDITOR
   Where you check every question before the gig. Built for a laptop and a cup
   of tea the afternoon before, not for use in a dark room.

   The most important thing on this screen is that the correct answer is
   obvious at a glance, because the whole point of this page is catching the
   one question the AI got wrong. */

body.editor { overflow: auto; background: #0b0b14; }
body.editor::after { display: none; }

.editor .wrap { max-width: 1000px; margin: 0 auto; padding: 0 16px 120px; }

.editor .topbar {
  position: sticky; top: 0; z-index: 20;
  margin: 0 -16px 18px;
  padding: 14px 22px;
  background: rgba(11, 11, 20, 0.95);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--panel-line);
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
}
.editor .topbar h1 { font-size: var(--fs-head); margin: 0; font-weight: 900; }
.editor .topbar .spacer { flex: 1 1 auto; }

.editor button, .editor select, .editor input, .editor textarea {
  font-family: inherit;
  font-size: var(--fs-ctl);
  color: var(--ink);
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid var(--panel-line);
  border-radius: var(--r-field);
  padding: 9px 11px;
}
.editor textarea { width: 100%; resize: vertical; line-height: 1.45; }
.editor input:focus, .editor textarea:focus, .editor select:focus { outline: 2px solid var(--hot); outline-offset: 1px; }
.editor button { cursor: pointer; font-weight: 700; }
.editor button:hover { background: rgba(255, 255, 255, 0.14); }
.editor button.go {
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  border: none; color: #12000c; font-weight: 900; padding: 11px 20px;
}
.editor button.go:disabled { opacity: 0.5; cursor: default; }
.editor button.danger { color: var(--bad); border-color: rgba(255, 77, 94, 0.4); }
.editor button.small { padding: 5px 9px; font-size: var(--fs-note); }

.round-block { margin-bottom: 26px; }
.round-head {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 14px 16px;
  border-radius: var(--r-card) 14px 0 0;
  background: linear-gradient(120deg, rgba(255, 46, 136, 0.2), rgba(75, 216, 255, 0.1));
  border: 1px solid var(--panel-line);
  border-bottom: none;
}
.round-head input.title { flex: 1 1 240px; font-weight: 800; font-size: var(--fs-lead); }
.round-head .type-tag {
  padding: 4px 11px; border-radius: var(--r-pill); font-size: var(--fs-tag); font-weight: 900;
  letter-spacing: 0.12em; text-transform: uppercase; background: rgba(0,0,0,0.35);
}

.qcard {
  border: 1px solid var(--panel-line);
  border-top: none;
  background: rgba(255, 255, 255, 0.03);
  padding: 16px;
}
.qcard:last-child { border-radius: 0 0 14px 14px; }
.qcard.flagged { background: rgba(255, 77, 94, 0.09); }

/* SOMEBODY IS PLAYING THIS PACK. Gold, not red: nothing is wrong and nothing
   is being destroyed. It is a fact you need before deciding — the same
   treatment "not invoiced" and a venue clash already get. Red is kept for the
   one question actually on a screen, so that when red appears it means
   something. */
.in-play {
  border: 1px solid var(--gold);
  border-radius: var(--r-card);
  background: rgba(255, 199, 61, 0.10);
  color: var(--ink);
  padding: 12px 14px;
  margin-bottom: 12px;
  font-size: var(--fs-note);
}
.in-play strong { display: block; margin-bottom: 2px; }

/* AND THE ONE QUESTION A ROOM IS LOOKING AT. Changing this changes it
   mid-clock in front of people, with the answer key. A left bar rather than a
   full wash, because the card is full of text boxes that still have to be
   readable — and because `.flagged` already owns the red background, and two
   red fills would say the same thing twice for different reasons. */
.qcard.on-screen-now {
  border-left: 3px solid var(--bad);
  background: rgba(255, 77, 94, 0.06);
}
.on-screen-flag {
  color: var(--bad);
  font-size: var(--fs-mini);
  font-weight: 800;
  letter-spacing: 0.03em;
  margin-bottom: 8px;
}

/* `flex-wrap` because the row is QUESTION 1 · a type · Duplicate · Delete, and
   on a 320px phone the buttons on the end were 8px off the side rather than on
   a line of their own. */
.qcard .qtop { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
.qcard .qnum { font-size: var(--fs-mini); font-weight: 900; letter-spacing: 0.02em; color: var(--ink-dim); }
.qcard .qtools { margin-left: auto; display: flex; gap: 6px; }

.opts { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin-top: 11px; }
/* `min-width: 0` for the third time — a flex child will not go below its own
   MIN-CONTENT without it, so an option box refused to shrink and dragged the
   whole editor 313px off the side of a 320px phone. Same fault as the pack
   dropdown on the control view and the theme box on the console. */
.opt-row { display: flex; align-items: center; gap: 9px; min-width: 0; }
.opt-row input[type="text"] { flex: 1 1 auto; min-width: 0; }
/* And one column on a phone: two boxes plus a tickbox each is about 150px a
   side, which is not an option anybody can read let alone type into. */
@media (max-width: 560px) { .opts { grid-template-columns: 1fr; } }
.opt-row label {
  display: flex; align-items: center; gap: 7px;
  padding: 8px 11px; border-radius: var(--r-field); cursor: pointer;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--panel-line);
  font-weight: 900; font-size: var(--fs-note);
  white-space: nowrap;
}
.opt-row.is-correct label { background: rgba(47, 224, 122, 0.24); border-color: var(--good); }
.opt-row.is-correct input[type="text"] { border-color: var(--good); }

.extra-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 9px; margin-top: 11px; }
.extra-fields label { display: grid; gap: 5px; font-size: var(--fs-mini); color: var(--ink-dim); font-weight: 700; }
.extra-fields.cue { padding: 11px; border-radius: var(--r-field); background: rgba(255, 210, 63, 0.08); border: 1px solid rgba(255, 210, 63, 0.3); }
.extra-fields.cue > .note { grid-column: 1 / -1; font-size: var(--fs-mini); color: var(--gold); font-weight: 700; }

.problems {
  border-radius: var(--r-card); padding: 14px 16px; margin-bottom: 18px;
  background: rgba(255, 77, 94, 0.14); border: 1px solid var(--bad);
}
.problems ul { margin: 8px 0 0; padding-left: 20px; }
.problems li { margin-bottom: 4px; font-size: var(--fs-body); }
.problems.ok { background: rgba(47, 224, 122, 0.14); border-color: var(--good); }

.saved-flash {
  position: fixed; right: 20px; bottom: 20px; z-index: 60;
  padding: 12px 20px; border-radius: var(--r-pill); font-weight: 900;
  background: var(--good); color: #04240f; box-shadow: var(--shadow);
}

/* If a picture file is missing, say so plainly rather than showing the
   browser's broken-image icon on a projector. The host can read the question
   out and carry on. */
.zoom-missing { display: none; }
.zoom-frame.no-image .zoom-img { display: none; }
.zoom-frame.no-image .zoom-caption { display: none; }
.zoom-frame.no-image .zoom-missing {
  display: grid;
  place-items: center;
  height: 100%;
  font-size: 3vh;
  font-weight: 800;
  color: var(--ink-dim);
}

/* ================================================================== BINGO */

/* ---- big screen */
.bingo-play { display: flex; flex-direction: column; height: 100%; gap: 2vh; }

.now-playing {
  flex: 0 0 auto;
  text-align: center;
  padding: 3vh 2vw;
  border-radius: var(--radius);
  background: linear-gradient(120deg, rgba(255, 46, 136, 0.2), rgba(75, 216, 255, 0.12));
  border: 2px solid var(--panel-line);
}
.now-playing .np-label {
  font-size: 2vh; letter-spacing: 0.4em; text-transform: uppercase;
  color: var(--gold); font-weight: 800; margin-bottom: 1vh;
}
.now-playing .np-title { font-size: 8vh; font-weight: 900; line-height: 1; letter-spacing: -0.02em; }
.now-playing .np-artist { font-size: 3.4vh; color: var(--ink-dim); font-weight: 700; margin-top: 1vh; }
.pop-in { animation: popIn 420ms cubic-bezier(0.2, 1.2, 0.3, 1); }
@keyframes popIn {
  from { opacity: 0; transform: scale(0.94) translateY(1.5vh); }
  to { opacity: 1; transform: none; }
}

.called-wrap { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; gap: 1.4vh; }
.called-head { display: flex; gap: 1vw; align-items: center; font-size: 1.9vh; }
.called-head .pill b { font-size: 1.3em; margin-left: 0.3em; }
.called-head .tension.hot { background: var(--hot); border-color: var(--hot); color: #fff; }

.called-list {
  flex: 1 1 auto; min-height: 0; overflow: hidden;
  display: flex; flex-wrap: wrap; gap: 1vh 1vw; align-content: flex-start;
}
.called-chip {
  padding: 1vh 1.3vw; border-radius: var(--r-card);
  background: var(--panel); border: 1px solid var(--panel-line);
  display: flex; flex-direction: column; gap: 0.2vh;
}
.called-chip .ct { font-size: 2.2vh; font-weight: 800; }
.called-chip .ca { font-size: 1.6vh; color: var(--ink-dim); font-weight: 600; }
.called-chip.newest { border-color: var(--gold); background: rgba(255, 210, 63, 0.16); }

/* ---- player card */
.bingo-wrap { display: flex; flex-direction: column; gap: 12px; flex: 1 1 auto; position: relative; }
/* The camera button floats over the bottom right of the phone, and on a card
   it sat ON a square — 58 pixels of button over "Fireflies", which is a square
   nobody can tap and so a full house nobody can get. On a card it drops down
   beside the BINGO button, which is wide enough to lose its right-hand end and
   still be the easiest thing on the screen to hit. */
body.bingo-card .camera-btn { bottom: 12px; right: 12px; width: 52px; height: 52px; font-size: var(--fs-title); }
body.bingo-card .bingo-call { margin-right: 72px; }

.bingo-status {
  display: flex; justify-content: space-between; align-items: center;
  font-size: var(--fs-ctl); font-weight: 700; color: var(--ink-dim);
  /* IT WRAPS. Once somebody has won a prize this row carries THREE things —
     what is being played for, how far away they are, and what they already
     hold — and at 390px they ran 72px off the side with the last one gone
     entirely. That is the moment right after a prize, which is the moment
     everybody is looking at their phone. */
  flex-wrap: wrap; gap: 2px 10px; row-gap: 2px;
}
.bingo-status > * { min-width: 0; }
.bingo-status .away { font-weight: 900; color: var(--ink); }
.bingo-status .away.hot { color: var(--gold); }
.bingo-status .won { color: var(--good); font-weight: 900; }

/* A team can hold more than one prize at once — a line, then a full house —
   so this stacks rather than assuming there is ever only one. Empty most of
   the round, which is why it takes no space until a voucher exists. */
.bingo-vouchers { display: flex; flex-direction: column; gap: 10px; }
.bingo-vouchers:empty { display: none; }
/* ALL THE PRIZES ARE GONE — the shared break, drawn on every phone in the
   room rather than only on the winners'. GOLD, because it is about a prize
   being won: the trophy colour, which is fixed for everybody and does not
   follow the quizmaster's scheme. Not the account gradient, which would make
   it read as a button to press. */
.bingo-allgone {
  display: flex; flex-direction: column; gap: 2px;
  padding: 12px 14px; border-radius: var(--r-card, 14px);
  border: 1px solid color-mix(in srgb, var(--gold) 45%, transparent);
  background: color-mix(in srgb, var(--gold) 12%, transparent);
}
.bingo-allgone b { color: var(--gold); font-size: 16px; }
.bingo-allgone span { color: var(--ink-dim); font-size: 14px; }

.bingo-grid { display: grid; gap: 7px; flex: 1 1 auto; min-height: 0; }
.bingo-cell {
  position: relative;
  display: flex; flex-direction: column; justify-content: center; align-items: center;
  gap: 2px; padding: 6px 4px;
  border-radius: var(--r-card);
  border: 2px solid var(--panel-line);
  background: var(--panel);
  color: var(--ink);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 120ms, background 180ms, border-color 180ms;
  overflow: hidden;
  min-height: 0;
}
.bingo-cell:active { transform: scale(0.96); }
.bingo-cell .bt {
  font-size: 12.5px; font-weight: 800; line-height: 1.15; text-align: center;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.bingo-cell .ba { font-size: 10px; color: var(--ink-dim); font-weight: 600; text-align: center; }
.bingo-cell .btick {
  position: absolute; inset: 0;
  display: none; place-items: center;
  font-size: 40px; font-weight: 900; color: var(--good);
}
.bingo-cell.marked .bt, .bingo-cell.marked .ba { opacity: 0.28; }
.bingo-cell.marked .btick { display: grid; }
.bingo-cell.confirmed { border-color: var(--good); background: rgba(47, 224, 122, 0.18); }
/* Marked, but you have not played it yet — a gentle "are you sure". */
.bingo-cell.unconfirmed { border-color: var(--gold); background: rgba(255, 210, 63, 0.14); }
.bingo-cell.unconfirmed .btick { color: var(--gold); }

/* A 5x5 is twenty-five squares on a phone, so the type has to come down with
   the cell. At the sizes above, "Bootylicious" came out as "Bootyliciou" and
   "Irreplaceable" as "rreplaceabl" — a square you cannot read is a square you
   cannot mark, and on the night that is somebody missing a full house. Four
   lines rather than three, because the words are what matter here, not the
   tidiness of the grid. */
.bingo-grid.cols-5 { gap: 4px; }
.bingo-grid.cols-5 .bingo-cell { padding: 4px 2px; gap: 1px; border-radius: var(--r-field); border-width: 1.5px; }
.bingo-grid.cols-5 .bingo-cell .bt { font-size: 10.5px; line-height: 1.1; -webkit-line-clamp: 4; }
.bingo-grid.cols-5 .bingo-cell .ba { font-size: 8.5px; }
.bingo-grid.cols-5 .bingo-cell .btick { font-size: 30px; }
/* Three across can afford to be read from further away — whether that is nine
   big squares or a strip of twenty-four. */
.bingo-grid.cols-3 .bingo-cell .bt { font-size: 15px; }
.bingo-grid.cols-3 .bingo-cell .ba { font-size: 12px; }
/* A strip is 3 across and 8 down: taller than the screen at square-ish cells,
   so the rows are short and wide rather than boxes. */
.bingo-grid.cols-3.strip .bingo-cell { min-height: 0; }
.bingo-grid.cols-3.strip .bingo-cell .bt { font-size: 12.5px; -webkit-line-clamp: 2; }
.bingo-grid.cols-3.strip .bingo-cell .ba { font-size: 10px; }
.bingo-grid.cols-4.strip .bingo-cell .bt { font-size: 11.5px; }
.bingo-grid.cols-4.strip .bingo-cell .ba { font-size: 9.5px; }

.bingo-call {
  flex: 0 0 auto;
  background: linear-gradient(120deg, var(--good), var(--cool));
  font-size: var(--fs-title); letter-spacing: 0.06em;
}
.bingo-call:disabled { background: var(--panel); color: var(--ink-dim); border: 2px solid var(--panel-line); font-size: var(--fs-ctl); letter-spacing: 0; }

/* What this phone has already won, kept beside what is being played for now. */
.bingo-status .yours {
  flex: 1 0 100%;
  font-size: var(--fs-note); font-weight: 800; color: var(--good);
}

.bingo-flash {
  position: fixed; left: 16px; right: 16px; bottom: 84px; z-index: 50;
  padding: 16px; border-radius: var(--r-card); text-align: center;
  font-weight: 900; font-size: var(--fs-lead);
  animation: popIn 300ms cubic-bezier(0.2, 1.2, 0.3, 1);
}
.bingo-flash.good { background: var(--good); color: #04240f; }
.bingo-flash.bad { background: var(--gold); color: #241a00; }

/* ---- the call sheet
   A grid, not a list: forty tracks down a single column is three screenfuls
   with the middle of every row empty, and you are looking for one song with a
   record playing. Boxes wrap to fit whatever screen it is on — seven across a
   laptop, two across a phone — so most of the sheet is in front of you.
   No inner scroller: the page scrolls, so the whole grid can be as tall as it
   needs and you are never scrolling a box inside a box. */
.trackgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
  gap: 6px;
}
/* The caller's own filter, on the page they drive the room from — it was 39px. */
#trackFilter { min-height: 44px; }

.trackbox {
  display: grid;
  grid-template-columns: 16px 1fr;
  grid-template-areas: 'tick title' '. artist';
  align-content: center;
  gap: 0 6px;
  min-height: 52px;
  padding: 8px 10px;
  border-radius: var(--r-field);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid transparent;
  color: var(--ink); text-align: left; cursor: pointer; width: 100%;
}
.trackbox .tick { grid-area: tick; color: var(--good); font-weight: 900; font-size: var(--fs-body); }
.trackbox .tt {
  grid-area: title; font-weight: 700; font-size: var(--fs-ctl); line-height: 1.2;
  /* Two lines at most, so no box is twice the height of its neighbours and the
     grid stays a grid. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.trackbox .ta {
  grid-area: artist; color: var(--ink-dim); font-size: var(--fs-mini); line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.trackbox.called { background: rgba(47, 224, 122, 0.16); border-color: var(--good); }
.trackbox.called .tt { opacity: 0.65; text-decoration: line-through; }
@media (max-width: 430px) {
  .trackgrid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
}
.prow .sc.hot { color: var(--gold); }

/* ================================================================ CONSOLE */

/* THE CONSOLE WAS THE ONE SURFACE IN THIS APP WITH ITS AMBIENCE SWITCHED OFF.
   `background: #0b0b14` and `::after { display: none }` — so the corner washes
   and the two drifting blobs that set the mood on the projector, the phone and
   the join page were turned off on the page a quizmaster spends the most time
   in. Flat near-black with grey cards, the account's colours reaching one
   button a screen, and the most saturated thing on the page the destructive
   red. That is what "the colours are not singing" was.
   Fixed rather than attached, so it does not slide about behind the content
   while you scroll a long tab — the drifting blobs are for a screen somebody
   is looking AT, and a console is a page you work in. */
body.console {
  overflow: auto;
  background:
    radial-gradient(1100px 720px at 8% -8%, var(--glow-1), transparent 62%),
    radial-gradient(900px 620px at 100% 2%, var(--glow-2), transparent 58%),
    radial-gradient(700px 500px at 70% 100%, var(--drift-1), transparent 60%),
    #07070e;
  background-attachment: fixed;
  /* ONE SURFACE FOR EVERY CARD ON THIS PAGE, in the account's own colour —
     see the note on `--surf-1`. Overriding the two tokens rather than editing
     twenty-nine rules is what makes this one decision instead of twenty-nine
     chances to disagree, and it is why the four greys existed in the first
     place. */
  --panel: var(--surface);
  --panel-line: var(--surf-line);
}
body.console::after { display: none; }

/*
 * THE GAUNTLET IS THE CURSOR NOW, EVERYWHERE — the quill was dropped:
 * *"drop the quill - I'm happy with the open hand and then the grabby hand
 * when grabbing stuff."* One glove doing both jobs is genuinely simpler than
 * a quill for pointing plus a glove for dragging, and it was the host's own
 * call once he had lived with both for a session.
 *
 * CONSOLE ONLY — My account, Console, Workshop, Post gig, Owner page. Never
 * the projector or a phone: those screens live by instant, high-contrast
 * readability in a dark room, which a decorative flourish works against.
 *
 * DRAWN AND JUDGED AT 32px, the only size a cursor is ever seen at. Three
 * silhouettes were rendered for the fist and the first two were unreadable
 * at that size; what survives the shrink is CHUNKY separated fingers, a
 * thumb clearly out to one side, and a cuff cut back to a band rather than a
 * full skirt, which at 32px is just a pale blob with the hand sat on it.
 *
 * A CURSOR CANNOT FOLLOW THE ACCOUNT'S COLOURS and nothing here should try:
 * it is a static image file, so it cannot read `--hot` the way a page
 * element can. Cream and gold, on purpose rather than by omission.
 *
 * THE INDEX FINGER READS AS A PRESS, NOT JUST A POINT — asked for directly,
 * on 21 August 2026, after four lengths were rendered side by side and
 * judged at 32px: *"D for the unclick and A on click."* Hovering shows the
 * finger reaching out (D, the longest of the four); the moment a button is
 * actually pressed the finger comes back down to its original, shorter
 * length (A) — the same gesture a real fingertip makes meeting a surface,
 * reaching before contact and flattening on it. Only that one finger's
 * length changes between the two; every other path is identical, and the
 * closed fist while dragging is untouched — this is about clicking, not
 * grabbing.
 */
/* ---- AND THE PUBLIC GALLERY WEARS IT TOO, asked for on 31 August 2026:
   *"can we keep the mouse clicker from the app for this as well?"*
 *
 * It is still never the projector and never a phone — the decision's own
 * prohibition, and untouched. The gallery is neither: it is a page somebody
 * reads on a laptop, and it is the one public page that should feel like the
 * app rather than like a stock web page. On a phone it costs nothing, because
 * a touch screen has no cursor to draw. */
body.console, body.console *,
body.gallery, body.gallery * {
  cursor:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cg stroke='%233a2a16' stroke-width='1.2' stroke-linejoin='round' stroke-linecap='round'%3E%3Cpath d='M8.8 21.6 L24.4 21.6 L27 26.4 Q23.53 28.3 20.07 26.1 Q16.6 28.3 13.13 26.1 Q9.67 28.3 6.2 26.4 Z' fill='%23e6d5ad'/%3E%3Cpath d='M7.4 23.5 L25.8 23.5' stroke='%23b8923f' stroke-width='1.15'/%3E%3Cpath d='M9.8 13.6 Q4.8 14 3.8 16.8 Q3 19.6 6 20.4 Q8.6 21 10.6 20.4 Z' fill='%23eddfbc'/%3E%3Cpath d='M8.8 21.6 L8.8 5 Q8.8 3.2 10.62 3.2 Q12.45 3.2 12.45 5 L12.45 11.2 Q12.7 12.15 12.95 11.2 L12.95 8 Q12.95 6.3 14.65 6.3 Q16.35 6.3 16.35 8 L16.35 11.2 Q16.6 12.15 16.85 11.2 L16.85 8.5 Q16.85 6.8 18.5 6.8 Q20.15 6.8 20.15 8.5 L20.15 11.2 Q20.4 12.15 20.65 11.2 L20.65 10.2 Q20.65 8.6 22.5 8.6 Q24.4 8.6 24.4 10.2 L24.4 21.6 Z' fill='%23f6ecd6'/%3E%3Cpath d='M12.7 13 L12.7 14.9 M16.6 13 L16.6 15 M20.4 13 L20.4 14.9' stroke='%23c9b183' stroke-width='0.95' fill='none'/%3E%3C/g%3E%3C/svg%3E")
    16 15, pointer !important;
}
/*
 * THE PRESS — an ordinary `:active`, at the SAME open-hand shape the default
 * used before this change, so clicking anything briefly draws the finger
 * back to where it always was. Broad on purpose (`body.console *:active`,
 * the same reach as the default rule above) rather than scoped to buttons
 * and links only — the default cursor is drawn just as broadly, and a click
 * that did not press-and-release would be a rule with a gap in it.
 *
 * WINS OVER THE DEFAULT ON SPECIFICITY ALONE, no source-order dependence:
 * `body.console *:active` carries one more pseudo-class than `body.console
 * *`, so it is not sensitive to which of the two rules is written first.
 * The closed fist below is more specific again (`[draggable="true"]:active`
 * carries an attribute selector on top), so a press that IS a drag still
 * shows the fist rather than this — the ordering here only has to make
 * sense to a reader, not to the cascade.
 */
body.console *:active,
body.gallery *:active {
  cursor:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cg stroke='%233a2a16' stroke-width='1.2' stroke-linejoin='round' stroke-linecap='round'%3E%3Cpath d='M8.8 21.6 L24.4 21.6 L27 26.4 Q23.53 28.3 20.07 26.1 Q16.6 28.3 13.13 26.1 Q9.67 28.3 6.2 26.4 Z' fill='%23e6d5ad'/%3E%3Cpath d='M7.4 23.5 L25.8 23.5' stroke='%23b8923f' stroke-width='1.15'/%3E%3Cpath d='M9.8 13.6 Q4.8 14 3.8 16.8 Q3 19.6 6 20.4 Q8.6 21 10.6 20.4 Z' fill='%23eddfbc'/%3E%3Cpath d='M8.8 21.6 L8.8 8.5 Q8.8 6.8 10.62 6.8 Q12.45 6.8 12.45 8.5 L12.45 11.2 Q12.7 12.15 12.95 11.2 L12.95 8 Q12.95 6.3 14.65 6.3 Q16.35 6.3 16.35 8 L16.35 11.2 Q16.6 12.15 16.85 11.2 L16.85 8.5 Q16.85 6.8 18.5 6.8 Q20.15 6.8 20.15 8.5 L20.15 11.2 Q20.4 12.15 20.65 11.2 L20.65 10.2 Q20.65 8.6 22.5 8.6 Q24.4 8.6 24.4 10.2 L24.4 21.6 Z' fill='%23f6ecd6'/%3E%3Cpath d='M12.7 13 L12.7 14.9 M16.6 13 L16.6 15 M20.4 13 L20.4 14.9' stroke='%23c9b183' stroke-width='0.95' fill='none'/%3E%3C/g%3E%3C/svg%3E")
    16 15, pointer !important;
}
/*
 * `!important` because dozens of scattered `cursor: pointer` rules on
 * classes this page shares with host.js/play.js (buttons, chat, tabs) are
 * each individually more specific than a body selector and would otherwise
 * keep the ordinary hand — see the note by the nine other !importants in
 * this file. Three cursor states still carry real meaning, so they are
 * restored right after at the same weight: the fist while actually
 * dragging, a disabled control, and a text field's insertion point. The
 * OPEN hand needs no separate rule of its own any more — it is the default
 * now, so a draggable element already shows it without being named.
 */
body.console [draggable="true"]:active,
body.console .drag-grip:active,
body.console .dragging {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cg stroke='%233a2a16' stroke-width='1.2' stroke-linejoin='round' stroke-linecap='round'%3E%3Cpath d='M8.8 21.2 L24.4 21.2 L27 26 Q23.53 27.9 20.07 25.7 Q16.6 27.9 13.13 25.7 Q9.67 27.9 6.2 26 Z' fill='%23e6d5ad'/%3E%3Cpath d='M7.4 23.1 L25.8 23.1' stroke='%23b8923f' stroke-width='1.15'/%3E%3Cpath d='M9.6 15.2 Q6.4 15.6 6.1 18 Q5.9 20.3 8.6 20.6 L10.8 20.6 Z' fill='%23eddfbc'/%3E%3Cpath d='M8.8 21.2 L8.8 14.4 Q8.8 10.9 10.7 10.7 Q12.5 10.5 12.7 11.9 Q12.9 10.2 14.7 10.2 Q16.4 10.2 16.6 11.7 Q16.8 10 18.6 10.1 Q20.2 10.2 20.4 11.7 Q20.7 10.4 22.3 10.7 Q24.4 11.2 24.4 14.2 L24.4 21.2 Z' fill='%23f6ecd6'/%3E%3Cpath d='M12.7 12.6 L12.7 14.8 M16.6 12.4 L16.6 14.9 M20.4 12.6 L20.4 14.8' stroke='%23c9b183' stroke-width='0.95' fill='none'/%3E%3C/g%3E%3C/svg%3E") 16 15, grabbing !important;
}
body.console button:disabled,
body.console [disabled],
body.gallery button:disabled,
body.gallery [disabled] { cursor: default !important; }
body.console input:not([type="checkbox"]):not([type="radio"]),
body.console textarea { cursor: text !important; }

/*
 * THE SCROLL SHADOW — one token, not a dozen rules, for the same reason
 * --panel/--panel-line above are one override rather than twenty-nine: a
 * single place to change it is a single chance to get it right.
 */
body.console { --scroll-shadow: inset 0 10px 14px -10px rgba(0, 0, 0, 0.5), inset 0 -10px 14px -10px rgba(0, 0, 0, 0.5); }
body.console .panel,
body.console .pack-card,
body.console .venue-card,
body.console .launchbar,
body.console .tier-panel,
/* :not()s so a live drag-drop indicator's own box-shadow is never fought —
   an element wearing .drop-before/.drop-after simply does not match this
   rule at all, rather than winning a specificity fight against it. */
body.console .lb-tile:not(.drop-before):not(.drop-after) {
  box-shadow: var(--scroll-shadow);
}

/*
 * THE SCROLL RODS — a turned dowel through the rolled top and bottom edge of
 * the page, poking out past it, in the account's own two colours.
 *
 * WHAT SELLS A CYLINDER IS THE SHADING, NOT THE SHAPE. The first attempt was a
 * pill with a highlight along its top edge and it read as a painted stripe: a
 * real rod is dark where it turns away at the very top, brightest a quarter of
 * the way down, then a long fall to a dark underside. Those five stops are the
 * whole illusion, and the white must stay LOW — pushed up, it bleaches the hue
 * out and the rod stops being the quizmaster's colour, which is the one thing
 * it was asked to be.
 *
 * ON `.panel` ALONE, and that is not laziness — `.launchbar` is only ever set
 * ALONGSIDE `.panel` in the markup, so naming both would give Tonight and the
 * two benches a doubled rod each.
 */
body.console .panel { position: relative; }
body.console .panel::before,
body.console .panel::after {
  content: '';
  position: absolute;
  /* 10px, against the wrap's own 16px of padding, so a rod can never be what
     pushes the page sideways on a phone. */
  left: -9px; right: -9px;
  height: 8px;
  border-radius: var(--r-pill);
  pointer-events: none;
  background:
    /* a catchlight on each turned knob, where a real one would take the light */
    radial-gradient(circle 4px at 4.5px 34%, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 70%),
    radial-gradient(circle 4px at calc(100% - 4.5px) 34%, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 70%),
    /* a dark rim low on each knob, so the ball reads as a sphere */
    radial-gradient(circle 6px at 5.5px 50%, rgba(0, 0, 0, 0) 55%, rgba(0, 0, 0, 0.42) 92%),
    radial-gradient(circle 6px at calc(100% - 5.5px) 50%, rgba(0, 0, 0, 0) 55%, rgba(0, 0, 0, 0.42) 92%),
    /*
     * THE LONG ROLL RECEDES AND THE ENDS KEEP THE COLOUR, which is both what
     * was asked for — *"little scroll endings… within the colours defined by
     * the account"* — and what a real one does: a stained dowel, brass at the
     * tips. Drawn evenly at full strength it came out brighter than Launch,
     * twice on every panel, which breaks the one-filled-gradient-per-screen
     * rule outright: the loudest thing on the console was a decoration nobody
     * can press. Darkening the MIDDLE rather than the whole rod keeps the
     * quizmaster's colour where the eye actually lands.
     */
    linear-gradient(90deg, rgba(0, 0, 0, 0) 7px, rgba(0, 0, 0, 0.5) 15px,
                    rgba(0, 0, 0, 0.5) calc(100% - 15px), rgba(0, 0, 0, 0) calc(100% - 7px)),
    /* the cylinder */
    linear-gradient(180deg, rgba(0, 0, 0, 0.6) 0%, rgba(255, 255, 255, 0.16) 24%,
                    rgba(0, 0, 0, 0.2) 46%, rgba(0, 0, 0, 0.5) 76%, rgba(0, 0, 0, 0.72) 100%),
    /* the account's own two colours, exactly as Launch takes them */
    linear-gradient(120deg, var(--hot), var(--hot-2));
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.45);
}
body.console .panel::before { top: -4px; }
body.console .panel::after { bottom: -4px; }

/*
 * AND THE KNOBS, where the browser can mask. A plain pill is the same width
 * end to end, which reads as a bar through the page rather than a rod with
 * finials on it. Masking lets the ends be a FATTER circle than the roll
 * between them — the reference silhouette — while the fill underneath stays
 * the account gradient, which an SVG data URI could never read.
 *
 * THE ROLL STOPS SHORT OF EACH KNOB, leaving a GROOVE. Three finial shapes
 * were rendered side by side — plain, groove, knob-plus-collar-ring — and
 * the groove won: it is the one turned-on-a-lathe detail that still reads
 * at 11px tall, where the collar ring is just a stutter.
 *
 * Behind `@supports` because without mask compositing the element would paint
 * as a full RECTANGLE, which is far worse than the pill it replaces — the same
 * guard, and the same reasoning, as the gradient hairline on the photo card.
 */
@supports (mask-composite: add) and (mask-image: radial-gradient(#000, #000)) {
  body.console .panel::before,
  body.console .panel::after {
    height: 11px;
    border-radius: 0;
    mask-image:
      radial-gradient(circle 5.5px at 5.5px 50%, #000 99%, transparent 100%),
      radial-gradient(circle 5.5px at calc(100% - 5.5px) 50%, #000 99%, transparent 100%),
      linear-gradient(#000, #000);
    mask-size: 100% 100%, 100% 100%, calc(100% - 26px) 7px;
    mask-position: 0 0, 0 0, center;
    mask-repeat: no-repeat;
    mask-composite: add;
  }
  body.console .panel::before { top: -5.5px; }
  body.console .panel::after { bottom: -5.5px; }
}
/* 1180 RATHER THAN 1000, AND THE TAB BAR IS WHY.
   Nine tabs come to a bit over 1000px, so in a 968px column one of them
   always wrapped — and a wrapped bar that is also STICKY costs a hundred and
   ten pixels of every screen instead of fifty, on a laptop that is 900 tall.
   Tightening the tabs enough to fit would have taken their side padding to
   8px, which is a cramped target for the one control you press most.
   The column was the cheaper thing to change and it was overdue anyway: on
   the 1440 screen this is driven from, 1000px left 440px of unused desk, the
   pack grid ran three across where four fit, and every panel was narrower
   than it needed to be. Nothing else in the app depends on the number. */
.console .wrap { max-width: 1180px; margin: 0 auto; padding: 0 16px 60px; }
/* Its height, written down because the sticky tab bar has to sit exactly
   under it and CSS cannot measure. Changed here and there together. */
.console { --topbar-h: 63px; }
.console .topbar {
  position: sticky; top: 0; z-index: 20;
  margin: 0 -16px 20px; padding: 14px 22px;
  background: rgba(11, 11, 20, 0.95); backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--panel-line);
  display: flex; align-items: center; gap: 14px;
}
.console .topbar h1 { font-size: var(--fs-head); margin: 0; font-weight: 900; }
.console .spacer { flex: 1 1 auto; }
.console .tiny { font-size: var(--fs-note); color: var(--ink-dim); }
/* The console's small buttons. Links and <button>s both, because the running
   panel mixes them — two links out to the other screens and one real action. */
/* `label.minor` is here because a file picker HAS to be a label wrapping a
   hidden `<input type=file>` — there is no way to style a file input itself,
   and nothing else opens the picker. Without it in this list the venue's
   "Change" came out as plain text sitting between two real buttons. */
.console a.minor, .console button.minor, .console label.minor {
  text-decoration: none; display: inline-block;
  padding: 9px 13px; border-radius: var(--r-field); font-size: var(--fs-note); font-weight: 800;
  font-family: inherit; cursor: pointer;
  /* NEVER A FLAT GREY BOX, and never a wall of colour either — the host's two
     constraints, in his own words. So the face is DEPTH rather than a swatch:
     a top-lit surface, so a button reads as an object you press. The account's
     own colour arrives as the bottom EDGE only, which is enough to say the
     control belongs to this quizmaster without six of them in a row shouting.

     The underline is deliberately the bottom BORDER of a boxed control, not a
     bar under a bare label — which is how the tab bar marks the tab you are
     on. Same colour, different object: one has a box round it and one does
     not. Worth watching if the two ever end up side by side. */
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.04));
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-bottom: 2px solid var(--hot);
  color: var(--ink);
}
.console button.minor.danger { color: var(--bad); border-color: rgba(255, 77, 94, 0.4);
  border-bottom-color: var(--bad); }
.console button.minor.danger:hover { background: rgba(255, 77, 94, 0.12); }
.console button.minor:disabled { opacity: 0.6; cursor: default; }
.console .panel { border-radius: var(--r-card); background: var(--panel); border: 1px solid var(--panel-line); padding: 16px; margin-bottom: 24px; }
/* The console's own override of the rule above — same change, same reason.
   See the note on `.panel h3`. */
.console .panel h3 { margin: 0 0 12px; font-size: var(--fs-note); letter-spacing: 0; color: var(--ink); font-weight: 800; }
.console .panel.running.live { border-color: var(--good); background: rgba(47, 224, 122, 0.1); }
.running-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.running-title { font-size: var(--fs-sub); font-weight: 900; }
.running-links { margin-left: auto; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
/* Where the game has got to. Same size as the line above it but not dimmed,
   because it is the line you came to this panel to read. */
.running-at { margin-top: 3px; font-size: var(--fs-note); font-weight: 700; color: var(--good); }
/* The way you actually run the night, so it is a button and not a footnote. */
.running-links .control-link {
  border: none; border-radius: var(--r-field); padding: 10px 16px;
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  color: #12000c; font-weight: 900; font-size: var(--fs-body);
  text-decoration: none; white-space: nowrap; cursor: pointer;
}

.game-section { margin-bottom: 34px; }
.game-head { display: flex; align-items: flex-end; gap: 14px; margin-bottom: 14px; }
.game-head h2 { margin: 0 0 3px; font-size: var(--fs-title); font-weight: 900; letter-spacing: -0.02em; }
/* A SECTION HEADING CARRIES THE ACCOUNT'S OWN COLOURS.
   The one thing that makes a page look designed rather than assembled, and it
   is safe where a button would not be: a heading is not something you press,
   so the gradient cannot be mistaken for "press this" — which is the reason
   that fill is otherwise rationed to one control a screen.
   Behind an `@supports`, and that guard is not optional: gradient text means
   TRANSPARENT text, so a browser without `background-clip` would draw every
   section heading on the console invisible. */
/* THREE STEPS AND NO MORE — the tab, a section in it, a panel in that.
   The tab's own heading is the biggest thing on the page and the only one
   that carries the account's colours; a section under it drops a rung and
   stays white, so it reads as part of the tab rather than as a rival to it;
   a panel keeps its small bold line (`.panel h3`). Without the demotion the
   Gigs tab printed "Gigs", "Headcount" and "Past gigs" at one size, which is
   three tabs as far as the eye is concerned. */
.console .tab-head { margin-bottom: 18px; }
/* The mark and the name are ONE object, so the mark cannot drift away from
   the word it belongs to when the head row grows a button. Baseline rather
   than centre: the mark is a square and the heading has a descender, so
   centring the boxes leaves the glyph sitting visibly low against the cap
   height it is meant to line up with. */
.console .tab-head-name { display: flex; align-items: center; gap: 10px; min-width: 0; }
.console .tab-head-mark { display: inline-flex; flex: 0 0 auto; }
.console .tab-head h2 { margin: 0; }
.console .game-section .game-head h2 { font-size: var(--fs-head); }
/* A SECTION HEADING CARRIES THE ACCOUNT'S OWN COLOURS.
   The one thing that makes a page look designed rather than assembled, and it
   is safe where a button would not be: a heading is not something you press,
   so the gradient cannot be mistaken for "press this" — which is the reason
   that fill is otherwise rationed to one control a screen.
   Behind an `@supports`, and that guard is not optional: gradient text means
   TRANSPARENT text, so a browser without `background-clip` would draw the
   heading on every tab invisible. */
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .console .tab-head h2 {
    background: linear-gradient(120deg, var(--hot), var(--hot-2));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}
.game-head a { margin-left: auto; }

/* The floor came down from 240px with the squares. At 240 the widened console
   column gives four cards a row, which as SQUARES is 284px each — a shelf of
   nine packs three rows deep, each one the size of a photograph. 176 puts six
   on a row at about the size of a tile in Tonight, which is the point of
   making them the same shape. */
/*
 * A PACK IS THE SHAPE OF THE HOLE IT GOES IN.
 *
 * Capped at 200px rather than `1fr`, because that is what a Tonight slot caps
 * at — stretched to fill, a card on a wide screen came out half as wide again
 * as the slot it was being dragged into, and the two read as different kinds
 * of object.
 *
 * **BUT NOT ON A PHONE, where the cap makes it look broken instead.** Below
 * 560px only one column fits, so a 200px card sat in a 358px row with 158px of
 * nothing beside it — which does not read as "the same size as the hole", it
 * reads as a card that failed to load. And there is nothing to match down
 * there anyway: the three Tonight slots still share one row on a phone, so a
 * slot is about 100px and no library card is ever going to be that. Matching
 * is a wide-screen job; filling the width is the phone's.
 */
/* MATCHED TO THE SLOTS AT 160px, chosen with the six-slot change: a pack has
   to look like the thing that goes in the hole, and that only holds while the
   two are the same width.

   `grid-auto-flow: dense` fixes what an OPEN card does to the row it is on.
   An open card is `grid-column: 1 / -1`, so it can only start in column one —
   which strands every shut card that would have shared its row, leaving one
   lonely card beside a large hole. Dense pulls the later cards up into those
   empty cells. It reorders the shelf visually rather than in the DOM, which is
   the right trade here: this grid is a thing you SCAN, and a hole in it reads
   as something failing to load. */
/* SIX ACROSS, BY DECISION — *"the packs have to be 6 in the section below the
   bays and not 5."* The shelf mirrors the six Tonight slots above it, so a
   row of packs reads as the row of holes they go into. An auto-fill floor was
   tried for a day (the 146px card had stopped holding its own content) and
   the host put six back; what actually made the poster fit again was not
   width but the two content fixes beside this rule — the meta's two-line
   clamp, and the title getting its stolen 44px back (see `.pack-title`'s own
   note). Do not "fix" a squeezed card by dropping a column again. */
.pack-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; grid-auto-flow: dense; }


/* ---- ONE PACK OPEN AT A TIME, and the rest are names.

   The common job on this tab is find tonight's pack and press Launch. Nine
   cards each carrying four dropdowns, a prize line, five buttons and a Launch
   is a wall you read rather than a shelf you scan — and the settings are
   per-night decisions about ONE pack, so they belong to the one you have
   chosen and nowhere else.

   The open card SPANS THE WHOLE ROW rather than growing inside its column: at
   240px a row of dropdowns is a tall single file, and the grid would reflow
   around a card three times the height of its neighbours. Full width it reads
   as a panel about the pack you picked, which is what it is. */
/* SQUARE, LIKE THE TILES IN TONIGHT — asked for as consistency, and it is
   the honest kind: these are the same object seen twice. A pack in the
   library and a pack dropped into tonight's row are one thing, so a square
   you drag from and a square you drag into say that without a word.
   ONLY WHEN SHUT. An open card carries a card-shape picker, a prize line and
   five buttons; forcing that into a square would either crop it or make the
   square the height of the page. Open is a panel about the pack you picked,
   which is what it has always been.
   Content sits at the BOTTOM, like the launch tiles, so a one-line title and
   a three-line one start from the same place and the row of cards reads
   along a line rather than as a ragged set of tops. */
/*
 * THE SAME SIZE AS THE SLOT IT IS DRAGGED INTO — asked for directly, and it is
 * a real affordance rather than tidiness: something the shape of the hole
 * reads as belonging in it, and the drag explains itself before anybody has
 * tried it.
 *
 * It was `aspect-ratio: 1 / 1`, which matched while the Tonight slots were
 * square too. They stopped being square when Tonight was measured at 600px
 * tall and had to lose the height — so this is the other half of that change,
 * and leaving it undone is what made a square card go into a letterbox hole.
 *
 * `min-height` rather than `height`, because a long pack title is allowed to
 * take a third line rather than being clipped. The slot has the same floor for
 * the same reason.
 */
/* ---- A SHUT PACK CARD IS A SQUARE POSTER.

   Chosen from four designs rendered side by side at the real shelf width. The
   era fills the card, the name and the meta sit on a dark fade at the bottom.
   It won on the job rather than the looks: the common task on this shelf is
   find tonight's pack and press Launch, and a poster is what you pick out of
   six at a glance.

   `.shut` NAMES THE CLASS RATHER THAN A STATE NOW — no card opens any more
   (see the note by `.pack-title::after`), so every card carries it, always.
   The class stayed rather than being folded into `.pack-card` itself only
   because the name is still the honest one: a square poster is what "shut"
   was always describing. */
.pack-card.shut {
  gap: 2px;
  aspect-ratio: 1 / 1;
  /* A floor as well, because a very narrow column (a phone at 320, two across)
     would otherwise square down to something too short to read. */
  min-height: 118px;
  justify-content: flex-end;
  overflow: hidden;
}
/* THE FADE THE WORDS SIT ON, and it is what makes the era safe to print big.
   Before this the word was tucked into the bottom-right corner and ran
   straight through the title — visible in the host's own screenshot, SOUL and
   ROCK printed across the names. A scrim means the two can overlap by design.
   A pseudo-element rather than a wrapper, so no markup differs between the
   open and shut states — the trap this file has already recorded is a template
   that varies while its wiring does not. The real children are lifted above it
   below; without that they would be painted under the fade. */
.pack-card.shut::before {
  content: '';
  position: absolute; left: 0; right: 0; bottom: 0; height: 62%;
  background: linear-gradient(to top, rgba(8, 7, 14, 0.92) 42%, transparent);
  z-index: 1;
  pointer-events: none;
}
.pack-card.shut > .pack-title,
.pack-card.shut > .tiny,
.pack-card.shut > .pack-yours { position: relative; z-index: 2; }
/* Two lines of meta, same argument as the title's three: at every real card
   width it IS two lines, so the clamp only guards the narrow case nobody
   predicted — a last resort, not the normal case. */
.pack-card.shut > .tiny {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
@media (max-width: 560px) {
  .pack-card.shut { min-height: 104px; }
  /* The disclosure that opens a shut card measured at 18px tall on a phone —
     under the 44px floor this shelf is meant to be scanned and tapped at.
     `.pack-title` is already `position: relative` on a shut card (two rules
     up), so an invisible `::before` widens the tap area without moving the
     visible one-to-three-line title or the fade behind it. */
  .pack-card.shut .pack-title::before { content: ''; position: absolute; inset: -14px -8px; }
}
/* THREE SIZES BY LENGTH — see `titleSize()`. The type gets BIGGER on a short
   name rather than smaller on a long one, which is the opposite of what fitting
   a title usually means and is only possible because the name is trimmed
   first. Three steps rather than a per-card fit: nine slightly different sizes
   on one shelf reads as sloppy. */
.pack-card.shut .pack-title.t-s { font-size: 15px; }
.pack-card.shut .pack-title.t-m { font-size: 12.5px; }
.pack-card.shut .pack-title.t-l { font-size: 11px; }
/* THREE LINES, NOT TWO — and that change came from measuring the host's own
   library rather than the mock-up. "2000-2010 Pop, Indie and Rock" naturally
   takes three, and clamping it at two printed "2000-2010 Pop, Indie and…",
   which drops a word off the end of a pack's name. A wrapped line is untidy; a
   truncated name is wrong, and on a shelf whose whole job is choosing between
   names it is the worse of the two by a distance.
   The clamp stays, because a square card cannot grow — it is a last resort for
   a name nobody predicted, not the normal case. */
.pack-card.shut .pack-title {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  white-space: normal;
}
/* The reserved two-line height is wrong on a square card: it pushes a
   one-line name up off the fade and leaves a gap under it. The card is a fixed
   height now, so nothing below needs the space held open. */
.pack-grid:not(.dense) .pack-card.shut .pack-title { min-height: 0; }
/* NO CARET ANYWHERE NOW — a card never opens in place on either door. A tap
   places the pack (Tonight on the Console, the bench in Workshop) instead of
   expanding it, so a caret would be the same fault as the venue name that
   looked pressable and did nothing. It used to be drawn on every card and
   suppressed only behind the Console door; the last thing left behind it —
   Read, Rename, Delete, Pictures, Playlist — moved to the bench, so the
   suppression is now the rule everywhere rather than the exception. */
/* The settings side by side where there is room. `minmax(0, …)` rather than a
   bare width, because a select will not go below its longest option and drags
   the page sideways at 320px — the min-content trap this file has now recorded
   six times. */
/* The settings row, capped.
   `auto-fit` with a `1fr` maximum STRETCHES whatever is left over, so with four
   controls and room for three the fourth — Venue — took a whole row to itself
   and became a 900px dropdown holding "The Station Tap". A track cannot exceed
   280px now and the row packs from the left, so a lone control is the same size
   as the ones above it instead of a slab. */
.pack-set {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 280px));
  justify-content: start;
  gap: 8px; margin: 4px 0;
}
.pack-set > * { min-width: 0; }

/* ---- finding a pack when there are eighty of them.

   COMPACT is about fitting more on screen. It always was, but it used to say
   it kept "every control" and that the launch-time pickers stayed — which
   stopped being true the day a card only carries them when it is open. What
   it does now is tighten the grid and the closed cards; the open one is
   unchanged, because that is the one you are actually working with. */
/* `min-width: 0`, AND IT IS THE WHOLE FIX — the eighth instance of the
   min-content fault in this app, diagnosed rather than guessed at.
 *
 * A flex child defaults to `min-width: auto`, which means it will not shrink
 * below the min-content width of what is inside it. Inside here is the search
 * box at a hardcoded `width: 190px`, and its own `max-width: 42vw` never got
 * to help: the global `input[type="search"]` rule is MORE SPECIFIC (0,1,1
 * against 0,1,0) and resets `max-width` to 100%. So the box asked for 190px,
 * this row refused to go below it, and the console scrolled sideways by 32px
 * at 320.
 *
 * Three plausible fixes were tried live in a browser before this one was
 * written — on `.tabbar`, on `.pack-search` and on `.game-section` — and none
 * of them moved the number. **The parent refusing to shrink was the
 * constraint, not the child asking to be wide**, which is the part that is
 * counter-intuitive every time this recurs.
 *
 * It only ever showed on the Workshop door, where the row has a heading beside
 * it and carries Compact and the edit link as well; the Console door has the
 * search box alone and measured clean, which is why it survived today's
 * checks. */
.pack-tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.pack-search {
  width: 190px; max-width: 42vw;
  padding: 8px 12px; border-radius: var(--r-field);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--panel-line);
  color: var(--ink); font: inherit; font-size: var(--fs-note);
}
.pack-search:focus { outline: none; border-color: var(--hot); background: rgba(255, 255, 255, 0.1); }
.pack-search::placeholder { color: var(--ink-dim); }

.pack-grid.dense { grid-template-columns: repeat(auto-fill, minmax(178px, 1fr)); gap: 8px; }
.pack-grid.dense .pack-card { padding: 10px 12px; gap: 3px; }
.pack-grid.dense .pack-title { font-size: var(--fs-ctl); }
/* The size and the play count, on a shut card only — read once, then noise.
   The OPEN card keeps them: you have chosen it, so they are what you check. */
.pack-grid.dense .pack-card.shut > .tiny:not([style]) { display: none; }
/* Dense trades detail for density — a round can still be dragged straight
   off an ordinary card, just not the compact ones a search for eighty packs
   is asking to see MORE of, not more to grab hold of. */
.pack-grid.dense .pack-card .pack-rounds { display: none; }

/* THE CENTRE OF A PACK CARD BELONGS TO THE PACK — reported as *"fix the pack
   card centre so a click gets the whole pack"*, and measured before it was
   changed rather than after.

   The card is 146x146 with 16px of padding, so 114px of inner width — and a
   round tick is 28px with a 4px gap, so FOUR of them come to 124px and wrap to
   a second row. That second row's top edge measured y=67 against a centre of
   y=73: six pixels, which is why the aim point of a thumb landed on a round
   and put ONE round into Tonight instead of the pack. Seven of the nine quiz
   packs here carry four or five rounds; the two with three fitted one row and
   were always correct, which is exactly why it looked intermittent.

   **ONE ROW, ALWAYS — that is the fix, and the wrapping was the whole bug.**
   Bottom-anchoring the strip was measured first and rejected: two rows are
   60px, the card's content ends at y=130, so the strip still reaches y=70
   against a centre of 73. Three pixels is not a fix, and at the 118px card the
   min-height floor allows it is wrong again.

   So the strip cannot wrap, which means five ticks have to fit 134px of
   full-bleed width: 5x24 + 4x3 = 132. **24px on the SHELF only.** The 28px
   rule was set for the Tonight tile, where a tick is an on/off SWITCH you
   press repeatedly — *"easier to click on/off"* — and it is untouched there.
   Here it is a shortcut sitting on top of the card's primary job, and one row
   puts the strip at y=106..130, clear of the centre by 33px rather than by 3.
   At `--r-field` a 24px box still reads as a rounded square; 22px was the size
   rejected for reading as a circle, and this is not that.

   Full-bleed with negative margins because 114px cannot hold five of anything
   useful, and the strip is the one child on this card that wants the whole
   width. */
.pack-card.shut > .pack-rounds {
  flex-wrap: nowrap;
  justify-content: center;
  gap: 3px;
  margin: 2px -16px 0;
  padding: 0 6px;
}
.pack-card.shut > .pack-rounds > .lb-rd {
  width: 24px; height: 24px;
  flex: 0 0 auto;
}
.pack-grid.dense .pack-shape { font-size: var(--fs-tag); }
.pack-grid.dense .pack-shape select { padding: 5px 24px 5px 8px; font-size: var(--fs-mini); }
.pack-grid.dense .pack-actions { gap: 4px; grid-template-columns: repeat(auto-fit, minmax(64px, 1fr)); }
.pack-grid.dense .pack-actions button, .pack-grid.dense .pack-actions a { padding: 5px 8px; font-size: var(--fs-tag); }
.pack-grid.dense .pack-card .go { padding: 8px; font-size: var(--fs-body); margin-top: 5px; }
.pack-card {
  display: flex; flex-direction: column; gap: 5px;
  padding: 16px; border-radius: var(--r-card);
  background: var(--panel); border: 1px solid var(--panel-line);
}
.pack-card.broken { border-color: var(--bad); }

/* The signpost to the workshop. Quiet on purpose — it is a way OUT of the
   launch page, not a thing to press before a gig. */
.pack-way-row { margin-top: 2px; }
.pack-way { margin: 0; }
.pack-way a { color: var(--ink-dim); text-decoration: none; border-bottom: 1px solid var(--panel-line); }
.pack-way a:hover { color: var(--ink); border-bottom-color: var(--hot); }

/* ---- THE PIN, top left.
 *
 * Every other corner of this card is spoken for: the "Yours" badge is top
 * right and the era word runs off the bottom right. Top left is also where the
 * eye starts, which is right for something that says "this one stays in
 * reach".
 *
 * OFF IS AN OUTLINE, ON IS FILLED — the app's own language, where fill means
 * commitment. It is deliberately quiet when off: nine cards each shouting an
 * unpressed control is the "wall of red" fault, and a pin nobody has set
 * should be almost invisible until looked for. It comes up on hover and on
 * focus so it can still be found.
 */
.pack-pin {
  /* THE BOX IS 44×44 AND THE DRAWN ICON STAYS 15×15 — a measured touch-target
     audit found the pin at 26×26px, well under the 44px floor, on a card
     tapped one-handed on a phone. Flush at top:0/left:0 rather than centred
     with a negative offset: `.pack-card` clips with `overflow: hidden` for
     the wordmark behind it, so a negative inset would be silently cropped
     back down. The icon moves a few px, which is not visible; the tap target
     does not. */
  position: absolute; top: 0; left: 0; z-index: 2;
  width: 44px; height: 44px; padding: 0;
  display: inline-grid; place-items: center;
  border: 1px solid transparent; border-radius: var(--r-pill);
  background: none; color: var(--ink-dim); cursor: pointer;
  opacity: 0.28; transition: opacity 120ms ease, background 120ms ease;
}
.pack-pin svg { width: 15px; height: 15px; display: block; transform: rotate(-20deg); }
.pack-card:hover .pack-pin { opacity: 0.75; }
.pack-pin:hover { opacity: 1; background: rgba(255, 255, 255, 0.10); }
/* FOCUS GETS ITS OWN SIGNAL — it used to share the card-hover opacity (0.75),
   dimmer than a direct mouse hover on the pin itself (1, plus a background),
   which is backwards: a keyboard user has no cursor confirming where they
   are. Full opacity and the account's own colour, like every input's focus
   ring already uses. */
.pack-pin:focus-visible { opacity: 1; outline: 2px solid var(--hot); outline-offset: 2px; }
/* ON follows the account's own colours, like everything that means "this is
   mine and it is set" — never gold, which is the trophy colour. */
.pack-pin.on {
  opacity: 1; color: var(--ink);
  background: rgba(255, 46, 136, 0.20);
  background: color-mix(in srgb, var(--hot) 22%, transparent);
  border-color: color-mix(in srgb, var(--hot) 55%, transparent);
}
.pack-pin.on svg { transform: none; }
/* THE TITLE GOT ITS 44px BACK. This was `padding-left: 44px` "to clear the
   pin" — written when the title lived at the TOP of the card. On the square
   poster the title anchors to the BOTTOM, the pin's 44px tap box is flush
   top-left, and the pin is invisible until hover besides — so the rule was
   costing every title a third of its width (114px down to 70) for a
   collision that can no longer happen. That theft is what wrapped "2000-2010
   Pop, Indie and Rock" to a third line and pushed the whole stack out of the
   146px card. Symmetric now, so the name is genuinely centred. */
.pack-card.shut .pack-title { padding-left: 6px; padding-right: 6px; }

/* ---- A PACK WEARS ITS OWN SUBJECT.
 *
 * The colours come from `pack-look.js`, per pack, inline — see that file for
 * why they are derived rather than stored and why "pop" is not a subject. This
 * is only the paint.
 *
 * A WASH BEHIND THE CARD, NEVER A FILL, and that is the whole reason this can
 * exist alongside the app's colour language: gold means winning, green means
 * good and red means destructive, so a Christmas pack tinted red and green must
 * not be mistakable for a broken one. It is not, because `broken` is a BORDER
 * and this never touches the border — the two say their piece in different
 * places. The strength is capped in the JS for the same reason: a saturated
 * card reads as a control that has been pressed.
 *
 * EXTRA BACKGROUND LAYERS, NOT A PSEUDO-ELEMENT, and that is not a style
 * preference — it was written the other way first. A `::after` covering the
 * card has to be pushed behind the card's contents, and the only way to do
 * that is `> * { position: relative }`, which silently un-pins EVERY
 * absolutely positioned child. There are three across these two elements: the
 * "Yours" badge on an own-pack, the round number on a Tonight tile, and the ×
 * that takes a pack out. All three would have dropped into the flow, none of
 * them would have thrown, and no test would have noticed.
 *
 * A background LIST cannot do that. The tint goes on as a layer above
 * `--panel` — which on the console is the quizmaster's own scheme-tinted
 * surface — so their colours still come through underneath, which is what
 * stops nine packs turning the shelf into somebody else's palette. Nothing
 * about positioning, stacking or overflow changes at all.
 */
.pack-card.tinted, .lb-tile.tinted {
  background: linear-gradient(120deg, var(--pk-a), var(--pk-b)), var(--panel);
}
/*
 * AND AN EDGE ALONG THE BOTTOM, in the same colour at full strength.
 *
 * The wash has to stay faint or it fights the title, which means the HUE is
 * hard to name on a dark card and two close colours read as one. Three pixels
 * with nothing written over them says it outright — so the two together do a
 * job neither does alone: the wash gives the card a mood, the edge gives it an
 * identity you can match against the Tonight slot from across the page.
 *
 * ON THE BOTTOM, at the host's own reading, and he is right: an ordinary button
 * already carries the account's colour on its bottom border and the tab bar
 * underlines the tab you are on, so this is a shape the eye has already learned
 * here. A stripe down the LEFT was rendered beside it and turned down for that
 * reason — nothing else in the app does it, so it would be a second visual
 * language invented for a job the first one already covers.
 *
 * `:not(.broken)` is load-bearing. A broken pack is bordered in `--bad`, and
 * this rule comes later in the sheet, so without the guard a pack's subject
 * colour would quietly overwrite the red on the one card that has something
 * wrong with it. Red keeps its meaning everywhere; that is the rule.
 */
.pack-card.tinted:not(.broken) { border-bottom: 3px solid var(--pk-edge); }

/* ---- THE ERA, PRINTED BIG BEHIND THE CARD.
 *
 * Cartoon figures were tried first and do not read at this size — see
 * docs/console.md. Type does: it is drawn to be legible at any size, it needs
 * no asset, and "80s" is unambiguous in a way a drawing of somebody in flares
 * is not.
 *
 * BOTTOM RIGHT AND CLIPPED, deliberately. The title is top left and wraps to
 * two lines, the count sits under it — so the free corner is the far one, and
 * letting the word run off the edge is what stops it reading as a label with
 * a value in it. It is scenery the card is cut out of.
 *
 * `pointer-events: none` because the whole card is a drop source and a drag
 * that started on the word would otherwise be a drag of the word.
 *
 * The card needs `position: relative` for this, and it did NOT have it — which
 * means `.pack-yours` (absolute, top right) was positioning against whatever
 * ancestor happened to be positioned instead of against its own card. Adding
 * it here fixes that at the same time.
 */
.pack-card { position: relative; overflow: hidden; }
.pack-word, .lb-tile .pack-word {
  position: absolute; right: -2px; bottom: -10px; z-index: 0;
  pointer-events: none; user-select: none;
  font-size: var(--pk-word-size); font-weight: 900; line-height: 1;
  letter-spacing: -0.03em; font-style: italic;
  /* The subject's own two colours, so the word and the wash agree — and a
     SOLID fallback first, because `background-clip: text` means transparent
     text and a browser without it would print nothing at all. */
  color: var(--pk-a);
}
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .pack-word {
    background: linear-gradient(120deg, var(--pk-a), var(--pk-b));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    /* The wash colours are capped low so they can sit under a title; as TEXT
       there is nothing on top of them, so they need lifting back up or the
       word is invisible. */
    filter: brightness(2.6) saturate(1.4);
  }
}
/* Everything real on the card sits above it. Only these two, rather than
   `> *` — that would un-pin the absolutely positioned children, which is the
   trap recorded on the tint rule above. */
.pack-card > .pack-title, .pack-card > .tiny,
.pack-card > .pack-set, .pack-card > .pack-actions, .pack-card > .go { position: relative; z-index: 1; }

/* ---- ON A SHUT CARD THE ERA IS THE POSTER, not a mark in the corner.

   The corner version is the one in the host's own screenshot with SOUL and
   ROCK printed through the titles, and the cause is that a corner has no room:
   the word had to be tucked under the text to fit, so the two were always
   going to collide on a long name. Centred and given the whole card, it has
   nowhere to collide — the fade above carries the words instead.

   Capped in `em` off its own size so it scales with the three word sizes
   `packLookAttrs` already sets, rather than being a fourth number to keep in
   step with them. */
.pack-card.shut .pack-word {
  inset: 0; right: auto; bottom: auto;
  display: grid; place-items: center;
  font-size: calc(var(--pk-word-size) * 1.45);
  /* Below the fade, so a long title is read against the dark rather than
     against the middle of a giant letter. */
  z-index: 0;
  opacity: 0.5;
}
.lb-tile > .lb-tile-name, .lb-tile > .lb-rounds, .lb-tile > .lb-tile-sub { position: relative; z-index: 1; }
/* Smaller in a Tonight slot, which is the same width but carries a number
   badge, a remove button and a row of round ticks — there is less free corner
   to run into. */
.lb-tile .pack-word { font-size: calc(var(--pk-word-size) * 0.72); bottom: -6px; }
/*
 * AND ON A TONIGHT TILE IT IS GONE. Measured, twice, and the corner simply has
 * no room left in it.
 *
 * `A PACK WEARS ITS OWN SUBJECT` put this word in the tile's bottom-right on
 * purpose — *"the Tonight tile keeps the corner"* — and that was written when
 * the corner was empty. It now holds a 44px gap dial. Shifting the word 52px
 * left was tried first and measured: on a 166px tile it then overlapped the
 * round ticks by 52 x 18px, so it had moved out of one collision into another.
 * There is no third place: the top is the title and the × , the bottom left is
 * the ticks, the bottom right is the dial.
 *
 * **The control wins and the decoration goes** — and it costs less than it
 * sounds, because the two things that actually carry a pack's subject are
 * still there: the wash across the whole tile, and the coloured bottom edge
 * that says which KIND of pack it is. The word stays on the shelf CARD, where
 * the corner is still free and where you are scanning nine of them.
 */
.lb-tile.is-pack .pack-word { display: none; }
/*
 * THE TILE NEEDS ITS OWN, MORE SPECIFIC RULE — and it was written without one,
 * which cost the feature its whole point on the half that matters most.
 *
 * `.lb-tile.is-pack` sets the SHORTHAND `border: 1px solid var(--panel-line)`,
 * it has the same specificity as `.lb-tile.tinted`, and it appears three
 * thousand lines further down — so it won, and the shorthand reset all four
 * sides. The slot showed a hairline in the account colour while the card below
 * it showed a bold subject-coloured edge: **the pack changed appearance on
 * being dragged into the hole**, which is the exact thing the shared function
 * exists to prevent. Nothing threw, nothing failed, and the card looked right.
 *
 * Naming `.is-pack` here out-specifies it wherever either rule sits in the
 * file, rather than relying on source order — which is what let this happen.
 */
.lb-tile.is-pack.tinted { border-bottom: 3px solid var(--pk-edge); }
/* Two patterns, shared by every subject that wants one — scan lines for the
   decades that had them and a diagonal for the heavy ones. Deliberately only
   two: a texture per subject would be a stylesheet that grows every time a
   word is added to a list, and at 200px wide nobody can tell fifteen textures
   apart anyway. They are white at very low alpha rather than a colour, so one
   rule works over every tint. */
.pack-card.tinted.pk-lines, .lb-tile.tinted.pk-lines {
  background:
    repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.07) 0 3px, transparent 3px 9px),
    linear-gradient(120deg, var(--pk-a), var(--pk-b)),
    var(--panel);
}
.pack-card.tinted.pk-slash, .lb-tile.tinted.pk-slash {
  background:
    repeating-linear-gradient(115deg, rgba(255, 255, 255, 0.05) 0 2px, transparent 2px 14px),
    linear-gradient(120deg, var(--pk-a), var(--pk-b)),
    var(--panel);
}
/*
 * Two lines are RESERVED for a title, whether or not it needs them.
 *
 * "The Madonna Quiz" is one line and "The 2000s Pop Rnb and Chart Quiz" is two,
 * so side by side every row below them — the count, the play line, the Look
 * picker — sat at a different height on each card. The buttons already line up
 * (`.pack-actions` has `margin-top: auto`), which made it worse rather than
 * better: the tops were ragged and the bottoms were not, so the gap landed in
 * the middle of the card where there is nothing to explain it.
 *
 * Not in Compact, where the whole point is to fit more on screen and a reserved
 * empty line is the opposite of that.
 */
.pack-title { font-size: var(--fs-lead); font-weight: 900; line-height: 1.2; }
.pack-grid:not(.dense) .pack-title { min-height: 2.4em; }
.pack-card .played { margin-bottom: 6px; }
.pack-card .go {
  width: 100%; margin-top: 8px;
  border: none; border-radius: var(--r-field); padding: 12px; cursor: pointer;
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  color: #12000c; font-family: inherit; font-weight: 900; font-size: var(--fs-field); letter-spacing: 0.01em;
}
.pack-card .go:hover { filter: brightness(1.08); }
.pack-card .go:disabled { opacity: 0.4; cursor: default; filter: none; }

/* ---- Past gigs: one row a night, the photos folded away behind it.
 *
 * The row is a BUTTON rather than a link, because opening a night loads its
 * pictures rather than going anywhere — and a control that looks like a link
 * and does not navigate is one people middle-click and then wonder about. It
 * keeps the old archive row's shape so the page still reads as a list. */
.gig-list { display: grid; gap: 7px; }
.gig-head {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap; width: 100%;
  /* `--r-card`, not `--r-field`: this is a card in a list of nights, the same
     object as `.diary-row`, `.venue-card` and `.inv-row`, and it was the only
     one of the four wearing a field's corner. */
  padding: 12px 14px; border-radius: var(--r-card); text-align: left; cursor: pointer;
  background: var(--panel); border: 1px solid var(--panel-line); color: var(--ink);
  font-family: inherit; font-size: var(--fs-note);
}
.gig-head:hover { border-color: var(--hot); }
.gig-head .an { font-weight: 800; font-size: var(--fs-ctl); }
.gig-head .gig-more { margin-left: auto; color: var(--hot); font-weight: 700; }
.gig-body { padding: 10px 14px 4px; display: grid; gap: 8px; }

/* ---- the one-button generator on the console */
.panel.generate { border-color: rgba(255, 46, 136, 0.4); background: rgba(255, 46, 136, 0.07); }
.gen-row { display: flex; gap: 10px; }
/* `min-width: 0`, or the theme box refuses to go below the width of its own
   placeholder and pushes Write it off the side of a 320px phone — the same
   fault, in a second place, that made the control view scroll sideways. */
.gen-row input {
  flex: 1 1 auto; min-width: 0; padding: 13px 14px; font-size: var(--fs-field); font-weight: 600;
  border-radius: var(--r-field); border: 1px solid var(--panel-line);
  background: rgba(255, 255, 255, 0.08); color: var(--ink);
}
.gen-row input:focus { outline: 2px solid var(--hot); outline-offset: 1px; }
.gen-row .go {
  border: none; border-radius: var(--r-field); padding: 13px 22px; cursor: pointer; white-space: nowrap;
  background: linear-gradient(120deg, var(--hot), var(--hot-2)); color: #12000c; font-weight: 900; font-size: var(--fs-ctl);
}
.gen-row .go:disabled { opacity: 0.45; cursor: default; }

/*
 * The topical quiz sits UNDER the theme box rather than beside it, because it
 * is a different job rather than a different setting: it takes no theme, and a
 * button on the same line as one you have to fill in reads as needing it.
 * Outlined rather than filled so "Write it" stays the loudest thing here — a
 * second gradient button of the same weight is two primaries and no primary.
 */
.gen-topical { display: flex; gap: 14px; align-items: flex-start; margin-top: 12px; flex-wrap: wrap; }
.gen-topical .go {
  border: 2px solid var(--hot); border-radius: var(--r-field); padding: 11px 18px; cursor: pointer;
  background: transparent; color: var(--ink); font-weight: 900; font-size: var(--fs-body); white-space: nowrap;
}
.gen-topical .go:disabled { opacity: 0.45; cursor: default; }
.gen-topical .tiny { flex: 1 1 240px; line-height: 1.5; }

.gen-opts { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; margin-top: 12px; font-size: var(--fs-note); color: var(--ink-dim); }
.gen-opts label { display: flex; gap: 6px; align-items: center; font-weight: 700; }
.gen-opts input, .gen-opts select {
  padding: 6px 8px; border-radius: var(--r-field); font-size: var(--fs-note);
  background: rgba(255, 255, 255, 0.08); color: var(--ink); border: 1px solid var(--panel-line);
}
.gen-opts a { color: var(--cool); }

.gen-status { margin-top: 12px; }
.gen-log {
  max-height: 220px; overflow-y: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-note); line-height: 1.7; color: var(--ink-dim);
  padding: 11px 13px; border-radius: var(--r-field); background: rgba(0, 0, 0, 0.35);
}
.gen-good {
  margin-top: 10px; padding: 13px 15px; border-radius: var(--r-field); font-size: var(--fs-ctl);
  background: rgba(47, 224, 122, 0.16); border: 1px solid var(--good);
}
.gen-good a { color: var(--good); font-weight: 800; }
.gen-bad {
  margin-top: 10px; padding: 13px 15px; border-radius: var(--r-field); font-size: var(--fs-ctl);
  background: rgba(255, 77, 94, 0.14); border: 1px solid var(--bad);
}
.tiny.warn { margin-top: 10px; color: var(--gold); }

/* Something the host needs to read mid-gig, in the dark, at a glance. */
.panel.warn {
  background: rgba(255, 210, 63, 0.12);
  border: 1px solid rgba(255, 210, 63, 0.55);
}
.panel.warn h3 { color: var(--gold); }

/* ---- bingo track list in the editor */
.tracklist-edit { display: grid; gap: 6px; }
.track-edit-row { display: flex; align-items: center; gap: 7px; }
.track-edit-row .tnum { width: 2.2em; text-align: right; color: var(--ink-dim); font-size: var(--fs-mini); font-weight: 800; }
.track-edit-row .ttitle { flex: 2 1 200px; }
.track-edit-row .tartist { flex: 1 1 140px; }
.editor .shortfall { color: var(--bad); }

/* Nothing on the big screen is clickable, and a mouse pointer sitting in the
   middle of a projected image looks amateurish. Hide it. */
/* NO CURSOR AND NO SCROLL. This is the one page in the app that must not move:
   it is a fixed 16:9 sheet on a wall, there is nothing below the fold to reach,
   and a stray scroll in a dark pub takes the question off the top of the room's
   screen with no way to know it has happened. It is the reason `body` said
   `overflow: hidden` for years; it now says it here, where it is true. */
body.screen { cursor: none; overflow: hidden; }

/* The quiz generator, distinguished from the bingo one below it. */
.panel.quiz-generate { border-color: rgba(75, 216, 255, 0.4); background: rgba(75, 216, 255, 0.07); }
.panel.quiz-generate .gen-row .go { background: linear-gradient(120deg, var(--cool), #7cf76b); color: #04222c; }
.gen-opts input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--hot); }
.gen-good ul, .gen-bad ul { font-size: var(--fs-note); }

/* ---- console tabs
   One tab per thing you can run. Big enough to hit with a thumb, because the
   console gets used on a phone as often as a laptop. */
/* THE BAR WRAPS ON A LAPTOP AND SCROLLS ON A PHONE.
   Asked for directly — *"the menu should wrap to the screen on a laptop"* —
   and it was a real defect rather than a preference: nine tabs at this size
   are wider than the console's 1000px column, so `overflow-x: auto` cut "My
   account" in half at the right-hand edge with no scrollbar drawn to say it
   could be reached. A tab you cannot see is a tab that does not exist, which
   is the fault this whole bar exists to prevent.
   It cannot simply wrap everywhere: at 320px nine tabs would take three rows
   and push the page's first line most of a screen down. So it wraps from
   860px, where two rows is the worst case, and keeps the sideways scroll
   below that — the same split the topbar's own rule already uses. */
/* THE TABS ARE A COLUMN DOWN THE LEFT, AND THE SAME COLUMN ON A PHONE.
   Two rules do the whole thing: the bar stacks, and from 900px the bar and the
   body become two grid columns. Below that they are simply blocks, so the list
   runs full width with the tab you pressed underneath it — which is the
   collapse that was chosen out of three rendered at 390.
   `minmax(0, 1fr)` on the content column is load-bearing rather than tidy: a
   grid child defaults to `min-width: auto`, so the pack grid's own minimum
   would push the column wider than the page and the whole console would scroll
   sideways. That is the same trap `.pack-tools` fell into. */
.consolecols { display: block; }

/* ---- THE DOOR SHELL — one frame, and exactly one thing in it scrolls.

   Asked for after the Console's layout was named as the thing that works:
   *"everything I need is on the screen — no scrolling down and no BS before a
   gig… each pill section should have their own area at the top that doesn't
   scroll with the page, the menu on the side doesn't scroll, but the contents
   DO."*

   Three fixed regions and one scroller: the topbar, the door's own header
   (Tonight on the Console), and the tab column all stay put; the tab body is
   the only thing that moves. The whole page stops scrolling, which is why
   `renderKeepingPlace()` had to learn to hold the PANE's offset instead of the
   window's.

   FROM 900px ONLY, and that is not a detail. Below it the tab column is a
   full-width stack and the door header is most of a phone screen — pinning
   both would leave a letterbox of content between them. A phone scrolls the
   page, exactly as it did. */
/* THE BAY HEIGHT EVERY DOOR SHARES — the launch bar's own open height,
   measured rather than chosen. Two values because the bar's settings row wraps
   below 1150px and the bar gets taller; see the rule further down. */
@media (min-width: 900px) { :root { --bay-h: 425px; } }
@media (min-width: 1150px) { :root { --bay-h: 362px; } }

/* AND IT NEEDS A MINIMUM HEIGHT TOO, WHICH IS THE SAME ARGUMENT AS THE WIDTH.
 *
 * The rule below already says the frame is off under 900px because "the tab
 * column is a full-width stack and the door header is most of a phone screen —
 * pinning both would leave a letterbox of content between them". That is a
 * statement about how much ROOM there is, and height was never checked.
 *
 * A 476px-tall window is a browser not maximised on a laptop, and the bay
 * alone is 425 of it. The frame pinned the bay and the topbar and handed
 * `.consolecols` ZERO height — inside a `.wrap` that is `overflow: hidden`, so
 * the tab column and the whole pack shelf were 592px down a 515px frame and
 * simply not on the page. Reported as *"the sub menu is still missing from the
 * console"*, twice, with nothing thrown and nothing looking broken.
 *
 * LETTING THE BAY SHRINK INSTEAD WAS TRIED AND IS WORSE. The Console's own bay
 * is the launch bar, which deliberately has no `--bay-h` — so the doorhead
 * shrank and the bar inside it did not, and it PAINTED OVER the tab column.
 * The tabs measured 200px tall at y=315 and were invisible: the same
 * in-the-document-but-not-on-the-screen trap this file records four times
 * over, which is why this was caught by looking at the render and not at the
 * numbers.
 *
 * So under 700px of height the console SCROLLS, exactly as it does under
 * 900px of width — the arrangement that has always worked, rather than a
 * fixed frame with nowhere to put anything. 700 is the topbar (73) plus the
 * bay (425) plus a readable tab column (200), so the frame exists precisely
 * where it can hold what it promises. */
/* TWO COLUMNS IS A WIDTH DECISION AND THE PINNED FRAME IS A HEIGHT ONE — they
   were in the same block and gating that block on height took the SIDEBAR away
   with it. On a short window the tabs went full width above the shelf, which
   pushed the pack cards down the page and put a wall of tab between them and
   the launch bar: *"the submenu was meant to allow for the content to be
   dragged to the launch bar, at the moment it takes up the full width of the
   page and is in the way of this functionality."*

   **The drag is what the layout is FOR**, and it needs the shelf beside a
   190px rail rather than under a full-width one. The page scrolling is fine
   for it — `pinTonightWhereItIs()` was written for exactly that case, and
   pins Tonight where the eye last saw it when a card is picked up. */
@media (min-width: 900px) {
  .consolecols {
    display: grid;
    grid-template-columns: 190px minmax(0, 1fr);
    gap: 8px 24px;
    /* `stretch`, not `start`: both columns have to fill the frame or the
       scroller has no height to scroll inside and the page grows instead. */
    align-items: stretch;
  }
}

/* THE MINIMUM HEIGHT IS TWO NUMBERS, BECAUSE THE DOORHEAD IS TWO HEIGHTS.
 *
 * 700 was 73 + 425 + 200 — the topbar, the bay and a readable tab column —
 * and it was worked out from an IDLE launch bar. Measured with a night
 * actually running, the doorhead is 573px at 1150px and up, and 636-690px
 * below that where the settings row wraps. So the frame was pinning itself at
 * heights it could not honour, and `.consolecols` came out at 0-134px with
 * the tab column off the bottom of the screen and nothing able to reach it.
 *
 * The two conditions are 73 + 573 + 200 = 846 and 73 + 690 + 200 = 963,
 * rounded up. Below them the console SCROLLS, which is what it has always
 * done under 900px of width and is strictly better than a control nobody can
 * get to. **Do not collapse this to one number**: a single threshold either
 * takes the frame away from a 1500x900 laptop that fits it perfectly well, or
 * keeps it at 960x760 where it does not fit at all. */
@media (min-width: 1150px) and (min-height: 850px),
       (min-width: 900px) and (min-height: 965px) {
  /* `dvh` because a mobile-shaped browser window reports `vh` including the
     chrome that slides away, which puts the bottom of the frame under it. The
     `vh` line before it is the fallback and must stay first. */
  .console .wrap {
    height: 100vh;
    height: 100dvh;
    display: grid;
    grid-template-rows: auto minmax(0, 1fr);
    /*
     * AND IT SCROLLS WHEN IT GENUINELY CANNOT FIT, rather than clipping.
     *
     * `overflow: hidden` below is what makes the frame a frame, and it was
     * also unreachable-tabs waiting to happen: with a night running the
     * doorhead is 573-690px rather than the 425 the 700px minimum was worked
     * out from, so `.consolecols` was handed 0-134px and the tab column went
     * off the bottom with nothing to reach it by.
     *
     * **A clipped overflow is worse than a scrolling one** — this file's own
     * rule, three sightings old, and the frame itself was breaking it. Paired
     * with the floor on `.consolecols` below, the worst case is now a console
     * that scrolls a little, which is exactly what it does under 900px anyway.
     * Nothing is ever cut off. `overflow-x` stays hidden: sideways is the one
     * direction that is always a fault.
     */
    overflow-x: hidden;
    overflow-y: auto;
    /* The 60px tail existed so the last card cleared the bottom of the page.
       Nothing runs off the bottom of a fixed frame, so it would only ever be
       dead space the content could have used. */
    padding-bottom: 0;
    /* AND NOTHING RESTATES `overflow` AFTER THOSE TWO. A trailing
       `overflow: hidden` sat here and wiped both of them — the essay above is
       the fix that was written for "the sub menu is still missing from the
       console", reported twice, and it had never once been in effect.
       Measured at 1500x900 with five packs in Tonight: 161px of overflow,
       Music Bingo, Prepare a night and Venues all off the bottom, six of six
       pack cards gone, and a real wheel moving nothing. FOURTH sighting of
       shorthand-beats-longhand, this time inside ONE declaration block — and
       `padding-bottom: 0` sitting between the pair and the line that killed
       them is the tell that the new rules were inserted ABOVE the old one
       rather than replacing it. */
  }
  /*
   * A COLUMN, NOT A TWO-ROW GRID — because the grid assumed exactly two
   * children and the page does not always have two. `auto minmax(0, 1fr)`
   * put the doorhead in the 1fr row the moment ANY banner rendered above it
   * (the no-accounts maker, Workshop's backup warning): the doorhead was
   * STRETCHED — ~160px of nothing under the launch bar on every door — and
   * the columns fell into an implicit row, so the fixed frame silently
   * became a scrolling page again. A flex column takes any number of
   * banners; only `.consolecols` grows, everything else sizes to itself.
   */
  .console main {
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  .console main > * { flex: 0 0 auto; }
  /* THE FLOOR GOES HERE, not on the bare `.consolecols` rule further down —
     this selector is more specific and was resetting it to 0. See the note on
     that rule for why there is a floor at all. */
  .console main > .consolecols { flex: 1 1 0; min-height: 200px; }
  /* The door's own area. It sizes to its content — a folded Tonight is a line,
     an open one is the panel — so the content pane grows and shrinks with it
     rather than the header being given a height it has to live inside. */
  .doorhead { flex: 0 0 auto; }

  /* ---- EVERY DOOR'S BAY IS THE LAUNCH BAY'S SIZE.
   *
   * A hard rule, set on 29 August 2026: *"the bay at the top ALWAYS has the
   * same dimensions as the launch bay, this must be consistent across
   * sections."*
   *
   * Before this, the top of the page changed shape as you moved between doors
   * — 386px on the Console, 194 on Workshop and Post gig, and whatever the
   * data came to on Community. That is the same fault a heading per render
   * function was: five sections each deciding their own version of one thing.
   * The frame is fixed from 900px up, so a bay that changes height moves the
   * tab column and the whole page under it every time a door is pressed.
   *
   * `--bay-h` IS THE LAUNCH BAR'S OWN OPEN PANEL HEIGHT, measured — the panel
   * rather than the doorhead round it, since the doorhead adds its own margin
   * and every bay is then the same on screen. It changes at
   * 1150px for the same reason the bar does — its settings row wraps below
   * that. The bar itself is deliberately NOT given this height: it is the
   * REFERENCE rather than a follower, it folds to a line on purpose, and
   * clipping the one panel on the protected launch path to a number in a
   * stylesheet is not a trade worth making. `scripts/community-bay.mjs`
   * asserts the two agree, so the bar growing fails a check rather than
   * silently making every other door the wrong size.
   *
   * BELOW 900px THERE IS NO RULE, because there is no fixed frame: the page
   * scrolls, the bay is most of a phone screen already, and forcing the
   * launch bar's 745px onto every door would be a screen of empty panel. */
  .doorhead > .panel.bench {
    height: var(--bay-h);
    /* The content decides what scrolls — the photos bay keeps its heading and
       scrolls its body, the league bay scrolls its two columns. Anything that
       has not said otherwise scrolls whole, which is the honest fallback: a
       bay you cannot see the bottom of is worse than one that moves. */
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /*
   * A READABLE TAB COLUMN OR THE FRAME GIVES WAY. 200px is the same number
   * the frame's own minimum height was built from — four tabs and a little
   * air. `min-height` rather than a media query because the thing that
   * squeezes it is the doorhead's CONTENT (a live night's bar is 120px taller
   * than an idle one), which no query can measure. When it will not fit, the
   * wrap above scrolls instead of hiding it.
   */
  .consolecols { min-height: 200px; }
  /* THE ONLY THING THAT SCROLLS. `overscroll-behavior` stops a flick that
     reaches the end of the list scrolling the window behind it, which on a
     trackpad reads as the whole app sliding. */
  .console .tabbody {
    overflow-y: auto;
    overscroll-behavior: contain;
    min-height: 0;
    /* Room for the last card to clear the bottom edge, replacing the tail that
       came off `.wrap` above. */
    padding-bottom: 40px;
  }
  /* The tab column does not scroll with the content — but it gets its own
     scrollbar if a door ever holds more tabs than the frame is tall, which is
     the honest way round: a tab you cannot reach is a tab that does not
     exist. */
  .console .tabbar { overflow-y: auto; min-height: 0; }

  /* AND THE TAB'S OWN HEADING STAYS IN VIEW while its content scrolls under
     it — *"the reason you went there is always in view"*. Sticky inside the
     scroller rather than lifted out of `tabBody()`, because the heading is
     drawn in exactly one place on purpose: a heading per render function is
     the arrangement that let four of them go missing. */
  .console .tab-head {
    position: sticky;
    top: 0;
    z-index: 5;
    /* Opaque, or the cards scroll through it. `--surf-1` is the console's own
       surface, so it reads as part of the room rather than as a slab. */
    background: var(--surf-1);
    margin-bottom: 0;
    padding: 2px 0 12px;
  }
}
.tabbar {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-bottom: 22px;
  padding-bottom: 2px;
}
/* The sidebar's own dressing, on WIDTH — it goes with the two columns above,
   not with the pinned frame. `top` only bites where something is sticky. */
@media (min-width: 900px) {
  /* IT STAYS WHERE IT IS WHILE THE TAB SCROLLS, which is most of what a
     sidebar buys over a bar: nine tabs pinned beside a long Gigs list rather
     than a strip you have to scroll back up to. `top` is the sticky topbar's
     own height, declared beside the rule that sets it so the two move
     together. No fill and no blur — a column that content passes UNDER needed
     an opaque background; a column with its own lane does not. */
  .tabbar {
    /* Not sticky any more — the shell holds it still by construction, and a
       sticky element inside a frame that does not scroll is a no-op that reads
       as intent. */
    top: calc(var(--topbar-h) + 12px);
    margin-bottom: 0;
    padding-bottom: 0;
    border-right: 1px solid var(--panel-line);
    padding-right: 8px;
  }
}
/* A TAB IS A FULL-WIDTH ROW, and the badge sits at the far end of it.
   `space-between` is what makes a column of nine read as a list rather than as
   nine differently-shaped buttons: the labels line up on the left and every
   count lines up on the right, so "3 invoices" is found by running down one
   edge instead of by reading. */
.tab {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  padding: 11px 14px;
  border: none;
  /* THE MARKER IS THE LEFT EDGE NOW, because the bar turned ninety degrees.
     It was a bottom border, which under a stacked list stops reading as "you
     are here" and starts reading as a rule between two tabs. Same colour, same
     3px, same job — rotated with everything else. */
  border-left: 3px solid transparent;
  background: none;
  color: var(--ink-dim);
  font-family: inherit;
  font-size: var(--fs-ctl);
  font-weight: 800;
  cursor: pointer;
  text-align: left;
  white-space: nowrap;
  border-radius: 0 var(--r-field) var(--r-field) 0;
  transition: color 160ms, background 160ms, border-color 160ms;
}
.tab:hover { color: var(--ink); background: rgba(255, 255, 255, 0.04); }
.tab.on {
  color: var(--ink);
  border-left-color: var(--hot);
  background: rgba(255, 255, 255, 0.06);
}
.tabcount {
  display: inline-grid;
  place-items: center;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.12);
  font-size: var(--fs-mini);
  font-weight: 900;
}
.tab.on .tabcount { background: var(--hot); color: #fff; }
/* SOMEBODY IS LATE PAYING. The same badge, in red — not a second one beside
   it, which would cost the first its meaning. Red is the app's "wrong" colour
   and it is the honest one here: an invoice past its terms is not a job you
   have not got to, it is one somebody else has not done. */
.tabcount.late { background: rgba(255, 77, 94, 0.22); color: var(--bad); }
.tab.on .tabcount.late { background: var(--bad); color: #fff; }

/* NO RESERVED HEIGHT. It carried a minimum for a while so that pressing a tab
   could scroll the bar to the top of the screen — which worked, and left most
   of a screen of black under a nearly-empty Adverts or Invoices tab. A tab
   press goes straight to `top: 0` now that the launch bar is small enough to
   want on screen, so there is nothing to scroll to and nothing to reserve. */
.tabbody { animation: cardIn 220ms cubic-bezier(0.2, 0.8, 0.2, 1); }

/* The card's buttons, in the order you actually use them: the small ones you
   might press first sit in a row, and LAUNCH is the full width of the card at
   the bottom — the last thing, the biggest thing, and impossible to hit by
   accident on the way to Rename.
   They share one rule rather than four near-identical ones, so they cannot
   drift apart on size or weight the way they had. */
/*
 * A GRID, not a wrapping flex row — so every action is the same size however
 * many there are.
 *
 * These were `flex: 1 1 auto`, which is fine on the three most cards carry and
 * wrong the moment a fourth appears: Playlist pushed Delete onto a row of its
 * own, where being the only item on the line stretched it full width. That made
 * the destructive button the biggest target on the card, sitting directly above
 * Launch — the exact thing "Launch is the last thing on a pack card, and full
 * width" exists to prevent. Columns are worked out from the card, so a fourth
 * button wraps into the next cell at the same width as the rest.
 */
.pack-actions {
  margin-top: auto;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(84px, 1fr)); gap: 6px;
}
/* Four actions are 2x2, not three and a straggler. `auto-fit` packs as many
   per row as will go, which is right for three and wrong for four: the fourth
   drops onto a row of its own and reads as a button that has come loose. Six
   go 3x2 for the same reason. */
.pack-actions[data-count="4"] { grid-template-columns: repeat(2, 1fr); }
.pack-actions[data-count="6"] { grid-template-columns: repeat(3, 1fr); }
.pack-actions > * {
  display: grid; place-items: center;
  min-height: 36px; padding: 0 10px;
  border-radius: var(--r-field);
  font-family: inherit; font-size: var(--fs-note); font-weight: 800; line-height: 1;
  text-decoration: none; cursor: pointer;
  color: var(--ink);
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid var(--panel-line);
  transition: background 140ms, border-color 140ms, color 140ms;
}
.pack-actions > *:hover { background: rgba(255, 255, 255, 0.16); }
.pack-actions > *:disabled { opacity: 0.5; cursor: default; }
.pack-actions > *:disabled:hover { background: rgba(255, 255, 255, 0.08); }

/* PICTURES STILL TO DRAW. The count is on the label; this is the glance, and
   the colour is GREEN because the thing behind the button MAKES something —
   the same meaning it carries everywhere else in this app. Not filled: the
   button only opens a panel, and filled green is reserved for the press that
   actually spends. `border-color` rather than the `border` shorthand, which
   this sheet has been bitten by four times. */
.pack-actions .pack-pics.wants {
  border-color: var(--good);
  background: rgba(47, 224, 122, 0.12);
}
.pack-actions .pack-pics.wants:hover { background: rgba(47, 224, 122, 0.22); }

/* Delete is quiet until you go near it, so it is never mistaken for Launch in
   a hurry — and it only turns red under the cursor. */
.pack-del { color: var(--ink-dim); background: rgba(255, 255, 255, 0.04) !important; }
.pack-del:hover { color: var(--bad); border-color: rgba(255, 77, 94, 0.5); background: rgba(255, 77, 94, 0.1) !important; }

/* Spotify green, so the playlist is findable at a glance in a dark room. */
.pack-spotify { color: #0b0b14 !important; background: #1ed760 !important; border-color: transparent !important; }
.pack-spotify:hover { background: #29e26b !important; }

/* Loud on purpose: without backup, work disappears and nothing looks wrong
   until the day it has gone. */
.panel.backup-warn { border-color: var(--gold); background: rgba(255, 210, 63, 0.12); }
.panel.backup-warn h3 { color: var(--gold); }
.panel.backup-warn .tiny { font-size: var(--fs-body); line-height: 1.6; color: var(--ink); }

/* ---- reading a pack without leaving the console */
.overlay {
  position: fixed; inset: 0; z-index: 80;
  background: rgba(4, 4, 10, 0.78);
  backdrop-filter: blur(6px);
  display: grid; place-items: center;
  padding: 20px;
  animation: cardIn 180ms ease-out;
}
.sheet {
  width: min(860px, 100%);
  max-height: 88vh;
  display: flex; flex-direction: column;
  border-radius: var(--r-card);
  background: #12121e;
  border: 1px solid var(--panel-line);
  box-shadow: var(--shadow);
  overflow: hidden;
}
/* The popover editor holds a whole pack's worth of rounds and questions —
   the same content editor.js lays out on a full page — so it gets more
   room than a read-through's summary lines need. */
.sheet-editor { width: min(980px, 100%); }
.sheet-head {
  flex: 0 0 auto;
  display: flex; align-items: flex-start; gap: 14px;
  padding: 16px 18px;
  border-bottom: 1px solid var(--panel-line);
  background: rgba(255, 255, 255, 0.03);
}
.sheet-title { font-size: var(--fs-head); font-weight: 900; margin-bottom: 3px; }
.sheet-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }
.sheet-actions .minor { cursor: pointer; }
.sheet-body { flex: 1 1 auto; overflow-y: auto; padding: 16px 18px 26px; }

.pv-round-head {
  display: flex; align-items: baseline; gap: 10px;
  margin: 18px 0 10px;
  font-size: var(--fs-field); font-weight: 900;
  padding-bottom: 7px;
  border-bottom: 1px solid var(--panel-line);
}
.pv-round-head .tiny { margin-left: auto; }
.pv-round:first-child .pv-round-head { margin-top: 0; }

.pv-q { padding: 11px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
.pv-prompt { display: flex; gap: 9px; font-size: var(--fs-ctl); font-weight: 700; line-height: 1.35; margin-bottom: 8px; }
.pv-num {
  flex: 0 0 auto; min-width: 1.7em;
  color: var(--ink-dim); font-weight: 900; font-size: var(--fs-note);
}
.pv-opts { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; margin-left: calc(1.7em + 9px); }
.pv-opt {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 9px; border-radius: var(--r-field);
  font-size: var(--fs-note);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid transparent;
  color: var(--ink-dim);
}
.pv-opt.right {
  background: rgba(47, 224, 122, 0.18);
  border-color: var(--good);
  color: var(--ink);
  font-weight: 800;
}
.pv-letter { font-weight: 900; font-size: var(--fs-tag); opacity: 0.7; }
.pv-note, .pv-cue {
  margin: 7px 0 0 calc(1.7em + 9px);
  font-size: var(--fs-note); color: var(--ink-dim); line-height: 1.5;
}
.pv-cue { color: var(--gold); }

.pv-tracks { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 4px; }
.pv-track { display: flex; align-items: baseline; gap: 8px; padding: 6px 9px; border-radius: var(--r-field); background: rgba(255,255,255,0.04); font-size: var(--fs-note); }
.pv-track .pv-tt { font-weight: 700; }
.pv-track .pv-ta { margin-left: auto; color: var(--ink-dim); font-size: var(--fs-mini); }

/* The title OPENS THE CARD — it is a disclosure, not a link.
   It used to open the read-through, so it was styled like one: coloured and
   underlined on hover. That is link semantics on a control that expands
   something in place, and after a click it left the heading of the open card
   sitting there blue and underlined as though it were a URL. It brightens and
   its caret comes up instead, which is what every other thing that opens on
   this page does. */
button.pack-title {
  background: none; border: none; padding: 0; text-align: left;
  color: var(--ink); font-family: inherit; cursor: pointer;
}
button.pack-title:hover { color: #fff; }
button.pack-title:hover::after { opacity: 1; }


@media (max-width: 560px) {
  .pv-opts { grid-template-columns: 1fr; margin-left: 0; }
  .pv-note, .pv-cue { margin-left: 0; }
}

.pv-warn {
  margin-bottom: 16px; padding: 13px 15px; border-radius: var(--r-field);
  background: rgba(255, 210, 63, 0.12); border: 1px solid rgba(255, 210, 63, 0.5);
  font-size: var(--fs-body); line-height: 1.55;
}
.pv-warn ul { margin: 7px 0 0; padding-left: 0; list-style: none; }

/* Once everything is ticked the panel stops shouting. Leaving it yellow would
   mean the one glance you get before a gig still says "problem". */
.pv-warn.all-clear {
  background: rgba(86, 211, 100, 0.09);
  border-color: rgba(86, 211, 100, 0.4);
}
.pv-warn-head.all-clear { color: var(--good, #56d364); }

/* Each flag is a row you can tick off, so a long list becomes a job you can
   work through rather than a wall of yellow. */
.pv-flag {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 6px 0; border-bottom: 1px solid rgba(255, 210, 63, 0.16);
}
.pv-flag:last-child { border-bottom: 0; }
.pv-flag-text { flex: 1 1 auto; }
.pv-tick {
  flex: 0 0 auto; cursor: pointer;
  font: inherit; font-size: var(--fs-mini); font-weight: 700;
  padding: 4px 11px; border-radius: var(--r-pill);
  color: var(--ink); background: rgba(255, 255, 255, 0.09);
  border: 1px solid rgba(255, 255, 255, 0.2);
}
.pv-tick:hover:not(:disabled) { background: rgba(255, 255, 255, 0.17); }
.pv-tick:disabled { opacity: 0.5; cursor: default; }

/* Checked ones are kept, not deleted — dismissing one by mistake should be
   undoable, and rewriting a question means wanting its flag back. */
.pv-cleared { margin-top: 9px; }
.pv-cleared-toggle {
  cursor: pointer; font: inherit; font-size: var(--fs-mini);
  padding: 0; border: 0; background: none; color: var(--ink-dim);
  text-decoration: underline; text-underline-offset: 3px;
}
.pv-cleared-toggle:hover { color: var(--ink); }
.pv-flag.done .pv-flag-text { opacity: 0.55; text-decoration: line-through; }

/* ================================================================== BRAND
   Logo and name, top left, linking back to the console — the way the corner of
   any website behaves. */

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  color: var(--ink);
  border-radius: var(--r-field);
  padding: 4px 8px 4px 4px;
  margin-left: -4px;
  transition: background 160ms;
}
.brand:hover { background: rgba(255, 255, 255, 0.07); }
.brand-mark { flex: 0 0 auto; display: block; }
.brand-name {
  font-weight: 900;
  letter-spacing: -0.01em;
  white-space: nowrap;
}

/* ---- the stacked wordmark: "Mark's" over "Quizporium".

   Whimsy without a font file, because there is no build step and a webfont is
   a request that can fail on pub wifi. So it comes from the ARRANGEMENT: the
   possessive small and tilted, sitting on the product name like a signature,
   and the product name heavy and wide enough to rule it off underneath.

   The tilt is 5 degrees and no more. It is the difference between playful and
   broken, and this is read from the back of a room. */
.brand-name.stacked {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  /* Tight, so the possessive nests into the wordmark rather than sitting on a
     line above it — see the note on `.brand-who`. */
  line-height: 0.78;
  white-space: nowrap;
}
.brand-who {
  /* WHOSE NIGHT IT IS — 0.82em now, up again from 0.70 and from 0.58 before
     that. It is the half a quizmaster is actually selling: the app name is
     the product, their name is the act. Still the smaller of the two, because
     the stack only works while one of them is the label — but no longer a
     superscript on somebody else's brand.

     AND IT IS A SIGNATURE RATHER THAN A SMALL LABEL, which is the "more
     interesting" half. Whimsy without a font file, because there is no build
     step and a webfont is a request that can fail on pub wifi — so it comes
     from the arrangement instead:

     - ITALIC, which is what a hand does and a label does not;
     - the LIGHTER end of the account's own gradient rather than grey, so it
       belongs to the quizmaster's colours like everything else that is
       theirs. Grey said "caption"; this says "whose";
     - wider TRACKING, because a signature is written across a space rather
       than packed into one;
     - and the tilt stays 5 degrees. It is the difference between playful and
       broken, and this is read from the back of a room.

     Behind an `@supports` for the same reason every other gradient text in
     this app is: gradient text is TRANSPARENT text, and a browser without
     `background-clip` would draw the quizmaster's own name invisible on every
     screen in the app. */
  font-size: 0.82em;
  font-weight: 800;
  font-style: italic;
  /* ---- E3, chosen from five variations on 15 August 2026.
   *
   * The old arrangement stacked FIVE devices on two words — italic, a 5°
   * tilt, wide tracking, a size drop and a different colour — and that is
   * what read as janky rather than any one of them. The tilt was the worst
   * offender at small sizes: five degrees on a 12px word is about a pixel of
   * drift, which the eye takes as misalignment rather than as handwriting.
   *
   * E3 keeps the signature and stops it wobbling:
   *
   *  - **2° instead of 5°** — still hand-placed, no longer crooked;
   *  - **NESTED rather than stacked.** A negative bottom margin sits the
   *    possessive INTO the wordmark instead of above it, so the two read as
   *    one object. That is the whole difference between this and the others,
   *    and it is why it wanted checking at 15px as well as 26 — nesting has
   *    the most to gain at large sizes and the least room to survive small;
   *  - **tracking back to normal**, because letters spread apart is a fifth
   *    signal saying the same thing the italic already said.
   */
  letter-spacing: 0;
  color: var(--ink-dim);
  transform: rotate(-2deg);
  transform-origin: 0 100%;
  /* Nudged in so the tilt does not put it left of the product name's stem. */
  margin-left: 0.04em;
  margin-bottom: -0.04em;
}
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .brand-who {
    background: linear-gradient(120deg, var(--hot-2), var(--mark-3));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    /* Italic overhangs its box, and a clipped background clips to the box —
       so the tail of a "k" or a "y" loses its colour without this. */
    padding-right: 0.12em;
  }
}
.brand-app {
  font-weight: 900;
  letter-spacing: -0.015em;
  background: linear-gradient(120deg, var(--hot), var(--hot-2) 60%, var(--mark-3));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* On the projector the possessive is the quieter half but still has to carry
   to the back of a room, so it keeps more of the size than it does elsewhere. */
/* On the projector it is read from the back of a room, so it keeps more of
   the size again — and drops the gradient for solid ink, because at 26px on a
   screen six feet wide a two-colour ramp across six letters is mud. */
.screen .brand-who { font-size: 0.86em; opacity: 0.9; }
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .screen .brand-who { background: none; color: var(--ink); }
}
/* ONE size for the name on every page that carries the menu, so the block to
   the left of the chips is the same width and the chips line up as you move
   between pages rather than shifting a few pixels each time. */
.console .brand-name, .host .brand-name, .editor .brand-name { font-size: var(--fs-ctl); }
/* The owner page drops the logo and the name straight into the slot rather
   than wrapping them in a `.brand` link, so the slot has to be the flex row —
   otherwise the name falls under the mark the moment anything shares the bar
   with it, which is what the hat switch does. */
.console .topbar #brandSlot { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }

/* THE MENU, next to the logo, on every page a quizmaster drives — the console,
   the control view, the editor and the owner page. Built by `navMenu()` in
   `client.js` so four copies cannot drift; styled once here for the same
   reason. Deliberately NOT on the projector or a player's phone: one is
   pointed at a room and the other belongs to somebody in it.

   Quiet on purpose: these are the way OUT of the page, and the page's own loud
   things — Launch, Take control, Write it — have to stay the loudest. The page
   you are on is lit rather than dropped, so the menu answers "where am I" as
   well as "where can I go". */
/* `flex: 0 0 auto` — THE MENU NEVER GIVES WAY. As a shrinkable item it was
   squeezed by whatever shared its row, and because the chips are `nowrap` they
   did not shrink with it: they spilled straight over the top of the text
   beside them. On the owner page that put the Owner chip on top of the email
   address. Everything else on the bar gives way instead; on a phone the media
   query below hands the menu a line of its own. */
/* ONE ROW THAT SCROLLS — not a row that wraps.
   On a 390px control view this read "Console | Worksh" with the last three
   doors past the right edge, so four of the five could not be reached from the
   page a host drives the night from. Wrapping was tried first and was WORSE:
   five pills became five ROWS in a sticky header, ~270px of a 844px screen,
   and the header then covered the panel behind it — a control that was merely
   off-screen became a control with something on top of it.
   The header has to stay one row tall, so the row scrolls instead. This is not
   the horizontal-scroll arrangement the console's TAB bar deleted: that was
   replaced by a vertical column because there was room for one. There is no
   room for one inside a fixed header. */
.topnav {
  display: flex; align-items: center; gap: 6px;
  flex: 1 1 auto; min-width: 0; flex-wrap: nowrap;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.topnav::-webkit-scrollbar { display: none; }
.topnav > * { flex: 0 0 auto; }
.topnav a {
  /* THE TOUCH FLOOR, on the one page driven with a thumb in a dark pub. These
     were 33px tall. */
  min-height: 44px; display: inline-flex; align-items: center;
  padding: 8px 14px;
  border-radius: var(--r-pill);
  border: 1px solid var(--panel-line);
  color: var(--ink-dim);
  text-decoration: none;
  font-size: var(--fs-note);
  font-weight: 700;
  white-space: nowrap;
}
.topnav a:hover { color: var(--ink); border-color: var(--hot); }
.topnav a:active { transform: scale(0.96); }
/* Where you are: the chip INVERTED — filled gold, dark text — so it reads at a
   glance rather than being a slightly different outline among five. The app's
   own gold rather than the hot gradient, because that gradient means "press
   this" and the page you are already on is the one thing in the row there is
   no point pressing. `--gold` is a variable, so it follows every seasonal look
   and the quizmaster's own scheme rather than being one fixed yellow. */
.topnav a.here {
  color: var(--ink);
  border-color: var(--hot);
  /* The fallback first, then the real thing. `color-mix` is recent, and a
     projector browser too old for it would drop the declaration entirely and
     leave the chip with no fill at all — readable, but no longer obviously
     "you are here". The fallback is the default scheme's own pink, so an old
     browser gets a sensible tint rather than nothing; a current one gets
     whatever the quizmaster's colours actually are. */
  background: rgba(255, 46, 136, 0.18);
  background: color-mix(in srgb, var(--hot) 18%, transparent);
}
.topnav a.here:hover { color: var(--ink); border-color: var(--hot); }

/* The control view's bar puts the brand above the "where am I" line, with the
   way back to the console on the end of that row — top of the screen, and a
   long way from the fixed action bar at the bottom that drives the night, so
   a thumb reaching for Next cannot land on it. */
.host .bar { flex-wrap: wrap; }
.host .bar .bar-top {
  flex: 1 1 100%;
  /* And the ROW needs it as well as the menu inside it. A flex item's default
     minimum is its content, so the row was sized to logo-plus-the-chips and
     grew past the phone before the menu ever got the chance to scroll. */
  min-width: 0;
  /* `flex-start`, NOT `space-between`. Pushed to the right the menu sat in a
     different place here from every other page, so the chips appeared to jump
     as you walked between them — which is the whole complaint a menu exists to
     answer. Beside the logo, everywhere. */
  /* 14px, the console topbar's gap, so the menu begins at the same pixel. */
  display: flex; align-items: center; justify-content: flex-start; gap: 14px;
  margin-bottom: 2px;
}
/* On the control view the menu SCROLLS rather than wrapping, always and not
   only on a phone: that bar is sticky, and a second row appearing mid-night
   would push the question and the answer key down the screen.

   `flex: 1 1 0` with `min-width: 0` is what makes `overflow-x` actually bite.
   A flex item's default minimum is its content, so `overflow: auto` on its own
   does nothing at all — the item simply grows to fit the chips and takes the
   page sideways with it, which is the same min-content trap as the pack
   dropdown and the generator's theme box. */
/* …AND A FLOOR, or "shrinkable" turns into "gone".

   `flex: 1 1 0` hands the menu whatever is LEFT, and on a 320px iPhone SE the
   logo (125px) and the hat slot (105px) plus the gaps come to more than the
   bar, so what was left was **nothing**: the nav was allotted 0 pixels. A
   scroll container with no width cannot be scrolled, so the menu was not
   merely tight, it was unreachable — the first chip clipped mid-word as
   "Consol" and the other two off the edge, on the one page driven from a phone
   in a dark pub. Measured at 390 as well: 60px of a 237px menu.

   96px is one whole chip and the edge of the next, which is the thing that
   says "there is more, swipe" rather than looking like a chip that got cut.
   Overflow still bites, because what matters for that is the used width being
   under the content width (96 < 237) — `min-width: 0` was only ever needed to
   let the item shrink below its own content, and a smaller floor does that
   just as well. The logo and the hat slot give way instead, which is the right
   way round: they are a label and an indicator, and this is a door. */
.host .bar .topnav { flex: 1 1 0; min-width: 96px; overflow-x: auto; scrollbar-width: none; }
.host .bar .topnav::-webkit-scrollbar { display: none; }
/* The editor's bar is already full of its own controls — a pack picker and
   five buttons — so the menu must NOT be the thing that gives way: as a
   shrinkable item it was squeezed to nothing and the menu simply was not there
   on that page. It keeps its size and the bar wraps instead. */
.editor .topbar { flex-wrap: wrap; row-gap: 10px; }

/* Big screen: brand in the corner all night, quiz title beside it. */
.screen .titles { display: flex; align-items: center; gap: 1.1vw; min-width: 0; }
/* The slot holds the logo and the name, side by side rather than stacked. */
.screen .titles #brandSlot { display: flex; align-items: center; gap: 0.7vw; }
.screen .titles .brand-mark { width: 3.4vh; height: 3.4vh; }
.screen .titles .brand-name {
  font-size: 1.9vh;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  opacity: 0.85;
}
.screen .titles .title {
  padding-left: 1.1vw;
  border-left: 1px solid var(--panel-line);
  opacity: 0.7;
}

/* Players see it too — they are looking at their phone all night. */
.phone-brand {
  display: flex;
  align-items: center;
  gap: 8px;
  justify-content: center;
  padding-bottom: 4px;
  opacity: 0.85;
}
.phone-brand .brand-name { font-size: var(--fs-body); letter-spacing: 0.02em; }

@media (max-width: 430px) {
  .console .brand-name { font-size: var(--fs-field); }
  /* On a phone the bar cannot hold the logo, five doors, the hat switch and a
     sign-out on one line, so the doors take a line of their own underneath —
     the same shape the control view's bar already uses. They SCROLL sideways
     rather than wrapping to a third line: the topbar is sticky, so a bar that
     grew a row would eat the top of the page on every tab all night. */
  .console .topbar, .editor .topbar { flex-wrap: wrap; row-gap: 8px; }
  /* SCOPED to the two bars that WRAP. It was a bare `.topnav`, so it reached
     the control view as well — whose bar is deliberately `nowrap` and already
     has a scrolling rule of its own — and there `order: 9` sent the menu to
     the far right, past the hat switch, squeezed to a sliver by
     `flex: 1 1 100%` in a row with nowhere to put it. The one page where the
     menu has to be beside the logo is the one where it was not. */
  .console .topnav, .editor .topnav {
    order: 9;
    flex: 1 1 100%;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .console .topnav::-webkit-scrollbar, .editor .topnav::-webkit-scrollbar { display: none; }
  /* A HALF-CUT WORD IS NOT A SCROLL HINT. "My accou" at the right edge reads
     as broken, not as "there is more" — the sideways scroll above is real and
     deliberate, this just says so. A static fade on the last stretch of the
     row rather than a JS-driven one that hides once fully scrolled: cheap,
     and wrong only in the harmless direction — it stays after the last door
     is reached and still just as truthfully says "there is more to scroll to". */
  @supports (mask-image: linear-gradient(#000, #000)) or (-webkit-mask-image: linear-gradient(#000, #000)) {
    .console .topnav, .editor .topnav {
      -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
      mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
    }
  }

  /* THE CONTROL VIEW CANNOT WRAP, SO SOMETHING ELSE HAS TO GIVE — and the
     wordmark is the right thing to lose.

     That bar is `nowrap` on purpose: it is sticky, and a second row appearing
     mid-night would push the question and the answer key down the screen. But
     at 320px the logo (125px) and the hat slot (105px) plus the gaps already
     came to more than the bar HAD, before the menu was given a single pixel —
     which is why the menu was allotted zero and clipped to "Consol". Giving it
     a floor without finding the space just moved the overflow onto the page.

     So on a phone the brand keeps its MARK and drops its WORDS. It is the same
     link doing the same job in the same place — and the alternative is a menu
     that is not there at all, which is a worse breach of "the same topbar on
     every page" than a smaller logo. The name is on the projector six feet
     wide and on every player's phone; this is the one screen where nobody
     needs telling whose night it is. Console and editor are untouched: their
     bars wrap, so they have the room. */
  .host .bar .brand-name { display: none; }
  .host .bar-top { gap: 10px; }
}

/* ---- renaming from the read sheet
   Titles look like text until you click them, so the sheet still reads as
   something to read rather than a form to fill in. */
input.sheet-title {
  width: 100%;
  font-size: var(--fs-head);
  font-weight: 900;
  font-family: inherit;
  color: var(--ink);
  background: none;
  border: 1px solid transparent;
  border-radius: var(--r-field);
  padding: 3px 7px;
  margin: 0 0 3px -7px;
}
input.sheet-title:hover { background: rgba(255, 255, 255, 0.06); }
input.sheet-title:focus { outline: none; border-color: var(--hot); background: rgba(255, 255, 255, 0.08); }

input.pv-round-name {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--fs-field);
  font-weight: 900;
  font-family: inherit;
  color: var(--ink);
  background: none;
  border: 1px solid transparent;
  border-radius: var(--r-field);
  padding: 3px 7px;
  margin-left: -7px;
}
input.pv-round-name:hover { background: rgba(255, 255, 255, 0.06); }
input.pv-round-name:focus { outline: none; border-color: var(--hot); background: rgba(255, 255, 255, 0.08); }

.sheet-actions .go {
  border: none; border-radius: var(--r-field); padding: 9px 16px; cursor: pointer;
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  color: #12000c; font-weight: 900; font-size: var(--fs-note);
}
.sheet-actions .go:disabled { opacity: 0.6; cursor: default; }

/* One tap from the cue to the track, so there is nothing to search for while
   a room waits. Host view only — the room never sees these. */
.cue-links { display: flex; gap: 8px; margin-top: 11px; }
.cue-open {
  flex: 1 1 auto;
  text-align: center;
  padding: 11px 12px;
  border-radius: var(--r-field);
  text-decoration: none;
  font-size: var(--fs-body);
  font-weight: 900;
  background: #1db954;           /* Spotify green — instantly recognisable */
  color: #04240f;
}
.cue-open.ghost { background: rgba(255, 255, 255, 0.1); color: var(--ink); border: 1px solid var(--panel-line); }

.pv-playlist {
  display: inline-block;
  margin: 2px 0 8px;
  font-size: var(--fs-note);
  font-weight: 700;
  color: #1db954;
}

/* ---- importing a list you already have */
.panel.import { border-color: rgba(29, 185, 84, 0.4); background: rgba(29, 185, 84, 0.06); }
.panel.import .gen-row .go { background: #1db954; color: #04240f; }
/* The paste box is the route now, not a fold-away alternative, so it is always
   open and sized to show a round's worth of tracks without scrolling. */
#impText {
  width: 100%; margin: 10px 0; padding: 11px 13px;
  border-radius: var(--r-field); border: 1px solid var(--panel-line);
  background: rgba(0, 0, 0, 0.3); color: var(--ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-note); line-height: 1.6; resize: vertical;
}
#impText:focus { outline: 2px solid #1db954; outline-offset: 1px; }

/* Tickboxes in the brand colour rather than the browser's blue, and big enough
   to hit with a thumb. */
.console input[type="checkbox"],
.editor input[type="checkbox"] { accent-color: var(--hot); width: 16px; height: 16px; cursor: pointer; }

/* ---- dropdowns
   A native select renders as a white slab on a dark page, which is exactly
   what it looked like: the loudest thing on a pack card, louder than Launch.
   So: the page's own dark fill, a gradient rim and a gradient chevron block,
   drawn rather than an emoji or a font glyph so every browser gets the same
   one. `color-scheme: dark` is the line that matters on a Mac and an iPhone —
   without it the OS still paints the open list white. */
.console select,
.editor select {
  appearance: none; -webkit-appearance: none;
  color-scheme: dark;
  padding: 8px 34px 8px 11px;
  border-radius: var(--r-field);
  font-family: inherit; font-size: var(--fs-note); font-weight: 800;
  color: var(--ink);
  background-color: rgba(255, 255, 255, 0.07);
  /* the chevron, then a gradient block behind it on the right-hand end */
  /*
   * THE CHEVRON SITS ON A SMALL BLOCK OF THE ACCOUNT'S OWN GRADIENT — and it
   * is the one deliberate difference between a dropdown and a text box.
   *
   * Taken off once, on the reasoning that a dropdown is furniture and three
   * gradient tabs beside a Launch button compete with it. The host looked at
   * both and put it back: *"the dropdowns with the small gradient where the
   * arrow is is perfect — adds a splash of colour but is obviously different
   * from the other button that has a full gradient."* He is right, and the
   * reason it does not compete is SIZE: a 27px tab and a full-width filled
   * button are not mistakable for one another, where two full-width filled
   * buttons would be.
   *
   * It is also not decoration. **It is the affordance that says "this
   * opens"** — which is exactly where a dropdown and a text box should differ,
   * because it is where they behave differently. Everything else about them
   * (height, fill, border, radius) stays identical.
   */
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%2312000c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
    linear-gradient(120deg, var(--hot), var(--hot-2));
  background-repeat: no-repeat, no-repeat;
  background-position: right 8px center, right center;
  background-size: 11px 8px, 27px 100%;
  border: 1px solid var(--panel-line);
  /* Wide enough that a short option like "4×4" is not squeezed up against the
     chevron block. */
  min-width: 92px;
  cursor: pointer;
  transition: border-color 140ms, background-color 140ms;
}
.console select:hover,
.editor select:hover { background-color: rgba(255, 255, 255, 0.13); }
.console select:focus-visible,
.editor select:focus-visible { outline: none; border-color: var(--hot); }
.console select:disabled,
.editor select:disabled { opacity: 0.5; cursor: default; }

/* Choosing tonight's card shape, on the pack card, above Launch — because the
   shape is a decision about tonight, not about the pack. */
/* NOT SHOUTED — capitals are for emphasis, never for labelling.
   These nine words are the launch bar's labels (Appearance, Secs per Q, Lobby
   game, Game sound, Playing, Winners, In the gaps, Card, Bingo prizes), and the
   rule names exactly three exceptions: the projector, the option letters A-F,
   and one-word badges. Nine multi-word labels on the console's main bar are
   none of those. It survived because the markup writes them in sentence case
   and only the stylesheet shouted them, so the diff looked right on either
   side. Told apart by BRIGHTNESS and weight instead, which is what the rule
   prescribes; the tracking stays, because that is spacing rather than volume. */
.pack-shape {
  display: flex; align-items: center; gap: 8px; margin: 8px 0 2px;
  font-size: var(--fs-mini); color: var(--ink-dim); font-weight: 800;
  letter-spacing: 0.04em;
}
/* `max-width` as well as `min-width: 0`, because a <select> will not go below
   the width of its LONGEST OPTION on its own — the trap this codebase has now
   hit eight times. `.lb-set` already had it; this one did not, and the lobby
   game picker put the longest option labels in the app on both. */
.pack-shape select { flex: 1 1 auto; min-width: 0; max-width: 100%; }
/* The seconds field is a number, not an option list — no longest-option trap
   to guard against, so it gets a fixed width instead of stretching the row. */
.pack-shape .seconds-pick { width: 60px; flex: 0 0 auto; }

/* What the last job said, kept where the panel rebuild cannot take it. */
.panel.done-banner {
  display: flex; align-items: flex-start; gap: 14px;
  border-color: rgba(47, 224, 122, 0.5);
  background: rgba(47, 224, 122, 0.09);
}
.panel.done-banner .done-text { flex: 1 1 auto; font-size: var(--fs-body); line-height: 1.55; }
.panel.done-banner .done-text a { color: var(--good); font-weight: 700; }
.panel.done-banner .done-close { flex: 0 0 auto; }
/* It worked, but something in it needs reading. A green box you have to read to
   the end of to find the problem is a box you stop reading to the end of. */
.panel.done-banner.warn {
  border-color: rgba(255, 176, 32, 0.55);
  background: rgba(255, 176, 32, 0.09);
}
.panel.done-banner.warn .done-text a { color: var(--gold); }
/* It did not work. Green is the default here, so a job that failed would
   otherwise announce itself in the colour that means "done" — and the reason
   is the only thing on the banner worth reading. */
.panel.done-banner.bad {
  border-color: rgba(255, 77, 94, 0.55);
  background: rgba(255, 77, 94, 0.1);
}
.panel.done-banner.bad .done-text a { color: var(--bad); }

/* Round 2 artwork, opened from a pack card. Free stand-ins and paid portraits
   are different decisions, so they are different buttons. */
.panel.pics { margin-top: 10px; }
.panel.pics .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.panel.pics .redo { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
.pics-slot:empty { display: none; }

/* ================================================================== PHOTOS
   Pictures from the room. On the projector they are a filmstrip along the
   bottom, on the phone a sheet with the filters, on the control view a grid
   small enough to scan and tap in the dark. */

/* ---- the projector */
.photo-strip {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  display: flex;
  gap: 0.8vw;
  padding: 1.2vh 2.2vw;
  overflow: hidden;
  align-items: flex-end;
  pointer-events: none;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.55), transparent);
}
.photo-strip .photo {
  margin: 0;
  flex: 0 0 auto;
  width: 13vh;
  border-radius: var(--r-card);
  overflow: hidden;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid var(--panel-line);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
  animation: pop 420ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.photo-strip .photo img { display: block; width: 100%; height: 13vh; object-fit: cover; }
.photo-strip .photo figcaption {
  font-size: 1.3vh; font-weight: 700; padding: 0.5vh 0.6vw;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Who picked each answer, under it, on the reveal only. Names the host can
   read straight off the screen while the room is still groaning. */
.keywho {
  display: flex; flex-wrap: wrap; gap: 5px;
  margin: 4px 0 8px 34px;
}
.keywho.shut { display: none; }
.keywho span {
  padding: 3px 8px; border-radius: var(--r-pill);
  font-size: var(--fs-mini); font-weight: 700;
  background: rgba(255, 255, 255, 0.07); color: var(--ink-dim);
  border: 1px solid var(--panel-line);
}
.keywho span.right { color: var(--good); border-color: rgba(47, 224, 122, 0.45); background: rgba(47, 224, 122, 0.12); }
.keywho.none { margin-left: 0; align-items: center; }
.keywho.none b { font-size: var(--fs-mini); color: var(--gold); margin-right: 2px; }

/* ---- the phone */
.camera-btn {
  position: fixed;
  right: 16px; bottom: 58px;
  width: 58px; height: 58px;
  border-radius: 50%;
  border: 1px solid var(--panel-line);
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  font-size: 26px; line-height: 1;
  cursor: pointer;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.5);
  z-index: 40;
}
.camera-btn:active { transform: scale(0.94); }

/* ---- chat, on an online night only
 *
 * The button sits ABOVE the camera one rather than beside it: two round
 * buttons side by side at the bottom right of a 320px phone leaves neither
 * with a comfortable thumb, and stacked they are the shape of every other
 * app's floating pair. It is deliberately quieter than the camera — outlined
 * rather than filled with the app's own gradient — because the gradient means
 * "the main thing to press", and on a quiz night that is never the chat.
 */
.chat-btn {
  position: fixed;
  right: 16px; bottom: 124px;
  width: 52px; height: 52px;
  border-radius: 50%;
  border: 1px solid var(--panel-line);
  background: var(--panel-solid, rgba(22, 22, 34, 0.94));
  font-size: 22px; line-height: 1;
  cursor: pointer;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.5);
  z-index: 40;
}
.chat-btn:active { transform: scale(0.94); }
.chat-badge {
  position: absolute; top: -4px; right: -4px;
  min-width: 20px; height: 20px; padding: 0 5px;
  border-radius: var(--r-pill);
  background: var(--hot); color: #fff;
  font-size: var(--fs-tag); font-weight: 900; line-height: 20px;
}

/* The sheet. Capped at a bit over half the screen so the ANSWERS are never
   entirely hidden behind it — a chat window that covers the question is the
   fastest way to lose somebody the points they came for. */
.chat-sheet {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 60;
  display: flex; flex-direction: column;
  max-height: 62vh;
  background: var(--panel-solid, rgba(16, 16, 26, 0.98));
  border-top: 1px solid var(--panel-line);
  border-radius: var(--r-card) 16px 0 0;
  box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.6);
  padding-bottom: env(safe-area-inset-bottom, 0);
}
.chat-head { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--panel-line); }
.chat-tabs { display: flex; gap: 6px; flex: 1 1 auto; min-width: 0; overflow-x: auto; }
.chat-tab {
  flex: 0 0 auto;
  padding: 6px 11px; border-radius: var(--r-pill);
  border: 1px solid var(--panel-line); background: none;
  color: var(--ink-dim); font-size: var(--fs-mini); font-weight: 800;
  cursor: pointer; white-space: nowrap;
}
.chat-tab.here { background: var(--gold); border-color: var(--gold); color: #12000c; }
.chat-dot { background: var(--hot); color: #fff; border-radius: var(--r-pill); padding: 0 5px; font-size: var(--fs-tag); }
.chat-x { flex: 0 0 auto; background: none; border: 0; color: var(--ink-dim); font-size: var(--fs-head); cursor: pointer; padding: 4px 6px; }

.chat-list { flex: 1 1 auto; overflow-y: auto; overscroll-behavior: contain; padding: 10px; display: grid; gap: 7px; align-content: start; }
.chat-msg { min-width: 0; overflow-wrap: anywhere; font-size: var(--fs-note); line-height: 1.4; }
.chat-who { font-weight: 900; color: var(--gold); margin-right: 6px; }
.chat-msg.mine .chat-who { color: var(--hot-2); }
.chat-empty { text-align: center; padding: 18px 8px; color: var(--ink-dim); }

.chat-quick { display: flex; gap: 6px; padding: 8px 10px 0; overflow-x: auto; }
.chat-emoji {
  flex: 0 0 auto; width: 40px; height: 40px;
  border-radius: var(--r-field); border: 1px solid var(--panel-line);
  background: none; font-size: 20px; line-height: 1; cursor: pointer;
}
.chat-emoji:active { transform: scale(0.92); }

.chat-say { display: flex; gap: 8px; padding: 8px 10px 10px; min-width: 0; }
.chat-say input { flex: 1 1 auto; min-width: 0; }
.chat-say.off input { opacity: 0.45; }
.chat-send { flex: 0 0 auto; padding: 0 16px; border-radius: var(--r-field); border: 0; font-weight: 900; background: linear-gradient(120deg, var(--hot), var(--hot-2)); color: #12000c; cursor: pointer; }
.chat-send:disabled { opacity: 0.4; }
.chat-why { padding: 0 12px 10px; text-align: center; }

.cam-overlay {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(0, 0, 0, 0.72);
  display: flex; align-items: flex-end; justify-content: center;
}
.cam-sheet {
  width: 100%; max-width: 560px;
  max-height: 92vh; overflow-y: auto;
  background: var(--bg-2);
  border: 1px solid var(--panel-line);
  border-radius: var(--r-card) 20px 0 0;
  padding: 16px;
}
.cam-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.cam-close {
  background: none; border: 0; color: var(--ink-dim);
  font-size: var(--fs-sub); cursor: pointer; padding: 4px 8px;
}
.cam-warn { color: var(--gold); }
.cam-pick {
  display: block; margin-top: 12px; padding: 18px;
  text-align: center; font-weight: 800; cursor: pointer;
  border: 2px dashed var(--panel-line); border-radius: var(--r-card);
}
.cam-stage { margin-top: 12px; }
/* SQUARE, because the photo is cropped square before it ever gets here — see
   drawFiltered. It used to be `height: auto` over a black background, which on
   a portrait phone photo meant two black bars down the sides of the preview
   and a frame that changed shape with whatever was photographed. */
.cam-canvas {
  width: 100%; aspect-ratio: 1; max-height: 44vh;
  display: block; margin: 0 auto;
  object-fit: contain; border-radius: var(--r-card); background: #000;
}

/* The looks. All of them on screen at once, each showing what it does to
   their own photo — a scrolling row of named grey pills got missed
   completely. */
.cam-looks-head {
  margin: 14px 0 8px;
  font-size: var(--fs-note); font-weight: 800; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--gold);
}
/* Directly under the photo, so the top of its own margin is the gap. */
.cam-send {
  width: 100%; margin-top: 12px; padding: 16px; cursor: pointer;
  font: inherit; font-size: var(--fs-lead); font-weight: 900;
  border: 0; border-radius: var(--r-card); color: #fff;
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
}
.cam-send:disabled { opacity: 0.6; }
.cam-status { margin-top: 10px; color: var(--bad); }

/* ---- the control view */
.panel.photos.off { opacity: 0.75; }
.panel.photos .photo-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.panel.photos .photo-head h3 { margin: 0; }
.photo-grid {
  display: grid; gap: 8px; margin-top: 10px;
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
}
.host-photo {
  position: relative; padding: 0; cursor: pointer;
  border: 1px solid var(--panel-line); border-radius: var(--r-field);
  overflow: hidden; background: none;
}
.host-photo img { display: block; width: 100%; height: 84px; object-fit: cover; }
.host-photo .who {
  display: block; font-size: var(--fs-tag); font-weight: 700; color: var(--ink);
  padding: 4px 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Says what a tap does before it does it. */
.host-photo .bin-badge {
  position: absolute; top: 5px; right: 5px;
  display: grid; place-items: center;
  width: 26px; height: 26px; border-radius: var(--r-field);
  color: #fff; background: rgba(214, 42, 62, 0.92);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.host-photo:active .bin-badge { transform: scale(0.9); }

/* ========================================================== PICK-THEM-ALL
   The fourth round type: several answers are right and you lock in all of
   them. Six options rather than four, so the grid gets a third row and the
   type comes down a little to keep it on one screen. */

.question.type-multi .options { grid-auto-rows: 1fr; }
.question.type-multi .option { font-size: 3.2vh; padding: 1.6vh 1.8vw; }

/* The instruction the room needs. Not the answer — which ones never reaches
   this screen at all. */
.pick-banner {
  flex: 0 0 auto;
  align-self: flex-start;
  margin-bottom: 1.6vh;
  padding: 0.6vh 1.4vw;
  border-radius: var(--r-pill);
  font-size: 2.4vh;
  font-weight: 800;
  background: rgba(75, 216, 255, 0.16);
  border: 1px solid var(--cool);
  color: var(--ink);
}
.pick-banner b { font-size: 1.2em; color: var(--cool); }

/* ---- the phone */
.answers.multi .answer-btn { position: relative; }
.answer-btn .tick {
  position: absolute;
  right: 12px; top: 50%;
  transform: translateY(-50%) scale(0.4);
  width: 26px; height: 26px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--good); color: #06210f;
  font-weight: 900; font-size: var(--fs-ctl);
  opacity: 0; transition: opacity 140ms, transform 140ms;
}
.answer-btn.picked { border-color: var(--good); background: rgba(47, 224, 122, 0.14); }
.answer-btn.picked .tick { opacity: 1; transform: translateY(-50%) scale(1); }

.lock-btn {
  flex: 0 0 auto;
  width: 100%;
  padding: 18px;
  cursor: pointer;
  font: inherit; font-size: var(--fs-head); font-weight: 900;
  color: #fff; border: 0; border-radius: var(--r-card);
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
}
.lock-btn:disabled {
  background: rgba(255, 255, 255, 0.09);
  color: var(--ink-dim);
  cursor: default;
}

/* Part marks get their own face. "Not this time" over a score of 180 reads
   as a bug. */
.result.part {
  background: rgba(255, 210, 63, 0.16);
  border: 1px solid var(--gold);
}
.result.part .big { color: var(--gold); }

/* ==================================================================== RULES
   The first slide of every quiz. The right half is the join code, as big as
   the projector allows — this slide is up while the room is still filling.
   Everything the room has to read lives in the left half. */

.rules { display: flex; flex-direction: column; height: 100%; justify-content: center; }
.rules h1 { font-size: 8vh; margin: 0 0 3vh; font-weight: 900; letter-spacing: -0.02em; }

.rules-split { display: grid; grid-template-columns: 1fr 1fr; gap: 3vw; align-items: center; }
.rules-left { min-width: 0; }

.rules-join { display: grid; justify-items: center; gap: 1.6vh; min-width: 0; }
.rules-join .qr-panel { width: 100%; max-width: min(44vw, 66vh); }
.rules-join .qr-panel .url { font-size: 2.4vh; }
.rules-join-head {
  font-size: 2.4vh; font-weight: 800; color: var(--gold); text-align: center;
}

/* The points box. The only thing on the slide besides the code, so it gets to
   be big: the number in orange, what it is for in white underneath it. */
.rules-score {
  padding: 3.4vh 2.4vw;
  border-radius: var(--radius);
  background: var(--panel);
  border: 1px solid var(--panel-line);
}
.rules-score-row { margin-bottom: 3.2vh; }
.rules-score-row:last-of-type { margin-bottom: 0; }
.rules-score-row .big {
  display: block;
  font-size: 7.4vh; font-weight: 900; line-height: 1;
  color: var(--hot-2);
}
.rules-score-row .big i {
  font-style: normal; font-size: 0.44em; font-weight: 900;
  letter-spacing: 0.06em; margin-left: 0.24em;
}
.rules-score-row .what {
  display: block; margin-top: 0.8vh;
  font-size: 2.6vh; font-weight: 600; color: var(--ink); line-height: 1.3;
}
.rules-note {
  margin-top: 3vh; padding-top: 2vh;
  border-top: 1px solid var(--panel-line);
  font-size: 2.2vh; color: var(--gold); font-weight: 700;
}

/* The mid-quiz scoreboard reuses the round board, with a line saying where in
   the quiz it was taken. */
.board-sub {
  font-size: 2.4vh; color: var(--ink-dim);
  margin: -1vh 0 2.4vh;
}

/* The scoreboard toggle shows its own state — you should be able to tell what
   is on the projector without turning round to look at it. */
.minor.on {
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  border-color: transparent;
  color: #fff;
}

/* ================================================================== ADVERTS
   A venue's slide, between rounds. This one earns money, so it gets the whole
   screen rather than a corner. */

.advert { display: flex; flex-direction: column; height: 100%; justify-content: center; }
.ad-venue {
  align-self: flex-start;
  font-size: 2vh; letter-spacing: 0.3em; text-transform: uppercase;
  color: var(--ink-dim); margin-bottom: 2vh;
}
.ad-body { display: flex; align-items: center; gap: 4vw; }
.ad-words { flex: 1 1 auto; min-width: 0; }
.ad-words h1 {
  font-size: 9vh; line-height: 0.98; margin: 0; font-weight: 900;
  letter-spacing: -0.02em;
  background: linear-gradient(120deg, var(--hot), var(--hot-2) 55%, var(--gold));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.ad-words p { font-size: 3.4vh; color: var(--ink); margin: 2.4vh 0 0; line-height: 1.3; }

.ad-image { flex: 0 0 32%; }
.ad-image img {
  width: 100%; height: auto; max-height: 56vh; object-fit: contain;
  border-radius: var(--radius);
}

/* Big enough to scan from a table at the back with a phone held badly. */
.ad-qr {
  flex: 0 0 auto;
  background: #fff; border-radius: var(--r-card); padding: 1.8vh;
  display: grid; justify-items: center; gap: 1vh;
  box-shadow: var(--shadow);
}
.ad-qr img { width: 26vh; height: 26vh; display: block; }
.ad-qr-label {
  color: #12121c; font-weight: 800; font-size: 1.9vh;
  max-width: 26vh; text-align: center;
}

/* ---- the console editor */
.ad-venue-in {
  display: block; width: 100%; margin-top: 6px;
  background: none; border: 0; color: var(--ink-dim);
  font: inherit; font-size: var(--fs-note); padding: 0;
}
.ad-slide {
  padding: 14px; margin-bottom: 12px;
  border-radius: var(--r-card); background: var(--panel);
  border: 1px solid var(--panel-line);
}
.ad-slide-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.ad-slide label { display: block; margin: 10px 0 4px; color: var(--ink-dim); }
.ad-slide input {
  width: 100%; padding: 10px 11px; border-radius: var(--r-field);
  background: rgba(0, 0, 0, 0.3); color: var(--ink);
  border: 1px solid var(--panel-line); font: inherit; font-size: var(--fs-body);
}
.ad-slide .ad-h { font-weight: 800; }

/* ---- the picker on the control view */
.ad-set {
  margin: 14px 0 6px; font-size: var(--fs-mini); font-weight: 800;
  letter-spacing: 0.02em; color: var(--ink);
}
.ad-pick {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; margin-bottom: 8px; padding: 14px;
  border-radius: var(--r-card); cursor: pointer; text-align: left;
  background: rgba(255, 255, 255, 0.07); color: var(--ink);
  border: 1px solid var(--panel-line); font: inherit;
}
.ad-pick-head { font-weight: 700; font-size: var(--fs-ctl); }
.ad-pick-tag {
  flex: 0 0 auto; font-size: var(--fs-tag); font-weight: 800; letter-spacing: 0.1em;
  padding: 3px 8px; border-radius: var(--r-pill);
  background: rgba(75, 216, 255, 0.2); color: var(--cool);
}

/* ============================================================ PHOTOS TAB
   A night's photos in the console: bin the duds, share the rest. */

.night { margin-bottom: 26px; }
.night-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 10px; }
.night-head h3 { margin: 0; font-size: var(--fs-lead); }
.night-head .minor { margin-left: auto; }

.night-grid {
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.cphoto {
  margin: 0; position: relative; overflow: hidden;
  border-radius: var(--r-card); background: var(--panel);
  border: 1px solid var(--panel-line);
}
.cphoto img { display: block; width: 100%; height: 150px; object-fit: cover; }
.cphoto figcaption {
  font-size: var(--fs-mini); font-weight: 700; padding: 7px 9px 8px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* A green edge means it is in the private repo and safe from a restart. */
.cphoto.filed { border-color: rgba(47, 224, 122, 0.5); }

/* ---- THE BIN ON A FILED PHOTO, bottom right.
 *
 * BOTTOM RIGHT BECAUSE THE TOP OF A PHOTO IS WHERE FACES ARE — this sits over
 * somebody's picture, so it goes in the corner least likely to be covering the
 * thing you are looking at to decide whether to delete it.
 *
 * On a dark plate rather than bare, because a bare icon over a photograph is
 * invisible on half of them and glaring on the other half: the plate makes it
 * the same control whatever is underneath. Red on hover, like everything that
 * destroys — and OUTLINED-feeling rather than a filled red slab, which would
 * be as loud as Launch on a grid of a hundred pictures.
 *
 * It stays visible rather than appearing on hover: there is no hover on the
 * phone this is driven from, and a control you can only find with a mouse is
 * one that does not exist on half the devices.
 */
.cphoto-bin {
  position: absolute; right: 6px; bottom: 6px; z-index: 2;
  width: 28px; height: 28px; padding: 0;
  display: inline-grid; place-items: center;
  border: 1px solid rgba(255, 255, 255, 0.22); border-radius: var(--r-pill);
  background: rgba(10, 8, 16, 0.62); color: var(--ink);
  cursor: pointer; opacity: 0.82;
}
.cphoto-bin:hover {
  opacity: 1; color: var(--bad);
  border-color: rgba(255, 77, 94, 0.55);
  background: rgba(10, 8, 16, 0.85);
}
.cphoto-bin:disabled { opacity: 0.4; cursor: default; }
.cphoto:not(.filed)::after {
  content: 'not filed';
  position: absolute; top: 8px; right: 8px;
  font-size: var(--fs-tag); font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase;
  padding: 3px 7px; border-radius: var(--r-pill);
  background: rgba(255, 210, 63, 0.9); color: #221a00;
}
/* ON THE GALLERY, OR NOT — a pill per photo, and it is a SWITCH.
   *"A little green pill to show it's on the public gallery for this night and
   a red one to show it isn't, and I can click one for each purpose."*

   It replaced the old grey "Screen only" badge in the same corner. That said
   what the camera guess THOUGHT; this says what will actually happen, which is
   the same fact once a human can overrule it — and two badges on one
   photograph saying overlapping things is a label collision.

   GREEN AND RED ARE THE APP'S FIXED PAIR and this is squarely what they mean:
   good/on versus wrong/off, read at a glance across eighteen thumbnails. It is
   OUTLINED rather than filled — the destructive rule, applied to both halves
   so neither shouts louder than the photograph. */
/* NO WORDS ON IT — *"just be an on off button with green for on and red for
   off, no text needed but it must be clickable"*. Eighteen of them is the
   argument: a label repeated across a grid stops being read and becomes
   furniture, where a colour is answered at a glance.

   FILLED, unlike a destructive button, and that is not a break of the
   outlined-never-filled rule: this is a LAMP rather than a button you press to
   destroy something. With no text, the fill IS the message, and an outlined
   ring at this size reads as a smudge on the photograph.

   A DARK RING ROUND IT so it survives whatever the picture is — green on a
   sunlit wall and red on a curtain both vanish without one. */
.cphoto-pub {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  width: 18px; height: 18px; padding: 0; border-radius: 999px;
  cursor: pointer;
  border: 2px solid rgba(10, 8, 16, 0.75);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}
.cphoto-pub.is-on { background: var(--good); }
.cphoto-pub.is-off { background: var(--bad); }
/* THE TARGET IS BIGGER THAN THE DOT. An 18px lamp is the right size to read
   across a grid and the wrong size to hit with a thumb, so the hit area is
   pushed out to the touch floor without the dot growing. */
.cphoto-pub::after { content: ''; position: absolute; inset: -15px; }
.cphoto-pub:hover { transform: scale(1.15); }
.cphoto-pub:disabled { opacity: 0.5; cursor: default; }
@media (prefers-reduced-motion: reduce) { .cphoto-pub:hover { transform: none; } }
/* Smaller on the wall's tighter tiles, but never below something a thumb can
   find — the ::after hit area carries that either way. */
.community-wall .cphoto-pub { width: 14px; height: 14px; top: 5px; right: 5px; }
/* ---- THE PIN — "this one leads on the night's card".

   BOTTOM LEFT, opposite the bin, so the control that chooses and the control
   that destroys are never adjacent under a thumb. The lamp keeps its own
   top-right corner.

   IT IS A PREFERENCE, NOT A STATE OF THE PHOTOGRAPH, so it does not borrow the
   lamp's green and red: those mean "public" and "not", which is the lamp's
   question and must not be said twice with the same colours. Off is quiet, on
   is the account's own `--hot` — the colour that follows a scheme, which is
   exactly what a preference should wear. Filled on, outlined off, because at
   15px an outline-only difference is not a difference.

   The same dark ring and oversized hit area as the lamp, for the same reasons:
   a mark this size disappears on a bright photograph, and a 22px control is
   the right size to read and the wrong size to press. */
.cphoto-pin {
  position: absolute; bottom: 8px; left: 8px; z-index: 2;
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; padding: 0; border-radius: 999px; cursor: pointer;
  color: rgba(255, 255, 255, 0.85); background: rgba(10, 8, 16, 0.72);
  border: 2px solid rgba(10, 8, 16, 0.75);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}
.cphoto-pin.is-on {
  color: #fff;
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  border-color: rgba(255, 255, 255, 0.55);
}
.cphoto-pin::after { content: ''; position: absolute; inset: -13px; }
.cphoto-pin:hover { transform: scale(1.15); }
@media (prefers-reduced-motion: reduce) { .cphoto-pin:hover { transform: none; } }
.community-wall .cphoto-pin { width: 18px; height: 18px; bottom: 5px; left: 5px; }
.community-wall .cphoto-pin svg { width: 12px; height: 12px; }

.cphoto-acts {
  position: absolute; left: 0; right: 0; bottom: 34px;
  display: flex; gap: 6px; justify-content: center;
  opacity: 0; transition: opacity 140ms;
}
.cphoto:hover .cphoto-acts, .cphoto:focus-within .cphoto-acts { opacity: 1; }
.cphoto-acts button {
  cursor: pointer; font: inherit; font-size: var(--fs-mini); font-weight: 700;
  padding: 6px 12px; border-radius: var(--r-pill);
  color: var(--ink); background: rgba(10, 10, 18, 0.86);
  border: 1px solid var(--panel-line);
}
.cphoto-acts button { display: inline-flex; align-items: center; gap: 5px; }
.cphoto-acts .bin { color: var(--bad); }

/* Touch screens have no hover, and this tab is used on a phone. */
@media (hover: none) {
  .cphoto-acts { opacity: 1; position: static; padding: 0 8px 8px; }
}

/* ============================================================ JOIN CORNER
   A small code on every screen where somebody could still walk in. Top right,
   under the topbar, so it never collides with the photo strip along the
   bottom. Deliberately absent during a question — the room should be looking
   at the answers, not at this. */

.join-corner {
  position: absolute;
  top: 8.5vh; right: 2.2vw;
  display: flex; align-items: center; gap: 1vw;
  padding: 1.1vh 1.2vw;
  border-radius: var(--r-card);
  background: rgba(255, 255, 255, 0.94);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  animation: pop 400ms cubic-bezier(0.2, 1.4, 0.4, 1);
  /* ABOVE THE BIG PHOTO, and this is a joining bug rather than a layering
     preference. `.photo-big` is a full-stage scrim at z-index 3, so for the
     four and a half seconds a photo was up it painted straight over this card
     and took the QR down to near-black — unscannable, on the one control that
     lets somebody into the game. Reported from a real night: people could not
     join while photos were coming in, which in a busy lobby is most of the
     time. The photo moves aside for it as well (see `.photo-big.beside-join`);
     this is the guarantee that the code is never dimmed even if it does not. */
  z-index: 4;
}
.join-corner img { width: 11vh; height: 11vh; display: block; }
.join-corner-words { display: grid; gap: 0.3vh; color: #12121c; }
.join-corner-words b { font-size: 2vh; font-weight: 900; }
.join-corner-words span { font-size: 1.6vh; font-weight: 700; opacity: 0.72; }

/* Real errors in the read-through: these stop the quiz being played, so they
   must not look like the advisory flags sitting under them. */
.pv-warn.pv-broken {
  border-color: rgba(255, 77, 94, 0.55);
  background: rgba(255, 77, 94, 0.08);
}
.pv-warn.pv-broken .pv-warn-head { color: var(--bad); }

/* Doing something about "answers land A x15 B x10 — lopsided". Sits on the end
   of that same line, so the complaint and the fix are one sentence. */
.pv-even {
  display: inline-block;
  margin-left: 8px;
  padding: 3px 10px;
  border: 1px solid var(--panel-line);
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.06);
  color: var(--ink);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
.pv-even:hover { background: rgba(255, 255, 255, 0.14); }
.pv-even.urge {
  border-color: var(--gold);
  background: rgba(255, 176, 32, 0.16);
  color: var(--gold);
}
/* Offered but refused, with the reason in the title — a button that vanishes
   mid-game just looks broken. */
.pv-even.off { opacity: 0.5; cursor: default; }
.pv-even.off:hover { background: rgba(255, 255, 255, 0.06); }

/* ============================================================ first letter
   The alphabet round. No options: the question is asked, the phone shows a
   keyboard, and only the first letter of the answer has to be right. */

/* The projector's twenty-six. Deliberately NOT the two-column option grid —
   at 3.9vh a row each they would run off the bottom of the screen. A tile is
   just the letter, big enough to read from the back, with the count under it
   as the answers come in. */
.options.alphabet {
  grid-template-columns: repeat(9, 1fr);
  grid-auto-rows: 1fr;
  gap: 1.4vh 1vw;
}
.options.alphabet .option {
  flex-direction: column;
  justify-content: center;
  gap: 0.3vh;
  padding: 1vh 0.4vw;
  font-size: 6vh;
  font-weight: 900;
  /* So the grid can give height back when the answer appears above it on the
     reveal, rather than the two of them ending up on top of each other. */
  min-height: 0;
  overflow: hidden;
}
/* The count under each letter as the answers come in. Its space is held even
   when it is empty, so the letters do not jump the moment somebody answers. */
.options.alphabet .option .tally { margin-left: 0; font-size: 0.34em; min-height: 1.1em; }

/* The answer said out loud on the reveal. A lit-up letter is not an answer —
   the room has to be told it was Fleetwood Mac, in words, or the reveal says
   nothing. Used on the projector and on the host's own answer key. */
.answer-said {
  display: flex;
  align-items: center;
  gap: 0.8em;
  margin: 1.4vh 0;
}
.answer-said .answer-letter {
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 2em; height: 2em;
  border-radius: var(--r-card);
  background: var(--good);
  color: #06110a;
  font-weight: 900;
}
.answer-said .answer-words { font-weight: 900; }
.screen .answer-said { font-size: 4.6vh; }
.host .answer-said { font-size: var(--fs-sub); margin: 4px 0 10px; }
.host .answer-said .answer-letter { border-radius: var(--r-field); }

/* The phone's keyboard. Five across, not nine like the projector: nine keys on
   a 320px phone is 28 pixels each, which nobody hits in a dark pub against a
   clock. The ORDER is the same, and that is the part that matters — a player
   looking for F already knows which letter they want. */
.keyboard {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1 1 auto;
  justify-content: center;
  min-height: 0;
}
/* The six rows share whatever height is left rather than each claiming a size
   of its own. A fixed key height fits one phone and pushes Z off the bottom of
   the next one, and a row you have to scroll to is a row nobody presses in
   time — which on a 320px iPhone SE is exactly what happened. */
.keyboard-row { display: flex; gap: 6px; flex: 1 1 0; min-height: 0; }
.answer-btn.key {
  flex: 1 1 0;
  min-width: 0;
  min-height: 0;
  height: 100%;
  padding: 0;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-size: clamp(20px, 7vw, 34px);
  font-weight: 900;
  border-radius: var(--r-card);
}
/* Z sits on its own on the last row. Left where the A above it is rather than
   stretched across five columns — a key five times the width of every other
   one reads as a different kind of button. */
.keyboard-row:last-child .answer-btn.key { flex: 0 0 calc((100% - 24px) / 5); }
/* A count per round type in the generator. Unticking greys the number rather
   than hiding it, so what you typed is still there when you tick it back on. */
.gen-rounds {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 8px;
  margin: 10px 0 4px;
}
.gen-round {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-radius: var(--r-card);
  border: 1px solid var(--panel-line);
  background: rgba(255, 255, 255, 0.04);
  cursor: pointer;
  transition: opacity 160ms, border-color 160ms;
}
.gen-round.off { opacity: 0.5; }
.gen-round-name { flex: 1 1 auto; font-weight: 700; font-size: var(--fs-note); }
.gen-round input[type="number"] { width: 58px; text-align: center; font-weight: 800; }

/* ========================================================= picture reveals
   Four ways for round 2 to give itself away. The zoom is the original and
   still the default; these are the same job done differently so ten questions
   are not ten of the same effect.

   All four run on the same curve in screen.js — that is a scoring decision,
   not a styling one. See REVEAL_MODES in src/quizzes.js. */

/* ---- pixelate: drawn tiny into a canvas and blown up by the browser, which
   is the one way to do this that needs no library and no ctx.filter. */
.pix-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* Least-known last, so a browser that understands `pixelated` uses it and one
     that does not still falls back to something sharp. Order matters here: get
     it the wrong way round and the projector smooths the blocks into mush,
     which is the whole effect gone. */
  image-rendering: -webkit-optimize-contrast;
  image-rendering: crisp-edges;
  image-rendering: pixelated;
}
/* The real picture waits underneath and takes over at the very end, so what
   the room finally sees is the photograph rather than a good impression. */
.reveal-pixelate .zoom-img { visibility: hidden; }
.reveal-pixelate.pix-done .zoom-img { visibility: visible; }
.reveal-pixelate.pix-done .pix-canvas { display: none; }

/* ---- blur: one CSS filter, supported everywhere including the old iOS that
   ctx.filter is not. */
.reveal-blur .zoom-img { will-change: filter, transform; }

/* ---- tiles: a blind of panels that come away a few at a time. */
.tile-grid {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: repeat(var(--cols, 6), 1fr);
  grid-template-rows: repeat(var(--rows, 4), 1fr);
  gap: 3px;
}
.tile-grid i {
  /* Opaque, not the usual translucent panel fill — a tile that lets the
     picture through is a tile that is not covering anything. */
  background: #14131f;
  border: 1px solid var(--panel-line);
  border-radius: var(--r-field);
  transition: opacity 420ms ease, transform 420ms ease;
}
.tile-grid i.gone { opacity: 0; transform: scale(0.82); }

/* A picture that did not load says so, whichever effect it was going to use. */
.zoom-frame.no-image .pix-canvas,
.zoom-frame.no-image .tile-grid { display: none; }

/* The primary action on a section heading. `.go` was only ever styled inside a
   pack card and the editor, so a button using it anywhere else came out looking
   like the secondary ones next to it — which on this panel is the wrong way
   round: New invoice is the thing you came here to press. */
/* A HEADING'S BUTTONS ARE A ROW, and this one was never told to be.
   `.host .row` and `.panel.pics .row` are both `display: flex` with a gap and
   wrapping; this one had `margin-left: auto` and nothing else — so its
   children were loose inline-blocks that broke onto a second line whenever
   they did not fit, at whatever width each happened to be. That is the
   Adverts tab's "Bring in a picture" with "New set" hanging under its
   right-hand end, and the Invoices tab's three buttons at three heights.
   `stretch` rather than `center`, because two of these are a <label> and a
   <button> with different padding — centred they line up on the middle and
   still read as two different sizes. */
.game-head .row {
  margin-left: auto;
  display: flex; gap: 8px; align-items: stretch; flex-wrap: wrap;
  justify-content: flex-end;
}
/* A <label> acting as a button has no button box, so it needs telling how to
   sit inside a stretched row. */
.game-head .row > label { display: inline-flex; align-items: center; }
/* THE SAME HOLE `.role-make` ALREADY HAD, in a rule nobody thought to check.
   This set a border, a background, a colour and a weight and NOTHING ELSE —
   so "Mark paid" on an invoice row inherited the browser's own button box:
   13.33px in a system face (off the ten-step ladder), `1px 6px` of padding,
   and SQUARE CORNERS. Beside "Open" and "Send" at 36px tall with 10px
   corners, in the same row. Found by measuring rather than by looking, which
   is the only way this kind of fault is ever found — it is invisible in a
   diff and obvious once you know, exactly as the note on `.role-make` says.
   Sized to match `.minor`, because these buttons sit in rows with them. */
.game-head .go, .sheet-actions .go, .inv-actions .go {
  border: none;
  border-radius: var(--r-field);
  padding: 9px 13px;
  font-family: inherit;
  font-size: var(--fs-note);
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  color: #12000c;
  font-weight: 900;
  cursor: pointer;
}
.game-head .go:hover, .sheet-actions .go:hover, .inv-actions .go:hover { filter: brightness(1.08); }
.game-head .go:disabled, .sheet-actions .go:disabled { opacity: 0.5; cursor: default; filter: none; }

/* ============================================================== invoicing
   Read at half eleven on a phone, in a car park, by somebody who wants to go
   home. Everything is one column and the amount is the biggest thing on it. */

.inv-row {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 12px 14px;
  border-radius: var(--r-card);
  border: 1px solid var(--panel-line);
  /* The one card class the surface sweep missed — it wrote its own grey out
     by hand instead of taking `--panel`, so an invoice row read visibly
     flatter than the summary panel directly above it. */
  background: var(--panel);
  margin-bottom: 8px;
}
.inv-main { flex: 1 1 220px; min-width: 0; }
.inv-top { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.inv-who { font-weight: 700; }
/*
 * WHICH RUNG SOMEBODY PRESSED ON THE WAY IN, on the owner's subscriber row.
 * `--hot` rather than `--gold`: gold is the trophy colour and means winning, and
 * a note about a tier somebody wanted is neither a prize nor a warning.
 */
.inv-wanted { color: var(--hot); font-weight: 700; }
.inv-status {
  font-size: var(--fs-tag);
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.09);
  color: var(--ink-dim);
}
.inv-row.status-paid .inv-status { background: rgba(47, 224, 122, 0.2); color: var(--good); }
.inv-row.status-cancelled { opacity: 0.55; }
.inv-row.status-cancelled .inv-amount { text-decoration: line-through; }
.inv-amount { font-weight: 900; font-size: var(--fs-head); margin-left: auto; }
.inv-actions { display: flex; gap: 6px; align-items: center; }
.inv-actions a.minor { text-decoration: none; display: inline-flex; align-items: center; }

/* The forms. A two-column grid that collapses on a phone, because these get
   filled in on a laptop and read on a phone. */
.inv-form { display: grid; gap: 16px; }
.inv-group {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 10px 14px;
  padding: 14px;
  border-radius: var(--r-card);
  border: 1px solid var(--panel-line);
  background: rgba(255, 255, 255, 0.03);
}
.inv-group h4 {
  grid-column: 1 / -1;
  margin: 0;
  font-size: var(--fs-mini);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.inv-field { display: grid; gap: 4px; font-size: var(--fs-note); font-weight: 700; }
.inv-field.wide, .inv-group .wide, .inv-group.wide { grid-column: 1 / -1; }
.inv-field span { color: var(--ink-dim); font-size: var(--fs-mini); }
.inv-field input[type="checkbox"] { justify-self: start; width: 20px; height: 20px; }

.inv-lines { display: grid; gap: 6px; grid-column: 1 / -1; }
.inv-line { display: flex; gap: 8px; }
.inv-line [data-desc] { flex: 1 1 auto; min-width: 0; }
/* Wide enough for "1,250.50" and no wider — it is a number, not a sentence. */
.inv-line [data-amount] { flex: 0 0 110px; text-align: right; font-weight: 800; }
.inv-total { grid-column: 1 / -1; font-size: var(--fs-ctl); text-align: right; }

.inv-customers { display: grid; gap: 6px; }
.inv-cust {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border-radius: var(--r-card);
  border: 1px solid var(--panel-line);
}
.inv-cust > div { flex: 1 1 auto; min-width: 0; }

/* ==================================================================== looks
   Dressing a night up: a palette and a handful of drawn shapes. The quiz
   underneath is identical — nothing about how a round plays changes.

   The palettes live here rather than in looks.js so there is one place to
   change a colour, and there is a test that every look in that file has a
   block here. A look with no palette would silently be the ordinary one
   wearing skulls.

   THE OPTION COLOURS (--a to --f) MUST CHANGE ON BOTH SCREENS OR NEITHER.
   A player looks up, decides "the pink one, bottom left", and looks back down
   — so if the projector goes orange and the phone stays pink, that mapping is
   broken and the theme has cost somebody points. Both pages carry data-look
   for exactly this reason. Keep the six well spaced round the wheel: they have
   to be told apart from the back of a room, which matters more than the
   palette being tasteful. */

/* ---- My account.

   Everything about the person rather than about a pack or a night. Laid out as
   plain rows of label-over-value, because most of it is a STATEMENT — what you
   are on, what the room sees — and only two things on the page are controls.
   Making the statements look like inputs is how somebody comes away thinking
   they turned invoicing off when they cancelled a subscription. */
.acct-grid {
  display: flex; flex-wrap: wrap; gap: 22px 40px;
  margin-bottom: 12px;
}
.acct-val { font-size: var(--fs-lead); font-weight: 800; margin-top: 3px; }
.acct-val.good { color: var(--good); }
.acct-val.bad { color: var(--bad); }
.acct-val.brand-preview {
  background: linear-gradient(120deg, var(--hot), var(--hot-2) 70%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.acct-note { margin-top: 4px; }
.acct-note.bad { color: var(--bad); }
.acct-actions { margin-top: 14px; }
.acct-missing { margin: 6px 0 0; padding-left: 20px; font-size: var(--fs-note); color: var(--ink-dim); }
.acct-missing li { margin-bottom: 4px; }

/* The only real switches on the page, so they are the only things that look
   like switches. A whole row is the target — a 14px tickbox is not something
   to aim at on a phone in a car park. */
.acct-toggles { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.acct-toggle {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 11px 14px;
  border-radius: var(--r-card);
  border: 1px solid var(--panel-line);
  background: rgba(255, 255, 255, 0.04);
  cursor: pointer;
}
.acct-toggle:hover { background: rgba(255, 255, 255, 0.07); }
.acct-toggle span { min-width: 0; }
/* The words take the room, the switch sits on the right at a fixed size. It
   was a tick box, which reads as a form you fill in; this is the SAME control
   as the hat switch in the top right, because one shape for "is this on"
   across the app is recognised rather than read. */
.acct-toggle { align-items: center; justify-content: space-between; cursor: default; }
.acct-toggle-what { flex: 1 1 auto; }
.feat-switch { flex: 0 0 auto; }
.feat-switch .hat-half { padding: 6px 13px; font-size: var(--fs-tag); }
/* A tier above yours gets the tab bar's "+" rather than a switch reading Off.
   You did not turn it off — you do not have it, and telling those two apart is
   the whole point of the three states. */
.feat-plus {
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 30px; height: 30px;
  border-radius: var(--r-pill);
  border: 1px dashed var(--panel-line);
  color: var(--ink-dim);
  font-weight: 900; font-size: var(--fs-ctl); line-height: 1;
}

.acct-links { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 4px; }

/* ---- the tier ladder.

   Bronze, Silver, Gold, and they stack. Each section is edged in its own metal
   so the three are told apart at a glance rather than by reading three
   headings — and a tier ABOVE yours is dimmed as a whole rather than hidden,
   because something you can see and cannot use is a thing you might buy.

   The metals are written out rather than taken from --hot: they are the same
   three on everybody's account, whatever colour scheme is on, because a tier is
   a fact about what you pay and not part of anybody's branding. */
.tier-panel { border-left: 3px solid var(--tier); }
.tier-panel.tier-bronze { --tier: var(--metal-bronze); }
.tier-panel.tier-silver { --tier: var(--metal-silver); }
.tier-panel.tier-gold   { --tier: var(--metal-gold); }
.tier-panel h3 { display: flex; align-items: center; gap: 9px; color: var(--ink); }
.tier-dot {
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--tier);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--tier) 22%, transparent);
  flex: 0 0 auto;
}
.tier-yours, .tier-price {
  margin-left: auto;
  font-size: var(--fs-tag); letter-spacing: 0.12em; text-transform: uppercase; font-weight: 800;
  padding: 3px 9px; border-radius: var(--r-pill);
}
.tier-yours { background: rgba(47, 224, 122, 0.18); color: var(--good); }
.tier-price { background: rgba(255, 255, 255, 0.08); color: var(--ink-dim); letter-spacing: 0.04em; text-transform: none; }

/* A rung you have not reached. Legible, obviously not yours, nothing to press. */
.tier-panel.locked { opacity: 0.72; }
.tier-panel.locked .acct-toggle { cursor: default; background: none; border-style: dashed; }
.tier-panel.locked .acct-toggle:hover { background: none; }
.tier-panel.locked .feat-plus { opacity: 0.75; }

/* The owner's tier picker on the subscriber list. */
.tier-pick { display: inline-flex; gap: 6px; }
.tier-pick .tierbtn.on { border-color: var(--hot); color: var(--ink); background: rgba(255, 255, 255, 0.12); }

/* ---- the colour picker on the console.

   The swatches are the colours themselves, not their names: "Orchid" means
   nothing until you have seen it, and the point is choosing one you like the
   look of. Same reasoning as the photo filters being shown rather than listed.

   Each blob is written out here rather than taking var(--hot), because they all
   have to be on screen AT ONCE and a variable would make every one of them the
   colour you already have. Keep these in step with the [data-scheme] blocks
   below — there is a test that the ids match, though not the hexes. */
/* Breathing room under the explanation. The swatches sat straight on top of
   it, so the words read as a label for the first swatch rather than for the
   row. */
.scheme-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
.scheme-swatch {
  display: flex; align-items: center; gap: 9px;
  padding: 8px 14px 8px 9px;
  border-radius: var(--r-pill);
  border: 1px solid var(--panel-line);
  background: rgba(255, 255, 255, 0.05);
  color: var(--ink-dim);
  font: inherit; font-size: var(--fs-note); font-weight: 800;
  cursor: pointer;
}
.scheme-swatch:hover:not(:disabled) { background: rgba(255, 255, 255, 0.09); color: var(--ink); }
.scheme-swatch.live { border-color: var(--hot); color: var(--ink); background: rgba(255, 255, 255, 0.1); }
.scheme-swatch:disabled { opacity: 0.45; cursor: default; }
.scheme-blob {
  width: 26px; height: 26px; border-radius: 50%;
  flex: 0 0 auto;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
}
.scheme-blob[data-swatch="sunset"] { background: linear-gradient(120deg, #ff2e88, #ff8a3d); }
.scheme-blob[data-swatch="orchid"] { background: linear-gradient(120deg, #ff4fa3, #a855f7); }
.scheme-blob[data-swatch="lagoon"] { background: linear-gradient(120deg, #2ad3c5, #3d8bff); }
.scheme-blob[data-swatch="ember"]  { background: linear-gradient(120deg, #ff3d3d, #ffa62b); }
.scheme-blob[data-swatch="citrus"] { background: linear-gradient(120deg, #9ae62b, #ffd23f); }
.scheme-blob[data-swatch="ultra"]  { background: linear-gradient(120deg, #8b5cf6, #ff2ed6); }
.scheme-blob[data-swatch="cobalt"]  { background: linear-gradient(120deg, #546dff, #22d3ee); }
.scheme-blob[data-swatch="meadow"]  { background: linear-gradient(120deg, #22c55e, #14b8a6); }
.scheme-blob[data-swatch="blush"]   { background: linear-gradient(120deg, #fb7185, #fb923c); }
.scheme-blob[data-swatch="wine"]    { background: linear-gradient(120deg, #e04444, #a25fdb); }
.scheme-blob[data-swatch="solstice"] { background: linear-gradient(120deg, #f5b642, #ca5b2e); }
.scheme-blob[data-swatch="frost"]   { background: linear-gradient(120deg, #94a3b8, #38bdf8); }

/* ============================================================== SCHEMES
   A quizmaster's own two colours — see public/assets/schemes.js.

   A SCHEME IS A BRAND; A LOOK IS A NIGHT. A scheme sets --hot and --hot-2 and
   the washes behind everything, and deliberately does NOT touch --a to --f.
   Those are the option colours, they are how a player matches "the pink one,
   bottom left" between the projector and their phone, and they belong to the
   looks below and to nothing else.

   THESE BLOCKS MUST STAY ABOVE THE LOOKS. Both are a single attribute selector,
   so they have the same specificity and source order is what decides. A themed
   night has to beat somebody's brand colours — Halloween is orange and black on
   everybody's account. There is a test that this file keeps them in this order.

   "sunset" is the original and needs no block: it is what :root already says,
   so an account with no scheme set, and every page before the scheme arrives,
   looks exactly as it always did. */

[data-scheme="orchid"] {
  --mark-3: #ffc2f0;
  --hot: #ff4fa3;
  --hot-2: #a855f7;
  --glow-1: #2d1245;
  --glow-2: #3b0f3a;
  --drift-1: rgba(255, 79, 163, 0.16);
  --drift-2: rgba(168, 85, 247, 0.15);
}

[data-scheme="lagoon"] {
  --mark-3: #8df2e4;
  --hot: #2ad3c5;
  --hot-2: #3d8bff;
  --glow-1: #0b2f45;
  --glow-2: #10324a;
  --drift-1: rgba(42, 211, 197, 0.16);
  --drift-2: rgba(61, 139, 255, 0.15);
}

[data-scheme="ember"] {
  --mark-3: #ffe08a;
  --hot: #ff3d3d;
  --hot-2: #ffa62b;
  --glow-1: #3a1108;
  --glow-2: #40210a;
  --drift-1: rgba(255, 61, 61, 0.16);
  --drift-2: rgba(255, 166, 43, 0.15);
}

[data-scheme="citrus"] {
  --mark-3: #fff6a8;
  --hot: #9ae62b;
  --hot-2: #ffd23f;
  --glow-1: #17330d;
  --glow-2: #3a3208;
  --drift-1: rgba(154, 230, 43, 0.15);
  --drift-2: rgba(255, 210, 63, 0.14);
}

[data-scheme="ultra"] {
  --mark-3: #ff9df0;
  --hot: #8b5cf6;
  --hot-2: #ff2ed6;
  --glow-1: #1f1150;
  --glow-2: #3d0b47;
  --drift-1: rgba(139, 92, 246, 0.17);
  --drift-2: rgba(255, 46, 214, 0.15);
}

[data-scheme="cobalt"] {
  --mark-3: #b8e8ff;
  --hot: #546dff;
  --hot-2: #22d3ee;
  --glow-1: #0d1640;
  --glow-2: #0a2f3a;
  --drift-1: rgba(84, 109, 255, 0.16);
  --drift-2: rgba(34, 211, 238, 0.15);
}

[data-scheme="meadow"] {
  --mark-3: #baf7dc;
  --hot: #22c55e;
  --hot-2: #14b8a6;
  --glow-1: #0d2a1a;
  --glow-2: #0a2b28;
  --drift-1: rgba(34, 197, 94, 0.16);
  --drift-2: rgba(20, 184, 166, 0.15);
}

[data-scheme="blush"] {
  --mark-3: #ffd9b8;
  --hot: #fb7185;
  --hot-2: #fb923c;
  --glow-1: #3a1420;
  --glow-2: #3a1f0d;
  --drift-1: rgba(251, 113, 133, 0.16);
  --drift-2: rgba(251, 146, 60, 0.15);
}

[data-scheme="wine"] {
  --mark-3: #f0c2ff;
  --hot: #e04444;
  --hot-2: #a25fdb;
  --glow-1: #3a0f14;
  --glow-2: #2a1040;
  --drift-1: rgba(224, 68, 68, 0.16);
  --drift-2: rgba(162, 95, 219, 0.15);
}

[data-scheme="solstice"] {
  --mark-3: #ffe9b8;
  --hot: #f5b642;
  --hot-2: #ca5b2e;
  --glow-1: #3a2a0d;
  --glow-2: #3a1f0f;
  --drift-1: rgba(245, 182, 66, 0.15);
  --drift-2: rgba(202, 91, 46, 0.15);
}

[data-scheme="frost"] {
  --mark-3: #d6ecff;
  --hot: #94a3b8;
  --hot-2: #38bdf8;
  --glow-1: #1a2230;
  --glow-2: #0d2740;
  --drift-1: rgba(148, 163, 184, 0.15);
  --drift-2: rgba(56, 189, 248, 0.15);
}

/* New Year — gold on midnight, the one look that is about a CLOCK rather than
   a season. Its range starts on the 27th because Christmas now ends on Boxing
   Day; two looks claiming the same evening would make the picker's answer
   depend on list order. */
[data-look="newyear"] {
  --glow-1: #0d1a3a;
  --glow-2: #2e2410;
  --drift-1: rgba(255, 209, 102, 0.20);
  --drift-2: rgba(120, 170, 255, 0.16);
  --panel: #121a2e;
  --hot: #ffd166;
  --hot-2: #ffa63a;
  --cool: #78aaff;
  --gold: #ffe08a;
  --bg: #060a18;
  --bg-2: #101a33;
  --a: #ffd166;   /* champagne */
  --b: #78aaff;   /* midnight */
  --c: #ff6f91;   /* rocket */
  --d: #6fe3c4;   /* sparkler */
  --e: #c9a0ff;   /* confetti */
  --f: #ff9f45;   /* ember */
}

/* Easter — the only PASTEL look, which is the point: it is the one time of
   year a pub quiz is in the afternoon with families in. Kept dark behind, like
   every other look, because the projector still has to read from the back. */
[data-look="easter"] {
  --glow-1: #1b2e1a;
  --glow-2: #2e2a12;
  --drift-1: rgba(167, 226, 148, 0.20);
  --drift-2: rgba(255, 214, 235, 0.18);
  --panel: #16211a;
  --hot: #ffb3d1;
  --hot-2: #ffd98a;
  --cool: #a7e294;
  --gold: #ffe9a3;
  --bg: #070f0a;
  --bg-2: #101c14;
  --a: #ffb3d1;   /* blossom */
  --b: #a7e294;   /* spring */
  --c: #9ad4ff;   /* duck egg */
  --d: #ffe07a;   /* daffodil */
  --e: #d3b0ff;   /* crocus */
  --f: #ff9a6c;   /* carrot */
}

/* St Patrick's — green and gold, and the most PUB of the looks. */
[data-look="stpatricks"] {
  --glow-1: #0b2a16;
  --glow-2: #2c2708;
  --drift-1: rgba(74, 214, 122, 0.20);
  --drift-2: rgba(255, 209, 102, 0.16);
  --panel: #10261a;
  --hot: #4ad67a;
  --hot-2: #ffd166;
  --cool: #7ae0ff;
  --gold: #ffdf7a;
  --bg: #04120a;
  --bg-2: #0c2114;
  --a: #4ad67a;   /* shamrock */
  --b: #ffd166;   /* gold */
  --c: #ff8fa3;   /* rosy */
  --d: #7ae0ff;   /* sky */
  --e: #c8a0ff;   /* heather */
  --f: #ff9f45;   /* stout head */
}

/* Bonfire Night — the darkest of them, because that is the whole look: a black
   November sky with things going off in it. */
[data-look="bonfire"] {
  --glow-1: #24102e;
  --glow-2: #3a1608;
  --drift-1: rgba(255, 138, 61, 0.20);
  --drift-2: rgba(120, 170, 255, 0.14);
  --panel: #171224;
  --hot: #ff8a3d;
  --hot-2: #ffd166;
  --cool: #78aaff;
  --gold: #ffcf5c;
  --bg: #05060f;
  --bg-2: #120e1e;
  --a: #ff8a3d;   /* ember */
  --b: #78aaff;   /* smoke-blue */
  --c: #6fe36f;   /* green rocket */
  --d: #ffd166;   /* sparkler */
  --e: #ff5470;   /* red burst */
  --f: #c9a0ff;   /* violet burst */
}

/* Eurovision — the loudest palette in the app, and rightly so: it is one night
   a year and the whole point is that it looks like a light rig. */
[data-look="eurovision"] {
  --glow-1: #2b0f3d;
  --glow-2: #0d2a4a;
  --drift-1: rgba(255, 90, 200, 0.20);
  --drift-2: rgba(90, 200, 255, 0.18);
  --panel: #1a1030;
  --hot: #ff5ac8;
  --hot-2: #ffc84a;
  --cool: #5ac8ff;
  --gold: #ffd76b;
  --bg: #0a0518;
  --bg-2: #180c2e;
  --a: #ff5ac8;   /* stage pink */
  --b: #5ac8ff;   /* stage blue */
  --c: #6fe36f;   /* laser green */
  --d: #ffc84a;   /* spotlight */
  --e: #c07bff;   /* violet wash */
  --f: #ff7a5a;   /* warm gel */
}

/* International — a tournament night. Deliberately NEUTRAL rather than any
   country's colours: it goes up for a World Cup, a Euros and an Olympics, and
   a palette that looked like one nation's flag would be wrong at two of them.
   The flags come from the props, where the room chooses. */
[data-look="international"] {
  --glow-1: #0e2438;
  --glow-2: #16321c;
  --drift-1: rgba(90, 170, 255, 0.18);
  --drift-2: rgba(110, 227, 140, 0.16);
  --panel: #121e2a;
  --hot: #4aa8ff;
  --hot-2: #6fe38c;
  --cool: #9ad4ff;
  --gold: #ffd76b;
  --bg: #060d16;
  --bg-2: #0f1c28;
  --a: #4aa8ff;   /* pitch blue */
  --b: #6fe38c;   /* grass */
  --c: #ff6f91;   /* rosette */
  --d: #ffd76b;   /* medal */
  --e: #c9a0ff;   /* floodlight */
  --f: #ff9f45;   /* terrace orange */
}

[data-look="halloween"] {
  --glow-1: #2a1038;
  --glow-2: #3d1a05;
  --drift-1: rgba(255, 117, 24, 0.18);
  --drift-2: rgba(157, 123, 255, 0.16);
  /* Opaque, unlike the usual translucent panel, so the shapes drifting behind
     the screen do not show through the answers. A ghost half-visible through
     option C is a muddier option C, and legibility beats atmosphere. */
  --panel: #1b1226;
  --hot: #ff7518;
  --hot-2: #ffb03a;
  --cool: #9d7bff;
  --gold: #ffc93c;
  --bg: #0b0611;
  --bg-2: #1a0d22;
  --a: #ff8a3d;   /* pumpkin */
  --b: #9d7bff;   /* witch purple */
  --c: #6fe36f;   /* slime */
  --d: #ffd23f;   /* candle */
  --e: #ff5470;   /* blood */
  --f: #63d6ff;   /* ghost */
}

[data-look="valentines"] {
  --glow-1: #3d0d29;
  --glow-2: #2a0616;
  --drift-1: rgba(255, 45, 111, 0.2);
  --drift-2: rgba(255, 158, 205, 0.14);
  --panel: #26101c;
  --hot: #ff2d6f;
  --hot-2: #ff86b0;
  --cool: #ff9ecd;
  --gold: #ffd6e4;
  --bg: #12040c;
  --bg-2: #26081a;
  --a: #ff6b9d;
  --b: #ffd23f;
  --c: #b48cff;
  --d: #ff9d5c;
  --e: #63d6ff;
  --f: #7cf76b;
}

[data-look="christmas"] {
  --glow-1: #0d2e1c;
  --glow-2: #35060f;
  --drift-1: rgba(232, 18, 60, 0.16);
  --drift-2: rgba(92, 225, 160, 0.16);
  --panel: #0f1f18;
  --hot: #e8123c;
  --hot-2: #ff5f5f;
  --cool: #5ce1a0;
  --gold: #ffd23f;
  --bg: #04100c;
  --bg-2: #0a2018;
  --a: #ff4d5e;   /* holly berry */
  --b: #ffd23f;   /* tinsel */
  --c: #5ce1a0;   /* tree */
  --d: #9fd8ff;   /* frost */
  --e: #ff9d5c;
  --f: #d9b3ff;
}

[data-look="summer"] {
  --glow-1: #0d3350;
  --glow-2: #4a2a05;
  --drift-1: rgba(255, 140, 26, 0.18);
  --drift-2: rgba(23, 195, 247, 0.16);
  --panel: #0d2130;
  --hot: #ff8c1a;
  --hot-2: #ffd23f;
  --cool: #17c3f7;
  --gold: #ffe066;
  --bg: #04121c;
  --bg-2: #0a2436;
  --a: #ffd23f;   /* sun */
  --b: #17c3f7;   /* sea */
  --c: #ff6f61;   /* coral */
  --d: #7cf76b;   /* palm */
  --e: #ff9ecd;
  --f: #b48cff;
}

/* ---- the shapes themselves
   Down the two side margins and nowhere near the middle. A question with a
   clock running wants the whole centre of the projector, and a ghost drifting
   over an option is the one way a decoration could actually lose somebody
   points. */
/* Starts below the topbar. The brand and the countdown live up there on a
   transparent strip, and a pumpkin behind the logo reads as a fault rather
   than as decoration. */
.look-layer {
  position: absolute;
  inset: 64px 0 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 0;
}
.motif {
  position: absolute;
  display: block;
  color: var(--hot-2);
  /* Faint enough to be atmosphere rather than something to look at, but not so
     faint that a projector in a bright function room shows nothing at all. */
  opacity: 0.22;
  transform: scale(var(--scale, 1));
  animation: motif-drift var(--drift, 22s) ease-in-out infinite alternate;
  animation-delay: var(--delay, 0s);
}
.motif.right { transform: scale(var(--scale, 1)) scaleX(-1); }
.motif:nth-child(3n) { color: var(--cool); }
.motif:nth-child(3n + 2) { color: var(--hot); }

@keyframes motif-drift {
  from { transform: translateY(-14px) rotate(-7deg) scale(var(--scale, 1)); }
  to   { transform: translateY(14px) rotate(7deg) scale(var(--scale, 1)); }
}
/* Mirrored ones need their own frames, or the scaleX is thrown away mid-drift. */
.motif.right { animation-name: motif-drift-right; }
@keyframes motif-drift-right {
  from { transform: translateY(-14px) rotate(7deg) scale(var(--scale, 1)) scaleX(-1); }
  to   { transform: translateY(14px) rotate(-7deg) scale(var(--scale, 1)) scaleX(-1); }
}

/* Everything the room reads sits above them, whatever else changes. */
.stage > .card, .stage > .topbar { position: relative; z-index: 1; }

/* And the big text separates itself from whatever drifts behind it. The panels
   are opaque so an answer is safe, but the question and the countdown sit on
   bare background — a snowflake behind the word "UK" is legible in a dark room
   and less so in a function room with the lights half up. One shadow costs
   nothing and takes the question out of the argument entirely. */
[data-look]:not([data-look="default"]) .q-prompt,
[data-look]:not([data-look="default"]) .timer-num,
[data-look]:not([data-look="default"]) .brand-name,
[data-look]:not([data-look="default"]) .round-intro h1,
[data-look]:not([data-look="default"]) .answer-said {
  text-shadow: 0 2px 12px var(--bg), 0 0 3px var(--bg);
}

/* Somebody who has asked their device to stop moving things gets a still
   picture rather than no picture — the look is the point, the drifting is not. */
@media (prefers-reduced-motion: reduce) {
  .motif { animation: none; }
}

/* On a phone they go behind everything at a lower strength: the screen is
   small, the buttons matter, and there is no margin to speak of.
   `.wrap` has to be lifted or the shapes float over the answer buttons — the
   projector gets this from `.stage > .card` and the phone has no equivalent. */
.phone .look-layer { position: fixed; z-index: 0; }
.phone .wrap { position: relative; z-index: 1; }
.phone .motif { opacity: 0.1; }

/* A tab for something not on this plan. Shown rather than hidden: a thing you
   can see and cannot use is a thing you might buy, and a thing that is not
   there is a thing nobody knew existed. */
.tab.locked { opacity: 0.55; }
.tab.locked:hover { opacity: 0.8; }
.tabcount.lock { background: rgba(255, 210, 63, 0.22); color: var(--gold); }

/* Signing in. One form, in the middle, on a page that does nothing else. */
.sign-in { max-width: 380px; margin: 12vh auto; display: grid; gap: 16px; }
.sign-in-brand { display: flex; align-items: center; gap: 10px; justify-content: center; }
.sign-in h1 { margin: 0; text-align: center; font-size: 30px; }
.sign-in form { display: grid; gap: 12px; }
.sign-in label { display: grid; gap: 5px; font-size: var(--fs-note); font-weight: 700; color: var(--ink-dim); }
.sign-in input {
  padding: 13px;
  border-radius: var(--r-card);
  border: 1px solid var(--panel-line);
  background: rgba(255, 255, 255, 0.06);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-field); /* iOS zooms the page in on anything smaller */
}
.sign-in input:focus { outline: none; border-color: var(--hot); }
.sign-in button.go {
  padding: 13px;
  border: none;
  border-radius: var(--r-card);
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  color: #12000c;
  font: inherit;
  font-weight: 900;
  font-size: var(--fs-field);
  cursor: pointer;
}
.sign-in button.go:disabled { opacity: 0.6; cursor: default; }
.sign-in-problem:not(:empty) {
  padding: 10px 12px;
  border-radius: var(--r-field);
  background: rgba(255, 77, 94, 0.14);
  border: 1px solid rgba(255, 77, 94, 0.4);
  color: var(--ink);
  font-size: var(--fs-body);
}
/* THE SAME BOX ALSO CARRIES GOOD NEWS, and it must not be wearing red when it
   does: "a link is on its way" inside a red-bordered slab reads as a failure,
   and somebody who has just been locked out will believe it. The colour is
   set by the STATE rather than by which box it is. */
.sign-in-problem.said:not(:empty) {
  background: rgba(47, 224, 122, 0.12);
  border-color: rgba(47, 224, 122, 0.4);
}
.sign-in .tiny { text-align: center; }
.subs { display: grid; gap: 8px; }
.inv-actions .addon.on { border-color: var(--good); color: var(--good); }

/* A photo somebody has just sent, big, in the middle, for a few seconds.
   The strip along the bottom is the record of the night; this is the moment.
   At 13vh in the strip a face is the size of a stamp on a projector, which is
   the opposite of the point of letting the room send them at all. */
.photo-big {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
  z-index: 3;
  background: radial-gradient(60% 60% at 50% 50%, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0.35));
  animation: photo-big-fade 420ms ease;
}
/* WHEN THE JOIN CODE IS UP, THE PHOTO CENTRES BESIDE IT RATHER THAN UNDER IT.
   The two are on screen together for the whole of the lobby and every round
   board — which is exactly where photos are allowed and exactly where a
   latecomer is trying to scan — so this is the normal case, not an edge one.

   Padding on the grid rather than a shift on the figure, so the photo is
   properly centred in what is left instead of being nudged off the middle of
   the screen. The class is set in JS rather than with `:has()`, which is one
   more thing a projector's browser has to be new enough for, on the screen
   where being conservative is worth most.

   **`--photo-clear`/`--photo-max-w` ARE MEASURED, NOT GUESSED** — see
   `photoClearance()` in `screen.js`. The round board's own join corner is a
   small box; the lobby's is `.qr-panel`, most of a whole grid column — a
   single fixed number could only ever be tuned against one of them, and a
   photo on the LOBBY quietly covered the only code a latecomer could scan.
   Both properties fall back to the old tuned numbers if JS could not measure
   anything, which keeps a projector that skipped the measurement (an old
   browser, a `.stage` not yet in the DOM) at the previous, still-safe
   behaviour rather than an unconstrained one. */
.photo-big.beside-join { padding-right: var(--photo-clear, 17vw); }
.photo-big.beside-join img { max-height: 56vh; max-width: var(--photo-max-w, 74vw); }
/* A POLAROID: white all round, deep at the bottom, and tilted a few degrees.
   The frame is what makes a phone snap read as a keepsake from the night
   rather than as a picture the projector happened to put up — and the tilt
   makes a run of them look like prints dropped on a table.

   The angle comes from JS as `--tilt`, and every transform here has to carry
   it: leave it out of a keyframe and the photo snaps square halfway through. */
.photo-big figure {
  margin: 0;
  padding: 1.6vh 1.6vh 0;
  background: #f7f5ef;
  border-radius: var(--r-field);
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.75), 0 2px 0 rgba(255, 255, 255, 0.6) inset;
  transform: rotate(var(--tilt, 0deg));
  animation: photo-big-in 520ms cubic-bezier(0.2, 1.1, 0.4, 1);
}
/* Tall rather than wide: a phone photo is portrait, and filling the height is
   what makes it read from the back of a room. A touch shorter than it was, to
   pay for the frame without the whole thing growing. */
.photo-big img {
  display: block;
  max-height: 60vh;
  max-width: 74vw;
  object-fit: contain;
  background: #241f1a;
}
/* The wide bottom lip is the half that says "polaroid". The name sits in it,
   dark on white, because a caption in the app's own light-on-dark would read
   as a label stuck over the print rather than written on it. */
.photo-big figcaption {
  padding: 1.1vh 0 1.9vh;
  font-family: 'Segoe Print', 'Bradley Hand', 'Comic Sans MS', cursive;
  font-size: 3.2vh;
  font-weight: 700;
  text-align: center;
  color: #241f1a;
}
.photo-big.going { animation: photo-big-fade 900ms ease reverse forwards; }
.photo-big.going figure { animation: photo-big-out 900ms ease forwards; }

@keyframes photo-big-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes photo-big-in {
  from { opacity: 0; transform: rotate(var(--tilt, 0deg)) scale(0.86); }
  to   { opacity: 1; transform: rotate(var(--tilt, 0deg)) scale(1); }
}
@keyframes photo-big-out {
  from { opacity: 1; transform: rotate(var(--tilt, 0deg)) scale(1); }
  to   { opacity: 0; transform: rotate(var(--tilt, 0deg)) scale(1.05); }
}

/* The strip underneath, a size up. It was small enough that a face in it was
   unidentifiable, which made it decoration rather than a record. */
.photo-strip .photo { width: 18vh; }
.photo-strip .photo img { height: 18vh; }
.photo-strip .photo figcaption { font-size: 1.6vh; }

/* Props to stick on a photo. The row people actually wanted — the colour looks
   are still there, folded away underneath. */
/* As many across as fit, rather than a fixed count — there are two dozen props
   now and a hard number is wrong on either a 320px phone or a wide one. All of
   them stay on screen and the panel scrolls: a row that scrolls SIDEWAYS is how
   the whole feature went unnoticed the first time. */
.cam-props {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(66px, 1fr));
  gap: 6px;
  margin-bottom: 6px;
}
/* A mid-grey tile, not the page's own dark panel.
   Half these props are nearly black — the shades, the moustache, the disguise —
   and on a dark tile they read as an empty square you would never tap. A pale
   slab each is three dozen white blocks shouting on a dark phone, so it is a
   mid tone that both the black shapes and the yellow crown stand out against.

   A GRADIENT rather than one flat fill, and that is not decoration. The props
   are flat colour, so a tile of a single tone can be matched exactly by one of
   them and that prop disappears — the viking helmet was drawn in the tile's
   own grey and read as two horns floating in nothing. Against a tone that
   changes down the tile, no flat colour can hide across the whole of it. */
.cam-prop {
  padding: 6px 3px 4px;
  border-radius: var(--r-card);
  border: 1px solid var(--panel-line);
  background: linear-gradient(160deg, #b6bccb 0%, #8b91a6 55%, #6f7689 100%);
  cursor: pointer;
  display: grid;
  gap: 2px;
  justify-items: center;
  -webkit-tap-highlight-color: transparent;
}
/* NAMED as well as drawn, unlike the colour looks — those were replaced by a
   thumbnail of your own photo because a name told you nothing. A prop is the
   other way round: at this size a monobrow and a moustache are the same dark
   smear, and a beard reads as a shield. */
.cam-prop-art { display: block; width: 100%; aspect-ratio: 1; }
.cam-prop-name {
  font-size: 9px;
  font-weight: 800;
  line-height: 1.1;
  color: #1b1e28;
  text-align: center;
}
/* `pan-y`, NOT `none`.
   A tile is dragged off, but scrolling the sheet to REACH the tray is the same
   movement of the same thumb. `none` claimed the gesture outright and the tray
   became unreachable — with three dozen tiles, most of that area is tiles, so
   there was nothing left to scroll with. The browser keeps vertical scrolling;
   the tile takes over only after a short hold. See HOLD_MS in play.js. */
.cam-prop { touch-action: pan-y; }
.cam-prop:active { transform: scale(0.94); }
.cam-prop-art svg { width: 100%; height: 100%; display: block; }

.cam-frame { position: relative; }
/* CENTRED, with Undo on the right and no say in where the words sit.
   It was `space-between`, so the heading sat left while Undo was showing and
   jumped to the middle the moment the last prop came off — a heading that
   moves when you press something else reads as a fault.

   THREE COLUMNS RATHER THAN AN ABSOLUTE BUTTON. Absolute was the first go and
   it collided: once the row was inside the guide card it lost 20px of padding
   either side, and at 320px the centred heading ran under Undo with nothing to
   stop it. Two matching `1fr` columns keep the middle one dead centre, and
   when there is genuinely not enough room the grid gives the button its width
   and moves the heading rather than printing one on top of the other.

   Measured: dead centre at 390px in both states, and at 320px the heading
   sits 7px left of centre while Undo is up, clearing it by 8px. Seven pixels
   once, when the first prop goes on, is the cheaper of the two — the other
   way of buying it back is a smaller button, and Undo is already a small
   target on a phone. */
.cam-looks-head {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 8px;
}
.cam-looks-head > span { grid-column: 2; }
.cam-looks-head .cam-undo { grid-column: 3; justify-self: end; }
/* Under the photo, centred, on a row of its own. It acts on the picture rather
   than on the props, and grouping it with Undo made it read as a button shoved
   into a corner — which is exactly how it was reported. */
.cam-photo-tools { display: flex; justify-content: center; margin: 8px 0 2px; }
.cam-flip {
  padding: 4px 10px;
  border-radius: var(--r-pill);
  border: 1px solid var(--panel-line);
  background: none;
  color: var(--ink-dim);
  font: inherit;
  font-size: var(--fs-mini);
  font-weight: 700;
  cursor: pointer;
}
/* Lit while it is on, so it is a STATE rather than a thing you press and
   wonder about. Gold is the only colour on this panel that means anything. */
.cam-flip.on { color: var(--gold); border-color: rgba(255, 210, 63, 0.55); }
/* Tonight's look gets the top row and says which night it is, in the gold the
   rest of the panel's headings use. Only ever on screen when the game carries
   a look — an ordinary quiz sees the tray it always saw. */
.cam-season-head .cam-season-name { color: var(--gold); }
.cam-props-season { margin-bottom: 10px; }
.cam-undo {
  padding: 4px 10px;
  border-radius: var(--r-pill);
  border: 1px solid var(--panel-line);
  background: none;
  color: var(--ink-dim);
  font: inherit;
  font-size: var(--fs-mini);
  font-weight: 700;
  cursor: pointer;
}
/* Four gestures as a 2x2 BLOCK of CHIPS rather than a sentence.
   Written as one line of prose it broke wherever the phone happened to run out
   of room — "pinch to size" on one line and "and turn" on the next, with a
   short ragged line under it. Wrapped as bare chunks it was even but looked
   like four pieces of text floating over the tray with nothing holding them.

   `1fr` columns rather than `auto`: with `auto` each column sizes to its own
   longest chunk, so "Tap to add" got a chip half the width of the one beside
   it and the block read as four different objects. Equal columns and equal
   rows (`grid-auto-rows: 1fr`) make four identical cards, which is the whole
   point of giving them a background.

   AND IT NEEDS ITS OWN FONT SIZE, a point below the phone's `.tiny`, because
   this text is inside chips two to a row rather than running the full width.
   Written as `.cam-hint.tiny` so it matches `.phone .tiny` for specificity and
   wins on source order — the element carries both classes. */
.cam-hint.tiny {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: 1fr;
  gap: 6px;
  margin: 0;
  font-size: var(--fs-mini);
  line-height: 1.3;
}
/* THE CHIP'S OWN FILL AND BORDER MOVE TO A PSEUDO-ELEMENT, which looks like
   fuss and is not: the text below is painted with `background-clip: text`, and
   that clips EVERY background on the element — so a fill left on the span
   itself would be cut to the shape of the words and the chip would vanish. */
.cam-hint span {
  position: relative;
  z-index: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px 7px;
  color: var(--ink-dim);
  font-weight: 700;
  text-align: center;
}
.cam-hint span::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: var(--r-field);
  border: 1px solid var(--panel-line);
  background: rgba(255, 255, 255, 0.06);
}
/* THE HEADING AND THE GESTURES ARE ONE CARD — a heading floating above a block
   of chips is a fifth loose object rather than a label on them.

   A HAIRLINE OF THE APP'S OWN GRADIENT, AND NOTHING INSIDE IT. It was a filled
   gold wash first, and a filled block IS the objection: the red-into-orange is
   the app's one "press this" — Launch on the console, Send it up directly above
   this — so a second slab of it doing nothing when pressed would cost the real
   button its meaning. One pixel of it cannot: a line draws the boundary round
   the five things and leaves the weight to the button.

   `--hot`/`--hot-2` rather than a colour written out, so it follows both the
   quizmaster's own scheme and every seasonal look. A gradient cannot go in
   `border-color`, so the ring is a gradient box with its middle masked out.
   THE FALLBACK IS THE POINT OF THE @supports: without mask compositing the
   ::after would paint as a solid gradient slab — the exact thing this is not
   meant to be — so the plain hairline is what a browser gets by default and
   the gradient is added only where it can be drawn as a ring. */
.cam-guide {
  position: relative;
  border-radius: var(--r-card);
  padding: 10px;
  margin: 14px 0 10px;
}
.cam-guide::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid var(--panel-line);
  pointer-events: none;
}
@supports ((mask-composite: exclude) or (-webkit-mask-composite: xor)) {
  .cam-guide::after {
    border: 0;
    padding: 1px;
    background: linear-gradient(120deg, var(--hot), var(--hot-2));
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask-composite: exclude;
  }
}
/* CENTRED at the ordinary tracking. It was justified out to both edges for a
   version — "fill the width" — and it was wrong: justification spends whatever
   is left over on the two word gaps, so STICK · SOMETHING · ON came out as
   three headings with holes between them, and widening the tracking to close
   the holes only traded one kind of stretched for another. Do not re-propose
   filling the width; the card's own line is what says these five belong
   together. */
.cam-guide .cam-looks-head { margin: 0 0 9px; }

/* ALL FIVE TEXTS RUN THE SAME GRADIENT AS THE HAIRLINE, and the four chips
   take a QUADRANT of it each — `background-size: 200% 200%` with a corner
   apiece — so the block reads as one sweep across the card rather than as
   four small rainbows repeating the same two colours.

   Behind an `@supports`, and this guard is not optional: painting text with a
   gradient means setting the text itself to transparent, so a browser without
   `background-clip: text` would render five INVISIBLE labels. The default is
   the ordinary ink and the gradient is added only where it can be seen. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .cam-guide .cam-looks-head > span,
  .cam-hint span {
    background-image: linear-gradient(120deg, var(--hot), var(--hot-2));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
  }
  .cam-hint span { background-size: 200% 200%; }
  .cam-hint span:nth-child(1) { background-position: 0% 0%; }
  .cam-hint span:nth-child(2) { background-position: 100% 0%; }
  .cam-hint span:nth-child(3) { background-position: 0% 100%; }
  .cam-hint span:nth-child(4) { background-position: 100% 100%; }
}
/* touch-action none so dragging a prop does not scroll the sheet under it —
   without this the picture slides away the moment a finger moves. */
.cam-frame .cam-canvas { touch-action: none; }

/* "Something wrong with this one?" on the control view.
   Deliberately quiet and full width at the bottom of the answer key, nowhere
   near the primary button — it is never needed in a hurry, and a mis-tap
   beside Reveal is the thing the rest of that screen exists to prevent. */
.report-q {
  margin-top: 12px;
  width: 100%;
  padding: 9px 12px;
  border-radius: var(--r-field);
  border: 1px dashed var(--panel-line);
  background: none;
  color: var(--ink-dim);
  font: inherit;
  font-size: var(--fs-note);
  cursor: pointer;
}
.report-q:hover:not(:disabled) { color: var(--ink); border-color: var(--gold); }
.report-q:disabled { opacity: 0.7; cursor: default; color: var(--gold); border-style: solid; }

/* Reported questions on the owner page. The question itself is the thing you
   read, so it gets the size; everything else is context underneath it. */
.rep-row {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 12px 0;
  border-bottom: 1px solid var(--panel-line);
}
.rep-row:last-child { border-bottom: 0; }
.rep-row.done { opacity: 0.55; }
.rep-main { flex: 1 1 auto; min-width: 0; }
.rep-q { font-weight: 700; margin-bottom: 4px; }
.rep-acts { flex: 0 0 auto; display: flex; gap: 8px; align-items: center; }
.rep-acts a.minor { text-decoration: none; display: inline-block; }

/* "You are wearing the quizmaster hat." Gold rather than red — it is a mode,
   not a fault — and in the flow of the page rather than floating over it, so
   it cannot cover a control on a phone held in the dark. */
.acting-bar {
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  padding: 8px 14px;
  background: rgba(255, 210, 63, 0.14);
  border-bottom: 1px solid rgba(255, 210, 63, 0.45);
  color: var(--ink);
  font-size: var(--fs-note);
}
.acting-bar button {
  padding: 5px 12px;
  border-radius: var(--r-pill);
  border: 1px solid rgba(255, 210, 63, 0.6);
  background: none;
  color: var(--gold);
  font: inherit;
  font-size: var(--fs-mini);
  font-weight: 800;
  cursor: pointer;
}

/* ---- the hat switch: Owner | Quizmaster, top right, one tap either way.
   It is the sign as well as the switch, so the LIVE half is a solid block of
   colour and the other is barely there — pink for the owner, gold for the
   quizmaster, the same gold the acting bar has always used. Answered at a
   glance, not read. The dim half still passes contrast, because it is a button
   you are meant to find. */
.hat-switch {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 3px;
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--panel-line);
  flex: 0 0 auto;
}
.hat-switch .hat-half {
  /* 10px rather than 7px top and bottom — 34px tall, not 28. The bar already
     has the room (73px tall, 45px of it inside the padding, and the brand mark
     is what sets that), so the whole switch grew without the topbar moving a
     pixel. It is levelled with the tier rungs beside it, which had to grow for
     the reason written over them; a secondary control taller than the primary
     one it sits inside would read as the wrong way round. */
  padding: 10px 14px;
  border: none;
  border-radius: var(--r-pill);
  background: none;
  color: var(--ink-dim);
  font: inherit;
  font-size: var(--fs-mini);
  font-weight: 800;
  letter-spacing: 0.04em;
  cursor: pointer;
  white-space: nowrap;
  transition: background 140ms ease, color 140ms ease;
}
.hat-switch .hat-half:hover:not(.live) { background: rgba(255, 255, 255, 0.08); color: var(--ink); }
/* Whichever half is live wears the app's own red-into-orange, whatever it is.
   The tier rungs beside it carry the metals — bronze, silver, gold — because
   there the colour IS the meaning. Two colour languages in one control would
   be one too many. Which HAT is on is still unmissable: the body takes a gold
   hairline under the topbar whenever the quizmaster hat is worn. */
.hat-switch .hat-half.live {
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  color: #12000c;
}
.hat-switch.working { opacity: 0.55; }
.hat-switch .hat-half:disabled { cursor: default; }

/* Which RUNG the hat is being worn as. Inside the switch, because it only means
   anything while the hat is on. Initials rather than words — B S G with "All"
   in front — because this is a corner of a topbar, not a settings page, and the
   metal on the live one says which is which faster than the label would. */
/* THE RUNGS WERE 24 x 22px WITH 2px BETWEEN THEM, and that is what was wrong
   with them: *"the clickable part of the G/S/B needs to increase, it's hard to
   click at the moment."* Measured before changing anything — 528px² each, and
   a two-pixel gap, so a slightly-off press lands on the neighbouring tier and
   silently previews the wrong one.

   They are 30 x 34 now with 5px between them: **the hit area roughly doubles
   and the gap between two adjacent targets more than doubles**, which is the
   half of "hard to click" that a bigger button on its own does not fix.

   HEIGHT IS THE FREE DIMENSION AND WIDTH IS NOT, which is why it is 34 rather
   than the app's 44px touch floor. The topbar has 45px of room inside its
   padding, so growing DOWNWARDS costs nothing at all; growing sideways costs
   the bar 68px it does not have at 1180px, and four 44px rungs would push the
   owner's topbar onto two rows at every width there is. A mouse on a laptop is
   what presses this — it is the owner's own control and lives in a corner of a
   sticky header — so the floor that exists for a thumb in a dark pub is the
   wrong rule to spend the whole bar on. */
.tier-preview {
  display: inline-flex; align-items: center; gap: 5px;
  margin-left: 4px; padding-left: 6px;
  border-left: 1px solid var(--panel-line);
}
.tier-preview .tier-half {
  min-width: 30px;
  /* 11px, not 10 — the rungs are a smaller font than the hat halves beside
     them, so equal padding came out two pixels shorter. Levelled on the drawn
     HEIGHT rather than on the number in the rule. */
  padding: 11px 7px;
  border: none; border-radius: var(--r-pill);
  background: none; color: var(--ink-dim);
  font: inherit; font-size: var(--fs-tag); font-weight: 800;
  cursor: pointer;
}
.tier-preview .tier-half:hover:not(.live) { background: rgba(255, 255, 255, 0.1); color: var(--ink); }
.tier-preview .tier-half.live { background: rgba(255, 255, 255, 0.16); color: var(--ink); }
.tier-preview .tier-half.t-bronze.live { background: var(--metal-sheen), var(--metal-bronze); color: var(--metal-bronze-ink); }
.tier-preview .tier-half.t-silver.live { background: var(--metal-sheen), var(--metal-silver); color: var(--metal-silver-ink); }
.tier-preview .tier-half.t-gold.live   { background: var(--metal-sheen), var(--metal-gold); color: var(--metal-gold-ink); }
.tier-preview .tier-half:disabled { opacity: 0.5; cursor: default; }

/* ---- THE SUBSCRIBER'S OWN RUNGS — `console-tiers.js`.
 *
 * The owner's control above is a downgrade tool and lives in a topbar corner,
 * so it is three initials. This one sells, is read by somebody who has never
 * seen it, and sits in a panel with room — so it spells the words. Rendered
 * side by side before choosing: a locked `S` at half opacity is a grey dot.
 *
 * The metal is the meaning here, exactly as on the rungs above and the tier
 * panels — a scheme changes personality, never meaning. A locked rung KEEPS
 * its metal, dimmed, rather than going flat grey: it has to read as the thing
 * you could have. */
.tier-rungs { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.tier-rung {
  padding: 7px 13px; min-height: 32px;
  border: 1px solid var(--panel-line); border-radius: var(--r-pill);
  background: none; color: var(--ink-dim);
  font: inherit; font-size: var(--fs-tag); font-weight: 800;
  cursor: pointer;
}
.tier-rung.tier-bronze { --tier: var(--metal-bronze); --tier-ink: var(--metal-bronze-ink); }
.tier-rung.tier-silver { --tier: var(--metal-silver); --tier-ink: var(--metal-silver-ink); }
.tier-rung.tier-gold   { --tier: var(--metal-gold); --tier-ink: var(--metal-gold-ink); }
/* Yours and on-trial are both "this works" and look the same, because they
   are the same fact from the account's side. What differs is how long, and
   the card says that in words rather than in a third colour nobody can
   decode. */
.tier-rung.is-yours, .tier-rung.is-trial {
  background: var(--metal-sheen), var(--tier); color: var(--tier-ink); border-color: transparent;
}
/* The rgba fallback comes FIRST, or a browser without `color-mix` drops the
   declaration and the locked rung loses its tint entirely. */
.tier-rung.is-locked { color: var(--ink-dim); opacity: 0.72; }
.tier-rung.is-locked { color: color-mix(in srgb, var(--tier) 72%, var(--ink-dim)); }
.tier-rung.is-locked:hover, .tier-rung.is-locked:focus-visible { opacity: 1; }
.tier-rung[aria-expanded="true"] { outline: 2px solid var(--hot); outline-offset: 2px; }

/* `rung-row`, NOT `tier-row` — that name is already the owner page's
   feature-mover (`owner.js`), whose own rule sits 3,300 lines lower in this
   sheet and therefore WON: my block laid itself out as a flex row, the card
   floated beside the pills, and at 390px the text wrapped to "A topical quiz /
   every week". Nothing threw and it looked deliberate. Same trap as the
   `border` shorthand, wearing a class name. */
.rung-row { margin-top: 10px; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }
.tier-card {
  width: 100%; max-width: 340px; padding: 12px 14px;
  background: var(--surf-2, rgba(255, 255, 255, 0.08));
  border: 1px solid var(--panel-line); border-radius: var(--r-card);
}
.tier-card-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; }
.tier-card-head.tier-bronze { --tier: var(--metal-bronze); }
.tier-card-head.tier-silver { --tier: var(--metal-silver); }
.tier-card-head.tier-gold   { --tier: var(--metal-gold); }
.tier-card-head b { color: var(--tier, var(--ink)); font-size: var(--fs-head); }
.tier-adds { margin: 0 0 8px; padding-left: 18px; }
.tier-adds li { color: var(--ink-dim); line-height: 1.6; }
.tier-card-foot { color: var(--ink); }

/* The whole page picks up a gold hairline while the quizmaster hat is on, so
   even a screenshot of the middle of the console says which hat it was taken
   in. Cheaper than a banner and it covers nothing. */
body.wearing-hat .topbar { border-bottom-color: rgba(255, 210, 63, 0.5); }

/* On a phone the topbar is already tight, so the switch loses its padding
   before it loses its words — "Owner" and "Quizmaster" are the whole point.

   AND THE RUNGS COME DOWN WITH IT, or the thing INSIDE the switch is taller
   than the switch. They were levelled at 34px for the reason written over
   `.tier-half` — *"the clickable part of the G/S/B needs to increase, it's hard
   to click"* — and that argument is about a mouse on a laptop, which is where
   the bar has 45px inside its padding to grow into. This rule was already
   putting the halves on a diet at 560px and the rungs were left out of it, so
   the owner's bar came out 34px of rung inside a 24px pill and ran 5px off the
   right of a 390px screen: the whole console scrolled sideways, on the one
   account that has these controls at all. Same padding as the halves, and the
   media rule above gives both the same font, so they draw the same height. */
/*
 * THE HAT SLOT MUST BE ABLE TO SHRINK, AND ON `/host` IT COULD NOT.
 *
 * `#hatSlot` is a bare `<span>`, so it is a flex item with the default
 * `min-width: auto` — the third sighting of that in this file, and the second
 * on this bar. The owner's three-position switch is 351px wide, the control
 * view's top row is 346, and because the span refused to give ground the
 * whole row simply grew: measured with the owner's own cookie and key,
 * **`/host` scrolled sideways 161px at 390, 191 at 360 and 231 at 320** — his
 * actual bookmark — with a menu chip 169px off the side of the screen.
 *
 * `/console` was the same fault one size smaller: 53px off at 320 and 13 at
 * 360, where `console-frame.mjs`'s narrowest phone is 390 and saw nothing.
 *
 * **Only the OWNER has any of this** — `/api/me` sends `tiers: []` to a real
 * quizmaster, so `hatSwitch()` returns null and neither page has a switch to
 * find room for.
 */
#hatSlot { min-width: 0; display: inline-flex; }

@media (max-width: 560px) {
  .hat-switch .hat-half { padding: 6px 10px; font-size: var(--fs-tag); }
  .tier-preview .tier-half { min-width: 24px; padding: 6px 6px; }
  /*
   * AND THE SWITCH ITSELF GIVES GROUND ON THE NARROWEST PHONES.
   *
   * Even on the diet the owner's three-position switch is 351px, which does
   * not fit a 320 or 360 screen however much room the row makes for it —
   * `.hat-switch` is `white-space: nowrap` (so the halves cannot break) and
   * carried the default `min-width: auto` (so it could not shrink either).
   * A LABEL losing its tail is the thing this bar can afford; a door off the
   * side of the screen is not, and the halves stay whole down to 390.
   */
  .hat-switch { min-width: 0; max-width: 100%; }
  .hat-switch .hat-half { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  /*
   * AND ON A PHONE THE SWITCH TAKES ITS OWN LINE rather than squeezing the
   * menu off the side. The control view's header is not the working surface —
   * the actions bar is fixed at the bottom — so a second row up here costs
   * nothing, where a door nobody can reach costs the page.
   */
  .host .bar .bar-top { flex-wrap: wrap; }
  .host .bar-top #hatSlot { flex: 1 1 100%; }
}

/* The record of what the owner did while inside somebody's account. Small and
   plain: it is a log to scan, not a feature to admire — and it has to stay
   readable when it is forty lines long. */
.support-log { margin-top: 8px; max-height: 260px; overflow-y: auto; }
.support-row {
  display: flex; gap: 12px; align-items: baseline;
  padding: 6px 0; border-top: 1px solid var(--panel-line);
  font-size: var(--fs-note);
}
.support-row .tiny { flex: 0 0 auto; min-width: 132px; }

/* "Still need help?" — shown while there is still time to answer, not at the
   moment the grant dies. Gold rather than red: nothing is wrong, something is
   about to lapse, and those are different feelings. */
.support-still {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-top: 10px; padding: 10px 14px;
  border-radius: var(--r-card);
  border: 1px solid rgba(255, 210, 63, 0.5);
  background: rgba(255, 210, 63, 0.1);
  font-size: var(--fs-note);
}

/* The suggestion box. Plain and quick: the point is that sending one costs
   almost nothing, so it must not look like a form to fill in. */
.sugg-kinds { flex-wrap: wrap; gap: 6px; }
.sugg-kind.on {
  background: linear-gradient(120deg, var(--hot), var(--hot-2)) !important;
  color: #12000c; border-color: transparent;
}
.sugg-text {
  width: 100%; margin-top: 10px; padding: 11px 13px;
  border-radius: var(--r-card); border: 1px solid var(--panel-line);
  background: rgba(255, 255, 255, 0.05); color: var(--ink);
  font: inherit; font-size: var(--fs-body); resize: vertical;
}
.sugg-text:focus { outline: none; border-color: var(--hot); }
/* Every `.go` in this stylesheet is scoped to the panel it sits in — there is
   no generic rule — so this button needs its own or it draws as the browser's
   default grey, which reads as disabled next to the lit kind chips. */
.sugg-send {
  border: none; border-radius: var(--r-field); padding: 11px 20px; cursor: pointer;
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  color: #12000c; font-family: inherit; font-weight: 900; font-size: var(--fs-ctl);
}
.sugg-send:hover { filter: brightness(1.08); }

/* ---- the inbox.
   A message list rather than a table: each one is somebody's sentence, and the
   thing you do with it is read it and answer it. Cleared ones fade rather than
   disappear, so the list still shows the shape of what has been dealt with. */
.sugg-item {
  padding: 14px 16px; margin-bottom: 10px;
  border-radius: var(--r-card);
  border: 1px solid var(--panel-line);
  background: rgba(255, 255, 255, 0.04);
}
.sugg-item.done { opacity: 0.62; }
.sugg-head { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.sugg-said { margin-top: 7px; font-size: var(--fs-ctl); line-height: 1.5; }
.sugg-kind-tag {
  font-size: var(--fs-tag); font-weight: 900; letter-spacing: 0.05em; text-transform: uppercase;
  padding: 3px 8px; border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.1); color: var(--ink-dim);
}
/* Broken is the one worth spotting from across the page — it is the only kind
   that might be costing somebody a gig right now. */
.sugg-kind-tag.broken { background: rgba(255, 77, 94, 0.18); color: var(--bad); }
.sugg-kind-tag.idea { background: rgba(47, 224, 122, 0.16); color: var(--good); }
/* A pack request wears Gold, because that is the rung it comes from and
 * because it is the one entry in this list that costs money to answer. */
.sugg-kind-tag.pack { background: rgba(255, 200, 90, 0.18); color: #ffc85a; }
.pile.packs.on { border-color: #ffc85a; color: #ffc85a; }
.sugg-reply {
  margin-top: 10px; padding: 9px 13px;
  border-left: 3px solid var(--hot);
  background: rgba(255, 255, 255, 0.04); border-radius: 0 10px 10px 0;
  font-size: var(--fs-body); line-height: 1.5;
}
.sugg-acts { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 11px; }
.sugg-compose { margin-top: 11px; }
.sugg-draft {
  width: 100%; padding: 11px 13px;
  border-radius: var(--r-card); border: 1px solid var(--panel-line);
  background: rgba(255, 255, 255, 0.05); color: var(--ink);
  font: inherit; font-size: var(--fs-body); resize: vertical;
}
.sugg-draft:focus { outline: none; border-color: var(--hot); }
.sugg-compose .go {
  border: none; border-radius: var(--r-field); padding: 10px 18px; cursor: pointer;
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  color: #12000c; font-family: inherit; font-weight: 900; font-size: var(--fs-body);
}
.pile.on {
  background: linear-gradient(120deg, var(--hot), var(--hot-2)) !important;
  color: #12000c; border-color: transparent;
}

/* What a quizmaster has sent, and what came back. */
.sugg-mine-row {
  padding: 10px 0; border-top: 1px solid var(--panel-line);
  font-size: var(--fs-body); line-height: 1.5;
}

/* Whether they have opened a reply. Deliberately "opened" rather than "read":
   it means their console drew it, which is all any read receipt has ever
   meant, and overstating it would be the one thing on this page that lies. */
.seen.yes { color: var(--good); }
.seen.no { color: var(--ink-dim); }

/* What the drafting model is told. Folded away, because it is a thing you edit
   once a month rather than something to read every time you open the inbox. */
.house { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--panel-line); }
.house summary { cursor: pointer; color: var(--ink-dim); }
.house summary:hover { color: var(--ink); }
.house-note { margin: 8px 0; max-width: 720px; line-height: 1.55; }
.house-text {
  width: 100%; padding: 11px 13px;
  border-radius: var(--r-card); border: 1px solid var(--panel-line);
  background: rgba(255, 255, 255, 0.05); color: var(--ink);
  font: inherit; font-size: var(--fs-note); resize: vertical;
}
.house-text:focus { outline: none; border-color: var(--hot); }


/*
 * A pack somebody wrote themselves.
 *
 * A quiet gold hairline and a small word, not a badge shouting for attention:
 * the grid is mostly the catalogue and this says "and this one is mine" at a
 * glance without turning the shelf into two shelves. Same gold as the hat's
 * hairline, because it means the same thing there — whose this is.
 */
.pack-card.own { position: relative; border-color: rgba(255, 200, 90, 0.45); }
/* The title is a button and runs full width, so the badge must not eat a tap
   meant for it — hence pointer-events: none below. */
.pack-card.own .pack-title { padding-right: 56px; }
.pack-yours {
  position: absolute;
  top: 10px;
  right: 10px;
  font-size: 0.62rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: #ffc85a;
  border: 1px solid rgba(255, 200, 90, 0.45);
  border-radius: var(--r-pill);
  padding: 1px 7px;
  pointer-events: none;
}

/*
 * "Open the editor" on the own-packs panel.
 *
 * Every `.go` in this stylesheet is scoped to the panel it sits in — there is
 * no generic rule — so an anchor with that class drew as a plain underlined
 * link, which reads as a footnote rather than as the way in. Its own class,
 * sized to sit under a paragraph rather than fill the panel.
 */
.own-open {
  /* inline-block, not a bare inline: with padding on an inline element the
     button overlaps the paragraph above and below it rather than taking a line
     of its own. */
  display: inline-block;
  align-self: flex-start;
  margin: 12px 0 4px;
  border-radius: var(--r-field);
  padding: 11px 20px;
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  color: #12000c;
  font-weight: 900;
  font-size: var(--fs-ctl);
  text-decoration: none;
}
.own-open:hover { filter: brightness(1.08); }

/* The not-backed-up line under it is a separate thought, not a caption. */
.panel .own-open + .warn { display: block; margin-top: 8px; }


/* ---------------------------------------------------- the owner's overview
 *
 * Four sections that did not exist: what is running tonight, the money, the
 * catalogue as a product, and one subscriber opened up. All of them are rows
 * of "a thing on the left, a number on the right", so they share one shape
 * rather than growing four near-identical ones that drift.
 */
.own-figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 10px;
  margin-top: 12px;
}
.own-fig {
  background: var(--panel);
  border: 1px solid var(--panel-line);
  border-radius: var(--r-card);
  padding: 14px;
}
.own-fig b { display: block; font-size: 26px; font-weight: 900; line-height: 1.1; }
.own-fig span { display: block; margin-top: 4px; font-size: var(--fs-mini); opacity: 0.72; }
/* Spending more than is arriving is the one figure that should catch the eye. */
.own-fig.bad { border-color: var(--bad); }
.own-fig.bad b { color: var(--bad); }

/*
 * The month against its ceiling.
 *
 * Three states rather than a colour that slides: "fine", "getting close" and
 * "over" are the only three things anybody does anything about, and a bar that
 * shaded gradually would need reading rather than glancing at.
 */
.budget-bar {
  margin-top: 12px;
  height: 12px;
  border-radius: var(--r-pill);
  background: var(--panel);
  border: 1px solid var(--panel-line);
  overflow: hidden;
}
.budget-bar span {
  display: block;
  height: 100%;
  border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--hot, #ff2e88), var(--hot-2, #ff8a3d));
}
.budget-bar.close span { background: var(--gold); }
.budget-bar.over span { background: var(--bad); }

.budget-set { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
.budget-set label { display: flex; align-items: center; gap: 8px; }
.budget-input {
  width: 108px;
  padding: 8px 10px;
  font: inherit;
  color: inherit;
  background: var(--panel);
  border: 1px solid var(--panel-line);
  border-radius: var(--r-field);
}

/* One job's share of the bill, under its name. Thin on purpose — it is a sense
 * of proportion beside a number, not a chart. */
.job-bar {
  margin-top: 6px;
  height: 5px;
  max-width: 380px;
  border-radius: var(--r-pill);
  background: var(--panel-line);
  overflow: hidden;
}
.job-bar span {
  display: block; height: 100%; border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--hot, #ff2e88), var(--hot-2, #ff8a3d));
}
.own-row-num .tiny { display: block; opacity: 0.7; font-weight: 600; }

.own-rows { display: grid; gap: 6px; }
.own-row {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 10px 12px;
  background: var(--panel);
  border: 1px solid var(--panel-line);
  border-radius: var(--r-field);
}
.own-row-main { min-width: 0; }
.own-row-num { flex: 0 0 auto; text-align: right; white-space: nowrap; }
/* A room with a game actually in it — the reason this tab exists. */
.own-row.live { border-color: rgba(255, 200, 90, 0.5); }
.own-live { color: #ffc85a; font-weight: 900; font-size: var(--fs-note); }
.own-code {
  margin-left: 8px; padding: 1px 7px; border-radius: var(--r-field);
  background: rgba(255,255,255,0.08); font-weight: 900; letter-spacing: 0.08em; font-size: var(--fs-mini);
}
.own-flag {
  margin-left: 8px; padding: 1px 7px; border-radius: var(--r-pill); font-size: var(--fs-tag);
  background: rgba(255, 200, 90, 0.18); color: #ffc85a;
}
.own-flag.bad { background: rgba(255, 90, 90, 0.18); color: var(--bad); }

/* One subscriber, opened up under their own row. */
.panel.person { border-color: var(--hot); margin: -2px 0 10px; }
.inv-main.open-person { cursor: pointer; }
.inv-main.open-person:hover b { text-decoration: underline; }


/* --------------------------------------------------------------- the shop
 *
 * A pack on the shelf rather than in the library. Deliberately quieter than a
 * card you can launch: it is something to consider, not something to do — and
 * the packs you can actually run tonight have to stay the loud half of the
 * page for somebody who opened it ten minutes before a gig.
 */
.pack-card.locked {
  border-style: dashed;
  border-color: var(--panel-line);
  opacity: 0.92;
}
.pack-card.locked .pack-title { opacity: 0.9; }
.shop-price { font-size: var(--fs-sub); font-weight: 900; opacity: 1; margin-top: 4px; }
.pack-card.locked .go.buy {
  width: 100%; margin-top: 10px;
  border: none; border-radius: var(--r-field); padding: 11px; cursor: pointer;
  /* Not the app's own red-into-orange: that gradient is Launch, and one shape
     and one colour for "start the night" is worth more than a loud shop. */
  background: rgba(255,255,255,0.1); color: var(--ink);
  font-family: inherit; font-weight: 900; font-size: var(--fs-ctl);
}
.pack-card.locked .go.buy:hover { background: rgba(255,255,255,0.16); }
/* THE SHOP IS ITS OWN TAB NOW, behind My account — the head is the section's
   own, so it needs no top margin pushing it away from a shelf above it. What
   is left is the gap between the packs and the tiers underneath them. */
.shop-tiers { margin-top: 26px; }


/* A pack written to be current, and one that no longer is.
 *
 * Not hidden when it expires — a pack that vanished reads as lost work, and
 * last month's news round is a fine thing to run on purpose. It just stops
 * being the first thing you see, and it says which week it was for.
 */
/*
 * What a rung includes, as a STATEMENT rather than a switch.
 *
 * Content is not something you turn off — it is what you pay for — so it gets
 * the word "included" where a capability gets an On | Off. Drawn quietly and
 * without a border of its own, so it reads as the heading of the section
 * rather than as the first item in a list of controls.
 */
.acct-toggle.content { background: rgba(255, 255, 255, 0.03); }
.feat-said {
  font-size: var(--fs-tag); font-weight: 900; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-dim); white-space: nowrap; padding: 6px 10px;
}
/* NOT BUILT YET is a different state from "included" and has to look it —
   side by side in the same grey they read as two ways of saying yes. Gold,
   which is what this app uses for "worth knowing", and outlined so it is
   plainly not something to press. */
.feat-soon {
  color: var(--gold); opacity: 0.75;
  border: 1px dashed rgba(255, 210, 63, 0.35); border-radius: var(--r-pill);
}

/*
 * "Nothing here for the night you have booked?" — under the shop, because that
 * is where the want arrives. Quieter than a pack card on purpose: it is the
 * last thing on the tab and must not compete with Launch.
 */
.panel.ask-pack { margin-top: 18px; }
.panel.ask-pack h3 { margin-bottom: 4px; }
.panel.ask-pack textarea {
  width: 100%; margin-top: 10px; padding: 10px; border-radius: var(--r-field);
  background: var(--panel-2, rgba(255,255,255,0.04)); color: var(--ink);
  border: 1px solid var(--panel-line); font: inherit; font-size: var(--fs-body); resize: vertical;
}
.panel.ask-pack.locked { border-style: dashed; opacity: 0.85; }
/* Ask for it is a role-make now, not a .go — it wore the full account gradient
 * and put a SECOND "press this" fill on the Music Quiz tab, a section below
 * Launch. Only the sizing is left here; the colour comes from `.role-make`,
 * which every other "this makes something" button on the console already
 * uses. */
.panel.ask-pack .role-make { padding: 11px 20px; white-space: nowrap; }

/*
 * "18 phones waiting to join." Gold rather than red: nothing has gone wrong,
 * the host is being asked a question. Above the player list, where they are
 * already looking while a room fills up.
 */
.panel.joinq { border-color: #ffc85a; }
.panel.joinq h3 { color: #ffc85a; }
.panel.joinq .go {
  margin-top: 10px; width: 100%; border: none; border-radius: var(--r-field); padding: 13px;
  background: linear-gradient(120deg, var(--hot), var(--hot-2)); color: #12000c;
  font-weight: 900; font-size: var(--fs-field); cursor: pointer;
}
.plist + .tidy, button.tidy { margin-top: 10px; width: 100%; }

.pack-card .fresh { color: #ffc85a; }
.pack-card .fresh.gone { color: var(--ink-dim); }
.pack-card.stale { opacity: 0.72; }
.pack-card.stale:hover { opacity: 1; }

/* Your four letters, at a size you can read out over a microphone. */
.room-code {
  font-size: 40px; font-weight: 900; letter-spacing: 0.14em;
  line-height: 1.1; margin: 6px 0 8px;
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}

/* Which tier a locked thing is on, in that tier's own colour. A bare gold plus
   on everything says "there is more" and nothing else; these say which rung,
   before the tooltip is read. */
.tabcount.lock.lock-silver { background: rgba(203, 213, 225, 0.2); color: var(--metal-silver); }
.tabcount.lock.lock-gold   { background: rgba(255, 210, 63, 0.22); color: var(--gold); }
.tabcount.lock.lock-bronze { background: rgba(205, 127, 50, 0.22); color: var(--metal-bronze); }

/* Sign out belongs in the furniture, not four panels down a settings page. */
.topbar-out { margin-left: 10px; }

/* ---- The launch bar: the fast way to start a night.
 *
 * Deliberately the loudest thing on the page when nothing is running, and gone
 * entirely when something is — the top of the console answers one question at
 * a time, and mid-game that question is "what is on the projector".
 */
.launchbar { border-color: rgba(255, 46, 136, 0.35); }
.lb-find { display: flex; gap: 8px; align-items: center; margin-top: 10px; flex-wrap: wrap; }
.lb-game { flex: 0 0 auto; }
.lb-search { position: relative; flex: 1 1 260px; min-width: 200px; }
.lb-text {
  width: 100%; padding: 13px 14px; font: inherit; font-size: var(--fs-lead);
  color: inherit; background: var(--panel); border: 1px solid var(--panel-line);
  border-radius: var(--r-field);
}
.lb-text:focus { outline: none; border-color: var(--hot); }

/* The matches, over the page rather than pushing it about — a list that shoved
   everything down on every keystroke would move the button you are aiming for. */
.lb-hits {
  position: absolute; left: 0; right: 0; top: calc(100% + 6px); z-index: 40;
  display: grid; gap: 2px; padding: 6px;
  background: var(--bg-2, #14141f); border: 1px solid var(--panel-line);
  border-radius: var(--r-field); box-shadow: var(--shadow);
}
.lb-hit {
  text-align: left; padding: 11px 12px; font: inherit; font-weight: 700;
  color: inherit; background: none; border: none; border-radius: var(--r-field); cursor: pointer;
}
.lb-hit:hover { background: rgba(255, 255, 255, 0.09); outline: none; }
/* FOCUS GETS ITS OWN SIGNAL, not the hover one borrowed — a touch-target audit
   found several non-input controls where a keyboard tab and a mouse hover
   were visually the same state, which is a weaker signal than either alone
   for someone navigating without a cursor. The account's own colour, like
   every input's focus already uses — same language, a control that is not
   an input. */
.lb-hit:focus-visible { background: rgba(255, 255, 255, 0.09); outline: 2px solid var(--hot); outline-offset: -2px; }

/* SET IT UP — the settings, once it is open.
   A GRID rather than a wrapping flex row, and every child is allowed to
   shrink: a `<select>` will not go below the width of its longest option on
   its own, so "Online — the question goes on their phones" dragged the whole
   console 90px off the side of a 320px phone. That is the seventh time this
   file has recorded that fault; `min-width: 0` on the child and
   `max-width: 100%` on the select are both needed. */
.lb-set {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 10px; margin: 12px 0 10px;
}
/*
 * ONE ROW OF FIVE ON A LAPTOP — *"starting to look a bit messy, can we
 * utilise space where possible"*.
 *
 * `auto-fit` with a 240px floor gives four columns on a wide console, and
 * "While they wait" spans two of them — so the five night settings landed as
 * three plus two, with a third of the second row empty and the bar a row
 * taller than it needed to be. At 1150px and up there is room for all five
 * side by side, which is also where they read as one question each rather
 * than as two groups that mean different things.
 *
 * The span is dropped in the same breakpoint: it exists so the longest label
 * on the bar is not cramped, and at a fifth of 1150px it is not.
 */
/*
 * EACH SETTING ONLY AS WIDE AS ITS ANSWER — *"'look — the usual' needs to only
 * be as wide as the pre-filled value… we need to save space."*
 *
 * A grid of five equal columns gave every control the width of the widest one,
 * so "On" and "20" were each a fifth of the bar. A wrapping flex row lets each
 * take what it needs and no more, and the row still fills left to right — the
 * open MENU is where the space comes from now, and a menu costs nothing until
 * somebody opens it (see `console-pick.js`).
 */
.lb-set-night {
  /* 12px between columns rather than 14: eight controls on one line means
     seven gaps, so two pixels each is a whole heading's worth of room. */
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px 12px;
}
/*
 * THE ROW STAYS ONE LINE, AND A CELL GIVES GROUND BEFORE THE ROW DOES.
 *
 * Each cell asks for the width of its own heading or its widest option,
 * whichever is longer — but it may SHRINK (`0 1 auto`, `min-width: 0`) rather
 * than push the row onto a second line. Every setting for the night stays on
 * one line at a laptop width, which is what was asked for, and what gives way
 * first is a heading's tail rather than the arrangement.
 *
 * Without the shrink the row wrapped the moment a face reserved its widest
 * option — which is exactly the trade: a control that never moves when you
 * pick something is a slightly wider control.
 */
.lb-set-night .pack-shape { flex: 0 1 auto; min-width: 0; }
/* The face fills the cell its label made — see the note below. */
.lb-set-night .pack-shape .pick, .lb-set-night .pack-shape .pick-face { width: 100%; }
.lb-set-night .pack-shape > input { width: 100%; }
/* A heading that has run out of room ellipsises rather than wrapping: a
   second line under one cell drags the whole row down with it. */
.lb-set-night .set-word { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
/*
 * AND WRAPPING IS OFF WHERE THE ROW IS MEANT TO BE ONE LINE.
 *
 * `flex-shrink` alone could not hold it: a wrapping flex row WRAPS FIRST and
 * shrinks afterwards, within each line — so a row over its width by twenty
 * pixels drops a control onto a second line rather than taking twenty pixels
 * off eight of them. Above 1150px, where every night setting is meant to read
 * as one row, nothing wraps and the shrink does its job.
 *
 * Below that it still wraps, because on a phone a single line of eight
 * controls is eight controls nobody can read.
 */
@media (min-width: 1150px) {
  .lb-set-night { flex-wrap: nowrap; }
}
/*
 * THE LABEL SITS ABOVE ITS CONTROL, at every width now that the row is
 * content-sized rather than five equal columns. Side by side, a label and a
 * value make a cell as wide as both; stacked, the cell is as wide as the wider
 * of the two — which for "Secs per Q" over a two-digit box is the label, and
 * for "Look" over "The usual" is the value.
 */
.lb-set-night .pack-shape {
  display: grid; grid-template-columns: minmax(0, auto);
  /*
   * STRETCH, NOT START — asked for on 24 August 2026: *"line up the headings
   * with the boxes; it's ok for the boxes to be wider than the headings but
   * when the headings are wider than the boxes it looks messy."*
   *
   * The cell is already as wide as the WIDER of its label and its control —
   * that is what stacking them buys. `justify-items: start` then left the
   * narrower one floating in the space the wider one made: "SECS PER Q" over a
   * two-digit box, "GAME SOUND" over "On". Stretching the control to the cell
   * makes the box the wider of the two rather than the label, every time,
   * which is exactly the rule he stated.
   *
   * It does NOT undo *narrow shut, wide open*. The face is still sized by what
   * is IN it — the cell is only ever as wide as the longer of the value and
   * the word above it, and the open menu is still free to be wider than both.
   */
  justify-items: stretch; align-items: start; gap: 4px; margin: 0;
}
/* =====================================================================
 * THE POPOVER PICKER — narrow shut, wide open. See `console-pick.js` for
 * why a native `<select>` cannot do this: a browser sizes the open list to
 * the control, so a control narrow enough for "The usual" opens a list that
 * clips "Halloween — in season now".
 * ===================================================================== */
.pick { position: relative; display: inline-flex; }
/* The real control is still there and still what the launch reads — it is
   simply not what anybody looks at. `position: absolute` rather than
   `display: none` so it keeps a box for anything that measures it. */
.pick > select {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: 0;
  opacity: 0; pointer-events: none; border: 0;
}
/*
 * THE FACE IS A DROPDOWN, and wears exactly what every other dropdown on the
 * bar wears — same height, fill, border, radius, and the account's own
 * gradient on the chevron block. The GUI rules put that gradient there as the
 * affordance that says "this opens", so a picker without it would read as a
 * label.
 */
.pick-face {
  display: inline-flex; align-items: center; gap: 8px;
  max-width: 100%; min-width: 0;
  padding: 9px 11px;
  border-radius: var(--r-field);
  border: 1px solid var(--panel-line);
  background: rgba(255, 255, 255, 0.07);
  color: var(--ink);
  font-family: inherit; font-size: var(--fs-note); font-weight: 800;
  text-align: left; cursor: pointer;
}
.pick-face:hover { border-color: var(--hot); }
.pick-face:disabled { opacity: 0.6; cursor: default; }
/*
 * THE FACE RESERVES ITS WIDEST OPTION AND NEVER MOVES.
 *
 * Every option's SHORT name is drawn into this one grid cell and all but the
 * chosen one is hidden, so the browser sizes the button to the widest of them.
 * That is what stops the row re-flowing every time somebody picks something —
 * *"would be good if those dropdowns don't change size at all regardless of
 * what's selected"* — and it needs no measuring code, so it cannot go stale
 * when a pack changes what the options are.
 *
 * `visibility: hidden` rather than `display: none`, which is the whole trick:
 * a hidden item still takes part in sizing, a removed one does not.
 */
.pick-words { display: grid; flex: 1 1 auto; min-width: 0; }
.pick-words > span { grid-area: 1 / 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pick-ghost { visibility: hidden; }
.pick-word { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pick-caret {
  flex: 0 0 auto; width: 27px; align-self: stretch; margin: -9px -11px -9px 0;
  border-radius: 0 var(--r-field) var(--r-field) 0;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%2312000c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center / 11px 8px,
    linear-gradient(120deg, var(--hot), var(--hot-2));
}
.pick-face:disabled .pick-caret { filter: grayscale(1); }
/*
 * AND THE MENU IS AS WIDE AS IT NEEDS TO BE, which is the whole point. It
 * floats, so it costs the bar no height and no width until it is opened.
 */
.pick-menu {
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 60;
  min-width: 100%; width: max-content; max-width: min(360px, 80vw);
  max-height: 320px; overflow-y: auto;
  padding: 4px;
  border-radius: var(--r-card);
  border: 1px solid var(--panel-line);
  background: var(--surf-1, var(--panel));
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.5);
}
/* Measured on open — the rightmost picker on the bar is a few pixels from the
   panel edge, and a menu wider than its button would hang off the console. */
.pick-menu.to-left { left: auto; right: 0; }
.pick-opt {
  display: block; width: 100%; text-align: left;
  padding: 9px 10px; border: 0; border-radius: var(--r-field);
  background: none; color: var(--ink);
  font-family: inherit; font-size: var(--fs-note); font-weight: 700;
  cursor: pointer; white-space: nowrap;
}
.pick-opt:hover:not(:disabled) { background: rgba(255, 255, 255, 0.09); }
/* The one you are on, in the account's own colour — the same "you are here"
   treatment the lit menu chip uses, and deliberately not the full gradient,
   which means "press this". */
.pick-opt.is-on { background: rgba(255, 46, 136, 0.16); background: color-mix(in srgb, var(--hot) 18%, transparent); }
.pick-opt:disabled { opacity: 0.45; cursor: default; }

/* Two digits. It was a fifth of the bar. */
.lb-set-night .seconds-pick { width: 72px; }

.lb-set > * { min-width: 0; }
.lb-set .pack-shape { margin: 0; min-width: 0; }
.lb-set select, .lb-set input { min-width: 0; max-width: 100%; }
/* "While they wait" carries the longest option text in the app — each game's
   name AND its one-line blurb, so the open list still explains itself. A
   native select cannot ellipsis its own CLOSED text no matter what CSS says,
   so an equal-width grid cell hard-clipped it mid-word ("Maze Mouth — a maze
   cha"). Two tracks instead of one gives it roughly double the room; on a
   single-column phone layout this is a no-op, since there is only one track
   to span. */
.lb-set .pack-shape-wide { grid-column: span 2; }
@media (max-width: 560px) { .lb-set .pack-shape-wide { grid-column: span 1; } }

/* THE PACK-SPECIFIC ROW, directly under the tiles — a FLEX row rather than
   the night row's grid, because it holds one label and at most two controls
   and an auto-fit grid would stretch them to full width each. It reads as a
   caption on the tiles above it, which is what it is. */
.lb-set-pack {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  /* A REAL GAP ABOVE. Measured at zero: this row sat flush against the venue
     info line that ends the tiles, and two same-sized lines of dim text
     touching read as one wrapped paragraph rather than as two facts about
     different things. */
  margin: 12px 0 10px;
}
/* THEY GROW INTO THE ROOM THE ROW HAS. Fixed at 260px they clipped their own
   option text exactly as they did inside the tile ("63% of cal", "then a full
   hou") — which was the whole reason for moving them out here, so leaving
   them too narrow would have moved the fault rather than fixed it. Capped, or
   two controls stretch across a 1200px console and stop reading as a pair. */
.lb-set-pack .pack-shape { flex: 1 1 260px; max-width: 440px; }
/* Which pack it is setting. Never a heading — with six tiles above it this is
   a caption, and a bold one would compete with the tile names it points at.
   700 rather than 800 for the same reason it needs the gap above: at 800 it
   was the heaviest thing in a stack of dim lines and pulled the eye off the
   controls it is captioning. */
.lb-set-for { color: var(--ink-dim); font-weight: 700; }
/* Big, because it is pressed in a dark room by somebody who is late.
 * The gradient is written out here rather than inherited: the app's `.go`
 * fill is scoped to `.pack-card .go`, so a Launch button anywhere else came
 * out grey — which on the loudest control on the page reads as disabled. */
.lb-go {
  /* IT NEEDS ITS OWN GAP NOW. This margin did not exist because the button
     used to be built inside `.lb-chosen`, which carried the spacing — pulling
     it out into the bar so it could stay put left it sitting hard against the
     bottom of the tile row. A little more than the gap BETWEEN the tiles, so
     the row reads as one object and the button as the thing under it rather
     than as a fourth item in the grid. */
  margin-top: 14px;
  width: 100%; padding: 18px; font-size: var(--fs-head); font-weight: 900;
  border: none; border-radius: var(--r-card); cursor: pointer;
  font-family: inherit; letter-spacing: 0.01em; color: #12000c;
  /* THE GRADIENT DRIFTS, VERY SLOWLY — asked for as "like very slow water".
     Three stops rather than two and a background twice the button's width, so
     what moves is the gradient THROUGH the button rather than the button
     itself: nothing reflows, nothing shifts, and the type stays exactly where
     it was. The middle stop is what makes it read as movement at all — with
     two stops sliding a symmetric ramp back and forth looks like a brightness
     wobble.
     Eighteen seconds, which is slow enough that you notice it only if you
     look. This is the one control on the section and it has to invite a press
     without becoming the thing that distracts a quizmaster mid-setup. */
  background: linear-gradient(120deg, var(--hot), var(--hot-2), var(--hot));
  background-size: 200% 200%;
  animation: lbDrift 18s ease-in-out infinite;
}
@keyframes lbDrift {
  0%   { background-position: 0% 50%; }
  50%  { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}
/* SOMEBODY WHO HAS ASKED FOR LESS MOTION GETS NONE. A slow drift is decoration
   and this app runs in front of a room; the setting exists because for some
   people movement is not decoration at all. */
@media (prefers-reduced-motion: reduce) {
  .lb-go { animation: none; }
}
.lb-go:hover { filter: brightness(1.08); }
/* HOLLOW UNTIL THERE IS SOMETHING TO LAUNCH. Outlined rather than a greyed
   slab: the button keeps its place in the bar so nothing below it jumps, and
   an outline reads as "not yet" where a dimmed fill reads as broken. The drift
   stops too — a gradient sliding about on a control that cannot be pressed is
   the app inviting a press it will refuse. */
.lb-go:disabled {
  cursor: default;
  background: none;
  animation: none;
  border: 2px dashed var(--panel-line);
  color: var(--ink-dim);
  filter: none;
}

/* TONIGHT'S HEADING, and where it thinks tonight is.
   The venue is at the TOP of the section rather than buried on a button,
   because it decides the prizes, the voucher and what the night is filed
   under — an unseen guess is how the wrong pub's prize ends up on the
   winner's phone. It is stated rather than shouted: it is what the press will
   do, not a second thing to weigh up, and the picker for it is one tap away
   under Set it up. */
/* A GRID, THREE CELLS: the heading, what tonight is, and the way in and out.
   Flex wrapped on a phone — the venue pushed the fold button onto a second
   line when the section was open and onto the first when it was shut, so the
   heading sat 6px lower open than shut. A grid's first row is sized by the
   heading and the button, which are in both states, so "Tonight" is at the
   same pixel whatever is going on underneath it. */
/*
 * THE NIGHT'S OWN ROW — where it is, whether to keep it, what kind of night,
 * and whether the panel is open. FOUR cells now that Save has moved up here
 * (see the markup): the `1fr` in the middle is what the button sits in, which
 * is the strip of dead space this row always had and the reason the bar read
 * as unfinished at the top and crowded lower down.
 *
 * It WRAPS, because the venue name is somebody else's and can be long — "The
 * Station Tap, Wokingham" and a Save button and two pills do not share 1280px
 * comfortably, and a row that overflows is worse than one that takes two
 * lines.
 */
/*
 * THE NIGHT'S OWN ROW — and it now carries everything that is true of the
 * evening rather than of a pack: where it is, whether to keep it, what is on
 * the projector right now, what the phones get before it starts, whether it
 * is in a room or online, and whether the panel is open.
 *
 * FLEX, not grid. It was three explicitly-placed grid cells; at five items of
 * wildly different widths — a pub name, a five-word button, a sentence, a
 * chip and two pills — every breakpoint needed its own placement, and the
 * last one to be added auto-placed into an empty cell and came out in the
 * wrong order. A wrapping flex row puts them in DOM order at every width and
 * needs no arithmetic: the pills are pushed to the end by a margin, and
 * everything else falls where it fits.
 */
.lb-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
}
/*
 * ONE HEIGHT FOR EVERY CONTROL IN THIS ROW — *"can we also standardise the
 * buttons to look the same."* Measured before the change, the head held four
 * different heights: 36px for the mode switch, 38px for Save and Unlaunch,
 * 44px for the fold, and a chip of its own size again. Six controls, four
 * heights, no two edges lining up.
 *
 * **44px because that is the floor, not because it is the biggest.** A
 * touch-target audit already forced the fold up to it and left everything
 * beside it underneath; levelling DOWN would have taken the one control that
 * passed and broken it. So the row now clears the floor everywhere, and the
 * tops and bottoms line up as a side effect of getting that right.
 *
 * **THE SHAPES STILL DIFFER, and deliberately.** The radius rule is a field,
 * a card or a pill, and it encodes what a control IS: Save and Unlaunch and
 * Doors are actions and stay fields, the venue is a dropdown, the mode switch
 * and the fold are pills. Standardising the SIZE is what was asked for;
 * flattening the roles would undo the system the GUI rules are built on.
 */
.lb-head .minor,
.lb-head .gap-dial,
.lb-head .lb-where,
.lb-head .lb-fold,
.lb-head .hat-half {
  min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
}
.lb-head > * { min-width: 0; }
/* What tonight is: the venue when it is open, the pack as well when it is
   shut. One cell, so the row always has exactly three items in it. */
/* `overflow: hidden` is GONE — it clipped the venue sheet, which now hangs
   off this cell. The long-name job it was doing is already done by
   `.lb-where`'s own ellipsis, which is where it belongs. */
.lb-what { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
/* PLACED EXPLICITLY, all three. Left to auto-placement, moving the middle cell
   to a second line on a phone let the fold button fall into column 2 — the
   third column collapsed to nothing and the button sat 10px shy of the right
   edge when open and flush when shut. Pinned, row one cannot move whatever
   happens underneath it. */
/*
 * THE VENUE LEADS THE ROW — it is the identity of the night and reads first.
 * Everything after it is in DOM order, which is also reading order.
 */
.lb-head > .lb-what { flex: 0 1 auto; }
.lb-head > .lb-live-row { flex: 1 1 auto; margin: 0; min-width: 0; }
/* THE TWO PILLS SHARE THE RIGHT-HAND END — what kind of night it is, and
   whether the panel is open. The mode switch was a row of its own under the
   head, which made a bar meant to be glanceable three rows deep; beside the
   fold it is one of two chips at the end of a line, and it survives the fold
   because it is no longer inside the part that folds. */
/* THE PILLS ARE PUSHED TO THE END, whatever is in front of them — one
   `margin-left: auto` instead of a placement rule per breakpoint. */
.lb-head > .lb-right {
  margin-left: auto;
  display: flex; align-items: center; gap: 8px;
}
/*
 * THE HEAD MAY NOT WRAP ABOVE 1150px, AND THIS IS THE SETTINGS ROW'S OWN RULE
 * ARRIVING ONE ROW HIGHER.
 *
 * Adding "Open the controls" beside the live line moved the wrap threshold
 * from between 1100 and 1000 up to between 1200 and 1150 — measured against
 * the commit before it, at seven widths — so the mode switch and the fold
 * dropped onto a second row at 1150, where they used to hold. Two rows read
 * as a second bar, which is the fault this bar already has a rule about.
 *
 * `flex-wrap: nowrap` is the fix rather than `flex-shrink`, and that
 * distinction is written down one row below: **a wrapping flex row WRAPS
 * FIRST and shrinks per line afterwards**, so a row twenty pixels over drops
 * a control onto a second line instead of taking two pixels off each of five.
 *
 * WHAT GIVES GROUND IS THE SENTENCE, NEVER A CONTROL — `.lb-live` already
 * ellipsises, so a long pack title shortens and every button keeps its size.
 * Below 1150 the row still wraps, exactly as it did.
 */
@media (min-width: 1150px) {
  .lb-head { flex-wrap: nowrap; }
}
.lb-live-row > .lb-live { flex: 0 1 auto; }
.lb-live-row > .minor { flex: 0 0 auto; }
.lb-head .lb-mode { margin: 0; }
/* THE VENUE PICKER LOOKS LIKE EVERY OTHER DROPDOWN ON THE BAR — reported
   directly: *"there's two places to select venue, and neither of them conform
   to the drop down aesthetic."* Both halves of that were true. The second
   selector is gone (the venue in the info line under the tiles is plain text
   now), and this one — the single remaining way in — takes the app's own
   dropdown treatment: same height, fill, border and radius as a `<select>`,
   with the gradient chevron block that means "this opens".

   It was deliberately understated before, on the reasoning that a box here
   would be a third control competing with the pack and Launch. That was the
   wrong trade: it made the ONE control that decides the prizes, the voucher
   and what the night is filed under look like a caption, so it read as
   unclickable and a second way in had to exist beside it. A 27px gradient tab
   does not compete with a full-width filled button — the same size argument
   the dropdown rule itself already records. */
.lb-where {
  appearance: none; -webkit-appearance: none;
  padding: 8px 34px 8px 11px;
  border-radius: var(--r-field);
  border: 1px solid var(--panel-line);
  background-color: rgba(255, 255, 255, 0.07);
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%2312000c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
    linear-gradient(120deg, var(--hot), var(--hot-2));
  background-repeat: no-repeat, no-repeat;
  background-position: right 8px center, right center;
  background-size: 11px 8px, 27px 100%;
  cursor: pointer;
  font-family: inherit; font-size: var(--fs-note); font-weight: 800;
  color: var(--ink);
  text-align: left; max-width: 100%; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  position: relative;
}
.lb-where:hover { background-color: rgba(255, 255, 255, 0.11); outline: none; }
/* Focus gets its own signal rather than the hover one — see the note on
   `.lb-hit`. An OUTLINE was tried first and is wrong here specifically: this
   element's own `overflow: hidden` (needed for the ellipsis truncation above)
   clips an outset outline down to a sliver, found by tabbing to it in a real
   browser rather than trusting the computed style. An INSET box-shadow draws
   inside the box it is already allowed to occupy, so nothing truncating the
   text can also truncate the ring. */
.lb-where:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--hot); }

/* The picker itself. A list you search rather than a dropdown you scroll:
   fifteen residencies in a native select, in a dark pub, is the thing this
   replaces. */
/*
 * THE VENUE SHEET FLOATS TOO — *"the venue dropdown box and all dropdown boxes
 * on the bay must popover."* It was an ordinary block with a top margin, so
 * opening it pushed the settings, the packs and Launch down the page: the one
 * control most likely to be touched in a hurry moved everything under it.
 *
 * Anchored to `.lb-what`, which is the picker's own cell, and given the same
 * surface and shadow as the option menus so the two read as one kind of
 * object. `min-width` rather than `width`, because a pub name is as long as
 * the pub is.
 */
.lb-venues {
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 60;
  display: grid; gap: 8px; padding: 10px;
  min-width: 280px; width: max-content; max-width: min(420px, 88vw);
  max-height: 340px; overflow-y: auto;
  border: 1px solid var(--panel-line); border-radius: var(--r-card);
  background: var(--surf-1, var(--panel));
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.5);
}
/* The anchor. `.lb-what` holds the picker AND the sheet now, so the sheet
   hangs off the cell it belongs to rather than off the whole head — which
   would have put it under the fold button at the far right. */
.lb-what { position: relative; }
.lb-venue-list { display: grid; gap: 2px; max-height: 220px; overflow-y: auto; }
.lb-venue-hit {
  text-align: left; padding: 10px 12px; font: inherit; font-weight: 700;
  color: inherit; background: none; border: none; border-radius: var(--r-field); cursor: pointer;
}
.lb-venue-hit:hover { background: rgba(255, 255, 255, 0.09); outline: none; }
/* Focus gets its own signal rather than the hover one — see the note on `.lb-hit`. */
.lb-venue-hit:focus-visible { background: rgba(255, 255, 255, 0.09); outline: 2px solid var(--hot); outline-offset: -2px; }
.lb-venue-foot { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.lb-venue-add { text-decoration: none; }
/* IN THE ROOM, OR ONLINE. The app's own On/Off shape, so it is recognised
   rather than read — with ONE deliberate difference, and it is the same
   argument that keeps Delete outlined rather than filled.

   Only the ONLINE half wears the gradient. "In the room" is the state of very
   nearly every night, and a permanently filled gradient block sitting directly
   above Launch would be a second "press this" on the one section that is
   allowed exactly one — which costs the real button its meaning. So the
   ordinary night shows no extra fill at all, and the colour arrives precisely
   when the setting is the one that can put a question on sixty phones in a
   pub. Off is silent; on is unmissable. */
.lb-mode { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 10px; }
/* SAME HEIGHT AS .lb-fold, which sits right beside this in the head row —
   reported directly, and it was a real mismatch: `.hat-switch` everywhere
   else is a small inline pill with no height floor of its own, so beside a
   44px touch-target button (`.lb-fold`'s own floor, set after a measured
   audit) it read as visibly shorter. Scoped to this one instance rather than
   `.hat-switch` generally — the switch is reused in tighter rows elsewhere
   (My account's feature toggles) where a 44px floor would be the wrong call. */
.lb-mode-switch { min-height: 44px; align-items: stretch; }
/* EQUAL HALVES, so the pill does not change shape as you switch it — "Online"
   is one character longer than "Venue" and was 2.4px wider for it. Nobody
   would name that, but a control whose two states are not quite the same
   object is a control that reads as slightly wrong without saying why. */
.lb-mode-switch .hat-half {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 1 1 0; min-width: 0;
}
/* THE SAME LIT TREATMENT WHICHEVER HALF IS ON — reported directly: *"the
   button need to be the same for both regardless of which mode you're in."*

   It was a flat grey for Venue and the full account gradient for Online,
   on the reasoning that only the dangerous half should shout. Measured, that
   made the control a different object in each state — grey pill on an
   ordinary pub night, gradient pill on an online one — and the thing it is
   saying ("this is the mode you are in") is the same either way.

   **NOT the gradient for both**, which would be the obvious way to make them
   match and would break the rule this bar exists to keep: ONE gradient on the
   console, and it is Launch. So both take the lit-chip treatment instead — a
   tint of the account colour with its own outline, exactly what the menu
   already uses for "you are here", for the same reason recorded there. The
   rgba is declared before the color-mix as a fallback, or a projector too old
   for it loses the fill entirely. */
.lb-mode-switch .hat-half.live,
.lb-mode-switch .hat-half[data-online="1"].live {
  background: rgba(255, 46, 136, 0.22);
  background: color-mix(in srgb, var(--hot) 26%, transparent);
  color: var(--ink);
  box-shadow: inset 0 0 0 1px rgba(255, 46, 136, 0.55);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--hot) 60%, transparent);
}
.lb-mode-said { color: var(--ink-dim); min-width: 0; }
.lb-prize { margin-top: 8px; }
/* What is on the projector right now. Gold when it is NOT what the box says,
   because that is the moment somebody is about to be surprised — the same
   treatment "not invoiced" and a venue clash get, for the same reason: not a
   fault, but a thing you want to have noticed. */
/*
 * UNLAUNCH SITS AGAINST THE SENTENCE IT UNDOES.
 *
 * `space-between` put the button at the far right of a 1900px bar, an inch of
 * empty space from the line naming what it would take down — reported off a
 * screenshot, and at that distance it reads as a control over the whole
 * panel rather than over one game. `flex-start` with a small gap makes the
 * two one phrase: "On the big screen now: The 2000s Metal Quiz [Unlaunch]".
 */
/*
 * UNLAUNCH SITS AGAINST THE SENTENCE IT UNDOES, and the pair is now IN the
 * head rather than on a line of its own underneath — a whole row for one
 * short sentence and one small button was the space this bar could least
 * afford. `min-width: 0` plus the ellipsis below is what lets a long quiz
 * title give way instead of pushing the pills off the end.
 */
.lb-live-row { display: flex; align-items: center; gap: 10px; min-width: 0; }
.lb-live-row .lb-live { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-live { color: var(--ink-dim); min-width: 0; }
/* The unlaunch button sits on the same line as the text it belongs to, never
   its own row — it is one small action about one small fact, not a second
   thing to notice on the bar. */
/*
 * NO SIZE OF ITS OWN — asked for directly: *"can we also standardise the
 * buttons to look the same."* This carried `padding: 2px 10px` and
 * `font-size: 0.8em`, which made it visibly smaller than every other button
 * on the bar and gave the row a third text size nobody chose. `.minor.danger`
 * already says everything it needs to: outlined, red, the same face and the
 * same height as Save two lines above it.
 */
.lb-unlaunch { flex: none; }
/* RED, not gold, and at the host's own instruction: "these should never be
   allowed to drift, but if they do it needs to be a red warning". The rest of
   this app keeps red for wrong and destructive, and that is exactly what this
   is — the panel and the projector disagreeing about what the room is playing
   is a fault, not a note. */
.lb-live.lb-warn { color: var(--bad, #ff5470); font-weight: 800; }
/* A DOUBLE BOOKING, named. Gold rather than red: it is not a fault and nothing
   is broken — it is a decision only the human can make, and it is the same
   treatment "not invoiced" gets for the same reason. */
.lb-where.lb-warn { color: var(--gold); font-weight: 800; }

/* COLLAPSED, IT IS A THIN LINE — and the line still says what it is set to,
   because a shut panel saying only "Tonight" makes you open it to find out
   what it would do, which is the tap this exists to save. Shut, the whole row
   is the target: one small chevron on the end of a bar is a thing you miss
   with a thumb in a dark pub. */
/* THE HEADING MUST NOT MOVE WHEN IT FOLDS. Same padding and the same
   alignment in both states, so "Tonight" is at the same pixel open and shut —
   a heading that jumps as you collapse a panel makes the whole page feel like
   it re-laid itself, and it is the thing your eye is using to find the
   section again. Only what is BELOW the head appears and disappears. */
.launchbar.shut { cursor: pointer; }
/* ONE ROW, and it has to stay one row — "collapsed" that takes three lines on
   a phone is not collapsed, it is smaller. So it does not wrap, the middle
   truncates, and the way back in keeps its full width. */
/* Both of these were flex-era leftovers: the head is a grid now, its children
   already carry `min-width: 0`, and the blanket child rule here was beating
   the fold button's fixed width — which is why the button was 96px wide open
   and 78px shut, and shuffled sideways as you pressed it. */
.launchbar.shut .lb-where,
.lb-shut-what { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-shut-what { font-weight: 800; color: var(--ink); }
/* On a phone there is room for the heading, ONE fact and the way back in. The
   pack is the more useful of the two — it is what the button would launch —
   and the venue is one tap away inside. */
@media (max-width: 560px) {
  /* Shut, there is room for the heading, ONE fact and the way back in — and
     the pack is the more useful of the two, because it is what the button
     would launch. Open, the venue takes a line of its own UNDER the head
     rather than squeezing into it: a truncated pub name is the one thing on
     this bar you cannot check at a glance. Row one is unaffected either way,
     which is what keeps the heading still. */
  .launchbar.shut .lb-where { display: none; }
  /* The venue takes a line of its own: a truncated pub name is the one thing
     on this bar you cannot check at a glance. Flex order does the rest. */
  .launchbar:not(.shut) .lb-what { flex: 1 0 100%; }
}
.lb-tucked { display: none !important; }
.lb-fold {
  margin-left: auto; flex: 0 0 auto;
  text-align: center;
  /* MIN-HEIGHT 44px — a measured touch-target audit found this at 29px tall,
     under the floor. Padding alone was font-size-dependent and fragile;
     `min-height` plus flex centring holds the floor regardless. */
  min-height: 44px; display: inline-flex; align-items: center; justify-content: center;
  padding: 6px 12px; border-radius: var(--r-pill);
  background: none; border: 1px solid var(--panel-line); color: var(--ink-dim);
  /* TWO RUNGS IN THIS ROW, NOT THREE. It was `--fs-mini` (12) beside a
     `--fs-note` (13) venue line and an `--fs-tag` (11) heading — three sizes
     inside two pixels of each other, which is not a hierarchy, it is the same
     decision taken three times. Exactly the fault CLAUDE.md already records
     about the two On/Off switches that came out 11 and 11.5.
     The heading is the LABEL and keeps `--fs-tag`; the venue and this button
     are both CONTENT and are both `--fs-note`, so the one size difference in
     the row means something. */
  font: inherit; font-size: var(--fs-note); font-weight: 800; cursor: pointer;
}
/* A FIXED WIDTH, and it has to out-specify `.lb-head > * { min-width: 0 }`
   two rules above — which is why it is written as a descendant. "Hide" and
   "Show" are not the same number of pixels, so without this the button
   shuffles sideways every time it is pressed: the same complaint as the
   heading moving, eighteen pixels smaller. */
.lb-head .lb-fold { min-width: 96px; }
.lb-fold:hover { border-color: var(--hot); color: var(--ink); }
.lb-fold::after {
  content: '';
  display: inline-block; margin-left: 7px; vertical-align: 1px;
  border: 5px solid transparent; border-top-color: currentColor; border-bottom: 0;
}
/* THE ARROW POINTS THE WAY THE PANEL IS ABOUT TO GO, and this one was the
   wrong way round — reported directly.
   The other two arrows in this bar are pickers: shut they point DOWN because
   the list opens downwards, and they flip up to say "press again to close".
   This is not a picker, it is a FOLD, and the panel folds UPWARDS out of the
   way. So open beside "Hide" it points up, and shut beside "Show" it points
   down at the thing it will bring back. Written as the true state rather than
   the false one, so the rule reads as the behaviour. */
.lb-fold[aria-expanded="true"]::after { transform: rotate(180deg); }

/* ---- TONIGHT, AS A ROW OF SQUARES
   The packs being played and the three facts a night is filed under, all the
   same shape. Twelve round-chips is a list you read; three squares is a night
   you see, which is what somebody wants five minutes before a gig.
   `auto-FIT` with a floor rather than a fixed count, so the row is six across
   a laptop and two across a phone without a media query per width.
   **`auto-fit` and not `auto-fill`, and the difference is the whole reason
   this looked wrong.** `auto-fill` makes as many tracks as would FIT — about
   nine at this width — and leaves the ones with nothing in them standing
   empty, so six tiles sat packed to the left with a hand's width of dead
   space to the right of them and the full-width Launch button underneath
   ending somewhere they did not. `auto-fit` collapses the empty tracks and
   the tiles share the whole row, so the block lines up with the button below
   it and reads as one object. */
.lb-order { margin-top: 10px; border-radius: var(--r-card); }
.lb-order.drop-here { outline: 2px dashed var(--hot); outline-offset: 4px; }
/* CAPPED AT 200px, and the cap is the whole point of this line.
   `auto-fit` with `minmax(118px, 1fr)` stretches its tracks to fill the row,
   which is right at six tiles and absurd at four: a bingo night draws ONE
   pack slot and three info squares, so each one grew to nearly 290px and the
   bar became six hundred pixels of enormous boxes. An upper bound keeps a
   tile a tile whatever else is on the row.
   The cost is dead space to the right when there are only a few, and that is
   the correct trade — it is exactly what the pack grid below already does on
   its last row, and a consistent tile size across states is worth more than a
   row that always reaches the edge. */
/*
 * THREE ACROSS, ON EVERY WIDTH — and `auto-fit` could never deliver it.
 *
 * The slots are a FIXED SET of three, numbered 1, 2, 3. `auto-fit` is for a
 * flowing list whose length nobody knows, and using it here cost two goes to
 * work out:
 *
 * **`auto-fit` counts columns off the MAX of `minmax()`, not the min**, when
 * that max is a definite length. So `minmax(118px, 200px)` sized for 200px
 * tracks, only one of which fits a phone — and lowering the min to 92px, which
 * looks like exactly the fix, changed nothing whatsoever. Measured twice:
 * three stacked rows both times, 493px then 442px, when the target was one row.
 *
 * `repeat(3, …)` says what is actually true. `minmax(0, 200px)` keeps the
 * 200px ceiling on a laptop and lets them shrink to a third of a phone, and
 * the `0` floor is the min-content trap this file has now hit nine times.
 *
 * They also must not stack: HTML5 drag events never fire on touch (CLAUDE.md),
 * so on a phone these are not drop targets at all — the taps and the arrows
 * are the touch path. Three short slots saying what the night is are right
 * there; three tall ones nobody can drag onto are not.
 */
/* SIX SLOTS, 160px each — see PACK_SLOTS in console.js for why it went from
   three. `auto-fit` rather than a fixed six, so the row WRAPS on a narrow
   window instead of squeezing six tiles to nothing: at 160 the ticks stay on
   one line, and that is the whole reason the width came down from 200. */
/* SIX COLUMNS THAT FILL THE WIDTH — no gap on the right.
   `auto-fit` at a fixed 160px left whatever the container did not divide by
   168 as dead space down the right-hand edge, which on a 1148px console is
   about 140px of nothing beside the last slot. Six equal fractions instead:
   the row is always flush, and the tile is ~180px rather than exactly 160,
   which is the same direction the width came down for. THE LIBRARY USES THE
   SAME SIX so a pack is still the shape of the hole it goes in. */
/*
 * THE DOORS SLOT IS HALF A COLUMN AT THE HEAD OF THE ROW, and the six pack
 * slots keep their share of what is left. `auto` on the first column plus six
 * equal ones, rather than seven equal ones — a night's packs must not get
 * narrower because the doors are drawn beside them.
 */
.lb-tiles { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 8px; }
.lb-tiles:has(.lb-doors-slot) { grid-template-columns: auto repeat(6, minmax(0, 1fr)); }
/*
 * NARROW, AND DELIBERATELY NOT A DROP TARGET. No dashed border, no number, no
 * plus — it must never read as a slot with a pack missing out of it. What it
 * holds is one dial and the word that says which gap it is, which is exactly
 * what it had in the head before it moved down here to sit where the gap
 * actually happens.
 */
.lb-doors-slot {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px; width: 64px; padding: 8px 5px;
  border: 1px solid var(--panel-line); border-radius: var(--r-card);
  background: rgba(255, 255, 255, 0.03);
}
.lb-doors-slot .gap-dial { position: static; }

/*
 * THE BREAK STRIP — one chip per gap in tonight, under the tiles it belongs
 * to. See `console-breaks.js` for why it is a row underneath rather than
 * markers between the tiles.
 *
 * It WRAPS rather than scrolls: a six-part night is ten or eleven chips, and
 * a strip that scrolled sideways would hide the end of the evening behind a
 * gesture nobody knows is there. The console's tab bar was rescued from
 * exactly that.
 */
/*
 * ABOVE THE TILES, NOT BELOW — *"that space between packs and launch button
 * needs to be clear, space is at a premium."* The band between the running
 * order and Launch is now empty by rule: the strip sits on top of the order
 * it describes, the prize warning moved into the head, and the pack settings
 * row only exists when it actually holds a control.
 *
 * Directly above works as well as directly below did, because what the chips
 * describe is the gaps BETWEEN the tiles either way — and it puts the one
 * filled gradient on the panel back where nothing competes with it.
 */
/*
 * THE GAP DIAL — one 44px symbol in a pack tile's bottom-right corner, and
 * the size is the whole design. Measured on the real thing before a line of
 * it was written: a tile is 179 x 76, its round ticks are 22px along the
 * bottom-left, and a four-round pack leaves 58px clear in that corner. One
 * touch-floor control fits; two never would.
 *
 * ABSOLUTE, so a pack with four ticks and a pack with one put it in the same
 * place. A control you drive with a thumb in a dark pub has to be findable
 * without reading the tile it is on first — the same reason Launch is the
 * only filled gradient on the panel.
 */
.gap-dial {
  position: absolute;
  right: 6px;
  bottom: 6px;
  z-index: 2;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-field);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.05));
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-bottom: 2px solid transparent;
  color: var(--ink);
  font: inherit;
  cursor: pointer;
}
.gap-dial:hover { border-color: var(--hot); }
/* Only a gap that was CHANGED wears the account colour, exactly as the chips
   it replaces did and for the same reason: every tile carrying it is a wall
   of colour that says nothing about which one you touched. */
.gap-dial.is-set { border-bottom-color: var(--hot); }
.gap-dial-icon { font-size: 17px; line-height: 1; letter-spacing: -1px; }
/*
 * THE TICKS STOP BEFORE THEY REACH IT, so a tick can never end up underneath a
 * control sitting on top of it — the pack tile's own `z-index` lesson, where a
 * bigger × was silently painted over by the title and half the button did
 * nothing.
 *
 * **40px, measured rather than picked.** The dial is 44 wide and sits 6 from
 * the tile's right edge, and `.lb-rounds` is already inset 10 by the tile's
 * padding — so 40 is the exact clearance. At 46 a four-round pack on a 166px
 * tile had 98px for 100px of ticks and wrapped its fourth onto a second row,
 * which made that one tile taller than the rest of the running order.
 */
.lb-tile .lb-rounds { padding-right: 40px; }
/* The word beside the doors dial in the head. The dial alone would be a
   symbol with nothing saying which gap it belongs to — every other one is
   sitting on the pack it acts on, and this one is not. */
.lb-doors-word { color: var(--ink-dim); font-weight: 800; white-space: nowrap; }

/*
 * THREE ACROSS ON A TABLET, AND BOTH GRIDS MOVE TOGETHER.
 *
 * Six across needs 6 x 118 + 5 x 12 = 768px INSIDE the panel, and a shut pack
 * card has `aspect-ratio: 1 / 1` with `min-height: 118px` — which propagates
 * to a minimum WIDTH, because a square cannot be 118 tall and 80 wide. So
 * between 561 and ~780px the six tracks are narrower than the cards standing
 * in them: **measured at 561, the cards overlap by 28px and the console
 * scrolls sideways by 24.** An iPad in portrait is 768.
 *
 * **This is not the six-across decision being dropped.** That rule is about a
 * shelf wide enough to honour it, and here six is not honourable — the cards
 * physically overlap. The pair moves TOGETHER, exactly as it already does at
 * 560, because the shelf mirrors the bays and a mirror that only half-changes
 * is worse than either.
 *
 * `console-frame.mjs` never looked between 561 and 900, which is why nothing
 * saw it: its phone size is 390 and its next one up is 960.
 */
@media (min-width: 561px) and (max-width: 779px) {
  .lb-tiles, .pack-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  /* Told twice, for the specificity reason spelled out in the phone band
     below: `:has()` is a more specific selector than the bare class. */
  .lb-tiles:has(.lb-doors-slot) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 560px) {
  /* Shorter still on a phone, where they are a summary rather than a target. */
  /* TWO ACROSS ON A PHONE, not one. `auto-fit` at 160px only fits a single
     column in a 358px viewport, which turned six slots into six rows — about
     400px of mostly-empty bar, on the panel that sits at the top of EVERY tab.
     Six was fine at three slots and is not at six. Two columns puts it back to
     three rows, and a half-width tile is still a target you cannot miss. */
  .lb-tiles, .pack-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /*
   * AND THE DOORS SLOT STOPS BEING A COLUMN, which it has to be told twice
   * because `.lb-tiles:has(.lb-doors-slot)` is a class MORE specific than
   * `.lb-tiles` — so the seven-column rule above won inside this media query
   * and beat the phone layout outright. Measured at 390: two packs and the
   * doors came out as four ~50px columns, the round ticks stacked one per row,
   * and the gap dial was wider than the tile holding it.
   *
   * The specificity trap this repo keeps meeting, wearing `:has()` this time
   * instead of a `border` shorthand. Nothing throws; the phone just gets the
   * laptop's grid.
   *
   * A full-width strip is also the better shape here: the doors are the first
   * thing in the evening, and on a narrow screen a row of its own says that
   * more clearly than a squeezed column ever could.
   */
  .lb-tiles:has(.lb-doors-slot) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .lb-doors-slot {
    grid-column: 1 / -1;
    width: auto; flex-direction: row; justify-content: flex-start; gap: 12px;
    min-height: 0;
  }
  .lb-tiles .lb-tile { min-height: 58px; padding: 8px; }
  /*
   * A TILE HOLDING A PACK KEEPS ITS FULL HEIGHT ON A PHONE, because it is
   * holding controls: measured at 390, a 58px tile put the × over the gap dial
   * by 28 x 26px — the same collision the desktop tile grew to 90 to solve,
   * and the phone rule was quietly undoing it.
   *
   * An EMPTY slot stays short. It has nothing on it but a number and a plus,
   * so the two are genuinely different objects and giving them one height
   * would spend a third of a phone screen on slots nobody has filled.
   */
  .lb-tiles .lb-tile.is-pack { min-height: 90px; }
  /* ONE EMPTY SLOT ON A PHONE, not up to six. The six-slot row is sized for a
     rare six-pack night on the laptop this console is built around — on a
     390px screen, before a single pack is added, it was three rows of empty
     "Add pack" tiles above the tab list a host actually needs. `.lb-drop` is
     only ever on an EMPTY slot (a chosen pack carries `.is-pack` instead), and
     they always sit after any filled tiles in the row — so hiding every
     `.lb-drop` that follows another one leaves exactly one "Add a pack"
     affordance, whatever the pack count, with the JS slot logic untouched. */
  .lb-tiles .lb-tile.lb-drop ~ .lb-tile.lb-drop { display: none; }
}
/* THE THREE FACTS GET THEIR OWN ROW. Three columns always, because there are
   always exactly three of them — where, when and what for — and an auto grid
   would wrap one of them under the others the moment the packs beside them
   changed count. Below 560px they go to two and then the third drops, which
   is the only place wrapping is honest: a 320px phone cannot hold three. */
/*
 * ONE LINE OF DESCRIPTION, not three cards.
 *
 * Where, when and what for are facts somebody READS. As squares they were the
 * same size as the pack slots above, which said they were the same kind of
 * thing — something you drag onto — and they took as much of the screen while
 * doing none of the work. Tonight is at the top of every tab, so its height is
 * charged to every page in the console.
 *
 * It WRAPS rather than scrolling or ellipsising: a venue name is as long as
 * the venue is, and on a phone this is allowed to become two lines. Two lines
 * of text is still a fraction of three squares.
 */
/*
 * ONE FACT, NOT FOUR. This line carried the venue name, whether tonight was a
 * one-off, what time it started and the prizes — and three of those were
 * settings edited elsewhere that changed nothing about what launches. What is
 * left is the prizes, which are read at LAUNCH onto the winner's voucher and
 * therefore the one thing here worth checking before pressing the button.
 */
.lb-say {
  margin-top: 8px; min-width: 0;
  font-size: var(--fs-note); color: var(--ink-dim);
}
/* No prize set is not an error — plenty of nights have none — so it is said
   rather than warned about, and it names where to fix it rather than leaving
   somebody to find the shelf. */
.lb-say-none { color: var(--ink-dim); }
/* MONEY IS THE OTHER STATED EXCEPTION TO THE SHORT-LABEL RULE, so this one is
   allowed its two sentences — and it is full ink rather than red, because
   nothing has gone wrong yet and tonight still runs. Red here would say the
   night was in trouble, which is the shock the whole policy exists to avoid.
   See `lastNightWarning()` in `console-tonight.js`. */
.lb-say-money { color: var(--ink); }
.lb-say-money b { color: var(--gold); }

.lb-tile {
  position: relative; display: flex; flex-direction: column; justify-content: flex-end;
  gap: 2px; min-width: 0; padding: 10px;
  /*
   * SHORT, NOT SQUARE — and square was measured as the fault.
   *
   * `aspect-ratio: 1/1` sounds harmless until the grid gives a tile 200px of
   * width: it then takes 200px of HEIGHT, and two rows of them plus the launch
   * button pushed Tonight past 600px on a laptop. Tonight sits at the top of
   * EVERY tab, so every pixel it takes is a pixel off the tab somebody
   * actually came to use.
   *
   * A drop target has to be big enough to hit with a mouse dragging a card,
   * and that is all — which is a height, not a proportion.
   */
  /*
   * 90, AND THE 14px IS BOUGHT RATHER THAN SPENT. Reported as *"a slight
   * overlap with the cross and the game selector"* and measured at 28 x 12px:
   * the × is 30px at the top right and the gap dial is 44px at the bottom
   * right, which is 74px of controls plus their padding in a 76px tile. They
   * cannot both live on that edge at this height.
   *
   * Three ways out were on the table and this is the least surprising: moving
   * the × to the top LEFT puts "remove this" where the eye lands first, and
   * moving the dial off the bottom right undoes the thing that was asked for.
   * Nothing moves; the tile gets tall enough to hold what is on it. The row is
   * one line, and deleting the break strip gave 59px back an hour earlier.
   */
  min-height: 90px;
  border-radius: var(--r-card); text-align: left;
  font: inherit; color: var(--ink);
}
.lb-tile > * { min-width: 0; }
.lb-tile-name {
  font-size: var(--fs-note); line-height: 1.2;
  /* Two lines of a title and then an ellipsis: a pack name is the thing you
     are identifying the square by, and one line loses most of them. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  /* A GUTTER FOR THE ×, or a two-line title runs its own tail under it — found
     on a real pack name at 1280px, wrapping straight into `.lb-tile-off`'s
     footprint. Same fix as `.pack-title` clearing `.pack-pin`: reserve the
     space rather than trust the two never to collide. */
  padding-right: 32px;
}
/*
 * THE MIXED ROW'S OWN HEAD — the grip lives in the name's row, in normal
 * flow, rather than floated over the top corner: absolute positioning would
 * have to guess how much blank space a two-line title left, and a bingo
 * tile's own prize/shape controls already eat the rest of the tile. See
 * `console-tonight-mix-ui.js`'s `filledTile()` for the reasoning in full.
 */
/*
 * ROOM FOR THE × ON THE RIGHT. Growing that button to a real 30px target put
 * it under the pack name — `.lb-tile-head` is `z-index: 1` and comes later in
 * the DOM, so the title PAINTED OVER the button and swallowed clicks aimed at
 * its lower half. Caught by a click that reported "<b class=lb-tile-name>
 * intercepts pointer events", which is the only way this shows up: nothing
 * throws, the button is plainly visible, and about half of it silently does
 * nothing. Making a target bigger is not the same as making it hittable.
 */
.lb-tile-head { display: flex; align-items: flex-start; gap: 4px; position: relative; z-index: 1; padding-right: 30px; }
.lb-tile-head .drag-grip { width: 16px; height: 16px; margin-top: 1px; }
.lb-tile-head .lb-tile-name { flex: 1 1 auto; min-width: 0; }
.lb-tile-sub, .lb-tile-cap { color: var(--ink-dim); }
/* A TICK PER ROUND, and a tap turns it into a cross. It replaces the line
   that said "3 rounds" — the same information with nothing you can do about
   it, where these say which three and let you drop one.
   Wraps, because a pack can carry five and a tile is 123px wide on a 320px
   phone; small enough that five fit, big enough to hit with a thumb. */
.lb-rounds { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
/*
 * A ROUND IS A SQUARE WITH SOFT CORNERS, NOT A DOT — asked for on 24 August
 * 2026: *"can you change the green ticks on the packs to be square shaped with
 * round edges… and I need to see when mousing over them as well so it's easier
 * to click on/off and also drag the individual elements."*
 *
 * `--r-field`, the app's own smallest radius, so it matches every other boxed
 * control rather than inventing a fourth corner. A pill said "badge"; a
 * rounded square says "button", which is what it is.
 */
.lb-rd {
  display: inline-grid; place-items: center;
  /*
   * 28, NOT 22. Two reasons and they pull the same way: *"easier to click
   * on/off"*, and the app's own `--r-field` corner only READS as a rounded
   * square on a box big enough to have sides — at 22px a 10px radius is very
   * nearly a circle, which is the shape being moved away from.
   *
   * A four-round pack wraps to two rows at this size, and that is fine rather
   * than a compromise: the grid stretches every tile to the tallest, so no one
   * tile ends up out of line with the rest of the running order.
   */
  width: 28px; height: 28px; padding: 0;
  border-radius: var(--r-field); cursor: pointer;
  font: inherit; font-size: var(--fs-mini); font-weight: 900; line-height: 1;
  transition: transform 90ms ease, box-shadow 90ms ease;
}
/* Green for on and red for off, which is the one place in this app those two
   mean exactly what they mean everywhere else: this round is in, this one is
   out. Outlined rather than filled, or a pack with five rounds is five bright
   dots shouting from a tile whose job is to name a quiz. */
.lb-rd.on { background: rgba(47, 224, 122, 0.16); border: 1px solid rgba(47, 224, 122, 0.55); color: var(--good); }
.lb-rd.off { background: rgba(255, 77, 94, 0.12); border: 1px solid rgba(255, 77, 94, 0.5); color: var(--bad); }
/*
 * AND THE HOVER HAS TO BE SEEN. `filter: brightness(1.25)` on a 22px dot that
 * is already faint is a change you cannot find with a mouse — reported as
 * exactly that, and it is why the ticks read as decoration rather than as
 * controls you can press and drag.
 *
 * It lifts, takes a ring in the account's own colour, and casts a shadow, so
 * the one under the cursor is unmistakable in a row of five. `translateY` and
 * a shadow rather than a size change, because growing a control on hover moves
 * the ones beside it and makes a row of them squirm under the pointer.
 */
.lb-rd:hover, .lb-rd:focus-visible {
  transform: translateY(-2px);
  box-shadow: 0 0 0 2px var(--hot), 0 4px 10px rgba(0, 0, 0, 0.45);
  outline: none;
}
.lb-rd:active { transform: translateY(0); }
.lb-tile-cap { text-transform: none; letter-spacing: 0; }

/* A PACK IN THE NIGHT. Filled, in the account's own surface, so it reads as
   something that is there rather than something waiting. */
.lb-tile.is-pack {
  background: var(--panel); border: 1px solid var(--panel-line); cursor: grab;
}
.lb-tile.is-pack.is-lifting { opacity: 0.45; }
/* WHICH PACK THE SETTINGS ROW BELOW IS TALKING ABOUT.
   An outline in the account's own colour plus a lift, NOT a fill: the tile
   already carries its own era wash and its kind on the bottom edge, and a
   third colour across the face would fight both. This says "you are here",
   which is the same job the lit menu chip does — and it uses the same answer,
   a tint of --hot rather than gold, for the reason recorded there: gold is
   the trophy colour and means winning, nowhere else.
   An rgba fallback before the color-mix, or a projector too old for it loses
   the ring entirely. */
.lb-tile.is-pack.is-picked {
  border-color: var(--hot);
  box-shadow: 0 0 0 2px rgba(255, 46, 136, 0.35);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--hot) 45%, transparent);
}
/* What a bingo game in the row is SET to, read rather than changed — the two
   dropdowns that used to be here moved to the row under the tiles, where they
   have room for their own option text. */
.mix-bingo-said { color: var(--ink-dim); margin-top: 3px; position: relative; z-index: 1; }
/* EVERY ROUND SWITCHED OFF — an empty pack. It stays in the row rather than
   vanishing, because it is one tap from being back and making somebody find
   it on the shelf again would be the app punishing a change of mind. Dimmed
   and dashed, which is the same language the empty slots beside it use. */
.lb-tile.is-pack.is-spent { opacity: 0.5; border-style: dashed; }
.lb-tile.is-pack.is-spent .lb-tile-n { background: rgba(255, 255, 255, 0.10); color: var(--ink-dim); }
/*
 * THE SLOT NUMBER IS FOR AN EMPTY SLOT, NOT A FULL ONE.
 *
 * Reported off a screenshot: *"the top left numbers can go once there's a pack
 * in the slot"* — and measured, the badge was overlapping the pack's own title
 * by 18 x 8px, so on a long name it read as "22000s Pop, R&B and Chart".
 *
 * He is right beyond the collision, too. On an EMPTY slot the number is the
 * whole label — "Add pack 3" is what tells you where a card would land. On a
 * full one the order is already visible from where the tile IS, so the badge
 * was a number telling you something the position had already said, sitting on
 * top of the one thing on the tile you actually read.
 */
.lb-tile.is-pack .lb-tile-n { display: none; }
/* Which slot it is — the label on an EMPTY one. Top left, clear of everything
   because there is nothing else on it. */
.lb-tile-n {
  position: absolute; top: 8px; left: 8px;
  display: inline-grid; place-items: center; min-width: 20px; height: 20px;
  border-radius: var(--r-pill); background: rgba(255, 46, 136, 0.22);
  font-size: var(--fs-tag); font-weight: 900;
}
/*
 * BIG ENOUGH TO HIT — asked for directly: *"the packs need a larger clickable
 * x to get rid of them from the grid"*. It was a 4px-padded glyph in a corner,
 * about 18px square, on a control used in a dark pub with a thumb.
 *
 * 30px, which is the size a touch target wants to be, and it is a CIRCLE with
 * a face rather than a bare character: the old one was invisible until you
 * hovered it, so there was nothing to aim at in the first place. It stays
 * quiet — dim ink on a faint disc — and only turns red under the cursor,
 * because a filled red × on every tile is the "wall of red" this app's GUI
 * rules already turn down, and destructive is outlined and never loud.
 */
.lb-tile-off {
  /* ABOVE the title, which carries `z-index: 1` of its own — see the note on
     `.lb-tile-head`. Without this the top-right corner of the tile is a
     button you can see and cannot press. */
  position: absolute; top: 4px; right: 4px; z-index: 3;
  display: flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0;
  border: 1px solid transparent; border-radius: 999px;
  background: rgba(0, 0, 0, 0.28); color: var(--ink-dim);
  font-size: 17px; line-height: 1; cursor: pointer;
}
.lb-tile-off:hover, .lb-tile-off:focus-visible {
  color: var(--bad); border-color: var(--bad); background: rgba(255, 77, 94, 0.16);
}
/* Which side a dragged tile will land. Without this a row can only ever be
   reordered one way and the last position is unreachable. */
/* An empty slot's number is quiet: it says where the next pack would land,
   not that something is there. */
.lb-tile-n.is-empty { background: rgba(255, 255, 255, 0.10); color: var(--ink-dim); }
.lb-tile.drop-before { box-shadow: inset 3px 0 0 var(--hot); }
.lb-tile.drop-after { box-shadow: inset -3px 0 0 var(--hot); }
/* THE MIXED ROW'S OWN TARGET — a swap has no "before" or "after" to land on,
   so it gets one ring round the whole tile rather than the ordinary row's
   one-sided line. See `swapSlots()` in `console-tonight-mix.js`. */
/*
 * A PACK TILE THAT WILL TAKE THE ROUND YOU ARE HOLDING — asked for as *"a
 * slight highlight effect or something to make it known that it's active and
 * accepting the drag."*
 *
 * **An inset ring alone was not enough, and the reason is what it sits next
 * to.** The PICKED tile already wears a full outline in the same colour, so on
 * the tile you are most likely to be dragging within, "accepting" and "picked"
 * were the same picture. This adds a wash and a lift on top, so the answer is
 * legible whether or not the tile underneath happens to be the chosen one.
 *
 * It is deliberately not the account GRADIENT: that means "press this", and
 * the one thing wearing it on this panel is Launch.
 */
.lb-tile.is-pack.drop-here {
  box-shadow: inset 0 0 0 2px var(--hot), 0 6px 18px rgba(0, 0, 0, 0.5);
  background-image: linear-gradient(rgba(255, 46, 136, 0.16), rgba(255, 46, 136, 0.16));
  transform: translateY(-2px);
}

/* AN EMPTY SLOT. Dashed, because here the thing genuinely is waiting to be
   filled — which is the one place in this app a dashed edge is right. It is a
   real button as well as a drop target: HTML5 drag events are never delivered
   on touch, so a drag-only bar would be a dead panel on a phone. */
.lb-tile.lb-drop {
  align-items: center; justify-content: center; text-align: center;
  border: 2px dashed var(--panel-line); background: none; color: var(--ink-dim);
  font-weight: 800; cursor: pointer;
}
.lb-tile.lb-drop:hover, .lb-tile.lb-drop:focus-visible {
  border-color: var(--hot); color: var(--ink); outline: none;
}
.lb-drop-plus { font-size: var(--fs-title); line-height: 1; }

/* THE MIXED ROW'S OWN EMPTY SLOT — same dashed look as `.lb-drop`, but
   deliberately a different class: `.lb-tiles .lb-tile.lb-drop ~
   .lb-tile.lb-drop { display: none }` hides every empty invite after the
   first, right for the ordinary strip's "one way in" and wrong here, where
   a round might need dropping into any one of several open gaps at once. */
.lb-tile.mix-drop {
  align-items: center; justify-content: center; text-align: center;
  border: 2px dashed var(--panel-line); background: none; color: var(--ink-dim);
  font-weight: 800; cursor: pointer;
}
.lb-tile.mix-drop.drop-here { border-color: var(--hot); color: var(--ink); background: rgba(255, 46, 136, 0.08); }
/* The ORDINARY row's empty slot lights the same way, because it now takes the
   same thing. It had no dragover of its own at all, so nothing happened as a
   round crossed it — and an inert square is a square that does not take what
   you are holding, whatever the drop handler further up would have done. */
.lb-tile.lb-drop.drop-here { border-color: var(--hot); color: var(--ink); background: rgba(255, 46, 136, 0.08); }
.mix-rd {
  display: inline-grid; place-items: center;
  width: 28px; height: 28px; padding: 0; margin: 2px 2px 0 0;
  /* Square with soft corners, like the tile's own — the two are the same idea
     drawn in two renderers, and a pill in one and a square in the other is the
     kind of drift the GUI rules exist to stop. */
  border-radius: var(--r-field); cursor: grab;
  transition: transform 90ms ease, box-shadow 90ms ease;
  font: inherit; font-size: var(--fs-mini); font-weight: 900; line-height: 1;
  background: rgba(47, 224, 122, 0.16); border: 1px solid rgba(47, 224, 122, 0.55); color: var(--good);
}
/* The mixed row's rounds get the same treatment, and they are the ones that
   genuinely DRAG — so the lift is doing double duty there: it says "press me"
   and "pick me up". Same rule as the tile's, one place each because the two
   are different sizes. */
.mix-rd:hover, .mix-rd:focus-visible {
  transform: translateY(-2px);
  box-shadow: 0 0 0 2px var(--hot), 0 4px 10px rgba(0, 0, 0, 0.45);
  outline: none;
}
.mix-rd:active { transform: translateY(0); }
.mix-rd.is-lifting { opacity: 0.35; border-style: dashed; }
/* Off — out of tonight entirely, same red/green language `.lb-rd` already
   uses. Not draggable (see console-tonight-mix-ui.js): a tap brings it back
   into its pack's own home slot, which is all a phone needs to be able to
   do, and dragging an OFF round anywhere would be a control with no target
   worth having. */
.mix-rd.off { cursor: pointer; background: rgba(255, 77, 94, 0.12); border: 1px solid rgba(255, 77, 94, 0.5); color: var(--bad); }
.mix-bingo-set {
  display: flex; gap: 4px; margin-top: 4px; position: relative; z-index: 1;
}
.mix-bingo-set select {
  font-size: var(--fs-mini); padding: 2px 4px; min-width: 0; max-width: 100%;
  background: rgba(255, 255, 255, 0.06); border: 1px solid var(--panel-line);
  border-radius: var(--r-field); color: var(--ink);
}

/* THE SHELF SHOWS THE GAP. A pack that is in tonight is drawn as an
   outline of itself, so it has visibly LEFT rather than been copied — which
   is what makes the flight read as a move rather than a duplicate appearing.
   Only a look: the pack is untouched on disk, and taking it out of the night
   puts the card straight back. */
.pack-card.in-tonight {
  background: none;
  border-style: dashed;
  border-color: var(--hot);
  opacity: 0.5;
}
.pack-card.in-tonight::after {
  content: 'In tonight';
  position: absolute; top: 8px; right: 10px;
  font-size: var(--fs-tag); font-weight: 900; letter-spacing: 0.06em;
  color: var(--hot);
}
.pack-card { position: relative; }

/* WHERE, WHEN AND WHAT FOR. Quieter than a pack square — they are read rather
   than pressed, and the night itself has to stay the loud half of the row. */
.lb-tile.is-info {
  background: rgba(255, 255, 255, 0.03); border: 1px solid var(--panel-line);
}
.lb-tile.is-venue { cursor: pointer; }
.lb-tile.is-venue:hover { border-color: var(--hot); }
.lb-tile.is-info.is-none .lb-tile-name { color: var(--ink-dim); }

/* The runner-up pack, as a chip rather than a second gradient button — there
   is one "press this" on a screen, and it is Launch. */
.lb-why { margin-top: 6px; }
/* The runner-up pack and Set it up, on one row — both are minor controls
   answering "not this one" and "not like this", and stacked they read as two
   separate steps. */
.lb-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.lb-row:empty { display: none; }
.lb-alt[hidden] { display: none; }

/* SET IT UP — everything a night needs that is not the pack itself. Shut by
   default: the common job is this pack, this pub, go, and a wall of dropdowns
   in front of that is the panic control defeating itself. */
.lb-more[hidden] { display: none; }
.lb-more::after {
  content: '';
  display: inline-block; margin-left: 8px; vertical-align: 1px;
  border: 5px solid transparent; border-top-color: currentColor; border-bottom: 0;
}
.lb-more[aria-expanded="true"]::after { transform: rotate(180deg); }

/* Who is already in the shared portrait library. Names rather than pictures:
   the job is spotting "michael-jackson" next to "mj", which is a reading job. */
.pic-lib { margin-top: 10px; }
.pic-lib summary { cursor: pointer; opacity: 0.8; }
.lib-names { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.lib-name {
  font-size: var(--fs-mini); font-weight: 700; padding: 4px 9px; border-radius: var(--r-pill);
  background: var(--panel); border: 1px solid var(--panel-line);
}
/* A stand-in is not a drawing anybody paid for, so it reads quieter. */
.lib-name.stand-in { opacity: 0.5; font-weight: 600; }

/* ---- forgotten password
   A BUTTON that looks like a link, not a link: it opens a form on this page
   rather than going anywhere, and an <a href="#"> that does nothing is the
   thing screen readers and a long-press both get wrong. */
.linky {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; color: var(--hot-2); text-decoration: underline;
}
.sign-in .forgot { margin-top: 14px; display: grid; gap: 10px; text-align: left; }

/* The topbar's right-hand end. `nowrap` because "Sign out" was breaking into
   two lines the moment the bar got tight, and a button that changes height
   drags the whole row with it. The "who am I" line gives way instead — it is
   the one thing up there you can afford to lose the end of. */
.topbar-out, .hat-switch { white-space: nowrap; }
/* THE BAR ITSELF NEEDS IT FIRST, AND THAT WAS THE HALF THAT WAS MISSING.
   `.topbar` is a GRID ITEM of `.wrap`, and a grid item defaults to
   `min-width: auto` exactly as a flex item does — so the bar refused to go
   below its own max-content and simply grew past the window. `.wrap` is
   `overflow-x: hidden`, so the excess was CLIPPED with no scrollbar to reach
   it: with a game running at ~960px the bar came out 1128px wide and the tier
   rungs were off the side of the screen, unreachable. Reported as "UI issue"
   off a screenshot with the ends cut off both sides.

   Constraining the bar is what makes the two rules below do anything at all —
   until it was, the children were never asked to give way, so `min-width: 0`
   on them had nothing to act on. The projector's own topbar has had this since
   a long pack name stacked it to four lines (`.screen .topbar > *`); the
   console's never got it. */
.console .topbar { min-width: 0; }
/* AND WHEN IT STILL WILL NOT FIT, THE BAR TAKES A SECOND ROW — it must never
   swallow a door.

   Constraining the bar above moved the overflow rather than removing it: the
   menu is `flex: 1 1 auto` with `overflow-x: auto`, so it was the thing that
   gave way, and at 1000px it was handed 376px of the 519px it needs. There is
   no scrollbar on it by design, so **"My account" simply was not there** —
   measured, and reported as *"what happened to the other menu?"* off a
   screenshot with the Community chip cut down the middle. A door you cannot
   see is a door that does not exist; that is the fault the tab bar's own wrap
   rule was written for, and this is the same fault one region higher.

   A wrapping flex row places items on lines at their FULL size and only
   shrinks per line afterwards, so `wrap` alone is the whole fix: the menu can
   no longer be squeezed, and the bar stays one row at every width where one
   row is enough. The owner is the account that overflows — the hat switch and
   the tier rungs are 295px nobody else carries — and a quizmaster's bar is
   unchanged at every size.

   It is not scoped to a breakpoint on purpose. The phone rule below still
   gives the menu a line of its own from 430px down, where a second row is
   wanted rather than merely tolerated. */
.console .topbar { flex-wrap: wrap; row-gap: 8px; }

/* …BUT WRAPPING IS THE FALLBACK, NOT THE ARRANGEMENT. *"The menu items all
   need to sit at the top."* Two rows put the hat switch and Sign out on a line
   of their own under the doors, which reads as a second bar; and under the
   fixed frame every row the header takes comes off the tab column below it.

   So below 1180px — where `.wrap` stops being capped and the bar stops being a
   constant width — the bar goes on a DIET instead, and the order of what it
   gives up is the point: the two things that must survive are the five doors
   and the account controls, because those are the only things up there anybody
   presses.

   THE WORDMARK IS WHAT GOES, and the mark stays. It is the same trade the
   control view already makes on a phone, for the same stated reason: a door
   that is not there is a worse breach than a smaller logo. The name is six
   feet wide on the projector and on every player's phone; this is a header on
   the operator's own laptop.

   SCOPED TO A BAR THAT HAS A HAT SWITCH, which is the owner's alone. Nobody
   else carries the 326px the switch and the tier rungs come to, so nobody
   else's bar was ever close to overflowing — and putting an ordinary
   quizmaster's wordmark on a diet to solve a problem they do not have is how a
   fix for one account lands on everybody. */
/* AND IT HAS NO UPPER BOUND, WHICH IS THE WHOLE CORRECTION.
 *
 * This was `max-width: 1179px` and that measured the WRONG BOX. `.console
 * .wrap` is `max-width: 1180px`, so the bar is ~1148px wide at EVERY window
 * width — a 1900px laptop gets exactly the same bar as a 1200px one. Keying
 * the diet to the window therefore switched it off above 1180px while the bar
 * stayed the width that needed it, and with a night running the owner's header
 * wrapped to two rows at every desktop size there is.
 *
 * **A media query on the window is the wrong tool the moment a container caps
 * the thing you are protecting.** Reported as *"the menu at the top is fucked
 * AGAIN"*, and it is the same fault as the first time wearing a threshold: the
 * bar cannot shrink and nothing was telling it to. */
/* AND WHAT THE DIET TAKES IS THE PRODUCT NAME, NEVER THE POSSESSIVE.
 *
 * The first version hid `.brand-name`, which is the whole stacked wordmark —
 * so the one account that lost its own name off its own console was the
 * OWNER's, because `:has(.hat-switch)` is his bar alone. The possessive exists
 * to put a quizmaster's name on their app; deleting it from the one bar this
 * rule reaches is the feature running backwards, and it was reported as the
 * name having "disappeared".
 *
 * SO THE MARK CARRIES THE PRODUCT AND THE WORD CARRIES WHOSE. The drawn
 * question-in-a-microphone already says which app this is, and it is 40px
 * where the stack is 81 — measured at 1000px, which is where the bar is
 * tightest, `#runningNow` having already stood down.
 *
 * RESTORING THE WHOLE STACK WAS RENDERED AND IS DEAD. At 1000px it does not
 * push a door off the bar — it drops `#hatSlot`, the switch and the tier
 * rungs, onto a second row 54px down, taking Sign out with it. That is the
 * two-row header this diet exists to prevent, and the account controls are the
 * other half of what it is protecting.
 *
 * A wordmark with no possessive in it still goes, and loses nothing: it says
 * only what the mark beside it already said. */
@media (min-width: 431px) {
  .console .topbar:has(.hat-switch) { gap: 10px; padding-left: 14px; padding-right: 14px; }
  .console .topbar:has(.hat-switch) .brand-name:not(.stacked) { display: none; }
  /* Below the cap the bar tracks the window, so the product name gives way and
     the possessive stays. See the block under this one for where it comes back. */
  .console .topbar:has(.hat-switch) .brand-name.stacked .brand-app { display: none; }
  /* The nesting margin sits the possessive INTO the product name; with nothing
     underneath to nest into it only clips the tail of a descender. */
  .console .topbar:has(.hat-switch) .brand-name.stacked .brand-who { margin-bottom: 0; }
  .console .topbar:has(.hat-switch) .topnav { gap: 5px; }
  .console .topbar:has(.hat-switch) .topnav a { padding: 8px 10px; }
  .console .topbar:has(.hat-switch) .hat-switch .hat-half { padding: 10px 10px; }
}
/* AND THE WHOLE WORDMARK IS BACK WHERE THE BAR IS AT ITS FULL WIDTH.
 *
 * *"The whole point is to have [QM's] Quizporium"* — the possessive alone is
 * half a brand, so "Mark's" over "Quizporium" is what this shows wherever it
 * fits, and the diet only takes the product name where the bar runs out.
 *
 * **1180px IS THE CONTAINER'S NUMBER, NOT THE WINDOW'S, WHICH IS WHY A MEDIA
 * QUERY IS THE RIGHT TOOL HERE.** `.console .wrap` caps at 1180px, so at that
 * width and above the bar is a CONSTANT ~1148px — a 1900px laptop gets exactly
 * the same bar as a 1200px one, and the full stack was measured on one row
 * there with every door still hit-testable. Below it the window and the bar
 * move together, which is the one region where asking the window is asking
 * about the bar. That is the same fact the note above uses to say a max-width
 * query was wrong: wrong for switching the diet OFF above the cap, right for
 * switching this ON at it.
 *
 * ONLY THESE TWO DECLARATIONS. The `.topnav` and `.hat-half` rules above are
 * the diet proper and stay in the 431px block — a scripted edit moved them in
 * here, which left the menu's own padding on a diet only above 1180px, exactly
 * backwards. The brace count still balanced and `style-structure` still passed;
 * measuring the bar is what found it. */
@media (min-width: 1180px) {
  .console .topbar:has(.hat-switch) .brand-name.stacked .brand-app { display: block; }
  .console .topbar:has(.hat-switch) .brand-name.stacked .brand-who { margin-bottom: -0.04em; }
}
/* AND UNDER ~1050px THE LIVE LINE STANDS DOWN, because the diet alone is not
   enough: the mark, five doors, the hat, four rungs and Sign out come to more
   than the bar has, and something has to be allowed to go.
   IT IS THE RIGHT THING TO LOSE. Every other item in that bar is the only copy
   of itself — a door you cannot see is a door that does not exist — while this
   is a SUMMARY of the running panel a few inches below, which says the same
   fact in full and never moves. Losing the summary costs a glance; losing a
   door costs the page. */
@media (max-width: 1050px) {
  .console .topbar:has(.hat-switch) #runningNow { display: none; }
}
/* The two bits of prose in the bar are what give way — "Owner — you@…" and
   "Now: The Madonna Quiz (6 in)". They are the only things up there you can
   afford to lose the end of, and without `min-width: 0` a flex item will not
   go below its own text however long that text is. */
.console .topbar #whoami,
.console .topbar #runningNow {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- picking a team, on the phone
 *
 * Only ever on a WAITING screen — the lobby and the gap between rounds — for
 * the same reason the engine refuses a mid-question switch: somebody watching
 * the tally and hopping into whichever team is doing well. The screen with
 * four options and a clock on it gains nothing.
 */
.team-pick {
  display: grid; gap: 10px; justify-items: stretch;
  padding: 14px; margin-top: 6px;
  border: 1px solid var(--panel-line); border-radius: var(--r-card);
  background: var(--panel);
  min-width: 0;
}
.team-pick-head { text-align: center; text-transform: uppercase; letter-spacing: 0.08em; }
.team-mine { font-size: var(--fs-sub); font-weight: 900; text-align: center; color: var(--gold); overflow-wrap: anywhere; }
/* Wrapping into equal cells rather than a flex row: sized to their own text
   the buttons came out four different widths and read as four objects. */
.team-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; }
.team-opt {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  min-width: 0; padding: 11px 12px;
  border: 1px solid var(--panel-line); border-radius: var(--r-field);
  background: none; color: var(--ink);
  font-size: var(--fs-body); font-weight: 800; text-align: left;
  overflow-wrap: anywhere; cursor: pointer;
}
.team-opt.here { border-color: var(--gold); color: var(--gold); }
.team-size { flex: 0 0 auto; font-size: var(--fs-tag); font-weight: 900; opacity: 0.7; }
.team-new { display: flex; gap: 8px; min-width: 0; }
.team-new input { flex: 1 1 auto; min-width: 0; }
.team-add {
  flex: 0 0 auto; padding: 0 16px; border: 0; border-radius: var(--r-field);
  font-weight: 900; cursor: pointer;
  background: linear-gradient(120deg, var(--hot), var(--hot-2)); color: #12000c;
}
/* Never disabled on the team-name form itself — only the breakout form
   (which reuses this row shape) locks its button once an answer is sent. */
.team-add:disabled { background: rgba(255, 255, 255, 0.09); color: var(--ink-dim); cursor: default; }
.team-leave { background: none; border: 0; color: var(--ink-dim); text-decoration: underline; cursor: pointer; padding: 2px; }

/* ---- where an account stands on money, as a £ in a coloured box
 *
 * It replaced a COMPED badge and a Comp / Charge them button — two controls
 * saying one thing, neither of them saying it in English. The colour is the
 * whole point: you are scanning a list for the row that needs doing something
 * about, not reading each one. Green is the state most accounts will be in, so
 * green has to be the quiet one.
 */
.money-badge {
  flex: 0 0 auto;
  width: 34px; height: 34px;
  border-radius: var(--r-field);
  border: 1px solid transparent;
  font-size: var(--fs-lead); font-weight: 900; line-height: 1;
  cursor: pointer;
}
.money-badge.paying { background: rgba(47, 224, 122, 0.16); border-color: rgba(47, 224, 122, 0.5); color: var(--good); }
.money-badge.free   { background: rgba(255, 138, 61, 0.18); border-color: rgba(255, 138, 61, 0.55); color: var(--hot-2); }
.money-badge.owing  { background: rgba(255, 92, 92, 0.18);  border-color: rgba(255, 92, 92, 0.6);  color: var(--bad); }
.money-badge.closed { background: rgba(255, 255, 255, 0.06); border-color: var(--panel-line); color: var(--ink-dim); }
.money-badge:active { transform: scale(0.94); }

/* Finding one account among many. Wraps to one control per line on a phone
   rather than squeezing four; a grid child's default minimum is its
   min-content, which is what drags a page sideways, so every one gets
   min-width: 0 and the selects a max-width as well. */
.people-filters {
  display: grid; gap: 8px; margin: 4px 0 14px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.people-filters > * { min-width: 0; max-width: 100%; }

/* The line under "Quick launch" that says where the settings are. Above the
   search box rather than under the two buttons, because it answers a question
   somebody has BEFORE they press one. */
.launchbar .lb-note { margin: -4px 0 12px; color: var(--ink-dim); }

/* The venue box on a pack card. Wider than the dropdowns beside it — a pub
   name is six words where "In the room" is three — and it must never be the
   thing that drags the card sideways, so it may shrink to nothing. */
/* Venue is an ordinary column now, not a full-width row.
   It spanned because it used to be a free-text box that wanted the room. As a
   dropdown holding "The Station Tap" it just became an 800px select on a wide
   screen, which is what the host reported as odd. */
.pack-shape.venue-wrap { min-width: 0; }
.venue-pick { width: 100%; min-width: 0; }


/* =====================================================================
 * THE FIVE ROLES, from the GUI rules in CLAUDE.md. What a control looks
 * like is decided by WHAT IT DOES, not by which page it happens to be on.
 * ===================================================================== */

/* DESTRUCTIVE — outlined red, never filled.
 *
 * Filled, it is as loud as Launch and sits directly beside it on a pack card
 * somebody is tapping in a dark pub. Fill means commitment, and the thing you
 * are committing to here is losing something. It was written out four times
 * in four places and had already drifted; this is the one rule. */
.danger, button.danger, .minor.danger,
.actions .minor.danger, .editor button.danger, .console button.minor.danger,
.pack-del, .pack-actions .pack-del {
  color: var(--bad);
  border: 1px solid rgba(255, 77, 94, 0.45);
  background: none;
}
.danger:hover, button.danger:hover, .minor.danger:hover,
.pack-del:hover { background: rgba(255, 77, 94, 0.12); }

/*
 * AND A DESTRUCTIVE BUTTON IN THE CONSOLE KEEPS THE FACE AND THE 2px EDGE.
 *
 * The rule directly above uses the `border` SHORTHAND, which overwrites all
 * four sides — so it silently flattened `.console button.minor.danger`'s 2px
 * bottom to 1px and blanked its top-lit face, leaving Unlaunch and Delete
 * structurally different objects from Save and Rename sitting beside them.
 * **That is the shorthand-beats-longhand trap this repo already records about
 * the pack tile's border**, hit again in the one place a rule calls itself
 * "the one rule".
 *
 * CLAUDE.md's own line settles which way to fix it: *"Destructive keeps RED
 * on its edge in every scheme, like everywhere else."* An edge, not a
 * different shape. Outlined-never-filled is about FILL and is untouched — the
 * face here is the same faint top-lit surface every ordinary button wears,
 * not a red fill.
 *
 * Scoped to the console on purpose: the bare `.danger` above also dresses
 * buttons that never had a face, and widening this would be a change to
 * screens nobody asked about.
 */
.console button.minor.danger, .console a.minor.danger {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.04));
  border: 1px solid rgba(255, 77, 94, 0.45);
  border-bottom: 2px solid var(--bad);
}
.console button.minor.danger:hover, .console a.minor.danger:hover {
  background: rgba(255, 77, 94, 0.12);
}

/* MAKE SOMETHING — filled green.
 *
 * `role-make`, NOT `make`: `.make` was already taken as a JS hook for the
 * "Make real portraits" button (`querySelector('.make')`), so a role class of
 * that name would have styled a button by accident and, worse, put a second
 * candidate under a selector that expects one. Role classes are prefixed for
 * exactly that reason — found by grepping before trusting the name. Writing, generating, importing: the actions
 * that produce content rather than run a night. Green because it is NOT the
 * night, and the night owns the account's own gradient. */
.role-make {
  /* THE PAGE'S OWN TYPE, not the browser's.
     This had no `font` at all, so every "makes something" button — Write it,
     Import, Add a venue, New set, Save — inherited the browser's default
     button font: 13.333px in a system UI face, which is both off the type
     ladder and a different TYPEFACE from every other control on the page.
     Measured across four console tabs before it was spotted; it is invisible
     in a diff and obvious once you know. */
  font: inherit;
  font-size: var(--fs-ctl);
  border-radius: var(--r-field);
  /* IT HAD NO PADDING EITHER, which is the same fault as the font one above
     and hid behind it. Every "makes something" button fell back to the
     browser's `1px 6px`, so a bare one came out 21px tall — half the height
     of the `.minor` beside it. It was invisible wherever one happened to sit
     in a flex row that stretched it, and plain to see the moment Adverts'
     "New set" wrapped onto a line of its own. */
  padding: 9px 14px;
  /*
   * DEEPER THAN `--good` ITSELF, and that is the whole tuning.
   *
   * `--good` into `--gold` at full saturation is a highlighter: it came out
   * LOUDER than Launch, which is the opposite of what this role is for. Green
   * has to say "this makes something" while leaving the night the loudest
   * thing on the screen, so the fill is darkened and the text is the bright
   * green — the same trick the destructive button uses, and for the same
   * reason.
   */
  background: linear-gradient(120deg, rgba(47, 224, 122, 0.18), rgba(255, 210, 63, 0.14));
  border: 1px solid rgba(47, 224, 122, 0.55);
  color: var(--good);
  font-weight: 900;
  cursor: pointer;
}
.role-make:hover { background: linear-gradient(120deg, rgba(47, 224, 122, 0.28), rgba(255, 210, 63, 0.2)); }
.role-make:disabled { opacity: 0.45; cursor: default; }

/* WHERE YOU ARE, when the hat switch cannot say it.
 *
 * Only ever on the bare host key. Quiet and outlined rather than coloured:
 * it is a statement of fact, not a warning and not something to press — and
 * the one thing it must not do is look like the switch it stands in for. */
.mode-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 14px;
  border: 1px solid var(--panel-line);
  border-radius: var(--r-pill);
  color: var(--ink-dim);
  font-size: var(--fs-mini); font-weight: 800;
  white-space: nowrap;
}
.mode-chip b { color: var(--ink-dim); opacity: 0.5; font-weight: 400; }
/* On a phone it loses the CREDENTIAL and keeps the ROOM, which is the half
   that decides whether the people in front of you can join. Same shape as the
   hat switch's own narrow rule — lose padding, then lose the least useful
   words — and it is what keeps the control view inside 320px, where the bar
   is already carrying a logo and three menu chips.

   700 rather than 560, because between the two the console's topbar had
   nowhere left to put it: its own wrap rule is 430 and this was 560, so at
   620px the full chip pushed the page 4px sideways with nothing on screen
   explaining why. Only ever visible on the host key, which is why it survived
   — and dropping the credential earlier costs nothing, because the ROOM is
   the half worth keeping at any width. */
@media (max-width: 700px) {
  .mode-chip { padding: 6px 10px; font-size: var(--fs-tag); }
  .mode-chip .mc-how { display: none; }
}

/* ---- the prize, on the bar's phone
 *
 * A page a stranger opens once, standing up, in a dark pub, with somebody
 * waiting in front of them. So it is one card, the reward is the biggest thing
 * on it, and there is exactly one button. Nothing here is a navigation surface
 * and there is deliberately no menu: this is not a page anybody comes back to.
 */
body.voucher { display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 18px; }
.v-wrap { width: 100%; max-width: 420px; }
.v-card {
  background: var(--panel); border: 1px solid var(--panel-line);
  border-radius: var(--r-card); padding: 26px 22px; text-align: center;
}
.v-flag {
  display: inline-block; margin-bottom: 16px;
  padding: 6px 14px; border-radius: var(--r-pill);
  font-size: var(--fs-tag); font-weight: 900; letter-spacing: 0.14em; text-transform: uppercase;
  border: 1px solid var(--panel-line); color: var(--ink-dim);
}
.v-flag.v-good { color: var(--good); border-color: rgba(47, 224, 122, 0.55); background: rgba(47, 224, 122, 0.12); }
/* THE DEMONSTRATION MARK. Never on a real voucher — see `demoMark()`.

   A full-width band rather than another pill, because it is a statement about
   the whole card, and a second pill beside "Winner" would read as a second
   status.

   NEUTRAL, AND THAT IS A DELIBERATE REFUSAL OF THE OBVIOUS COLOURS. Gold was
   written first and is wrong: gold means winning everywhere in this app, and
   this band sits directly under a flag that says WINNER — it would borrow the
   one meaning it must not have. Red is wrong too, for the opposite reason:
   red means something has GONE wrong, and this is read before anything
   happens rather than after a mistake. So it carries no meaning-colour at
   all, and does its job with a dashed edge and weight instead. */
.v-demo {
  margin: -8px 0 16px; padding: 8px 12px; border-radius: var(--r-field);
  font-size: var(--fs-tag); font-weight: 800; letter-spacing: 0.04em;
  color: var(--ink-dim); border: 1px dashed var(--panel-line);
  background: rgba(255, 255, 255, 0.04);
}
.v-lead { margin: 0 0 4px; font-size: var(--fs-body); color: var(--ink-dim); font-weight: 700; }
/* Not on the type ladder on purpose: this is display type, one per surface,
   like the countdown and the room code. It has to be readable at arm's length
   by somebody who is not looking for it. */
.v-big { margin: 0 0 10px; font-size: 30px; line-height: 1.15; font-weight: 900; }
.v-who { margin: 0 0 20px; font-size: var(--fs-sub); font-weight: 800; color: var(--gold); }
/* FILL MEANS COMMITMENT, and this is the only commitment on the page — so it
   takes the app's own gradient, the way Launch does. There is nothing here for
   it to compete with: the bar sees one screen with one button on it. `.go` is
   scoped to the console, so it is written out rather than borrowed. */
.v-go {
  width: 100%; padding: 18px; font-size: var(--fs-sub); font-weight: 900;
  border: none; border-radius: var(--r-card); cursor: pointer;
  color: #12000c; background: linear-gradient(120deg, var(--hot), var(--hot-2));
  font-family: inherit; -webkit-tap-highlight-color: transparent;
}
.v-go:disabled { opacity: 0.5; }
.v-note { margin: 12px 0 0; font-size: var(--fs-note); color: var(--ink-dim); }
.v-note.v-bad { color: var(--bad); }
.v-foot { margin: 18px 0 0; font-size: var(--fs-mini); color: var(--ink-dim); opacity: 0.7; }
/* Spent: the whole card goes quiet, so it cannot be mistaken at a glance for
   one that is still good. */
.v-spent .v-big { color: var(--ink-dim); text-decoration: line-through; }
.v-spent .v-who { color: var(--ink-dim); }

/* The winner's own card, on their own phone. The QR is the biggest thing on
   it because the whole interaction is holding the phone up to be scanned. */
.win-card {
  background: var(--panel); border: 1px solid rgba(255, 210, 63, 0.5);
  border-radius: var(--r-card); padding: 18px; text-align: center;
}
.win-card .sub {
  font-size: var(--fs-tag); font-weight: 900; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--gold);
}
/* THE VENUE'S OWN MARK, at the top of the voucher. A stranger behind the bar
   has to decide whether to honour this, and the pub's own logo is what makes
   it read as something the pub issued rather than something the quiz app made
   up.

   Capped in HEIGHT and `object-fit: contain`, because a venue's logo arrives
   at whatever shape the venue has — a wide wordmark and a square badge both
   have to sit in this card without pushing the prize off the screen. A white
   background is common in logos and would be a bright slab on a dark card, so
   it gets a rounded white plate of its own: deliberate rather than accidental,
   and it makes a transparent PNG legible too. */
.win-logo {
  display: block; margin: 0 auto 12px;
  max-height: 56px; max-width: 70%;
  object-fit: contain;
  background: #fff; border-radius: var(--r-field); padding: 6px;
}
.win-what { font-size: var(--fs-sub); font-weight: 900; margin: 6px 0 14px; }
.win-qr {
  width: 100%; max-width: 220px; aspect-ratio: 1; display: block; margin: 0 auto;
  border-radius: var(--r-field); background: #fff; padding: 8px;
}
.win-code {
  margin-top: 10px; font-size: var(--fs-sub); font-weight: 900;
  letter-spacing: 0.22em; color: var(--gold);
}
.win-spent { border-color: var(--panel-line); }
.win-spent .sub, .win-spent .win-what { color: var(--ink-dim); }
.win-spent .win-what { text-decoration: line-through; }

/* The prize panel on the control view. One row per voucher, and the row goes
   quiet once it is spent so a glance tells you which are still out there. */
.v-rows { display: grid; gap: 8px; margin-top: 10px; }
.v-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 12px; border-radius: var(--r-field);
  background: rgba(255, 255, 255, 0.05); border: 1px solid var(--panel-line);
}
.v-row-who { flex: 1 1 auto; min-width: 0; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.v-row-code { font-size: var(--fs-note); letter-spacing: 0.18em; color: var(--gold); font-weight: 800; }
.v-row-again { font-size: var(--fs-tag); font-weight: 900; color: var(--gold); text-transform: uppercase; letter-spacing: 0.1em; }
.v-row-state { font-size: var(--fs-note); color: var(--ink-dim); }
.v-row.is-spent { opacity: 0.7; }
.v-row.is-spent .v-row-code { color: var(--ink-dim); }

/* First, second and third. The medal COLOURS, never the words — "gold",
   "silver" and "bronze" are the subscription tiers, and a quizmaster reading
   "Gold prize" on the page that says they are on Gold is the same collision
   one level up. Gold already means first place everywhere in this app. */
.win-card.place-2 { border-color: rgba(198, 205, 214, 0.55); }
.win-card.place-2 .sub, .win-card.place-2 .win-code { color: var(--metal-silver); }
.win-card.place-3 { border-color: rgba(205, 137, 90, 0.55); }
.win-card.place-3 .sub, .win-card.place-3 .win-code { color: var(--metal-bronze); }
.v-place {
  flex: 0 0 auto; min-width: 30px;
  font-size: var(--fs-tag); font-weight: 900; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--gold);
}
.v-place-2 { color: var(--metal-silver); }
.v-place-3 { color: var(--metal-bronze); }
.v-row-what { font-size: var(--fs-note); color: var(--ink-dim); }

/* First, second and third at launch. One row each, the place on the left so
   the first box is obviously one of a set — without it a lone unlabelled box
   reads as the only field there is, which is exactly how it was reported. */
.reward-stack { display: grid; gap: 6px; }
.reward-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
/* NEUTRAL BY DEFAULT, medals only for the first three.

   Gold was the default here, which was harmless while there were exactly three
   rows and became a lie the moment a venue could list five: 4th and 5th came
   out in the trophy colour, which in this app means FIRST PLACE everywhere —
   the winner, the top of the board, the reveal banner. A prize table where
   three different places are gold says nothing at all.

   So the medals are stated, not defaulted: gold, silver and bronze belong to
   the three that are actually podium places, and everything after them is
   ordinary text. That is also why 4th place on the projector is a LINE and not
   a card — see the podium note in CLAUDE.md. */
.reward-row .reward-place {
  flex: 0 0 auto; min-width: 26px;
  font-size: var(--fs-tag); font-weight: 900; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-dim);
}
.reward-row[data-place="1"] .reward-place { color: var(--gold); }
.reward-row[data-place="2"] .reward-place { color: var(--metal-silver); }
.reward-row[data-place="3"] .reward-place { color: var(--metal-bronze); }
/* `min-width: 0` or the input refuses to go below its own placeholder and
   drags the pack card sideways — the same trap, for the fourth time. */
.reward-row .reward-pick { flex: 1 1 auto; min-width: 0; }

/* ---- the Venues tab
   One card per place you play, and deliberately small: everything a venue
   needs for a NIGHT is its name and what is behind the bar. The address and
   the fee stay on the Invoices tab, because a page opened ten minutes before
   a quiz has no business carrying somebody's postal address. */
.venue-list { display: grid; gap: 10px; margin-top: 12px; }
.venue-card {
  border: 1px solid var(--panel-line); border-radius: var(--r-card);
  background: var(--panel); padding: 12px 14px;
}
.venue-top { display: flex; align-items: center; gap: 10px; justify-content: space-between; }
/* WHICH NIGHT THEY HAVE YOU — the cheapest half of a diary, and the thing the
   launch bar reads to know whose night tonight is. Above the prizes because it
   is the more often true of the two: a residency outlives whatever they happen
   to be putting behind the bar this month. `min-width: 0` on the select for
   the reason every other one in this file has it — a dropdown will not go
   below its longest option and drags the card sideways on a 320px phone. */
.venue-night { display: flex; align-items: center; gap: 8px; margin-top: 10px; font-size: var(--fs-note); color: var(--ink-dim); font-weight: 800; }
.venue-night select { flex: 1 1 auto; min-width: 0; max-width: 100%; }
/* WHERE TO SEND THEM at the end of the night. Labelled by the job rather than
   by the field, which makes the label long — so on a phone it goes above the
   box instead of squeezing it to a third of the row. */
.venue-link input { flex: 1 1 auto; min-width: 0; }
@media (max-width: 560px) {
  .venue-link { flex-direction: column; align-items: stretch; gap: 4px; }
}
.venue-prizes { display: grid; gap: 6px; margin-top: 10px; }
.venue-prizes .reward-row { margin: 0; }
.venue-card .v-save { margin-top: 10px; }

/* ---- HEADCOUNT PER VENUE
   "The Crown went from 22 on a Thursday to 58" — the sentence a quizmaster
   shows a landlord in January. Drawn twice out of one record: the whole
   history inside a venue card, and every venue side by side on the Gigs tab.

   NEITHER OF THEM IS A GRADIENT FILL. The account's own colour is right here —
   this is a highlight, which is the half of the scheme that follows the
   quizmaster — but a filled gradient is what "press this" looks like in this
   app, and a block of it doing nothing when pressed costs the real buttons
   their meaning. So the bars carry the colour at reduced strength and the
   LATEST night carries it at full, which is also the one worth finding.

   No red for a night that went down, either. Red means wrong or destructive
   everywhere else in this app, and a quieter Tuesday in February is neither —
   it is a fact, and colouring it as a fault would be the app editorialising
   about somebody's own work. */
.heads { margin-top: 14px; border-top: 1px solid var(--panel-line); padding-top: 12px; }
.heads-tag {
  font-size: var(--fs-tag); font-weight: 800; letter-spacing: 0.02em;
  color: var(--ink-dim);
}
.heads-line { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-top: 3px; }
.heads-big { font-size: var(--fs-sub); font-weight: 900; font-variant-numeric: tabular-nums; white-space: nowrap; }
.heads-arrow { color: var(--ink-dim); font-weight: 700; }

/* Every night, newest first — the same order as every other list of nights
   here. It scrolls past a dozen rather than being cut short: two years of
   Thursdays is a hundred nights, and the page's whole job is showing somebody
   all of them. */
.heads-nights { display: grid; gap: 5px; margin-top: 10px; max-height: 236px; overflow-y: auto; }
.heads-night {
  display: grid; grid-template-columns: 82px 1fr 34px; gap: 8px; align-items: center;
  font-size: var(--fs-note);
}
/* min-content is a grid child's default minimum, which is how one long label
   drags a card sideways on a 320px phone. Bitten six times now. */
.heads-night > * { min-width: 0; }
.hn-when { color: var(--ink-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hn-bar { height: 8px; border-radius: var(--r-pill); background: rgba(255, 255, 255, 0.08); overflow: hidden; }
.hn-bar i { display: block; height: 100%; border-radius: var(--r-pill); background: var(--hot); opacity: 0.5; }
.heads-night:first-child .hn-bar i { opacity: 1; }
.hn-num { text-align: right; font-weight: 800; font-variant-numeric: tabular-nums; }

/* ---- Past gigs — venue cards, opening into the nights at each one
   Same `.venue-card`/`.venue-top`/`.venue-name` component the Venues tab
   uses; see `venueCard()` in console-gigs.js. Shut, the gist is whatever
   `headcountLine()` says. Open, `.venue-gig-list` reuses `.gig-list`'s own
   grid/gap — a night row here is the same object shown on the Post gig
   bench, so nothing about it needed a second style. */
.venue-cards { display: grid; gap: 10px; margin-top: 12px; }
.venue-gist { margin-top: 4px; }
.venue-gig-list { margin-top: 10px; }

/* THE ROW CURRENTLY ON THE BENCH, so it is findable again in a long list
   without having to remember which one it was. Same colour a hovered row
   already carries, just held rather than momentary. */
.gig-head.is-selected { border-color: var(--hot); background: rgba(255, 255, 255, 0.06); }

/* ---- one night's detail, on the POST GIG BENCH — `fillNightDetail()` in
   console-gigs.js, called from `nightBenchPanel()`. Winner lines, one row of
   buttons, one row of photos and the gallery toggle, laid out in the
   bench's own body rather than a second panel of their own.

   NO VERTICAL CAP NEEDED HERE ANY MORE — a night with thirty photos used to
   grow this past the fixed frame's bottom edge with nothing left to scroll
   it back into view (the door's own area above the tab column sizes to its
   content and does not scroll itself; see `.doorhead`). The fix was not a
   ceiling on this container, it was making the photo row unable to grow
   tall in the first place — see `.night-strip` below, which scrolls
   SIDEWAYS instead of wrapping into more rows. */
.bench-detail { display: grid; gap: 8px; margin-top: 10px; }
/* Buttons that used to be full-width rows of their own — Report for the
   venue, and anything that joins it — share one row now. `flex-wrap` is the
   fallback for a phone too narrow for both at once, not the normal case. */
.bench-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.bench-actions > * { flex: 1 1 auto; }
/* ONE ROW OF PHOTOS, SCROLLED SIDEWAYS RATHER THAN WRAPPED. Fixed width per
   photo so the row has a fixed height whatever the count — the one part of
   the old layout with no natural ceiling now has one, structurally. */
.night-strip { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 2px; }
.night-strip .cphoto { flex: 0 0 130px; }
/* ---- the diary
   Coming up sits ABOVE the past nights: one is a thing you can still act on,
   the other is a record. A row is a date, a place and a way to say you are not
   doing it — nothing else, because this is read at a glance rather than
   worked through. */
.diary-list { display: grid; gap: 8px; margin-top: 12px; }
.diary-row {
  display: grid; grid-template-columns: auto 1fr auto; gap: 12px; align-items: center;
  border: 1px solid var(--panel-line); border-radius: var(--r-card);
  background: var(--panel); padding: 10px 12px;
}
/* min-content is a grid child's default minimum, which is how one long venue
   name drags a page sideways on a 320px phone. Bitten five times now. */
.diary-row > * { min-width: 0; }
.d-when { text-align: center; }
.d-when b { display: block; }
/* A one-off is worth marking BECAUSE the rest are automatic: everything else
   on this list came out of a usual night, so the tag says "somebody typed
   this" rather than "this one is special". */
.diary-row.is-booked { border-color: rgba(255, 210, 63, 0.35); }
.d-tag {
  font-size: var(--fs-tag); font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--gold); margin-left: 6px; white-space: nowrap;
}
/* A night in the "coming up" list is a thing you READ, and the whole row picks
   its date — which lands you in the day panel, where the actions are. It was
   the same card with an Invoice it and a Not on this week on every one of ten
   rows: a wall of red, which is the one thing the host asked for explicitly to
   be avoided, on a list where nothing is wrong. */
.diary-row.is-pick {
  width: 100%; font: inherit; color: var(--ink); text-align: left; cursor: pointer;
}
.diary-row.is-pick:hover { border-color: var(--ink-dim); background: rgba(255, 255, 255, 0.07); }
/* A written-off night: shown on its own date so "Not on this week" is a thing
   somebody can see they did, and undo. Quiet rather than red — nothing is
   wrong, it is a night you are not doing. */
.diary-row.is-off { opacity: 0.8; }
.d-tag.off { color: var(--ink-dim); }
/* What time it starts, when somebody typed one. A residency has none, so this
   is only ever on a night that was booked in. */
.d-time {
  font-size: var(--fs-tag); font-weight: 800; margin-left: 6px; white-space: nowrap;
  padding: 2px 7px; border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.08); color: var(--ink);
}
/* The venue box is a select AND a free-text input; one cell holds both, and
   only one of them is ever visible. */
.d-venue { display: grid; min-width: 0; }
/* ---- A NIGHT IS A DIARY ROW: the date as a block, the venue as the headline.
 *
 * Chosen from four layouts on 15 August 2026. The old row was five spans in a
 * flat sequence, which wrapped into a ragged block at any narrow width and
 * buried the venue third in a run of dim text. A grid puts the date where the
 * eye can run down it, gives the venue a line of its own, and pins the badges
 * right so they line up between rows instead of floating wherever the text
 * happened to end.
 *
 * `min-width: 0` on the middle cell, because a long pack list is one
 * unbreakable run of text and a grid child will not shrink below its content
 * without it — the eighth instance of that fault in this app, fixed here
 * before it could be the ninth. */
.gig-head {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 0 14px; align-items: center; width: 100%; text-align: left;
  padding: 10px 14px; cursor: pointer; color: inherit; font: inherit;
  background: var(--panel); border: 1px solid var(--panel-line);
  border-radius: var(--r-card);
}
.gig-head:hover { border-color: rgba(255, 255, 255, 0.22); }
.gig-cal {
  display: grid; place-items: center; width: 54px; padding: 6px 0; line-height: 1;
  border-radius: var(--r-field); background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--panel-line);
}
.gig-cal b { font-size: 1.25rem; font-weight: 900; color: var(--ink); }
.gig-cal span { font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-dim); }
.gig-mid { min-width: 0; }
/* The venue, and it may be long — one line with an ellipsis rather than a wrap
   that pushes the row taller than its neighbours. */
.gig-mid b {
  display: block; font-weight: 800; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gig-mid .tiny { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gig-badges { display: flex; align-items: center; gap: 8px; }
/* On a phone the badges drop under the text rather than squeezing the venue
   into nothing — the date block stays, because it is what you are scanning. */
@media (max-width: 560px) {
  .gig-head { grid-template-columns: auto minmax(0, 1fr); }
  .gig-badges { grid-column: 2; margin-top: 4px; }
}
/* Said for a screen reader only: the date block is two fragments visually. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* PUTTING A NIGHT ON THE PUBLIC GALLERY — under the photographs it would
   publish, separated by a rule, because it is a different kind of act from
   everything above it: the rest of this panel is about your own record of the
   night, and this one puts other people's faces on the open web. */
.gig-gallery {
  margin-top: 14px; padding-top: 12px;
  border-top: 1px solid var(--panel-line);
  display: flex; flex-direction: column; align-items: flex-start; gap: 8px;
}
.gig-gal-note { color: var(--ink-dim); }
/* Green, like everything else in this app that means live and good — and the
   link is the proof, so it goes with it rather than being announced somewhere
   else. */
.gig-gal-live { color: var(--good); }
.gig-gal-live a { color: inherit; }

.venue-add { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
.venue-add .venue-new { flex: 1 1 220px; min-width: 0; }

/* The demo prize code on My account. Big enough to be scanned off a laptop
   screen across a pub table, which is the whole use. */
/* The venue picker above the demo QR. A label wrapping its own select, so the
   words are the target too — this is read once a year and pressed with a thumb
   in whatever room the pitch is happening in. */
/* THE LEAGUE BAND on the projector, at the final scores. Everything here is
   in vh because this screen is read from the back of a dark room, and the band
   sits between the draw and the comeback slide — see `leagueBand()`. Gold on
   first place only, which is what gold means everywhere in this app. */
.lgb { margin-top: 2.2vh; padding-top: 1.6vh; border-top: 1px solid rgba(255,255,255,0.10); }
.lgb-label {
  font-size: 1.5vh; font-weight: 900; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink-dim); margin-bottom: 1vh;
}
.lgb-rows { display: flex; flex-direction: column; gap: 0.5vh; max-width: 46vw; margin: 0 auto; }
.lgb-row { display: grid; grid-template-columns: 3vh 1fr auto; align-items: baseline; gap: 1.4vh; }
.lgb-pos { font-size: 1.8vh; color: var(--ink-dim); font-variant-numeric: tabular-nums; text-align: right; }
.lgb-name { font-size: 2.2vh; font-weight: 800; text-align: left; }
.lgb-pts { font-size: 2.2vh; font-weight: 900; font-variant-numeric: tabular-nums; }
.lgb-top .lgb-name, .lgb-top .lgb-pts { color: var(--gold); }
.lgb-note { margin-top: 1vh; font-size: 1.4vh; color: var(--ink-dim); }

/* THE END-OF-NIGHT STEP on the running panel, once a game has finished. It is
   the one moment the app asks for something rather than offering it, so it is
   a band across the panel rather than another button in the row — and it takes
   the account's own gradient on its action, because at that point there is
   nothing else on the panel competing to be pressed. */
.night-done {
  display: flex; gap: 14px; align-items: center; justify-content: space-between;
  flex-wrap: wrap; margin-top: 14px; padding-top: 14px;
  border-top: 1px solid var(--panel-line);
}
.night-done .go { flex: 0 0 auto; }

/* WHAT A SAVE DID TO THE INTRO CUES. Stays until dismissed rather than
   flashing: it is the one thing worth reading before a gig, and a toast is no
   way to check four track matches. Neutral rather than green or red — it is
   information about a save that already succeeded, not a verdict on it. */
.cue-said {
  position: relative; margin: 0 0 14px; padding: 12px 34px 12px 14px;
  border: 1px solid var(--panel-line); border-radius: var(--r-card);
  background: var(--panel); font-size: var(--fs-body); line-height: 1.5;
}
.cue-said .tiny { color: var(--ink-dim); }
.cue-said-x {
  position: absolute; top: 6px; right: 8px; background: none; border: 0;
  color: var(--ink-dim); font-size: 20px; line-height: 1; cursor: pointer;
}

/* THE QUIZ LEAGUE on a venue card. Deliberately plain: this is a table of
   numbers somebody reads, and the card it sits on is already carrying a
   headcount, a sparkline and a prize list. The only colour is on first place,
   and it is GOLD because that is what gold means everywhere in this app —
   winning — which is the one place the trophy colour genuinely belongs. */
.lg-table { width: 100%; border-collapse: collapse; margin-top: 8px; }
.lg-table th {
  font-size: var(--fs-tag); font-weight: 800; text-align: left;
  color: var(--ink-dim); padding: 0 6px 4px; border-bottom: 1px solid var(--panel-line);
}
.lg-table td { padding: 5px 6px; border-bottom: 1px solid var(--panel-line); }
.lg-table tr:last-child td { border-bottom: 0; }
.lg-pos { width: 22px; color: var(--ink-dim); font-variant-numeric: tabular-nums; }
.lg-name { font-weight: 700; }
/* Right-aligned and tabular so the columns line up as numbers rather than as
   text — the whole point of a table is comparing down a column. */
.lg-played, .lg-pts { width: 34px; text-align: right; font-variant-numeric: tabular-nums; }
/* WIDE ENOUGH FOR "9 (6)" ON ONE LINE. At 34px the counted-nights note wrapped
   under the number it qualifies and made those rows a line taller than the
   rest — a table of teams reads down the points column, and rows of two
   different heights break that scan for a footnote. */
.lg-played { color: var(--ink-dim); width: 52px; white-space: nowrap; }
.lg-table .lg-top .lg-name, .lg-table .lg-top .lg-pts b { color: var(--gold); }
.lg-note { margin-top: 8px; }
.demo-at { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; max-width: 320px; }
.demo-prize { display: flex; gap: 16px; align-items: center; margin-top: 12px; flex-wrap: wrap; }
.demo-qr {
  width: 160px; height: 160px; flex: 0 0 auto;
  background: #fff; border-radius: var(--r-card); padding: 8px;
}
.demo-side { display: grid; gap: 8px; min-width: 0; }
.demo-side .tiny { word-break: break-all; }

/* The picture on a read-through. Small, because the page is a list you scan —
   and a link to the full size, because judging a likeness at 120px is not
   judging it. */
.pv-pic { margin: 8px 0 10px; }
.pv-pic-img {
  width: 120px; height: 120px; object-fit: cover;
  border-radius: var(--r-field); border: 1px solid var(--panel-line); display: block;
}
.pv-pic-note { margin-top: 6px; color: var(--gold); }
.pv-pic-none {
  font-size: var(--fs-note); color: var(--ink-dim);
  padding: 10px 12px; border-radius: var(--r-field);
  border: 1px dashed var(--panel-line);
}

/* What tonight is playing for, said on the pack card before Launch is pressed.
   The night that ended with no voucher ended that way because nothing here
   said there were no prizes — and nothing on screen looks exactly like not
   having looked. */
.prize-line {
  grid-column: 1 / -1;
  font-size: var(--fs-note); color: var(--ink-dim);
  padding: 8px 10px; border-radius: var(--r-field);
  background: rgba(255, 210, 63, 0.08);
  border: 1px solid rgba(255, 210, 63, 0.3);
}
.prize-line.none { background: none; border-style: dashed; border-color: var(--panel-line); }
.prize-line .prize-place { color: var(--gold); font-weight: 900; }
.prize-line .prize-place.p2 { color: var(--metal-silver); }
.prize-line .prize-place.p3 { color: var(--metal-bronze); }


/* ---- the countdown before kick-off
   The code goes up at ten to and the quiz starts at nine. This fills the gap:
   it gives a room something to settle to, and it is the thing somebody points
   at when they turn round and tell the next table. That word-of-mouth IS the
   mechanism — it never has to reach every phone, only enough of them.

   NEVER a wall-clock time and it never counts UP. At zero it says "any moment
   now" and stops, because a countdown is a promise and the app must not be the
   thing that tells sixty people the host is late. */
.lobby-count {
  margin-top: 3.2vh;
  display: inline-block;
  padding: 1.4vh 2.4vh;
  border: 1px solid var(--panel-line); border-radius: var(--r-pill);
  background: var(--panel);
  font-size: 3.4vh; font-weight: 800; color: var(--ink-dim);
  letter-spacing: 0.01em;
}
.lobby-count b { color: var(--ink); font-size: 1.35em; font-weight: 900; }
/* Under a minute is when it is worth watching, so that is when it takes the
   account's own colour. Not before — a number that shouted for ten solid
   minutes would be the loudest thing on a screen whose job is the join code. */
.lobby-count.soon b, .lobby-count.now {
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.lobby-count.now { font-size: 4.2vh; font-weight: 900; }

/* The phone's copy of the same number. Quieter, because it is six inches from
   somebody's face rather than across a room — but it is the whole reason the
   tab stays in the foreground, so it is not tucked away either. */
.wait-count { font-size: var(--fs-lead); font-weight: 800; color: var(--ink-dim); }
.wait-count b { color: var(--ink); font-size: 1.3em; font-weight: 900; }
.wait-count.soon b, .wait-count.now {
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}

/* Setting it: three presets and Off, pressed with one thumb while holding a
   microphone. No number to type — five, ten or fifteen minutes covers every
   version of "we are nearly ready", and pressing one again pushes it back. */
.starts-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.starts-row .tiny { margin-right: 2px; }


/* ---- an invoice somebody has not paid
   The row says how late it is and grows a Chase it, and neither is loud: an
   unpaid invoice is usually somebody forgetting rather than somebody refusing,
   and the words in the draft are mild for the same reason. A quizmaster wants
   the money AND the booking next month. */
.inv-row.is-late { border-color: rgba(255, 77, 94, 0.3); }
.inv-late { color: var(--bad); font-weight: 700; }


/* ---- the tiers, side by side
   A comparison table rather than three panels of switches. The question it
   answers is "what would Silver actually get me", and that is a question you
   answer by looking ACROSS rather than by reading three sections and holding
   them in your head.

   Only what DIFFERS gets a row. Everything else is Bronze — both games, every
   round type, your own packs, photos, past gigs, invoicing, the diary — so
   listing them would be eleven rows of three identical ticks, which reads as
   Bronze being thin when the whole point is that it is the entire app. One
   sentence underneath says so instead. */
.cmp-scroll { overflow-x: auto; margin-top: 12px; scrollbar-width: thin; }
.cmp { width: 100%; border-collapse: collapse; font-size: var(--fs-note); }
.cmp th, .cmp td { padding: 10px 12px; text-align: center; vertical-align: middle; }
.cmp thead th { border-bottom: 1px solid var(--panel-line); }
/* The row labels stay left — they are words, not values, and centring them
   makes the table read as a grid of equals rather than as rows of a thing. */
.cmp tbody th { text-align: left; font-weight: 800; color: var(--ink); white-space: nowrap; }
.cmp tbody tr + tr th, .cmp tbody tr + tr td { border-top: 1px solid rgba(255, 255, 255, 0.06); }
.cmp-tier { display: block; font-weight: 900; letter-spacing: 0.06em; text-transform: uppercase; font-size: var(--fs-tag); }
.cmp-price { display: block; font-weight: 900; font-size: var(--fs-ctl); color: var(--ink); }
/* YOURS in green, the app's own "you have this" colour — the same word and the
   same green the tier panels used, so nothing had to be relearned. */
.cmp-yours {
  display: inline-block; margin-top: 4px;
  font-size: var(--fs-tag); font-weight: 900; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--good);
}
/* The column you are on is lifted, so "where am I" is answered before any of
   the rows are read. */
.cmp th.mine, .cmp td.mine { background: rgba(255, 255, 255, 0.045); }
/* `1.15em` of a 13px parent is 14.95px, which is on no ladder step and is
   the same decision-taken-by-accident the ladder exists to stop. One rung
   up, stated. */
.cmp-yes { color: var(--good); font-weight: 900; font-size: var(--fs-body); }
.cmp-no { color: var(--ink-dim); opacity: 0.5; }
.cmp-soon { color: var(--gold); opacity: 0.8; font-size: var(--fs-tag); font-weight: 800; text-transform: uppercase; letter-spacing: 0.06em; }
.cmp-tier.tier-bronze { color: var(--metal-bronze); }
.cmp-tier.tier-silver { color: var(--metal-silver); }
.cmp-tier.tier-gold { color: var(--metal-gold); }


/* ---- venues, collapsed
   Same problem as the pack grid and the same shape solves it: fifteen
   residencies each carrying a dropdown and three prize boxes is a wall you
   scroll rather than a list you use. Closed, a venue is its name and the two
   facts you scan for — which night it has you, and what it puts up. */
.venue-tools { margin-top: 12px; }
.venue-search { width: 100%; max-width: 280px; }
.venue-card.shut { padding: 10px 14px; }
.venue-name {
  background: none; border: none; padding: 0; text-align: left;
  color: var(--ink); font-family: inherit; font-size: var(--fs-ctl); font-weight: 800;
  cursor: pointer; min-width: 0;
}
/* The caret says it opens — drawn from a border rather than a glyph, the same
   reason the pack titles' is. */
.venue-name::after {
  content: ''; display: inline-block; margin-left: 8px;
  border: 4px solid transparent; border-top-color: currentColor;
  transform: translateY(2px); opacity: 0.5;
}
.venue-card.open .venue-name::after { transform: translateY(-2px) rotate(180deg); }
/* BEHIND THE CONSOLE DOOR IT IS A LABEL, NOT A CONTROL — there is nothing to
   open there, so the caret and the pointer both go. The card itself is still
   the drag source, which is what `cursor: grab` above already says. */
.venue-name.flat { cursor: inherit; }
.venue-name.flat::after { content: none; }
/* ON THE CONSOLE DOOR THE WHOLE CARD IS THE CONTROL — tapping it picks the
   venue for tonight. `grab` above says it can be dragged, which is true and is
   the fast way on a laptop; this says it can also just be pressed, which is
   the only way on a phone. */
.venue-card.shut:hover { border-color: rgba(255, 255, 255, 0.24); background: rgba(255, 255, 255, 0.06); }
.venue-name:hover { color: #fff; }
.venue-name:hover::after { opacity: 1; }
.venue-card.open { border-color: rgba(255, 255, 255, 0.22); background: rgba(255, 255, 255, 0.07); }
/* The one line a shut card keeps. Dim, because it is what you scan past on
   the way to the one you want rather than something to read. */
.venue-gist { margin-top: 2px; }

/* ---- SHOWS — a whole evening, built in advance.

   A GRID RATHER THAN A LIST, and the same six-across shape as the pack shelf:
   a show is dragged onto Tonight, and a drag needs to SEE both ends. The
   minimum is wider than a pack card because a show carries a sentence — the
   venue, the round count and whether it is online — where a pack carries a
   title. */
.show-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 12px;
}
.show-card {
  position: relative;
  border: 1px solid var(--panel-line);
  border-radius: var(--r-card);
  background: rgba(255, 255, 255, 0.04);
  padding: 12px 14px;
  cursor: grab;
  transition: border-color 160ms, background 160ms;
}
.show-card:hover { border-color: rgba(255, 255, 255, 0.24); background: rgba(255, 255, 255, 0.07); }
.show-card.is-dragging { opacity: 0.45; }
.show-top { display: flex; align-items: baseline; gap: 8px; justify-content: space-between; }
.show-name { font-weight: 800; font-size: var(--fs-ctl); color: var(--ink); min-width: 0; }
/* WHICH GAME, in the same two colours the pack shelf uses for the same fact —
   quiz green, bingo purple. A pill rather than a word in the sentence below,
   because it is the one thing you filter by at a glance. */
.show-kind {
  flex: 0 0 auto;
  padding: 2px 8px;
  border-radius: var(--r-pill);
  font-size: var(--fs-mini);
  font-weight: 900;
}
.show-kind.is-quiz { background: rgba(47, 224, 122, 0.16); color: #2fe07a; }
.show-kind.is-bingo { background: rgba(168, 85, 247, 0.18); color: #c084fc; }
/* MORE THAN ONE PART — a quiz and the bingo after it. Neither colour, because
   claiming either would be naming half the evening; the parts below say which
   is which, in the same two colours. */
.show-kind.is-both { background: rgba(255, 255, 255, 0.12); color: var(--ink); }

/* WHAT IT PLAYS, IN ORDER. A numbered list, because the order is the point —
   a show is an evening, and the room gets these one after another. */
.show-parts { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 4px; }
.show-part { display: flex; align-items: center; gap: 8px; min-width: 0; }
.show-part-name {
  font-size: var(--fs-note);
  color: var(--ink);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.show-part-n { flex: 0 0 auto; color: var(--ink-dim); }
/* The kind, as a dot rather than a second pill. Six pills on one card is a
   wall; a dot in the same two colours says the same thing at a glance. */
.show-dot { flex: 0 0 auto; width: 8px; height: 8px; border-radius: var(--r-pill); }
.show-dot.is-quiz { background: #2fe07a; }
.show-dot.is-bingo { background: #a855f7; }
.show-gist { margin-top: 8px; }

/* THE PARTS EDITOR — Workshop only. A select per part rather than a drag,
   because HTML5 drag never fires on touch and half this console is driven
   from a phone. */
.parts-edit { margin-top: 12px; border-top: 1px solid var(--panel-line); padding-top: 12px; }
.parts-list { list-style: none; margin: 0 0 10px; padding: 0; display: grid; gap: 8px; }
.parts-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.parts-row .parts-pick { flex: 1 1 160px; min-width: 0; }
.parts-moves { display: flex; gap: 4px; flex: 0 0 auto; }
.parts-moves .minor { padding: 6px 10px; }
.parts-note { flex: 1 1 100%; color: var(--ink-dim); }
.parts-add { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }

/* THEN — the next part of tonight's show, under the Launch button. It LOADS
   rather than launches: the bingo starts when the quiz has finished and the
   prizes are handed out, and only the person on the mic knows when that is. */
.lb-then {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--panel-line);
}
.lb-then-what { color: var(--ink); font-weight: 700; }
/* SOMETHING IN IT HAS GONE. Red on the border, like a broken pack — the one
   place red is right here, because it is the app saying this will not run
   rather than editorialising about somebody's work. */
.show-card.broken { border-color: rgba(255, 77, 94, 0.5); }
.show-wrong { margin-top: 6px; color: var(--bad); }
.show-tools { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }

/* KEEPING TONIGHT AS A SHOW, at the bottom of the settings. A row rather than
   a lone button so the reason it is off can sit beside it — present and
   inert, never absent. */
/*
 * IN THE HEAD NOW, so no top margin and no row of its own — it used to sit
 * between the tiles and Launch with 16px above it, in the one strip of the
 * bar that answered no question at all.
 */
.set-keep { display: flex; align-items: center; gap: 10px; min-width: 0; }
/*
 * THE DOORS DIAL IN THE HEAD — the one gap with no tile of its own, because
 * it happens before the first pack rather than after anything.
 *
 * It is the SAME control every tile carries, which is what stopped the doors
 * and "after round 1" reading as two different objects doing one job. Only
 * its place is set here: a word beside it rather than a bare symbol, since
 * every other dial is sitting on the pack it acts on and this one has nothing
 * underneath it to say which gap it means.
 */
.lb-doors { display: flex; align-items: center; flex: 0 0 auto; gap: 8px; }
.lb-doors[hidden] { display: none; }
/* Its own line, always — see the markup note. Hidden entirely when the night
   has prizes, which is the ordinary case and costs nothing. */
/* A BLOCK OF ITS OWN, OUTSIDE THE HEAD — see the note in `console-tonight.js`.
   `flex: 1 0 100%` is kept harmlessly for any other flex parent, but the line
   it gets no longer depends on a parent choosing to wrap. */
.lb-warn-slot { flex: 1 0 100%; min-width: 0; margin-top: 10px; }
.lb-warn-slot[hidden] { display: none; }
.lb-warn-slot .lb-say { margin: 0; }
/* Open, a chip wears the account edge whether or not it has been changed —
   it is the one whose panel is on screen, and "you are looking at this" has
   to outrank "you have touched this". */
/* In the head the dial is not inside a tile, so it stops being absolute. */
.lb-doors .gap-dial { position: static; }
.set-keep .set-save { white-space: nowrap; }


/* ---- what you heard back, where you asked
   The request is made under the shop because that is where the want arrives;
   the reply used to appear only on the Help tab, so you asked in one place and
   heard back in another. Green, because it is an answer rather than a warning
   — and above the box, because it is about the thing you already asked. */
.ask-heard {
  margin: 10px 0 12px; padding: 10px 12px;
  border: 1px solid rgba(47, 224, 122, 0.3); border-radius: var(--r-card);
  background: rgba(47, 224, 122, 0.07);
}
.ask-heard-q { margin-bottom: 4px; }
.ask-heard-a { font-size: var(--fs-note); color: var(--ink); white-space: pre-wrap; }

/* The divider under tonight's venue's slides in the advert picker. Dimmer than
   a venue name, because it is a boundary rather than a thing you are choosing
   — the same reason a fourth place is a line and not a card on the podium. */
.ad-set-rest {
  margin-top: 18px; padding-top: 12px;
  border-top: 1px solid var(--panel-line);
  color: var(--ink-dim); opacity: 0.75;
}

/* The venue's logo control, on its card. Laid out like a feature row rather
   than like a field, because it is a thing you HAVE rather than a thing you
   type — the same shape as the On/Off rows on My Account. */
.venue-logo-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  justify-content: space-between;
  margin: 12px 0; padding: 12px;
  border: 1px solid var(--panel-line); border-radius: var(--r-card);
}
.venue-logo-what { min-width: 0; flex: 1 1 220px; }
.venue-logo-side { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
/* The white plate is the same treatment the voucher gives it, so what you see
   here is what the winner sees rather than a preview that flatters it. */
.venue-logo-pic {
  height: 40px; max-width: 120px; object-fit: contain;
  background: #fff; border-radius: var(--r-field); padding: 4px;
}
/* A `<label>` wrapping a hidden file input, so it needs the button's own
   cursor — nothing else makes it look pressable. */
.venue-logo-pick { cursor: pointer; }

/* THEIR ADVERTS, on a venue's card. A summary and a door — never a second
   editor, because two places that edit one thing is how they end up
   disagreeing. Dimmer than the prizes above it: this is a fact about the
   venue, where the prize boxes are a thing you are filling in. */
.venue-ads {
  margin: 12px 0; padding: 12px;
  border: 1px solid var(--panel-line); border-radius: var(--r-card);
}
.venue-ads-tag { font-size: var(--fs-tag); font-weight: 800; color: var(--ink-dim); }
.venue-ads-line { margin-top: 3px; }
.venue-ads .minor { margin-top: 10px; }

/* A slide's picture in the advert editor. The same white plate the venue logo
   gets, for the same reason — half of what a venue sends is a transparent PNG
   and half is a photo, and one of those is invisible on a dark sheet. */
.ad-pic-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; }
.ad-pic {
  height: 56px; max-width: 160px; object-fit: contain;
  background: #fff; border-radius: var(--r-field); padding: 4px;
}
.ad-pic-pick { cursor: pointer; }

/* THE PRICE IS GREEN, because money is green everywhere in this app — the same
   language as "paying" on an account and "makes something" on a button. A pill
   rather than a line so it reads at a glance on a grid of nine cards, and it
   is deliberately NOT a control: the card already has Buy underneath it, and
   two things to press for one job is the fault this file keeps recording. */
.shop-price {
  /* `align-self` as well as `inline-block`: the card is a flex column, which
     stretches its children — so the pill came out full width and read as a
     button rather than a tag. */
  display: inline-block; align-self: flex-start; margin-top: 6px;
  padding: 3px 10px; border-radius: var(--r-pill);
  font-size: var(--fs-note); font-weight: 900;
  color: var(--good); border: 1px solid rgba(63, 219, 133, 0.45);
  background: rgba(63, 219, 133, 0.12);
}
/* The way to the tiers, beside the shop heading. Quiet: it is the alternative,
   not the offer, and the packs below are what the section is for. */

/* A prize row can be taken away now that there is no fixed three. The cross is
   quiet and only earns attention on hover: it sits beside a box somebody is
   typing in, and a permanently loud red on every row would read as a warning
   about the prize rather than a way to remove it. */
.reward-off {
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: var(--r-field);
  background: none; border: 1px solid var(--panel-line); color: var(--ink-dim);
  font: inherit; font-size: var(--fs-note); cursor: pointer;
}
.reward-off:hover { color: var(--bad); border-color: rgba(255, 77, 94, 0.4); }
.v-reward-add { margin-top: 4px; }

/* WHAT IS ON THE SCREEN, AND WHAT IS NEXT — on the console's running panel.
   Two rows, tagged rather than headed: the tag is the same width on both so
   the two labels line up and the pair reads as one thing you scan, not two
   facts you read. Next is dimmer than Now, because one of them is true and the
   other is a prediction. */
.now-next { margin-top: 10px; display: grid; gap: 6px; }
.nn-row { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.nn-tag {
  flex: 0 0 auto; min-width: 38px;
  font-size: var(--fs-tag); font-weight: 900; color: var(--ink-dim);
}
.nn-what { min-width: 0; font-size: var(--fs-note); }
.nn-row + .nn-row { opacity: 0.72; }

/* A diary row's two buttons share a cell, so "Invoice it" and "Not on" cannot
   push the row's layout about as one appears and the other does not. */
.d-acts { display: flex; gap: 8px; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; }

/* THE MONTH ON THE LEFT, WHAT YOU DO ABOUT A DATE ON THE RIGHT.
   Asked for directly — "perhaps the calendar needs to sit off to the left in
   the section to allow room for the right to populate?" — and it fixes a real
   fault as well as looking better: full width, the month was seven columns of
   mostly-empty boxes, and the panel answering a date sat UNDER it, so picking
   the 23rd pushed the answer below the fold.
   Two columns only from 900px. Below that the month itself is the tight thing
   — seven columns of a 320px phone is 38px a day — so the panel goes under it
   and the picker scrolls it into view. */
.cal-wrap { display: grid; gap: 14px; margin-top: 12px; }
/* min-content is a grid child's default minimum: the month's seven columns and
   a long venue name would otherwise drag the console sideways. Bitten eight
   times now, so it goes on with the layout rather than after it. */
.cal-wrap > * { min-width: 0; }
@media (min-width: 900px) {
  .cal-wrap { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); align-items: start; gap: 18px; }
  /* The month STAYS while the panel beside it scrolls. A month is five rows
     and the list beside it is however many nights you have — so the left
     column ran out a long way above the right one and the bottom half of the
     section was a black void with a list floating in it. Sticky, the calendar
     is still there to click when you reach the end of the list, which is
     precisely when you want it.
     It has to clear BOTH pinned things — the topbar and the tab bar under it
     — or the top row of the month slides underneath the tabs. Written off the
     same token, so the day the topbar changes height this follows. */
  .cal-left { position: sticky; top: calc(var(--topbar-h) + 58px); }
}
/* The panel is a card so it reads as the answer to the month beside it rather
   than as more page. */
.cal-side {
  display: grid; gap: 10px; align-content: start;
  border: 1px solid var(--panel-line); border-radius: var(--r-card);
  background: var(--panel); padding: 12px;
}
.cal-side > * { min-width: 0; }
.cal-side-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  flex-wrap: wrap;
}
.cal-side-head b { font-size: var(--fs-lead); }
/* Book a quiz is the one thing this panel is FOR, so it takes the width. The
   invoice button under it is an ordinary control and stays one. */
.cal-add { width: 100%; }
.cal-book { display: grid; gap: 10px; }
.cal-f { display: grid; gap: 4px; }
.cal-f > span { font-size: var(--fs-tag); font-weight: 800; color: var(--ink-dim); }
.cal-f input, .cal-f select { min-width: 0; max-width: 100%; }
.cal-book-acts { display: flex; gap: 8px; }
.cal-book-acts .d-go { flex: 1 1 auto; }

/* THE MONTH. A grid you click into, because "book a quiz on the 23rd" needs a
   23rd to click — which a list of nights that already exist cannot offer. */
.cal-head { display: flex; align-items: center; gap: 8px; margin: 0 0 8px; }
.cal-month { flex: 1 1 auto; min-width: 0; font-size: var(--fs-lead); }
.cal-days, .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; }
.cal-day {
  font-size: var(--fs-tag); font-weight: 800; color: var(--ink-dim);
  text-align: center; padding-bottom: 2px;
}
.cal-cell {
  min-height: 68px; padding: 6px; text-align: left; cursor: pointer;
  border: 1px solid var(--panel-line); border-radius: var(--r-field);
  background: rgba(255, 255, 255, 0.03); color: var(--ink);
  font: inherit; display: block; overflow: hidden;
}
/* A day with a night on it is raised, so the month reads at a glance as "where
   the work is" rather than as a wall of identical boxes. */
.cal-cell.has { background: linear-gradient(180deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.03)); }
.cal-cell.is-today { border-color: var(--ink-dim); }
.cal-cell.open { border-color: var(--hot); background: rgba(255, 46, 136, 0.10); }
/* An empty leading cell is not a button and must not look like one. */
.cal-cell.is-blank { background: none; border-color: transparent; cursor: default; }
.cal-num { display: block; font-size: var(--fs-tag); font-weight: 900; color: var(--ink-dim); }
.cal-dot {
  display: block; margin-top: 3px; padding: 2px 6px; border-radius: var(--r-pill);
  font-size: var(--fs-tag); font-weight: 800;
  background: rgba(255, 46, 136, 0.18);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A one-off is a different colour from a residency, because one is a habit and
   the other is a decision somebody made. */
.cal-dot.one { background: rgba(75, 216, 255, 0.18); }
.cal-dot.more { background: none; color: var(--ink-dim); }
/* A diary row inside the right-hand panel has about a third of the width it
   has full-page, so the date stops being a column and sits above the venue.
   Scoped to the panel rather than to a screen width: the same row is wide on
   a phone, where the panel takes the whole page. */
.cal-side .diary-row { grid-template-columns: 1fr; gap: 4px; }
.cal-side .d-when { display: flex; gap: 6px; align-items: baseline; text-align: left; }
.cal-side .d-when b { display: inline; }
.cal-side .d-acts { justify-content: flex-start; }
@media (max-width: 560px) {
  /* Below this the seven columns give each day about 38px, which is a tap
     target but not a place for a venue name — so the pills go and the number
     carries a dot instead. */
  .cal-cell { min-height: 44px; }
  .cal-dot { font-size: 0; padding: 3px; margin-top: 4px; border-radius: var(--r-pill); }
  .cal-dot.more { display: none; }
}

/* The calendar subscription address. Under the month rather than above it: the
   month is what you came for and this is set up once and then never again. */
.cal-feed { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--panel-line); }
.cal-feed-row { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
/* `min-width: 0` or the URL refuses to go below its own content and takes the
   page sideways — the same trap, for the eighth time. */
.cal-url { flex: 1 1 260px; min-width: 0; font-size: var(--fs-note); }

/* The referral link, same shape as the calendar's subscription address —
   a readonly field and a Copy button, the pattern this app already uses
   anywhere a link has to be handed to somebody else rather than clicked. */
.acct-refer-row { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.acct-refer-url { flex: 1 1 260px; min-width: 0; font-size: var(--fs-note); }
.acct-refer-credit { margin-top: 10px; }
.acct-refer-credit b { color: var(--good); }

/* ---- THE LOBBY GAME, on a phone.
   `touch-action: none` is the line that matters and it is not cosmetic: this
   game is played by TAPPING the maze, and rapid tapping on a canvas in mobile
   Safari triggers double-tap-to-zoom — so a frantic player would zoom the page
   instead of moving. It also stops the page scrolling under a dragged finger.
   `user-select` for the same reason: a fast double tap selects text otherwise
   and the phone offers to copy it mid-game. */
.arcade { display: grid; gap: 8px; justify-items: center; }
.arcade-box { width: 100%; display: grid; gap: 6px; justify-items: center; }
.arcade-canvas {
  width: 100%; max-width: 420px; aspect-ratio: 1 / 1;
  touch-action: none;
  -webkit-user-select: none; user-select: none;
  -webkit-tap-highlight-color: transparent;
  border-radius: var(--r-card);
  background: rgba(0, 0, 0, 0.35);
}
/*
 * RALLY'S COURT IS 2:3, NOT SQUARE — and the height is what has to be bounded
 * rather than the width. `aspect-ratio` alone with `width: 100%` makes the
 * canvas one and a half screens tall on a phone, which puts "Send a photo" and
 * the rest of the waiting screen below the fold on the one screen this app
 * most wants used. Capping `max-width` bounds the height through the ratio,
 * which is the only way round that keeps the court its real shape — letterbox
 * it instead and every bat, ball and pip shrinks with it.
 */
.arcade-canvas.tall { aspect-ratio: 2 / 3; max-width: min(420px, 42vh); }

/*
 * THE SOUND TOGGLE, in the hint row UNDER the game.
 *
 * Not on the canvas: every one of these games draws its score in a top corner,
 * and a tap that missed the button by a few pixels would land on the game —
 * which on Quick Draw is a shot, and the shot could be the sheriff. Small and
 * quiet, because it is a preference rather than a thing to press.
 */
/*
 * WHAT THE ROOM IS LOOKING AT, on the host's own screen.
 *
 * Deliberately the quietest thing in the bar — it is a glance, not a control,
 * and it sits directly under a status line that is already doing the loud job.
 * Dim, and it wraps rather than pushing the clock off a 320px phone, which is
 * the one width this bar has been broken at before.
 */
.phones-are {
  color: var(--ink-dim);
  opacity: 0.85;
  line-height: 1.3;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* ---- PICKING ONE, when the quizmaster left it to the room.
   A grid rather than a row of pills: five names have to be readable at 390px
   and a wrapping row of pills puts one game on a line of its own, which reads
   as the odd one out rather than as the fifth choice. `auto-fit` with a floor
   means two or five both come out even. */
.arcade-pick {
  display: grid; gap: 6px; width: 100%;
  grid-template-columns: repeat(auto-fit, minmax(64px, 1fr));
}
.arcade-pick-one {
  display: grid; gap: 4px; justify-items: center;
  /* 44px is the touch floor and this is the one control in a game that is not
     the game — a miss here opens the wrong one and costs the tap. */
  min-height: 44px; padding: 8px 4px;
  border-radius: var(--r-field);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.04));
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: var(--ink); font: inherit; cursor: pointer;
}
.arcade-pick-one:active { transform: translateY(1px); }
/* The one being played wears the account's own colour on its edge — the same
   "you are here" the console's ordinary buttons use, and deliberately not a
   fill: a filled chip in a row of five would read as the thing to press. */
.arcade-pick-one.on { border-color: var(--hot); background: rgba(255, 255, 255, 0.14); }
.arcade-pick-icon { font-size: var(--fs-sub); line-height: 1; }
.arcade-pick-name { font-size: var(--fs-mini); line-height: 1.15; text-align: center; color: var(--ink-dim); }
.arcade-pick-one.on .arcade-pick-name { color: var(--ink); }

.arcade-under {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  /* The hint must be allowed to shrink, or on a 320px phone it pushes the
     button off the side — the min-content trap, for the ninth time. */
  min-width: 0;
}
.arcade-under .arcade-said { min-width: 0; }
.arcade-sound {
  flex: 0 0 auto;
  /* 44px, THE TOUCH FLOOR. It was 34x34 — in every game, at every viewport, and
     it is the ONLY control on a game screen. The button keeps its own size
     rather than gaining one from a rule higher up the sheet: this one is
     8,600 lines lower and would win anyway, which is the trap. */
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: rgba(0, 0, 0, 0.45);
  color: var(--ink);
  font-size: var(--fs-note);
  line-height: 1;
  cursor: pointer;
  /* Or a tap meant for the button steers the game underneath it — which on
     Quick Draw is a shot, and a shot could be the sheriff. */
  touch-action: manipulation;
}
.arcade-sound:hover { background: rgba(0, 0, 0, 0.62); }
.arcade-said { color: var(--ink-dim); }

/* The countdown over the game. `pointer-events: none` or it would swallow the
   taps that steer, in the ten seconds somebody is trying hardest. */
.arcade-stage { position: relative; width: 100%; display: grid; justify-items: center; }
/* A CLOCK IN THE CORNER, not a banner across the game. Quiet while there is
   time — it is information you glance at, and a loud one for two minutes is
   the furniture this app keeps refusing. Top RIGHT: a thumb tapping the maze
   comes from below, so the one corner a hand is never over is that one. */
.arcade-going {
  position: absolute; top: 8px; right: 8px; pointer-events: none;
  min-width: 34px; text-align: center;
  font-weight: 900; font-size: var(--fs-note); font-variant-numeric: tabular-nums;
  color: var(--ink-dim); background: rgba(0, 0, 0, 0.55);
  padding: 4px 8px; border-radius: var(--r-pill);
  transition: transform 200ms, background 200ms, color 200ms;
}
/* Under ten seconds it goes loud — the same element rather than a second
   thing appearing, so there is nothing new to notice at the moment somebody
   is least able to notice anything. */
.arcade-going.soon {
  color: #12000c; background: linear-gradient(120deg, var(--hot), var(--hot-2));
  font-size: var(--fs-sub);
  padding: 6px 12px;
  /* A BEAT A SECOND, which is what makes it obvious without making it big.
     Growing it further would start covering the maze — the one thing a clock
     over a game must not do — where a pulse is caught by the corner of the eye
     from a thing that has not moved for two minutes. It is tied to no
     JavaScript: a one-second animation beside a one-second countdown reads as
     the clock ticking whether or not the two are exactly in step. */
  animation: arcadeTick 1s ease-in-out infinite;
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.25), 0 8px 22px rgba(255, 46, 136, 0.5);
}
@keyframes arcadeTick {
  0%, 100% { transform: scale(1.08); }
  18%      { transform: scale(1.3); }
  40%      { transform: scale(1.08); }
}
/* Somebody who has asked for less motion still gets the size, the colour and
   the glow — everything except the beat. */
@media (prefers-reduced-motion: reduce) {
  .arcade-going.soon { animation: none; transform: scale(1.15); }
}

/* The one line that says a night was never launched — see `nowPlaying()`.
   Full ink rather than dim: it is the difference between the pack sitting on
   the projector and a night that is actually on, and it was misread once. */
.running-note { color: var(--ink); margin: -2px 0 8px; max-width: 62ch; }

/* ---- WHILE YOU WAIT — the two things there are to do, as a menu.
   Rows rather than a row of buttons: each one has a name and a line saying
   what happens next, which is the house style everywhere else and is worth
   more here than anywhere, because both of these end up on the big screen and
   nobody would guess that from an icon. */
.wait-menu { display: grid; gap: 8px; }
.wait-item {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 12px 14px; border-radius: var(--r-card);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.04));
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-bottom: 2px solid var(--hot);
  color: var(--ink); font: inherit; text-align: left; cursor: pointer;
}
.wait-item:active { transform: translateY(1px); }
.wait-item-icon { font-size: var(--fs-sub); line-height: 1; flex: 0 0 auto; }
.wait-item-what { display: grid; min-width: 0; }
.wait-item-what .tiny { color: var(--ink-dim); }
.arcade .wait-item { width: 100%; }

/* ------------------------------------------------------------- the gallery
 * The one page in this app a CUSTOMER sees on their own, days later, with no
 * account and no context. So it is quiet, wide-open and says what it is —
 * nothing to drive, nothing to sign into, and the photos are the page.
 */
body.gallery { padding: 0; }
.gal-wrap { max-width: 1100px; margin: 0 auto; padding: 24px 16px 64px; }
.gal-head { text-align: center; margin-bottom: 24px; }
.gal-brand { display: inline-flex; align-items: center; gap: 8px; text-decoration: none; margin-bottom: 12px; }
.gal-head h1 { margin: 0 0 4px; font-size: var(--fs-title); }
.gal-sub { color: var(--ink-dim); margin: 0; font-size: var(--fs-note); }
.gal-empty { text-align: center; padding: 32px 8px; }

/* One line for the quizmaster, never for a customer — see gallery.js.

   IT USED TO SAY "quiet rather than a warning: it is a state", AND THAT WAS
   THE FAULT — the state it reports is that nobody else can see this page.

   A WARNING IS THE STATED EXCEPTION TO THE SHORT-LABEL RULE, and this one had
   been written in `--fs-note` at `--ink-dim` — the two weakest tools this app
   has — above a wall of large photographs. It is the one line that separates
   "published" from "only you can see this", read by the person least able to
   tell the difference, so it gets a surface of its own and full-strength ink.

   NOT RED: nothing has gone wrong. It is a state, not a mistake — the same
   reasoning the publish control's own line follows. */
.gal-drafts {
  max-width: 640px; margin: 0 auto 26px; padding: 12px 16px;
  border-radius: var(--r-card);
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: rgba(255, 255, 255, 0.06);
  text-align: center; font-size: var(--fs-body); color: var(--ink-dim);
  line-height: 1.5;
}
.gal-drafts b { color: var(--ink); display: block; margin-bottom: 2px; }
.gal-drafts a { color: var(--ink); }

/* AND WHICH ONES — the server has sent `live` per night since the day this
   page was written and the card never drew it, so two drafts sat beside three
   live nights looking identical. The banner says how many; only the card can
   say WHICH. */
.gal-card { position: relative; }
.gal-card.is-draft .gal-fan { opacity: 0.75; }
.gal-draft-tag {
  position: absolute; top: 8px; right: 8px;
  padding: 3px 9px; border-radius: var(--r-pill);
  font-size: var(--fs-note); font-weight: 800; letter-spacing: 0.04em;
  color: #17131b; background: rgba(255, 255, 255, 0.88);
}

/* SEEING IT AS A VISITOR — the control that settles "is it really up?".

   TWO STRENGTHS FOR ONE CONTROL, and the difference is which way it points.
   Offering the check is a quiet aside on a page that is working normally;
   BEING in visitor mode is a state you must not forget you are in, so it gets
   the account's own colour on its edge and full ink. Nobody should scroll a
   deliberately reduced page and mistake it for the real one.

   NOT RED. Standing the preview down is not a fault and not destructive — it
   is the app telling the truth on request. */
.gal-asvisitor {
  max-width: 640px; margin: 0 auto 22px; padding: 10px 16px;
  border-radius: var(--r-card);
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-left: 3px solid var(--hot);
  background: rgba(255, 255, 255, 0.06);
  text-align: center; font-size: var(--fs-body); color: var(--ink);
  line-height: 1.5;
}
.gal-asvisitor b { display: block; margin-bottom: 2px; }
.gal-asvisitor a { color: var(--ink); }
/* The offer, on a page with nothing wrong with it: present, and quiet. */
.gal-asvisitor.is-quiet {
  border-left-color: transparent; background: none; border-color: transparent;
  margin-bottom: 10px; padding: 2px 16px;
  font-size: var(--fs-note); color: var(--ink-dim);
}

/* ---- THE NUMBERS, AND SOMEWHERE TO PRESS — the pitch above the proof.

   OFF THE TYPE LADDER, DELIBERATELY, AND WITH PRECEDENT. The ladder tops out
   at 22px because it is sized for a console; a figure on a page a landlord
   opens once is display type, and the sales page already `clamp()`s its own
   for the same reason. It is the only off-ladder size on this page.

   THE FIGURE TAKES `--hot`, NEVER `--gold`. Gold means winning, everywhere in
   this app; a headcount is not a trophy. `--hot` is the one colour that
   follows the quizmaster's own scheme, which is right on the page that is
   most theirs.

   THE LABEL IS UNDER THE NUMBER AND STAYS SENTENCE-CASE — capitals are for
   emphasis, and the number is already the emphasis.

   `auto-fit` with a `minmax` floor rather than a fixed four across: at 320px
   four figures side by side are 70px each and the labels wrap to three lines,
   and one figure stretched across a laptop reads as a mistake. The floor is
   120px because 96 gave a phone THREE across and left "2 venues" alone on a
   second row — measured, not chosen; 120 folds it to a two-by-two and still
   fits all four on a laptop. */
.gal-about {
  max-width: 640px; margin: 0 auto 30px; padding: 18px 16px;
  border-radius: var(--r-card);
  border: 1px solid var(--panel-line);
  background: var(--panel);
  text-align: center;
}
.gal-figs {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 16px 10px;
}
.gal-fig { display: block; }
.gal-fig b {
  display: block; color: var(--hot); font-weight: 900; line-height: 1.05;
  font-size: clamp(26px, 6vw, 34px); font-variant-numeric: tabular-nums;
}
.gal-fig span {
  display: block; margin-top: 2px;
  font-size: var(--fs-note); color: var(--ink-dim); line-height: 1.3;
}
/* The growth line is a sentence about the room, so it is body copy rather than
   a fifth figure — four numbers and a fifth phrased differently would read as
   one of them having gone wrong. */
.gal-grew {
  margin: 14px 0 0; font-size: var(--fs-body); color: var(--ink-dim);
}
.gal-grew b { color: var(--ink); font-variant-numeric: tabular-nums; }
/* AND THE ONE THING TO PRESS. Ordinary, never the gradient: this page has no
   filled gradient button on it, and a page selling one thing should not have
   two controls competing to be it. */
.gal-book {
  margin: 16px 0 0; padding-top: 14px; border-top: 1px solid var(--panel-line);
  display: flex; flex-wrap: wrap; gap: 10px 14px;
  align-items: center; justify-content: center;
  font-size: var(--fs-body); color: var(--ink);
}
.gal-book-link {
  flex: 0 0 auto;
  padding: 10px 18px; min-height: 44px; display: inline-flex; align-items: center;
  border-radius: var(--r-field);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-bottom: 2px solid var(--hot);
  background: rgba(255, 255, 255, 0.06);
  color: var(--ink); text-decoration: none; font-weight: 800;
}
.gal-book-link:hover { border-color: var(--hot); }

/* ---- NIGHTS AS CARDS, GROUPED BY PUB.

   The pub is a heading and its nights are cards under it. `auto-fill` rather
   than a fixed count, so one night is not stretched across a laptop and twenty
   are not postage stamps on a phone — the same rule the photo grid follows. */
.gal-groups { display: grid; gap: 34px; }
.gal-group-name {
  margin: 0 0 12px; font-size: var(--fs-head); font-weight: 800; color: var(--ink);
}
.gal-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 18px; }
.gal-card {
  display: block; text-decoration: none; color: var(--ink);
  background: var(--panel); border: 1px solid var(--panel-line);
  border-radius: var(--r-card); padding: 16px 14px 14px;
}
.gal-card b { display: block; text-align: center; margin-top: 14px; font-size: var(--fs-note); }
.gal-card:hover { border-color: var(--hot); }

/* ---- THE FANNED PILE — snapshots dropped on a table.

   THE TILT IS FIXED PER POSITION, never random: a random angle would change on
   every visit and make a page of twelve cards restless. Three set angles read
   as a pile and stay where they were.

   The pictures are absolutely positioned inside a fixed-ratio box so the card
   height never depends on how many photographs came back — a night with one
   photo and a night with three are the same shape on the shelf. The white
   border is the polaroid edge the projector's big photo already wears. */
.gal-fan { display: block; position: relative; aspect-ratio: 4 / 3; }
.gal-fan img {
  position: absolute; left: 50%; top: 50%; width: 56%; aspect-ratio: 1 / 1;
  object-fit: cover; border-radius: 4px;
  border: 4px solid rgba(255, 255, 255, 0.92);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.45);
  background: var(--panel-2, var(--panel));
}
/* The FIRST one sits on top — it is the one a human pinned, if they pinned any,
   so the choice actually shows. They are printed in reverse for that. */
/* Spread far enough that the back two are PHOTOGRAPHS rather than slivers —
   at a tighter offset each showed about a sixth of itself, which reads as an
   edge rather than as three pictures. `spill` is measured in the driver: the
   pile has to stay inside its own card at every card width. */
.gal-fan-0 { transform: translate(-50%, -46%) rotate(-4deg); z-index: 3; }
.gal-fan-1 { transform: translate(-78%, -56%) rotate(-13deg); z-index: 2; }
.gal-fan-2 { transform: translate(-22%, -52%) rotate(9deg); z-index: 1; }
.gal-fan.is-empty { background: var(--panel-line); border-radius: var(--r-field); opacity: 0.4; }
@media (prefers-reduced-motion: reduce) { .gal-card { transition: none; } }

/* NO DRAFT MARKER HERE — see the note in `gallery.js`. Whether a night is
   published is the console's to say, on the control that sets it. */

/* The photos. `auto-fill` rather than a fixed column count, so one photo is
   not stretched across a laptop and forty are not postage stamps on a phone. */
.gal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; }
/* A BUTTON NOW, because it is pressed — reset to the figure it replaced, so
   the wall looks exactly as it did and only the behaviour is new. */
.gal-shot {
  margin: 0; padding: 0; border: 0; background: none; display: block;
  width: 100%; cursor: zoom-in;
}
.gal-shot img {
  width: 100%; aspect-ratio: 1 / 1; object-fit: cover; display: block;
  border-radius: var(--r-card); background: var(--panel);
  transition: transform 140ms ease;
}
.gal-shot:hover img, .gal-shot:focus-visible img { transform: scale(1.02); }
@media (prefers-reduced-motion: reduce) { .gal-shot img { transition: none; } }

/* ---- ONE PHOTOGRAPH, FILLING THE SCREEN.

   `fixed`, not `absolute`: this covers the WINDOW rather than a panel, and the
   page behind it genuinely scrolls — the console's own version sits inside a
   fixed bay and is `absolute` for that reason.

   `contain`, because this is the moment somebody is actually looking at it. A
   crop is right on a wall of thumbnails and wrong here.

   The whole picture is the way back, so the cursor says so and there is no
   button in a corner competing with the photograph. Opaque, or the thumbnails
   show through the letterboxing. */
.gal-big {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; padding: 12px;
  border: 0; background: rgba(6, 4, 12, 0.94); cursor: zoom-out;
  /* Under the notch and above the home bar on a phone. */
  padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right))
           max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
}
.gal-big img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: var(--r-card); }
/* THE PAGE BEHIND IT DOES NOT SCROLL — a flick meant for the photograph would
   otherwise move the wall somebody is about to come back to, which is the
   scroll the overlay exists to preserve. */
body.gal-zoomed { overflow: hidden; }

/* ---- AN ARROW EITHER SIDE OF THE DATE — forwards and backwards in time.

   Three columns with the sides on `auto` and the middle taking the rest, so
   the date stays centred whether there are neighbours on both sides, one, or
   neither: at an end of the run the cell is still there, empty, and the
   heading does not jump sideways.

   A 44px round target, quiet until it is wanted — the arrows must not compete
   with the date they sit beside, which is the thing somebody came to read. */
.gal-title-row {
  /* THEY HUG THE DATE, they do not sit at the edges of the page. Asked for as
     *"arrows either side of the date"* — a `1fr` middle column pushed them to
     the far sides of an 1100px wrap, which is a page control rather than
     something on the heading. `auto` columns, centred as a group, and the
     middle one may shrink so a long date still wraps rather than overflows. */
  display: grid; grid-template-columns: auto minmax(0, auto) auto;
  justify-content: center; align-items: center; gap: 4px; margin: 0 0 4px;
}
.gal-title-row h1 { margin: 0; min-width: 0; }
.gal-arrow, .gal-arrow-gap {
  width: 44px; height: 44px; border-radius: var(--r-pill); flex: 0 0 auto;
}
.gal-arrow {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 30px; line-height: 1; text-decoration: none;
  color: var(--ink-dim); border: 1px solid transparent;
}
.gal-arrow:hover, .gal-arrow:focus-visible {
  color: var(--ink); border-color: var(--panel-line);
}
@media (max-width: 560px) {
  /* The date wraps to two lines on a phone and the arrows stay on its row. */
  .gal-title-row { gap: 0; }
  .gal-arrow, .gal-arrow-gap { width: 36px; }
  .gal-arrow { font-size: 26px; }
}

.gal-back { text-align: center; margin-top: 24px; }

/* ---- THE TIER BUCKETS on the owner page — what each rung includes.

   Three columns from 860px and a stack below it. A column per tier rather than
   one long list, because the question being asked is "what does Silver get
   you" and a list answers that only by being read end to end. */
.tier-buckets {
  display: grid;
  grid-template-columns: 1fr;
  gap: 14px;
  margin-top: 14px;
}
@media (min-width: 860px) {
  .tier-buckets { grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: start; }
}
.tier-bucket {
  border: 1px solid var(--panel-line);
  border-radius: var(--r-card);
  padding: 12px;
  background: rgba(255, 255, 255, 0.03);
  min-width: 0;
}
.tier-bucket-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 8px; margin-bottom: 8px;
}
/* THE METALS STAY THE METALS. A quizmaster's own colours change the app's
   personality, never what a rung MEANS — and here the colour is the meaning,
   the same reason the B/S/G rungs on the hat switch keep theirs. */
.tier-name { font-size: var(--fs-ctl); font-weight: 900; }
.tier-name.tier-bronze { color: var(--metal-bronze); }
.tier-name.tier-silver { color: var(--metal-silver); }
.tier-name.tier-gold { color: var(--metal-gold); }
.tier-row {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 8px 0; border-top: 1px solid var(--panel-line);
}
.tier-row-what { flex: 1 1 auto; min-width: 0; display: grid; gap: 1px; }
/* Not red: an unbuilt feature is not a fault, it is a thing that is coming —
   and it is still part of what the rung is sold as, which is why it is shown
   at all rather than filtered out. */
.tier-soon { color: var(--gold); }
.tier-row .tier-move { flex: 0 0 auto; max-width: 42%; }
.tier-said { margin-top: 12px; color: var(--good); min-height: 1.2em; }

/* ---- THE WORKSHOP BENCH — the door's own section, mirroring Tonight.

   Same place, same weight, same classes underneath (`.lb-tile`, `.lb-drop`),
   because two doors that behave the same way are one thing to learn. What
   differs is what it holds: Tonight composes an evening out of several packs,
   the bench works on ONE, so it has one slot rather than six. */
.bench .bench-where { font-size: var(--fs-ctl); font-weight: 900; color: var(--ink); }
/* WHOSE PACK IS ON THE BENCH - "Yours" or "Quizporium", on the head line so it
   survives the fold.

   A PILL, because that is what this app's one-word facts already wear - PAID,
   BRONZE, YOURS - and the badge exception in the capitals rule is written for
   exactly this shape: a four-letter pill is something you recognise rather than
   a word you read.

   YOURS TAKES THE ACCOUNT'S OWN COLOUR AND QUIZPORIUM STAYS NEUTRAL. Never
   gold: gold is the trophy colour here and means first place, which is not what
   authorship is. `--hot` is the one colour that follows a quizmaster's own
   scheme, and "this one is mine" is precisely the thing worth saying in it. */
.bench-whose {
  flex: 0 0 auto;
  font-size: 0.62rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  font-weight: 800;
  color: var(--ink-dim);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: var(--r-pill);
  padding: 1px 8px;
  white-space: nowrap;
}
/* ---- THE WORKSHOP BENCH FILLS ITS BAY, once the rail came off it.

   The bay is 362px by rule and the bench came to 142 of it — two thirds empty,
   on the door whose bay the host had already called *"a lot of wasted space"*.
   Taking the rail away made that worse, so the bench takes the height it is
   sitting in: `.bench-body` is now a direct child of the panel, which is
   already a flex column of a fixed height, so ONE `flex` line does it.

   THE PACK IS A FULL-HEIGHT SQUARE POSTER, which is what the pack shelf
   already does on a 146px card. It is the thing you are working on; a bench
   that draws it at 92px and leaves 220px of nothing under it has the emphasis
   exactly backwards.

   AND THE BUTTONS STAY THEIR OWN SIZE — three across for what you do WITH the
   pack, four across for what you do TO it, never one 800px-wide button. *"A
   button's width should say how big the action is"* is the rule that put them
   beside the slot in the first place, and a full-width bay is where it is
   easiest to break.

   BELOW 900px NONE OF THIS APPLIES: no frame, no `--bay-h`, and `.bench-body`
   is one column already. */
/* THE POSTER'S COLUMN IS A NUMBER, NOT `auto` — and `auto` is why three packs
   came out three different sizes.

   Reported off the live console: *"the packs are different sizes on the
   bench?"* An `auto` grid track sizes to its item's MAX-CONTENT width, and the
   item here is a pack tile with the pack's NAME inside it — so
   "2000-2010 Pop, Indie and Rock" made a wide column, `aspect-ratio` turned
   that width into a tall square, and "2006" got a small one. The poster was
   being sized by how long the title is.

   TWO NUMBERS, BECAUSE `--bay-h` IS TWO. Each is the row the poster sits in:
   the bay, less the panel's own head and padding, less the pack-actions row
   and the gap above it. They move with `--bay-h` and nothing else. */
:root { --bench-poster: 297px; }
@media (min-width: 1150px) { :root { --bench-poster: 234px; } }
@media (min-width: 900px) {
  .bench.launchbar > .bench-body {
    flex: 1 1 auto;
    min-height: 0;
    grid-template-columns: var(--bench-poster) minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) auto;
    align-items: stretch;
    gap: 12px 18px;
  }
  .bench.launchbar > .bench-body > .bench-slot {
    grid-row: 1 / -1;
    display: flex;
    align-items: flex-start;
    justify-content: flex-start;
  }
  /* THE SQUARE COMES FROM THE COLUMN'S WIDTH, NEVER FROM THE ROW'S HEIGHT.
     Height-first is the obvious way round and it is the one that broke: the
     row is `1fr` of a body that only has a definite height while the FIXED
     FRAME is on — and the frame needs a tall window as well as a wide one, so
     on a 900px-high laptop `height: 100%` fell back to auto and the tile went
     back to being sized by its own text. Width-first has one answer at every
     size. */
  .bench.launchbar > .bench-body > .bench-slot .lb-tile,
  .bench.launchbar > .bench-body > .bench-slot .lb-drop {
    width: 100%;
    height: auto;
    min-height: 0;
    aspect-ratio: 1;
    flex: 0 0 auto;
  }
  .bench.launchbar > .bench-body > .bench-do {
    grid-column: 2;
    grid-row: 1;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-rows: auto minmax(0, 1fr);
    align-content: stretch;
    gap: 10px;
    min-height: 0;
  }
  /* THE NOTE TAKES THE MIDDLE, rather than sitting under the buttons with a
     void beneath it. The two button rows are anchored top and bottom — what
     you do next, and what you do to the file — so the one thing left is the
     sentence, and the middle is where it stops being a gap. */
  .bench.launchbar > .bench-body > .bench-do p {
    grid-column: 1 / -1;
    margin: 0;
    max-width: 70ch;
    align-self: center;
  }
  .bench.launchbar > .bench-body > .bench-pack-actions { grid-column: 2; grid-row: 2; }
  /* AS MANY COLUMNS AS THERE ARE BUTTONS — a fixed four leaves a hole on the
     packs that have no picture round, which is most of them. `grid-auto-flow:
     column` lets the row size itself to what is actually in it. */
  .bench.launchbar > .bench-body > .bench-pack-actions .pack-actions {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    gap: 10px;
  }
  .console .bench.launchbar > .bench-body > .bench-do .go,
  .console .bench.launchbar > .bench-body > .bench-do .minor,
  .bench.launchbar > .bench-body > .bench-pack-actions .pack-actions > * { width: 100%; }
}

/* AN IMAGE ROUND'S CARD — a pack poster with a glyph in the corner.

   The glyph goes in a CORNER rather than beside the name, where it pushed the
   title along and read as part of it. Top LEFT, because the era word this app
   already draws on a pack card is a full-bleed watermark and the top right is
   where a pin and a Yours badge live: one corner per job, as everywhere else.

   `pointer-events: none` — the whole card is the tap target here, and a glyph
   that swallowed the press would be an inert patch in the middle of it. */
.round-card-glyph {
  position: absolute;
  top: 8px; left: 10px;
  font-size: 15px;
  line-height: 1;
  pointer-events: none;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6));
}

/* DRAW THIS ROUND, FROM THE SHELF THE ROUND IS ON.

   The one control on a round card that is not "place this" — so it sits in
   the BOTTOM RIGHT, the corner nothing else on a shut card uses, well clear
   of the glyph at the top left and the title's own fade at the foot. It is an
   ordinary outlined button rather than a filled one: it opens the panel, and
   the press that actually spends money is inside that panel and is the green
   one. Same split, same words, same colours as the pack card's own Pictures
   button — this is that button, on the shelf where the round lives.

   `border-color`, never the `border` shorthand: five sightings in this sheet.
   `z-index` because the card's title fade is a `::before` above the content,
   and a visible button that swallows nothing is the fault this file already
   records twice. */
.round-card .round-pics {
  position: absolute;
  right: 8px; bottom: 8px;
  z-index: 2;
  padding: 4px 8px;
  border-radius: var(--r-field);
  border: 1px solid var(--panel-line);
  background: rgba(0, 0, 0, 0.55);
  color: var(--ink);
  font: inherit; font-size: 11px; line-height: 1.1;
  cursor: pointer;
}
.round-card .round-pics:hover { background: rgba(0, 0, 0, 0.75); }
.round-card .round-pics.wants {
  border-color: var(--good);
  background: rgba(47, 224, 122, 0.18);
}
.round-card .round-pics.is-open { border-color: var(--hot); }
/* THE PANEL HANGS UNDER THE GRID, AND SAYS WHOSE IT IS. Two of these open at
   once would be two spending controls with nothing naming the pack either
   belongs to, which is why only one is ever open. */
.pics-for { margin: 10px 0 4px; color: var(--ink-dim); }
.bench-whose.is-mine {
  color: var(--hot);
  border-color: color-mix(in srgb, var(--hot) 55%, transparent);
}
/* AND THE GREEN GOES HOLLOW ON SOMEBODY ELSE'S PACK - the launch button's own
   treatment, for the same reason it was chosen there: an outline reads as "not
   this one" where a dimmed FILL reads as broken. Filled green means *make
   something*, and a Quizporium pack is not a thing this account makes.

   `.console .bench .bench-go` sets `border: none`, so this needs the same three
   classes plus the state to reach it - the shorthand-beats-longhand trap
   recorded in the project notes, met head on rather than by luck. */
.console .bench .bench-go:disabled {
  cursor: default;
  background: none;
  border: 2px dashed var(--panel-line);
  color: var(--ink-dim);
  filter: none;
  opacity: 1;
}
/* THE SLOT ON THE LEFT, WHAT YOU DO WITH IT ON THE RIGHT.

   Reported as *"I don't want a tiny button taking up a whole row"*, and that
   was exactly the fault: one small green button stretched the width of a panel,
   under a drop zone half its width, with the rest of the row empty. **A
   button's width should say how big the action is** — Launch is full width
   because it is THE decision on that screen; "write a new one" is not.

   So the panel is two columns: the thing on the bench, and the things you can
   do to it, side by side. Each button is its own width, and the row is full
   because there is something in all of it. */
.bench-body {
  display: grid;
  grid-template-columns: 190px minmax(0, 1fr);
  gap: 14px 18px;
  align-items: start;
}
@media (max-width: 700px) {
  /* One column on a phone, slot first — the same order, stacked. */
  .bench-body { grid-template-columns: 1fr; }
}
.bench-slot { min-width: 0; }
/* The drop zone matches the tile it will be replaced by, so nothing moves when
   a pack lands on it — a panel that changes height on a drop is the reflow
   this file already refuses on the pack shelf. */
.bench-slot .lb-drop, .bench-slot .lb-tile { min-height: 92px; }
/* AND THE BENCH'S DROP ZONE LOOKS LIKE ONE. It carries `lb-drop` without
   `lb-tile`, and the dashed border lives on `.lb-tile.lb-drop` — so the empty
   bench has always been a bare "+" and a line of text with no box round it,
   which reads as a caption rather than as somewhere to let go of a pack. It
   was 92px tall and easy to miss; at the full height of the bay it is not. */
.bench-slot .lb-drop {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center;
  gap: 4px;
  border: 2px dashed var(--panel-line);
  border-radius: var(--r-card);
  color: var(--ink-dim);
  font-weight: 800;
  cursor: pointer;
}
.bench.drop-here .bench-slot .lb-drop { border-color: var(--hot); color: var(--ink); }
/* ---- A ROW OF BUTTONS SHARES A HEIGHT AND A TYPE SIZE; THE ROLE SHOWS IN
   THE FILL, NEVER IN THE SIZE.

   Reported as *"this UI is ugly, we need standards across buttons, fonts"* —
   and the bench was the clearest case of it: three buttons, three heights,
   three type sizes and three treatments, sitting inches apart. A big green
   one, then two smaller ones with a pink underline.

   **The size was doing the job the fill is supposed to do.** The GUI rules
   already say what a control looks like is decided by what it DOES: filled
   green means *make something*, outlined means *ordinary*. Nothing in them
   says a primary is TALLER — so making it taller as well says the same thing
   twice, and says it in the one channel that also has to keep a row tidy. */
.bench-do {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 8px; min-width: 0;
  /* THE ACTIONS COLUMN RESERVES ITS FULL HEIGHT, or the panel grows when
     something lands on it. Measured: the empty state came out 18px shorter
     than the loaded one, because loading adds a second button and drops the
     explanation to one line — so dropping a pack nudged everything below the
     bench down. Small, and exactly the reflow this file refuses on the pack
     shelf: a panel that changes height under your hand is one you cannot aim
     at twice. The slot beside it already matches at 92px for the same reason.

     THE NUMBER IS MEASURED, NOT GUESSED. It was 106 first — a guess at what
     two buttons and a line of text come to — and the loaded state measured
     110, so the panel still moved, by 4px instead of 18. A reserved height
     that is nearly right is still a panel that moves. Re-measured after the
     buttons became one wrapping row of equal height. */
  min-height: 92px;
}
.bench-do p { margin: 2px 0 0; max-width: 46ch; }
/* THE PACK ACTIONS SPAN BOTH COLUMNS, under the slot and the Edit/Read row —
   its own third grid row, at the same gap the two above it already keep. */
.bench-pack-actions { grid-column: 1 / -1; }
/* GREEN, NOT THE ACCOUNT GRADIENT, and sized to its words. One filled gradient
   per screen means "the night", and there is no night behind these doors —
   making something is the green role. So the Console has exactly one Launch and
   the Workshop exactly one green primary, and neither can be mistaken for the
   other in a dark pub. */
/* EVERY BUTTON ON A BENCH, whatever its role: one height, one type size, one
   radius. Written once for both `.go` and `.minor` so the two cannot drift —
   which is exactly how they came to differ in the first place. */
/* `.console` IS ON THE FRONT DELIBERATELY. Without it this loses to
   `.console a.minor` — same class count, but that one also names an element,
   which outranks it — so the ordinary buttons kept their own padding and
   font-size and the row stayed ragged. The third time this trap has been
   recorded here, after the shorthand `border` on `.lb-tile` and
   `input[type="search"]` beating `.pack-search`. */
.console .bench-do .go, .console .bench-do .minor {
  height: 38px;
  padding: 0 16px;
  display: inline-flex; align-items: center; justify-content: center;
  font-family: inherit;
  font-size: var(--fs-ctl);
  font-weight: 800;
  border-radius: var(--r-field);
  text-decoration: none;
  white-space: nowrap;
  margin: 0;
  width: auto;
}
/* MAKE SOMETHING IS FILLED GREEN — the role as the GUI rules define it. It was
   outlined, which is the *ordinary* treatment, so the one button that commits
   to something looked like the two that do not. Fill means commitment. */
.console .bench .bench-go {
  background: linear-gradient(120deg, #2fe07a, #17b45c);
  border: none;
  color: #05210f;
}
.console .bench .bench-go:hover { filter: brightness(1.06); }
/* The date beside the heading is the same size as it, a rung dimmer — two
   different type sizes on one line is what made the head read as a caption
   stuck onto a title. */
.bench .lb-shut-what { font-size: var(--fs-ctl); font-weight: 700; }
.bench.drop-here { border-color: var(--good); }

/* A night dragged up to the Post gig bench. Same affordance as a pack card. */
.gig[draggable="true"] { cursor: grab; }
.gig.is-dragging { opacity: 0.45; }

/* ================================================================ LANDING
   /home and /signup — the shop window. Persuade, not Operate: the visitor
   here is deciding whether to get in touch, not driving a night, so the
   type runs bigger and the hero gets real weight. Same tokens as everywhere
   else in the app — colours, radii, the one gradient angle — so it reads as
   Quizporium rather than a separate marketing site bolted on. Scheme-aware
   for free: nothing here overrides --hot/--hot-2, so it wears whatever
   [data-scheme] the page has (the ordinary sunset default, since nobody has
   signed in yet). */
body.landing {
  margin: 0;
  background:
    radial-gradient(1200px 600px at 15% -10%, var(--glow-1) 0%, transparent 60%),
    radial-gradient(1200px 600px at 100% 0%, var(--glow-2) 0%, transparent 55%),
    var(--bg);
  color: var(--ink);
}
.ld-wrap { max-width: 1080px; margin: 0 auto; padding: 0 20px; }

.ld-top { padding: 20px 0; }
.ld-top-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.ld-top .brand { display: flex; align-items: center; gap: 10px; text-decoration: none; padding: 4px; }
.ld-top .brand-name { font-size: var(--fs-lead); font-weight: 900; color: var(--ink); }
.ld-nav { display: flex; align-items: center; gap: 14px; }
.ld-nav > a:first-child { color: var(--ink-dim); text-decoration: none; font-size: var(--fs-ctl); font-weight: 700; }
.ld-nav > a:first-child:hover { color: var(--ink); }
.ld-nav-cta { padding: 9px 18px; font-size: var(--fs-note); }

.ld-hero { padding: 40px 0 60px; }
.ld-hero-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 48px; align-items: center; }
@media (max-width: 860px) { .ld-hero-grid { grid-template-columns: 1fr; gap: 36px; } }
.ld-eyebrow {
  margin: 0 0 12px; font-size: var(--fs-tag); font-weight: 900; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--hot-2);
}
.ld-h1 {
  margin: 0 0 16px; font-size: clamp(32px, 5vw, 52px); font-weight: 900; line-height: 1.08;
  letter-spacing: -0.02em; text-wrap: balance;
}
.ld-lede { margin: 0 0 26px; font-size: var(--fs-field); line-height: 1.6; color: var(--ink-dim); max-width: 46ch; }
.ld-cta-row { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
/* THE ONE FILLED GRADIENT CONTROL, same as everywhere else in the app — "Get
   in touch" is this page's version of Launch. There is a second one in the
   header only because it is small and functions as a nav item, not a second
   commitment competing with it. */
.ld-cta {
  display: inline-block; padding: 15px 30px; font-size: var(--fs-field); font-weight: 900;
  text-decoration: none; border-radius: var(--r-field); border: none; cursor: pointer;
  background: linear-gradient(120deg, var(--hot), var(--hot-2)); color: #12000c;
}
.ld-cta:hover { filter: brightness(1.08); }
.ld-cta-secondary {
  color: var(--ink-dim); text-decoration: none; font-size: var(--fs-ctl); font-weight: 700;
  border-bottom: 1px dashed var(--panel-line); padding-bottom: 2px;
}
.ld-cta-secondary:hover { color: var(--ink); border-bottom-color: var(--hot); }
.ld-hero-note { margin: 14px 0 0; color: var(--ink-dim); }

/* The mock projector — drawn from the app's own tokens rather than a
   screenshot, so it never goes stale and never adds an asset to load. */
/* ---- THE SALES PAGE'S REAL SCREENS -----------------------------------------

   The hero used to be `.ld-mock` — a hand-drawn CSS impression of a question
   screen. It is real screenshots now, which is what the marketing entry asked
   for in the first place: *"screenshots or a thirty-second video, not prose"*.

   A drawn mock-up is the one thing on a sales page a buyer cannot check, and
   it was also slightly WRONG — four flat option chips and a bare clock, where
   the real screen has the countdown bar, the answered count and the round
   pills. Showing the actual thing is both more honest and better looking.

   `.ld-mock` is left in the sheet deliberately: nothing uses it today, and
   deleting a block on the way past is how a page nobody was looking at breaks.

   THE IMAGE CARRIES ITS OWN ASPECT via width/height on the tag, so the page
   does not jump about while the shots load on a pub's wifi. */
.ld-shot { margin: 0; }
.ld-shot img {
  display: block; width: 100%; height: auto;
  border-radius: var(--r-card);
  border: 1px solid var(--panel-line);
  /* A screenshot of a dark app on a dark page needs an edge, or it bleeds
     into the background and reads as part of the page rather than as a thing
     the app actually drew. */
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
}
.ld-shot figcaption {
  margin-top: 10px; color: var(--ink-dim); line-height: 1.5;
}
.ld-shot-hero figcaption { text-align: center; }

.ld-shots {
  display: grid; gap: 26px; margin-top: 26px;
  grid-template-columns: 1fr;
}
@media (min-width: 900px) {
  /* Two projector shots side by side and the phone beside them — the phone is
     narrow, so it gets its own column rather than being stretched to match. */
  .ld-shots { grid-template-columns: 1fr 1fr; align-items: start; }
  .ld-shot-phone { grid-column: 1 / -1; display: grid; gap: 18px;
    grid-template-columns: 260px 1fr; align-items: center; }
  .ld-shot-phone figcaption { margin-top: 0; font-size: var(--fs-body); }
}

/* ---- THE HALF THAT WINS THE NEXT BOOKING.

   This section is the whole reason the page works for two audiences at once —
   the host's own words: a quizmaster who runs on this software is a better
   option as a host. Everything in it is evidence a venue can be SHOWN, which
   is what a venue decides on, and having it is what a subscriber is buying. */
.ld-sell-grid {
  display: grid; gap: 18px; margin-top: 26px;
  grid-template-columns: 1fr;
}
@media (min-width: 700px) { .ld-sell-grid { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1050px) { .ld-sell-grid { grid-template-columns: repeat(3, 1fr); } }
.ld-sell-item {
  padding: 20px 20px 22px;
  border-radius: var(--r-card);
  border: 1px solid var(--panel-line);
  background: var(--panel);
  /* The account colour on the edge, never on the face — the same rule an
     ordinary button in the console follows, for the same reason: six of these
     in a grid should say whose app this is once, not six times. */
  border-bottom: 2px solid var(--hot);
}
.ld-sell-item h3 {
  margin: 0 0 8px; font-size: var(--fs-head); color: var(--ink);
}
.ld-sell-item p { margin: 0; color: var(--ink-dim); line-height: 1.55; }

/* A rung with no button is a price list; a rung with one is a shop. Silver
   keeps the filled gradient because it is the recommended one and there is
   still only ONE of those on the screen. */
.ld-tier-go { display: block; margin-top: 16px; text-align: center; }

.ld-mock { display: grid; gap: 14px; }
.ld-mock-screen {
  background: var(--bg-2); border: 1px solid var(--panel-line); border-radius: var(--r-card);
  overflow: hidden; box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
}
.ld-mock-bar { display: flex; gap: 6px; padding: 12px 14px; background: rgba(255, 255, 255, 0.04); }
.ld-mock-dot { width: 9px; height: 9px; border-radius: 50%; background: rgba(255, 255, 255, 0.18); }
.ld-mock-body { padding: 26px 22px 22px; }
.ld-mock-q { font-size: var(--fs-lead); font-weight: 800; line-height: 1.35; margin-bottom: 18px; }
.ld-mock-opts { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.ld-mock-opt {
  padding: 12px 14px; border-radius: var(--r-field); font-size: var(--fs-note); font-weight: 800;
  color: #12000c;
}
.ld-mock-opt.a { background: var(--a); }
.ld-mock-opt.b { background: var(--b); }
.ld-mock-opt.c { background: var(--c); }
.ld-mock-opt.d { background: var(--d); }
.ld-mock-clock {
  margin-top: 16px; font-size: var(--fs-tag); font-weight: 900; letter-spacing: 0.06em;
  color: var(--ink-dim); font-variant-numeric: tabular-nums;
}
.ld-mock-phones { display: flex; gap: 10px; justify-content: center; }
.ld-mock-phone {
  width: 34px; height: 58px; border-radius: 8px; background: var(--bg-2);
  border: 1px solid var(--panel-line);
}

.ld-strip { padding: 30px 0; border-top: 1px solid var(--panel-line); border-bottom: 1px solid var(--panel-line); }
.ld-strip-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; }
@media (max-width: 760px) { .ld-strip-row { grid-template-columns: 1fr; gap: 18px; } }
.ld-strip-row > div { font-size: var(--fs-body); line-height: 1.55; color: var(--ink-dim); }
.ld-strip-row strong { color: var(--ink); }

.ld-section { padding: 64px 0; }
.ld-h2 { margin: 0 0 8px; font-size: clamp(24px, 3.4vw, 32px); font-weight: 900; letter-spacing: -0.01em; }
.ld-section-lede { margin: 0 0 32px; color: var(--ink-dim); font-size: var(--fs-field); max-width: 56ch; }

.ld-tiers { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
@media (max-width: 760px) { .ld-tiers { grid-template-columns: 1fr; } }
.ld-tier {
  position: relative; background: var(--panel); border: 1px solid var(--panel-line);
  border-radius: var(--r-card); padding: 26px 22px;
}
.ld-tier-best { border-color: var(--hot); background: rgba(255, 255, 255, 0.08); }
.ld-tier-badge {
  position: absolute; top: -12px; left: 22px; background: linear-gradient(120deg, var(--hot), var(--hot-2));
  color: #12000c; font-size: var(--fs-tag); font-weight: 900; letter-spacing: 0.04em;
  text-transform: uppercase; padding: 4px 10px; border-radius: var(--r-pill);
}
.ld-tier-name { font-size: var(--fs-lead); font-weight: 900; margin-bottom: 6px; }
.ld-tier-price { font-size: 34px; font-weight: 900; letter-spacing: -0.01em; margin-bottom: 14px; }
.ld-tier-price span { font-size: var(--fs-note); font-weight: 700; color: var(--ink-dim); }
.ld-tier-blurb { color: var(--ink-dim); font-size: var(--fs-body); line-height: 1.55; }
.ld-tiers-note { margin: 20px 0 0; color: var(--ink-dim); }
.ld-tiers-note a { color: var(--hot-2); font-weight: 700; }

.ld-honest { background: rgba(255, 255, 255, 0.03); }
.ld-honest-list { margin: 0; padding: 0; list-style: none; display: grid; gap: 14px; max-width: 68ch; }
.ld-honest-list li {
  padding-left: 26px; position: relative; color: var(--ink-dim); font-size: var(--fs-body); line-height: 1.6;
}
.ld-honest-list li::before { content: '—'; position: absolute; left: 0; color: var(--ink-dim); }

.ld-faq { display: grid; gap: 10px; max-width: 68ch; }
.ld-faq-item {
  background: var(--panel); border: 1px solid var(--panel-line); border-radius: var(--r-field);
  padding: 4px 18px;
}
.ld-faq-item summary {
  padding: 14px 0; cursor: pointer; font-weight: 800; font-size: var(--fs-field); list-style: none;
}
.ld-faq-item summary::-webkit-details-marker { display: none; }
.ld-faq-item summary::after {
  content: ''; float: right; margin-top: 8px;
  border: 4px solid transparent; border-top-color: var(--ink-dim); border-bottom: 0;
}
.ld-faq-item[open] summary::after { transform: rotate(180deg); }
.ld-faq-item p { margin: 0 0 16px; color: var(--ink-dim); line-height: 1.6; }
.ld-faq-item a { color: var(--ink); }
/* THE SECTION HEADINGS THE FULL LIST GAINED. The sales page's handful are
   picked across sections and print none — see `faqHtml({ onlyHome })`. */
.faq-section {
  margin: 26px 0 10px; font-size: var(--fs-head); font-weight: 800; color: var(--ink);
}
.ld-faq > .faq-section:first-child { margin-top: 0; }

/* ---- THE SAME LIST INSIDE THE CONSOLE — open, because the Help tab is redrawn
   on every state push and a <details> would shut itself under a thumb.

   A QUESTION IS A HEADING AND AN ANSWER IS BODY COPY, which is this app's own
   way of telling them apart: full ink against dim, one rung up the ladder,
   heavy. Not capitals — those are for emphasis, and a question is a label. */
.faq-list { max-width: 68ch; }
.faq-list .faq-section { font-size: var(--fs-lead); margin-top: 22px; }
.faq-list .faq-section:first-child { margin-top: 14px; }
.faq-item { margin-top: 12px; }
.faq-item h4 {
  margin: 0 0 4px; font-size: var(--fs-body); font-weight: 800; color: var(--ink);
}
.faq-item p {
  margin: 0 0 8px; font-size: var(--fs-note); color: var(--ink-dim); line-height: 1.55;
}
.faq-item a { color: var(--ink); }

.ld-final { text-align: center; }
.ld-final-inner { display: flex; flex-direction: column; align-items: center; }
.ld-final .ld-section-lede { margin-left: auto; margin-right: auto; }

.ld-foot { padding: 26px 0 40px; border-top: 1px solid var(--panel-line); }
.ld-foot-row { display: flex; align-items: center; justify-content: space-between; color: var(--ink-dim); font-size: var(--fs-note); }
.ld-foot-row a { color: var(--ink-dim); }

/*
 * ---- /terms, /privacy, /refunds — plain prose, not a marketing page ----
 *
 * Reuses .ld-wrap/.ld-section for the same margins as home.html rather than
 * a second layout system, then narrows to a reading column and turns the
 * heading ladder into ordinary section headings.
 */
.ld-legal-body { max-width: 68ch; line-height: 1.65; color: var(--ink-dim); }
.ld-legal-body h3 { margin: 40px 0 10px; font-size: var(--fs-head); font-weight: 800; color: var(--ink); }
.ld-legal-body h3:first-child { margin-top: 0; }
.ld-legal-body p, .ld-legal-body li { margin: 0 0 14px; }
.ld-legal-body ul { margin: 0 0 14px; padding-left: 22px; }
.ld-legal-body strong { color: var(--ink); }
.ld-legal-updated { margin: 0 0 28px; font-size: var(--fs-note); color: var(--ink-dim); }
.ld-legal-nav { display: flex; gap: 16px; flex-wrap: wrap; margin: 0 0 8px; font-size: var(--fs-note); }
/* A bracketed placeholder Mark has to fill in before this page is real — never a fabricated business detail standing in for one. */
.ld-legal-todo { color: var(--c); background: color-mix(in srgb, var(--c) 16%, transparent); border-radius: 4px; padding: 0 4px; }

/* ---- /signup, its own smaller page ---- */
.ld-signup-wrap { max-width: 460px; margin: 8vh auto; padding: 0 20px; }
.ld-signup-back { color: var(--ink-dim); text-decoration: none; font-size: var(--fs-note); font-weight: 700; }
.ld-signup-back:hover { color: var(--ink); }
.ld-signup-card {
  margin-top: 20px; background: var(--panel); border: 1px solid var(--panel-line);
  border-radius: var(--r-card); padding: 30px 26px;
}
.ld-signup-card h1 { margin: 0 0 8px; font-size: var(--fs-title); }
.ld-signup-card .ld-section-lede { margin: 0 0 24px; font-size: var(--fs-body); }
.ld-signup-form { display: grid; gap: 14px; }
.ld-signup-form label { display: grid; gap: 5px; font-size: var(--fs-note); font-weight: 700; color: var(--ink-dim); }
.ld-signup-form input, .ld-signup-form textarea {
  padding: 12px 14px; font: inherit; font-size: var(--fs-field); border-radius: var(--r-field);
  border: 1px solid var(--panel-line); background: rgba(255, 255, 255, 0.05); color: var(--ink);
}
.ld-signup-form input:focus, .ld-signup-form textarea:focus { outline: none; border-color: var(--hot); }
.ld-signup-form textarea { resize: vertical; min-height: 70px; font-family: inherit; }
.ld-signup-form button.go {
  margin-top: 4px; padding: 14px; font-size: var(--fs-field); font-weight: 900;
  border: none; border-radius: var(--r-field); cursor: pointer;
  background: linear-gradient(120deg, var(--hot), var(--hot-2)); color: #12000c;
}
.ld-signup-form button.go:hover { filter: brightness(1.08); }
.ld-signup-form button.go:disabled { opacity: 0.6; cursor: default; }
.ld-signup-problem:not(:empty) {
  padding: 10px 12px; border-radius: var(--r-field); font-size: var(--fs-note); font-weight: 700;
  background: rgba(255, 77, 94, 0.12); color: var(--bad);
}
.ld-signup-done { text-align: center; padding: 10px 0; }
.ld-signup-done h1 { margin: 0 0 10px; font-size: var(--fs-title); }
.ld-signup-done p { color: var(--ink-dim); line-height: 1.6; }
.ld-referral-note {
  margin: 0 0 20px; padding: 10px 12px; border-radius: var(--r-field); font-size: var(--fs-note);
  font-weight: 700; background: rgba(47, 224, 122, 0.12); color: var(--good);
}

/* ---- the Community door: what is running, and who is winning -------------
 *
 * The bay and the tables. The table itself already had a style — `.lg-table`
 * was written for the block on a venue card — so nothing about a row is set
 * again here: the same table in a bigger room, which is what stops one venue's
 * league and every venue's league looking like two different features.
 */
.community-bench .bench-head { display: flex; flex-direction: column; gap: 2px; }
.community-bench .bench-head b { font-size: var(--fs-head); }
/*
 * THE LEADERS RUN ALONG, one per venue, and they WRAP rather than scroll — a
 * quizmaster with six venues is reading a list, not driving a carousel, and a
 * row that scrolls sideways hides its own end.
 */
.community-bench .bench-row {
  display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px;
}
.community-lead {
  display: flex; flex-direction: column; gap: 1px;
  padding: 7px 11px; border-radius: var(--r-field);
  background: var(--surf-2, rgba(255, 255, 255, 0.04));
  border: 1px solid var(--surf-line, rgba(255, 255, 255, 0.09));
  min-width: 0;
}
.community-where { color: var(--ink-dim); }
/* Gold, because it is first place — the trophy colour, used for the one thing
   in this app it is allowed to mean. */
.community-who { color: var(--gold); }

/* ---- THE BAY IS THE TAB YOU ARE ON — the wall, and the venue rail.
 *
 * *"Anything that loads should load onto the top bar bit"*, and *"the bottom
 * is for controls and options, not for displaying the actual thing — so you
 * click the thing at the bottom to reveal it at the top."*
 *
 * THE HEIGHT IS NOT THIS BLOCK'S TO CHOOSE. Every door's bay is exactly the
 * launch bar's size — see `--bay-h` in the door shell — so what is decided
 * here is only what happens INSIDE that fixed box. */

/* SIX ACROSS, mirroring the pack shelf and the six Tonight bays two inches
   below it. `minmax(0, 1fr)` because a grid child defaults to `min-width:
   auto` and an image's own intrinsic width would otherwise push the column
   wider than the page — the trap `.consolecols` already carries a note about. */
.community-wall {
  display: grid; gap: 6px;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  align-content: start;
}
.community-wall-none { grid-column: 1 / -1; }
/* A FIXED CELL HEIGHT, so three rows are a known height inside a fixed bay and
   a night with thirty pictures scrolls rather than growing the box. 96px is
   what three rows plus the heading come to inside `--bay-h`.
   `object-position` ABOVE CENTRE, because a wide crop of a portrait photo
   taken on a phone otherwise lands on torsos, and a pub photograph is people. */
.community-wall .cphoto {
  margin: 0; padding: 0; width: auto; flex: none;
  border-radius: var(--r-field); overflow: hidden;
}
.community-wall .cphoto img { height: 96px; object-position: center 30%; }
.cphoto.is-openable { cursor: pointer; }
.cphoto.is-openable:hover { border-color: var(--hot); }

/* ONE PICTURE, OVER THE BAY — `contain`, because this is the moment somebody
   is actually looking at it, and a crop is right on a wall of thumbnails and
   wrong here. The whole thing is the button back.

   AN OVERLAY RATHER THAN A REPLACEMENT, which is what keeps the wall's scroll:
   the grid underneath is never destroyed, so closing the picture puts you back
   exactly where you were rather than at the top. Opaque, or the thumbnails
   show through the letterboxing.

   IT HANGS ON THE COLUMN, NEVER ON THE SCROLLING GRID INSIDE IT. `inset: 0`
   anchors to the nearest positioned ancestor's PADDING BOX, and for a scrolled
   container that box begins at the top of the content rather than at the top
   of what you can see — so hung on `.bay-body` it drew exactly one scroll
   offset too high, with thumbnails showing round it. `.bay-side` does not
   scroll (`overflow: hidden`, with the grid scrolling inside it), so it is the
   honest anchor. */
.bay-side { position: relative; }
.community-big {
  position: absolute; inset: 0; z-index: 2;
  display: block; width: 100%; height: 100%; padding: 0;
  background: var(--surf-1, #14101c); border: 0; cursor: pointer;
}
.community-big img { display: block; width: 100%; height: 100%; object-fit: contain; }

/* THE RAIL AND WHAT IT PICKS — the same two columns as `.consolecols` under
   it, deliberately: it is the same object doing the same job, and a second
   way of saying "pick one of these" on one screen is a label collision. */
.baycols { display: grid; gap: 8px 18px; grid-template-columns: 1fr; }
/* `flex: 0 0 auto` on the children is load-bearing rather than tidy: a flex
   column SHRINKS its children when the content is taller than the box, and the
   rail always is — it scrolls. The rows survived on `min-height: 44px`, but the
   pub headings had no floor and were squashed to 2px, so a rail that had
   compartmentalised its nights perfectly rendered as one undivided list and
   nothing threw. Measured, not spotted: they were in the DOM the whole time. */
.bay-rail { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.bay-rail > * { flex: 0 0 auto; }
/* The lit marker is the LEFT edge, exactly as the tab column's is — under a
   stacked list a bottom border reads as a rule between two rows. */
/* THE NIGHT'S PUBLIC ADDRESS, at the far end of the bay's head — the empty
   half of a row that had a date at one end and nothing at the other.

   `margin-left: auto` rather than a spacer, so it sits at the end whatever is
   beside it and the head needs no layout change. Quiet by default and the
   account's own colour once the night is actually live: the difference between
   "you could look at this" and "anybody can". */
.bay-head-live {
  margin-left: auto; flex: 0 0 auto;
  display: inline-flex; align-items: center; min-height: 34px;
  font-size: var(--fs-note); font-weight: 800; text-decoration: none;
  padding: 6px 13px; color: var(--ink);
  /* ---- IT IS SHAPED LIKE A BUTTON NOW, AND IT WAS SHAPED LIKE A BADGE.

     Reported as *"it needs a mouseover and click animation so it's obvious
     it's a button"*, and the shape was half of why: a flat outlined PILL is
     what this app's badges wear — BRONZE, PAID, YOURS — so it was saying
     "label" before anybody got near it. `--r-field` is what a button wears
     here, and the ordinary-button face is a top-lit surface with the
     account's colour on the bottom EDGE: never a flat grey box, never a wall
     of colour. The same treatment `.minor` gets, at the size this one sits at. */
  border-radius: var(--r-field);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.04));
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-bottom: 2px solid var(--hot);
  transition: transform 90ms ease, box-shadow 90ms ease, background 90ms ease;
}
/* THE LIFT ON HOVER AND THE PRESS ON CLICK — the two halves that make an
   object feel like one. The press goes PAST where it started (1px down, and
   the bottom edge thins to 1px) so the whole thing shortens under the finger,
   which is what a real button does; a hover-only control still reads as a
   label that happens to glow. */
.bay-head-live:hover {
  transform: translateY(-1px);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.07));
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.35);
}
.bay-head-live:active {
  transform: translateY(1px);
  border-bottom-width: 1px;
  box-shadow: none;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02));
}
.bay-head-live:focus-visible { outline: 2px solid var(--hot); outline-offset: 2px; }
/* LIVE WEARS GREEN ON THE EDGE, which is this app's fixed meaning for good and
   paying — the account's colour says "yours", green says "it is actually up".
   The face is untouched, so it is the same object in both states. */
.bay-head-live.is-live { color: var(--good); border-bottom-color: var(--good); }
@media (prefers-reduced-motion: reduce) {
  /* The ANSWER survives, the movement goes — the rule the finish layer sets:
     hover and press still change the surface, they just do not move it. */
  .bay-head-live { transition: none; }
  .bay-head-live:hover, .bay-head-live:active { transform: none; }
}

/* ---- A ROW WITH A LAMP ON IT — the publish switch, on the Photos rail only.

   The lamp is a SIBLING of the row rather than a child: a button inside a
   button is invalid HTML, and the browser re-nests it so the inner one stops
   receiving clicks — silently.

   `flex: 1 1 auto` plus `min-width: 0` on the row is what stops a long date
   pushing the lamp out of the rail; `flex: 0 0 auto` on the lamp is the
   `.bay-rail > *` rule's reasoning one level in — a flex line shrinks its
   children, and a squashed 28px lamp is a smear. */
.bay-pick-row { display: flex; align-items: stretch; gap: 6px; min-width: 0; }
.bay-pick-row .bay-pick { flex: 1 1 auto; min-width: 0; }

/* GREEN FOR ON AND RED FOR OFF, which is the one place in this app they are
   allowed to mean this — "on a public page" versus "not" is exactly the
   good/wrong pair, read at a glance down a list of nights. The same words the
   per-photo lamp uses, so the two controls agree about what a colour means.

   It carries the letter P rather than being a bare dot: a night row is read,
   not scanned like a grid of eighteen thumbnails, and a lone dot beside a date
   has nothing near it to say what it is about. */
.bay-lamp {
  flex: 0 0 auto; align-self: center;
  width: 30px; height: 30px; padding: 0; border-radius: 999px; cursor: pointer;
  font: inherit; font-size: var(--fs-mini); font-weight: 800; line-height: 1;
  color: #fff; border: 2px solid rgba(10, 8, 16, 0.55);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
}
.bay-lamp.is-on { background: var(--good); }
.bay-lamp.is-off { background: var(--bad); }
.bay-lamp:hover { transform: scale(1.1); }
@media (prefers-reduced-motion: reduce) { .bay-lamp:hover { transform: none; } }
.bay-rail-trouble { color: var(--bad); padding: 4px 10px; }

.bay-pick {
  display: flex; flex-direction: column; gap: 1px; text-align: left;
  min-height: 44px; padding: 6px 10px; min-width: 0;
  border: 1px solid var(--surf-line, rgba(255, 255, 255, 0.09));
  border-left: 3px solid transparent;
  border-radius: var(--r-field);
  background: var(--surf-2, rgba(255, 255, 255, 0.04));
  color: var(--ink-dim); cursor: pointer;
}
.bay-pick:hover { color: var(--ink); }
.bay-pick.on {
  color: var(--ink);
  border-left-color: var(--hot);
  background: rgba(255, 255, 255, 0.07);
  background: color-mix(in srgb, var(--hot) 12%, transparent);
}
/* THE ROW'S NAME WRAPS TO TWO LINES TOO, for the same reason — a pack called
   "2000s Pop, R&B and Chart" is unreadable ellipsised at 190px, and it had a
   `title` on it only because of that. */
.bay-pick-name {
  font-weight: 800; font-size: var(--fs-ctl); min-width: 0; overflow: hidden;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
}
/* A PUB, THEN ITS NIGHTS UNDER IT — *"can this section be collapsible within
   its current dimensions, i.e. Station Tap Wokingham collapses and then
   expands into each night?"*

   IT IS A DISCLOSURE, so it is shaped like one and not like a row: shorter
   than the 44px rows under it, a caret that turns, and the count on the right
   so a shut pub still says how much is in there. Nothing clickable in this app
   is a flat grey box, so it takes the same face and hover the rows do. */
.bay-rail-group {
  display: flex; align-items: center; gap: 8px; width: 100%;
  min-height: 40px; padding: 6px 10px; text-align: left;
  border: 1px solid transparent;
  /* A LEFT EDGE THAT IS ALWAYS THERE, so the heading lines up with the rows
     under it rather than sitting 3px left of them — and so it has somewhere to
     light up when it is shut over the night you are looking at. */
  border-left: 3px solid transparent;
  border-radius: var(--r-field);
  background: none; color: var(--ink-dim); cursor: pointer;
  /* A RUNG BELOW THE ROWS, not level with them. The rows are the thing you
     press; this names the pile they are in, and at the same size and weight
     the two read as one undifferentiated list. */
  font-size: var(--fs-note); font-weight: 700;
}
.bay-rail-group:hover { color: var(--ink); background: rgba(255, 255, 255, 0.05); }
.bay-rail-group.on { color: var(--ink); }
/* SHUT, OVER THE NIGHT YOU ARE LOOKING AT — the rail still says where you are
   without forcing itself open, which is what made the fold do nothing. */
.bay-rail-group.holds-picked { color: var(--ink); border-left-color: var(--hot); }
/* IT WRAPS RATHER THAN ELLIPSISING, which is what let the `title` attribute go
   — a native tooltip is an unstyled box that lands over the rows beneath it. A
   pub name is worth two lines; it is the thing being read. */
.bay-rail-what {
  min-width: 0; overflow: hidden;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
}
.bay-rail-count { margin-left: auto; flex: 0 0 auto; opacity: 0.75; }
/* A TRIANGLE, DRAWN — no font, no asset, and it turns rather than swapping for
   a different glyph, so the fold reads as one control in two positions. */
.bay-rail-caret {
  flex: 0 0 auto; width: 0; height: 0;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent; border-bottom: 4px solid transparent;
  transform-origin: 2px 50%; transition: transform 0.12s ease;
}
.bay-rail-group.on .bay-rail-caret { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .bay-rail-caret { transition: none; } }
/* WHERE THE REST OF THEM ARE. Text, not a link: a rail picks and never acts,
   and the list it points at is already the next thing down the page. */
.bay-rail-more { padding: 2px 10px 6px; color: var(--ink-dim); }
/* A RAIL ROW MAY WEAR ITS PACK'S OWN COLOURS. Same wash `.pack-card.tinted`
   and `.lb-tile.tinted` take, capped the same way — the subject at a glance on
   a 190px row. `.on` still wins, because "you are here" outranks the era. */
.bay-pick.tinted {
  background: linear-gradient(120deg, var(--pk-a), var(--pk-b)), var(--surf-2, rgba(255, 255, 255, 0.04));
}
/* THE KIND ALONG THE BOTTOM, exactly as a pack card wears it — quiz green,
   bingo purple. Three pixels that answer "which game is this" before the words
   are read, on the one list where both kinds sit together. */
.bay-pick.tinted { border-bottom: 2px solid var(--pk-edge); }
.bay-side { min-width: 0; }
.bay-side .lg-table { margin-top: 8px; }

/* A bay whose right column scrolls under a heading that stays. The panel is a
   flex column so the two-column grid inside it gets the full fixed height, and
   the grid's own rows are `min-content` then `1fr` so the heading keeps its
   size and the body takes the rest. */
.bay-scroller { display: flex; flex-direction: column; min-height: 0; }
.bay-scroller > .baycols { flex: 1 1 auto; min-height: 0; }
.bay-side { display: flex; flex-direction: column; min-height: 0; }
.bay-head { flex: 0 0 auto; display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.bay-head b { font-size: var(--fs-head); }
.bay-body { flex: 1 1 auto; min-height: 0; margin-top: 8px; }

/* A NIGHT BEING DRAGGED TO ANOTHER PUB, and the heading that will take it.
   The heading is the only thing that lights, and only where the drop will
   actually be taken — a target that lights and then does nothing promises a
   change it is not going to make. */
.bay-pick.is-dragging { opacity: 0.45; }
.bay-rail-group.drop-on {
  border-color: var(--hot);
  background: color-mix(in srgb, var(--hot) 18%, transparent);
}

/* THE NIGHT SHOWING IN THE BAY, marked in the list — the same held hover the
   Post gig bench uses for the row it has on the bench. Its one control sits
   under it, which is where every control that ACTS lives on this door. */
.photo-night.is-showing { background: rgba(255, 255, 255, 0.05); }
.photo-night.is-showing .photo-night-top b { color: var(--hot); }
.photo-night-controls { padding: 0 12px 10px; }
/* THE QUIZMASTER'S OWN PHOTOGRAPHS. A `<label>` wrapping a hidden file input,
   because a bare `<input type="file">` cannot be styled and would be the one
   flat grey box in the app. Ordinary role — outlined, no fill — since adding a
   photograph is not the loudest thing on this page; publishing is. */
/* WHERE A PAST NIGHT WAS. A label, a dropdown and one line — the *Choose*
   role, so a quiet field and no gradient at all. `min-width: 0` on the select
   for the reason every other one in this file carries it: a dropdown will not
   go below the width of its longest option, and a pub name with a town on it
   is long. It wraps to two lines under 560px rather than squeezing the box. */
.night-venue { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; font-size: var(--fs-note); color: var(--ink-dim); font-weight: 800; }
.night-venue-pick { flex: 0 1 auto; min-width: 0; max-width: 100%; }
.night-venue-said { color: var(--ink-dim); flex: 1 1 140px; }
.mine-add { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
.mine-pick { cursor: pointer; }
.mine-pick.is-busy { opacity: 0.6; pointer-events: none; }
.mine-said { color: var(--ink-dim); }

@media (min-width: 900px) {
  .baycols { grid-template-columns: 190px minmax(0, 1fr); }
  /* Each column scrolls on its own inside the fixed bay: a quizmaster with ten
     venues can reach the tenth without the table moving, and a long table can
     be read without losing the rail. */
  /* Each column scrolls on its own inside the fixed bay: a quizmaster with ten
     venues can reach the tenth without the table moving, and a long table can
     be read without losing the rail. The heading does not scroll with it. */
  .bay-rail { overflow-y: auto; min-height: 0; }
  .bay-side { overflow: hidden; }
  .bay-side > .lg-table { overflow-y: auto; display: block; }
  .bay-body { overflow-y: auto; }
}
/* A PHONE GETS THREE ACROSS. Six 52px thumbnails is a contact sheet nobody can
   see; the row count is what the bay cared about, and on a phone the bay
   scrolls with the page anyway. */
@media (max-width: 700px) {
  .community-wall { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* DOES THIS PUB RUN A LEAGUE — the switch the publish control depends on, so
   it sits above it with a rule between them: two questions, asked in the order
   the second one needs. */
/* THE ADDRESS, WRITTEN OUT so it can be read off the screen and typed, or
   selected and copied. Monospace because it is a thing to transcribe, and it
   wraps rather than ellipsising: half an address is worse than none. */
.pub-address {
  display: block; margin-top: 4px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-note); color: var(--ink-dim);
  overflow-wrap: anywhere; user-select: all;
}
.lg-running { display: grid; gap: 8px; justify-items: start; margin-bottom: 12px;
  padding-bottom: 12px; border-bottom: 1px solid var(--panel-line); }
.league-panel + .league-panel { margin-top: 12px; }
.league-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.league-head b { font-size: var(--fs-head); }

/*
 * The Photos tab's own rows. A venue card holds its nights; a night opens its
 * strip in place, because this tab is ONLY about photographs — there is no
 * competing "night detail" for a bay to own, which is what Past gigs has.
 */
.photo-night { border-top: 1px solid var(--panel-line); }
.photo-night-top {
  display: flex; align-items: baseline; gap: 10px; width: 100%;
  min-height: 44px; padding: 8px 12px;
  background: none; border: 0; color: var(--ink); font: inherit;
  text-align: left; cursor: pointer;
}
.photo-night-top:hover { color: var(--hot); }
.photo-night-body:not(:empty) { padding: 0 12px 12px; }
/* The headcount beside the league's own count — one line, dimmed, so the two
   numbers a landlord asks about sit together without competing. */
.league-heads::before { content: "· "; }
.asks-link { margin: 0 0 10px; }
/* The pack row's caption when it is waiting rather than naming a pack — dim,
   because it is the REASON the two controls beside it are off. */
.lb-set-for.is-waiting { color: var(--ink-dim); }


/* =====================================================================
 * THE FINISH LAYER — the browser surfaces nobody draws, and the states a
 * mouse and a keyboard actually meet. Added on 25 August 2026 as one block
 * rather than scattered per control, so the next control can see in one
 * place what "finished" means here.
 * ===================================================================== */

/* SELECTING TEXT IS PART OF THE DESIGN TOO. The default blue selection is
   the one colour this app never uses anywhere else, so every drag across a
   label broke the scheme. The rgba is the fallback; `color-mix` follows the
   account's own hot colour, the same pattern the lit menu chip uses. */
::selection { background: rgba(255, 46, 136, 0.45); color: #fff; }
@supports (background: color-mix(in srgb, red 50%, transparent)) {
  ::selection { background: color-mix(in srgb, var(--hot) 45%, transparent); color: var(--ink); }
}
/* And the caret in a field blinks in the account's colour rather than white —
   read at the moment somebody is typing into this app's one dark surface. */
input, textarea, [contenteditable] { caret-color: var(--hot); }

/* KEYBOARD FOCUS, EVERYWHERE A THUMBNAIL AUDIT FOUND IT MISSING. The app's
   own convention is a 2px hot outline (`.pack-pin`, `.lb-hit` had it); these
   are the controls that grew up without one. `:focus-visible`, never
   `:focus`, so a mouse press does not paint rings on a gig night. */
.topnav a:focus-visible,
.console button.minor:focus-visible, .console a.minor:focus-visible, .console label.minor:focus-visible,
.pick-face:focus-visible,
.gap-dial:focus-visible,
.pack-title:focus-visible {
  outline: 2px solid var(--hot);
  outline-offset: 2px;
  border-radius: var(--r-field);
}

/* A CARD IS AN OBJECT YOU PICK UP — so it answers the hand before the hand
   commits, the same language the round squares already speak (lift + ring).
   A lift plus a real shadow (offset and blur, never a flat halo), and the
   hairline brightens. Kept SMALL: sixty cards must not shimmer like a slot
   machine, they must just feel solid one at a time. */
.pack-card.shut {
  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.pack-card.shut:hover {
  transform: translateY(-2px);
  border-color: rgba(255, 255, 255, 0.3);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.35);
}
/* The nav chips and the console's small buttons ease instead of snapping —
   the hover styles already existed, only the jump between them was new. */
.topnav a, .console button.minor, .console a.minor, .console label.minor, .gap-dial, .pick-face {
  transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}
/* Somebody who asked their device to stop moving things still gets the
   border and shadow answer — the lift is the flourish, not the information. */
@media (prefers-reduced-motion: reduce) {
  .pack-card.shut, .topnav a, .console button.minor, .gap-dial, .pick-face { transition: none; }
  .pack-card.shut:hover { transform: none; }
}


/* ---- THE PUBLIC LEAGUE PAGE — `/league`, `league-page.js`.
   It borrows the gallery's shell (`body.gallery`, `.gal-wrap`, `.gal-head`)
   because it is the same kind of page: something the people who were in the
   room come back to, with no account and no navigation. Only the parts the
   gallery has no equivalent of are here. */
.lgp-venue { margin-bottom: 18px; }
.lgp-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; margin-bottom: 10px; }
.lgp-where { margin: 0; font-size: var(--fs-head); font-weight: 900; }
/* THE OWNER'S PREVIEW MARK. Only ever drawn on a table that is NOT public, so
   it can never be mistaken for a badge the regulars see — and it says the
   state rather than the action, because there is nothing to press here. */
.lgp-draft {
  border: 1px solid var(--panel-line); color: var(--ink-dim);
  padding: 2px 10px; font-size: var(--fs-mini); font-weight: 800;
}
/* WHEN THEY ARE NEXT ON — the loudest thing under the table, by decision:
   a team lying fourth wants to know when it can do something about it. Gold,
   because coming back to win is the trophy colour's own job. */
.lgp-next {
  margin: 12px 0 0; padding-top: 12px;
  border-top: 1px solid var(--panel-line);
  color: var(--gold); font-size: var(--fs-lead);
}
.lgp-note { margin-top: 18px; text-align: center; color: var(--ink-dim); }

/* HOW MANY OF A TEAM'S NIGHTS ACTUALLY SCORED, beside how many they played —
   only ever drawn once some are being dropped, so a team adding their own
   weeks up can see why the total is not the sum. Quieter than the number it
   qualifies: the count is the fact and this is the footnote. */
.lg-drop { opacity: 0.72; font-variant-numeric: tabular-nums; }

/* A NAME THE PUBLIC TABLE WILL NOT CARRY, marked on the console's own table
   only — the room's view shows what was typed. A quiet outlined pill, not red:
   nothing has gone wrong and nobody is being told off, it is a statement about
   where the name goes. */
.lg-hidden {
  display: inline-block; margin-left: 6px; padding: 1px 8px;
  border: 1px solid var(--panel-line); border-radius: var(--r-pill);
  color: var(--ink-dim); font-size: var(--fs-mini); font-weight: 800;
  vertical-align: middle; white-space: nowrap;
}

/* ---- OVERRULING THE NAME FILTER — `nameReview()` in console-community.js.
   Folded away behind one line under each table, because it is checked before
   publishing rather than read: thirty toggles on a page whose job is being
   scanned is the clutter rule exactly. */
.lg-review { margin-top: 10px; }
.lg-review-list {
  display: grid; gap: 6px; margin-top: 8px;
  padding: 10px; border-radius: var(--r-field);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--panel-line);
}
/* Name, then what will happen to it, then the one control. `auto` on the
   state so a long name gives way first — the verdict is the column somebody
   is scanning down. */
.lg-review-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 10px; align-items: center; }
.lg-review-name { font-weight: 800; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lg-review-state { color: var(--ink-dim); white-space: nowrap; }
/* "Your call" — this row is a human's decision rather than the list's, so a
   later change to the word list will not touch it. Quiet: it is a footnote on
   a state, not a state of its own. */
.lg-review-yours {
  margin-left: 4px; padding: 1px 7px; border-radius: var(--r-pill);
  border: 1px solid var(--panel-line); color: var(--ink-dim); font-size: var(--fs-mini);
}
@media (max-width: 560px) {
  /* The control drops under the name rather than squeezing three columns
     into a phone — same give the settings row makes. */
  .lg-review-row { grid-template-columns: minmax(0, 1fr) auto; }
  .lg-review-state { grid-column: 1 / -1; }
}


/* A round tick carrying its TYPE as a glyph. The colour still means on/off —
   see roundGlyph() for why the type may not have it. */
.lb-rd.has-glyph { font-size: 13px; line-height: 1; }

/* TAP FOR SOUND — the projector's one-press arm for the soundboard.
   Bottom LEFT and small, deliberately: the join code owns the other corner
   and nothing may dim it. It removes itself on the first press of the night. */
.sound-arm {
  position: fixed; left: 14px; bottom: 14px; z-index: 40;
  padding: 8px 14px; border-radius: var(--r-pill, 999px);
  font-size: 14px; font-weight: 600; cursor: pointer;
  color: var(--ink); background: rgba(12, 12, 20, 0.82);
  border: 1px solid color-mix(in srgb, var(--hot) 55%, transparent);
}

/* THE SOUNDBOARD on the control view. A wrapping row of ordinary buttons —
   never the account gradient, which means "this is the night" and belongs to
   Launch. Each is a full touch target: this is pressed on reflex, in the dark,
   with a microphone in the other hand. */
.stingrow { display: flex; flex-wrap: wrap; gap: 8px; }
.stingrow .sting {
  flex: 1 1 30%; min-height: 44px; display: flex;
  align-items: center; justify-content: center; gap: 6px;
  font-size: 13px; white-space: nowrap;
}
.stingrow .sting-ic { font-size: 17px; }

/* A WORD FROM THE QUIZMASTER — one phone, never the projector. It sits over
   whatever is on screen because it has to be read, and the only way out is the
   button: no timeout, so a phone face down on a table still has it waiting. */
.hostnote-wrap {
  position: fixed; inset: 0; z-index: 60; display: grid; place-items: center;
  padding: 20px; background: rgba(6, 6, 12, 0.72);
}
/*
 * AN OPAQUE GROUND, and `--panel` is NOT one.
 *
 * It is `rgba(255,255,255,.06)` — six per cent white, which over a dimmed
 * bingo card is no surface at all: the squares read straight through the
 * words, and "From the quizmaster" landed on top of a track title. The same
 * fault `body.console`'s own surfaces are opaque to avoid, hit in the one
 * place it matters most, because this card is the whole point of the feature.
 * `--bg-2` is what the camera sheet beside it already uses.
 */
.hostnote {
  width: 100%; max-width: 420px; display: grid; gap: 12px; text-align: center;
  padding: 22px 20px; border-radius: var(--r-card, 14px);
  background: var(--bg-2); border: 1px solid var(--panel-line);
  box-shadow: 0 24px 60px rgba(0, 0, 0, .55);
}
.hostnote .sub { font-size: 13px; color: var(--ink-dim); letter-spacing: .04em; }
.hostnote-text { font-size: 20px; font-weight: 700; margin: 0; overflow-wrap: anywhere; }

/* Sent, and whether they looked at it. The ticked one STAYS. */
.notemark { font-size: 13px; opacity: .65; }
.notemark.seen { opacity: 1; color: var(--good, #48d17a); }

/* ------------------------------------------------------------- My prizes
 *
 * *"The QR code needs to be collapsible so they can still interact with the app
 * even when they have drinks"* — and a QR is square, so one at a lobby pushes
 * the game and the camera below the fold, and on a bingo card the box sits
 * ABOVE the grid and pushes the squares off a 390px screen.
 *
 * SHUT IS THE RESTING STATE, so this is styled as a row first and a panel
 * second: 44px, the touch floor, and nothing taller until somebody asks.
 */
.prizes {
  border: 1px solid var(--panel-line);
  border-radius: var(--r-card);
  background: var(--bg-2);
  overflow: hidden;
}
.prizes-head {
  width: 100%;
  min-height: 44px;
  display: flex; align-items: center; gap: 10px;
  padding: 0 14px;
  border: 0; background: none; color: var(--ink);
  font: inherit; font-size: 15px; font-weight: 700;
  cursor: pointer; text-align: left;
}
.prizes-name { flex: 1 1 auto; }
/* The count is the only thing the shut row can say about what is behind it,
   so it is a pill rather than a bare digit — gold, because a prize is the one
   thing in this app that colour has always meant. */
.prizes-n {
  flex: 0 0 auto;
  min-width: 22px; padding: 1px 7px;
  border-radius: 999px;
  background: rgba(255, 199, 74, 0.16);
  background: color-mix(in srgb, var(--gold) 16%, transparent);
  color: var(--gold);
  font-size: 13px; text-align: center;
}
.prizes-caret {
  flex: 0 0 auto;
  color: var(--ink-dim);
  transition: transform .15s ease;
}
.prizes.shut .prizes-caret { transform: rotate(-90deg); }
.prizes-body { display: grid; gap: 12px; padding: 0 12px 12px; }
.prizes.shut .prizes-body { display: none; }
/* Inside the section the card has already been framed once — a second border
   and a second ground is a box in a box. */
.prizes-body .win-card { border: 0; background: none; padding: 0; }

/* ------------------------------------------------- a tab's own label
 *
 * THE CONSOLE DOOR'S TABS DRAW THEIR OWN HEADING, because `tabBody()` draws the
 * shared one on every door but this — its bay is the launch bar. Two tabs do,
 * and they had drifted to two different steps: Venues at 13px (an `h3`, the
 * tab's NAME at section size) and Prepare a night at 19px, its `h2` inside a
 * `.game-head` that drops to `--fs-head`.
 *
 * A tab's own label is a TITLE wherever it is drawn, so this names that in one
 * place rather than leaving it to where the markup happens to sit. The long
 * selector is not decoration: `.console .game-section .game-head h2` is what it
 * has to out-rank, and equal specificity later in the sheet is what wins.
 */
.console h2.tab-own-head,
.console .game-section .game-head h2.tab-own-head {
  font-size: var(--fs-title);
  font-weight: 900;
  letter-spacing: -0.02em;
  margin: 0 0 4px;
}

/* ------------------------------------------------------------ the soundboard
 *
 * SHUT BY DEFAULT. Eight buttons across two rows sit above the player list at
 * every phase, and on a phone that pushes who-is-playing off the screen — the
 * board is the job and the soundboard is a garnish.
 *
 * The same shape as the phone's `.prizes`: a 44px row, a caret that turns, and
 * the body hidden rather than removed so the buttons keep their listeners.
 */
.panel.sounds { padding: 0; overflow: hidden; }
.sounds-head {
  width: 100%;
  min-height: 44px;
  display: flex; align-items: center; gap: 10px;
  padding: 0 16px;
  border: 0; background: none; color: var(--ink);
  font: inherit; font-size: 15px; font-weight: 700;
  cursor: pointer; text-align: left;
}
.sounds-name { flex: 1 1 auto; }
.sounds-caret { flex: 0 0 auto; color: var(--ink-dim); transition: transform .15s ease; }
.panel.sounds.shut .sounds-caret { transform: rotate(-90deg); }
.sounds-body { padding: 0 16px 14px; }
.panel.sounds.shut .sounds-body { display: none; }

/* ------------------------------------------------------------- a DJ set
 * The phone on a DJ night: a camera, then a box to ask for a song. Two
 * controls, in that order, because the photograph is what buys the request.
 *
 * It borrows the phone's own card shape (`.wait-item`) rather than inventing
 * one — same radius, same top-lit face, same account-coloured bottom edge —
 * so a DJ set looks like the app it is part of. Three radii, one angle.
 */
/* GOLD IS THE TROPHY COLOUR AND NOTHING ELSE. The phone's head paints its
   number gold because on a quiz that number is the SCORE; on a DJ set it is
   how many requests are left, which is not winning anything. */
body.dj .head .score { color: var(--ink); }

.dj-phone { display: grid; gap: 14px; }

.dj-card {
  display: grid; gap: 10px;
  padding: 14px; border-radius: var(--r-card);
  background: linear-gradient(120deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.04));
  border: 1px solid var(--panel-line);
  border-bottom: 2px solid var(--hot);
}
/* The reason a control is off goes ON the control, so the card stays put and
   dims rather than disappearing — *a control never appears out of nothing*. */
.dj-card.is-off { opacity: .72; border-bottom-color: var(--panel-line); }
.dj-card-what { display: grid; gap: 2px; min-width: 0; }
.dj-card-what b { font-size: var(--fs-head); }
.dj-card-what .tiny { color: var(--ink-dim); }
.dj-why { color: var(--ink-dim); }

/* THE CAMERA IS THE ONE FILLED CONTROL ON THIS SCREEN, and only until it has
   been used: fill means commitment, and on a DJ set the photograph is the
   commitment being asked for. Afterwards it drops to an ordinary button so
   the request box is the loudest thing left. */
.dj-shoot {
  min-height: 48px; border-radius: var(--r-field);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.04));
  border: 1px solid var(--panel-line); border-bottom: 2px solid var(--hot);
  color: var(--ink); font: inherit; font-weight: 800; cursor: pointer;
}
.dj-camera-first .dj-shoot {
  background: linear-gradient(120deg, var(--hot), var(--hot-2));
  border: 0; color: #fff;
}
.dj-shoot:active { transform: translateY(1px); }

.dj-search, .dj-manual { display: grid; gap: 10px; }
.dj-row { display: flex; gap: 8px; }
.dj-q { flex: 1 1 auto; min-width: 0; }
.dj-go, .dj-send {
  flex: 0 0 auto; min-height: 44px; padding: 0 16px;
  border-radius: var(--r-field);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.04));
  border: 1px solid var(--panel-line); border-bottom: 2px solid var(--hot);
  color: var(--ink); font: inherit; font-weight: 800; cursor: pointer;
}
.dj-go:disabled, .dj-send:disabled { opacity: .6; cursor: default; }
.dj-say { color: var(--ink-dim); margin: 0; }

.dj-tracks { display: grid; gap: 6px; }
/* A whole row is the target, not the words in it — 52px, above the touch
   floor, because this is the one control somebody uses in the dark. */
.dj-track {
  display: grid; gap: 1px; min-height: 52px; padding: 8px 12px;
  border-radius: var(--r-field); text-align: left;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--panel-line);
  color: var(--ink); font: inherit; cursor: pointer;
}
.dj-track:active { transform: translateY(1px); }
.dj-track-title { font-weight: 800; }
.dj-track-artist { color: var(--ink-dim); }
.dj-swap { justify-self: start; }

.dj-mine-list { display: grid; gap: 6px; }
.dj-mine-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-radius: var(--r-field);
  background: rgba(255, 255, 255, 0.04);
}
.dj-mine-what { flex: 1 1 auto; min-width: 0; }
.dj-mine-what .tiny { color: var(--ink-dim); }
.dj-mine-state { flex: 0 0 auto; color: var(--ink-dim); }
/* Played is the good news, so it is the one thing on this list that is not
   grey — and it is GREEN rather than gold, gold being the trophy colour. */
.dj-mine-row.is-played .dj-mine-state { color: var(--good); }

/* ------------------------------------------------ the DJ desk, in the booth
 * Read in a dark booth with a record running out, so the LINE is the biggest
 * thing in a row and the buttons sit under it on a phone rather than beside
 * it — a three-button row and a song title do not share 390px.
 */
.djq-list, .djq-done { display: grid; gap: 8px; }
.djq-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  padding: 10px; border-radius: var(--r-field);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--panel-line);
}
.djq-what { flex: 1 1 220px; min-width: 0; display: grid; gap: 2px; }
.dj-line { font-weight: 800; font-size: var(--fs-note); }
.djq-who { color: var(--ink-dim); }
.djq-buttons { flex: 0 0 auto; display: flex; gap: 6px; }
.djq-buttons .minor { min-height: 40px; padding: 0 12px; }
.djq-done-row {
  display: flex; justify-content: space-between; gap: 10px;
  padding: 6px 10px; border-radius: var(--r-field);
  color: var(--ink-dim); font-size: var(--fs-note);
}
@media (max-width: 520px) {
  /* The line gets the row to itself and the buttons get a full one under it —
     at 390px the three of them beside a title left 40px for the song. */
  .djq-what { flex: 1 1 100%; }
  .djq-buttons { flex: 1 1 100%; }
  .djq-buttons .minor { flex: 1 1 0; }
}

/* --------------------------------------------------------- the DJ set's door
 * Three things: start it, put it on the screen, drive it. Nothing else belongs
 * on this page, so it is centred and short rather than laid out like a console.
 */
.dj-door { max-width: 560px; margin: 0 auto; text-align: center; display: grid; gap: 14px; }
.dj-door-brand { display: flex; align-items: center; justify-content: center; gap: 8px; font-size: var(--fs-head); }
.dj-door-links { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
/*
 * THE ORDINARY BUTTON IS SPELT OUT HERE BECAUSE `.minor` IS SCOPED.
 *
 * Every `.minor` rule in this sheet is under `.host` or `.console`, so on a
 * page that is neither the class means nothing and an `<a class="minor">`
 * rendered as a bare underlined link — *nothing clickable is a flat grey
 * box*, and a naked link beside a filled gradient is exactly that fault
 * wearing the browser's own default. Caught by looking at the screenshot.
 *
 * The treatment is the ordinary role's, unchanged: a top-lit face, and the
 * account's own colour on the EDGE rather than the face.
 */
.dj-door-links a.minor {
  min-height: 44px; padding: 0 18px;
  display: inline-flex; align-items: center;
  border-radius: var(--r-field);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.04));
  border: 1px solid var(--panel-line); border-bottom: 2px solid var(--hot);
  color: var(--ink); font-weight: 800; text-decoration: none;
}
.dj-door-note { color: var(--ink-dim); }
.dj-door-warn { color: var(--ink-dim); }
