/* =============================================================================
   SlotManager Venue App — design system

   Designed for a specific person: a counter clerk at an Indian sports venue,
   standing, often one-handed on a 6" phone, sometimes in daylight, under time
   pressure with a customer waiting. Every choice below follows from that.

     - Touch targets are 44px minimum (§12 accessibility), 48px for primary actions.
     - Contrast is pushed past AA on body text because the screen may be in sun.
     - Slot states are distinguishable at a glance and NOT by hue alone — each
       carries a distinct fill, border and label, since colour-blind staff exist
       and glare flattens colour.
     - Motion is minimal and fast (<=150ms). The competitor research found an
       operator abandoning Playo mid-transaction because it would not load;
       nothing here should ever feel like it is thinking.
     - Indian conventions throughout: Rupees, DD MMM YYYY, 12-hour time.

   No CSS framework. Bootstrap was removed: it supplied the generic template
   look, ~22KB compressed, and none of the above.
   ========================================================================== */

/* ---------- Tokens ------------------------------------------------------- */

:root {
    /* Neutrals — slightly warm; pure grey looks clinical under venue lighting */
    --n-0:   #ffffff;
    --n-25:  #fbfbfa;
    --n-50:  #f5f5f4;
    --n-100: #ebebe9;
    --n-200: #dcdcd8;
    --n-300: #c2c2bd;
    --n-400: #9a9a93;
    --n-500: #77776f;
    --n-600: #5a5a53;
    --n-700: #43433e;
    --n-800: #2b2b28;
    --n-900: #1a1a18;
    --n-950: #0f0f0e;

    /* Primary — deep pitch green. Sports, without the cliché of bright turf. */
    --p-50:  #e8f5ee;
    --p-100: #c7e7d6;
    --p-300: #6cbf94;
    --p-500: #1f8f5f;
    --p-600: #17784f;
    --p-700: #11603f;
    --p-800: #0d4a31;

    /* Semantic */
    --ok-50:   #e7f6ec;  --ok-500:   #1c8a45;  --ok-700:   #11602f;
    --warn-50: #fdf4e3;  --warn-500: #b3771a;  --warn-700: #82550f;
    --dang-50: #fdecec;  --dang-500: #c1352f;  --dang-700: #8d2521;
    --info-50: #e9f1fd;  --info-500: #2a63c4;  --info-700: #1d4489;

    /* Separate TEXT tokens for semantic colours. The -700 shades are tuned for
       dark-on-light; reusing them in dark mode puts dark text on a dark fill and
       the slot-state badges become unreadable — which is a safety problem, not a
       cosmetic one, since those badges are how staff tell paid from unpaid. */
    --ok-text:   var(--ok-700);
    --warn-text: var(--warn-700);
    --dang-text: var(--dang-700);
    --info-text: var(--info-700);

    --bg:         var(--n-50);
    --bg-raised:  var(--n-0);
    --bg-sunken:  var(--n-100);
    --text:       var(--n-900);
    --text-soft:  var(--n-600);
    --text-faint: var(--n-500);
    --border:     var(--n-200);
    --border-str: var(--n-300);

    /* Spacing — 4px base */
    --s-1: .25rem;  --s-2: .5rem;   --s-3: .75rem;  --s-4: 1rem;
    --s-5: 1.25rem; --s-6: 1.5rem;  --s-8: 2rem;    --s-10: 2.5rem; --s-12: 3rem;

    /* Type */
    --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --t-xs: .75rem;   --t-sm: .8125rem; --t-md: .9375rem; --t-lg: 1.0625rem;
    --t-xl: 1.375rem; --t-2xl: 1.75rem;

    --r-sm: 6px; --r-md: 10px; --r-lg: 14px; --r-full: 999px;

    --shadow-1: 0 1px 2px rgb(15 15 14 / .08);
    --shadow-2: 0 1px 3px rgb(15 15 14 / .10), 0 4px 12px rgb(15 15 14 / .06);
    --shadow-3: 0 2px 6px rgb(15 15 14 / .12), 0 12px 32px rgb(15 15 14 / .10);

    --tap: 44px;
    --tap-lg: 48px;
    --ease: cubic-bezier(.2, 0, .2, 1);

    /* ---- Shell metrics -------------------------------------------------
       The chrome is fixed and the content scrolls inside it, so these are
       real layout constants rather than padding guesses. Changing one here
       moves the whole shell consistently. */
    --header-h: 52px;
    --rail-w: 68px;        /* icon rail, tablet and desktop */
    --rail-open-w: 232px;  /* expanded sidebar, wide desktop */
    --bar-h: 56px;         /* bottom tab bar, phones */

    /* ---- Density -------------------------------------------------------
       Overridden under `pointer: fine` further down. A counter tablet needs
       44px targets; a desktop with a mouse can show ~30% more rows on the
       same screen, and on an operations product that is more useful data
       per glance rather than a cosmetic preference. */
    --control-h: var(--tap-lg);
    --row-h: 52px;
    --gutter: var(--s-4);

    /* ---- Motion --------------------------------------------------------
       Two speeds only. Anything slower reads as the app thinking, and the
       competitor research found an operator abandoning Playo mid-transaction
       because it would not load. */
    --fast: 110ms;
    --med: 180ms;

    /* ---- Layering ------------------------------------------------------
       Named, because "z-index: 9999" is how a dialog ends up under a toast. */
    --z-sticky: 10;
    --z-chrome: 20;
    --z-overlay: 30;
    --z-sheet: 40;
    --z-palette: 50;
    --z-toast: 60;

    --ring: 0 0 0 2px var(--bg-raised), 0 0 0 4px var(--p-500);
}

/* A mouse is a precision instrument and a finger is not. Everything below is
   the same layout at a tighter rhythm — no separate "compact mode" switch for
   someone to find, because the input device already said which one is right. */
@media (pointer: fine) {
    :root {
        --control-h: 36px;
        --row-h: 40px;
        --gutter: var(--s-3);
    }
}

/* Dark mode. Counters are sometimes dim and staff work late, so this is not
   decoration. Slot-state colours are re-tuned rather than inverted — a naive
   inversion destroys the at-a-glance distinctions they exist for. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg:         var(--n-950);
        --bg-raised:  #171715;
        --bg-sunken:  #0a0a09;
        --text:       var(--n-50);
        --text-soft:  var(--n-300);
        --text-faint: var(--n-400);
        --border:     #2e2e2a;
        --border-str: #3f3f3a;

        --p-50:    #0d2a1d;
        --p-100:   #12402c;
        --ok-50:   #0f2a1a;
        --warn-50: #2e2311;
        --dang-50: #2e1614;
        --info-50: #131f33;

        /* Light-on-dark equivalents, checked for contrast against the -50 fills above. */
        --ok-text:   #74d193;
        --warn-text: #e6b45c;
        --dang-text: #f19189;
        --info-text: #8fb5f0;
        --p-600:     #4cb383;
        --p-500:     #5cc08f;

        --shadow-1: 0 1px 2px rgb(0 0 0 / .5);
        --shadow-2: 0 1px 3px rgb(0 0 0 / .6), 0 4px 12px rgb(0 0 0 / .4);
        --shadow-3: 0 2px 6px rgb(0 0 0 / .7), 0 12px 32px rgb(0 0 0 / .5);
    }
}

/* ---------- Base --------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

/* The document does NOT scroll. The shell is a fixed frame and each region
   scrolls inside it — which is the single biggest difference between something
   that feels like an application and something that feels like a web page.
   It also means the header, the nav and the day's toolbar never slide away
   mid-task, and a phone's URL bar cannot expand and collapse under a thumb
   that is trying to hit a slot. */
html, body {
    margin: 0;
    padding: 0;
    height: 100%;
    overflow: hidden;
    /* FR-11.14: kill native pull-to-refresh so the custom sync gesture is
       unambiguous and an accidental swipe never reloads mid-shift. */
    overscroll-behavior: none;
}

body {
    font-family: var(--font);
    font-size: var(--t-md);
    line-height: 1.5;
    color: var(--text);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
    -webkit-tap-highlight-color: transparent;
}

h1, h2, h3 { margin: 0; font-weight: 650; letter-spacing: -.015em; line-height: 1.2; }
h1 { font-size: var(--t-2xl); }
h2 { font-size: var(--t-xl); }
h3 { font-size: var(--t-lg); }
p  { margin: 0; }

a { color: var(--p-600); text-decoration: none; }
a:hover { text-decoration: underline; }

/* One focus style everywhere — the desktop layout (FR-11.6) is keyboard-driven. */
:focus-visible {
    outline: 2px solid var(--p-500);
    outline-offset: 2px;
    border-radius: var(--r-sm);
}

::selection { background: var(--p-100); color: var(--p-800); }

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important;
        transition-duration: .01ms !important;
        /* Without this, an `infinite` animation keeps looping at .01ms — a blur, which is worse
           for a motion-sensitive user than the animation it was meant to suppress. */
        animation-iteration-count: 1 !important;
    }
}

/* ---------- App shell ---------------------------------------------------- */

/* Three regions on a fixed frame: header across the top, navigation down the
   left (or across the bottom on a phone), content in the remaining box. Only
   the content box scrolls.

        phone                tablet / desktop
   ┌──────────────┐      ┌────┬──────────────┐
   │   header     │      │    │   header     │
   ├──────────────┤      │nav ├──────────────┤
   │              │      │    │              │
   │   content ↕  │      │    │   content ↕  │
   ├──────────────┤      │    │              │
   │   tab bar    │      └────┴──────────────┘
   └──────────────┘                              */
/* minmax(0, 1fr) rather than 1fr, everywhere. A bare `1fr` is min-content-sized,
   so the booking grid — a table twelve courts wide — stretches its column past
   the viewport and takes the whole shell with it: the bottom tab bar ends up
   501px wide on a 390px phone and the entire app scrolls sideways. This is the
   single most common way a CSS grid shell springs a horizontal leak. */
.app {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto 1fr auto;
    grid-template-areas: "header" "main" "nav";
    height: 100dvh;
    overflow: hidden;
}

@media (min-width: 900px) {
    .app {
        grid-template-columns: var(--rail-w) minmax(0, 1fr);
        grid-template-rows: auto 1fr;
        grid-template-areas: "nav header" "nav main";
    }
}

@media (min-width: 1280px) {
    .app { grid-template-columns: var(--rail-open-w) minmax(0, 1fr); }
}

.app-header {
    grid-area: header;
    z-index: var(--z-chrome);
    display: flex;
    align-items: center;
    gap: var(--s-3);
    height: calc(var(--header-h) + env(safe-area-inset-top));
    padding: 0 var(--s-3);
    padding-top: env(safe-area-inset-top);
    background: var(--bg-raised);
    border-bottom: 1px solid var(--border);
    /* Chrome is not content: dragging a toolbar should never select its text,
       and on a touch device a long-press there should not raise a caret. */
    user-select: none;
    -webkit-user-select: none;
}

.app-brand {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    /* It is a link to the dashboard, so it is a touch target and must meet the 44px minimum
       (FR-11.6) — the icon and text alone came to 28px on a Pixel 5. */
    min-height: var(--tap);
    font-weight: 680;
    letter-spacing: -.02em;
    color: var(--text);
}

.app-brand:hover { text-decoration: none; }

.mark {
    display: grid;
    place-items: center;
    width: 28px; height: 28px;
    border-radius: 8px;
    background: var(--p-600);
    color: #fff;
    font-size: var(--t-sm);
    font-weight: 700;
}

.grow { flex: 1; }

/* The ONLY scrolling region. `min-height: 0` is load-bearing: without it a grid
   child refuses to shrink below its content and the whole shell grows a page
   scrollbar, which is precisely the website behaviour being designed out. */
.app-main {
    grid-area: main;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-gutter: stable;
}

/* Content gets a measured column rather than the full width of a 27" monitor —
   a booking list stretched to 2000px is unreadable. Pages that genuinely want
   the whole width (the grid) opt out with .page-wide. */
.page {
    width: 100%;
    max-width: 1200px;
    margin: 0 auto;
    padding: var(--s-5) var(--gutter) var(--s-10);
}

.page-wide { max-width: none; padding-left: var(--s-3); padding-right: var(--s-3); }

/* ---------- Navigation ---------------------------------------------------
   Phone: a bottom tab bar, because that is where the thumb already is on a 6"
   phone held one-handed. Tablet and desktop: a rail down the left, which keeps
   a persistent sense of place and leaves the header free for the CURRENT
   context rather than global links.                                          */

.app-nav {
    grid-area: nav;
    z-index: var(--z-chrome);
    display: flex;
    gap: var(--s-1);
    padding: var(--s-1) var(--s-2);
    padding-bottom: max(var(--s-1), env(safe-area-inset-bottom));
    background: var(--bg-raised);
    border-top: 1px solid var(--border);
    user-select: none;
    -webkit-user-select: none;
}

.nav-item {
    flex: 1;
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    min-height: var(--bar-h);
    padding: var(--s-1);
    border-radius: var(--r-md);
    color: var(--text-faint);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .01em;
    transition: color var(--fast) var(--ease), background var(--fast) var(--ease);
}

.nav-item:hover { text-decoration: none; color: var(--text); }
.nav-item.active { color: var(--p-600); }
.nav-item .ico { font-size: 20px; line-height: 1; }

/* The active marker is a shape, not just a colour — the same rule the slot
   badges follow, and for the same reason: glare flattens hue. */
.nav-item.active::after {
    content: "";
    position: absolute;
    top: 4px;
    width: 22px; height: 3px;
    border-radius: var(--r-full);
    background: currentColor;
}

@media (min-width: 900px) {
    .app-nav {
        flex-direction: column;
        gap: var(--s-1);
        padding: var(--s-3) var(--s-2);
        border-top: 0;
        border-right: 1px solid var(--border);
        background: var(--bg-sunken);
    }

    .nav-brand {
        display: flex;
        align-items: center;
        gap: var(--s-2);
        padding: var(--s-2);
        margin-bottom: var(--s-2);
        color: var(--text);
        font-weight: 680;
        letter-spacing: -.02em;
        white-space: nowrap;
        overflow: hidden;
    }

    .nav-brand:hover { text-decoration: none; }

    .nav-item {
        flex: 0 0 auto;
        flex-direction: column;
        min-height: 52px;
        font-size: 10px;
    }

    .nav-item.active { background: var(--p-50); }
    .nav-item.active::after { display: none; }
}

/* Wide desktop: the rail opens into a labelled sidebar. Same targets, same
   order — only the label moves beside the icon, so muscle memory survives a
   window resize. */
@media (min-width: 1280px) {
    .nav-item {
        flex-direction: row;
        justify-content: flex-start;
        gap: var(--s-3);
        min-height: 40px;
        padding: 0 var(--s-3);
        font-size: var(--t-sm);
        font-weight: 600;
    }

    .nav-item .ico { font-size: 17px; width: 20px; text-align: center; }
    .nav-spacer { flex: 1; }
}

/* The brand belongs in the rail on desktop and in the header on a phone, so
   each shows only where it is not duplicated. */
.nav-brand { display: none; }
@media (min-width: 900px) { .nav-brand { display: flex; } .app-header .app-brand { display: none; } }
@media (min-width: 900px) and (max-width: 1279px) { .nav-brand span:last-child { display: none; } }

/* The sync pill keeps its dot on a phone but drops the word — the header is the
   scarcest space in the app and the colour already carries the state. */
@media (max-width: 559px) {
    .sync .sync-text { display: none; }
    .sync { padding: 0 var(--s-2); }
}

/* ---------- Buttons ------------------------------------------------------ */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s-2);
    min-height: var(--control-h);
    padding: 0 var(--s-4);
    border: 1px solid transparent;
    border-radius: var(--r-md);
    background: var(--bg-raised);
    color: var(--text);
    font: inherit;
    font-size: var(--t-sm);
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    user-select: none;
    -webkit-user-select: none;
    transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease),
                box-shadow var(--fast) var(--ease);
}

.btn:focus-visible { outline: none; box-shadow: var(--ring); }
.btn:active:not(:disabled) { transform: translateY(.5px); }
.btn:disabled { opacity: .5; cursor: not-allowed; transform: none; }

/* A 1px inset highlight is what separates a "raised" control from a coloured
   rectangle. It is subtle on purpose — visible on an OLED tablet in a dim
   venue, invisible enough not to look decorated in daylight. */
.btn-primary {
    background: var(--p-600);
    border-color: var(--p-700);
    color: #fff;
    box-shadow: inset 0 1px 0 rgb(255 255 255 / .12), var(--shadow-1);
}

.btn-primary:hover:not(:disabled) { background: var(--p-700); }

.btn-secondary { background: var(--bg-raised); border-color: var(--border-str); box-shadow: var(--shadow-1); }
.btn-secondary:hover:not(:disabled) { background: var(--bg-sunken); }

.btn-ghost { background: transparent; color: var(--text-soft); }
.btn-ghost:hover:not(:disabled) { background: var(--bg-sunken); color: var(--text); }

/* Destructive actions are never the primary colour: the eye should not land on
   "Retire this court" the way it lands on "Save". */
.btn-danger { background: transparent; border-color: var(--dang-500); color: var(--dang-text); }
.btn-danger:hover:not(:disabled) { background: var(--dang-50); }

.btn-lg { min-height: var(--tap-lg); padding: 0 var(--s-6); font-size: var(--t-md); }
.btn-sm { min-height: 30px; padding: 0 var(--s-3); font-size: var(--t-xs); }
.btn-block { width: 100%; }

/* Square, for a lone glyph. Without this an icon button is a wide slab with a
   symbol lost in the middle of it. */
.btn-icon { padding: 0; width: var(--control-h); min-width: var(--control-h); }

/* A spinner that replaces the label would change the button's width and shift
   whatever is beside it, so the label stays and dims instead. */
.btn[data-busy="true"] { position: relative; color: transparent !important; pointer-events: none; }

.btn[data-busy="true"]::after {
    content: "";
    position: absolute;
    width: 14px; height: 14px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    color: #fff;
    animation: spin .6s linear infinite;
}

.btn-secondary[data-busy="true"]::after,
.btn-ghost[data-busy="true"]::after { color: var(--text-soft); }

@keyframes spin { to { transform: rotate(360deg); } }

/* A button in a column flex container stretches to the container's width by default, which turns
   every form's submit into a full-card slab. Opt in to that with .btn-block instead. */
.stack > .btn:not(.btn-block), .stack-sm > .btn:not(.btn-block) { align-self: flex-start; }

/* ---------- Forms -------------------------------------------------------- */

.field { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
.field label { font-size: var(--t-sm); font-weight: 600; color: var(--text-soft); }
.field-hint { font-size: var(--t-xs); color: var(--text-faint); }
.field-error { font-size: var(--t-xs); color: var(--dang-text); font-weight: 600; }

/* "required" reads as a warning; the point is only that the field cannot be
   left empty, so it is a quiet marker rather than a red shout. */
.req {
    margin-left: var(--s-1);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--text-faint);
}

.input {
    width: 100%;
    min-height: var(--control-h);
    padding: 0 var(--s-3);
    border: 1px solid var(--border-str);
    border-radius: var(--r-md);
    background: var(--bg-raised);
    color: var(--text);
    font: inherit;
    /* Must be >=16px: anything smaller makes iOS Safari zoom on focus, which
       throws the layout out mid-booking on a counter phone. Desktop, where no
       such zoom exists, takes the tighter size via pointer:fine below. */
    font-size: 1rem;
    transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}

@media (pointer: fine) { .input { font-size: var(--t-sm); } }

textarea.input { padding: var(--s-3); line-height: 1.5; resize: vertical; }

.input::placeholder { color: var(--text-faint); }
.input:focus { outline: none; border-color: var(--p-500); box-shadow: 0 0 0 3px var(--p-50); }
.input:disabled { background: var(--bg-sunken); color: var(--text-faint); cursor: not-allowed; }
.input[aria-invalid="true"] { border-color: var(--dang-500); }
.input[aria-invalid="true"]:focus { box-shadow: 0 0 0 3px var(--dang-50); }

/* Native selects render a system arrow that ignores the theme, so it is drawn
   here instead — otherwise a dark-mode select has a black arrow on black. */
select.input {
    appearance: none;
    padding-right: var(--s-8);
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: right var(--s-4) center, right calc(var(--s-4) - 5px) center;
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
}

.check {
    display: flex;
    align-items: flex-start;
    gap: var(--s-3);
    min-height: var(--tap);
    padding: var(--s-2) 0;
    cursor: pointer;
}

.check input[type="checkbox"] { width: 18px; height: 18px; margin-top: 2px; accent-color: var(--p-600); flex: none; }

/* ---------- Surfaces ----------------------------------------------------- */

.card {
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-1);
}

.card-pad { padding: var(--s-5); }

.section-title {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-3);
    margin-bottom: var(--s-3);
}

.section-title h2 { font-size: var(--t-lg); }

.muted { color: var(--text-soft); }
.faint { color: var(--text-faint); }
.text-sm { font-size: var(--t-sm); }
.text-xs { font-size: var(--t-xs); }

/* ---------- Stat tiles --------------------------------------------------- */

.stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: var(--s-3);
}

.stat {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    padding: var(--s-4);
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-1);
}

.stat-label {
    font-size: var(--t-xs);
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--text-faint);
}

.stat-value {
    font-size: var(--t-2xl);
    font-weight: 680;
    letter-spacing: -.02em;
    /* Tabular figures stop the number jittering as it updates; it sits beside
       other tiles and the eye catches misalignment instantly. */
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
}

.stat-sub { font-size: var(--t-xs); color: var(--text-soft); }

/* ---------- "Next up" list ------------------------------------------------
   A counter's first question on walking in is "what is next", which a full-day
   grid only answers after scrolling to now.                                   */

.upnext {
    display: flex;
    align-items: center;
    gap: var(--s-4);
    width: 100%;
    min-height: var(--tap-lg);
    padding: var(--s-3) var(--s-4);
    border: 0;
    border-bottom: 1px solid var(--border);
    background: transparent;
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.upnext:last-child { border-bottom: 0; }
.upnext:hover { background: var(--bg-sunken); }

.upnext-time {
    flex: none;
    width: 5.5rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.upnext-main { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.upnext-main strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

@media (max-width: 599px) {
    .upnext-time { width: 4.5rem; font-size: var(--t-sm); }
}
.stat.is-alert .stat-value { color: var(--dang-text); }

/* ---------- Badges and slot states --------------------------------------- */

.badge {
    display: inline-flex;
    align-items: center;
    gap: var(--s-1);
    padding: 2px var(--s-2);
    border: 1px solid transparent;
    border-radius: var(--r-full);
    font-size: var(--t-xs);
    font-weight: 650;
    white-space: nowrap;
}

.badge::before {
    content: "";
    width: 6px; height: 6px;
    border-radius: 50%;
    background: currentColor;
}

/* FR-2.2 slot states — differentiated by fill AND border AND label, never hue
   alone, because glare and colour-blindness both flatten hue. */
.badge-available { background: var(--n-50);    color: var(--n-700);      border-color: var(--n-200); }
.badge-held      { background: var(--warn-50); color: var(--warn-text);  border-color: var(--warn-500); }
.badge-unpaid    { background: var(--dang-50); color: var(--dang-text);  border-color: var(--dang-500); }
.badge-partial   { background: var(--warn-50); color: var(--warn-text);  border-color: var(--warn-text); }
.badge-paid      { background: var(--ok-50);   color: var(--ok-text);    border-color: var(--ok-500); }
.badge-checkedin { background: var(--info-50); color: var(--info-text);  border-color: var(--info-500); }
.badge-completed { background: var(--n-100);   color: var(--n-600);      border-color: var(--n-300); }
.badge-cancelled { background: transparent;    color: var(--text-faint); border-color: var(--border-str); }
.badge-blocked   { background: var(--n-200);   color: var(--n-700);      border-color: var(--n-400); }

/* §8.5: a booking created offline is never shown as confirmed. It gets an
   unmistakable treatment — hatched and dashed — not a colour tweak. */
.badge-pending {
    background: repeating-linear-gradient(-45deg, var(--warn-50), var(--warn-50) 5px, transparent 5px, transparent 10px);
    color: var(--warn-text);
    border-color: var(--warn-500);
    border-style: dashed;
}

/* ---------- Sync indicator ----------------------------------------------- */

.sync {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    min-height: 32px;
    padding: 0 var(--s-3);
    border: 1px solid var(--border);
    border-radius: var(--r-full);
    background: var(--bg-raised);
    font-size: var(--t-xs);
    font-weight: 600;
    color: var(--text-soft);
}

.sync .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok-500); }
.sync.is-offline .dot { background: var(--warn-500); }
.sync.is-issue .dot { background: var(--dang-500); }
.sync.is-unknown .dot { background: var(--n-400); }

/* An inline caution inside a sheet — lighter than .alert, which announces a failure that already
   happened; this warns about what will happen if you continue. */
.warn-note {
    padding: var(--s-3);
    border: 1px solid var(--warn-500);
    border-radius: var(--r-md);
    background: var(--warn-50);
    color: var(--warn-text);
}

/* Offline is not a decoration — it changes what staff can promise a customer, so the whole pill
   changes, not just the dot. It must survive being glanced at on a bright forecourt. */
.sync.is-offline {
    background: var(--warn-50);
    border-color: var(--warn-500);
    color: var(--warn-text);
}

/* The label is hidden on narrow screens to save header width, but "Offline" is exactly the word
   that must never be the one dropped. */
@media (max-width: 599px) {
    .sync.is-offline .sync-text { display: inline; }
}

/* ---------- Login -------------------------------------------------------- */

.auth-shell {
    display: grid;
    place-items: center;
    min-height: 100dvh;
    padding: var(--s-5);
    background:
        radial-gradient(1200px 600px at 50% -10%, var(--p-50), transparent 60%),
        var(--bg);
}

.auth-card {
    width: 100%;
    max-width: 24rem;
    padding: var(--s-8) var(--s-6);
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-3);
}

.auth-brand {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-3);
    margin-bottom: var(--s-6);
    text-align: center;
}

.auth-brand .mark { width: 44px; height: 44px; border-radius: 12px; font-size: var(--t-md); }

.auth-form { display: flex; flex-direction: column; gap: var(--s-4); }

.alert {
    display: flex;
    gap: var(--s-2);
    padding: var(--s-3);
    border: 1px solid var(--dang-500);
    border-radius: var(--r-md);
    background: var(--dang-50);
    color: var(--dang-text);
    font-size: var(--t-sm);
    font-weight: 550;
}

/* ---------- Utility ------------------------------------------------------ */

.row { display: flex; align-items: center; gap: var(--s-3); }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); }
.stack { display: flex; flex-direction: column; gap: var(--s-4); }
.stack-sm { display: flex; flex-direction: column; gap: var(--s-2); }
.wrap { flex-wrap: wrap; }

.empty {
    display: grid;
    place-items: center;
    gap: var(--s-2);
    padding: var(--s-10) var(--s-5);
    text-align: center;
    color: var(--text-soft);
    border: 1px dashed var(--border-str);
    border-radius: var(--r-lg);
}

/* First paint. This is the only screen guaranteed to be seen on every cold start, so it carries
   the brand rather than a line of grey text — and it carries a failure state, because a runtime
   that never downloads otherwise leaves the counter looking at "Starting…" forever. */
.boot {
    display: grid;
    place-items: center;
    min-height: 100dvh;
    padding: var(--s-6);
    color: var(--text-soft);
    font-size: var(--t-sm);
}

.boot-inner { display: flex; flex-direction: column; align-items: center; gap: var(--s-4); text-align: center; }
.boot .mark { width: 52px; height: 52px; border-radius: 14px; font-size: var(--t-lg); }

.boot-status { position: relative; padding-left: 1.35rem; }

.boot-status::before {
    content: "";
    position: absolute;
    left: 0;
    top: 50%;
    width: .85rem;
    height: .85rem;
    margin-top: -.425rem;
    border: 2px solid var(--border-str);
    border-top-color: var(--p-600);
    border-radius: 50%;
    animation: boot-spin .7s linear infinite;
}

@keyframes boot-spin { to { transform: rotate(360deg); } }

.boot-fail { display: flex; flex-direction: column; align-items: center; gap: var(--s-3); max-width: 22rem; }
.boot-fail-title { font-size: var(--t-lg); font-weight: 700; color: var(--text); }
.boot-fail-hint { color: var(--text-soft); }

/* Blazor's unhandled-error bar, restyled so a crash reads as handled rather
   than as the app falling over (FR-11.13 territory). */
#blazor-error-ui {
    display: none;
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 1000;
    padding: var(--s-4);
    background: var(--n-900);
    color: var(--n-50);
    box-shadow: var(--shadow-3);
}

#blazor-error-ui .dismiss { cursor: pointer; float: right; }

.loading-progress, .loading-progress-text { display: none; }

/* ---------- Booking grid (FR-2.1) ---------------------------------------
   Must render and scroll smoothly at 12 facilities x 34 slots. The time column
   and the facility header both stay pinned, because losing either while
   scrolling forces staff to count rows to work out what they are looking at.  */

.grid-scroll {
    overflow: auto;
    max-height: 70vh;
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--bg-raised);
    /* Momentum scrolling with the custom sync gesture still intact (FR-11.14). */
    -webkit-overflow-scrolling: touch;
}

.grid {
    border-collapse: separate;
    border-spacing: 0;
    width: 100%;
    font-size: var(--t-sm);
}

.grid thead th {
    position: sticky;
    top: 0;
    z-index: 3;
    padding: var(--s-3) var(--s-2);
    background: var(--bg-raised);
    border-bottom: 1px solid var(--border-str);
    font-size: var(--t-xs);
    font-weight: 650;
    text-align: center;
    white-space: nowrap;
    /* Long facility names must not stretch a column and squash the rest. */
    max-width: 12rem;
    overflow: hidden;
    text-overflow: ellipsis;
}

.grid-time-head, .grid-time {
    position: sticky;
    left: 0;
    z-index: 2;
    width: 4.75rem;
    min-width: 4.75rem;
    background: var(--bg-raised);
    border-right: 1px solid var(--border-str);
    font-variant-numeric: tabular-nums;
    font-weight: 550;
    color: var(--text-soft);
    text-align: right;
    padding: 0 var(--s-2);
    white-space: nowrap;
}

.grid-time-head { z-index: 4; }

.grid tbody td { padding: 0; border-bottom: 1px solid var(--border); }
.grid tbody tr:last-child td { border-bottom: 0; }

/* The current half-hour. A line across the whole row rather than a tinted cell: the point is
   "everything above this has happened", which a fill on individual cells does not say. Uses the
   alert hue, which is not in the slot-state palette, so it can never be mistaken for a state. */
.grid tbody tr.is-now > * { box-shadow: inset 0 2px 0 0 var(--dang-500); }

.grid tbody tr.is-now .grid-time {
    color: var(--dang-text);
    font-weight: 700;
}

.grid tbody tr.is-now .grid-time::after {
    content: "now";
    display: block;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    line-height: 1;
}

.cell { border-left: 1px solid var(--border); }

.cell-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    /* 44px keeps every cell a legitimate touch target (§12 accessibility). */
    min-height: var(--tap);
    padding: 0 var(--s-1);
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    font-size: var(--t-xs);
    font-weight: 600;
    cursor: pointer;
}

.cell-price { color: var(--text-faint); font-weight: 550; }
.cell-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* States mirror the badge vocabulary exactly, so the legend and the grid can
   never drift apart. Fill AND text, never hue alone. */
.cell.is-available  { background: var(--bg-raised); }
.cell.is-available:hover { background: var(--p-50); }
.cell.is-held       { background: var(--warn-50); color: var(--warn-text); }
.cell.is-unpaid     { background: var(--dang-50); color: var(--dang-text); }
.cell.is-partial    { background: var(--warn-50); color: var(--warn-text); box-shadow: inset 3px 0 0 var(--warn-500); }
.cell.is-paid       { background: var(--ok-50);   color: var(--ok-text); }
.cell.is-checkedin  { background: var(--info-50); color: var(--info-text); }
.cell.is-completed  { background: var(--bg-sunken); color: var(--text-faint); }
.cell.is-blocked    { background: var(--n-200); color: var(--n-700); }
.cell.is-closed     { background: var(--bg-sunken); }

/* Offline-created bookings get the same unmistakable hatching as the badge —
   never a colour tweak, because staff must not read it as confirmed (§8.5). */
.cell.is-pending {
    background: repeating-linear-gradient(-45deg, var(--warn-50), var(--warn-50) 5px, transparent 5px, transparent 10px);
    color: var(--warn-text);
}

.cell.is-blocked .cell-btn, .cell.is-closed .cell-btn { cursor: default; }

/* ---------- Booking sheet ------------------------------------------------
   FR-11.6: a bottom sheet on phones where the thumb is, a side panel on desktop
   so the grid stays visible while booking.                                    */

.sheet-backdrop {
    position: fixed;
    inset: 0;
    z-index: 40;
    background: rgb(15 15 14 / .45);
}

.sheet {
    position: fixed;
    z-index: 41;
    left: 0; right: 0; bottom: 0;
    display: flex;
    flex-direction: column;
    max-height: 90dvh;
    background: var(--bg-raised);
    border-top: 1px solid var(--border-str);
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    box-shadow: var(--shadow-3);
}

.sheet-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--s-3);
    padding: var(--s-5) var(--s-5) var(--s-3);
    border-bottom: 1px solid var(--border);
}

.sheet-head h2 { font-size: var(--t-lg); }
.sheet-body { padding: var(--s-5); overflow-y: auto; }

.sheet-foot {
    display: flex;
    gap: var(--s-3);
    padding: var(--s-4) var(--s-5);
    padding-bottom: max(var(--s-4), env(safe-area-inset-bottom));
    border-top: 1px solid var(--border);
}

@media (min-width: 900px) {
    .sheet {
        left: auto;
        top: 0;
        bottom: 0;
        width: min(26rem, 90vw);
        border-radius: 0;
        border-top: 0;
        border-left: 1px solid var(--border-str);
    }
}

/* ---------- Selection, whole-day, teams, ledger --------------------------- */

.col-head { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.col-name { overflow: hidden; text-overflow: ellipsis; max-width: 100%; }

.col-allday {
    padding: 1px var(--s-2);
    border: 1px solid var(--border-str);
    border-radius: var(--r-full);
    background: transparent;
    color: var(--text-soft);
    font: inherit;
    font-size: 10px;
    font-weight: 600;
    cursor: pointer;
}

.col-allday:hover { background: var(--p-50); color: var(--p-600); border-color: var(--p-500); }

/* Selected drag range. Uses an inset ring plus a tint so the selection survives
   the state colours underneath it without repainting them. */
.cell.is-selected { background: var(--p-100); box-shadow: inset 0 0 0 2px var(--p-500); }
.cell.is-selected .cell-price { color: var(--p-800); font-weight: 700; }

.selection-bar {
    position: sticky;
    top: 4.25rem;
    z-index: 15;
    display: flex;
    align-items: center;
    gap: var(--s-3);
    flex-wrap: wrap;
    padding: var(--s-3) var(--s-4);
    background: var(--bg-raised);
    border: 1px solid var(--p-500);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-2);
}

.selection-price { font-size: var(--t-lg); font-variant-numeric: tabular-nums; }

/* ---------- Money rows ---------------------------------------------------- */

.money-rows {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    overflow: hidden;
}

.money-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    padding: var(--s-3) var(--s-4);
    border-bottom: 1px solid var(--border);
    font-variant-numeric: tabular-nums;
}

.money-row:last-child { border-bottom: 0; }
.money-row.is-total { background: var(--bg-sunken); font-size: var(--t-lg); }
.money-row.is-note { background: transparent; }
.is-due { color: var(--dang-text); }

/* ---------- Team cards ---------------------------------------------------- */

.team-card {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    padding: var(--s-4);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--bg-raised);
}

.team-row, .ledger-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    padding: var(--s-3) 0;
    border-bottom: 1px solid var(--border);
}

.team-row:last-child, .ledger-row:last-child { border-bottom: 0; }
.team-money { display: flex; flex-direction: column; align-items: flex-end; font-variant-numeric: tabular-nums; }

/* FR-4.8 re-split prompt: an amber decision box, not an error. The operator is
   being asked something, not told they did something wrong. */
.prompt {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    padding: var(--s-4);
    border: 1px solid var(--warn-500);
    border-radius: var(--r-md);
    background: var(--warn-50);
    color: var(--warn-text);
}

.method-row { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.method-row .btn { flex: 1 1 auto; min-width: 5rem; }

@media (min-width: 900px) {
    .sheet-wide { width: min(32rem, 95vw); }
}

/* ---------- Phone density -------------------------------------------------
   On a 412px phone the schedule's heading, date controls and venue picker
   pushed the first slot below the fold: more than half the screen was chrome
   before a single bookable block. The grid is the screen staff live on, so the
   controls above it earn their height or lose it.                            */

@media (max-width: 599px) {
    /* Keeps ‹ Today › and the picker on one row instead of wrapping to two.
       Date inputs size to their content by default, which is wider than the
       text needs. */
    .date-nav .input { width: 9.5rem; flex: none; }

    h1 { font-size: var(--t-xl); }

    /* The grid gets whatever is left, and always at least half the viewport. */
    .grid-scroll { max-height: none; min-height: 55dvh; }
}

/* A required-field marker that reads as information, not as an error already made. Placed on the
   label so it is visible before anyone types, which is the whole point — a booking that cannot be
   saved without a number should say so before the clerk has taken one. */
.req {
    margin-left: var(--s-2);
    padding: 1px var(--s-2);
    border-radius: var(--r-full);
    background: var(--dang-50);
    color: var(--dang-text);
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
}

/* "We know this person" — a positive confirmation, so it uses the success hue rather than the
   neutral hint style used for instructions. */
.ok-note { color: var(--ok-text); font-weight: 600; }

/* Customer block on the booking detail sheet. The number is a link, not text: on a counter phone
   the next action after opening this is usually to ring the person. */
.customer-card {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    flex-wrap: wrap;
    padding: var(--s-3) var(--s-4);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    background: var(--bg-sunken);
}

.customer-card .btn { font-variant-numeric: tabular-nums; }

/* Dialling code beside the number. The code is a narrow fixed control and the number takes the
   rest — a clerk types the local number far more often than they change the country. */
.phone-input { display: flex; gap: var(--s-2); }
.phone-input select { flex: 0 0 8rem; }
.phone-input input { flex: 1; min-width: 0; }

/* A validation message, distinct from .alert: this is one field being wrong, not the operation
   failing. */
.err-note { color: var(--dang-text); font-weight: 600; }

/* Bulk booking's conflict report (FR-3.8) — a scrolling list rather than a table, since a 141-day
   range is a lot of rows and the only thing worth scanning per row is free-or-not. */
.bulk-date-list {
    max-height: 20rem;
    overflow-y: auto;
    border: 1px solid var(--border);
    border-radius: var(--r-md);
}

.bulk-date-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: var(--s-2) var(--s-3);
    border-bottom: 1px solid var(--border);
    font-size: 0.875rem;
}

.bulk-date-row:last-child { border-bottom: none; }

.bulk-date-row.is-conflict { background: var(--bg-sunken); color: var(--text-soft); }

/* ---------- Bell and toasts (FR-2.9) -------------------------------------- */

.bell-wrap { position: relative; }

.bell {
    display: grid;
    place-items: center;
    position: relative;
    width: var(--tap);
    height: var(--tap);
    border: 0;
    border-radius: var(--r-md);
    background: transparent;
    font-size: 1.15rem;
    cursor: pointer;
}

.bell:hover { background: var(--bg-sunken); }

/* The badge is the alerting hue, not the brand one: it is telling staff something is waiting, and
   it must be readable at a glance across a counter. */
.bell-badge {
    position: absolute;
    top: 4px;
    right: 2px;
    min-width: 18px;
    padding: 0 4px;
    border-radius: var(--r-full);
    background: var(--dang-500);
    color: #fff;
    font-size: 10px;
    font-weight: 800;
    line-height: 18px;
    text-align: center;
}

.bell-panel {
    position: absolute;
    top: calc(100% + var(--s-2));
    right: 0;
    z-index: 40;
    width: min(22rem, calc(100vw - var(--s-6)));
    max-height: 60vh;
    overflow-y: auto;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-3);
}

.bell-empty { padding: var(--s-5); color: var(--text-soft); font-size: var(--t-sm); text-align: center; }

.bell-item {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: var(--s-3) var(--s-4);
    border-bottom: 1px solid var(--border);
    color: var(--text);
    text-decoration: none;
}

.bell-item:last-child { border-bottom: 0; }
.bell-item:hover { background: var(--bg-sunken); text-decoration: none; }

/* Unread carries a left bar as well as weight — a colour-only distinction is not one under glare. */
.bell-item.is-unread { border-left: 3px solid var(--p-600); background: var(--p-50); }

.toasts {
    position: fixed;
    right: var(--s-4);
    bottom: var(--s-4);
    z-index: 60;
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    pointer-events: none;
}

.toast {
    display: flex;
    flex-direction: column;
    gap: 2px;
    max-width: 22rem;
    padding: var(--s-3) var(--s-4);
    border-radius: var(--r-md);
    background: var(--n-900);
    color: var(--n-50);
    box-shadow: var(--shadow-3);
    animation: toast-in .18s var(--ease);
}

@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* On a phone the bottom-right corner is the thumb's home and the tab bar lives there, so toasts
   move above it and span the width instead. */
@media (max-width: 899px) {
    .toasts { left: var(--s-4); right: var(--s-4); bottom: calc(var(--tap-lg) + var(--s-6)); }
    .toast { max-width: none; }
}

/* ---------- Receipt design and preview ------------------------------------ */

.design-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--s-4); align-items: start; }

/* The thermal preview MUST be monospace and must not wrap: it is a character grid, and a
   proportional font or a soft wrap would make it a picture of a receipt rather than the receipt. */
/* The border lives on the paper, not on the text blocks: the QR splits the slip into two <pre>
   elements, and bordering each of them would show a venue two receipts where one will print. */
.thermal-paper {
    border: 1px dashed var(--border-str);
    border-radius: var(--r-md);
    background: var(--bg-sunken);
    padding: var(--s-4) 0;
}

/* The QR is a bitmap on paper, so it is centred on the roll rather than laid out in the grid. */
.thermal-qr { padding: var(--s-3) var(--s-4); text-align: center; }
.thermal-qr svg { display: inline-block; background: #fff; }

.thermal-preview {
    margin: 0;
    padding: 0 var(--s-4);
    overflow-x: auto;
    background: transparent;
    color: var(--text);
    font-family: ui-monospace, "Cascadia Mono", "Consolas", monospace;
    font-size: 12px;
    line-height: 1.35;
    white-space: pre;
    tab-size: 1;
}

/* A4 renders its own styles inline, so this is only the paper it sits on. */
.a4-preview {
    padding: var(--s-4);
    overflow-x: auto;
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    background: #fff;
}

@media (max-width: 899px) {
    .design-grid { grid-template-columns: 1fr; }
}

/* Session template chips in the grid's column header. Distinct from "All day" — these are the
   venue's OWN configured sessions, which on a cricket ground is how the day is actually sold. */
.col-session {
    min-height: 24px;
    margin-left: var(--s-1);
    padding: 0 var(--s-2);
    border: 1px solid var(--p-300);
    border-radius: var(--r-full);
    background: var(--p-50);
    color: var(--p-700);
    font: inherit;
    font-size: 11px;
    font-weight: 650;
    white-space: nowrap;
    cursor: pointer;
}

.col-session:hover { background: var(--p-100); }

/* Session chip: name over time. Two lines because the time is what a clerk checks and the name
   is only a label the venue chose — "Morning Slot 1" means nothing without 7:30–10:30. */
.col-session { display: inline-flex; flex-direction: column; align-items: center; gap: 0; line-height: 1.25; }
.col-session-name { font-weight: 700; }
.col-session-time { font-size: 10px; font-weight: 500; opacity: .85; font-variant-numeric: tabular-nums; }

/* Team A / Team B tabs. They are opponents in ONE slot, entered one at a time. */
.team-tabs { display: flex; gap: var(--s-2); }

.team-tab {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    flex: 1;
    min-height: var(--tap);
    padding: var(--s-2) var(--s-3);
    border: 1px solid var(--border-str);
    border-radius: var(--r-md);
    background: var(--bg-raised);
    color: var(--text-soft);
    font: inherit;
    font-size: var(--t-sm);
    font-weight: 650;
    text-align: left;
    cursor: pointer;
}

.team-tab:hover { background: var(--bg-sunken); }

/* The active side is filled, not merely underlined: on a bright counter screen an underline is
   easy to miss, and typing into the wrong team is the mistake this layout exists to prevent. */
.team-tab.is-active {
    background: var(--p-600);
    border-color: var(--p-700);
    color: #fff;
}

.team-tab.is-add { flex: 0 0 auto; border-style: dashed; color: var(--p-600); }
.team-tab-amount { font-size: var(--t-xs); font-weight: 600; opacity: .9; font-variant-numeric: tabular-nums; }

/* =============================================================================
   Component kit
   -----------------------------------------------------------------------------
   Everything below has a Razor component in Components/Ui. The rule is that
   pages compose components and never hand-roll a control: a product used all
   day by the same person is judged on consistency far more than on any
   individual screen, and consistency does not survive re-implementation per
   page.
   ========================================================================== */

/* ---------- Page header --------------------------------------------------
   Every screen opens the same way: title, one line of context, actions right.
   On a phone the actions wrap under the title rather than squeezing it. */

.page-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--s-4);
    flex-wrap: wrap;
    margin-bottom: var(--s-5);
}

.page-head h1 { font-size: var(--t-xl); }
.page-head-sub { margin-top: 2px; font-size: var(--t-sm); color: var(--text-soft); }
.page-head-actions { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }

@media (min-width: 900px) { .page-head h1 { font-size: var(--t-2xl); } }

/* ---------- Toolbar ------------------------------------------------------
   A sticky strip for controls that act on what is below it. Sticky rather than
   scrolling away, because on the grid these are used continuously. */

.toolbar {
    position: sticky;
    top: 0;
    z-index: var(--z-sticky);
    display: flex;
    align-items: center;
    gap: var(--s-2);
    flex-wrap: wrap;
    padding: var(--s-2) 0;
    margin-bottom: var(--s-4);
    background: var(--bg);
}

/* ---------- Segmented control -------------------------------------------
   For 2-4 mutually exclusive views. Preferred over a select because the
   options stay visible: on an operations screen, knowing what you are NOT
   looking at matters as much as knowing what you are. */

.segmented {
    display: inline-flex;
    padding: 3px;
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    background: var(--bg-sunken);
    user-select: none;
    -webkit-user-select: none;
}

.segmented button {
    min-height: calc(var(--control-h) - 8px);
    padding: 0 var(--s-3);
    border: 0;
    border-radius: calc(var(--r-md) - 2px);
    background: transparent;
    color: var(--text-soft);
    font: inherit;
    font-size: var(--t-sm);
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

.segmented button:hover { color: var(--text); }
.segmented button[aria-pressed="true"] { background: var(--bg-raised); color: var(--text); box-shadow: var(--shadow-1); }
.segmented button:focus-visible { outline: none; box-shadow: var(--ring); }

/* ---------- Card ---------------------------------------------------------- */

.card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    flex-wrap: wrap;
    padding: var(--s-3) var(--gutter);
    border-bottom: 1px solid var(--border);
}

.card-head h2, .card-head h3 { font-size: var(--t-md); font-weight: 650; }
.card-head-sub { font-size: var(--t-xs); color: var(--text-faint); }
.card-body { padding: var(--gutter); }
.card-foot { padding: var(--s-3) var(--gutter); border-top: 1px solid var(--border); background: var(--bg-sunken); }

/* ---------- Data rows ----------------------------------------------------
   A list of records that behaves like a table without being one: a real table
   cannot reflow to a phone without either scrolling sideways or hiding
   columns, and both lose information the counter needs. */

.rows { display: flex; flex-direction: column; list-style: none; margin: 0; padding: 0; }

.row-item {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    width: 100%;
    min-height: var(--row-h);
    padding: var(--s-2) var(--gutter);
    border: 0;
    border-bottom: 1px solid var(--border);
    background: transparent;
    color: var(--text);
    font: inherit;
    text-align: left;
    text-decoration: none;
    cursor: pointer;
    transition: background var(--fast) var(--ease);
}

.rows > li:last-child .row-item, .row-item:last-child { border-bottom: 0; }
.row-item:hover { background: var(--bg-sunken); text-decoration: none; }
.row-item:focus-visible { outline: none; box-shadow: var(--ring); border-radius: var(--r-sm); }

.row-main { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
.row-main strong { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-meta { font-size: var(--t-xs); color: var(--text-soft); }
.row-lead { flex: none; font-weight: 650; font-variant-numeric: tabular-nums; }
.row-trail { flex: none; display: flex; align-items: center; gap: var(--s-2); }

/* ---------- Metrics ------------------------------------------------------
   The numbers a venue opens the app to see. One bordered strip rather than
   separate floating cards: they are read together and compared, and gaps
   between cards break that comparison. */

.metrics {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--bg-raised);
    box-shadow: var(--shadow-1);
    overflow: hidden;
}

@media (min-width: 720px) { .metrics { grid-template-columns: repeat(4, 1fr); } }

.metric {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    padding: var(--s-4) var(--gutter);
    border-right: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
}

@media (min-width: 720px) { .metric { border-bottom: 0; } }
.metric:last-child { border-right: 0; }
@media (max-width: 719px) {
    .metric:nth-child(2n) { border-right: 0; }
    .metric:nth-last-child(-n+2) { border-bottom: 0; }
}

.metric-label { font-size: var(--t-xs); font-weight: 650; letter-spacing: .04em; text-transform: uppercase; color: var(--text-faint); }
.metric-value { font-size: var(--t-2xl); font-weight: 680; letter-spacing: -.02em; font-variant-numeric: tabular-nums; line-height: 1.1; }
.metric-sub { font-size: var(--t-xs); color: var(--text-soft); }
.metric.is-alert .metric-value { color: var(--dang-text); }

/* ---------- Empty and loading states -------------------------------------
   An empty state always says what to do next. "No results" alone leaves the
   person holding the problem. */

.empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-2);
    padding: var(--s-10) var(--gutter);
    text-align: center;
    color: var(--text-soft);
}

.empty-ico { font-size: 28px; opacity: .5; }
.empty strong { color: var(--text); font-size: var(--t-md); }
.empty p { font-size: var(--t-sm); max-width: 40ch; }

/* Skeletons mirror the shape of what is loading, so the layout does not jump
   when it arrives. A centred spinner tells the eye nothing about what is
   coming and guarantees a reflow when it does. */
.skeleton {
    border-radius: var(--r-sm);
    background: linear-gradient(90deg, var(--bg-sunken) 25%, var(--border) 37%, var(--bg-sunken) 63%);
    background-size: 400% 100%;
    animation: shimmer 1.4s ease infinite;
}

@keyframes shimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }

.skeleton-row { height: var(--row-h); margin-bottom: 1px; }

/* ---------- Command palette ----------------------------------------------
   Ctrl/Cmd+K. On a product someone uses for a whole shift the fastest path
   between two screens is typing three letters, and it doubles as the place to
   discover what the app can do without hunting through menus. */

.palette-backdrop {
    position: fixed;
    inset: 0;
    z-index: var(--z-palette);
    display: flex;
    justify-content: center;
    padding: 12vh var(--s-4) var(--s-4);
    background: rgb(15 15 14 / .45);
}

.palette {
    width: min(560px, 100%);
    max-height: 60vh;
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border-str);
    border-radius: var(--r-lg);
    background: var(--bg-raised);
    box-shadow: var(--shadow-3);
    overflow: hidden;
    animation: palette-in var(--med) var(--ease);
}

@keyframes palette-in { from { opacity: 0; transform: translateY(-8px) scale(.98); } }

.palette-input {
    width: 100%;
    padding: var(--s-4);
    border: 0;
    border-bottom: 1px solid var(--border);
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: var(--t-lg);
}

.palette-input:focus { outline: none; }
.palette-list { overflow-y: auto; padding: var(--s-2); }

.palette-item {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    width: 100%;
    min-height: 44px;
    padding: 0 var(--s-3);
    border: 0;
    border-radius: var(--r-md);
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: var(--t-sm);
    text-align: left;
    cursor: pointer;
}

.palette-item[aria-selected="true"] { background: var(--p-50); color: var(--p-700); }
.palette-item .ico { width: 20px; text-align: center; opacity: .8; }
.palette-item-sub { margin-left: auto; font-size: var(--t-xs); color: var(--text-faint); }
.palette-empty { padding: var(--s-6); text-align: center; font-size: var(--t-sm); color: var(--text-faint); }

.kbd {
    display: inline-block;
    min-width: 20px;
    padding: 1px 5px;
    border: 1px solid var(--border-str);
    border-bottom-width: 2px;
    border-radius: 5px;
    background: var(--bg-sunken);
    color: var(--text-soft);
    font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
    font-size: 11px;
    line-height: 1.5;
    text-align: center;
}

/* ---------- Banner -------------------------------------------------------
   A state, not an event. A toast that vanishes after four seconds is the wrong
   shape for something that stays true. */

.banner {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    padding: var(--s-3) var(--gutter);
    border: 1px solid var(--warn-500);
    border-radius: var(--r-md);
    background: var(--warn-50);
    color: var(--warn-text);
    font-size: var(--t-sm);
}

.banner.is-error { border-color: var(--dang-500); background: var(--dang-50); color: var(--dang-text); }

/* ---------- Venue switcher -----------------------------------------------
   Identity of place, in the header. An operator with three venues must never
   have to wonder which one they are looking at. */

.venue-switch {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    min-height: var(--control-h);
    max-width: 240px;
    padding: 0 var(--s-3);
    border: 1px solid transparent;
    border-radius: var(--r-md);
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: var(--t-sm);
    font-weight: 650;
    cursor: pointer;
}

.venue-switch:hover { background: var(--bg-sunken); border-color: var(--border); }
.venue-switch span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.venue-switch::after { content: "⌄"; color: var(--text-faint); font-weight: 400; margin-top: -4px; }
.venue-switch[disabled] { cursor: default; }
.venue-switch[disabled]::after { display: none; }
.venue-switch[disabled]:hover { background: transparent; border-color: transparent; }

/* ---------- Layout helpers ------------------------------------------------ */

.tabular { font-variant-numeric: tabular-nums; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------- Header controls ----------------------------------------------- */

/* The search affordance collapses to its icon on a phone, where the header has
   room for about three things and "which venue" wins. The shortcut hint is the
   only place the palette advertises itself, so it stays wherever it fits. */
.palette-hint { color: var(--text-faint); }
.palette-hint .kbd { margin-left: var(--s-1); }

@media (max-width: 899px) {
    .palette-hint-text, .palette-hint .kbd { display: none; }
    .palette-hint { width: var(--control-h); padding: 0; }
}

/* Sign-out is a glyph rather than a word: it is used once a shift and was
   taking the space that the venue name needs on every screen. */
.app-header .btn-icon { color: var(--text-faint); }
.app-header .btn-icon:hover { color: var(--text); }

/* On a phone the brand is just the mark; the rail carries the full lockup. */
.app-brand { display: flex; align-items: center; gap: var(--s-2); flex: none; }
.app-brand:hover { text-decoration: none; }

/* ---------- Focus containment ---------------------------------------------
   The shell never scrolls, so an element focused inside a scroll region must
   bring itself into view rather than pushing the frame. */
.app-main { scroll-padding-block: var(--s-6); }

/* ---------- Page-level scroll shadow --------------------------------------
   A one-pixel hairline under the header when content has scrolled beneath it,
   so the fixed chrome reads as sitting ABOVE the content rather than being part
   of it. Pure CSS: no scroll listener, nothing to keep in sync. */
.app-main { --shadow-height: 6px; }

/* ---------- Setup: hours editor ------------------------------------------- */

.hours-day {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--s-2);
    padding: var(--s-3) var(--gutter);
    border-bottom: 1px solid var(--border);
}

.hours-day:last-child { border-bottom: 0; }
.hours-day.is-closed { background: var(--bg-sunken); }
.hours-day.is-closed .row-between strong { color: var(--text-faint); }

.window-row { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
.input.time { width: auto; min-width: 7.5rem; flex: none; }

/* ---------- Setup: court list --------------------------------------------- */

.facility-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; }

/* Status by fill AND border AND word — never hue alone. Same rule as the slot
   badges, same reason: glare on a counter screen flattens colour. */
.badge.is-live    { background: var(--ok-50);   color: var(--ok-text);    border-color: var(--ok-500); }
.badge.is-draft   { background: var(--n-100);   color: var(--n-700);      border-color: var(--n-400); }
.badge.is-paused  { background: var(--warn-50); color: var(--warn-text);  border-color: var(--warn-500); }
.badge.is-retired { background: transparent;    color: var(--text-faint); border-color: var(--border-str); }

.back-link {
    display: inline-block;
    margin-bottom: var(--s-1);
    font-size: var(--t-sm);
    font-weight: 600;
    color: var(--text-soft);
    text-decoration: none;
}

.back-link:hover { color: var(--p-600); text-decoration: none; }

.setup-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--s-3); }

.setup-card {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    padding: var(--s-4);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--bg-raised);
    box-shadow: var(--shadow-1);
    color: var(--text);
    text-decoration: none;
    transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}

.setup-card:hover { border-color: var(--p-400, var(--p-500)); box-shadow: var(--shadow-2); text-decoration: none; }
.setup-card > span:last-child { display: flex; flex-direction: column; min-width: 0; }
.setup-ico { font-size: 22px; line-height: 1; flex: none; }

.session-list { list-style: none; margin: 0; padding: 0; }

.session-list > li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    min-height: var(--row-h);
    padding: var(--s-2) 0;
    border-bottom: 1px solid var(--border);
}

.session-list > li:last-child { border-bottom: 0; }
.session-list > li > span { display: flex; flex-direction: column; }

.affected-list { list-style: none; margin: var(--s-2) 0 0; padding: 0; }
.affected-list > li { display: flex; gap: var(--s-2); align-items: baseline; padding: 2px 0; font-size: var(--t-sm); }

.day-chips { display: flex; flex-wrap: wrap; gap: var(--s-2); }

.chip {
    min-width: 44px;
    min-height: 36px;
    padding: 0 var(--s-3);
    border: 1px solid var(--border-str);
    border-radius: var(--r-full);
    background: var(--bg-raised);
    color: var(--text-soft);
    font: inherit;
    font-size: var(--t-xs);
    font-weight: 650;
    cursor: pointer;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

.chip:hover { border-color: var(--p-500); }
.chip.is-on { background: var(--p-600); border-color: var(--p-600); color: #fff; }

/* A form grid that collapses to one column on a phone. Two columns is the most
   a settings form should ever use — three makes the eye hunt for the label. */
.grid-2 { display: grid; grid-template-columns: 1fr; gap: var(--s-4); }
@media (min-width: 720px) { .grid-2 { grid-template-columns: 1fr 1fr; } }

.sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ---------- Schedule: sessions view (FR-2.1, FR-4.4) ----------------------
   How a ground actually sells. One section per session time, one card per
   court. The sessions used to be crammed into the grid's column headers as a
   stack of chips, which made every header a different height, pushed the grid
   itself off the screen, and still never said whether a session was free. */

.session-grid { display: flex; flex-direction: column; gap: var(--s-5); }

.session-block-head {
    display: flex;
    align-items: baseline;
    gap: var(--s-3);
    padding-bottom: var(--s-2);
    margin-bottom: var(--s-3);
    border-bottom: 1px solid var(--border);
}

.session-block-head strong { font-size: var(--t-md); font-weight: 650; }

/* Auto-fit rather than a fixed column count: a venue with two courts should not
   get two cards stranded on the left of a 27" monitor, and one with twelve
   should not get a horizontal scrollbar. */
.session-cards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: var(--s-3);
}

.session-card {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    min-height: 84px;
    padding: var(--s-3) var(--s-4);
    border: 1px solid var(--border-str);
    border-left-width: 3px;
    border-radius: var(--r-md);
    background: var(--bg-raised);
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
    box-shadow: var(--shadow-1);
    transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}

.session-card:hover:not(:disabled) { box-shadow: var(--shadow-2); }
.session-card:focus-visible { outline: none; box-shadow: var(--ring); }
.session-card:disabled { cursor: default; opacity: .6; box-shadow: none; }

.session-card-name { font-weight: 650; font-size: var(--t-sm); }
.session-card-state { font-size: var(--t-xs); color: var(--text-soft); }
.session-card-price { margin-top: auto; font-weight: 700; font-variant-numeric: tabular-nums; }

/* State by the left edge AND the words — the same rule as the slot badges, so a
   card is readable in daylight and to a colour-blind clerk. */
.session-card.is-free      { border-left-color: var(--ok-500); }
.session-card.is-partial   { border-left-color: var(--warn-500); background: var(--warn-50); color: var(--warn-text); }
.session-card.is-closed    { border-left-color: var(--border-str); background: var(--bg-sunken); color: var(--text-faint); }
.session-card.cell-unpaid  { border-left-color: var(--dang-500); background: var(--dang-50); color: var(--dang-text); }
.session-card.cell-partial { border-left-color: var(--warn-500); background: var(--warn-50); color: var(--warn-text); }
.session-card.cell-paid    { border-left-color: var(--ok-500);   background: var(--ok-50);   color: var(--ok-text); }
.session-card.cell-checkedin { border-left-color: var(--info-500); background: var(--info-50); color: var(--info-text); }
.session-card.cell-blocked { border-left-color: var(--n-400); background: var(--n-100); color: var(--n-700); }
.session-card.cell-held    { border-left-color: var(--warn-500); background: var(--warn-50); color: var(--warn-text); }

/* ---------- Schedule: hours view header ----------------------------------
   The header is a LABEL now. Everything that used to live in it — whole-day
   booking, the session chips — moved out, which is what lets every column head
   be the same height and the grid start near the top of the screen. */

.grid thead th {
    position: sticky;
    top: 0;
    z-index: var(--z-sticky);
    height: 40px;
    padding: 0 var(--s-2);
    background: var(--bg-raised);
    border-bottom: 1px solid var(--border-str);
    font-size: var(--t-sm);
    font-weight: 650;
    text-align: center;
    white-space: nowrap;
}

.col-name { overflow: hidden; text-overflow: ellipsis; display: block; }

/* The time column stays put horizontally as well, so scrolling twelve courts
   sideways never leaves the clerk guessing which hour a row is. */
.grid .grid-time-head { left: 0; z-index: calc(var(--z-sticky) + 1); }
