/* Cueforge — the Pro Pool language.
 *
 * A pool room at night: near-black with a green baize glow, brass for the
 * numbers that matter and chalk-blue for whatever is live right now. Three
 * faces do three jobs and never swap — a display face for figures, a
 * condensed sans for prose, a mono for labels and data. That separation is
 * the hierarchy; size is only the seasoning.
 *
 * Deliberately one theme. This is a dark room by design, and a light version
 * would be a different product rather than the same one inverted.
 */
:root {
  --bg:        #0A0C0F;   /* the room */
  --card:      #12151A;   /* a plate on the wall */
  --raised:    #0D0F13;   /* a control at rest */
  --line:      #1A1D22;   /* hairline */
  --line-2:    #232730;   /* a control's edge */

  --felt:      #0F3D2E;   /* baize, for the one green button */

  --chalk:     #2DD4BF;   /* live, yours, good */
  --brass:     #C9A227;   /* the number that matters */
  --ivory:     #EDE8DD;   /* text */
  --muted:     #7C8288;   /* secondary text */
  --faint:     #3A3E44;   /* disabled, empty */
  --danger:    #C25450;   /* it went against you */

  --red:       #D1483C;   /* the balls, and only the balls */
  --yellow:    #E0B33C;

  --radius:    10px;
  --radius-sm: 8px;
  --pill:      999px;

  --display: 'Wallpoet', ui-serif, serif;
  /* The reading face. Was Rajdhani — condensed, squared-off, and handsome in
     a heading but tiring in a paragraph and cramped at small sizes, which is
     most of this app. The system's own UI face instead: it is the one the
     reader already reads everything else in, it is hinted for their screen,
     and it costs no bytes at all. Four woff2 files left the boot with it.
     Wallpoet stays on the wordmark and the rating; the mono stays on labels
     and data. Only prose changed. */
  --body:    system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue',
             Arial, sans-serif;
  --mono:    'Share Tech Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ivory);
  font-family: var(--body);
  font-size: 15px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  font-variant-numeric: tabular-nums;
}

/* 30rem, not 24. The cap is there to keep a phone-shaped column on a desktop
   monitor, but at 24rem (384px) it also bit on PHONES: anything reporting more
   than 384 CSS px — a Pixel at 412, or any phone whose Android display size is
   set smaller — got the app letterboxed in the middle of its own screen with
   40px of dead margin each side, while a 360px Samsung filled edge to edge.
   Same build, same CSS, two very different-looking phones. 30rem (480px)
   clears every phone in portrait and is still a column on a monitor. */
#app { max-width: 30rem; margin: 0 auto; min-height: 100vh;
       padding: 0 1rem 3rem; }

/* Desktop mode — GitHub issue #52. OFF unless asked for: the phone column is
   the default and stays it, because this app is used standing at a table.

   Not a media query. "I am on a wide screen" and "I want the wide layout" are
   different claims — a tablet on a pub wall is 1024px and wants the column,
   and the kiosk already opts out on its own terms below. So this keys on an
   attribute the person sets, written to the root before first paint by
   index.html so a laptop does not render a phone column and then jump.

   72rem, not `none`. A list of two-word rows stretched across a 34-inch
   monitor is not a desktop layout, it is a phone layout with the eye asked to
   travel a foot between a name and its score. This is roughly the width the
   longest thing here — the stats board — actually wants. */
:root[data-width="desktop"] #app { max-width: 72rem; }

/* The chrome hangs off `#app` with negative margins, so it follows the cap on
   its own and needs nothing here. The BOARD is the one thing that was built
   to be six columns on a phone and has room to breathe when there is room:
   letting it use the width is most of what "desktop mode" means in practice.

   Two columns for the lists that are cards rather than tables — circles,
   venues, competitions. `auto-fill` with a floor rather than a fixed two, so
   the window can be any size and the rows still fill it without a media
   query per breakpoint. */
:root[data-width="desktop"] [data-testid="circle-list"],
:root[data-width="desktop"] [data-testid="venues-screen"],
:root[data-width="desktop"] [data-testid="league-cups"] {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
  gap: .5rem; align-items: start;
}
/* A label introduces the list under it, so it spans rather than taking a
   cell of its own and leaving a hole beside it. */
:root[data-width="desktop"] [data-testid="circle-list"] > label,
:root[data-width="desktop"] [data-testid="venues-screen"] > label,
:root[data-width="desktop"] [data-testid="venues-screen"] > input,
:root[data-width="desktop"] [data-testid="league-cups"] > label {
  grid-column: 1 / -1;
}
/* The chrome spans the full width; the padding above is for content. */
.brand, .tabs, .matchbar { margin-inline: -1rem; }

/* Labels, counts, timestamps and statuses wear the mono; PROSE DOES NOT.
   It is how the eye separates chrome from content without a rule or a box —
   but run a paragraph through it and the whole screen reads as a readout. */
.mono, .pending, .tag { font-family: var(--mono); letter-spacing: .05em; }

/* A section heading and an input's label are the same kind of thing — chrome
   telling you what something is — so they move together. Only the FACE
   changes: the letterspacing, size, weight and colour are what make these
   read as headings rather than prose, and they stay. The mono keeps every
   other use, where it is doing real work: a code, a scoreline or a timestamp
   should read as data, and the comment above is right that running a heading
   through it makes the whole screen read as a readout. */
label { font-family: var(--body); }

/* ---- the brand lockup ------------------------------------------------- */

/* Brushed metal, lit from the top left. Applied to text, so it needs the
   clip; the drop-shadow is what stops it reading as flat yellow. */
.gold {
  background-image: linear-gradient(160deg, #FFE9A8 0%, var(--brass) 28%,
                                    #7A5A0F 50%, var(--brass) 72%, #FFE9A8 100%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
  filter: drop-shadow(0 0 6px rgb(201 162 39 / .35));
}

.brand { display: flex; align-items: center; justify-content: space-between;
         padding: 1.15rem 1rem 0; margin: 0; }
.brand .wordmark { font-family: var(--display); font-size: 1.65rem;
                   letter-spacing: .19em; line-height: 1.1; }
/* The wordmark and its stage, on one line and baseline-aligned: `alpha` is a
   qualifier of the name, and a second row would read as part of the "who is
   signed in" line under it. */
.wordmark-row { display: flex; align-items: baseline; gap: .45rem; }
/* Deliberately quiet. It has to be legible enough that nobody is surprised by
   a rough edge, and small enough that it is not competing with the wordmark
   it qualifies — an outline rather than a filled badge, for the same reason. */
.brand .stage {
  font-family: var(--body); font-size: .625rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--brass); border: 1px solid rgb(201 162 39 / .45);
  border-radius: 999px; padding: .1rem .4rem; line-height: 1;
  position: relative; top: -.3rem;
}
/* 14px. The mono face runs small and this line is the one that says who is
   signed in — the thing you glance at to check you are you. */
.brand .where { font-family: var(--body); font-weight: 500; font-size: .875rem;
                color: var(--muted); letter-spacing: .04em; }
/* A cue ball, lit from the same corner as everything else — and the way in to
   your account. It is a <button>, so the base button rule below would give it
   full width and padding if these did not say otherwise; the class beats the
   element selector on everything else. */
.ball {
  width: 2.25rem; height: 2.25rem; border-radius: 50%; flex: none;
  padding: 0; margin: 0;
  background: radial-gradient(circle at 35% 30%, var(--ivory), #C4BFAF 55%, #9A9585 100%);
  box-shadow: 0 2px 10px rgb(0 0 0 / .5);
  cursor: pointer;
}
/* Enough to say it is pressable without turning the lockup into a control. */
.ball:hover { box-shadow: 0 2px 10px rgb(0 0 0 / .5), 0 0 0 2px var(--faint); }
.ball:focus-visible { outline: 2px solid var(--chalk); outline-offset: 2px; }

/* The bell and the cog, as ONE child of the lockup. GitHub issue #49.
   `.brand` is `space-between` and depends on having exactly two children: a
   third would space the wordmark, the bell and the cog evenly across the row
   and drag the name off the left edge — the failure the comment in shell.js
   records from the last time a control was added here. */
.brand-controls { display: flex; align-items: center; gap: .5rem; flex: none; }

/* Role marks beside a name — GitHub issue #59. Inline with the text and
   baseline-ish, so a name with one does not sit a pixel lower than a name
   without: a list where the rows jog is harder to read than one with no
   marks at all. `flex: none` so a long name never squeezes the mark into a
   sliver. */
.rolemarks { display: inline-flex; align-items: center; gap: .25rem;
             flex: none; vertical-align: -2px; margin-right: .35rem; }
.rolemarks > span { display: inline-flex; }
/* `.brassc` already existed; this is its counterpart, added here because the
   scoped roles want a different tone from the site-wide ones and there was
   no chalk text class. */
.chalkc { color: var(--chalk); }

/* The unread count, riding on the ball rather than sitting beside it: a
   number in the lockup would be a third thing competing with the wordmark,
   and this is the shape every phone already reads as unread. Positioning it
   needs the ball to be a containing block, which is why that is set here and
   not on every ball in the app. */
.brand-controls .ball { position: relative; }
.dot { position: absolute; top: -2px; right: -2px; min-width: 1.1rem;
       height: 1.1rem; padding: 0 .2rem; border-radius: var(--pill);
       background: var(--danger); color: var(--ivory);
       font-family: var(--mono); font-size: .625rem; line-height: 1.1rem;
       box-shadow: 0 0 0 2px var(--bg); }

/* ---- tabs -------------------------------------------------------------- */

.tabs { display: flex; border-bottom: 1px solid var(--line); margin-top: .9rem; }
.tabs button {
  flex: 1; display: flex; flex-direction: column; align-items: center; gap: .25rem;
  padding: .6rem 0 .5rem; margin: 0; width: auto;
  background: none; border: none; border-bottom: 2px solid transparent;
  border-radius: 0;
  /* 13px, in the reading face. These are the app's four destinations and were
     the smallest type on the screen, set in a mono that reads small at
     uppercase — the words are labels for places, not data, and the mono is for
     data. At this size VENUES is the widest; check 320px before growing it. */
  font-family: var(--body); font-weight: 600; font-size: .8125rem;
  letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
}
.tabs button[aria-current="page"] { color: var(--ivory); border-bottom-color: var(--chalk); }
.tabs svg { width: 16px; height: 16px; }

/* Vertical rhythm lives in exactly two places: this gap between the blocks of
   a screen, and .stack's gap between the rows inside one. Nothing sets its own
   top margin to make room — a margin plus a gap add up, so a screen whose
   spacing came from both measured 24px after a button and 20px after anything
   else while claiming a flat 20. */
.screen { padding: 1rem 0; display: flex; flex-direction: column; gap: 2rem; }
/* A labelled group — a heading and its rows — inside one of those blocks. */
.stack { display: flex; flex-direction: column; gap: .5rem; }

/* A stack that belongs to the thing above it. Padding and a rule down the
   left, not a margin -- the gap between siblings is the parent's job, and a
   margin here would fight it. Used for the teams under a league on the venue
   page, where a flat run of cards left "which teams are in which league"
   answerable only by reading the badge on every row. */
.indent {
  padding-left: .75rem;
  border-left: 2px solid var(--line-2);
}

/* ---- type -------------------------------------------------------------- */

/* Not the display face. Wallpoet earns its keep on the wordmark and on a
   number you read across a room; a heading is a sentence, and a sentence
   set in an ornament is harder to read for no gain. */
h1, h2 { font-family: var(--body); font-size: 1.15rem; letter-spacing: .02em;
         font-weight: 600;
     margin: 0; text-wrap: balance; }
/* The two sizes a card is topped with: `label` is the heading ("YOUR
   CIRCLES"), `.sub` the line under it. Both were two pixels smaller — 10px
   and 12px — and at that size, uppercase, letter-spaced and in muted grey,
   the heading was hard to read at arm's length, which is the distance a
   phone is actually held at in a pub. They move together: a subtitle larger
   than the heading above it reads as a mistake. */
.sub { color: var(--muted); font-size: .875rem; margin: 0; max-width: 36ch; }
label { display: block; font-size: .75rem; text-transform: uppercase;
/* No bottom margin: the .stack it sits in owns the gap to its rows. A
   margin here would add to that gap, not replace it. */
        color: var(--muted); margin: 0; letter-spacing: .1em; }

/* The answer to the screen. Display face, because a rating is the one number
   somebody came here to read. */
/* Wallpoet's glyphs are blocky and nearly touch at their default tracking:
   at gauge size "1522" reads as one shape. A little air is what makes it a
   number you can take in at a glance, which is the only job it has. */
.figure { font-family: var(--display); font-size: 1.9rem; line-height: 1;
          font-weight: 400; letter-spacing: .06em; }

/* ---- controls ---------------------------------------------------------- */

input, select {
  width: 100%; padding: .7rem .75rem;
  background: var(--raised); border: 1px solid var(--line-2);
  border-radius: var(--radius-sm); color: var(--ivory);
  font-size: .95rem; font-family: var(--body); font-weight: 500;
  font-variant-numeric: tabular-nums;
}
input:focus, select:focus, button:focus-visible {
  outline: 2px solid var(--chalk); outline-offset: 2px;
}
input::placeholder { color: var(--faint); }

button {
  width: 100%; padding: .8rem;
  border: none; border-radius: var(--radius); cursor: pointer;
  background: var(--chalk); color: #06110E;
  /* What a button SAYS is a word, not a reading. The mono is for counts,
     timestamps and scores; an instruction to press something is prose with a
     border round it. */
  font-family: var(--body); font-size: .8125rem; font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase;
}
button:active { transform: translateY(1px); }
button.secondary { background: var(--raised); color: var(--muted);
                   border: 1px solid var(--line-2); }
button.secondary:hover { color: var(--ivory); border-color: var(--faint); }
/* The one button that looks like a table: baize under glass. */
button.felt {
  background: linear-gradient(180deg, var(--felt) 0%, #0B241B 100%);
  border: 1px solid rgb(45 212 191 / .35); color: var(--chalk);
  display: flex; align-items: center; justify-content: center; gap: .6rem;
  padding: .9rem 0; letter-spacing: .14em;
}

/* LAST of the plain-button rules, deliberately. It used to sit above .felt,
   which carries the same specificity and so won on order — leaving the kiosk's
   "Open the match" in full baize while it was disabled. A button that looks
   pressable and does nothing is worse than one that is plainly not ready. */
button:disabled { background: var(--line); color: var(--muted);
                  border-color: var(--line); box-shadow: none;
                  cursor: not-allowed; transform: none; }

/* Segmented picker — the spin wheels' job, done with something you can tap
   and tab to. Active takes the colour, the border and the glow. */
.seg { display: flex; flex-wrap: wrap; gap: .45rem; }
.seg button {
  width: auto; flex: 0 0 auto; margin: 0;
  padding: .55rem .9rem; border-radius: var(--pill);
  background: var(--raised); border: 1px solid var(--line-2);
  color: var(--muted); font-size: .6875rem; letter-spacing: .06em;
}
.seg button[aria-pressed="true"] {
  background: #151920; border: 2px solid var(--chalk); color: var(--chalk);
  box-shadow: 0 0 12px rgb(45 212 191 / .27);
  padding: calc(.55rem - 1px) calc(.9rem - 1px);
}
.seg.brass button[aria-pressed="true"] {
  border-color: var(--brass); color: var(--brass);
  box-shadow: 0 0 12px rgb(201 162 39 / .27);
}

/* ---- cards ------------------------------------------------------------- */

.card { background: var(--card); border: 1px solid var(--line);
        border-radius: var(--radius-sm); padding: .65rem .75rem; }
.card + .card { margin-top: .375rem; }
.card.act  { border-color: rgb(201 162 39 / .45); }
.card.done { border-color: rgb(45 212 191 / .4); background: #12201C; }
.card.warn { border-color: rgb(194 84 80 / .4); }

/* `gap` so the two ends can never touch. GitHub issue #45: with nothing
   between them, a name that happens to fill its side finishes hard against
   the status beside it and the two read as one run-on string. This is a
   floor, not spacing — `space-between` still pushes them apart whenever
   there is room. */
.rowbetween { display: flex; align-items: center; justify-content: space-between;
              gap: .75rem; }
/* Each end leans towards its own end. GitHub issue #51.

   `space-between` positions the two BOXES and says nothing about the text
   inside them, and most of these rows are buttons — which centre their text.
   On one line that is invisible, because a box shrink-wrapped to its text has
   nowhere to centre it. The moment either side WRAPS, the second line centres
   under the first and the row reads as two ragged paragraphs adrift in the
   middle: "TEAMS IN THE / LEAGUE" on the left, "MATCH THE ARCHIVE'S TEAMS TO /
   CIRCLES HERE" on the right.

   Three places had already reached for `style="text-align:left"` on the left
   span — history.js, admin.js and circle.js — which is the shape of a rule
   that is wrong rather than one needing exceptions. Same reasoning as the gap
   rules: fix the rule, delete the local patches.

   `:not(:last-child)` so a row with a SINGLE child is left alone. Without it
   that child is both first and last, the later rule wins, and every one-sided
   row silently right-aligns. A middle child in a three-part row stays
   centred, which is where it wants to be. */
.rowbetween > :first-child:not(:last-child) { text-align: left; }
.rowbetween > :last-child:not(:first-child) { text-align: right; }

/* The competitions room: the NAME wraps, the status does not. Issue #45.

   Both ends default to `flex: 0 1 auto`, so both may shrink — and the browser
   shrinks whichever it likes, which turned out to be the short one. The
   archive holds "Division 1/2 Singles Knockout Main"; that stayed on one line
   crushed against the edge while "Not finished yet" broke into "Not finished
   / yet" beside it. Backwards: the status is three predictable words and the
   name is anything the league has ever called a competition.

   Pinning the status to its content leaves the name as the only thing that
   can give, which is what "split competition titles over multiple lines"
   asks for. Nothing is capped at a percentage — the name takes whatever the
   status does not need, so a short one still runs long and a long one wraps
   as many times as it must. */
.cuprow > :last-child { flex: 0 0 auto; }
.cuprow > :first-child { flex: 1 1 auto; min-width: 0; }

/* The circles list: name, venue, what it is. GitHub issue #53.

   A GRID rather than `rowbetween`, and that is the point of the change. The
   venue used to sit under the name in the left-hand column, so a long team
   name and its pub shared one narrow box — "Riverdene Royals" ran out of room
   and clipped. Moving the venue out needs a THIRD column, and three flex
   children under `space-between` put the middle one wherever that row's spare
   space happened to leave it: the venue would land at a different x on every
   row and read as scattered rather than as a column.

   Grid columns are declared once and every row obeys them, so the venues line
   up down the screen. `min-width: 0` on the first two so a long name or a long
   pub wraps inside its column instead of widening it. */
.circlerow { display: grid; grid-template-columns: 1fr 1fr auto;
             gap: .75rem; align-items: center; }
.circlerow > :nth-child(1), .circlerow > :nth-child(2) { min-width: 0; }
.circlerow > :nth-child(1), .circlerow > :nth-child(2) { text-align: left; }

/* A group of controls that folds away. GitHub issue #48.

   Bordered like the rows above it, so a closed one reads as another control
   in the column rather than as a stray line of text. `list-style: none` and
   the WebKit pseudo-element remove the browser's own triangle in both
   engines; the +/− is drawn here instead, because the built-in marker sits at
   a different offset per browser and lines up with nothing. */
.advanced { border: 1px solid var(--line); border-radius: var(--radius);
            padding: .6rem .75rem; }
.advanced > summary { cursor: pointer; list-style: none;
                      font-family: var(--mono); font-size: .625rem;
                      letter-spacing: .08em; text-transform: uppercase;
                      color: var(--muted); display: flex;
                      justify-content: space-between; align-items: center; }
.advanced > summary::-webkit-details-marker { display: none; }
.advanced > summary::after { content: '+'; font-size: .9rem; line-height: 1; }
.advanced[open] > summary::after { content: '−'; }
.advanced[open] > summary { margin-bottom: .6rem; }

.row { display: flex; gap: .5rem; }
.row > * { flex: 1; }
.row.pair > :first-child { flex: 1 1 auto; min-width: 0; }
.row.pair > button { flex: 0 0 auto; width: auto; padding-inline: 1rem; margin: 0; }

/* ---- match screen ------------------------------------------------------ */

.matchbar { display: flex; align-items: center; justify-content: space-between;
            padding: .9rem 1rem; border-bottom: 1px solid var(--line);
            font-family: var(--mono); font-size: .625rem; color: var(--muted);
            letter-spacing: .08em; }
.matchbar button { width: auto; margin: 0; padding: .2rem; background: none;
                   border: none; color: var(--muted); }

.scoreline { display: flex; align-items: center; justify-content: center;
             gap: 1.1rem; padding: 1.1rem 0 .25rem; text-align: center; }
.scoreline .name { font-family: var(--mono); font-size: .5625rem;
                   color: var(--muted); letter-spacing: .1em; }
/* Whoever is at the table is the live one. It is the thing you forget. */
.scoreline .attable .figure { color: var(--chalk); }
.scoreline .dash { font-family: var(--display); color: var(--faint); font-size: .85rem; }

.group { font-family: var(--mono); font-size: .53rem; letter-spacing: .06em;
         margin-top: .2rem; color: var(--muted); }
.group.red { color: var(--red); }
.group.yellow { color: var(--yellow); }

.balls { display: flex; gap: .9rem; justify-content: center; padding: .35rem 0 .6rem;
         font-family: var(--mono); font-size: .5625rem; color: var(--muted); }
.balls .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block;
              margin-right: .3rem; vertical-align: middle; }

.turn { text-align: center; margin-bottom: .6rem; }
.turn .tag { display: inline-block; font-size: .625rem; letter-spacing: .1em;
             padding: .3rem .85rem; border-radius: var(--pill);
             background: rgb(45 212 191 / .09); border: 1px solid rgb(45 212 191 / .35);
             color: var(--chalk); }
/* Two visits owed decides who is at the table for the next two shots, so it
   belongs on the badge rather than in a footnote. */
.turn .tag.owed { background: rgb(201 162 39 / .09);
                  border-color: rgb(201 162 39 / .4); color: var(--brass); }

/* The options button sits ON the turn line, not under it — it is a rare
   action and must not take a row of its own on a phone. */
.turn { position: relative; }
.turn .dots { width: 1.65rem; height: 1.65rem; padding: 0; margin: 0 0 0 .4rem;
              border-radius: 50%; display: inline-flex; align-items: center;
              justify-content: center; vertical-align: middle; }
.turn .menu { position: absolute; top: calc(100% + .4rem); left: 50%;
              transform: translateX(-50%); z-index: 5; width: 15rem;
              background: var(--card); border: 1px solid var(--line-2);
              border-radius: var(--radius-sm); padding: .4rem;
              box-shadow: 0 8px 24px rgb(0 0 0 / .5); }
.turn .menu button { display: flex; align-items: center; gap: .5rem;
                     text-align: left; text-transform: none;
                     font-family: var(--body); font-size: .8rem;
                     letter-spacing: normal; }

/* Two columns of equal weight, because no shot is the default one. */
.events { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; }
.events button {
  width: auto; margin: 0; display: flex; flex-direction: column;
  align-items: center; gap: .4rem; padding: .7rem .4rem;
  background: var(--raised); border: 1px solid #1E2229; color: var(--muted);
  font-size: .59rem; letter-spacing: .03em; line-height: 1.15; text-align: center;
}
.events button[aria-pressed="true"] { background: #151920; color: var(--chalk);
                                      border: 2px solid var(--chalk);
                                      padding: calc(.7rem - 1px) calc(.4rem - 1px); }
.events button.danger[aria-pressed="true"] { color: var(--danger); border-color: var(--danger); }
.events button.brass[aria-pressed="true"]  { color: var(--brass);  border-color: var(--brass); }
.events svg { width: 18px; height: 18px; }

/* Pot chips cap at what is left, so a count can never describe a table that
   cannot exist. */
.chips { display: flex; gap: .5rem; justify-content: center; flex-wrap: wrap; }
.chips button { width: auto; margin: 0; display: flex; align-items: center; gap: .4rem;
                padding: .5rem .75rem; border-radius: var(--pill);
                background: var(--raised); border: 1px solid var(--line-2);
                color: var(--muted); font-size: .625rem; }
.chips .dot { width: 10px; height: 10px; border-radius: 50%; }
.chips button.on { background: #151920; border-width: 2px;
                   padding: calc(.5rem - 1px) calc(.75rem - 1px); }
.chips button.on.red    { border-color: var(--red);    color: var(--red); }
.chips button.on.yellow { border-color: var(--yellow); color: var(--yellow); }
.chips button.on.black  { border-color: #D8D5CC;       color: #D8D5CC; }

.log { display: flex; flex-direction: column; gap: .3rem;
       max-height: 10rem; overflow-y: auto; }
.log .card { display: flex; justify-content: space-between; padding: .4rem .6rem;
             font-family: var(--mono); font-size: .625rem; margin: 0; }
.log .who { color: var(--muted); }
/* The colour a player is on, in the log. A letter as well as a hue, because
   red and yellow are exactly the pair a colourblind player cannot separate. */
.log .grp { display: inline-flex; align-items: center; justify-content: center;
            width: 1rem; height: 1rem; border-radius: 50%; margin-left: .3rem;
            font-style: normal; font-size: .5rem; font-weight: 700;
            vertical-align: middle; color: #0A0C0F; }
.log .grp.red { background: var(--red); color: var(--ivory); }
.log .grp.yellow { background: var(--yellow); }

/* ---- gauge and lists --------------------------------------------------- */

/* Sized in rem, not pixels. The text inside is in rem, so a fixed-pixel ring
   meant the text-size control grew the number and left the circle where it
   was — at "Large" it was already outside the ring, and at "Biggest" it was
   half as wide again as the circle holding it. Everything scales together
   now, so the relationship between the two is fixed at every size. */
.gauge { position: relative; width: 11.25rem; height: 11.25rem; margin: 0 auto; }
.gauge svg { display: block; }
/* Inset by the ring's own width plus air, so the box the text is centred in
   is the space INSIDE the ring rather than the whole square — a corner of a
   wide number is what touches the stroke first. */
.gauge .inner { position: absolute; inset: 1.6rem; display: flex;
                flex-direction: column; align-items: center;
                justify-content: center; text-align: center; }
.gauge .figure { font-size: 2rem; line-height: 1.05; }
.gauge .cap { font-family: var(--mono); font-size: .625rem; color: var(--muted);
              letter-spacing: .06em; margin-top: .3rem; }
.gauge .delta { font-family: var(--mono); font-size: .625rem; margin-top: .3rem;
                display: flex; align-items: center; gap: .2rem; }

.result { width: 22px; height: 22px; border-radius: 50%; flex: none;
          display: flex; align-items: center; justify-content: center;
          font-family: var(--mono); font-size: .625rem; font-weight: 700; }
.result.w { background: rgb(45 212 191 / .13); color: var(--chalk); }
.result.l { background: rgb(194 84 80 / .13); color: var(--danger); }

.form { display: flex; gap: .2rem; margin-top: .2rem; }
.form i { width: 6px; height: 6px; border-radius: 50%; background: var(--danger); }
.form i.w { background: var(--chalk); }

.rank { width: 22px; text-align: center; font-family: var(--mono);
        font-size: .75rem; color: var(--muted); }
.rank.gold { color: var(--brass); }
.rank.silver { color: #C7CBD1; }
.rank.bronze { color: #B08D57; }

.qr { background: var(--ivory); padding: .6rem; border-radius: var(--radius-sm);
      display: inline-block; line-height: 0; }
.qr svg { display: block; width: 168px; height: 168px; }

code { font-family: var(--mono); color: var(--brass); font-size: .9rem; word-break: break-all; }

.error { color: var(--danger); font-size: .8rem; margin: .6rem 0 0; }
.ok { color: var(--chalk); font-size: .8rem; margin: .6rem 0 0; }
.pending { color: var(--brass); font-size: .7rem; }
/* Padding, not margin, and only sideways-safe: an empty state sits in a
   .stack like anything else and takes its spacing from the gap. */
.empty { color: var(--faint); font-size: .8rem; text-align: center; margin: 0; }

.chalk { color: var(--chalk); }
.brassc { color: var(--brass); }
.dangerc { color: var(--danger); }
.mutedc { color: var(--muted); }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* ---- the tablet on the wall -------------------------------------------- */

/* The venue's name and whether a game is on, which is what somebody walking up
   to a wall screen is asking. Its own rule rather than .rowbetween so the
   status can never be squeezed out by a long pub name. */
.kioskhead { display: flex; align-items: baseline; justify-content: space-between;
             gap: .75rem; }
.status { font-family: var(--mono); font-size: .625rem; letter-spacing: .1em;
          text-transform: uppercase; color: var(--muted); flex: none;
          border: 1px solid var(--line-2); border-radius: var(--pill);
          padding: .25rem .6rem; }
.status.on { color: var(--chalk); border-color: rgb(45 212 191 / .45);
             background: rgb(45 212 191 / .08); }

/* The phone column is capped at 30rem so the app is not letterboxed on a
   monitor. A tablet on a wall is landscape, nobody is holding it, and that cap
   would strand the venue's name in the middle of a ten-inch screen. Scoped by
   :has so the cap itself is untouched — every phone keeps the width it had. */
#app:has(.kiosk) { max-width: 56rem; }
/* The match on a wall takes the whole screen: scores belong at the EDGES, and
   a 30rem column puts them either side of the middle instead. */
#app:has(.wallscreen) { max-width: none; padding-inline: clamp(1rem, 4vw, 3rem); }
/* Killer likewise. Four buttons across and a table of up to a dozen names
   under them, inside a 30rem column, is four narrow buttons and one long
   list — the cap has to lift for the same reason it lifts for a match. */
#app:has(.killerscreen) { max-width: none;
                          padding-inline: clamp(1rem, 4vw, 3rem); }
/* Read from across the room, not from your hand. */
/* Its own gap, like .koscreen, .killerscreen and .wallacts .screen: the only
   wall-facing variant without one inherited the phone's 2rem, which on a
   1280x720 kiosk with nothing on grew the document from 844px to 904px
   against a 720px viewport -- and it only grows from there as a match
   goes live. */
.kiosk { gap: clamp(.75rem, 2vh, 1.25rem); }
.kiosk h1 { font-size: 1.6rem; }
.kiosk .seg button { font-size: .8125rem; padding: .7rem 1.1rem; }
.kiosk button.felt { padding: 1.1rem; font-size: .875rem; }
/* Two tables side by side when there is room for them. */
.kiosk .tables { display: grid; gap: .5rem;
                 grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); }

/* ---- waiting ------------------------------------------------------------ */

/* A row's shape while its content is on its way. Reserving the space is the
   point: the alternative is a screen that empties, refills, and jumps twice. */
.skel { background: var(--raised); border: 1px solid var(--line);
        border-radius: var(--radius-sm); opacity: .55;
        animation: skel 1.4s ease-in-out infinite; }
@keyframes skel { 0%, 100% { opacity: .35 } 50% { opacity: .6 } }
@media (prefers-reduced-motion: reduce) {
  .skel { animation: none; }
}

/* A wrapper that is not a box: on a phone these two exist only so the wall
   can put the scores either side of them. */
.plain { display: contents; }

/* ---- the match, on a wall ----------------------------------------------- */

/* Written small-first, and the room-sized version is the enhancement.
   Doing it the other way round put a three-column layout with a 20rem middle
   on a phone held sideways, where it did not fit and scrolled sideways — the
   features looked like they were on top of each other because half of them
   were off the edge. A phone is not big enough for this and that is fine; it
   just has to say so by degrees rather than by breaking. */
.wallscreen {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: start; gap: .5rem clamp(.5rem, 3vw, 1.5rem);
}
/* The controls go under both scores until there is width to sit between them. */
.wallacts { grid-column: 1 / -1; order: 3;
            display: flex; flex-direction: column;
            gap: clamp(.5rem, 1.4vh, 1rem); min-width: 0; }
.wallscreen .wall { text-align: center; min-width: 0; }
.wallscreen .name { font-family: var(--mono); color: var(--muted);
                    font-size: clamp(.75rem, 2.4vw, 1.375rem);
                    letter-spacing: .12em; }
.wallscreen .figure { font-size: clamp(2.25rem, 9vw, 5rem); line-height: .95;
                      margin: .1rem 0; }
.wallscreen .attable .figure { color: var(--chalk); }
.wallscreen .group { font-size: clamp(.5625rem, 1.8vw, 1rem); }
.wallscreen .left { font-family: var(--mono); color: var(--faint);
                    font-size: clamp(.5rem, 1.5vw, .875rem);
                    letter-spacing: .08em; margin-top: .3rem; }

.wallacts label, .wallacts .sub { text-align: center; margin: 0; }
.wallacts label { font-size: clamp(.75rem, 1.3vw, 1rem); }
.wallacts .sub { font-size: clamp(.875rem, 1.2vw, 1.0625rem); max-width: none; }
/* The controls sit inside .screen, so these are descendants, not children —
   a `>` here quietly matched nothing and left Confirm a 40px strip. */
.wallacts .screen { padding: 0; gap: clamp(.5rem, 1.4vh, 1rem); }
/* Whatever the size, a name in a button may wrap rather than push the layout
   wider than the screen. */
.wallacts button { overflow-wrap: anywhere; }
.wallacts .log { max-height: 30vh; overflow-y: auto; }

/* Room for the real thing: a tablet on a wall, landscape, at arm's length or
   further. Both dimensions, because a phone turned sideways has the width and
   nothing like the height. */
@media (min-width: 900px) and (min-height: 520px) {
  .wallscreen {
    /* minmax(0, 1fr), not 1fr: a bare 1fr is minmax(AUTO, 1fr), so the side
       with the longer name grows its own column, squeezing the middle and
       leaving the two sides unequal. */
    grid-template-columns: minmax(0, 1fr) minmax(20rem, 40rem) minmax(0, 1fr);
    align-items: center; gap: clamp(.75rem, 2.5vw, 2rem);
    min-height: calc(100vh - 7rem);
  }
  .wallacts { grid-column: auto; order: 0; }

  /* The number checked from the baulk end. It used to be the smallest thing
     on the screen. */
  .wallscreen .figure { font-size: clamp(4rem, 16vw, 11rem); line-height: .85; }
  .wallscreen .name { font-size: clamp(.875rem, 2.1vw, 1.375rem); }
  .wallscreen .group { font-size: clamp(.6875rem, 1.5vw, 1rem); }
  .wallscreen .left { font-size: clamp(.625rem, 1.3vw, .875rem); }

  /* The shot pad is what this screen is FOR, so it gets the room. Three
     across rather than two, each big enough to hit without looking. */
  .wallacts .events { grid-template-columns: repeat(3, 1fr);
                      gap: clamp(.4rem, 1vw, .75rem); }
  .wallacts .events button { padding: clamp(.6rem, 1.6vh, 1.1rem) .4rem;
                             gap: .5rem;
                             font-size: clamp(.6875rem, 1.25vw, .9375rem); }
  .wallacts .events svg { width: clamp(20px, 2.4vw, 30px);
                          height: clamp(20px, 2.4vw, 30px); }
  .wallacts .chips button { padding: clamp(.5rem, 1.2vh, .8rem) 1rem;
                            font-size: clamp(.75rem, 1.3vw, .9375rem); }
  .wallacts .chips .dot { width: 14px; height: 14px; }
  /* Confirm is pressed after every single shot. */
  .wallacts .screen > button { font-size: clamp(.8125rem, 1.5vw, 1.125rem);
                               padding: clamp(.85rem, 2.2vh, 1.35rem);
                               margin: 0; }
  .wallacts [data-testid=shot-confirm] { font-weight: 700; letter-spacing: .1em; }
  .wallacts .turn .tag { font-size: clamp(.6875rem, 1.4vw, 1rem); }

  /* Who won: side by side, and deliberately NOT the biggest thing here. It is
     one tap at the end of a frame; the pad above is a tap every visit. */
  .wallacts .whowon { display: grid; grid-template-columns: 1fr 1fr;
                      gap: clamp(.4rem, 1vw, .75rem); }
  .wallacts .whowon button { margin: 0; font-size: clamp(.75rem, 1.4vw, 1rem);
                             padding: clamp(.8rem, 2.2vh, 1.4rem) .5rem; }
  .wallacts .log { max-height: 22vh; }
}

/* A wrapper that is not a box: on a phone these exist only so the wall can
   put the scores either side of the controls. */
.whowon { display: contents; }

/* ---- the rulebook ------------------------------------------------------- */

/* Sized against the viewport rather than fixed at 12.8px. These are read in
   an argument, sometimes off a tablet on a wall from a few feet back, and a
   rule nobody can read settles nothing. On a phone it lands where it was. */
.rules [data-testid="rules-line"] { font-size: clamp(.8rem, 1.35vw, 1.0625rem);
                                    line-height: 1.45; }
.rules label { font-size: clamp(.75rem, 1.1vw, .9375rem); }
.rules .cite { font-size: clamp(.5625rem, .95vw, .8125rem); }
.rules > .sub { font-size: clamp(.875rem, 1.2vw, 1.0625rem); max-width: 60ch; }

/* ---- clocks ------------------------------------------------------------- */

/* Shown only when a match has one. A friendly should never have to look at a
   clock it did not ask for. */
.clocks { display: flex; justify-content: center; gap: clamp(.75rem, 3vw, 2rem);
          padding: .5rem 0 0; flex-wrap: wrap; }
.clock { display: flex; flex-direction: column; align-items: center; gap: .1rem; }
.clocklabel { font-family: var(--mono); font-size: clamp(.5rem, 1.1vw, .6875rem);
              letter-spacing: .12em; text-transform: uppercase;
              color: var(--muted); }
/* NOT the display face. A countdown is read at a glance, and Wallpoet's
   figures are an ornament — the reason the shot clock was hard to read. */
.clockleft { font-family: var(--mono); font-weight: 700; line-height: 1;
             font-size: clamp(1.4rem, 5vw, 3rem); color: var(--ivory); }
.clock.soon .clockleft { color: var(--brass); }
/* Out of time says so and keeps saying it — a clock that quietly sat at zero
   would be indistinguishable from one nobody had started. */
.clock.out .clockleft { color: var(--danger); animation: outoftime 1s steps(2) infinite; }
.clocksays { font-family: var(--mono); font-size: clamp(.5rem, 1vw, .625rem);
             letter-spacing: .06em; color: var(--danger); }
@keyframes outoftime { 0%, 100% { opacity: 1 } 50% { opacity: .35 } }
@media (prefers-reduced-motion: reduce) {
  .clock.out .clockleft { animation: none; }
}

/* ---- a circle's three ways in ------------------------------------------- */

/* The same shape as the tab bar, because it is the same idea: a few
   destinations, side by side, each an icon over a word. Stacked rows read as a
   settings list, which is what these are not. */
.ways { display: flex; border-block: 1px solid var(--line); }
.ways button {
  flex: 1; display: flex; flex-direction: column; align-items: center;
  gap: .3rem; padding: .7rem 0 .6rem; margin: 0; width: auto;
  background: none; border: none; border-radius: 0;
  font-family: var(--body); font-weight: 600; font-size: .8125rem;
  letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
}
.ways button:hover { color: var(--ivory); }
/* Which room you are in, the same way the tab bar says it. */
.ways button[aria-current="page"] { color: var(--ivory);
                                    box-shadow: inset 0 -2px 0 var(--chalk); }
.ways svg { width: 18px; height: 18px; }

/* ---- a figure you step ---------------------------------------------------*/

/* One number with a minus and a plus, in place of a row of presets. The figure
   is what somebody is reading, so it is the biggest thing here and the buttons
   flank it rather than competing with it. */
.stepper { display: flex; align-items: center; gap: .5rem; }
.stepper button { width: auto; flex: 0 0 auto; margin: 0;
                  min-width: 2.75rem; min-height: 2.75rem;
                  font-size: 1.25rem; line-height: 1; padding: 0; }
.stepval { flex: 1; text-align: center; font-family: var(--mono);
           font-weight: 700; font-size: clamp(1.1rem, 3.5vw, 1.5rem);
           letter-spacing: .04em; color: var(--ivory); }

/* The platform's own picker, dressed to match the inputs beside it. */
.combo { width: 100%; }

/* ---- chat --------------------------------------------------------------- */

.chat { display: flex; flex-direction: column; gap: .4rem;
        max-height: 46vh; overflow-y: auto; }
.said { background: var(--raised); border: 1px solid var(--line);
        border-radius: var(--radius-sm); padding: .5rem .65rem; }
/* Your own, marked without a second column: a room this small does not need
   one side of the screen kept empty. */
.said.mine { border-color: rgb(45 212 191 / .35); }
.said .who { display: flex; justify-content: space-between; align-items: baseline;
             gap: .5rem; margin-bottom: .2rem; }
.said .who span { font-family: var(--mono); font-size: .625rem;
                  letter-spacing: .06em; color: var(--muted); }
.said .when { flex: none; }

/* A poll inside a message. The bar is drawn behind the words rather than
   beside them, so an answer keeps its full width to be read at and the count
   is legible whether it won or got nothing. */
.poll { display: flex; flex-direction: column; gap: .3rem; }
.pollq { margin-bottom: .15rem; }
.polloption { position: relative; display: flex; align-items: center;
              gap: .5rem; width: 100%; text-align: left;
              background: var(--bg); border: 1px solid var(--line);
              color: var(--ivory); font: inherit; text-transform: none;
              letter-spacing: 0; padding: .45rem .6rem;
              border-radius: var(--radius-sm); overflow: hidden;
              min-height: 2.25rem; }
.polloption .fill { position: absolute; inset: 0 auto 0 0;
                    background: rgb(45 212 191 / .16);
                    transition: width .25s ease; }
.polloption .polltext { position: relative; flex: 1; }
.polloption .pollcount { position: relative; flex: none;
                         font-family: var(--mono); color: var(--muted); }
.polloption.chosen { border-color: rgb(45 212 191 / .55); }
.polloption.chosen .pollcount { color: var(--chalk); }
.polloption:disabled { opacity: 1; cursor: default; }

/* ---- venue bulletin ------------------------------------------------------ */

.bulletin { display: flex; flex-direction: column; gap: .35rem;
            background: var(--raised); border: 1px solid var(--line);
            border-left: 3px solid rgb(45 212 191 / .5);
            border-radius: var(--radius-sm); padding: .6rem .75rem; }
.bulletin .head { display: flex; justify-content: space-between;
                  align-items: baseline; gap: .5rem; }
.bulletin h4 { margin: 0; font-size: 1rem; }
.bulletin .tag { font-family: var(--mono); font-size: .625rem;
                 letter-spacing: .06em; color: var(--muted); flex: none; }
.bulletin.event { border-left-color: rgb(201 162 39 / .7); }
.bulletin .whenat { font-family: var(--mono); color: var(--chalk);
                    font-size: .8125rem; }

/* ---- killer -------------------------------------------------------------- */

/* Small-first, like the match on a wall, and for the same reason: a phone
   turned sideways has the width and nothing like the height, and the failure
   there should be "cramped" rather than "on top of each other". */
.killerscreen {
  display: grid; grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "head" "mid" "side" "order";
  gap: clamp(.5rem, 2vh, 1rem); align-content: start;
}
.killerhead { grid-area: head; display: flex;
              justify-content: space-between; align-items: baseline; }
.killerorder { grid-area: order; }
.killermid { grid-area: mid; }
.killerside { grid-area: side; }

/* Stacked, the names go under the buttons: a killer game can be a dozen
   people, and above them the buttons would be off the bottom of a phone. It
   lays itself out in as many columns as the width allows rather than one long
   list. On a wall it becomes the left-hand column instead — see below. */
.killerorder { display: grid; gap: .35rem; min-width: 0;
               grid-template-columns: repeat(auto-fill,
                                             minmax(min(100%, 13rem), 1fr)); }
.killerrow { display: flex; align-items: center; gap: .6rem;
             background: var(--raised); border: 1px solid var(--line);
             border-radius: var(--radius-sm);
             padding: clamp(.35rem, 1.2vh, .7rem) .6rem; min-width: 0; }
.killerrow .seat { font-family: var(--mono); color: var(--faint);
                   font-size: .75rem; flex: none; width: 1.2em; }
.killerrow .who { flex: 1; min-width: 0; overflow-wrap: anywhere;
                  font-size: clamp(.875rem, 2vw, 1.5rem); }
.killerrow .hearts { flex: none; display: flex; gap: .12em;
                     font-size: clamp(.9rem, 2.2vw, 1.6rem); }
.killerrow .heart { color: var(--red); line-height: 1; }
/* Whose shot it is, said by the row rather than by a marker somewhere else. */
.killerrow.attable { border-color: var(--chalk); background: var(--card); }
.killerrow.attable .who { color: var(--chalk); }
/* Out, not gone: the order is what it is, and a name disappearing from it
   would make the sequence look wrong to anybody counting round the table. */
.killerrow.out { opacity: .45; }
.killerrow.out .who { text-decoration: line-through; }
.killerrow .dead { font-family: var(--mono); font-size: .6875rem;
                   letter-spacing: .08em; color: var(--muted); }

.killermid, .killerside { display: flex; flex-direction: column;
                          gap: clamp(.5rem, 1.6vh, 1rem); min-width: 0; }
.killermid .sub { text-align: center; margin-inline: auto; }
.killerturn, .killerdone { text-align: center; }
.killerturn .name, .killerdone .name {
  font-family: var(--mono); color: var(--muted); letter-spacing: .12em;
  font-size: clamp(.6875rem, 1.6vw, 1rem); }
/* break-word, not anywhere: a name in a narrow middle column should wrap at
   its space and only be cut mid-word if one word will not fit at all.
   `anywhere` put "Former player" down the screen a letter at a time. */
.killerturn .figure, .killerdone .figure {
  font-size: clamp(1.75rem, 7vw, 4rem); line-height: 1.05;
  overflow-wrap: break-word; }
.killerdone .figure { color: var(--chalk); }
.killerturn .sub { margin: .2rem auto 0; max-width: none;
                   font-size: clamp(.6875rem, 1.4vw, 1rem); }

/* Square-ish and big: the four things that can happen to a shot, hit from
   arm's length by somebody holding a cue. */
.killerpad { display: grid; grid-template-columns: 1fr 1fr;
             gap: clamp(.4rem, 1.4vw, .9rem); }
.killerpad button { display: flex; flex-direction: column;
                    align-items: center; justify-content: center; gap: .2rem;
                    min-height: clamp(4.5rem, 16vh, 9rem);
                    padding: .5rem; overflow-wrap: anywhere; }
.killerpad .big { font-size: clamp(.9375rem, 2.4vw, 1.75rem);
                  line-height: 1.1; }
.killerpad .hint { font-family: var(--mono); font-size: .625rem;
                   letter-spacing: .08em; opacity: .75; }
/* Four buttons, three answers. Potted is the baize, because it is the one
   that costs nothing; the two that cost a life are the same red as the hearts
   they take away, so the pad is read by colour from across the room rather
   than by reading four labels. The default chalk is the app's "do this" and
   was exactly wrong on the two that punish you. */
.killerpad button.felt { flex-direction: column; padding: .5rem;
                         letter-spacing: normal; }
.killerpad button.cost { background: var(--danger); color: #1A0B0A;
                         border: 1px solid rgb(194 84 80 / .5); }
/* The black pays a life back, so it is neither the safe green nor the red. */
.killerpad button.gain { background: var(--brass); color: #14110A;
                         border: 1px solid var(--brass); }
/* Once the black is down it is down until the rack, and a dead button has to
   look dead — the disabled rule already says so, and this only stops the
   colour above outranking it. */
.killerpad button:disabled { background: var(--line); color: var(--muted);
                             border-color: var(--line); }

/* Room for the real thing: a tablet on a wall, landscape. The order down the
   left, the shot pad in the rest — which is the layout somebody standing at
   the table is looking at. */
/* A tablet on a wall, landscape: who is playing on the left, how it stands in
   the middle, and the buttons on the right where the hand that is holding a
   cue reaches. Three columns rather than a stack, because at this size a
   stack puts the thing being tapped below the fold. */
@media (min-width: 900px) and (min-height: 520px) {
  .killerscreen {
    /* Proportions rather than fixed widths: at 1024 a 22rem list and a 26rem
       pad left about 200px for the middle, which is narrower than the name it
       has to hold. The buttons get the largest share because they are the
       thing being hit from arm's length. */
    grid-template-columns: 1.1fr 1fr 1.35fr;
    grid-template-areas: "head head head" "order mid side";
    align-items: start; gap: clamp(.75rem, 2.5vw, 2rem);
  }
  /* Sized to its column, not to the window. */
  .killermid .figure { font-size: clamp(1.5rem, 3.4vw, 3.25rem); }
  /* One column now: it has a column of its own to be in. Scrolls past a
     tableful rather than shrinking the names to nothing. */
  .killerorder { grid-template-columns: minmax(0, 1fr); gap: .5rem;
                 max-height: 78vh; overflow-y: auto; }
  .killermid { align-self: center; }
  .killerpad { gap: clamp(.6rem, 1.6vw, 1.2rem); }
}

/* ---- a venue, as a player sees it ---------------------------------------- */

/* The postcode sits under the name and IS the map link — one target, big
   enough to hit, rather than an address you read and a button you press. */
.pin { display: inline-flex; align-items: center; gap: .4rem;
       align-self: flex-start; color: var(--chalk); text-decoration: none;
       font-family: var(--mono); font-size: .8125rem; letter-spacing: .06em;
       border: 1px solid var(--line-2); border-radius: var(--pill);
       padding: .35rem .7rem; }
.pin:hover { border-color: var(--chalk); }
.landlord { margin: 0; }

/* Free is the answer somebody wants, so it gets the colour that means good. */
.free { color: var(--chalk); margin: 0; }

/* Press to see what is under it. Hours and cloth are checked once and then
   never again; they must not push the noticeboard off the top of the page. */
button.panel { display: flex; justify-content: space-between;
               align-items: center; text-transform: none; letter-spacing: 0;
               font-size: .9375rem; }
button.panel .chev { font-family: var(--mono); color: var(--muted); }
button.panel.open { color: var(--ivory); border-color: var(--faint); }

.week { display: flex; flex-direction: column; gap: .3rem; }
.week .mono { color: var(--muted); font-size: .75rem; }
.daylabel { flex: none; width: 2.5rem; align-self: center;
            color: var(--muted); font-size: .75rem; }

/* The circle's strength, on the same line as its name — the list exists to
   be chosen from, and "which one am I any good for" is the choice. */
.standard { color: var(--muted); font-size: .6875rem; flex: none; }

/* ---- the kiosk's queue --------------------------------------------------- */

.waiting { color: var(--chalk); font-size: .875rem; }

/* Whoever has been called gets the width and the countdown: this is the
   screen they are walking towards, read while still crossing the room. */
.uptonow { text-align: center; background: var(--card);
           border: 1px solid rgb(45 212 191 / .45);
           border-radius: var(--radius); padding: .9rem; }
.uptonow .name { font-family: var(--mono); color: var(--muted);
                 letter-spacing: .12em; font-size: .6875rem; }
.uptonow .figure { font-size: clamp(1.5rem, 4vw, 2.75rem); line-height: 1.1;
                   margin: .1rem 0 .6rem; overflow-wrap: break-word; }

.line { list-style: none; margin: 0; padding: 0;
        display: flex; flex-direction: column; gap: .3rem; }
.line .seat { display: flex; align-items: center; gap: .6rem;
              background: var(--raised); border: 1px solid var(--line);
              border-radius: var(--radius-sm); padding: .4rem .6rem; }
.line .seat .mono { color: var(--faint); font-size: .75rem; flex: none; }
.line .seat .who { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.line .seat .tag { color: var(--muted); letter-spacing: .06em; }
.line .seat.called { border-color: rgb(45 212 191 / .5); }
.line .seat.here { border-color: var(--chalk); }

/* Three panels along the bottom of the wall: who is best, what plays here,
   and how to join in. They wrap to one column on anything narrow. */
.kioskgrid { display: grid; gap: .6rem;
             grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); }
.kioskgrid .card { align-content: start; }
.kioskgrid .figure.small { font-size: 1.125rem; }
.kioskgrid .qr { display: flex; justify-content: center; }
.kioskgrid .qr svg { width: 100%; max-width: 9rem; height: auto; }

/* Free play. Loud, because a table left claimed stops the queue — the way
   out has to be the most obvious thing on the screen while it stands. */
.claimed { display: flex; justify-content: space-between; align-items: center;
           gap: .75rem; background: var(--card);
           border: 1px solid rgb(201 162 39 / .5);
           border-radius: var(--radius); padding: .7rem .9rem; }
.claimed .mono { display: block; color: var(--brass);
                 letter-spacing: .08em; font-size: .8125rem; }
.claimed .sub { margin: 0; }

/* A game already on. Where "Start a match" would be, saying why it is not —
   a button that would be refused is worse than no button. */
.onthetable { display: flex; flex-direction: column; gap: .15rem;
              text-align: center; background: var(--card);
              border: 1px solid var(--line-2); border-radius: var(--radius);
              padding: .8rem; }
.onthetable .mono { color: var(--muted); letter-spacing: .08em;
                    font-size: .8125rem; }
.onthetable .sub { margin: 0; max-width: none; }

/* ---- the knockout -------------------------------------------------------- */

.koscreen { display: flex; flex-direction: column;
            gap: clamp(.5rem, 2vh, 1rem); }
.kohead { display: flex; justify-content: space-between; align-items: baseline; }
.kodone { text-align: center; }
.kodone .name { font-family: var(--mono); color: var(--muted);
                letter-spacing: .12em; font-size: .6875rem; }
.kodone .figure { font-size: clamp(1.75rem, 6vw, 3.5rem); color: var(--chalk);
                  overflow-wrap: break-word; }

/* Rounds side by side, scrolling sideways rather than shrinking: a bracket of
   sixteen is five columns, and squeezing them into a phone makes every name
   unreadable instead of making four of them off-screen. */
.kobracket { display: flex; gap: .6rem; overflow-x: auto;
             padding-bottom: .4rem; align-items: stretch; }
.koround { display: flex; flex-direction: column; gap: .4rem;
           justify-content: space-around; min-width: 10rem; flex: 1 0 10rem; }
.koround .name { font-family: var(--mono); font-size: .625rem;
                 letter-spacing: .08em; color: var(--muted);
                 text-align: center; }

.kotie { display: flex; flex-direction: column; gap: 2px;
         border: 1px solid var(--line); border-radius: var(--radius-sm);
         padding: 3px; background: var(--raised); }
.kotie.ready { border-color: rgb(45 212 191 / .5); }
.koslot { display: block; width: 100%; text-align: left;
          padding: .35rem .5rem; border-radius: 5px;
          font-size: .8125rem; overflow-wrap: anywhere;
          background: transparent; border: none; color: var(--ivory); }
.koslot.empty { color: var(--faint); font-family: var(--mono); }
.koslot.won { color: var(--chalk); }
.koslot.lost { color: var(--muted); text-decoration: line-through; }
/* A tie you can decide looks like something to press, because it is. */
.koslot.pick { background: var(--card); border: 1px solid var(--line-2);
               cursor: pointer; text-transform: none; letter-spacing: 0;
               font-weight: 400; }
.koslot.pick:hover { border-color: var(--chalk); color: var(--chalk); }
.kobye { font-family: var(--mono); font-size: .5625rem; color: var(--muted);
         letter-spacing: .08em; text-align: right; padding: 0 .5rem .2rem; }

/* The code that puts you in the line as yourself, beside the guest buttons —
   the choice between them is the point, so they are next to each other. */
.queueqr { display: flex; align-items: center; gap: .8rem;
           background: var(--card); border: 1px solid var(--line);
           border-radius: var(--radius); padding: .7rem .8rem; }
.queueqr .qr { flex: none; line-height: 0; }
.queueqr .qr svg { width: 5.5rem; height: 5.5rem; display: block; }
.queueqr .mono { font-family: var(--mono); color: var(--chalk);
                 font-size: .8125rem; letter-spacing: .06em; }
.queueqr .sub { margin: .2rem 0 0; }
.line .seat.mine { border-color: rgb(45 212 191 / .45); }

/* A table with nothing on it says so in the colour that means good. */
.mono.free { color: var(--chalk); }

/* The second row of the bar. Smaller than the first, because these are the
   things you go to occasionally and the first row is where you live. */
.tabs.more { margin-top: -.35rem; }
.tabs.more button { font-size: .625rem; padding: .5rem .25rem; }
/* Placeholders read as plainly unavailable. A control that does nothing when
   pressed reads as a fault, which is worse than one that is visibly not
   ready. */
.tabs.more button:disabled { background: transparent; color: var(--faint);
                             border-color: transparent; cursor: default; }

/* A league's seven rooms. `.ways` was built for three and ran them together
   into one unreadable word at seven, so this scrolls sideways instead of
   squeezing: every room stays legible and the ones off the edge are one
   swipe away, which is what a phone does with a long row of tabs anyway. */
/* A league's rooms. These used to scroll sideways, which meant half of them
   were off the edge and nothing said so — you had to discover the swipe. They
   wrap now, in the same four-across grid the tab bar uses, so every room is
   visible at once and the row reads as one control rather than a filmstrip.
   Eight rooms make two tidy rows of four on a phone. */
.rooms { display: grid; grid-template-columns: repeat(4, 1fr); gap: .35rem; }
.rooms button {
  width: auto; margin: 0; padding: .5rem .25rem;
  /* The tab bar's shape: reading face, uppercase, and small enough that
     "Competitions" fits a quarter of a 320px screen without wrapping mid-word. */
  font-family: var(--body); font-weight: 600; font-size: .625rem;
  /* No letter-spacing, unlike the tab bar. "Competitions" is twelve
     characters and a quarter of a 360px screen is about 80px — the tracking
     was the difference between fitting and breaking mid-word into
     "COMPETITIO / NS". */
  text-transform: uppercase; line-height: 1.15;
}

/* ---- a league table ----------------------------------------------------- */

/* Eight columns will not fit a phone, and shrinking the type until they do
   makes a table nobody can read standing at a bar. It scrolls sideways
   instead, with the position and the team held still — those are what you are
   looking for, and losing them off the left edge while hunting for points is
   how you end up reading the wrong row.

   `table.league.stats` (the Stats board) is the exception: reported, the
   Rating column — arguably the most important one on the board — sat off
   the edge of a phone until it was scrolled to. Its rules below narrow it
   to six columns rather than eight (Division moves under the player's
   name, Won/Lost share one column — see `cueforge/web/leagues.js`'s
   `Stats`) so the whole board fits without that scroll, instead of relying
   on the sideways-scroll escape hatch every other table here still uses. */
.tablewrap { overflow-x: auto; scrollbar-width: thin;
             border: 1px solid var(--line); border-radius: var(--radius);
             background: var(--card); }

table.league { border-collapse: collapse; width: 100%; font-size: .8125rem; }
table.league th, table.league td { padding: .5rem .45rem; text-align: left;
                                   white-space: nowrap; }
table.league thead th { font-size: .625rem; letter-spacing: .06em;
                        text-transform: uppercase; color: var(--muted);
                        font-weight: 500;
                        border-bottom: 1px solid var(--line-2); }
table.league tbody tr + tr td { border-top: 1px solid var(--line); }
table.league .num { text-align: right; font-variant-numeric: tabular-nums;
                    font-family: var(--mono, ui-monospace, monospace); }
/* The number the table is read for. */
table.league .pts { color: var(--brass); }
table.league td.sub { color: var(--muted); }

/* Position and team stay put while the numbers scroll under them. */
table.league th:nth-child(1), table.league td:nth-child(1),
table.league th:nth-child(2), table.league td:nth-child(2) {
  position: sticky; background: var(--card); }
table.league th:nth-child(1), table.league td:nth-child(1) { left: 0;
  padding-left: .6rem; }
table.league th:nth-child(2), table.league td:nth-child(2) { left: 2rem;
  box-shadow: 1px 0 0 var(--line); }

/* The Stats board's Division line, tucked under the player's name (see
   `cueforge/web/leagues.js`'s `Stats`) rather than in its own column.
   Smaller than the name above it, the same relationship a card's name
   and its venue already have (`.card` two-line rows elsewhere) — no
   margin of its own, `display:block` is what puts it on its own line. */
table.league.stats td .sub { font-size: .6875rem; }

/* Six columns fit a phone at the table's own size everywhere but the
   narrowest phones; below 400px the padding is what is spent, not the
   type — a table nobody can read is not "fitting" either. */
@media (max-width: 400px) {
  table.league.stats th, table.league.stats td { padding: .5rem .3rem; }
}

/* A name that goes somewhere, without looking like a button in a table. */
.linky { width: auto; background: none; border: 0; padding: 0;
         color: var(--chalk); font: inherit; text-align: left;
         cursor: pointer; }
.linky:hover { text-decoration: underline; }

/* Home over away, rather than "Home v Away" across. Two pub names and a "v"
   is the widest thing on a fixtures row by a distance, and it pushed the
   frame columns off a phone -- the table scrolled sideways to reveal the
   numbers that are the whole point of it. Stacking halves the column and
   costs a line nobody reads across anyway. */
.linky.stacked { display: flex; flex-direction: column; gap: .1rem;
                 line-height: 1.25; }

/* A fixture's frames. The winning side is the one you are looking for, so it
   is the one that is not grey. */
.screen .card .pts { color: var(--brass); }
select.combo[multiple] { padding: .3rem; }

/* The league calendar (issue #33). A grid, because the point of a month view
   is the EMPTY squares: "are we out over the bank holiday" is answered by
   the shape, not by reading a list of dates. */
.cal-week { display: grid; grid-template-columns: repeat(7, 1fr);
            gap: .2rem; text-align: center; font-size: .7rem; }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr);
            gap: .2rem; }
.cal-cell { min-height: 3.4rem; padding: .2rem; border-radius: .35rem;
            border: 1px solid var(--line); background: var(--raised);
            display: flex; flex-direction: column; gap: .1rem;
            overflow: hidden; }
/* A day outside the month is drawn but not bordered — the row has to keep
   its shape or the weekday columns stop lining up. */
.cal-blank { border-color: transparent; background: none; }
.cal-today { border-color: var(--brass); }
.cal-num { font-size: .65rem; color: var(--muted); line-height: 1; }
/* Each fixture is a whole tappable line rather than a dot: a dot says a night
   is busy and nothing else, and the next question is always "against who". */
.cal-fix { width: 100%; padding: .1rem .15rem; border: 0; background: none;
           text-align: left; font-size: .6rem; line-height: 1.15;
           color: var(--text); text-transform: none;
           white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cal-fix:hover { background: var(--line); border-radius: .2rem; }
/* A cup night is still a night the league is out, so it is shown and not
   filtered — coloured apart rather than left to look like a fixture. */
.cal-cup { color: var(--brass); }

/* The screen's own name, bottom-right of every page. Small and dim: it is
   for pointing at a screen when asking for a change, not part of the app's
   own furniture — but it has to survive a screenshot, which is how most
   requests arrive, so it is not so faint that a phone camera loses it. */
.pagetag { position: fixed; right: .35rem; bottom: .3rem; z-index: 40;
           font-family: var(--mono, monospace); font-size: .58rem;
           letter-spacing: .06em; color: var(--muted); opacity: .55;
           background: var(--bg); padding: .1rem .3rem; border-radius: .25rem;
           pointer-events: none; }

/* A knockout, as a TREE (issue #30). Rounds are columns, earliest on the
   left, each tie centred between the two that feed it, and the pair JOINED.

   NOTHING HERE DECIDES A POSITION. Every top and height is a slot number
   `layoutBracket` computed, and this only turns a slot into a distance. That
   is the whole fix: the three attempts before it left the vertical position
   to `justify-content: space-around`, which cannot align two columns holding
   different numbers of things — and could not have, because the round's
   label was itself one of the things being spaced, and a rem gap between
   ties is a fixed distance that does not scale with how many there are.

   Scrolls sideways rather than wrapping: a bracket that wraps is no longer a
   bracket, and on a phone a five-round cup cannot be anything else. */
.bracket { display: flex; gap: 0; overflow-x: auto; padding-bottom: .3rem;
           align-items: flex-start; --korow: 6.4rem; --kogutter: 1.4rem; }
/* WIDE ENOUGH FOR THE NAMES THE ARCHIVE ACTUALLY HOLDS, and it scrolls
   sideways rather than squeezing them (Ben, issue #30: "don't mind scrolling
   to the right if you need to widen the text boxes"). 12rem fitted "Ann v
   Bob" and put "Paul Smurf Richardson" through "Wellington Social Club Table
   1 (By the Bar)". A bracket cannot fit a phone anyway -- five rounds side by
   side is not a phone-shaped thing -- so the width goes to the names. */
.bracket-col { display: flex; flex-direction: column; flex: 0 0 auto;
               min-width: 17rem; }
/* The round's own name, quiet: the column IS the round, and a heavy label on
   each one competes with the names that matter. Outside the positioned area
   below, so it cannot shift what the arithmetic placed — which is exactly
   what it did when it was a flex child of the column. */
.bracket-col > .cal-num { text-align: center; padding-bottom: .35rem; }

.bracket-slots { position: relative; }
/* One slot: a tie's own row. The card is centred in it, so a tie with a
   venue line and one without still hang from the same middle. */
.bracket-slot { position: absolute; left: 0; right: var(--kogutter);
                height: var(--korow); display: flex; align-items: center; }
.bracket .card { margin: 0; width: 100%; }

/* One tie: a row per name, then the venue and anything else beneath. Aligned
   left and stacked, not a rowbetween -- two long names on one line is the
   overlap this replaced. */
.kotie { display: flex; flex-direction: column; align-items: stretch;
         gap: .1rem; text-align: left; padding: .45rem .55rem; }
.kotie .korow { display: flex; justify-content: space-between;
                align-items: baseline; gap: .5rem; }
/* The name may wrap onto a second line rather than colliding with the score;
   the score never wraps and never shrinks. */
.kotie .koname { text-transform: none; min-width: 0; overflow-wrap: anywhere; }
.kotie .korow .pts { flex: none; white-space: nowrap; }
.kotie .kometa { display: block; text-transform: none; font-size: .65rem;
                 overflow-wrap: anywhere; }

/* The lines, drawn from the same slots as the boxes and therefore unable to
   drift from them: a stub out of each feeder, a vertical spanning the
   feeders, and the onward stub at that vertical's midpoint — which IS the
   parent's slot, because the parent's slot is their mean. */
.bracket-stub, .bracket-join { position: absolute; pointer-events: none; }
.bracket-stub { right: calc(var(--kogutter) / 2); width: calc(var(--kogutter) / 2);
                border-top: 1px solid var(--line); }
.bracket-join { right: calc(var(--kogutter) / 2); width: 0;
                border-left: 1px solid var(--line); }
.bracket-join::after { content: ''; position: absolute; left: 0; top: 50%;
                       width: calc(var(--kogutter) / 2);
                       border-top: 1px solid var(--line); }
/* A lone feeder — a bye, a walkover — makes a vertical of zero height, which
   draws as the straight line onward that it actually is. The last column
   feeds nothing, so it has no gutter and no lines. */
.bracket-col:last-child .bracket-slot { right: 0; }
.bracket-col:last-child .bracket-stub,
.bracket-col:last-child .bracket-join { display: none; }
