/* ---------------------------------------------------------------------------
   Table experience (/table/{id}) — the small private space a handful of people
   gather in to play one game. Its own compact surface rather than a variant of
   the community clubhouse: a full-bleed, sticky teal band carrying the table's
   name and its invitation, with the roster and the game choice inline under it.
   Backed by TablePage.razor, TableHeader.razor, TableParticipants.razor,
   TableInvitePanel.razor, TableGlance.razor, TableGamePicker.razor,
   GamePickerRow.razor, TableReadyState.razor. See docs/design/group.md.
   --------------------------------------------------------------------------- */
/* The page itself is unbounded and unpadded: the band is a direct child of it, so it spans the screen at every
   width — the fix for a lead that used to stop at the reading column's edge and float in the middle of a tablet.
   Everything under the band is bounded by .table-page__cols instead. */
.table-page {
    /* What the collapsed band comes to — its two paddings plus the bar's tap-target floor. Declared once here
       because a second sticky element further down the page (the picker's own bar) has to park under it, and two
       independently-guessed numbers would drift apart the first time the band's padding changed. */
    --table-band-h: 4.1rem;
    /* The measure everything on this page lines up on. The band spans the screen but its contents must not: a
       title at the far left of a tablet with the roster centred under it reads as two pages stacked. The band pads
       itself out to this, so one number moves both. */
    --table-measure: 32rem;
    display: flex;
    flex-direction: column;
    width: 100%;
    padding: 0 0 2rem;
}
/* The reading column: everything the band does not carry. Bounded rather than full-width — a table's cards are
   sized for a phone, and a row of a list gets no better for being wider. */
.table-page__cols {
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
    width: 100%;
    max-width: var(--table-measure);
    margin: 0 auto;
    padding: 0.9rem 0.85rem 0;
}
/* The lobby column. It grows with its content and never scrolls on its own: the shell's .app-main is the single
   scroll context on this page, so a drag over the roster chains out to it. */
.table-page__main {
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
    min-width: 0;
}
/* The three states before a lobby exists — minting, redirecting, gone. Each is a whole screen with one line on
   it, so the line is centred in the screen rather than left at the top of an empty field: on a dark canvas a
   muted sentence in the corner of a blank page reads as a page that failed to load. Same posture as the
   matchmaking wait, which is the same moment of the same journey seen from the other entry point. */
.table-page--loading {
    flex: 1 0 auto;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    /* These states are the page's own direct children — there is no reading column to inherit padding from. */
    padding-inline: 0.85rem;
    padding-block: clamp(1rem, 6vh, 4rem);
}
.table-page__loading {
    margin: 0;
    color: var(--text-secondary);
    font-size: clamp(1.15rem, 3vw, 1.55rem);
    font-weight: 600;
    text-align: center;
}
.table-page__loading-back { margin: 0; font-size: 0.95rem; }
.table-page__warning {
    padding: 0.5rem 0.7rem;
    border-radius: 0.6rem;
    background: color-mix(in srgb, var(--warn-ember) 12%, var(--bg-card));
    border: 1px solid color-mix(in srgb, var(--warn-ember) 40%, transparent);
    color: var(--warn-ember-ink);
    font-size: 0.85rem;
}
.table-page__playing { padding: 0.4rem 0; color: var(--text-secondary); font-size: 0.9rem; }

/* The teal band the create-a-table card opens into: the same teal, the same object, so the panel that was
   pressed reads as having become this screen, with the illustration cropped in behind it.

   It is the page's own child rather than the reading column's, so it spans the screen at every width — a lead
   that stopped at a 32rem column read, on anything wider than a phone and narrower than a laptop, as a card that
   had failed to stretch. And it is sticky: the bar row pins to the top of the scrollport as the lobby scrolls
   under it, keeping the screen's colour and its two standing controls (which table this is, and the way out) in
   place, while the invitation it carries collapses away. The reader is never more than one press from the top,
   because the thing pinned there is the control that goes back. */
.table-header {
    position: sticky;
    top: 0;
    z-index: 3;
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
    /* Full-bleed background, bounded contents: the inline padding grows with the screen so the title, the way out,
       and the invitation sit on the same left edge as the roster below them. The floor is the phone's own gutter. */
    padding-block: 0.6rem 0.75rem;
    padding-inline: max(0.95rem, calc((100% - var(--table-measure)) / 2 + 0.85rem));
    overflow: hidden;
    background:
        radial-gradient(110% 130% at 88% 8%, color-mix(in srgb, var(--accent-teal) 55%, white), transparent 62%),
        linear-gradient(140deg, var(--accent-teal), var(--accent-teal-deep));
    color: var(--text-on-accent);
    transition: gap 0.22s ease, box-shadow 0.22s ease;
    /* The band opens rather than appears: it grows out of the card's proportions in the beat after the press,
       which is what carries the create-a-table panel's identity across the navigation. Short and once. `backwards`
       rather than `both` so the `from` state is held for the first paint (no flash of the open band before the
       entrance) but its `to` state is not retained once it ends: the `to` is an identity transform, and a permanent
       transform on a sticky element promotes it to its own compositing layer, which WebKit's touch-scroll does not
       reliably scroll — the table lobby would not move under a touch on iOS. */
    animation: table-header-open 0.32s var(--ease-spring, ease) backwards;
}
@keyframes table-header-open {
    from { transform: scaleY(0.86); opacity: 0.6; transform-origin: top center; }
    to   { transform: scaleY(1);    opacity: 1;   transform-origin: top center; }
}
@media (prefers-reduced-motion: reduce) {
    .table-header { animation: none; transition: none; }
}
/* Collapsed: the invitation is gone and the strip is riding over content, so it takes a shadow to lift off the
   page it is covering — the one cue that says this is a bar now rather than the top of the screen. */
.table-header--collapsed { gap: 0; box-shadow: 0 6px 16px -8px rgba(0, 0, 0, 0.55); }
/* Scenery, not an illustration the title shares a line with: anchored off the band's top-right corner and
   cropped by it, at the size and angle it had on the card that opened this screen. */
.table-header .app-icon.table-header__art {
    position: absolute;
    z-index: 0;
    /* Cropped by the band's right edge, not its top one: the table keeps its whole silhouette and simply runs
       out of the screen, the way it did on the card. */
    top: 0.2rem;
    right: -2.4rem;
    width: 12.5rem;
    height: auto;
    pointer-events: none;
    filter: drop-shadow(0 10px 16px rgba(0, 0, 0, 0.3));
    opacity: 1;
    transition: opacity 0.18s ease;
}
/* The collapsed strip is shorter than the illustration, so what would show of it is the tops of three chairs —
   scenery cropped past the point of reading as scenery. A bar is plain. */
.table-header--collapsed .app-icon.table-header__art { opacity: 0; }
/* A phone band makes the same case by width: the invitation card fills it, and what is left beside the card is
   two or three chair fragments at the screen edge with no table under them. */
@media (max-width: 30rem) {
    .table-header .app-icon.table-header__art { display: none; }
}
@media (prefers-reduced-motion: reduce) {
    .table-header .app-icon.table-header__art { transition: none; }
}
/* A wash from the title's side across the art, so the title reads at full contrast whatever the art does
   behind it — and the art still shows through on the right, where nothing is written. */
.table-header::after {
    content: "";
    position: absolute;
    z-index: 1;
    inset: 0;
    pointer-events: none;
    background: linear-gradient(100deg,
        color-mix(in srgb, var(--accent-teal-deep) 88%, transparent) 20%,
        transparent 68%);
}
/* The standing row: the table's name and the way out, on one line, over the art and its wash. */
.table-header__bar {
    position: relative;
    z-index: 2;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-height: 2.75rem;
}
.table-header__title {
    margin: 0;
    min-width: 0;
    font-size: clamp(1.35rem, 5.5vw, 1.9rem);
    line-height: 1.05;
    font-weight: 700;
    letter-spacing: 0.01em;
}
/* The title is a control, so it has to be sized like one — but it must not look like a button, because at rest
   it is simply the screen's heading. The affordance arrives with the collapse (the arrow below), which is also
   the only state in which pressing it does anything. */
.table-header__totop {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    max-width: 100%;
    min-height: 2.75rem;
    padding: 0;
    border: none;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
    overflow: hidden;
}
.table-header__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Drawn by the stylesheet rather than the markup so the heading's text content stays the table's name — the
   arrow is an affordance, not part of what the screen is called. */
.table-header--collapsed .table-header__totop::after {
    content: "↑";
    flex: 0 0 auto;
    font-size: 0.8em;
    opacity: 0.75;
}
/* The way out. Sized to the app's 44px tap-target floor rather than to its label: a pill this short is easy to
   miss with a thumb. Labelled rather than a chevron, because it drops the membership rather than retracing a
   step — and pushed to the bar's far end, away from the title it shares the row with. */
.table-header__leave {
    flex: 0 0 auto;
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2.75rem;
    min-height: 2.75rem;
    padding: 0.25rem 0.85rem;
    border-radius: 9999px;
    border: 1px solid rgba(255, 255, 255, 0.45);
    background: color-mix(in srgb, var(--scrim) 26%, transparent);
    color: inherit;
    font-size: 0.8rem;
    cursor: pointer;
}
.table-header__leave:hover { background: rgba(255, 255, 255, 0.16); }
/* The invitation's slot in the band, and the thing the collapse gives up. A 0fr/1fr grid row rather than a height
   or a max-height, so the open state is the invitation's own natural height (whatever the QR and the two controls
   come to) and the closed one is genuinely zero — no magic number to drift from the content. */
.table-header__invite {
    position: relative;
    z-index: 2;
    /* A panel, not a column of text: stretched across a tablet the two controls end up an arm's length from the
       code they belong to, and "Share invite" becomes a button the width of the screen. */
    max-width: 30rem;
    display: grid;
    grid-template-rows: 1fr;
    opacity: 1;
    visibility: visible;
    transition: grid-template-rows 0.22s ease, opacity 0.18s ease, visibility 0s;
}
.table-header__invite-inner { min-height: 0; overflow: hidden; }
/* Not merely faded: hidden. A control faded to nothing is still in the tab order and still announced, so a
   keyboard reaching past the collapsed strip would land on a Share button nobody can see. The visibility flip is
   delayed by the length of the collapse so the fade is still watchable, and reverses instantly on the way open. */
.table-header--collapsed .table-header__invite {
    grid-template-rows: 0fr;
    opacity: 0;
    visibility: hidden;
    transition: grid-template-rows 0.22s ease, opacity 0.18s ease, visibility 0s linear 0.22s;
}
@media (prefers-reduced-motion: reduce) {
    .table-header__invite,
    .table-header--collapsed .table-header__invite { transition: none; }
}

/* Who is here — a row of faces, not a list of rows. A table is a ring of seats, so the people who have arrived
   read best side by side, each face over its own name; the row wraps rather than scrolls, so a big table takes
   a second line instead of hiding anyone. No section heading: three faces say "people" better than the word. */
/* Six columns, always — the group's membership cap, so the ring is the table's whole seating and the empty
   chairs in it are the room that is genuinely left. Fractional columns rather than fixed slots, so six seats
   fit the width of the narrowest phone instead of orphaning one onto a second line. */
/* The roster: one seat per place at the table, filled or waiting. The track count is not fixed at the table's
   size — six 1fr columns on a phone leave each seat about 55px, which is narrower than the word under it, and
   "Waiting…" arrives as "Waitin…" on every empty chair. Sizing the tracks instead lets the row wrap to a second
   line when the seats outnumber the width, which costs one line and keeps every label whole. */
.table-people {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(4.2rem, 1fr));
    align-items: start;
    gap: 0.6rem 0.35rem;
    margin: 0;
    padding: 0.15rem 0;
    list-style: none;
}
.table-people__person {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.3rem;
    min-width: 0;
    text-align: center;
}
.table-people__avatar { --avatar-size: clamp(2.3rem, 11vw, 3.3rem); flex: 0 0 auto; box-shadow: var(--shadow-sm); }
/* An empty chair: the same disc as a face, drawn as an outline so a seat reads as waiting rather than taken. */
.table-people__empty {
    display: grid;
    place-items: center;
    width: clamp(2.3rem, 11vw, 3.3rem);
    height: clamp(2.3rem, 11vw, 3.3rem);
    border-radius: 9999px;
    border: 2px dashed color-mix(in srgb, var(--text-muted) 45%, transparent);
    font-size: clamp(1rem, 5vw, 1.4rem);
    line-height: 1;
    filter: grayscale(1);
    opacity: 0.55;
}
/* A shade smaller than a person's name, so "Waiting…" fits a sixth of a narrow phone whole rather than
   truncating to "Waitin…" — and so the empty chairs stay quieter than the people beside them. */
.table-people__name--waiting { color: var(--text-muted); font-weight: 500; font-size: 0.7rem; }
/* The viewer's own face, ringed in the table's teal so they find themselves without reading the names. */
.table-people__avatar--you {
    box-shadow: 0 0 0 2px var(--bg-primary), 0 0 0 4px var(--accent-teal), var(--shadow-sm);
}
.table-people__name {
    font-size: 0.78rem;
    font-weight: 600;
    line-height: 1.15;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 100%;
}
.table-people__alone { margin: 0; color: var(--text-muted); font-size: 0.85rem; }

/* Once a game is chosen: the shared gathering panel under the game the table settled on. */
.table-ready { display: flex; flex-direction: column; gap: 0.5rem; }

/* The game slot — what the table is playing, and the way to put it back. A row rather than a bare heading
   because the name is not the only thing here: a choice that can be undone has to show its undo, and a Change
   control floating loose under the title would read as an action on the seats below it. */
.table-slot {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.6rem 0.75rem;
    border-radius: 0.8rem;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
}
.table-slot__text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.table-slot__label {
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}
.table-slot__game {
    margin: 0;
    font-size: 1.15rem;
    font-weight: 700;
    line-height: 1.15;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* Quiet on purpose: the table's forward path is Start, below in the gathering panel, and a second prominent
   button beside the game's name would compete with it for the press. */
.table-slot__change {
    flex: 0 0 auto;
    margin-left: auto;
    min-height: 2.75rem;
    padding: 0.35rem 0.9rem;
    border-radius: 9999px;
    border: 1px solid var(--border-color);
    background: var(--bg-secondary);
    color: var(--text-secondary);
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
}
.table-slot__change:hover {
    border-color: color-mix(in srgb, var(--accent-blue) 45%, var(--border-color));
    color: var(--text-primary);
}

/* The table in one line — faces and a count — for the picker's sticky bar. Overlapped rather than spaced, so a
   full table costs about the width of two avatars instead of six, leaving the bar's room for the search.

   Hidden until the roster has scrolled under the band: the glance is the roster's condensed stand-in for the
   moment the full ring of seats has left the screen, so the two together read as the same faces twice. A class
   the band's scroll module toggles (table-header.js) reveals it once the roster's bottom has passed the band's;
   with no script it stays hidden, which is the cleaner fallback than a duplicated set of faces. */
.table-glance { display: none; align-items: center; gap: 0.5rem; min-width: 0; }
.table-glance--revealed { display: flex; }
.table-glance__faces { display: flex; align-items: center; margin: 0; padding: 0; list-style: none; }
.table-glance__face + .table-glance__face { margin-left: -0.55rem; }
.table-glance__avatar {
    --avatar-size: 1.75rem;
    /* A ring in the bar's own stock, so overlapping discs stay separable rather than merging into one blob. */
    box-shadow: 0 0 0 2px var(--bg-card);
}
.table-glance__count {
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--text-secondary);
    white-space: nowrap;
}

/* --- The game picker (TableGamePicker.razor) --------------------------------
   The table's game choice, inline under the lobby instead of in a dialog over it. It is the tallest thing on the
   page and it is meant to be: the page scrolls into it, which is a shape a phone handles natively and a 90vh
   card floating on a scrim never did. Sized to stay usable as the catalog grows — the list virtualizes, so the
   rules here describe one row and the box around it, never a count of them. */
.table-picker { display: flex; flex-direction: column; min-width: 0; }
/* The section's name. Quiet to the eye — a list of games says what it is long before a heading is read — but a
   real heading, so the question the page is asking sits in the document outline where someone walking headings
   will find it. */
.table-picker__title {
    margin: 0 0 0.5rem;
    font-size: 0.8rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-secondary);
}

/* One sticky element carrying both the table's glance and the search, so scrolling into the catalog never costs
   sight of who is waiting or the way to filter. Its own card stock rather than the page canvas: the canvas is a
   gradient, and a translucent bar over scrolling rows is unreadable at any opacity that still looks like a bar.
   The search wraps to its own line rather than being squeezed — flex-basis, not a breakpoint, because what
   decides it is how wide the count's own text runs. */
.table-picker__bar {
    position: sticky;
    /* Under the band, not behind it: by the time this bar is pinning, the band above has collapsed to its strip,
       and two sticky elements sharing top:0 would leave the search hidden behind the table's name. */
    top: var(--table-band-h, 0rem);
    z-index: 1;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 0.75rem;
    padding: 0.5rem 0.6rem;
    border-radius: 0.8rem;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    box-shadow: var(--shadow-sm);
}
/* The basis is what decides the bar's shape, and it is set past what a phone can spare beside the glance so that
   decision is the width's rather than the count's: at 14rem a phone always wraps the search to its own line and a
   reading column never does. Left nearer the space actually available, the bar flipped between one line and two
   as the count went from "Just you so far" to "6 at the table" — same screen, same bar, different height. */
.table-picker__search { flex: 1 1 14rem; min-width: 0; }

.table-picker__heading {
    display: flex;
    align-items: baseline;
    gap: 0.4rem;
    margin: 0.9rem 0 0.5rem;
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}
.table-picker__count {
    padding: 0.05rem 0.4rem;
    border-radius: 9999px;
    background: var(--bg-secondary);
    font-size: 0.7rem;
    letter-spacing: 0;
}
.table-picker__list { margin: 0; padding: 0; list-style: none; }
/* The row's own gap. A margin on the item rather than a flex gap on the list, because the list's other children
   are the spacer elements Virtualize sizes itself — a gap would be added to those too and the pitch the
   virtualizer measures against (ItemSize) would drift from the pitch the page actually draws. */
.table-picker__item { margin-bottom: 0.5rem; }
.table-picker__empty { margin: 0; padding: 0.75rem 0; color: var(--text-muted); font-size: 0.9rem; }

/* One game (GamePickerRow.razor). The whole row is the control — it carries a single action, so a surface that
   is not itself pressable would only add a dead target around the one that is. Its drawn height comes from the
   art crop (3.25rem) plus the padding and border either side, which lands at 70px — comfortably past the app's
   44px tap-target floor, and the number the virtualizer's ItemSize is derived from with the item's margin added. */
.table-pick {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    width: 100%;
    min-height: 4.25rem;
    padding: 0.5rem 0.6rem;
    border: 1px solid var(--border-color);
    border-radius: 0.8rem;
    background: var(--bg-card);
    color: var(--text-primary);
    text-align: left;
    cursor: pointer;
    transition: border-color 0.15s ease, transform 0.15s ease;
}
.table-pick:hover:not(:disabled) {
    border-color: color-mix(in srgb, var(--game-accent, var(--accent-blue)) 55%, var(--border-color));
    transform: translateX(2px);
}
.table-pick:disabled { opacity: 0.55; cursor: default; }
@media (prefers-reduced-motion: reduce) {
    .table-pick { transition: border-color 0.15s ease; }
    .table-pick:hover:not(:disabled) { transform: none; }
}
/* A crop of the game's art, not the catalog's 16:10 billboard: at list scale the art's job is recognition, and
   a panel tall enough to sell a game is a screen and a half per row on a phone. */
.table-pick__art {
    position: relative;
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 4.5rem;
    height: 3.25rem;
    overflow: hidden;
    border-radius: 0.55rem;
    background: linear-gradient(150deg, var(--game-accent), var(--game-accent-deep));
}
.table-pick__glyph { font-size: 1.5rem; line-height: 1; }
.table-pick__text { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; flex: 1 1 auto; }
.table-pick__name {
    font-size: 1rem;
    font-weight: 700;
    line-height: 1.2;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.table-pick__meta {
    font-size: 0.78rem;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.table-pick__go { flex: 0 0 auto; color: var(--text-muted); font-size: 1.3rem; line-height: 1; }

/* The invitation: one QR beside the shared Share / Copy pair, all carrying one URL — the two ways a table is
   handed out, side by side rather than stacked, so the whole thing is one short panel rather than a column that
   pushes the lobby off the screen. It rides in the band, which is what gives that band something to be: a lead
   strip carrying one line of text takes a phone's whole first screen to say what a line would, and carrying the
   invitation it answers the only question an empty table raises. The panel takes a warm paper tone rather than the
   app's white card: it is the one thing on this screen a stranger's phone camera is pointed at, and paper reads as
   the thing you hold up. */
.table-invite {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    padding: 0.85rem;
    border-radius: 0.9rem;
    background: linear-gradient(160deg, var(--invite-paper), var(--invite-paper-deep));
    border: 1px solid color-mix(in srgb, var(--invite-paper-edge) 22%, transparent);
    box-shadow: var(--shadow-sm);
}
.table-invite .invite-qr--inline { flex: 0 0 auto; margin-top: 0; }
/* The code itself sits on its own white patch, so the scan target keeps maximum contrast on the paper; the
   caption stays on the paper below it. */
.table-invite .invite-qr__canvas {
    width: 116px;
    height: 116px;
    padding: 0.35rem;
    border-radius: 0.45rem;
    background: var(--bg-card);
}
.table-invite .invite-qr__hint { color: var(--invite-paper-ink); font-size: 0.7rem; }
/* The two controls stack beside the code — Share first, since a phone's own share sheet is the fastest way
   the link reaches whichever app the group actually uses. */
.table-invite .invite-actions { flex: 1 1 auto; min-width: 0; }
.table-invite .invite-actions__row { flex-direction: column; gap: 0.45rem; }
/* The panel is warm paper, not a surface of either canvas, so the control on it takes the paper's own stock and
   ink rather than the page palette's card and text colours — which are near-white on the shell's dark canvas and
   would leave this label invisible on cream. */
.table-invite .invite-actions__copy {
    background: var(--invite-paper-control);
    border-color: color-mix(in srgb, var(--invite-paper-edge) 30%, transparent);
    color: var(--invite-paper-ink);
}
.table-invite .invite-actions__copy:hover {
    background: var(--invite-paper-deep);
    border-color: color-mix(in srgb, var(--invite-paper-edge) 55%, transparent);
}
/* The QR's own patch is the scan target, so it takes the paper's brightest stock rather than a page surface
   that could go dark underneath it. */
.table-invite .invite-qr__canvas { background: var(--invite-paper-control); }

/* Two columns from the wide-composition stop: the roster on the left and the catalog beside it, both under the
   full-width band. Side by side rather than stacked because the lobby's two halves answer each other — who is
   here, and what shall we play — and a laptop has the width to ask both at once. Document order is untouched
   (band, people, picker), so focus order and a screen reader still follow the lobby's stated hierarchy. */
@media (min-width: 72rem) {
    /* Two columns and a gap, so the pair has no dead slack inside it to drift within — and the band above pads
       itself out to the same measure, keeping one left edge down the whole page. */
    .table-page { --table-measure: 56.25rem; }
    .table-page__cols {
        display: grid;
        /* Each column stays bounded rather than taking whatever is left: a table's cards are phone-sized and a row
           of a list gets no better for being wider — stretched across a laptop it puts a game's name and its facts
           at opposite ends of the screen. */
        grid-template-columns: minmax(0, 26rem) minmax(0, 1fr);
        grid-template-areas: "main picker";
        align-items: start;
        gap: 1.25rem;
        padding-block: clamp(0.9rem, 3vh, 2rem);
    }
    .table-page__main { grid-area: main; }
    .table-picker { grid-area: picker; }
    /* A table with no catalog beside its column — one that has chosen its game, or is playing — drops the second
       track rather than leaving half the measure reserved next to a roster. */
    .table-page__cols:not(:has(.table-picker)) {
        grid-template-columns: minmax(0, 34rem);
        grid-template-areas: "main";
    }
    /* The state screens have no columns to place side by side — they stay a centered stack. */
    .table-page--loading { display: flex; flex-direction: column; }
}

