/* =============================================================================
   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: 4rem;/* 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);
}

/* The bar stays put. It used to slide away on scroll-down, which left anyone who
   didn't know the scroll-up gesture with no way to move on. */

@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;
    /* 0.75rem is as small as a label people need to read gets. */
    font-size: 0.75rem;
    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 and its Menu
   =============================================================================
   GuestLayout renders under static SSR for anonymous visitors - there is no
   Blazor circuit, so an @onclick would silently do nothing. The Menu is a
   <details> disclosure, which opens and closes with no script; guest-menu.js
   adds closing on an outside tap and on Escape.

   Breakpoints: the four guide links sit in the header row only from 1320px,
   where they fit beside Sign in and Create account; below that they are in the
   Menu. Sign in joins the row from 640px (it is in the Menu on phones).        */

.guest-nav-wide,
.guest-nav-signin {
    display: none !important;
}

@media (min-width: 640px) {
    .guest-nav-signin {
        display: inline-flex !important;
    }
}

@media (min-width: 1320px) {
    .guest-nav-wide {
        display: inline-flex !important;
    }

    .guest-menu {
        display: none;
    }
}

/* A narrow phone fits the logo, Create account and the Menu only if the
   button's icon goes. */
@media (max-width: 419px) {
    .guest-join .mud-icon-root {
        display: none;
    }
}

.guest-menu {
    position: relative;
}

.guest-menu-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    padding: 0.4rem 0.6rem;
    border-radius: 0.6rem;
    color: var(--pastel-dark);
    font-weight: 600;
    cursor: pointer;
    list-style: none;
}

/* <summary> draws a disclosure triangle by default; the icon does that job. */
.guest-menu-btn::-webkit-details-marker {
    display: none;
}

.guest-menu-btn::marker {
    content: '';
}

.guest-menu-btn:hover,
.guest-menu[open] .guest-menu-btn {
    background: var(--pastel-light);
    color: var(--pastel-primary);
}

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

@media (max-width: 479px) {
    .guest-menu-btn__text {
        display: none;
    }
}

.guest-menu-panel {
    position: absolute;
    top: calc(100% + 0.5rem);
    right: 0;
    z-index: 50;
    min-width: 16rem;
    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);
}

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

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


/* =============================================================================
   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 both show. They used to be hidden there as redundant, but
   dragging is not something everyone can do - a keyboard user, or anyone with
   a tremor using a mouse, had no way to change the order at all. WCAG 2.5.7
   asks for a single-pointer alternative to every drag, and the buttons are it. */
.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 (max-width: 1023px) {
    /* Bigger targets at the ring, where the order is changed on a phone in a hurry. */
    .entrant-reorder-btn {
        width: 40px;
        height: 34px;
    }

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

@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;
    }
}



/* ── Entrant rows (SubEventEntrantItem) ────────────────────────────────────
   A rigid flex row that broke on phones. 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) {
    a.evc:hover {
        transform: none;
    }

    a.evc:hover::before {
        opacity: 0;
    }

    .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. */
    a.evc: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%;
}
