/* =============================================================================
   mobile.css — responsive foundation
   =============================================================================

   Loaded LAST in App.razor, after site.css and tailwind-generated.css, so rules
   here win ties on specificity without needing !important. Keep it that way:
   the old responsive sidebar rules leaned on !important and that is precisely
   why the mobile menu button stopped working (a `width: 5rem !important` media
   query beat the Blazor-toggled `w-64`, so the button toggled state that could
   never be seen).

   ── Breakpoints ────────────────────────────────────────────────────────────
   These match Tailwind's defaults exactly, because the markup mixes Tailwind
   `sm:`/`md:`/`lg:` prefixes with hand-written media queries. Two scales that
   disagree by even a pixel produce layouts that break only in a narrow band,
   which is miserable to debug. One scale:

       (max-width:  639px)   phone            below Tailwind `sm`
       (min-width:  640px)   `sm`             large phone / small tablet
       (min-width:  768px)   `md`             tablet
       (min-width: 1024px)   `lg`             desktop — sidebar becomes permanent
       (min-width: 1280px)   `xl`

   The app's structural breakpoint is `lg` (1024px). At and above it the layout
   is the classic desktop shell (persistent sidebar, no bottom bar). Below it
   the layout is the mobile app shell (off-canvas drawer, bottom tab bar).
   Prefer those two states over inventing intermediate ones.

   ── Editing rules ──────────────────────────────────────────────────────────
   - No !important unless you are overriding MudBlazor's own inline styles.
   - Tailwind utilities are generated from markup by an MSBuild target, so a new
     `sm:` class in a .razor file works as soon as you build. You do not need to
     add it here.
   - Component-local <style> blocks render after this file. If a component needs
     to override something here, that is where to do it.
   ========================================================================== */


/* ── Layout tokens ─────────────────────────────────────────────────────────
   Single source of truth for the app shell's fixed dimensions. The drawer, the
   bottom tab bar and the scroll container all have to agree on these; when they
   were literals scattered across files they drifted. */
:root {
    --app-header-height: 4rem;      /* top bar (h-16) */
    --app-sidebar-width: 16rem;     /* expanded sidebar / drawer (w-64) */
    --app-bottomnav-height: 3.75rem;/* mobile bottom tab bar, excl. safe area */

    /* iOS home-indicator / notch insets. env() resolves to 0px everywhere that
       has no inset, so these are safe to add unconditionally. The fallback in
       the second argument matters for older WebKit that knows `env` but not the
       specific keyword. */
    --safe-top: env(safe-area-inset-top, 0px);
    --safe-bottom: env(safe-area-inset-bottom, 0px);
    --safe-left: env(safe-area-inset-left, 0px);
    --safe-right: env(safe-area-inset-right, 0px);

    /* Total space the bottom bar occupies, including the home indicator. Page
       content pads by this so the last row is never trapped under the bar. */
    --app-bottomnav-total: calc(var(--app-bottomnav-height) + var(--safe-bottom));
}

@media (min-width: 1024px) {
    :root {
        /* No bottom bar on desktop — collapse the reserve rather than making
           every consumer write a media query around its padding. */
        --app-bottomnav-total: 0px;
    }
}


/* ── Viewport height ───────────────────────────────────────────────────────
   `100vh` is a lie on mobile: iOS Safari and Chrome Android both report the
   viewport as if the browser chrome were retracted, so a 100vh element is
   taller than what you can actually see and its last ~60-100px sit under the
   toolbar. `100dvh` is the honest value and tracks the chrome as it collapses.

   Declared as a variable with a vh fallback first so browsers without dvh
   (Safari < 15.4) still get something sensible. */
:root {
    --app-viewport-height: 100vh;
}

@supports (height: 100dvh) {
    :root {
        --app-viewport-height: 100dvh;
    }
}


/* ── Touch affordances ─────────────────────────────────────────────────────
   Apply to interactive elements that are otherwise below the 44px that both
   Apple's and Google's guidance ask for. Used as an explicit opt-in class
   rather than a blanket rule, because forcing a min-height on every button in
   the app reflows dense desktop toolbars for no benefit. */
@media (max-width: 1023px) {
    .touch-target {
        min-height: 44px;
        min-width: 44px;
    }

    /* Kill the grey flash Android/iOS paint over tapped elements. The app draws
       its own :active states; the double effect reads as a rendering glitch. */
    a, button, [role="button"], .mud-button-root {
        -webkit-tap-highlight-color: transparent;
    }
}


/* ── Hover is not universal ────────────────────────────────────────────────
   A touch browser fires :hover on tap and then LEAVES IT APPLIED until you tap
   something else. Every `:hover { transform: translateY(-4px) }` in this app
   therefore leaves cards visibly stuck lifted after a tap. Rather than hunt
   every rule, components should nest hover effects inside this query.

   Usage:  @media (hover: hover) and (pointer: fine) { .card:hover { ... } }

   This block exists to document the pattern; the individual fixes live with the
   components they belong to. */


/* =============================================================================
   App shell — AuthenticatedLayout
   =============================================================================

   Two layouts, one markup tree, switched at lg (1024px):

     < lg   MOBILE     off-canvas drawer (full labels) + fixed bottom tab bar,
                       content full-bleed, page scrolls on <body>
     >= lg  DESKTOP    persistent sidebar that collapses to a 5rem icon rail,
                       no bottom bar — i.e. exactly what desktop had before

   Two independent pieces of state, deliberately NOT one shared bool:

     .app-shell--drawer-open      mobile: is the drawer slid in?   default off
     .app-sidebar--collapsed      desktop: is the rail collapsed?  default off

   They mean different things ("visible?" vs "expanded?") and want opposite
   defaults, so a single flag would either open the drawer over the content on
   every mobile page load or open desktop in the collapsed rail. Keeping them
   separate also means resizing a window never lands you in a nonsense state:
   a collapsed desktop rail that becomes a mobile drawer still shows its labels,
   because the collapse rule is scoped inside the lg query.                    */

.app-shell {
    display: flex;
    flex-direction: column;
    min-height: var(--app-viewport-height);
}

.app-body {
    display: flex;
    flex: 1 1 auto;
}

.app-main {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    /* Without min-width:0 a flex child refuses to shrink below its content's
       intrinsic width, so one wide table anywhere in the page silently widens
       the whole column and pushes the layout sideways. This single line is what
       stops most horizontal scroll. */
    min-width: 0;
}

.app-content {
    flex: 1 1 auto;
    min-width: 0;
    /* Clear the fixed bottom bar so the last element on a page is reachable.
       Resolves to 0 at lg where the bar is gone. */
    padding-bottom: var(--app-bottomnav-total);
}


/* ── Sidebar / drawer ───────────────────────────────────────────────────── */

.app-sidebar {
    display: flex;
    flex-direction: column;
    width: var(--app-sidebar-width);
    flex-shrink: 0;
}

@media (max-width: 1023px) {
    /* Off-canvas. Fixed rather than absolute so it is unaffected by page scroll,
       and translated rather than toggled with display:none so it animates and
       stays in the accessibility tree in a predictable place. */
    .app-sidebar {
        position: fixed;
        top: 0;
        bottom: 0;
        left: 0;
        z-index: 60;
        transform: translateX(-100%);
        transition: transform 0.25s ease;
        /* Nav can be taller than a phone once the Admin section is present. */
        overflow-y: auto;
        overscroll-behavior: contain;
        padding-top: var(--safe-top);
        padding-bottom: var(--safe-bottom);
        box-shadow: 0 0 24px rgba(35, 22, 37, 0.18);
    }

    .app-shell--drawer-open .app-sidebar {
        transform: translateX(0);
    }

    /* An off-screen drawer must not be keyboard-reachable — tabbing into an
       invisible menu is a classic screen-reader trap. visibility:hidden removes
       it from the tab order; the delayed transition lets the slide-out finish
       before it disappears. */
    .app-sidebar {
        visibility: hidden;
        transition: transform 0.25s ease, visibility 0s linear 0.25s;
    }

    .app-shell--drawer-open .app-sidebar {
        visibility: visible;
        transition: transform 0.25s ease, visibility 0s linear 0s;
    }
}

@media (min-width: 1024px) {
    /* The shell used to be a fixed-height box with its own scrolling pane, which
       pinned the sidebar to the viewport for free. Now that the document scrolls
       natively (see .app-content), a plain flex child would scroll away with the
       page and the nav would be gone by the second screenful.

       sticky + an explicit height restores the old behaviour. align-self is
       load-bearing: flex items stretch to the row height by default, and an item
       already as tall as its container has nothing to stick within, so sticky
       silently does nothing without it. */
    .app-sidebar {
        position: sticky;
        top: 0;
        align-self: flex-start;
        height: var(--app-viewport-height);
        overflow-y: auto;
        transition: width 0.25s ease;
    }

    /* Desktop rail. Scoped inside the lg query so it can never apply to the
       mobile drawer — that scoping is the actual fix for the dead menu button. */
    .app-sidebar--collapsed {
        width: 5rem;
    }

    .app-sidebar--collapsed .sidebar-label,
    .app-sidebar--collapsed .sidebar-section {
        display: none;
    }
}


/* ── Scrim ─────────────────────────────────────────────────────────────── */

.app-scrim {
    position: fixed;
    inset: 0;
    z-index: 50;
    background: rgba(35, 22, 37, 0.45);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s ease;
    -webkit-backdrop-filter: blur(1px);
    backdrop-filter: blur(1px);
}

.app-shell--drawer-open .app-scrim {
    opacity: 1;
    pointer-events: auto;
}

@media (min-width: 1024px) {
    .app-scrim {
        display: none;
    }
}


/* ── Bottom tab bar ────────────────────────────────────────────────────── */

.app-bottomnav {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 40;
    display: flex;
    align-items: stretch;
    height: var(--app-bottomnav-total);
    padding-bottom: var(--safe-bottom);
    background: rgba(255, 255, 255, 0.94);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    border-top: 1px solid var(--pastel-border);
    box-shadow: 0 -2px 12px rgba(35, 22, 37, 0.06);
    transition: transform 0.25s ease;
}

/* Hide on scroll-down, reveal on scroll-up. Toggled from app-shell.js. The bar
   is worth ~60px of a phone screen; on a long list that space is better spent
   on content, and the gesture to get it back (scroll up a little) is one people
   already make. translateY rather than display so it animates and so the layout
   reserve stays constant. */
.app-shell--nav-hidden .app-bottomnav {
    transform: translateY(100%);
}

@media (min-width: 1024px) {
    .app-bottomnav {
        display: none;
    }
}

.app-bottomnav-item {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.15rem;
    padding: 0.35rem 0.2rem;
    color: var(--pastel-dark);
    text-decoration: none;
    font-size: 0.65rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    background: none;
    border: none;
    /* Long labels must not wrap the row to two lines and shove the icons up. */
    min-width: 0;
    transition: color 0.15s ease;
}

.app-bottomnav-item span:last-child {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.app-bottomnav-item .mud-icon-root,
.app-bottomnav-item .material-icons {
    font-size: 1.35rem;
}

/* NavLink stamps this on the item matching the current route. */
.app-bottomnav-item.active-bottomnav-item {
    color: var(--pastel-primary);
}

/* A tapped tab keeps :hover applied on touch, so the "hovered" tab would be
   whichever you last touched rather than the current page. Gate it. */
@media (hover: hover) and (pointer: fine) {
    .app-bottomnav-item:hover {
        color: var(--pastel-primary);
    }
}


/* ── Header ────────────────────────────────────────────────────────────── */

.app-header {
    /* Sticky rather than fixed: it participates in layout, so nothing needs a
       compensating top margin, and it still stays put while the body scrolls. */
    position: sticky;
    top: 0;
    z-index: 30;
    flex-shrink: 0;
    padding-top: var(--safe-top);
}

/* The page title is the header's whole job — let it take the room and ellipse
   rather than shove the actions off the right edge. */
.app-header-title {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (max-width: 639px) {
    /* Phone: reclaim the horizontal padding the desktop header uses. */
    .app-header {
        padding-left: 0.75rem;
        padding-right: 0.75rem;
    }
}


/* =============================================================================
   Guest header disclosure menu
   =============================================================================
   A checkbox-and-label disclosure rather than a scripted dropdown. GuestLayout
   renders under static SSR for anonymous visitors - there is no Blazor circuit,
   so an @onclick would silently do nothing, which is exactly the failure the
   authenticated sidebar already had.                                          */

.guest-menu {
    position: relative;
}

/* Visually hidden but still focusable, so the control is keyboard-operable:
   Tab reaches the checkbox, Space toggles it. display:none would take it out of
   the tab order and strand keyboard users. */
.guest-menu-toggle {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    margin: 0;
    pointer-events: none;
}

.guest-menu-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.4rem;
    border-radius: 0.6rem;
    color: var(--pastel-dark);
    cursor: pointer;
}

.guest-menu-toggle:focus-visible + .guest-menu-btn {
    outline: 2px solid var(--pastel-primary);
    outline-offset: 2px;
}

.guest-menu-panel {
    position: absolute;
    top: calc(100% + 0.5rem);
    right: 0;
    z-index: 50;
    min-width: 15rem;
    display: flex;
    flex-direction: column;
    padding: 0.4rem;
    background: #fff;
    border: 1px solid var(--pastel-border);
    border-radius: 0.85rem;
    box-shadow: 0 10px 30px rgba(35, 22, 37, 0.14);

    /* Closed. Not display:none, so the open transition has something to animate
       from; visibility keeps the links out of the tab order while hidden. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(-0.35rem);
    transition: opacity 0.16s ease, transform 0.16s ease, visibility 0s linear 0.16s;
}

.guest-menu-toggle:checked ~ .guest-menu-panel {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
    transition: opacity 0.16s ease, transform 0.16s ease, visibility 0s linear 0s;
}

.guest-menu-panel a {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.7rem 0.75rem;
    border-radius: 0.6rem;
    font-size: 0.9rem;
    font-weight: 500;
    color: var(--pastel-text);
    text-decoration: none;
}

.guest-menu-panel a:hover {
    background: var(--pastel-light);
    color: var(--pastel-primary);
}

/* Belt and braces: the panel is inside a `md:hidden` wrapper, but if that class
   is ever lost the panel must not hang over the desktop header. */
@media (min-width: 768px) {
    .guest-menu {
        display: none;
    }
}


/* =============================================================================
   Overflow containment
   =============================================================================
   A safety net, NOT a fix. Every horizontal-scroll bug found in the audit was
   corrected at source (Home's non-wrapping h1, its four `calc(100% + 100px)`
   images, .page-header's non-wrapping flex row, EventView's fixed-size title,
   and the missing min-width:0 on the shell's main column). This block only stops
   the *next* one from making the entire document scroll sideways, which is a
   uniquely bad failure: it shifts every element on the page, not just the guilty
   one, so the report you get is "the whole site is broken on my phone".

   `clip` rather than `hidden` is load-bearing. `overflow: hidden` makes the
   element a scroll container, and the app shell now relies on position:sticky
   for both the header and the desktop sidebar - sticky resolves against its
   nearest scrolling ancestor, so hidden here would break both. `clip` crops
   without creating a scroll container.

   Safari < 16 does not support `clip` and simply ignores this - degrading to
   today's behaviour, which is the correct direction to fail in.               */
html {
    overflow-x: clip;
}

body {
    overflow-x: clip;
    /* Belt and braces for the same reason: a rogue wide child cannot stretch the
       body past the viewport. */
    max-width: 100vw;
}

/* Long unbroken strings - a pasted URL in an event description, a 40-character
   kennel name - are a common overflow source that no layout fix prevents. */
.break-anywhere {
    overflow-wrap: anywhere;
    word-break: break-word;
}


/* =============================================================================
   Touch interactions
   ============================================================================= */

/* ── Dialogs go full-screen on phones ──────────────────────────────────────
   Nine dialogs across the app were opened with MaxWidth.Small and several
   without FullWidth. On a desktop that is a tidy modal; on a 375px screen it is
   a small box with large margins, inside which forms and score grids then have
   to fight for room - while 40% of the screen shows a dimmed backdrop.

   Fixed centrally rather than at the 42 call sites: this is a property of the
   viewport, not of any one dialog, and per-call-site options would drift.
   Individual dialogs can still opt out with .mud-dialog-fullscreen semantics or
   their own class if one ever genuinely needs to stay small.                  */
@media (max-width: 639px) {
    .mud-dialog-container .mud-dialog {
        /* !important is unavoidable here: MudBlazor sets width via its own
           mud-dialog-width-* classes at the same specificity, and these are its
           stylesheet's rules rather than ours. */
        width: 100% !important;
        max-width: 100% !important;
        max-height: 100%;
        margin: 0 !important;
        border-radius: 0;
        /* dvh, not vh - see the viewport height note above. A vh-sized dialog
           puts its action buttons under the iOS toolbar. */
        min-height: var(--app-viewport-height);
        padding-bottom: var(--safe-bottom);
    }

    .mud-dialog-container {
        align-items: stretch;
        justify-content: stretch;
    }

    /* The action row is the whole reason the dialog is open - it must not be
       the thing that scrolls off the bottom. */
    .mud-dialog .mud-dialog-actions {
        position: sticky;
        bottom: 0;
        background: var(--pastel-light, #fff);
        border-top: 1px solid var(--pastel-border);
        margin: 0;
        padding: 0.75rem 1rem calc(0.75rem + var(--safe-bottom));
        z-index: 1;
    }

    /* Wrap rather than squeeze: two or three buttons with icons and labels do
       not fit on one 375px row. */
    .mud-dialog .mud-dialog-actions .btn-container {
        flex-wrap: wrap;
    }
}


/* ── Tab bars scroll instead of paging ─────────────────────────────────────
   MyEvents has four icon+text tabs and MyEntries three. Below ~500px MudBlazor
   falls back to arrow buttons that page the strip - two tiny targets, and no
   indication of how much is off-screen. Native horizontal scrolling is the
   gesture people already use, and a touch device has no need of the arrows. */
@media (max-width: 1023px) {
    .mud-tabs-tabbar-inner {
        overflow-x: auto;
        scrollbar-width: none;              /* Firefox */
        -webkit-overflow-scrolling: touch;
        /* Snap so a flick lands on a tab boundary rather than mid-label. */
        scroll-snap-type: x proximity;
    }

    .mud-tabs-tabbar-inner::-webkit-scrollbar {
        display: none;
    }

    .mud-tabs-tabbar-inner .mud-tab {
        scroll-snap-align: start;
        flex-shrink: 0;
    }

    /* The paging arrows are redundant once the strip scrolls, and they eat
       ~80px of a 375px bar. */
    .mud-tabs-tabbar .mud-tab-slider-pos-horizontal ~ .mud-icon-button,
    .mud-tabs-tabbar > .mud-icon-button {
        display: none;
    }
}


/* ── Touch reordering (running order, class order) ─────────────────────────
   The drag handle next to these is a lie on touch: MudDropContainer reorders
   through the HTML5 drag-and-drop API, whose events never fire for a finger. So
   below lg the buttons are the mechanism and the handle is hidden; at lg and
   above the handle works and the buttons would be redundant clutter. Exactly one
   of the two is visible at any width. */
.entrant-reorder {
    display: flex;
    flex-direction: column;
    flex-shrink: 0;
    gap: 2px;
}

.entrant-reorder-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    /* 28px each, stacked to 58px total - the pair is the touch target, which is
       what keeps a two-button control usable without making the row enormous. */
    width: 32px;
    height: 28px;
    padding: 0;
    border: 1px solid var(--pastel-border);
    border-radius: 0.4rem;
    background: #fff;
    color: var(--pastel-primary);
    cursor: pointer;
    transition: background-color 0.15s ease, opacity 0.15s ease;
}

.entrant-reorder-btn .material-icons {
    font-size: 1.1rem;
}

.entrant-reorder-btn:disabled {
    opacity: 0.3;
    cursor: default;
}

.entrant-reorder-btn:not(:disabled):active {
    background: var(--pastel-light);
}

@media (min-width: 1024px) {
    .entrant-reorder {
        display: none;
    }
}

@media (max-width: 1023px) {
    /* Hide the drag handle wherever the buttons are shown - leaving a grab
       cursor and a drag icon that cannot do anything is worse than no
       affordance at all. Scoped to rows that actually got the buttons, so
       read-only entrant lists elsewhere are untouched. */
    .entrant-reorder ~ .material-icons.cursor-move {
        display: none;
    }
}


/* ── Class rows (SubEventListItem) ─────────────────────────────────────────
   The owner's view of a class carries three labelled actions - Add Free Entry,
   Edit Class, View Class - beside the title, status pill and reorder buttons,
   in a rigid `flex justify-between` row. That needs roughly 700px. On a phone
   the result was not merely tight, it was actively broken: `.btn-sm` sets
   `min-width: 80px` with no white-space rule, so the buttons refused to shrink
   past 80px but happily broke their own labels into three stacked lines
   ("Add / Free / Entry"), grew tall, AND still overflowed the right edge with
   "View Class" cut off the screen.

   Fixed by changing what wraps. The card becomes a stack, and the actions wrap
   as whole buttons onto as many rows as they need - so a label never breaks and
   nothing leaves the card.                                                    */
@media (max-width: 1023px) {
    .subevent-card {
        flex-direction: column;
        align-items: stretch;
        gap: 0.75rem;
    }

    /* Title, status pill and the Overspill badge were competing for whatever
       width the reorder buttons left. Let them wrap instead of squeezing the
       title to three lines. */
    .subevent-heading {
        flex-wrap: wrap;
    }

    /* ml-3 is fine when the pill trails the title on one line, and reads as a
       stray indent once it wraps to its own. */
    .subevent-status {
        margin-left: 0;
    }

    .subevent-actions {
        width: 100%;
        flex-wrap: wrap;
    }

    /* grow so each row of buttons fills the width evenly rather than leaving a
       ragged edge; nowrap is the load-bearing half - it stops a button solving
       its width problem by breaking its label. min-width overrides .btn-sm's
       80px floor so shrinking is bounded by the label, not an arbitrary number. */
    .subevent-actions .btn {
        flex: 1 1 auto;
        min-width: 0;
        white-space: nowrap;
    }
}

/* Deliberately NO forced one-button-per-row rule at very narrow widths. The
   arithmetic says the three owner actions ("Add Free Entry" ~157px, "Edit Class"
   and "View Class" ~127px each) already settle into two rows everywhere from
   320px up, and forcing full-width made it three - taller for no benefit.

   The safety property does not depend on those estimates being exact: nowrap
   plus wrap means a button either fits on the current row or moves to the next
   one whole. It can never clip or break its label at any width. */


/* ── Entrant rows (SubEventEntrantItem) ────────────────────────────────────
   Same rigid shape as the class rows above, and the same failure. Once an
   entrant has music, is scoreable, and the organiser's two status checkboxes
   are shown, the trailing controls are a music chip + a Score button + a
   checkbox pair, all fighting the name block in one non-wrapping row.

   Note the flex-direction stays row here rather than becoming a stack: the
   entrant list is long and read top-to-bottom by running order, so making every
   row twice as tall costs more than it gains. Instead the trailing controls
   become one wrapping group that drops below the name only when it must.     */
@media (max-width: 1023px) {
    .entrant-card {
        flex-wrap: wrap;
        /* align-items:center leaves the controls floating mid-row once they wrap
           onto their own line. */
        align-items: flex-start;
        row-gap: 0.6rem;
    }

    /* The name block takes the first row outright, which pushes the controls to
       the second - predictable, rather than depending on how long a dog's name
       happens to be. */
    .entrant-card > .flex-1 {
        flex-basis: 100%;
    }

    .entrant-control {
        flex-shrink: 0;
    }

    /* Checkboxes side by side rather than stacked: on their own row there is
       width to spare, and two stacked MudCheckBoxes make the row very tall. */
    .entrant-card .entrant-control.flex-col {
        flex-direction: row;
        flex-wrap: wrap;
        gap: 0.25rem 0.75rem;
    }
}

/* Very narrow: the music chip's title is already ellipsed at 120px, so let the
   chip take the full width rather than squeezing the Score button. */
@media (max-width: 400px) {
    .entrant-card .entrant-music-group {
        max-width: 100%;
    }
}


/* ── Sticky hover ──────────────────────────────────────────────────────────
   A touch browser fires :hover on tap and leaves it applied until you tap
   something else, so a tapped event card stays visibly lifted and "hovered"
   while you read the page - it looks selected, or broken.

   These are the app's transform-on-hover surfaces. Each is neutralised on
   pointers that cannot truly hover; the rules themselves stay where they are so
   desktop is untouched. */
@media (hover: none), (pointer: coarse) {
    .modern-event-card:hover {
        transform: translateY(-0.5px);      /* the resting value */
        box-shadow: 0 4px 12px rgba(35, 22, 37, 0.06), 0 1px 3px rgba(35, 22, 37, 0.03);
    }

    .dog-avatar:hover {
        transform: none;
    }

    .hover\:scale-105:hover {
        transform: none;
    }

    /* Give a tap some immediate feedback in place of the hover it no longer
       gets - without it the card feels unresponsive on touch. */
    .modern-event-card:active {
        transform: scale(0.995);
    }
}


/* ── Body scroll lock ──────────────────────────────────────────────────── */

/* Applied to <body> by app-shell.js while the drawer is open. Without it the
   page behind the scrim scrolls under your finger, which reads as the drawer
   itself being broken. */
body.app-scroll-locked {
    overflow: hidden;
    /* iOS ignores overflow:hidden on body in some cases; pinning the width
       stops the reflow-jump when the scrollbar disappears on desktop. */
    width: 100%;
}
