﻿/* Self-hosted DM Sans — eliminates render-blocking Google Fonts request */
@font-face {
    font-family: 'DM Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('fonts/dm-sans-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'DM Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('fonts/dm-sans-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

html, body {
    font-family: 'DM Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
    margin: 0;
    padding: 0;
    /* Painted statically rather than by the MudBlazor theme: when a phone suspends the
       PWA the circuit dies and the theme's injected styles can go with it, which used to
       flash the page white on resume. Also covers iOS overscroll beyond the safe area. */
    background-color: var(--surface-page);
    color-scheme: light;
}

/* === Club Theme Overrides === */

/* MudBlazor AppBar customization */
.mud-appbar {
    background: var(--gradient-appbar) !important;
    border-bottom: 1px solid color-mix(in srgb, var(--ink) 5%, transparent);
}

/* The page's own padding — see MainLayout. clamp() gives a 320px phone 12px gutters and a desktop
   the full 24px, instead of a flat 24px that cost a small phone 15% of its width. The left/right
   safe-area insets are added on top so landscape on a notched phone clears the notch. */
.mud-main-content.app-main {
    padding-top: calc(76px + env(safe-area-inset-top, 0px));
    padding-bottom: calc(40px + env(safe-area-inset-bottom, 0px));
    padding-left: calc(clamp(12px, 4vw, 24px) + env(safe-area-inset-left, 0px));
    padding-right: calc(clamp(12px, 4vw, 24px) + env(safe-area-inset-right, 0px));
}

/* MudBlazor Paper/Card styling.
   Excluding popovers and dialogs is the point of the selector, not a detail. .mud-paper is on
   every MudBlazor surface — cards, drawer, menus, dropdowns, dialogs — so styling it flat also
   restyled things that are not cards, and position:relative in particular broke every dropdown
   into a full-width band across the page. Three separate patch rules used to undo that. Dialogs
   get the same look from .mud-dialog below, which they carry themselves. */
.mud-paper:not(.mud-popover):not(.mud-dialog) {
    background: var(--gradient-card) !important;
    border: 1px solid color-mix(in srgb, var(--ink) 5%, transparent) !important;
    position: relative;
}

/* Accent top-line effect on elevated papers */
.mud-paper.mud-elevation-2::before,
.mud-paper.mud-elevation-1::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 1px;
    background: linear-gradient(to right, transparent, color-mix(in srgb, var(--club-accent) 30%, transparent), transparent);
    pointer-events: none;
    z-index: 1;
}

/* --- Stacked tables on a phone ---
   MudBlazor stacks a table into per-row cards at its xs breakpoint and prints the column label
   beside every cell, which on these three tables is far more label than data. Opting in with
   .stacked-table stops the table parts being table boxes, so each row can become a grid or flex
   container, and drops the generated labels.

   Shared because all three tables — /players, /users and the builder's playing-time overview —
   needed exactly these five rules and each carried its own copy. What legitimately differs is
   only the row layout, which stays with each table below. */
@media (max-width: 599.98px) {
    .stacked-table .mud-table-root,
    .stacked-table .mud-table-body {
        display: block;
        width: 100%;
    }

    .stacked-table .mud-table-body .mud-table-row:not(:last-child) {
        border-bottom: 1px solid color-mix(in srgb, var(--ink) 6%, transparent);
    }

    .stacked-table .mud-table-body .mud-table-cell {
        border: none !important;
        padding: 0 !important;
    }

    .stacked-table .mud-table-body .mud-table-cell::before {
        display: none !important;
    }
}

/* Table overrides */
.mud-table .mud-table-head .mud-table-row .mud-table-cell {
    /* Full strength, not a 60% mix: this is 0.7rem uppercase text, and the mix measured about
       3.0:1 on white — under AA for a label people actually have to read. */
    color: var(--club-primary-bright) !important;
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1px;
    border-bottom-color: color-mix(in srgb, var(--ink) 6%, transparent) !important;
}

.mud-table .mud-table-body .mud-table-row .mud-table-cell {
    border-bottom-color: color-mix(in srgb, var(--ink) 4%, transparent) !important;
}

.mud-table .mud-table-body .mud-table-row:hover {
    background: color-mix(in srgb, var(--ink) 3%, transparent) !important;
}

.mud-table .mud-table-body .mud-table-row.mud-table-row-striped:nth-child(even) {
    background: color-mix(in srgb, var(--ink) 2%, transparent) !important;
}

/* Tab overrides */
.mud-tabs .mud-tab.mud-tab-active {
    color: var(--club-primary-bright) !important;
}

.mud-tabs .mud-tab-slider {
    background: var(--club-primary-bright) !important;
}

/* Dialog styling */
.mud-dialog {
    background: var(--gradient-card) !important;
    border: 1px solid color-mix(in srgb, var(--ink) 8%, transparent) !important;
}

/* MudBlazor's switch is 38px tall — six short of the floor — and the whole label is the target, so
   the height goes there, the same way .live-timeline-toggle handles its checkbox. Global because
   every switch in the app is in something filled in on a phone: guest in Add to squad and Edit
   player, own goal on the live screen, public on a match comment. */
@media (pointer: coarse) {
    label.mud-switch {
        min-height: 44px;
    }
}

/* --- Long form dialogs on a phone (.dialog-sheet) ---
   Opt in with `Class="dialog-sheet"` on the MudDialog — MudBlazor 9.7's DialogOptions has no
   CssClass, but MudDialog's own Class lands straight on .mud-dialog, which is what these rules
   need. GameDialog (the new/edit match form on /games) is the one user. Everything is inside the
   media query on purpose: above 600px a .dialog-sheet is an ordinary centred MudBlazor dialog and
   nothing below applies.

   MudBlazor gives every dialog a 32px margin per side (.mud-dialog-width-full is
   `calc(100% - 64px)`), which on a 360px phone leaves 296px of dialog. Two things break at that
   width, and both were reported from a touchline:

     1. The date picker's popover has `min-width: 310px` and is anchored to the input's left edge,
        so it hung 9px off a 360px screen and 49px off a 320px one — the whole Saturday column was
        unreachable. Widening the dialog is half the fix; the .mud-picker-popover block further
        down is the other half.
     2. The action buttons sat 28px below the last field, and MudSelect's hit box reaches ~10px
        past its own underline — so only ~18px of dead space separated "Annuleren" from the
        unavailable-players dropdown. Mobile browsers snap a tap that misses every target to the
        nearest one by contact area, and the select is by far the bigger target, so a thumb aimed
        at the button opened the dropdown instead. Capping the sheet at the viewport turns the
        action row into a footer the flex layout pins to the bottom, and the rule, padding and
        44px-tall buttons below are what actually separate it from the field above. */
@media (max-width: 599.98px) {
    .mud-dialog.dialog-sheet {
        width: 100% !important;
        max-width: 100% !important;
        height: 100dvh;
        max-height: 100dvh;
        margin: 0;
        border: none !important;
        border-radius: 0;
        /* MudBlazor makes .mud-dialog itself scrollable as well as .mud-dialog-content. Two nested
           scrollers means a flick can move either one, and a tap that lands during momentum
           scrolling resolves against wherever the *other* one has since moved to. Hidden here
           leaves .mud-dialog-content as the single scroller. */
        overflow: hidden;
    }

    /* The sheet covers the app bar, so the title takes over its safe-area duty. */
    .mud-dialog.dialog-sheet .mud-dialog-title {
        padding-top: calc(16px + env(safe-area-inset-top, 0px));
        border-bottom: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
    }

    .mud-dialog.dialog-sheet .mud-dialog-content {
        overscroll-behavior: contain;
        padding-bottom: 20px;
    }

    /* Opaque, not the gradient: content scrolls underneath it. */
    .mud-dialog.dialog-sheet .mud-dialog-actions {
        background: var(--surface-card);
        border-top: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
        padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
    }
}

/* The action row's own size is not a property of the sheet, so it is not in the block above.
   Turned sideways, a phone is wider than 600px and gets the ordinary centred dialog — but it is the
   same thumb, and MudBlazor's own button is 36.5px tall, which is what "Annuleren" was when a tap
   aimed at it opened the dropdown above instead. 44px is the platform minimum touch target, and the
   width keeps the two buttons far enough apart that a thumb cannot take Opslaan while aiming for
   Annuleren. Found by scripts/touch-targets.mjs, which measures this at 844x390. */
@media (max-width: 599.98px), (max-height: 559.98px) {
    .mud-dialog.dialog-sheet .mud-dialog-actions .mud-button-root {
        min-height: 44px;
        min-width: 112px;
    }

    /* A numeric field's steppers are two 24x16 buttons stacked flush inside a 32px input row —
       a third of the 44px floor, with no gap between them and opposite effects, so the tap that
       misses one hits the other and counts down instead of up. Two 44px targets cannot fit a 48px
       field, so on a phone the steppers go and the field is what it already was: a number you type,
       behind a numeric keyboard. The arrows are back above 600px, where a pointer can hit them.
       Matched at MudBlazor's own specificity — its rule is
       `.mud-input-control.mud-input-number-control .mud-input-numeric-spin`, so a bare class here
       loses. */
    .mud-input-control.mud-input-number-control .mud-input-numeric-spin {
        display: none;
    }
}

/* --- Date pickers on a phone ---
   Both dialogs that carry a MudDatePicker (new/edit match, and the season editor on /settings) are
   smaller than the picker itself on a phone: MudBlazor's popover is 310x410 and anchored to the
   input's left edge, which hung it 49px off a 320px-wide screen (the whole Saturday column was
   untappable) and, in landscape, 20px below a 390px-tall one.

   So the query is width **or** height — a landscape phone fails on height alone. Inside it the
   calendar stops being anchored and is centred on the viewport, where it always fits. MudBlazor
   writes `left`/`top` inline from JS, which is why every positioning line needs `!important`.

   Only .mud-popover-open is pinned; a closed popover keeps MudBlazor's off-screen -9999px parking
   spot rather than sitting invisible in the middle of the page.

   Centring it was only half the job, and the half that was left is why picking a day still misfired
   on a phone. MudBlazor's day is a 36px circle on a 40px column pitch (`.mud-day` is
   `width/height: 36px; margin: 0 2px`) with rows flush at 36px. That is under Apple's 44px and
   Android's 48dp minimum in both axes, and the 4px gutters between columns are dead — a tap landing
   there hits `.mud-picker-calendar` and does nothing at all. Meanwhile the popover was pinned to
   310px on a sheet that is now the full width of the phone, so 40px of screen sat unused on either
   side of the calendar *and* left live form controls exposed there: with the picker open,
   elementFromPoint 14px outside the calendar returns the season/venue MudSelect underneath.

   --dp-day spends that width instead. Seven columns are sized from the viewport, the 2px side
   margins go (so the column pitch *is* the target — no dead gutters left to miss into), and the
   popover is sized to the row so only ~6px of form shows around it. 320px is the one phone that
   cannot reach 44px — seven of those need 308px and it only has 308px of usable width — so it
   settles at 41.7px, and every wider phone gets 44px or more.

   It has to be the *smaller* of what the width and the height allow, or landscape breaks: 844px of
   width would ask for 52px cells, and six rows of those plus the picker's chrome is 510px in a
   390px-tall viewport — the popover would scroll for most of the month. --dp-chrome is everything
   above the grid (toolbar, month nav, weekday letters, slack); the short-viewport block below
   trims the toolbar and lowers it to match. */
@media (max-width: 599.98px), (max-height: 559.98px) {
    .mud-popover.mud-picker-popover {
        --dp-chrome: 210px;
        --dp-day: clamp(36px, min(calc((100vw - 28px) / 7), calc((100dvh - var(--dp-chrome)) / 6)), 52px);
    }

    .mud-popover.mud-picker-popover.mud-popover-open {
        position: fixed !important;
        top: 50% !important;
        left: 50% !important;
        right: auto !important;
        bottom: auto !important;
        transform: translate(-50%, -50%);
        /* An explicit width, not max-width. .mud-picker-calendar is a wrapping flex row, so its
           min-content width is a single day cell — let the popover shrink-to-fit and the month
           collapses into one tall column. */
        width: min(calc(var(--dp-day) * 7 + 16px), calc(100vw - 12px)) !important;
        max-width: none !important;
        /* Landscape has no room for all of it; scrolling beats clipping the last week. */
        max-height: calc(100dvh - 16px);
        overflow-y: auto;
    }

    /* MudBlazor hard-codes 310px in four places. Now that the popover carries the width, the
       containers just fill it. */
    .mud-picker-popover .mud-picker,
    .mud-picker-popover .mud-picker-container,
    .mud-picker-popover .mud-picker-content,
    .mud-picker-popover .mud-picker-calendar-container,
    .mud-picker-popover .mud-picker-month-container {
        min-width: 0;
        width: 100%;
        max-width: 100%;
    }

    /* The day itself, and the weekday letter above it, which has to keep the same column width or
       the header stops lining up with the grid. The label is inside a max-height:16px flex row, so
       it takes the width only. */
    .mud-picker-popover .mud-picker-calendar .mud-day {
        width: var(--dp-day);
        height: var(--dp-day);
        margin: 0;
        font-size: .875rem;
    }

    .mud-picker-popover .mud-picker-calendar-header-day .mud-day-label {
        width: var(--dp-day);
        margin: 0;
    }

    /* .mud-picker-slide-transition takes its children out of flow (`position: absolute`), so this
       min-height is the only thing reserving room for the grid — MudBlazor's 216px is six 36px
       rows, and taller rows would otherwise be drawn over whatever follows. Always six rows: the
       calendar renders 42 cells whatever the month. */
    .mud-picker-popover .mud-picker-calendar-transition {
        min-height: calc(var(--dp-day) * 6);
    }

    /* The other two views behind the same popover: the month grid off the header's month name, and
       the year list off "2026" in the toolbar. 60px rows were already fine; the 40px year row was
       not, and it is the one target in the picker a finger has to hit inside a scrolling list. */
    .mud-picker-popover .mud-picker-year-container .mud-picker-year {
        height: 48px;
    }

    /* 24px icon + 8px padding = a 40px arrow. These step the month, so they are tapped as often as
       a day is. MudBlazor also gives them `margin: 6px`, which put 6px of dead space between each
       arrow and the month name — the same kind of hole as the 4px gutters between the day cells,
       and in the row docs/known_issues/touch-pwa.md already calls the worst of the picker. The margin stays on the
       vertical axis, where it sets the row's height; sideways it goes, and the month button grows
       to meet the arrows. */
    .mud-picker-popover .mud-picker-calendar-header-switch > .mud-icon-button {
        padding: 10px;
        margin: 6px 0;
    }

    /* "augustus 2026" between those arrows — the button that opens the month grid, and the one
       reported as the worst of the lot. MudBlazor gives it `height: 23px` (it doubles as the slide
       transition's viewport, hence the fixed height and overflow: hidden), so it is a 23px target
       inside a 56px row: 17px of dead div above it and 16px below, and a thumb aimed at the month
       name mostly lands in one of those and does nothing. 44px still fits the row the arrows
       already set, so nothing moves. */
    .mud-picker-popover .mud-picker-calendar-header-switch .mud-picker-calendar-header-transition {
        height: 44px;
        /* Fills whatever the two arrows leave, so the row is three targets edge to edge with no
           dead space in it — the same reason --dp-day drops the day cells' side margins. */
        flex: 1;
    }

    /* The label is taken out of flow by .mud-picker-slide-transition > * (`position: absolute` with
       top/left/right pinned), so it would sit against the top of the taller button. Pinning bottom
       too and centring in the box keeps the month name where it looks anchored. */
    .mud-picker-popover .mud-picker-calendar-header-switch .mud-picker-calendar-header-transition > * {
        bottom: 0;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    /* The banner's year opens the year list — a 64x40 target, and the only way into it. */
    .mud-picker-popover .mud-picker-datepicker-toolbar .mud-button-root {
        min-height: 44px;
    }
}

/* A landscape phone is short, not narrow, and the picker's 100px banner — the "za, 08 aug" restatement
   of the date already in the field — is the one part of it that buys nothing. Halving it is what lets
   six rows fit a 390px-tall viewport without the popover scrolling at all, so the last week of the
   month is on screen rather than one flick below it. --dp-chrome drops by the 44px that frees, less
   8px so a 390px-tall phone lands on 36px rows and 370px of content inside 374px of popover rather
   than 4px over it. Shorter than that (a 360px-tall landscape phone, or a 320px-wide one turned
   sideways) still scrolls — 6 rows at MudBlazor's own 36px plus the chrome is 370px however it is
   sliced — but 44px less than it used to. */
@media (max-height: 559.98px) {
    .mud-popover.mud-picker-popover {
        --dp-chrome: 174px;
    }

    .mud-picker-popover .mud-picker-toolbar {
        height: 56px;
    }

    /* Which leaves room for one 44px button, not two, so the decorative half goes: "za, 08 aug"
       only restates the date that is already in the field behind the popover and in the month
       header below it. The year button stays, because it is the only way into the year list —
       shrinking both to fit would be the same mistake this whole block exists to undo. Picking a
       year returns to the calendar on its own, so nothing is stranded by dropping the date. */
    .mud-picker-popover .mud-picker-datepicker-toolbar .mud-button-date {
        display: none;
    }
}

/* Input field focus */
.mud-input.mud-input-underline::after {
    border-bottom-color: var(--club-primary-bright) !important;
}

/* Progress linear */
.mud-progress-linear .mud-progress-linear-bar {
    border-radius: 4px;
}

/* Chip overrides for formation badge */
.mud-chip.mud-chip-color-secondary {
    background: color-mix(in srgb, var(--club-primary) 12%, transparent) !important;
    color: var(--club-primary-bright) !important;
    border: 1px solid color-mix(in srgb, var(--club-primary) 20%, transparent) !important;
}

/* Snackbar */
/* Success stays green regardless of club primary — a red club would otherwise
   get error-looking success toasts */
.mud-snackbar.mud-alert-filled-success {
    background: var(--gradient-accent) !important;
    color: #fff !important;
}

/* --- Icon action buttons ---
   Global, not scoped, because two pages use them: the game cards on /games and the season rows
   on /settings. They lived in Games.razor.css and so compiled to .action-btn[b-<gamesHash>],
   which meant the /settings buttons matched nothing and rendered as native browser chrome.
   Scoped CSS fails silently like that, so anything more than one page uses belongs here.

   The squad rows on /players are the same row of actions and take the same size, but they cannot
   take the same class: they are MudIconButtons, and one of them is a MudMenu activator, whose
   Class lands on a wrapper div (9.7 has no ActivatorClass). So the size is the token below rather
   than a number written out per page — change it here and every row of icon actions moves. */
:root {
    --action-btn-size: 32px;
}

/* Switching team on /teams is a link rather than a handler, because the team is read off the request
   that creates a scope — so one action in that row is an <a> wearing the same clothes. */
a.action-btn {
    text-decoration: none;
}

.action-btn {
    width: var(--action-btn-size);
    height: var(--action-btn-size);
    border: none;
    background: none;
    color: var(--ink-muted);
    border-radius: 6px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.15s;
    padding: 0;
}

/* Hover is a pointer idea; on a touch screen it sticks after the tap and reads as "selected". */
@media (hover: hover) {
    .action-btn:hover {
        color: var(--club-primary-bright);
        background: color-mix(in srgb, var(--club-primary-bright) 10%, transparent);
    }

    .action-btn.action-delete:hover {
        color: var(--color-danger-bright);
        background: color-mix(in srgb, var(--color-danger) 10%, transparent);
    }

    .action-btn.action-live:hover,
    .action-btn.action-add:hover {
        color: var(--club-on-primary);
        background: var(--club-primary-bright);
    }

    .action-btn.action-needs-lineup:hover {
        color: var(--color-warning-bright);
        background: color-mix(in srgb, var(--color-warning) 22%, transparent);
    }
}

/* Every way into the live screen wears the crest red — the one action on a game card with a colour
   of its own, and it leads the row. Green was one of several greens on a card that already has a
   green edge stripe. */
.action-btn.action-live {
    color: var(--club-on-primary);
    background: var(--club-primary);
}

/* The add button heading a card on /teams, painted like the live button above: it leads the card
   rather than sitting in a row of equals, and grey would leave nothing saying so. */
.action-btn.action-add {
    color: var(--club-on-primary);
    background: var(--club-primary);
    flex-shrink: 0;
}

/* A match actually under way pulses on top of that, which is what separates being played now from
   being played today. Paint and pulse are separate classes so the colour is not also the state. */
.action-btn.action-live-now {
    animation: action-live-pulse 2s ease-in-out infinite;
}

@keyframes action-live-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.6; }
}

@media (prefers-reduced-motion: reduce) {
    .action-btn.action-live-now {
        animation: none;
    }
}

/* Formation action button on a card that has no lineup yet — tint it amber to match
   the panel and nudge the admin toward filling it in. */
.action-btn.action-needs-lineup {
    color: var(--color-warning-bright);
    background: color-mix(in srgb, var(--color-warning) 12%, transparent);
}

/* A finger needs more than the 32px the pointer version uses. 40px was the first answer and it was
   short of the 44px floor on every phone viewport — the guess nobody could check until /games came
   into scripts/touch-targets.mjs. Six of these sit side by side on a game card on match day; how
   they are arranged so six of them fit is Games.razor.css's problem, but none of them is under 44
   anywhere. */
@media (pointer: coarse) {
    :root {
        --action-btn-size: 44px;
    }
}

/* Navigation links in top bar */
.topbar-nav-link {
    padding: 6px 14px !important;
    border-radius: 6px !important;
    font-size: 0.8rem !important;
    font-weight: 500 !important;
    color: var(--ink-muted) !important;
    cursor: pointer;
    transition: all 0.15s;
    text-decoration: none !important;
    min-height: auto !important;
}

/* The min-height: auto above is what keeps five links on a dense app bar; on a touch screen the bar
   is the wrong thing to save height on, so the floor comes back. On the anchor, not the element
   carrying the class: MudNavLink puts Class on its wrapper, the same trap MudMenu has. */
@media (pointer: coarse) {
    .topbar-nav-link .mud-nav-link {
        min-height: 44px !important;
    }
}

.topbar-nav-link:hover {
    color: color-mix(in srgb, var(--ink) 80%, transparent) !important;
    background: color-mix(in srgb, var(--ink) 4%, transparent) !important;
}

.topbar-nav-link.active,
.topbar-nav-link[aria-current="page"] {
    color: var(--club-primary-bright) !important;
    background: color-mix(in srgb, var(--club-primary) 10%, transparent) !important;
}

/* Hide MudNavLink default icon area in top bar */
.topbar-nav-link .mud-nav-link-icon {
    display: none;
}

/* --- Season picker ---
   Not scoped CSS: it renders from both the app bar and the mobile drawer, and isolation would force
   a duplicate stylesheet for the second placement.

   A <details> disclosure rather than MudMenu. It renders in the layout, which is statically
   rendered on every page, so there is no circuit to open a popover from — and the browser gives
   keyboard and screen-reader behaviour for a disclosure for free. */
.season-picker {
    position: relative;
}

.season-picker > summary {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    /* 44px so a thumb finds it; the visual chip is smaller than the hit area. */
    min-height: 44px;
    padding: 4px 10px;
    border-radius: 6px;
    font-size: 0.8rem;
    font-weight: 500;
    white-space: nowrap;
    color: var(--ink-muted);
    cursor: pointer;
    list-style: none;
}

/* Safari draws its own triangle through a pseudo-element the standard property does not reach. */
.season-picker > summary::-webkit-details-marker {
    display: none;
}

.season-picker > summary:hover {
    color: color-mix(in srgb, var(--ink) 80%, transparent);
    background: color-mix(in srgb, var(--ink) 4%, transparent);
}

.season-picker-menu {
    position: absolute;
    top: 100%;
    right: 0;
    z-index: 1400;
    min-width: 180px;
    padding: 4px;
    display: flex;
    flex-direction: column;
    border-radius: 8px;
    background: var(--surface-card);
    box-shadow: 0 4px 16px color-mix(in srgb, var(--ink) 18%, transparent);
}

/* Menu rows pair the season name with its "current" pill */
.season-menu-item {
    display: flex;
    align-items: center;
    gap: 8px;
    /* Same 44px floor as every other row a thumb has to hit. */
    min-height: 44px;
    padding: 0 12px;
    border-radius: 6px;
    font-size: 0.875rem;
    color: var(--ink);
    text-decoration: none;
}

.season-menu-item:hover {
    background: var(--surface-card-alt);
}

/* The list and "All seasons" are different kinds of choice, so a rule separates them — this is
   what MudDivider used to draw. */
.season-menu-all {
    margin-top: 4px;
    border-top: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
    border-radius: 0 0 6px 6px;
}

/* In the drawer the picker is a block row under the title, not an app-bar chip */
.drawer-season-picker {
    padding: 0 8px 8px;
}

/* In the drawer there is no room to the right, so the menu opens under the summary in flow. */
.drawer-season-picker .season-picker-menu {
    position: static;
    box-shadow: none;
    background: none;
    min-width: 0;
}

/* --- List rows ---
   The season list on /settings and the club and team rows on /teams. Global, not scoped, for the
   same reason .action-btn is: a second page's copy of it compiles to .list-row[b-otherHash] and
   renders as nothing. */
.list-rows {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.list-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 12px;
    border-radius: 8px;
    background: color-mix(in srgb, var(--ink) 3%, transparent);
}

/* Grows, so a row carrying a leading crest keeps its name against it rather than letting
   space-between push the two apart. */
.list-row-info {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    flex: 1;
}

.list-row-name {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-weight: 600;
    font-size: 0.9rem;
    color: var(--ink);
}

.list-row-meta {
    font-size: 0.75rem;
    color: var(--ink-muted);
}

.list-row-actions {
    display: flex;
    align-items: center;
    gap: 2px;
    flex-shrink: 0;
}

/* Flush on a touch screen, for the same reason the game card's action row is: 2px between two
   targets is a dead gutter — too narrow to see or aim around, wide enough to swallow a tap, and
   handed by the browser to whichever neighbour has the larger contact area. A gap has to be
   nothing or at least 8px. See scripts/touch-targets.mjs. (Neither page is in that audit's scope
   yet — this row is the same three-button shape as the game card's, fixed with it.) */
@media (pointer: coarse) {
    .list-row-actions {
        gap: 0;
    }
}

/* Gold button style */
.btn-gold {
    background: var(--gradient-primary) !important;
    color: var(--club-on-primary) !important;
    font-weight: 600 !important;
    border: none !important;
}

.btn-gold:hover {
    filter: brightness(1.15);
    transform: translateY(-1px);
}

/* MudMenu applies its Class to the root wrapper, not to the activator button, so
   btn-gold on a MudMenu paints an invisible div and leaves the button in MudBlazor's
   default filled colours. Push the style down one level instead. */
.btn-gold.mud-menu {
    background: none !important;
    border: none !important;
    display: inline-flex;
}

.btn-gold.mud-menu .mud-button-root {
    background: var(--gradient-primary) !important;
    color: var(--club-on-primary) !important;
    font-weight: 600 !important;
    border: none !important;
}

.btn-gold.mud-menu:hover {
    transform: none;
}

.btn-gold.mud-menu .mud-button-root:hover {
    filter: brightness(1.15);
    transform: translateY(-1px);
}

/* Ghost button */
.btn-ghost {
    background: transparent !important;
    border: 1px solid color-mix(in srgb, var(--ink) 10%, transparent) !important;
    color: var(--ink-muted) !important;
}

.btn-ghost:hover {
    border-color: var(--ink-faint) !important;
    color: color-mix(in srgb, var(--ink) 90%, transparent) !important;
}

/* Badge styles */
.badge-gold {
    font-size: 0.7rem;
    font-weight: 600;
    padding: 4px 10px;
    border-radius: 4px;
    background: color-mix(in srgb, var(--club-primary) 12%, transparent);
    color: var(--club-primary-bright);
    border: 1px solid color-mix(in srgb, var(--club-primary) 20%, transparent);
}

.badge-teal {
    font-size: 0.7rem;
    font-weight: 600;
    padding: 4px 10px;
    border-radius: 4px;
    background: color-mix(in srgb, var(--club-accent) 12%, transparent);
    color: var(--club-accent-bright);
    border: 1px solid color-mix(in srgb, var(--club-accent) 20%, transparent);
}

.badge-guest {
    font-size: 0.6rem;
    font-weight: 700;
    letter-spacing: 0.5px;
    padding: 2px 6px;
    border-radius: 4px;
    background: color-mix(in srgb, var(--color-guest) 12%, transparent);
    color: var(--color-guest-bright);
    border: 1px solid color-mix(in srgb, var(--color-guest) 20%, transparent);
}

/* Not a badge: a red cross in front of the name is read without being spelled out. GUEST and
   ARCHIVED still need their word — nothing about them is pictured. */
.injured-mark {
    color: var(--color-danger-bright);
    font-size: 1rem;
    flex-shrink: 0;
}

/* "No longer at the club" — deliberately the quietest badge on the row. It is a statement of
   absence, not a warning, and it sits next to GUEST, which is the one that still needs to be
   read at a glance. */
.badge-archived {
    font-size: 0.6rem;
    font-weight: 700;
    letter-spacing: 0.5px;
    padding: 2px 6px;
    border-radius: 4px;
    background: color-mix(in srgb, var(--ink) 6%, transparent);
    color: var(--ink-subtle);
    border: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
}

/* The venue badge's shape only. Its colour is in the two rules below, one of which the VenueBadge
   component always adds — nothing renders this class on its own. */
.badge-venue {
    font-size: 0.65rem;
    font-weight: 600;
    padding: 3px 8px;
    border-radius: 4px;
    border: 1px solid;
}

/* Green for home and blue for away: the pair the match overview already stripes a card's edges
   with. The stripe and the word then say the same thing in the same colour, instead of being two
   conventions to learn — and the badge carries that colour onto the screens with no stripe.

   The text takes the *deep* end of each ramp, not the -bright one the larger badges use. Contrast
   here is against the badge's own 12% fill, not the bare card, and that fill lightens the
   background by more than it looks: --club-accent-bright measures 4.31:1 over it on --surface-card
   and 3.99:1 on --surface-card-alt, both under AA for text this small. The deep pair clears it on
   every surface the badge lands on (5.27 / 4.88 home, 5.22 / 4.82 away). */
.badge-venue-home {
    background: color-mix(in srgb, var(--club-accent) 12%, transparent);
    color: var(--club-accent-deep);
    border-color: color-mix(in srgb, var(--club-accent) 20%, transparent);
}

.badge-venue-away {
    background: color-mix(in srgb, var(--color-away) 12%, transparent);
    color: var(--color-away-bright);
    border-color: color-mix(in srgb, var(--color-away) 20%, transparent);
}

/* The same badge trailing an opponent's name rather than standing alone in a page header. It
   annotates that name, so it has to stay shorter than it — at the header size the box is taller
   than the 0.82rem text it follows and reads as the louder of the two. */
.badge-venue-inline {
    font-size: 0.6rem;
    padding: 1px 6px;
    margin-left: 8px;
}

.badge-unavailable {
    font-size: 0.65rem;
    font-weight: 600;
    padding: 3px 8px;
    border-radius: 4px;
    background: color-mix(in srgb, var(--color-danger) 10%, transparent);
    color: var(--color-danger-bright);
    border: 1px solid color-mix(in srgb, var(--color-danger) 15%, transparent);
}

/* The counterpart to .badge-unavailable, on a session the whole squad turned up to — the register is only worth reading when it
   also says when nobody was missing. */
.badge-present {
    font-size: 0.65rem;
    font-weight: 600;
    padding: 3px 8px;
    border-radius: 4px;
    background: color-mix(in srgb, var(--color-success-bright) 10%, transparent);
    color: var(--color-success-deep);
    border: 1px solid color-mix(in srgb, var(--color-success-bright) 20%, transparent);
}

/* Amber, not red: worth a look, not an alarm. */
.badge-warning {
    font-size: 0.65rem;
    font-weight: 600;
    padding: 3px 8px;
    border-radius: 4px;
    background: color-mix(in srgb, var(--color-warning) 12%, transparent);
    color: var(--color-warning-bright);
    border: 1px solid color-mix(in srgb, var(--color-warning) 20%, transparent);
}

/* --- The scoreline, on the live screen and on the result page ---
   One block for both, because the two screens show the same score and have already drifted apart
   once: MatchResult's away colour went missing and both numbers rendered club-red there while the
   live screen had them right. Each page keeps only the row it lays these out in — a flex row while
   the clock is running, a grid once the result is filed. */
.score-name,
.live-score-name {
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--ink-muted);
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.score-value,
.live-score-value {
    font-size: 2.75rem;
    font-weight: 800;
    line-height: 1.1;
    color: var(--club-primary);
}

.score-away,
.live-score-away {
    color: var(--color-away);
}

.score-vs,
.live-score-dash {
    font-size: 1.75rem;
    font-weight: 300;
    color: var(--ink-faint);
}

/* --- Live match ---
   These sit here rather than in LiveMatch.razor.css because they target MudBlazor components
   (two MudPapers and the buttons inside them), and scoped CSS never reaches a child component's
   root element. The rest of that page's styling is scoped as normal. */
.live-scoreboard,
.live-controls {
    border-radius: 16px;
}

.live-scoreboard {
    text-align: center;
}

.live-action-btn {
    flex: 1;
    /* Thumb-sized: pressed while watching the pitch, not the phone. */
    min-height: 56px;
}

/* The clock controls. A grid rather than a wrapping flex row: how many buttons the panel holds
   depends on where the match is, and equal columns keep them the same width and the same height
   whichever set is showing. The odd one out spans the row instead of sitting half-width beside a
   gap — which is every case with a single button, and the third of three. */
.live-control-row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
}

.live-control-row > * {
    width: 100%;
    /* Not as tall as a scoring button: those are the ones pressed without looking. */
    min-height: 48px;
}

.live-control-row > *:last-child:nth-child(odd) {
    grid-column: 1 / -1;
}

/* A card's heading row: the label on the left, the one control that belongs to the card on the
   right — the plan behind the line-up's half, the substitutions toggle on the timeline.
   .card-label carries its own bottom margin, so the row takes it over and the label gives it up —
   otherwise the control sits 16px above the card body and the label doesn't. */
.live-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 16px;
}

.live-card-head .card-label {
    margin-bottom: 0;
}

.live-plan-btn {
    flex: none;
    /* Small on a desktop, still a thumb target on the phone this is opened on. */
    min-height: 44px;
    white-space: nowrap;
}

/* The "Show substitutions" toggle rides the timeline's heading, so its label is sized down to sit
   beside a card label rather than to head the list itself. The size goes on the label element:
   MudBlazor renders it as its own .mud-typography-body1, which carries a font-size of its own and
   inherits nothing from the wrapper. */
.live-timeline-toggle {
    flex: none;
}

.live-timeline-toggle .mud-checkbox .mud-typography {
    font-size: 0.8rem;
    color: var(--ink-muted);
}

/* The checkbox is dense, which is right on a desktop and under the 44px floor on the phone this
   screen is actually used on. Its label is the target, so the height goes there. */
@media (pointer: coarse) {
    .live-timeline-toggle .mud-checkbox {
        min-height: 44px;
    }
}

/* The injury cross in the timeline. Global rather than in MatchTimeline.razor.css because it paints a MudIcon, and scoped CSS never
   reaches a child component's root element. The same red as the squad page's .injured-mark. */
.live-event-injury {
    color: var(--color-danger-bright);
}

/* On a phone what just happened matters more than where everyone stands, so the line-up card
   drops below the timeline, taking the minutes table with it. Every other child of .live-layout
   keeps the default order of 0 and so stays in source order above them. */
@media (max-width: 599.98px) {
    .live-lineup {
        order: 1;
    }

    .live-minutes-card {
        order: 2;
    }

    /* Last in source order is not last once anything above it has been reordered. */
    .live-controls-foot {
        order: 3;
    }
}

/* Home tiles. The whole card is a link — an anchor wrapping the MudPaper rather than a div with an
   @onclick, so it works with no circuit and the browser gives it keyboard activation for free. */
.home-tile-link {
    display: block;
    height: 100%;
    color: inherit;
    text-decoration: none;
    border-radius: 16px;
}

.home-tile-link:focus-visible {
    outline: 2px solid var(--club-primary-bright);
    outline-offset: 2px;
}

.home-tile {
    height: 100%;
    display: flex;
    flex-direction: column;
    cursor: pointer;
    transition: transform 120ms ease, box-shadow 120ms ease;
}

.home-tile-link:hover .home-tile,
.home-tile:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 20px color-mix(in srgb, var(--club-primary) 22%, transparent);
}

.home-tile-link:active .home-tile,
.home-tile:active {
    transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
    .home-tile {
        transition: none;
    }

    .home-tile-link:hover .home-tile,
    .home-tile:hover {
        transform: none;
    }
}

/* Page header styling */
.page-header-subtitle {
    color: var(--ink-muted);
    font-size: 0.85rem;
}

/* PageHeader renders the heading and the wrapper, so anything targeting them belongs here rather
   than in a page's scoped stylesheet — the scope attribute would be PageHeader's, not the page's. */

/* Match result: the date · venue · formation line is kept a touch darker than a normal body2
   so it reads at a glance. */
.result-header .result-subtitle {
    color: color-mix(in srgb, var(--ink) 68%, transparent);
    font-weight: 500;
}

/* The comments span the whole width below the score and timeline: comment bodies are prose, and the scoreboard's column is not a column for prose.
   Global rather than scoped because it lands on a MudPaper, whose root element carries no scope attribute for the page's stylesheet. */
.result-comments {
    grid-column: 1 / -1;
}

/* Formation builder: "Formation: [dropdown]" on a desktop, where the select is a MudBlazor
   component whose own elements a scoped rule cannot reach. On a phone the select carries its own
   floating label and stands on its own line, so only the desktop row is written here. */
.formation-picker {
    display: flex;
    align-items: center;
    gap: 8px;
}

.formation-picker-label {
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--ink-muted);
}

.formation-picker .formation-select {
    width: 180px;
}

/* Formation builder: the opponent has to give way to the badges beside it on a narrow screen. */
.builder-opponent {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (max-width: 599.98px) {
    /* The opponent name repeats in the score card below, so drop the heading on a phone and give
       the subtitle that prominence instead. */
    .result-header .result-opponent {
        display: none;
    }

    .result-header .result-subtitle {
        font-size: 0.95rem;
        font-weight: 600;
        color: color-mix(in srgb, var(--ink) 82%, transparent);
    }
}

/* --- The settings sections and the notification row ---
   Global, not scoped: the row renders from the start page and from /settings, and MatchNotifications
   carries the section wrapper for the second of those — a copy in either page's .razor.css would
   compile to .notify-row[b-otherHash] and match nothing. */
.settings-section {
    max-width: 500px;
}

.settings-section + .settings-section {
    margin-top: 40px;
}

.settings-section-title {
    margin: 0 0 12px;
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--ink);
}

.notify-row {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 20px;
    padding: 12px 16px;
    border: 1px solid var(--ink-faint);
    border-radius: 16px;
    background: var(--gradient-card);
}

.notify-body {
    flex: 1;
    min-width: 0;
}

.notify-title {
    font-weight: 600;
    color: var(--ink);
}

/* Quieter than the label it follows, and free to wrap onto its own line on a narrow phone. */
.notify-team {
    font-weight: 400;
    color: var(--ink-muted);
}

.notify-text {
    font-size: 0.85rem;
    color: var(--ink-muted);
}

/* Inside a settings section the row is the card, so it does not also carry the gap to the next one. */
.settings-section > .notify-row {
    margin-bottom: 0;
}

@media (max-width: 599.98px) {
    .notify-row {
        flex-wrap: wrap;
    }
}

/* Card label */
.card-label {
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--club-primary-bright);
    margin-bottom: 16px;
}

/* Custom scrollbar */
::-webkit-scrollbar {
    width: 6px;
}

::-webkit-scrollbar-track {
    background: color-mix(in srgb, var(--ink) 2%, transparent);
}

::-webkit-scrollbar-thumb {
    background: color-mix(in srgb, var(--ink) 10%, transparent);
    border-radius: 3px;
}

::-webkit-scrollbar-thumb:hover {
    background: color-mix(in srgb, var(--ink) 30%, transparent);
}

/* Fix: MudPopoverProvider sits before MudLayout in DOM — collapse it to zero height
   so it doesn't push content down when popovers open. Do NOT set position or z-index
   here, as that creates a stacking context that traps popovers behind the dialog. */
.mud-popover-provider {
    height: 0 !important;
    overflow: visible;
}

/* The two rules that used to sit here restored .mud-popover{position:absolute} and
   .mud-popover-fixed{position:fixed} after the card rule had overwritten them with
   position:relative — which is what turned every dropdown into a full-width band across the
   page. The card rule now excludes .mud-popover outright, so MudBlazor's own positioning is
   never disturbed and there is nothing left to put back. */

/* Fix: MudSelect dropdown double scrollbar — MudBlazor v9 only sets overflow:hidden on
   menu popovers (:has(>.mud-menu-list)) but not select popovers. The .mud-popover gets
   a scrollbar AND .mud-popover .mud-list also has overflow-y:auto, producing two nested
   scrollbars. Mirror the menu fix for select lists. */
.mud-popover:has(> .mud-list) {
    overflow: hidden;
}

/* Unhandled circuit error. Blazor unhides this itself; the markup is in App.razor.
   Themed rather than the template's lightyellow, and safe-area padded like every other
   fixed element so it clears the home indicator on a phone. */
#blazor-error-ui {
    display: none;
    position: fixed;
    left: 0;
    bottom: 0;
    width: 100%;
    z-index: 1000;
    padding: 12px 20px calc(12px + env(safe-area-inset-bottom, 0px));
    background: var(--color-danger-bright);
    color: #fff;
    font-size: 0.85rem;
    text-align: center;
}

/* Blazor reveals the bar by setting display:block inline, which would beat any flex layout
   declared above — so the contents are laid out inline instead. */
#blazor-error-ui .reload {
    margin-left: 16px;
}

#blazor-error-ui .reload {
    color: #fff;
    font-weight: 600;
    text-decoration: underline;
    cursor: pointer;
}

/* Touch drag support (see js/drag-drop-touch.js): the browser must not claim
   touches on draggable elements for text selection or the iOS callout, or the
   synthetic drag never starts. */
[draggable="true"] {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    touch-action: none;
}

/* …but touch-action: none also forbids scrolling, and every row of the formation builder's squad
   and bench is draggable — a finger landing on a player could scroll neither the panel it sits in
   nor the page. pan-y gives the vertical gesture back; the shim only starts a drag once the finger
   has moved 8px, and a drag that begins as a vertical pan is the one case worth losing.
   The pitch chips keep touch-action: none — there, every direction is a drag. */
.draggable-player,
.sub-item {
    touch-action: pan-y;
}

/* --- Mobile navigation & PWA safe areas --- */

/* --- The drawer, which is a checkbox ---
   No JS and no circuit: the layout is statically rendered on every page, so the open state is an
   :checked sibling rather than a bool on a component. A drawer that depended on a script would
   fail the way docs/known_issues/touch-pwa.md records for blazor.web.js — silently, and completely. */

/* Visually hidden but still focusable: this is the control a keyboard or screen reader gets, and
   display:none or visibility:hidden would take it out of the tab order with it. */
.nav-drawer-toggle {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* On every width, because above the breakpoint the bar now drops the nav links it cannot fit and the
   drawer is where those stay reachable — see the .topbar-nav block below. */
.nav-hamburger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    margin-inline-start: -8px;
    border-radius: 50%;
    color: color-mix(in srgb, var(--ink) 80%, transparent);
    cursor: pointer;
}

.nav-drawer-toggle:focus-visible ~ .mud-appbar .nav-hamburger {
    outline: 2px solid var(--club-primary-bright);
    outline-offset: 2px;
}

.app-drawer {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    z-index: 1300;
    width: 260px;
    max-width: 80vw;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    background: var(--surface-card);
    box-shadow: 2px 0 12px color-mix(in srgb, var(--ink) 20%, transparent);
    transform: translateX(-100%);
    /* Parked off-screen hides it from the eye and from nothing else, so a closed drawer would be ten
       tab stops and a second copy of the menu to a screen reader — on every width, now that it is on
       every width. Delayed to the end of the slide so the transform still animates out. */
    visibility: hidden;
    transition: transform 200ms ease, visibility 0s linear 200ms;
}

.drawer-scrim {
    position: fixed;
    inset: 0;
    z-index: 1299;
    background: color-mix(in srgb, var(--ink) 40%, transparent);
    opacity: 0;
    pointer-events: none;
    transition: opacity 200ms ease;
}

.nav-drawer-toggle:checked ~ .app-drawer {
    transform: translateX(0);
    visibility: visible;
    transition: transform 200ms ease;
}

.nav-drawer-toggle:checked ~ .drawer-scrim {
    opacity: 1;
    pointer-events: auto;
}

@media (prefers-reduced-motion: reduce) {
    .app-drawer,
    .drawer-scrim {
        transition: none;
    }
}

/* The install banner is fixed to the foot of the viewport and sits above the drawer (1390 to its
   1300), which is exactly where the drawer keeps its administration group — on a phone it covered
   settings and users, and the app bar has no nav there to reach them from instead. Out of the way
   while the drawer is open; pwa.js only ever toggles the hidden attribute, so nothing overrides it. */
.nav-drawer-toggle:checked ~ .install-banner {
    display: none;
}

/* Settings, users and teams sit at the foot of the drawer rather than among the sections a coach
   opens during a match. Scoped to the drawer, not a global .mud-* layout rule: the menu has to
   stretch before the auto margin below has anything to push into. */
.app-drawer .mud-navmenu {
    flex: 1;
    display: flex;
    flex-direction: column;
}

.nav-group-admin {
    margin-top: auto;
    padding-top: 8px;
    border-top: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
}

/* How many links fit is the bar's own arithmetic rather than a number kept in step by hand. The nav
   is the only item allowed to give way: what it cannot fit wraps onto a second row that max-height
   clips, and the drawer carries the whole menu at every width, so a dropped link is one tap away.
   #137 is what the hand-kept number cost — 700px was derived for five links, and there are seven,
   which put the season picker, the language picker and sign out off the right-hand edge entirely. */
/* clip, not hidden: hidden makes a scroll container, and tabbing to a link on the clipped row would
   scroll the visible row out of the bar to reach it. */
.topbar-nav {
    min-width: 0;
    flex-wrap: wrap;
    align-content: flex-start;
    max-height: calc(2.4rem + 12px);
    overflow: clip;
}

/* One row exactly: MudBlazor's 2.4rem nav link plus the 6px .topbar-nav-link pads it with, and here
   the 44px touch floor standing in for the 2.4rem. A hair short cuts the row the clip is there to
   keep, and the touch audit stops measuring a link it can only see part of. */
@media (pointer: coarse) {
    .topbar-nav {
        max-height: calc(44px + 12px);
    }
}

/* MudNavLink's wrapper is a full-width row — which nowrap was quietly shrinking back. Once the nav
   wraps, that width is taken literally and every link lands on a line of its own. */
.topbar-nav > * {
    flex: none;
    width: auto;
}

/* Only where there is a nav to give way: below the breakpoint it is hidden, and the phone bar is
   already tight enough to need whatever room its own items can spare. */
@media (min-width: 701px) {
    .mud-appbar .mud-toolbar > *:not(.topbar-nav) {
        flex-shrink: 0;
    }
}

/* A design tier now, not arithmetic: a phone bar is the hamburger, the crest and who is signed in,
   and the sections live in the drawer whether or not one or two of them would have fitted. */
@media (max-width: 700px) {
    .topbar-nav {
        display: none !important;
    }

    /* The title link. Was selected as .ml-2.mr-8 — by the MudBlazor spacing utilities it happens
       to carry — so changing mr-8 to mr-6 in MainLayout would have silently switched this off.
       .app-title-link is on the same element and means what it says. */
    .mud-appbar .app-title-link {
        margin-right: 8px !important;
    }

    /* The app bar is already crowded on a phone; the drawer copy of the picker is the
       one that stays. Targets only the app-bar instance — the drawer one sits outside
       .mud-appbar, in .drawer-season-picker. */
    .mud-appbar .season-picker {
        display: none !important;
    }

    /* Header actions on /games and /players: the icon carries the meaning on a phone,
       and the labels ("Copy squad from 2024/25") are far too wide for the header row.
       font-size:0 collapses the bare text node MudBlazor renders next to the icon —
       the icon keeps its own font-size from .mud-icon-size-*, so it is unaffected. */
    .btn-compact .mud-button-label {
        font-size: 0;
    }

    .btn-compact .mud-button-icon-start {
        margin-right: 0 !important;
        margin-inline-end: 0 !important;
    }

    .btn-compact.mud-button-root,
    .btn-compact .mud-button-root {
        min-width: 0 !important;
    }

    /* Stack the squad header actions, Add on top. The markup order is Copy then Add so
       that desktop reads left-to-right; column-reverse flips it without a second layout. */
    .squad-actions {
        flex-direction: column-reverse;
        align-items: stretch !important;
    }
}

/* A header action is 36.5px tall — MudBlazor's own height — and collapsing its label to an icon
   above makes it 51.6x32. Both are under the 44px floor. Keyed on the pointer rather than the
   width, because the landscape case is the one a width query misses: a phone turned sideways is
   844px wide, so the block above stops applying while the thumb does not change size. A minimum
   only, so a labelled button keeps whatever height its text asks for. */
@media (pointer: coarse) {
    .btn-gold.mud-button-root,
    .btn-gold .mud-button-root,
    .btn-compact.mud-button-root,
    .btn-compact .mud-button-root,
    .btn-ghost.mud-button-root,
    .btn-ghost .mud-button-root {
        min-height: 44px;
    }
}

/* --- Statistics: tiles, bars and figure columns ---
   Shared by /stats and /players/{id}/stats. Both sheets carried their own byte-identical copy of
   this block — about seventy duplicated lines, and SeasonStats.razor.css opened with the comment
   "same look as the player-stats page", which is the sort of thing that stays true right up until
   it doesn't. Every element here is a plain div the page owns, so scoping bought nothing. */

.stat-tiles {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 12px;
}

/* A visitor's player page has no minutes tile; an admin's gains a trainings one once the season has
   sessions on file. The track count is explicit rather than auto-fit because auto-fit needs a
   definite minimum, and a px minimum would wrap the row on a phone. */
.stat-tiles.stat-tiles-3 {
    grid-template-columns: repeat(3, 1fr);
}

.stat-tiles.stat-tiles-5 {
    grid-template-columns: repeat(5, 1fr);
}

.stat-tile {
    background: var(--surface-card);
    border: 1px solid color-mix(in srgb, var(--ink) 6%, transparent);
    border-radius: 14px;
    padding: 16px 12px;
    text-align: center;
    box-shadow: 0 2px 4px color-mix(in srgb, var(--ink) 8%, transparent);
}

.stat-value {
    font-size: 1.9rem;
    font-weight: 800;
    line-height: 1;
    color: var(--club-primary-bright);
}

.stat-unit {
    font-size: 1rem;
    font-weight: 700;
    margin-left: 1px;
}

.stat-label {
    margin-top: 6px;
    font-size: 0.62rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.8px;
    color: var(--ink-muted);
}

.stat-sub {
    margin-top: 3px;
    font-size: 0.65rem;
    color: var(--ink-muted);
}

/* Two-column body */
.stats-layout {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 20px;
    align-items: start;
}

/* Proportion bars — minutes per position on one page, playing-time fairness on the other */
.position-track {
    height: 8px;
    border-radius: 6px;
    background: color-mix(in srgb, var(--ink) 6%, transparent);
    overflow: hidden;
}

.position-fill {
    height: 100%;
    border-radius: 6px;
    background: var(--gradient-primary);
    min-width: 2px;
}

/* Right-aligned figure columns in the per-game lists */
.g-num {
    text-align: center;
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--ink-muted);
}

.g-num.g-hit {
    color: var(--club-primary-bright);
    font-weight: 800;
}

@media (max-width: 760px) {
    /* Every selector, or a two-class rule outranks this one and stays three- or five-across on a
       phone. */
    .stat-tiles,
    .stat-tiles.stat-tiles-3,
    .stat-tiles.stat-tiles-5 {
        grid-template-columns: repeat(2, 1fr);
    }

    .stats-layout {
        grid-template-columns: 1fr;
    }
}

/* --- App title / home link --- */

/* Neither the title link nor a drawer entry takes --action-btn-size: both are sized by the text and
   icon inside them, which lands under the touch floor on every phone width. */
@media (pointer: coarse) {
    .app-title-link,
    .app-drawer .mud-nav-link,
    .logout-btn {
        min-height: 44px;
    }
}

.logout-btn {
    background: none;
    border: none;
    color: var(--ink-subtle);
    cursor: pointer;
    font-size: 0.875rem;
    padding: 4px 8px;
    text-transform: none;
}

.app-title-link {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    text-decoration: none;
    white-space: nowrap;
}

/* An img rather than the --club-logo background it used to be: the crest is the club's, and a club
   is a database row now. The chip behind it stays a token, so a transparent crest keeps its plate. */
.app-title-logo {
    width: 28px;
    height: 28px;
    border-radius: 6px;
    background-color: var(--club-logo-bg);
    object-fit: contain;
    flex: none;
}

.app-title-text {
    font-size: 1.15rem;
    font-weight: 700;
    background: var(--gradient-primary);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
}

/* --- Squad table --- */

/* Alternative positions and the row actions hug the right edge of the table */
.players-table .cell-right {
    justify-content: flex-end;
}

/* Badge spacing comes from the container's gap, not a margin on each badge. It used to be an
   inline style="margin-right: 4px" in Players.razor, which then needed !important rules in two
   places to switch it off again on mobile — this app's CSS beating this app's own markup. A gap
   has no trailing edge to undo. */
.players-table .cell-alt,
.players-table .cell-right {
    display: flex;
    gap: 4px;
    flex-wrap: wrap;
}

/* An anchor to that player's statistics, so it brings a colour and an underline to override — the
   same shape as .pd-name-cell on /stats/positions. The 44px floor is what makes it a target a thumb
   can hit rather than a word-sized one, and it is why the row does not need a height of its own. */
.players-table .player-name-cell {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    min-height: 44px;
    color: inherit;
    text-decoration: none;
}

.player-name-cell .badge-guest,
.player-name-cell .badge-archived {
    margin-left: 6px;
}

.player-name-cell .injured-mark {
    margin-right: 4px;
}

/* Position abbreviations are two or three letters (GK, CB, CDM), and a badge that shrink-wraps
   each one makes a column of ragged chips — the alternatives on the row below never line up with
   the preferred position above them. Sized for the widest abbreviation instead, so every position
   on the page is the same box. Only the squad table: elsewhere .badge-teal/.badge-gold carry
   words, which a min-width would only pad. */
.players-table .cell-pref .badge-teal,
.players-table .cell-alt .badge-gold {
    display: inline-block;
    min-width: 3.25rem;
    text-align: center;
}

/* Row actions: MudMenu wraps its trigger in a plain div, whose inline-block baseline sits
   lower than the inline-flex MudIconButtons beside it — the overflow ⋮ ended up a couple of
   pixels below the other icons. Making the row and the wrapper flex takes baselines out of
   the equation entirely. */
.players-table .row-actions {
    display: inline-flex;
    align-items: center;
    gap: 2px;
}

.players-table .row-actions .mud-menu {
    display: inline-flex;
    align-items: center;
}

/* The same box as a game card's action row, from the same token — see --action-btn-size above for
   why this row cannot simply wear .action-btn. MudBlazor sizes a small icon button from its
   padding, which lands at 26px and left two rows of icons that do the same job on two pages at two
   different widths. Its rule is a bare .mud-icon-button-size-small, so this selector outspecifies
   it without !important. */
.players-table .row-actions .mud-icon-button {
    width: var(--action-btn-size);
    height: var(--action-btn-size);
    padding: 0;
}

/* Flush, for the same reason the game card's action row is: a 2px gap between two 44px targets is
   a dead gutter, and a gap has to be nothing or at least 8px. */
@media (pointer: coarse) {
    .players-table .row-actions {
        gap: 0;
    }
}

/* The name links to that player's stats. The chevron sits right after it rather than at the far
   edge of the row: on a phone the row is a two-column grid, and a right-aligned chevron would land
   between the name and the position badge instead of at the end. */
.player-name-chevron {
    margin-left: 6px;
    font-weight: 700;
    line-height: 1;
    color: var(--ink-faint);
}

.players-table .mud-table-row:hover .player-name-chevron,
.position-dev-table .mud-table-row:hover .player-name-chevron {
    color: var(--club-primary-bright);
}

/* Spells out what the chevron hints at, once, above the table */
.squad-hint {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-top: 2px;
    color: var(--ink-subtle);
}

/* Shirt number sits in its own # column on wide screens */
.shirt-inline {
    display: none;
}

/* 599.98px is where MudBlazor stacks a table into per-row cards (its xs breakpoint),
   so these rules and that layout switch together. */
@media (max-width: 599.98px) {
    /* MudBlazor's stacked card prints the column label beside every cell, which on
       this table is far more label than data. Lay each row out as a grid instead:
       name + preferred position share the first line, alternatives get the second,
       the row actions the third. The table parts have to stop being table boxes
       for the row to become a grid container. */
    .players-table .mud-table-body .mud-table-row {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: center;
        column-gap: 8px;
        row-gap: 6px;
        padding: 10px 14px;
    }

    /* Below its breakpoint MudBlazor replaces the sortable headers with a select above the rows.
       This table sets no SortLabel, so it arrives unlabelled: a blank dropdown across the top of
       the squad that says nothing about what it does — and it only turns up once something has
       re-rendered the table, which is why it reads as a glitch rather than a control. The list
       is already ordered — guests last, then shirt number, then name — so sorting it stays a
       desktop affordance. */
    .players-table .mud-table-smalldevices-sortselect {
        display: none;
    }

    /* A whole card row just for the number is wasteful: drop the # cell and print
       the number next to the name instead. */
    .players-table th:first-child,
    .players-table td[data-label="#"] {
        display: none !important;
    }

    .shirt-inline {
        display: inline;
        margin-right: 6px;
        font-weight: 700;
        color: var(--club-primary-bright);
    }

    .players-table .cell-name {
        min-width: 0;
    }

    /* Sized on the wrapper, not the cell: MudBlazor's dense-table rule for
       .mud-table-cell outspecifies a plain .cell-name selector */
    .players-table .cell-name .player-name-cell {
        font-size: 1rem;
    }

    .players-table .cell-pref {
        justify-content: flex-end;
    }

    /* Alternatives and the actions each take a full-width line of their own */
    .players-table .cell-alt,
    .players-table .cell-actions {
        grid-column: 1 / -1;
        justify-content: flex-end;
        flex-wrap: wrap;
        gap: 4px;
    }

    /* A player without alternatives should not get a blank line */
    .players-table .cell-alt:not(:has(.badge-gold)) {
        display: none;
    }

}

/* --- Users table --- */

.users-table .cell-right {
    justify-content: flex-end;
}

/* Same baseline fix as the squad table: MudMenu's plain-div wrapper sits lower
   than the inline-flex icon buttons beside it unless the row is flex. */
.users-table .row-actions {
    display: inline-flex;
    align-items: center;
    gap: 2px;
}

.users-table .row-actions .mud-menu {
    display: inline-flex;
    align-items: center;
}

/* The same 26px small-icon-button box the squad table sizes up — see .players-table .row-actions
   .mud-icon-button above for why MudBlazor lands there and why this selector needs no !important. */
.users-table .row-actions .mud-icon-button {
    width: var(--action-btn-size);
    height: var(--action-btn-size);
    padding: 0;
}

@media (pointer: coarse) {
    .users-table .row-actions {
        gap: 0;
    }
}

/* MudBlazor renders this button, so a scoped rule in MatchResult.razor.css would carry an attribute
   its markup never gets. */
.half-lengths .mud-icon-button {
    width: var(--action-btn-size);
    height: var(--action-btn-size);
    padding: 0;
}

/* 599.98px is MudBlazor's xs breakpoint, where it stacks a table into per-row cards. Its
   default stack prints a column label beside every cell, which here costs three lines and
   ~150px per account to say what a bold name, a muted login and a role badge already say.
   Lay the row out as a grid instead: name + role on the first line, login on the second,
   the actions hugging the right edge. */
@media (max-width: 599.98px) {
    /* Column two is sized by its content but never past a share of the card. An `auto` track took
       whatever the role badge asked for — 120px for ApplicationAdmin — and the 1fr track beside it
       gave up all of its width to pay for that, which left the name and the "You" badge rendering
       over the role. */
    .users-table .mud-table-body .mud-table-row {
        display: grid;
        grid-template-columns: minmax(0, 1fr) fit-content(30%) auto;
        align-items: center;
        column-gap: 8px;
        row-gap: 4px;
        padding: 10px 14px;
    }

    /* The "You" badge stays whole and the name is the half that shortens — but only once the cap
       above has taken back everything the role column was holding. */
    .users-table .cell-name {
        min-width: 0;
    }

    .users-table .cell-name-text {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .users-table .cell-name .badge-teal {
        flex: none;
    }

    /* The login goes under the name, leaving the role and actions on the first line */
    .users-table .cell-user {
        grid-column: 1;
        grid-row: 2;
        min-width: 0;
    }

    .users-table .cell-role {
        grid-column: 2;
        grid-row: 1;
        justify-content: flex-end;
        min-width: 0;
    }

    /* Under the role badge it belongs with */
    .users-table .cell-team {
        grid-column: 2;
        grid-row: 2;
        justify-content: flex-end;
        min-width: 0;
    }

    /* Both run longer than the cap allows — "Applicatiebeheerder", a club-and-team name — and a cap
       holds only while nothing under it insists on its own width. Without min-width: 0 on the cells
       above, the badge's own minimum is the track's minimum and fit-content has nothing to cap. */
    .users-table .cell-role span,
    .users-table .cell-team span {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* Spans both rows at the right edge, so a card is two lines tall rather than three */
    .users-table .cell-actions {
        grid-column: 3;
        grid-row: 1 / span 2;
        justify-content: flex-end;
    }
}

@media (max-width: 700px) {
    /* Logged-in admin name costs room the nav needs on a phone */
    .admin-name {
        display: none !important;
    }
}

/* The desktop period tab strip is used purely as a selector — the lineup editor
   is rendered once below it — so its (empty) panel body is collapsed away. */
.builder-desktop-tabs .mud-tabs-panels {
    display: none;
}

/* --- Playing time overview (formation builder) --- */

/* Sorting goes on a phone: MudBlazor hides the header row and offers a 60px "Sort by" select above
   the cards instead, which is the only sort control there is. 600px, not the 599.98px the layout
   below uses — this one tracks MudBlazor's own reveal query, and at exactly 600 that select is
   showing. The table keeps the descending-total order it starts in. */
@media (max-width: 600px) {
    .playtime-table .mud-table-smalldevices-sortselect {
        display: none !important;
    }
}

/* Same 599.98px xs breakpoint MudBlazor stacks tables at. The default stack puts
   every cell on its own line, which turns a player's per-period positions into a
   tall vertical strip. Lay each row out as a card instead: name on top, the period
   positions in one horizontal band, and the percentage bar on its own line. */
@media (max-width: 599.98px) {
    .playtime-table .mud-table-body .mud-table-row {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        column-gap: 6px;
        row-gap: 8px;
        padding: 10px 14px;
    }

    /* Sorting goes on a phone. MudBlazor hides the header row at this width and puts a 60px "Sort
       by" select above the cards in its place — the only sort control there is, so this is the rule
       that removes it. The table keeps the descending-total order it starts in. */
    .playtime-table .mud-table-smalldevices-sortselect {
        display: none !important;
    }

    .playtime-table .mud-table-body .mud-table-cell {
        border: none !important;
        padding: 0 !important;
        display: flex;
        align-items: center;
        width: auto;
        justify-content: flex-start;
    }

    /* The shirt-number column is noise on a phone */
    .playtime-table .pt-num {
        display: none !important;
    }

    /* Player name owns the first line */
    .playtime-table .pt-name {
        flex: 1 1 100%;
        font-weight: 600;
    }

    /* Each part-of-game position sits beside the next rather than stacking */
    .playtime-table .pt-period {
        flex: 0 0 auto;
        justify-content: center;
    }

    /* Total minutes trail the positions, pinned to the right of that line */
    .playtime-table .pt-total {
        flex: 0 0 auto;
        margin-left: auto;
    }

    /* The percentage bar spans the full width on its own line */
    .playtime-table .pt-percent {
        flex: 1 1 100%;
    }
}

/* --- Position development grid (/stats/positions) ---
   Players x positions: as wide a table as the app draws. Desktop keeps every column, dash and
   all, so the shape of a season is visible at a glance; the mobile card below drops the dashes
   instead of stacking a wall of them. */
.position-dev-table .pd-num,
.position-dev-table .pd-pos {
    text-align: center;
}

/* An anchor to that player's statistics, so it brings a colour and an underline to override. The
   44px floor is what makes it a target a thumb can hit rather than a word-sized one. */
.position-dev-table .pd-name-cell {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    min-height: 44px;
    color: inherit;
    text-decoration: none;
}

.position-dev-table .pd-pos-value {
    font-weight: 600;
    color: var(--ink);
}

.position-dev-table .pd-pos-pct {
    display: block;
    font-size: 0.7rem;
    font-weight: 400;
    color: var(--ink-subtle);
}

.position-dev-table .pd-pos-dash {
    color: var(--ink-faint);
}

/* The column header names the position on the desktop grid, so the chip's own label would only
   repeat it. It comes back in the stacked block below, where there is no header to read. */
.position-dev-table .pd-pos-label {
    display: none;
}

/* Same 599.98px xs breakpoint MudBlazor stacks tables at, and the same card recipe as
   .playtime-table above: name on its own line, then one compact chip per column. The difference
   is which columns show up — a position this player never played drops off their card instead of
   padding it out with a dash, since on a phone the dashes outnumber the data. */
@media (max-width: 599.98px) {
    .position-dev-table .mud-table-body .mud-table-row {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        column-gap: 10px;
        row-gap: 8px;
        padding: 10px 14px;
    }

    /* No border/padding reset here: .stacked-table is on this table too and already clears both. */
    .position-dev-table .mud-table-body .mud-table-cell {
        display: flex;
        align-items: center;
        width: auto;
        justify-content: flex-start;
    }

    .position-dev-table .pd-num {
        display: none !important;
    }

    .position-dev-table .pd-name {
        flex: 1 1 100%;
        font-weight: 600;
    }

    .position-dev-table .pd-pos {
        flex: 0 0 auto;
        justify-content: center;
        background: color-mix(in srgb, var(--ink) 5%, transparent);
        border-radius: 6px;
        padding: 4px 8px !important;
    }

    .position-dev-table .pd-pos-empty {
        display: none !important;
    }

    /* Which position this chip is. Without it the card is a row of bare "30' 50%" figures with
       nothing naming them — the header is gone and .stacked-table drops the DataLabel. */
    .position-dev-table .pd-pos-label {
        display: block;
        font-size: 0.65rem;
        font-weight: 700;
        letter-spacing: 0.3px;
        color: var(--club-primary-bright);
    }

    /* Label above, figure below — the chip stays narrow enough that several fit on a line. */
    .position-dev-table .pd-pos-value {
        display: flex;
        flex-direction: column;
        align-items: center;
        line-height: 1.3;
    }
}

/* --- Reconnect overlay ---
   Blazor's stock overlay is light and reads as "the app went white" when a phone
   suspends the tab. Keep it on-theme, and pwa.js reloads once reconnecting fails. */
#components-reconnect-modal {
    display: none;
}

#components-reconnect-modal.components-reconnect-show,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-rejected {
    display: flex;
    position: fixed;
    inset: 0;
    z-index: 2000;
    align-items: center;
    justify-content: center;
    background: color-mix(in srgb, var(--surface-page) 92%, transparent);
    color: color-mix(in srgb, var(--ink) 85%, transparent);
    font-size: 0.95rem;
    text-align: center;
    padding: 24px;
}

/* --- Back arrow ---
   An anchor rather than a MudIconButton, because it renders on pages with no circuit to dispatch a
   click to. These rules are what gives it the icon button's look; the element MudIcon renders is
   out of reach of a page's scoped stylesheet, which is why this lives here. */
.back-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    color: var(--ink-subtle);
    text-decoration: none;
}

.back-button:hover {
    background: color-mix(in srgb, var(--ink) 6%, transparent);
}

/* --- Install banner ---
   Rendered hidden by the server and unhidden by js/pwa.js, which owns every input to the decision.
   Not MudPaper: this is in the layout, which renders statically. */
.install-banner {
    position: fixed;
    bottom: calc(12px + env(safe-area-inset-bottom, 0px));
    left: 12px;
    right: 12px;
    z-index: 1390;
    max-width: 480px;
    margin: 0 auto;
    padding: 12px;
    border-radius: 12px;
    background: var(--surface-card);
    box-shadow: 0 8px 24px color-mix(in srgb, var(--ink) 24%, transparent);
}

.install-banner[hidden],
.install-banner-actions[hidden] {
    display: none;
}

/* 44px hit area around a small cross: it reads as a corner dismiss, but a thumb still finds it. */
.install-banner-close {
    position: absolute;
    top: 2px;
    right: 2px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border: none;
    border-radius: 50%;
    background: none;
    color: var(--ink-subtle);
    cursor: pointer;
}

.install-banner-body {
    display: flex;
    align-items: center;
    gap: 12px;
    padding-right: 44px;
}

.install-banner-actions {
    display: flex;
    justify-content: flex-end;
    margin-top: 12px;
}

.install-banner-install {
    min-height: 44px;
    padding: 0 16px;
    border: none;
    border-radius: 6px;
    background: var(--gradient-primary);
    color: var(--club-on-primary);
    font-size: 0.875rem;
    font-weight: 500;
    cursor: pointer;
}

/* --- Inline notice ---
   The error line of a page that has no circuit, so no MudSnackbarProvider to raise one on. The
   colours are the login form's, which is where this look started. */
.inline-notice {
    border-radius: 8px;
    padding: 12px;
    margin-bottom: 20px;
    font-size: 0.875rem;
}

.inline-notice-error {
    background: color-mix(in srgb, var(--color-danger) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--color-danger) 30%, transparent);
    color: var(--color-danger-bright);
}

.inline-notice-warning {
    background: color-mix(in srgb, var(--color-warning) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--color-warning) 30%, transparent);
    color: var(--color-warning-bright);
}

/* Only a -bright variant exists for success — unlike danger/warning there is no plain tone beside
   it, so the mix below is struck from the same color the text uses. */
.inline-notice-success {
    background: color-mix(in srgb, var(--color-success-bright) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--color-success-bright) 30%, transparent);
    color: var(--color-success-bright);
}

.inline-notice[hidden] {
    display: none;
}

/* Plain <button>s with an inline onclick on /games/{id}/overview, so there is no MudButton layout to
   inherit — .btn-gold only paints. Not .overview-capture: that name belongs to the captured block. */
.overview-btn {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-height: 44px;
    padding: 0 16px;
    border-radius: 6px;
    font-size: 0.875rem;
    cursor: pointer;
}

/* The notification row's two MudBlazor components, whose root elements carry no scope attribute — the same reason .live-action-btn is
   here. See docs/known_issues/css-scoping.md. */
.notify-icon {
    flex-shrink: 0;
    color: var(--club-primary-bright);
}

.notify-button {
    flex-shrink: 0;
    /* Under MudBlazor's own ~36px default, which is below the touch floor. */
    min-height: 44px;
}

/* Narrow enough that the button beside the text would leave neither room to read: it takes its own row. */
@media (max-width: 599.98px), (max-height: 559.98px) {
    .notify-button {
        width: 100%;
    }
}
