/* ---------------------------------------------------------------------------
   Games catalog and play home — the featured hero, the game shelves and start-a-game tiles,
   the my-matches shelf, the invite actions/panel/QR, the app toast, and the desktop/tablet scale-up.
   Backed by GamesPage.razor, FeaturedHero.razor, GameShelf.razor, GameStartTile.razor,
   GameStartCatalog.razor, GameStartSelector.razor, GamePlayParams.razor, MyMatchesShelf.razor,
   MatchCard.razor, InviteActions.razor, InvitePanel.razor, InviteQr.razor, GroupPreviewModal.razor.
   --------------------------------------------------------------------------- *//* ===========================================================================
   Featured hero (FeaturedHero.razor)
   A clean white spotlight card: the name/blurb/CTA sit on the white left, a soft
   painterly art panel — pastel sky, warm bloom, the game emoji as a medallion —
   fills the right and fades into the card. Per-game accent arrives inline as
   --game-accent / --game-accent-deep from the component.
   =========================================================================== */
.hero {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    display: flex;
    align-items: center;
    min-height: clamp(10.5rem, 20vw, 14rem);
    border-radius: 1.5rem;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    box-shadow: var(--shadow-md);
    transition: transform 0.16s var(--ease-spring), box-shadow 0.16s ease;
}
/* Press feedback: the whole banner dips as it's tapped (:active propagates from the full-cover button up to
   the banner), echoing the tiles' tactile feel. */
.hero:active {
    transform: scale(0.986);
    box-shadow: var(--shadow-sm);
}
/* The painterly art panel: a warm pastel sky over an accent wash, layered clouds and a soft foliage bloom,
   masked so it dissolves into the white card toward the content. */
.hero__art {
    position: absolute;
    /* The art takes most of the card's width and dissolves over a long run. On the dark canvas the picture is
       the brightest thing on the page, so a short fade against a navy panel reads as a seam down the middle of
       the card; a slow dissolve across most of the width makes the banner one image with type on it. The
       content column's own cap (below) is what keeps the words off the picture. */
    inset: 0 0 0 22%;
    z-index: -1;
    pointer-events: none;
    display: flex;
    align-items: center;
    justify-content: center;
    background:
        radial-gradient(46% 40% at 30% 20%, rgba(255, 255, 255, 0.65), transparent 70%),
        radial-gradient(38% 34% at 66% 12%, rgba(255, 255, 255, 0.5), transparent 70%),
        radial-gradient(85% 95% at 80% 34%, color-mix(in srgb, var(--game-accent) 55%, white), transparent 72%),
        radial-gradient(90% 80% at 60% 118%, color-mix(in srgb, var(--accent-warm) 40%, var(--game-accent-deep)), transparent 66%),
        linear-gradient(120deg,
            color-mix(in srgb, var(--game-accent) 16%, #eaf3ff) 0%,
            color-mix(in srgb, var(--game-accent) 58%, white) 55%,
            var(--game-accent) 120%);
    -webkit-mask-image: linear-gradient(90deg, transparent 0, rgba(0, 0, 0, 0.55) 30%, #000 62%);
    mask-image: linear-gradient(90deg, transparent 0, rgba(0, 0, 0, 0.55) 30%, #000 62%);
}
.hero__content {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.4rem;
    /* The percentage branch is what keeps the text off the art, so it has to clear where the art turns
       opaque: the panel starts at 22% of the card and its mask reaches half strength across the panel's first
       30%, i.e. around 22% + 30%×78% = 45% of the card. A column ending at 42% stays on the faint side of
       that, where the type still reads. The 30rem cap governs on cards wider than ~72rem, so wide layouts are
       unaffected — it is the band between the phone breakpoint and there, tablet portrait and landscape
       phones, where the percentage decides and a long blurb would otherwise land on un-faded art. */
    max-width: min(30rem, 42%);
    padding: clamp(1.1rem, 3vw, 1.9rem);
}
.hero__eyebrow {
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.16em;
    color: var(--accent-blue-ink);
}
.hero__name {
    margin: 0.1rem 0;
    font-size: clamp(1.8rem, 4.5vw, 2.7rem);
    line-height: 1.02;
    color: var(--text-primary);
}
.hero__meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.55rem;
    font-size: 0.85rem;
    color: var(--text-secondary);
}
.hero__chip {
    font-size: 0.66rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: color-mix(in srgb, var(--game-accent-deep) 70%, var(--text-primary));
    background: color-mix(in srgb, var(--game-accent) 18%, var(--bg-card));
    border: 1px solid color-mix(in srgb, var(--game-accent) 30%, transparent);
    border-radius: 9999px;
    padding: 0.2rem 0.65rem;
}
.hero__dot { opacity: 0.5; }
.hero__blurb {
    margin: 0.2rem 0 0.5rem;
    max-width: 30rem;
    font-size: 0.98rem;
    line-height: 1.55;
    color: var(--text-secondary);
}
/* The explicit call to action: a pill on the game's accent gradient. It only reads as the banner's button —
   the stretched .hero__open above is the real hit target — and brightens on hover like the platform's
   primary buttons. */
.hero__cta {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.5rem 1.1rem;
    border-radius: 9999px;
    font-size: 0.88rem;
    font-weight: 700;
    color: var(--text-on-accent);
    background: linear-gradient(160deg,
        color-mix(in srgb, var(--game-accent) 82%, white) 0%,
        var(--game-accent-deep) 118%);
    box-shadow: 0 10px 18px -12px color-mix(in srgb, var(--game-accent-deep) 80%, transparent);
    transition: filter 0.15s ease;
}
.hero:hover .hero__cta { filter: brightness(1.07); }
/* The whole banner is one big button: a stretched, transparent hit-target over the decorative content.
   A soft lift on hover echoes the tiles' feedback. */
.hero__open {
    position: absolute;
    inset: 0;
    z-index: 3;
    padding: 0;
    background: transparent;
    border: 0;
    border-radius: inherit;
    cursor: pointer;
    transition: box-shadow 0.18s ease;
}
.hero:hover .hero__open { box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--game-accent) 45%, transparent); }
/* The hit target is inset:0 inside .hero's hidden overflow, so the platform ring's positive offset would be
   clipped away entirely. Pull the same outline inside the banner's edge instead. */
.hero__open:focus-visible { outline-offset: -4px; }

/* On a phone the hero goes cinematic: the art fills the whole card at full strength behind a directional
   scrim in the game's deep accent (the same treatment as .group-ongoing-card), with the per-game
   --hero-art-focus keeping the art's subject inside the wide-band crop. The content pins to the scrimmed
   bottom edge and its text flips to --text-on-accent. */
@media (max-width: 48rem) {
    .hero { align-items: flex-end; }
    .hero__art { inset: 0; -webkit-mask-image: none; mask-image: none; }
    .hero__art::after {
        content: "";
        position: absolute;
        inset: 0;
        background: linear-gradient(
            to top,
            color-mix(in srgb, var(--game-accent-deep) 92%, #000) 0%,
            color-mix(in srgb, var(--game-accent-deep) 60%, transparent) 52%,
            transparent 100%);
    }
    .hero__img { object-position: var(--hero-art-focus); }
    .hero__content { max-width: 100%; }
    .hero__eyebrow,
    .hero__name,
    .hero__meta,
    .hero__blurb { color: var(--text-on-accent); }
    /* The content stack has no height budget of its own, so a long blurb pushes the name and eyebrow up past
       the 52% mark where the scrim thins toward transparent — near-white type on full-strength art. Two lines
       keeps the whole stack inside the gradient's strong half whatever the featured game's blurb runs to; the
       full text is on the game's own page one tap away. */
    .hero__blurb {
        display: -webkit-box;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 2;
        line-clamp: 2;
        overflow: hidden;
    }
}

/* ===========================================================================
   Shelves & tiles
   Every landing shelf (.shelf__row) scrolls horizontally like a streaming rail.
   The wrapping .shelf__grid layout serves the group's start-a-game selector,
   whose tiles fill the wide modal instead of a rail.
   =========================================================================== */
.shelf { display: flex; flex-direction: column; gap: 0.7rem; }
.shelf__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
}
.shelf__title {
    font-size: 1.2rem;
    font-weight: 700;
    color: var(--text-primary);
}
/* Empty catalog state (no search matches). A search that matches nothing replaces the whole page, so the
   state carries the way out of itself rather than leaving the field above a blank page as the only exit. */
.catalog-empty {
    margin: 2.5rem auto;
    padding: 1.5rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.85rem;
    font-size: 1rem;
    color: var(--text-secondary);
    text-align: center;
}
.catalog-empty__line { margin: 0; }
.shelf__row {
    display: flex;
    gap: 0.9rem;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    /* Bleed to the column edges; padding keeps the first/last card off the edge. */
    margin-inline: -1rem;
    padding-inline: 1rem;
    /* overflow-x:auto forces overflow-y to compute to auto, so the row clips vertically — this padding gives
       the hovered tile's lift and drop shadow room, so they aren't cut off with a hard edge along the rail. */
    padding-block: 1.5rem;
    scrollbar-width: none;
}
.shelf__row::-webkit-scrollbar { display: none; }
/* A rail's only cue that it continues is a partially-cut card, and whether one exists is arithmetic — a shelf
   of exactly six ends flush and looks like the whole set. A mask fade cannot be used (padding-block exists to
   give the hovered tile's lift and shadow room, and a mask would clip them), so a pointer device gets a thin
   scrollbar back. Touch keeps the clean rail: the swipe is discoverable there. */
@media (pointer: fine) {
    .shelf__row { scrollbar-width: thin; scrollbar-color: var(--border-color) transparent; }
    .shelf__row::-webkit-scrollbar { display: block; height: 0.35rem; }
    .shelf__row::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: var(--radius-pill); }
    .shelf__row::-webkit-scrollbar-track { background: transparent; }
}

/* A complete shelf wraps instead of scrolling: same tiles, laid out as a grid so the whole set is on screen
   wherever the width allows, rather than hidden past a right edge that looks like the layout simply stopped.
   It keeps the rail's block padding so a hovered tile's lift still has room. */
.shelf__row--wrap {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(11.5rem, 1fr));
    overflow: visible;
}
/* The tile fills its track instead of keeping the rail's fixed width. Doubled class so this outranks the
   fixed-width rules for a rail item further down (and the wider one inside a media query) whatever the source
   order: a 13rem card in a narrower track would overflow it and swallow the grid's gap. */
.shelf__row.shelf__row--wrap > .game-card-wrapper { width: auto; }
/* The start-a-game selector's grid: uniform-width tracks, so every tile is the same width regardless of how
   many fill the row. auto-fill retains empty tracks, so a partial last row left-packs under the tiles above
   it without stretching. Not clipped, so hover shadows spill freely. */
.shelf__grid {
    display: grid;
    /* Tracks share the row rather than each capping at a tile width. A fixed maximum leaves a wide screen with
       four tiles and a column of nothing beside them, which reads as a catalog that ran out rather than a grid;
       the 12rem minimum still decides how many fit. */
    grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
    gap: clamp(0.9rem, 1.6vw, 1.4rem);
    padding-block: 0.75rem;
}

/* --- Game tile ---
   A full-bleed accent art panel (emoji over the game's colour) above a body of
   name + category chip + player/duration. Accent arrives inline as --game-accent. */
.game-card-wrapper {
    position: relative;
    display: flex;
    flex: 0 0 auto;               /* a fixed-width rail item in a row… */
    scroll-snap-align: start;
}
.shelf__row > .game-card-wrapper { width: 13rem; }

.game-card {
    position: relative;
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border-radius: 1.15rem;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    color: var(--text-primary);
    text-align: left;
    box-shadow: var(--shadow-sm);
    cursor: pointer;
    transition: transform 0.18s var(--ease-spring), border-color 0.18s ease, box-shadow 0.18s ease;
}
.game-card:hover {
    transform: translateY(-5px);
    border-color: color-mix(in srgb, var(--game-accent) 45%, var(--border-color));
    box-shadow: 0 22px 40px -22px color-mix(in srgb, var(--game-accent) 55%, transparent), var(--shadow-md);
}
.game-card__art {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 16 / 10;
    background: var(--accent-tile-bg);
}
/* A soft light sheen over the art so the pastel panel has gentle depth, not flat colour. */
.game-card__art::before {
    content: "";
    position: absolute;
    inset: 0;
    background: radial-gradient(120% 90% at 50% -20%, rgba(255, 255, 255, 0.55), transparent 62%);
}
/* A slow zoom on hover gives the tile a gentle lift. A plain ease-out (no spring overshoot) keeps the zoom
   from wobbling as it settles on the full-bleed image. */
.game-card__img { transition: transform 0.35s ease-out; }
.game-card:hover .game-card__img { transform: scale(1.05); }
.game-card__body {
    display: flex;
    flex-direction: column;
    gap: 0.45rem;
    padding: 0.8rem 0.9rem 0.95rem;
}
.game-card__name { font-size: 1.08rem; color: var(--text-primary); }
.game-card__tags { display: flex; flex-direction: column; gap: 0.35rem; }
.game-card__cat {
    align-self: flex-start;
    font-size: 0.62rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: color-mix(in srgb, var(--game-accent-deep) 72%, var(--text-primary));
    background: color-mix(in srgb, var(--game-accent) 15%, var(--bg-card));
    border: 1px solid color-mix(in srgb, var(--game-accent) 24%, transparent);
    border-radius: 9999px;
    padding: 0.14rem 0.55rem;
}
.game-card__meta { font-size: 0.76rem; color: var(--text-secondary); }
/* A game with no catalog art gets the accent-and-glyph panel the table's game picker uses, rather than an
   empty tile that reads as an image that failed to load beside its illustrated neighbours. The glyph is
   positioned so it paints over the panel's sheen instead of under it. */
.game-card__art--glyph { background: linear-gradient(150deg, var(--game-accent), var(--game-accent-deep)); }
.game-card__glyph { position: relative; font-size: 2.6rem; line-height: 1; }

/* --- Start-a-game tile (group page) ---
   The two-action catalog tile the "Start a game" selector renders (Play + Details over the same art panel and
   name as a game card), and the “＋” tile appended to the Live games shelf that opens it. Both sit in the
   same rail/grid footprint as a GameCard, so the wrapper mirrors .game-card-wrapper's sizing and the surface
   reuses the game-card art/name visual classes; the two actions are real, labelled sibling buttons (never
   nested) so assistive tech hears each. */
.game-start-tile-wrapper {
    position: relative;
    display: flex;
    flex: 0 0 auto;
    scroll-snap-align: start;
}
.shelf__grid > .game-start-tile-wrapper { width: 100%; }  /* fill the uniform grid track */
.shelf__row > .game-start-tile-wrapper { width: 13rem; }

.game-start-tile {
    position: relative;
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border-radius: 1.15rem;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    color: var(--text-primary);
    box-shadow: var(--shadow-sm);
    transition: transform 0.18s var(--ease-spring), border-color 0.18s ease, box-shadow 0.18s ease;
}
.game-start-tile:hover {
    transform: translateY(-5px);
    border-color: color-mix(in srgb, var(--game-accent) 45%, var(--border-color));
    box-shadow: 0 22px 40px -22px color-mix(in srgb, var(--game-accent) 55%, transparent), var(--shadow-md);
}
.game-start-tile__body {
    display: flex;
    flex-direction: column;
    gap: 0.45rem;
    padding: 0.8rem 0.9rem 0.6rem;
}
.game-start-tile__actions {
    display: flex;
    gap: 0.5rem;
    padding: 0 0.9rem 0.85rem;
    margin-top: auto;
}
.game-start-tile__btn {
    flex: 1 1 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Clear of the app chrome's 44px tap-target floor rather than exactly on it: this is the primary action on
       a page whose whole job is one tap, and a control sitting exactly on the line measures a sub-pixel under
       it. */
    min-height: 2.9rem;
    padding: 0.5rem 0.6rem;
    border-radius: 0.7rem;
    font-size: 0.86rem;
    font-weight: 700;
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease, transform 0.12s ease;
}
.game-start-tile__btn:active { transform: translateY(1px); }
/* The label is a fixed light token, so lightening the fill toward white pairs light on light on the pale
   accents (Can't Stop's yellow, Dominion's gold, The Magnum Opus's lilac) — and made hover, which dropped back
   to the accent itself, more legible than rest. The fill is the accent at full strength, and hover brightens
   the whole control the way the platform's primary buttons do. */
.game-start-tile__btn--play {
    border: 1px solid color-mix(in srgb, var(--game-accent) 35%, transparent);
    background: var(--game-accent);
    color: var(--text-on-accent);
}
.game-start-tile__btn--play:hover { filter: brightness(1.08); }
.game-start-tile__btn--details {
    border: 1px solid var(--border-color);
    background: var(--bg-card);
    color: var(--text-primary);
}
.game-start-tile__btn--details:hover { background: var(--bg-hover); }

/* The “＋” tile appended to the Live games shelf: a game-card-width dashed card (not the group-card's
   11rem) that opens the selector. The base .game-start-tile supplies the rounded surface; the modifier centres
   the plus glyph and label and tints it the platform blue. */
.shelf__row > .game-start-tile--new { flex: 0 0 auto; width: 13rem; scroll-snap-align: start; }
.game-start-tile--new {
    min-height: 8rem;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 1rem;
    border-style: dashed;
    color: var(--accent-blue-ink);
    background: color-mix(in srgb, var(--accent-blue) 6%, var(--bg-card));
    box-shadow: none;
    cursor: pointer;
}
.game-start-tile--new:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.10);
    background: color-mix(in srgb, var(--accent-blue) 12%, var(--bg-card));
    border-color: var(--accent-blue);
}
.game-start-tile__plus { font-size: 2rem; font-weight: 700; line-height: 1; }
.game-start-tile__new-label { font-size: 0.9rem; font-weight: 600; }

/* The shared chooser body: a full-width search field over the catalog grid, filling whatever its host gives it —
   the modal card below, or a chooser page's content column. */
.game-start-catalog { display: flex; flex-direction: column; }
.game-start-catalog__search { flex: none; width: 100%; margin-bottom: 0.85rem; }
.game-start-catalog__grid { margin-top: 0.25rem; }

/* The full playable catalog fills a wide, multi-column card bounded to the viewport — several tiles per row on
   a wide monitor, wrapping down to fewer columns on a narrow one (the phone stop below states the two-up). A
   card taller than the viewport overflows the fixed, viewport-sized scrim top and bottom with no scroll, so
   tiles outside the viewport are unreachable; bound the card to the viewport (mirrors .gd-shell's tall-card
   recipe) and scroll the grid inside the body, keeping the search pinned and every tile and both its buttons
   reachable. On a chooser page there is no card to bound: the page itself scrolls. */
.app-modal__card.game-start-selector { max-width: min(96vw, 88rem); align-items: stretch; max-height: 90vh; }
.app-modal__card.game-start-selector .app-modal__body { text-align: left; flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.app-modal__card.game-start-selector .game-start-catalog { flex: 1 1 auto; min-height: 0; }
.app-modal__card.game-start-selector .game-start-catalog__grid { flex: 1 1 auto; min-height: 0; overflow-y: auto; }

/* The lean Play params modal's body lays out its audience seg, tempo seg, seat slider, and Advanced disclosure
   in a single vertical column with comfortable spacing. */
.app-modal__card.game-play-params .app-modal__body { display: flex; flex-direction: column; gap: 0.65rem; }

/* The visitor preview layered over the communities directory: a wide modal card that lets the shared clubhouse
   fill it and scroll internally, so the directory behind stays put while the preview scrolls. Mirrors the
   game-start-selector modal's tall-card recipe — bound to the viewport, body scrolls inside. */
.app-modal__card.group-preview-modal {
    max-width: min(96vw, 60rem);
    width: min(96vw, 60rem);
    align-items: stretch;
    max-height: 90vh;
    padding: 0;
    text-align: left;
}
/* The empty head carries only the close button (pinned to the card corner); reserve a strip so it clears the
   clubhouse hero beneath it. */
.app-modal__card.group-preview-modal .app-modal__head { min-height: 0.5rem; }
/* The preview hosts the same clubhouse the full group page does, so it inherits the same "vertical scroll only"
   rule: hide horizontal overflow so an over-wide child can't make the modal body scroll sideways (overflow-y:auto
   alone would compute overflow-x to auto). Mirrors the hub's .app-main. */
.app-modal__card.group-preview-modal .app-modal__body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; }
/* Inside the modal the clubhouse fills the card rather than its own centered page column, and trims the top pad
   so the hero clears the pinned close button. */
.app-modal__card.group-preview-modal .group-page { max-width: none; margin-inline: 0; padding-top: 1.75rem; }

/* --- My matches shelf ---
   The player's ongoing async tables and forming queues: wide, low cards (game icon + status) on the same
   scrolling rail as the game shelves. A card owing the player a move carries the gold "Your turn" pill. */
.my-match-card {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.8rem;
    flex: 0 0 auto;
    scroll-snap-align: start;
    width: 19rem;
    padding: 0.85rem 1rem;
    border-radius: 1.1rem;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    color: var(--text-primary);
    box-shadow: var(--shadow-sm);
    text-align: left;
    cursor: pointer;
    transition: transform 0.18s var(--ease-spring), border-color 0.18s ease, box-shadow 0.18s ease;
}
.my-match-card:hover {
    transform: translateY(-3px);
    border-color: color-mix(in srgb, var(--accent-gold) 45%, var(--border-color));
    box-shadow: var(--shadow-md);
}
.my-match-card--turn {
    border-color: color-mix(in srgb, var(--accent-gold) 55%, var(--border-color));
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-gold) 35%, transparent), var(--shadow-md);
}
/* The turn clock is nearly out on the player's own move: the gold turn tint shifts to an ember warning. */
.my-match-card--urgent {
    border-color: color-mix(in srgb, var(--warn-ember) 60%, var(--border-color));
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--warn-ember) 40%, transparent), var(--shadow-md);
}
.my-match-card--urgent .my-match-card__status { color: color-mix(in srgb, var(--warn-ember-light) 75%, var(--text-primary)); }
.my-match-card--urgent .my-match-card__badge {
    background: linear-gradient(180deg, var(--warn-ember-pale) 0%, var(--warn-ember) 60%, var(--warn-ember-deep) 100%);
    color: var(--text-on-accent-deep);
}
.my-match-card--forming { cursor: default; }
.my-match-card--forming:hover { transform: none; box-shadow: none; border-color: var(--border-color); }
.my-match-card__icon { font-size: 1.9rem; line-height: 1; }
.my-match-card__text {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 0;
    flex: 1 1 auto;
}
.my-match-card__name { font-size: 1.02rem; }
.my-match-card__opponents,
.my-match-card__status {
    font-size: 0.74rem;
    color: var(--text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.my-match-card--turn .my-match-card__status { color: var(--text-primary); }
.my-match-card__badge {
    flex: 0 0 auto;
    font-size: 0.62rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    padding: 0.22rem 0.55rem;
    border-radius: 9999px;
    background: linear-gradient(180deg, var(--accent-gold-pale) 0%, var(--accent-gold) 55%, var(--accent-gold-deep) 100%);
    color: var(--text-on-accent-deep);
    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.35);
}
.my-match-card__cancel {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    /* The only action on two card kinds, on the shelf that is the player's way back into an ongoing game — so
       it holds the same 44px floor its neighbours in this sheet state. */
    min-height: 2.75rem;
    font-size: 0.72rem;
    padding: 0.3rem 0.7rem;
    border-radius: 9999px;
    border: 1px solid var(--border-color);
    background: transparent;
    color: var(--text-secondary);
    cursor: pointer;
    transition: color 0.15s ease, border-color 0.15s ease;
}
.my-match-card__cancel:hover { color: var(--text-primary); border-color: var(--text-secondary); }

/* --- Invite actions (InviteActions.razor) ---
   The Share / Copy pair every surface that hands out an invitation URL mounts: the table lobby's invitation panel,
   the group's invite dialog, a forming private table, the waiting card on the play home. One control, so one link
   behaves the same way everywhere — Share leads (it is the shorter path on a phone, where invitations are actually
   sent), Copy is the always-works baseline, and the confirmation lands on the pressed control as an inline label
   rather than in a toast that would cover whatever the player is watching. Both controls hold the 44px tap-target
   floor the rest of the app chrome holds. */
.invite-actions { display: flex; flex-direction: column; gap: 0.35rem; }
.invite-actions__row { display: flex; gap: 0.5rem; width: 100%; }
.invite-actions__share, .invite-actions__copy {
    flex: 1 1 0;
    min-width: 0;
    min-height: 2.75rem;
    padding: 0.55rem 0.75rem;
    border-radius: 0.6rem;
    font-size: 0.9rem;
    cursor: pointer;
    transition: background-color 0.15s ease, border-color 0.15s ease, filter 0.15s ease;
}
.invite-actions__share {
    border: none;
    background: linear-gradient(150deg, color-mix(in srgb, var(--accent-teal) 88%, white), var(--accent-teal-deep));
    color: var(--text-on-accent);
    font-weight: 700;
}
.invite-actions__share:hover { filter: brightness(1.05); }
.invite-actions__copy {
    border: 1px solid var(--border-color);
    background: var(--bg-card);
    color: var(--text-primary);
    font-weight: 600;
}
.invite-actions__copy:hover { background: var(--bg-hover); border-color: var(--accent-teal); }
/* The URL lives in the DOM for a manual copy and E2E reads, hidden by `.sr-only` until the clipboard is
   blocked — when this restates position and size and it appears as a small, muted, selectable field. It
   out-ranks the hook by loading later, at equal specificity. */
.invite-actions__link--shown {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    padding: 0.4rem 0.55rem;
    clip: auto;
    clip-path: none;
    white-space: normal;
    word-break: break-all;
    font-size: 0.72rem;
    border-radius: 0.5rem;
    border: 1px solid var(--border-color);
    background: var(--bg-card);
    color: var(--text-secondary);
}

/* --- Invite panel ---
   The link-sharing block on the group's invite dialog, a forming private table, and the waiting my-matches card:
   a short label saying what the link is for, over the shared Share / Copy pair. */
.invite-panel { margin-top: 0.5rem; display: flex; flex-direction: column; gap: 0.35rem; }
.invite-panel__label { margin: 0; display: flex; flex-direction: column; gap: 0.05rem; min-width: 0; }
.invite-panel__label-title { font-size: 0.82rem; font-weight: 600; color: var(--text-primary); }
.invite-panel__label-sub { font-size: 0.72rem; color: var(--text-secondary); }

/* --- Invite QR + WhatsApp ---
   The QR-and-share affordance that sits beside the copy-link control: a tap toggles a popover with a QR
   code of the join link (rendered onto a canvas by js/invite-qr.js), and a WhatsApp share anchor. Both reuse
   the same token as the invite actions beside them. The buttons match the 2rem icon-button idiom of .voice-mic
   so the row reads as one toolbar. */
.invite-qr { display: inline-flex; align-items: center; gap: 0.4rem; position: relative; flex-shrink: 0; }
/* Anchored under the actions so it floats over the hero without pushing layout. */
.invite-qr__popover {
    position: absolute;
    top: calc(100% + 0.4rem);
    right: 0;
    z-index: var(--z-menu);
    padding: 0.6rem;
    border-radius: 0.8rem;
    border: 1px solid var(--border-color);
    background: var(--bg-elevated);
    box-shadow: var(--shadow-md);
}
/* The canvas is drawn small (6px modules) and scaled up crisp so edges stay sharp on any display. */
.invite-qr__popover canvas { display: block; width: 168px; height: 168px; image-rendering: pixelated; }
.invite-qr__hint { margin: 0.4rem 0 0; font-size: 0.72rem; color: var(--text-secondary); text-align: center; }

/* Inline shape: the QR renders directly (no toggle/popover) inside a sharing dialog such as Add Friend. */
.invite-qr--inline { display: flex; align-items: center; justify-content: center; gap: 0.85rem; margin-top: 0.7rem; }
.invite-qr--inline .invite-qr__canvas-block { display: flex; flex-direction: column; align-items: center; gap: 0.35rem; }
.invite-qr--inline .invite-qr__canvas { display: block; width: 140px; height: 140px; image-rendering: pixelated; }
.invite-qr--inline .invite-qr__hint { margin: 0; }

/* --- App toast ---
   A single app-wide transient confirmation (a copied link, say), mounted once in the layout. Bottom-centre and
   self-dismissing — the same corner as the connection toast, but neutral and low-stakes. */
.app-toast {
    position: fixed;
    left: 50%;
    bottom: calc(1rem + env(safe-area-inset-bottom));
    transform: translateX(-50%);
    z-index: var(--z-reading);
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    max-width: calc(100vw - 2rem);
    padding: 0.55rem 0.95rem;
    border-radius: 9999px;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--text-on-accent);
    background: var(--text-primary);
    box-shadow: var(--shadow-lg);
    /* A dedicated slide-in that keeps the translateX(-50%) centering (a generic translateY would strand it). */
    animation: app-toast-in 0.24s var(--ease-spring) both;
}
@keyframes app-toast-in {
    from { opacity: 0; transform: translate(-50%, 14px); }
    to   { opacity: 1; transform: translate(-50%, 0); }
}
.app-toast__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.05rem;
    height: 1.05rem;
    border-radius: 9999px;
    background: var(--accent-blue);
    color: var(--text-on-accent);
    font-size: 0.7rem;
    flex: none;
}
/* The error variant recolours only the icon circle — the dark pill background stays, so a failure reads as a
   warning (⚠, red) against the same surface as a confirmation (✓, blue). */
.app-toast--error .app-toast__icon { background: var(--accent-red, #e0584c); }
/* The prompt variant recolours the icon circle green, so an invitation/opportunity (▸) the player can act on
   reads as distinct from a passive confirmation (✓) and a failure (⚠). */
.app-toast--prompt .app-toast__icon { background: var(--accent-green, #16a34a); }
.app-toast__text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The action button on a prompt toast (e.g. Join). flex:none like the icon, so the text takes the overflow and
   the pill's left:50% + translateX(-50%) centering stays correct when the button widens it. */
.app-toast__action {
    flex: none;
    margin-left: 0.15rem;
    padding: 0.2rem 0.6rem;
    border-radius: 9999px;
    border: 1px solid currentColor;
    background: transparent;
    color: inherit;
    font-size: 0.8rem;
    font-weight: 700;
    cursor: pointer;
}
.app-toast__action:hover { background: rgba(255, 255, 255, 0.12); }
@media (prefers-reduced-motion: reduce) {
    .app-toast { animation: none; }
}

/* The direct "invite a friend to this table" list, under the copy-link panel in a forming Friends room. */
.invite-friends { margin-top: 0.6rem; display: flex; flex-direction: column; gap: 0.3rem; }
.friend-picker__row { display: flex; align-items: center; gap: 0.4rem; }
.friend-picker__row .friend-row { flex: 1 1 auto; }
.app-btn.invite-friends__btn { flex: 0 0 auto; font-size: 0.78rem; padding: 0.25rem 0.7rem; }
.invite-friends__sent { flex: 0 0 auto; font-size: 0.78rem; font-weight: 600; color: var(--accent-green-ink); }

/* --- Entrance motion ---
   Each top-level section of a content page — a play-mode card, the hero, a shelf —
   rises in with a short stagger so the page assembles rather than snapping in.
   Collapsed under reduced motion (below). */
.landing > * { animation: mp-slide-up 0.4s var(--ease-spring) both; }
.landing > *:nth-child(1) { animation-delay: 0.02s; }
.landing > *:nth-child(2) { animation-delay: 0.08s; }
.landing > *:nth-child(3) { animation-delay: 0.14s; }
.landing > *:nth-child(4) { animation-delay: 0.20s; }
.landing > *:nth-child(n+5) { animation-delay: 0.26s; }

/* --- Desktop & tablet-landscape scale-up ---
   At tablet width and up, give the catalog room: wider gaps, a larger wordmark, and bigger tiles so the
   page reads as a desktop catalog rather than a stretched phone column. */
@media (min-width: 48rem) {
    .shelf__row > .game-card-wrapper { width: 15rem; }
    /* The wrapping shelf takes the rail's widened tile too. Left at the narrow floor it packs seven tracks into
       the desktop column and renders the library's own tiles smaller than the rails' directly above it — two
       card sizes for the same card on one screen, and the wide layout spent on a tighter grid rather than on
       art the tiles exist to show. */
    .shelf__row--wrap { grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); }
    .game-card__name { font-size: 1.15rem; }
    .game-card__meta { font-size: 0.78rem; }
}

/* Below the phone stop a min-track wide enough to look right on a tablet resolves to a single column: two
   11.5rem tracks plus the gap need 382px, and a 414px phone leaves 382px of content — so the whole library
   becomes a vertical scrub through 16:10 art cards, which is the failure the wrapping shelf exists to avoid.
   State the count rather than hoping a min track produces it. Two tracks fit at 320px. */
@media (max-width: 30rem) {
    .shelf__row--wrap,
    .shelf__grid { grid-template-columns: repeat(2, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
    .landing > * { animation: none; }
}

