/* ==========================================================================
   Cbms operator portal

   Mobile first. Every base rule below targets a phone; wider layouts are added
   at min-width breakpoints. This order matters for more than tidiness - venue
   staff use this on a phone while standing on the floor, and the desktop-first
   alternative means the phone pays to override rules it never wanted.

   Breakpoints:
     (base)    phone,   < 640px
     >= 640px  large phone / small tablet
     >= 900px  tablet
     >= 1200px desktop

   The look is calm and spatial rather than technical: separation comes from
   whitespace, and a hairline is used only where a boundary has to be explicit.
   Colour carries information and nothing else - four hues, one meaning each,
   and nothing is tinted for decoration. Weight and space carry hierarchy, so
   there are no uppercase labels and no letter-spaced eyebrows anywhere.
   ========================================================================== */

:root {
    color-scheme: light dark;

    /* Surfaces, lightest content on the calmest ground. */
    --page: #f5f5f7;
    --surface: #ffffff;
    --surface-2: #fafafa;
    --surface-sunken: #f0f0f2;

    /* Separators. --sep is the hairline inside a group; --sep-strong is a real
       boundary, and is deliberately rare. */
    --sep: #e8e8ed;
    --sep-strong: #d2d2d7;

    --text: #1d1d1f;
    --muted: #6b6b70;
    --faint: #86868b;

    /* Four hues, each with exactly one job. Anything not in this list is grey. */
    --blue: #0071e3;    /* actions and links */
    --green: #34c759;   /* online, healthy */
    --red: #ff3b30;     /* fault, offline, and the buttons that reach into a machine */
    --orange: #ff9500;  /* figures that carry weight */

    /* Tints for badge and notice grounds. Kept very pale: a status pill should
       read as a label with a colour, not as a coloured block. */
    --blue-tint: #e8f1fd;
    --green-tint: #e6f8ec;
    --red-tint: #ffeceb;
    --orange-tint: #fff3e3;
    --grey-tint: #f0f0f2;

    /* Text-safe variants. The raw hues above are calibrated for fills; several of
       them fail contrast as small text on a pale tint, so labels use these. */
    --blue-ink: #0058b0;
    --green-ink: #1c7a3a;
    --red-ink: #c9271d;
    --orange-ink: #9a5b00;

    /* Radius varies by role rather than being one value everywhere: a pill badge
       and a grouped card are different kinds of object and should not rhyme. */
    --r-group: 14px;
    --r-control: 10px;
    --r-pill: 980px;

    --gutter: 16px;

    /* Minimum comfortable touch target. Applied to anything tappable rather than
       sprinkled ad hoc, so controls stay usable with a thumb on a busy floor. */
    --tap: 44px;

    /* On the phone a venue employee is holding, this renders real SF Pro; on a
       Windows desktop, Segoe UI Variable. A webfont would be worse than either:
       the app is served from a NUC on the LAN, so a face fetched from the public
       internet degrades in exactly the place the product is used. */
    --font: -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text", "Segoe UI",
            Roboto, "Helvetica Neue", Arial, sans-serif;

    /* Reserved for machine-generated identifiers - serials, device ids, hex
       condition codes, build revisions. Never used for labels or headings: it is
       here because a serial has to be compared character by character. */
    --mono: ui-monospace, SFMono-Regular, "SF Mono", "Cascadia Mono", Consolas,
            "Liberation Mono", monospace;
}

/* Dark is designed rather than inverted: the greys are Apple's dark system set,
   and the four hues swap to their darker-background variants, which are lighter
   and less saturated so they do not vibrate against black. Worth having for its
   own sake, and this product gets read on a dim floor at two in the morning. */
@media (prefers-color-scheme: dark) {
    :root {
        --page: #000000;
        --surface: #1c1c1e;
        --surface-2: #242426;
        --surface-sunken: #141416;

        --sep: #2c2c2e;
        --sep-strong: #38383a;

        --text: #f5f5f7;
        --muted: #98989d;
        --faint: #7c7c80;

        --blue: #0a84ff;
        --green: #30d158;
        --red: #ff453a;
        --orange: #ff9f0a;

        --blue-tint: #0a2540;
        --green-tint: #0d2c17;
        --red-tint: #3a1412;
        --orange-tint: #3a2607;
        --grey-tint: #2c2c2e;

        --blue-ink: #6cb6ff;
        --green-ink: #5cdd82;
        --red-ink: #ff7b73;
        --orange-ink: #ffbe4d;
    }
}

* { box-sizing: border-box; }

html {
    /* Stops iOS inflating text in landscape, which otherwise breaks dense tables. */
    -webkit-text-size-adjust: 100%;

    /* The scrollbar's room is kept whether or not the page needs one, so moving between a long
       report and a short one (Device moves, on the 0.60.4 walk) does not shift the header. */
    scrollbar-gutter: stable;
}

html, body {
    margin: 0;
    padding: 0;
    font-family: var(--font);
    font-size: 16px;
    line-height: 1.47;
    letter-spacing: 0;
    color: var(--text);
    background: var(--page);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    /* The page must never scroll sideways; wide content scrolls inside its own box. */
    overflow-x: hidden;
}

@media (min-width: 900px) {
    html, body { font-size: 15px; }
}

a { color: var(--blue); text-decoration: none; }
a:hover { text-decoration: underline; }

h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: 0; }
h1 { font-size: 21px; letter-spacing: 0; }
h2 { font-size: 17px; }
h3 { font-size: 15px; }

@media (min-width: 900px) {
    h1 { font-size: 22px; }
}

/* One visible focus treatment for the whole product. The stylesheet this replaces
   set outline:none on inputs and signalled focus with a border colour, which is a
   keyboard accessibility failure rather than a style decision. */
:focus-visible {
    outline: 2px solid var(--blue);
    outline-offset: 2px;
    border-radius: 4px;
}

/* The page heading takes focus on navigation for screen readers; it is not a control. */
h1:focus,
h1:focus-visible {
    outline: none;
}

/* ---------- Chrome --------------------------------------------------------- */

/* The utility bar and the header used to be two full-width bands on two different
   grounds, above a third band of navigation. They are one band now, sharing a
   surface and separated only by space, so content begins roughly 90px higher on a
   phone. Nothing was lost with the height: the strapline and the search box that
   went were decoration and dead chrome respectively. */
.utility-bar {
    background: var(--surface);
    color: var(--muted);
    font-size: 12px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 12px;
    padding: 8px var(--gutter) 0;
}

.utility-bar a, .utility-bar .utility-link { color: var(--muted); }
.utility-bar a:hover, .utility-bar .utility-link:hover { color: var(--text); }

.utility-bar .utility-link {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    cursor: pointer;
    text-decoration: none;
}

.utility-bar .sep { color: var(--sep-strong); }

/* The strapline is the first thing worth losing on a narrow screen. */
.utility-bar .optional { display: none; }

@media (min-width: 640px) {
    .utility-bar .optional { display: inline; }
}

.site-header {
    background: var(--surface);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 12px;
    padding: 6px var(--gutter) 10px;
}

.brand { display: flex; align-items: baseline; gap: 8px; }

.brand-mark {
    font-size: 20px;
    font-weight: 600;
    letter-spacing: 0;
    color: var(--text);
}

.brand-sub { display: none; font-size: 12px; color: var(--muted); }

.header-actions { display: flex; align-items: center; gap: 20px; margin-left: auto; }

.header-action {
    display: flex;
    align-items: baseline;
    gap: 6px;
    font-size: 13px;
    color: var(--muted);
}

.header-action strong { font-weight: 500; color: var(--text); }
a.header-action:hover { text-decoration: none; }
a.header-action:hover strong { color: var(--blue); }

@media (min-width: 640px) {
    .brand-sub { display: inline; }
}

@media (min-width: 900px) {
    .site-header { flex-wrap: nowrap; gap: 28px; }
}

/* ---------- Primary navigation --------------------------------------------- */

.main-nav {
    background: var(--surface);
    border-bottom: 1px solid var(--sep);
    display: flex;
    flex-direction: column;
}

/* On a phone the nav is a horizontally scrollable strip rather than a hamburger:
   one tap to reach any section beats two, and the scroll hint is visible. */
.nav-links {
    display: flex;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    padding: 0 calc(var(--gutter) - 10px);
}

.nav-links::-webkit-scrollbar { display: none; }

/* The strip's last tab fades out at the edge on a phone, so a cut-off "Bet" reads as more to
   scroll to rather than as a tab with a short name. Off where everything fits. */
@media (max-width: 899px) {
    /* The padding lets the last tab scroll clear of the fade. */
    .nav-links {
        padding-right: 28px;
        mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
    }
}

.nav-links a {
    display: flex;
    align-items: center;
    white-space: nowrap;
    padding: 0 10px;
    min-height: var(--tap);
    font-size: 15px;
    color: var(--muted);
}

.nav-links a:hover { color: var(--text); text-decoration: none; }

/* Weight and colour mark the section, not a coloured underline bar. */
.nav-links a.active { color: var(--text); font-weight: 500; }

/* The old blue "Operations" block sat here with a hamburger glyph that opened
   nothing. Cut rather than restyled. */
.nav-block { display: none; }

.nav-right {
    display: flex;
    align-items: center;
    padding: 0 var(--gutter) 10px;
}

/* Venue scope is a permanent fixture, not a per-page filter: every figure in the
   app is scoped by it, and a stale selection misreads the data without looking wrong. */
.venue-scope { display: flex; align-items: center; gap: 8px; font-size: 13px; width: 100%; }

.venue-scope label { color: var(--muted); flex: 0 0 auto; }

.venue-scope select {
    flex: 1 1 auto;
    min-width: 0;
    height: var(--tap);
    border: 1px solid var(--sep-strong);
    border-radius: var(--r-control);
    padding: 0 10px;
    background: var(--surface);
    color: var(--text);
    font-family: inherit;
    font-size: 16px;
}

@media (min-width: 900px) {
    .main-nav { flex-direction: row; align-items: center; padding: 0 var(--gutter); }
    .nav-links { padding: 0; margin-left: -10px; }
    .nav-links a { padding: 0 12px; min-height: 40px; }
    .nav-right { margin-left: auto; padding: 0; }
    .venue-scope { width: auto; }
    .venue-scope select { height: 32px; min-width: 200px; font-size: 13px; }
}

/* ---------- Page shell ----------------------------------------------------- */

.page { padding: 20px var(--gutter) 56px; }

@media (min-width: 900px) {
    .page { padding: 28px var(--gutter) 72px; }
}

.page-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 14px;
    margin-bottom: 18px;
}

.page-head .spacer { margin-left: auto; }
.page-head-actions { display: flex; gap: 8px; margin-left: auto; }

/* "Overview" after a venue's name in the heading: the name is what the reader needs to see. */
.page-head-scope { color: var(--muted); font-weight: 500; }
.page-sub { color: var(--muted); font-size: 13px; }

/* The game a cabinet is running, beside its asset number - see CabinetName.razor. Muted and in
   the body face rather than mono, so the number still reads as the label and this as its name. */
.cabinet-game { color: var(--muted); font-family: var(--font); font-weight: normal; margin-left: 6px; }

/* A refusal shown beside the button that was refused: the reason has to carry, or the button
   reads as broken rather than as declining. */
.page-sub.accent { color: var(--orange-ink); }

.layout-split { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; align-items: start; }

@media (min-width: 1200px) {
    .layout-split { grid-template-columns: minmax(0, 1fr) 340px; gap: 24px; }
}

/* ---------- Cards ---------------------------------------------------------- */

/* The grouped container this whole portal is built from. No border and no shadow:
   the radius and the lift off the page ground are enough to read as a group, and a
   border plus a shadow plus a fill is three devices doing one job. */
.card {
    background: var(--surface);
    border-radius: var(--r-group);
    overflow: hidden;
}

.card + .card { margin-top: 16px; }

.card-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    padding: 16px 18px 12px;
}

.card-head .spacer { margin-left: auto; }
.card-body { padding: 4px 18px 18px; }
.card-body.flush { padding: 0; }

/* The head's mirror, for what a card says after its content: a truncation note and the link to
   the whole list. Lighter than the head, because it is a caption rather than a title. */
.card-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    padding: 14px 18px;
    border-top: 1px solid var(--sep);
    font-size: 13px;
    color: var(--muted);
}

.card-foot .spacer { margin-left: auto; }

/* ---------- Stat tiles ----------------------------------------------------- */

/* Two across on a phone: one is wasteful, three is unreadable. */
.stat-row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px;
    margin-bottom: 18px;
}

@media (min-width: 640px) {
    .stat-row { grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }
}

/* Seven tiles instead of six, for the one row that grows a tile when the floor needs attention.

   The 170px minimum fits six across at 1280px and seven at 1440px, so a seventh would wrap to an
   orphan on the commonest desktop width - which is the width most of this is read at. Dropping the
   minimum to 148px fits seven at 1280 and is applied only where a seventh tile actually exists, so
   the resting six-tile row and the four-tile rows on the drill-downs keep the wider track. */
@media (min-width: 640px) {
    .stat-row.has-seven { grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); }
}

/* Six tiles on a tablet are three and three. The 170px track fits four at 800px, which left the
   second row two tiles and two empty slots. */
@media (min-width: 640px) and (max-width: 1099px) {
    .stat-row:not(.has-seven):has(> :nth-child(6):last-child) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* A tile that is a way somewhere, not just a figure. Inherits its colour so the .bad value inside
   it still reads as the warning rather than as a link. */
a.stat {
    display: block;
    color: inherit;
    text-decoration: none;
}

a.stat:hover { background: var(--surface-2); }

.stat {
    background: var(--surface);
    border-radius: var(--r-group);
    padding: 16px 18px 18px;
    min-width: 0;
}

/* Sentence case. The uppercase, letter-spaced version this replaces is the single
   commonest tell of a templated page, and it sat three rules away from the data. */
.stat .label {
    font-size: 13px;
    color: var(--muted);
    font-weight: 400;
}

/* The size a figure gets when it fits. wwwroot/stat-fit.js measures each tile after render and
   scales this down for the figures that do not - two tiles across on a phone leaves about 130px,
   and a period turnover wants half as much again. overflow-wrap stays as the last resort for a
   figure too long even for the script's floor, and is what the whole row did before it. */
.stat .value {
    font-size: 30px;
    font-weight: 590;
    letter-spacing: 0;
    line-height: 1.1;
    margin-top: 8px;
    font-variant-numeric: tabular-nums;
    overflow-wrap: anywhere;
}

@media (min-width: 900px) {
    .stat .value { font-size: 34px; }
}

.stat .value.accent { color: var(--orange-ink); }
.stat .value.bad { color: var(--red-ink); }
/* .good is deliberately not coloured. Everything being fine is the resting state of this
   product, and a green figure for it leaves nothing for the figures that need a person. */
.stat .value.good { color: inherit; }
.stat .foot { font-size: 12px; color: var(--muted); margin-top: 6px; }

/* ---------- Loading -------------------------------------------------------- */

/* A tile or panel whose figures are being re-read. The old figures stay where they are, faded,
   rather than being blanked: a reader mid-glance keeps their place, and the fade says the numbers
   are about to be replaced rather than that they are wrong.

   Two ways in. .is-loading is set by the page on the one panel a click is re-reading, and cleared
   as each read lands. html.cbms-rescoping is set by the venue selector just before it posts, and
   covers everything, because a new scope changes every figure on every page; the reload that
   follows takes it away. The wrapper form (.is-loading > .card) is for the shared table components,
   which own their card and are wrapped by the page rather than given a parameter. */
.stat.is-loading,
.card.is-loading,
.is-loading > .card,
html.cbms-rescoping .stat,
html.cbms-rescoping .card {
    position: relative;
}

.stat.is-loading > *,
.card.is-loading > *,
.is-loading > .card > *,
html.cbms-rescoping .stat > *,
html.cbms-rescoping .card > * {
    opacity: .45;
    transition: opacity .15s ease;
}

.stat.is-loading::after,
.card.is-loading::after,
.is-loading > .card::after,
html.cbms-rescoping .stat::after,
html.cbms-rescoping .card::after {
    content: "";
    position: absolute;
    box-sizing: border-box;
    border-radius: 50%;
    border: 2px solid var(--muted);
    border-top-color: var(--orange-ink);
    animation: spin .8s linear infinite;
    pointer-events: none;
}

/* The corner a tile leaves empty - the label is on the left and nothing is on the right. */
.stat.is-loading::after,
html.cbms-rescoping .stat::after {
    top: 14px;
    right: 14px;
    width: 16px;
    height: 16px;
}

/* The middle of a panel, because the right of its heading is where its pager and its period are. */
.card.is-loading::after,
.is-loading > .card::after,
html.cbms-rescoping .card::after {
    top: 50%;
    left: 50%;
    width: 24px;
    height: 24px;
    margin: -12px 0 0 -12px;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .stat.is-loading::after,
    .card.is-loading::after,
    .is-loading > .card::after,
    html.cbms-rescoping .stat::after,
    html.cbms-rescoping .card::after {
        animation: lamp 2.4s ease-in-out infinite;
    }
}

/* A page on its way. html.cbms-navigating is set by nav-progress.js from the click on a link until
   Blazor has patched the new page in, because enhanced navigation leaves the old page standing,
   untouched, while it waits - and a click with nothing to show for it reads as a click that missed.

   A bar across the top, and the page being left faded. The fade waits a beat so that a navigation
   that lands at once does not flicker; the bar does not, because the bar is the acknowledgement. */
html.cbms-navigating body::before {
    content: "";
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 3px;
    z-index: 2000;
    background: linear-gradient(90deg, transparent 0%, var(--blue) 35%, var(--blue) 65%, transparent 100%);
    background-size: 40% 100%;
    background-repeat: no-repeat;
    animation: nav-sweep 1.1s ease-in-out infinite;
    pointer-events: none;
}

.page {
    transition: opacity .2s ease;
}

html.cbms-navigating .page {
    opacity: .5;
    transition-delay: .25s;
}

@keyframes nav-sweep {
    from { background-position: -40% 0; }
    to   { background-position: 140% 0; }
}

@media (prefers-reduced-motion: reduce) {
    html.cbms-navigating body::before {
        background: var(--blue);
        animation: lamp 2.4s ease-in-out infinite;
    }
}

/* ---------- Tables --------------------------------------------------------- */

.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }

table.grid { width: 100%; border-collapse: collapse; font-size: 14px; }

/* No fill behind the header and no rule under it. The gap and the lighter colour
   separate it from the rows, which is what the grouped-list idiom does and is the
   main reason a dense table stops reading as a spreadsheet. */
table.grid th {
    text-align: left;
    font-weight: 400;
    font-size: 12px;
    color: var(--muted);
    padding: 4px 14px 8px;
    white-space: nowrap;
}

/* The hairline is drawn as an inset shadow rather than a border so that the last
   row can drop it cleanly and the group's rounded corner is never crossed by a line. */
table.grid td {
    padding: 12px 14px;
    box-shadow: inset 0 -1px 0 var(--sep);
    vertical-align: top;
}

table.grid tbody tr:last-child td { box-shadow: none; }
table.grid tbody tr:hover td { background: var(--surface-2); }
table.grid td.num, table.grid th.num { text-align: right; font-variant-numeric: tabular-nums; }

/* Identifiers only - serials, device ids, condition codes. */
table.grid td.mono, .mono {
    font-family: var(--mono);
    font-size: 12.5px;
    letter-spacing: 0;
}

/* ---------- Meter grid ------------------------------------------------------

   Hours down the side, one column per meter. This is the one table in the app that is
   genuinely wide by nature - a cabinet reporting eighty meters is eighty columns and there
   is no honest way to narrow it - so it scrolls sideways and pins the column that says
   which row you are on. Without the pin, scrolling to meter 0022 loses the hour, and a raw
   counter with no time against it is not evidence of anything.

   Not .stackable, deliberately: see the note on the component. */

table.grid.meter-grid { width: auto; min-width: 100%; }

/* Every cell is one number or one instant; wrapping them would cost height on every row to
   save width on one column. */
table.grid.meter-grid th,
table.grid.meter-grid td { white-space: nowrap; }

/* The header carries three things - code, name, and any qualifier - stacked rather than run
   together, so the column stays as narrow as its numbers. */
table.grid.meter-grid thead th {
    vertical-align: bottom;
    padding-bottom: 10px;
}

.meter-grid-name,
.meter-grid-qualifier { display: block; font-size: 11px; line-height: 1.3; }

.meter-grid-name { color: var(--muted); }
.meter-grid-qualifier { color: var(--faint); }

/* Pinned. z-index only on the cells that overlap, and a background of their own: a sticky
   cell is transparent by default and would let the scrolling columns show through it. */
table.grid.meter-grid .meter-grid-time {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--surface);
    text-align: left;
    padding-right: 20px;
}

/* The hover fill is painted per cell, so the pinned one has to opt in or it stays white
   while the rest of its row lights up. */
table.grid.meter-grid tbody tr:hover td.meter-grid-time { background: var(--surface-2); }

/* EventTime stacks its two clocks flush right, which is what every other table wants because
   the instant is the last column there. Here it is the row's label and sits at the left edge,
   so it reads against the numbers rather than drifting away from the heading above it. */
table.grid.meter-grid .meter-grid-time .event-time { align-items: flex-start; }

/* --faint is this sheet's placeholder colour, and a meter nobody read in that hour is
   exactly that: an absence, not a zero. */
.meter-grid-absent { color: var(--faint); }

/* A reading kept at a boundary, interleaved between the hours. A sunken ground and a line of
   type, not a badge and not a hue: it is a row of the same table holding the same kind of
   number, the four colours are spoken for, and a pill in the time column would read as a
   status on the hour beside it.

   Painted on the cells rather than the row, because the pinned time column sets a background
   of its own and would otherwise sit in a stripe of plain white while the rest of the row was
   tinted. --surface-sunken rather than --surface-2 so that hover, which is --surface-2, still
   moves: a row that already looks hovered is a row whose hover says nothing. */
table.grid.meter-grid tbody tr.meter-grid-kept td,
table.grid.meter-grid tbody tr.meter-grid-kept td.meter-grid-time { background: var(--surface-sunken); }

table.grid.meter-grid tbody tr.meter-grid-kept:hover td,
table.grid.meter-grid tbody tr.meter-grid-kept:hover td.meter-grid-time { background: var(--sep); }

/* The live row. A hairline under it rather than a fill: the boundary between where the counters
   stand and what they have done is the one boundary on this table that has to be explicit, and
   this sheet spends a hairline exactly there and nowhere else. The row itself stays on the plain
   surface, because it is the answer the page opens with and tinting it would push it back. */
table.grid.meter-grid tbody tr.meter-grid-live td { border-bottom: 1px solid var(--sep-strong); }

/* Why the row is between the hours, or what the top one is. --muted rather than --faint: it is
   read, not a placeholder. */
.meter-grid-why {
    display: block;
    font-size: 11px;
    line-height: 1.3;
    color: var(--muted);
}

/*
   A table marked .stackable turns into one block per row on a phone, with each cell
   labelled from its data-label attribute. Used for tables people genuinely need to
   read on a phone - venue summaries, alerts. Wide diagnostic tables such as the audit
   log are deliberately left to scroll sideways instead: stacking forty columns
   produces a page nobody can scan, and horizontal scrolling is the honest answer.
*/
@media (max-width: 767px) {
    table.stackable thead { display: none; }

    table.stackable, table.stackable tbody, table.stackable tr, table.stackable td {
        display: block;
        width: 100%;
    }

    table.stackable tr {
        padding: 10px 0 12px;
        box-shadow: inset 0 -1px 0 var(--sep);
    }

    table.stackable tbody tr:last-child { box-shadow: none; }

    /* The label floats left and the value flows to its right, rather than the two being flex
       items. As flex items every child of the cell was a column of its own, so a value with a
       line under it - a ticket and its venue, a net win and its change, a count and its badge -
       put the line in a third column a few pixels wide and cut it off at the card's edge (the
       cage on a phone read "Jo / Brisba / Sha"). Flowing, the line sits under its value. */
    table.stackable td {
        box-shadow: none;
        display: flow-root;
        padding: 4px 18px;
        text-align: right;
        font-size: 14px;
    }

    table.stackable tbody tr:hover td { background: none; }

    table.stackable td::before {
        content: attr(data-label);
        float: left;
        width: 45%;
        padding-right: 14px;
        font-weight: 400;
        color: var(--muted);
        text-align: left;
    }

    /* A cell with no data-label has no label to sit beside, so it takes the full width and
       stays left-aligned rather than being shoved right by an empty 45% gutter. Alerts is
       the case that matters: none of its cells are labelled, because an alert summary
       describes itself and "Severity:" in front of "Critical" is noise. Without this the
       whole card reads as right-aligned fragments floating away from their heading. */
    table.stackable td:not([data-label]) {
        text-align: left;
    }

    table.stackable td:not([data-label])::before { content: none; }

    /* A cell whose value is a row of controls - Access's grant - takes the card's width under
       its label, because beside it the controls had 55% of a phone and ran off the edge. */
    table.stackable td.stack-full { text-align: left; }
    table.stackable td.stack-full::before { float: none; display: block; width: auto; margin-bottom: 6px; }

    /* Block, not flex. A title cell carries the summary, sometimes an inline "N affected"
       badge, and sometimes a detail line - as flex children those lay out as three columns
       and the heading loses its own line. Stacking them is what makes the row read as one
       alert instead of fragments. */
    table.stackable td.row-title {
        display: block;
        text-align: left;
        font-weight: 600;
        font-size: 16px;
        letter-spacing: 0;
        padding-top: 6px;
        padding-bottom: 6px;
    }

    /* The detail line under a title is secondary, so it drops back to body weight. */
    table.stackable td.row-title .page-sub {
        font-weight: 400;
        letter-spacing: 0;
        margin-top: 2px;
    }

    table.stackable td.row-title::before { content: none; }
}


/*
   The same rules again, asked of the container instead of the window.

   Site revenue moved into the narrow column of the reports grid on 2026-09-14, which is about
   700px at the 1200px breakpoint - too little for ten columns, and .table-wrap would have scrolled
   it sideways inside a panel, which is worse than the squeeze it replaced. The stacking above is
   exactly the right answer and was reachable only by making the whole window narrow, because a
   media query asks about the viewport and the question here is the panel.

   Scoped to elements that opt in, so alerts, the audit log and the full-width cabinet table keep
   the viewport rule and are untouched. Duplicated rather than shared: a condition is not a value,
   so no custom property can stand in for a width in an at-rule, and the alternatives - a
   preprocessor, or a class toggled from script - both cost more than one copy of a settled block.

   860px rather than the 767px above, measured rather than guessed. The site ledger is ten columns
   and a venue name: at 880px the rows sit on one line, and at 780px - which is what the 1200px
   breakpoint gives it - every name wraps to two lines and "-306.6% vs previous" wraps to three.
   The table fits either way, so this is not about overflow. It is that a table nobody can scan is
   worse than the same figures as labelled rows.
*/
.stack-when-narrow { container-type: inline-size; }

@container (max-width: 860px) {
    table.stackable thead { display: none; }

    table.stackable, table.stackable tbody, table.stackable tr, table.stackable td {
        display: block;
        width: 100%;
    }

    table.stackable tr {
        padding: 10px 0 12px;
        box-shadow: inset 0 -1px 0 var(--sep);
    }

    table.stackable tbody tr:last-child { box-shadow: none; }

    /* The label floats left and the value flows to its right, rather than the two being flex
       items. As flex items every child of the cell was a column of its own, so a value with a
       line under it - a ticket and its venue, a net win and its change, a count and its badge -
       put the line in a third column a few pixels wide and cut it off at the card's edge (the
       cage on a phone read "Jo / Brisba / Sha"). Flowing, the line sits under its value. */
    table.stackable td {
        box-shadow: none;
        display: flow-root;
        padding: 4px 18px;
        text-align: right;
        font-size: 14px;
    }

    table.stackable tbody tr:hover td { background: none; }

    table.stackable td::before {
        content: attr(data-label);
        float: left;
        width: 45%;
        padding-right: 14px;
        font-weight: 400;
        color: var(--muted);
        text-align: left;
    }

    /* A cell with no data-label has no label to sit beside, so it takes the full width and
       stays left-aligned rather than being shoved right by an empty 45% gutter. Alerts is
       the case that matters: none of its cells are labelled, because an alert summary
       describes itself and "Severity:" in front of "Critical" is noise. Without this the
       whole card reads as right-aligned fragments floating away from their heading. */
    table.stackable td:not([data-label]) {
        text-align: left;
    }

    table.stackable td:not([data-label])::before { content: none; }

    /* A cell whose value is a row of controls - Access's grant - takes the card's width under
       its label, because beside it the controls had 55% of a phone and ran off the edge. */
    table.stackable td.stack-full { text-align: left; }
    table.stackable td.stack-full::before { float: none; display: block; width: auto; margin-bottom: 6px; }

    /* Block, not flex. A title cell carries the summary, sometimes an inline "N affected"
       badge, and sometimes a detail line - as flex children those lay out as three columns
       and the heading loses its own line. Stacking them is what makes the row read as one
       alert instead of fragments. */
    table.stackable td.row-title {
        display: block;
        text-align: left;
        font-weight: 600;
        font-size: 16px;
        letter-spacing: 0;
        padding-top: 6px;
        padding-bottom: 6px;
    }

    /* The detail line under a title is secondary, so it drops back to body weight. */
    table.stackable td.row-title .page-sub {
        font-weight: 400;
        letter-spacing: 0;
        margin-top: 2px;
    }

    table.stackable td.row-title::before { content: none; }
}

/* An empty screen is an invitation to act, so it gets room rather than a thin line of grey. */
.empty { padding: 48px 18px; text-align: center; color: var(--muted); font-size: 14px; }

/* ---------- Buttons -------------------------------------------------------- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-height: var(--tap);
    padding: 0 18px;
    border-radius: var(--r-pill);
    border: 1px solid var(--sep-strong);
    background: var(--surface);
    color: var(--text);
    font-size: 15px;
    font-family: inherit;
    font-weight: 500;
    letter-spacing: 0;
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
}

.btn:hover { background: var(--surface-sunken); color: var(--text); text-decoration: none; }
.btn:disabled { color: var(--faint); cursor: default; }
.btn:disabled:hover { background: var(--surface); }

.btn-primary {
    background: var(--blue);
    border-color: var(--blue);
    color: #fff;
}

.btn-primary:hover { background: var(--blue); border-color: var(--blue); color: #fff; opacity: .85; }
.btn-primary:disabled { opacity: .4; }

/* The one kind of button in the product that reaches into a gaming machine. Outlined rather than
   filled: it should look deliberate to press, not alarming to look at, and it sits beside
   Acknowledge on rows people work through all night. */
.btn-danger {
    border-color: var(--red);
    color: var(--red-ink);
}

.btn-danger:hover { background: var(--red); border-color: var(--red); color: #fff; }
.btn-danger:disabled { border-color: var(--sep-strong); color: var(--faint); }
.btn-danger:disabled:hover { background: var(--surface); }

.btn-block { width: 100%; }
.btn-sm { min-height: 36px; padding: 0 14px; font-size: 13px; }

@media (min-width: 900px) {
    .btn { min-height: 34px; font-size: 14px; }
    /* 32px, the height of the period switch, the filter fields and the hour picker it sits
       beside on the reports, events, audit and cabinet pages; at 28 it hung 4px short of them. */
    .btn-sm { min-height: 32px; padding: 0 12px; font-size: 13px; }
}

/* ---------- Badges --------------------------------------------------------- */

.badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 3px 9px;
    border-radius: var(--r-pill);
    font-size: 12px;
    font-weight: 500;
    white-space: nowrap;
}

/* A lamp before the word, for the three badges that report a live condition. The
   other two label a thing rather than report its state, which is why they do not
   get one - the dot has to mean something or it is decoration. */
.badge-online::before,
.badge-offline::before,
.badge-warn::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
    flex: 0 0 auto;
}

.badge-online  { background: var(--green-tint);  color: var(--green-ink); }
.badge-offline { background: var(--red-tint);    color: var(--red-ink); }
.badge-warn    { background: var(--orange-tint); color: var(--orange-ink); }
.badge-info    { background: var(--blue-tint);   color: var(--blue-ink); }
.badge-muted   { background: var(--grey-tint);   color: var(--muted); }

/* A delivery state is not a live condition, so it does not get the lamp. Live, Beta
   and Decision describe how finished a feature is, which is a fact about the product
   and not about the floor - and the rule above is that the dot has to mean something.
   Without this, a status list would mix dotted greens and undotted blues down one
   column and imply a difference that is not there. */
.badge-status::before { display: none; }

/* The only thing in the product that moves without being asked to. It means the
   stream is being read right now, which is the one fact a still page cannot tell. */
.badge-live::before { animation: lamp 2.4s ease-in-out infinite; }

@keyframes lamp {
    0%, 100% { opacity: 1; }
    50%      { opacity: .3; }
}

@media (prefers-reduced-motion: reduce) {
    .badge-live::before { animation: none; }
}

/* ---------- Notices -------------------------------------------------------- */

.notice {
    padding: 12px 16px;
    border-radius: var(--r-control);
    font-size: 14px;
    margin-bottom: 16px;
}

.notice-accent { background: var(--orange-tint); color: var(--orange-ink); }
.notice-error  { background: var(--red-tint);    color: var(--red-ink); }
.notice-info   { background: var(--blue-tint);   color: var(--blue-ink); }

/* The result of a form the person just submitted, said next to the form rather than as a banner
   at the top of a page they have already scrolled past. */
.form-note  { margin: 10px 0 0; font-size: 13px; color: var(--muted); }
.form-error { margin: 10px 0 0; font-size: 13px; color: var(--red-ink); }
.text-danger { color: var(--red-ink); }

/* Access screen. One granted venue and the button that takes it away, on a line that wraps
   rather than pushing the table wide on a phone. */
.grant-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
.grant-controls { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; }
.grant-controls label { display: flex; flex-direction: column; gap: 5px; font-size: 13px; color: var(--muted); }

/* A button in a row of inputs is the inputs' height. The desktop .btn and .btn-sm are shorter
   than an .input, which stays at the tap height, so beside one they read as a different control
   sitting on the row's floor. */
.grant-controls .btn,
.grant-controls .btn-sm { min-height: var(--tap); font-size: 14px; }

/* The cage. Its ticket tiles sit inside a card that is already the surface, so they drop the
   tile's own padding and line up with the controls above them, with room for the number box's
   focus ring, which a tile flush beneath it painted over. The 18-digit number fits whole. The
   action row mixes a sentence, small buttons and full-height inputs, so it centres them. */
.cage-ticket { margin: 18px 0 14px; }
.cage-ticket .stat { padding: 0; }
.cage-number { width: 24ch; font-variant-numeric: tabular-nums; }
.cage-actions { align-items: center; }

/* A value in a control's place where there is nothing to choose, such as the cage's one venue: the
   same height as the input beside it, so the row's baselines still meet. */
.grant-controls .input-static {
    display: flex;
    align-items: center;
    min-height: var(--tap);
    font-size: 15px;
    color: var(--text);
}

/* The one checkbox in the grant row, which reads across rather than down like the selects
   beside it - a label stacked over a tickbox looks like a heading with a stray box under it. */
.grant-controls label.grant-floor {
    flex-direction: row;
    align-items: center;
    gap: 6px;
    min-height: var(--tap);
}

/* Access's tickboxes and Grant button, as one group that wraps below the selects together. The
   level select is as wide as its longest level on every row, so the rows line up. */
.grant-perms { display: flex; align-items: center; flex-wrap: wrap; gap: 0 12px; }
.grant-controls .grant-level { min-width: 13ch; }

/* ---------- Machine controls ----------------------------------------------- */

/* Wraps rather than scrolls: this is read on a phone standing in front of the cabinet it acts on,
   and three buttons plus their refusal reasons do not fit on one line there. */
.control-buttons {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    flex-wrap: wrap;
}

.control-button { display: inline-flex; flex-direction: column; gap: 4px; max-width: 100%; }
.control-button .page-sub { max-width: 28ch; }

/* The step between deciding and doing. Bounded so the reason field does not run the width of a
   desktop card, which would make a one-line note look like a form somebody has to fill in. */
.control-confirm { max-width: 46rem; }
.control-confirm .notice { margin-bottom: 14px; }
.control-confirm .field { margin-bottom: 12px; }
.control-confirm .field input { max-width: 22rem; }

/* Inside a table cell the buttons sit tight and the refusal becomes the button's own tooltip:
   a cell beside seven columns of money is not the place for a sentence. */
.machine-controls-inline .control-buttons { gap: 6px; }
.machine-controls-inline .page-sub { margin-top: 4px; }

/* The floor-wide pair, in the heading of the table they act on. */
.floor-controls { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* ---------- Forms ---------------------------------------------------------- */

.field { margin-bottom: 16px; }
.field label { display: block; font-size: 14px; margin-bottom: 6px; color: var(--text); }

/* Text-like inputs only: a tickbox given a full-width, tap-height box draws as a large empty
   square on a line of its own, away from the words it belongs to. */
.field input:where(:not([type="checkbox"], [type="radio"])), .field select {
    width: 100%;
    min-height: var(--tap);
    border: 1px solid var(--sep-strong);
    border-radius: var(--r-control);
    padding: 0 12px;
    background: var(--surface);
    color: var(--text);
    font-size: 16px; /* 16px or iOS zooms on focus */
    font-family: inherit;
}

.field input::placeholder { color: var(--faint); }

/* Filters collapse to one per row on a phone, two up from small tablet. */
.filter-bar {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 12px;
    padding: 4px 18px 18px;
}

.filter-bar .field { margin-bottom: 0; }

/* A tickbox in the bar reads across, beside its words, at the height of the inputs. */
.filter-bar .field label.field-check {
    display: flex;
    align-items: center;
    gap: 6px;
    min-height: 32px;
    margin-bottom: 0;
}

@media (min-width: 640px) {
    .filter-bar { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 900px) {
    .filter-bar {
        display: flex;
        flex-wrap: wrap;
        align-items: flex-end;
        gap: 12px;
    }

    /* Not a checkbox: a 170px-wide tickbox sat a field's width away from its own label. */
    .filter-bar .field input:where(:not([type="checkbox"], [type="radio"])), .filter-bar .field select {
        min-height: 32px;
        min-width: 170px;
        width: auto;
        font-size: 13px;
    }
}

/* ---------- Summary panel -------------------------------------------------- */

.summary-row { display: flex; justify-content: space-between; gap: 12px; padding: 9px 0; font-size: 14px; }
.summary-row .label { color: var(--muted); }
.summary-row .value { font-variant-numeric: tabular-nums; text-align: right; }

.summary-row.total {
    border-top: 1px solid var(--sep);
    margin-top: 8px;
    padding-top: 14px;
    font-size: 17px;
    font-weight: 600;
    letter-spacing: 0;
}

.summary-row.total .value { color: var(--orange-ink); }

/* ---------- Reports -------------------------------------------------------- */

/* align-items: start, matching .layout-split, because the panels that share a row are different
   heights by nature: a chart is a fixed 320px shell and the event list beside it is however tall
   eight rows come out. Stretching would leave the shorter card padded out with empty space below
   its content rather than ending where its content ends. */
.report-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 16px;
    align-items: start;
}

/* A column of panels rather than panels the grid places itself. Each one is only as tall as what
   is in it, so the panel below a chart starts where the chart ends rather than where the taller
   column beside it ends. align-content: start for the same reason the grid has align-items. */
.report-column {
    display: grid;
    gap: 16px;
    align-content: start;
    min-width: 0;
}

.report-panel { min-width: 0; }

/* The gap already separates these. .card + .card is written for cards stacked in the flow of a
   page and adds a second 16px here - on every panel but the first, which is how the event list
   came to sit 16px below the chart it is meant to line up with. */
.report-grid .card + .card { margin-top: 0; }

/* Overview's panels are two grids on the same columns, spaced as if they were one. */
.report-grid + .report-grid { margin-top: 16px; }

/* The ranking and the venue map: a row inside the wide column, under the financial history and no
   wider than it, that starts and ends together - the one place stretching is wanted. The map takes
   whatever height the ranking chart sets rather than a fixed one of its own; venue-map.js tells
   Leaflet when its box changes. The ranking gets the larger share for its cabinet labels. */
.report-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 16px;
    align-items: stretch;
    min-width: 0;
}
.report-row .venue-map-panel { display: flex; flex-direction: column; }
.report-row .venue-map-panel .card-body { flex: 1; display: flex; flex-direction: column; }
.report-row .venue-map { flex: 1; height: auto; min-height: 240px; }

/* The floor card on Overview, between the stat row and the panels: the grid's own gap below it. */
.overview-floor { margin-bottom: 16px; }

.chart-shell {
    width: 100%;
    min-height: 320px;
}

.chart-shell-small { min-height: 270px; }

.chart-empty {
    min-height: inherit;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--muted);
    font-size: 14px;
    text-align: center;
    padding: 20px;
}

.event-pager {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
    flex-wrap: wrap;
}

.event-pager .page-sub { white-space: nowrap; }

.event-time {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 2px;
    font-variant-numeric: tabular-nums;
}

.event-time-inline {
    flex-direction: row;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 6px;
}

.event-time-browser,
.event-time-reported {
    font-size: 12px;
    line-height: 1.25;
}

/* In a table column a time keeps its line: "Oct 28 13:48:21" broken over two lines costs the
   row its height for the sake of a column that has room to give (the Events table on a tablet). */
table.grid td .event-time > time { white-space: nowrap; }

.event-list { display: grid; }

.event-row {
    display: block;
    padding: 12px 18px;
    color: var(--text);
    box-shadow: inset 0 -1px 0 var(--sep);
}

.event-row:last-child { box-shadow: none; }
.event-row:hover { background: var(--surface-2); text-decoration: none; }

.event-row-main {
    display: flex;
    align-items: baseline;
    gap: 8px;
    flex-wrap: wrap;
}

.event-row .page-sub { display: block; margin-top: 3px; }

@media (min-width: 1200px) {
    .report-grid { grid-template-columns: minmax(0, 1fr) minmax(320px, 420px); }
    .report-panel-wide { grid-column: 1 / -1; }
    .report-row { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }

    /* A side column that takes the height of the one beside it rather than setting it: its
       contents are lifted out of the flow, so only the left column sizes the row, and the event
       list scrolls inside its card for whatever does not fit. A floor under it for while the left
       column is still loading. */
    .report-column-fit { position: relative; align-self: stretch; min-height: 560px; }

    .report-column-fill {
        position: absolute;
        inset: 0;
        display: grid;
        grid-template-rows: minmax(0, 1fr) auto;
        gap: 16px;
    }

    .report-column-fit .event-card { display: flex; flex-direction: column; min-height: 0; }
    .report-column-fit .event-card .card-body { flex: 1; min-height: 0; overflow-y: auto; }
}

/* Below the wide layout the wrapper is just a column. */
.report-column-fill { display: grid; gap: 16px; align-content: start; min-width: 0; }

/* ---------- Capability list ------------------------------------------------ */

/* A summary-row carries one figure and fits on one line. A capability carries a name,
   a state and a sentence explaining the state, and the sentence is the part that does
   the work - so this stacks instead of spreading, and keeps the explanation directly
   under the thing it explains rather than in a column beside it.

   One hairline between rows and nothing else: a list of two dozen items is exactly
   where boxes would start competing with the words in them. */
/* Capped to a readable measure rather than the width of the card. The note wraps at
   68ch whatever the window does, so without this the state pill on a desktop sits a
   thousand pixels from the name it labels, with empty space between them - and the
   eye stops associating the two.

   The cap is on the head rather than the row, so the hairline between rows still runs the width
   of the card; on the row it stopped two thirds of the way across (Beta and Delivery status). */
.capability { padding: 12px 0; }
.capability-head { max-width: 920px; }
.capability + .capability { border-top: 1px solid var(--sep); }

.capability-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 12px;
}

.capability-name { font-size: 14px; font-weight: 500; }

/* The note sits under the name, indented by nothing: the eye already has the name as
   the anchor, and an indent would push two dozen sentences off a phone. */
.capability .page-sub { margin-top: 3px; max-width: 68ch; }

/* ---------- Period switch -------------------------------------------------- */

/* A segmented control: one sunken track with the chosen segment raised out of it.
   The selection is marked by lift rather than by filling it with the action colour,
   which keeps blue meaning "this does something when you press it". */
.period-switch {
    display: inline-flex;
    background: var(--surface-sunken);
    border-radius: var(--r-control);
    padding: 2px;
    gap: 2px;
}

.period-switch button {
    min-height: 32px;
    padding: 0 14px;
    border: none;
    border-radius: 8px;
    background: none;
    color: var(--muted);
    font: inherit;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
}

.period-switch button:hover { color: var(--text); }

.period-switch button.active {
    background: var(--surface);
    color: var(--text);
    box-shadow: 0 1px 3px rgba(0, 0, 0, .1);
}

@media (prefers-color-scheme: dark) {
    /* A raised segment reads by lift on white and by value on black; the shadow does
       nothing there and only muddies the track. */
    .period-switch button.active { background: var(--sep-strong); box-shadow: none; }
}

/* The period switch and the dates behind it, stacked so that opening the range picker grows the
   page head downwards rather than pushing the switch off the side of a phone. */
.period-controls { display: flex; flex-direction: column; align-items: stretch; gap: 10px; }

.range-picker {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 12px;
    padding: 12px 14px;
    border-radius: var(--r-control);
    background: var(--surface);
}

.range-picker .field { margin-bottom: 0; }

/* On a phone the segments share the width and wrap, rather than running off the side: Overview's
   seven left Range out of reach at 375px, and the cabinet table's six its Asset. */
@media (max-width: 639px) {
    .period-switch { display: flex; flex-wrap: wrap; max-width: 100%; }
    .period-switch button { flex: 1 0 auto; padding: 0 8px; }
}

/* From the tablet up rather than the desktop: at 800px the picker opened in the flow and pushed
   the title, its buttons and the table under it down the page. */
@media (min-width: 640px) {
    /* Floats under the switch rather than opening a row in the header, which moved the title and
       its buttons the moment Range was pressed. */
    .period-controls { position: relative; }

    .range-pop {
        position: absolute;
        top: calc(100% + 6px);
        right: 0;
        z-index: 20;
        padding: 4px;
        border: 1px solid var(--sep);
        border-radius: var(--r-control);
        background: var(--surface);
        box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
        min-width: 360px;
    }

    .range-pop > .page-sub, .range-pop > .notice { margin: 0 14px 12px !important; }
}

@media (min-width: 900px) {
    /* Right-aligned under the switch it belongs to, and the inputs no wider than a date needs.
       The 16px font on the phone stays, or iOS zooms the whole page on focus. */
    .period-controls { align-items: flex-end; }
    .range-picker .field input { min-height: 32px; min-width: 150px; width: auto; font-size: 13px; }
}

/* The hour the cabinet drill-down is showing. A date, an hour and two steppers, which is the
   whole control: the bucket is a UTC hour by design, so a free-text local time would be a lie on
   any venue whose offset is not a whole number of hours. */
.hour-picker { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }

.hour-picker input, .hour-picker select {
    min-height: var(--tap);
    border: 1px solid var(--sep-strong);
    border-radius: var(--r-control);
    padding: 0 10px;
    background: var(--surface);
    color: var(--text);
    font-size: 16px; /* 16px or iOS zooms on focus */
    font-family: inherit;
}

@media (min-width: 900px) {
    .hour-picker input, .hour-picker select { min-height: 32px; font-size: 13px; }
}

/* A line of one meter over the retention window. No axis and no legend: the question it answers
   is "did this counter move, and did it ever fall", which a shape answers and a grid does not. */
.sparkline { display: block; width: 100%; height: 120px; overflow: visible; }

.sparkline path { fill: none; stroke: var(--blue); stroke-width: 2; stroke-linejoin: round; }

.sparkline .drop { stroke: var(--red); }

/* ---------- Sign-in -------------------------------------------------------- */

/* The blue gradient wash that used to fill this screen is gone. The card on the plain
   page ground is the whole design, and this is the one screen in the product with
   enough room to be quiet. */
.signin-shell {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--page);
    padding: 24px 16px;
}

.signin-card {
    width: 100%;
    max-width: 380px;
    background: var(--surface);
    border-radius: 18px;
    padding: 32px 28px;
}

@media (min-width: 640px) {
    .signin-card { padding: 40px 36px; }
}

.signin-card .brand { justify-content: center; margin-bottom: 6px; }
.signin-card .brand-mark { font-size: 24px; }

.signin-card .signin-title {
    text-align: center;
    color: var(--muted);
    font-size: 14px;
    margin-bottom: 28px;
}

/* A rule with a word in the middle, between the password form and the passkey button. */
.signin-or {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 18px 0;
    color: var(--muted);
    font-size: 13px;
}

.signin-or::before,
.signin-or::after {
    content: "";
    flex: 1;
    border-top: 1px solid var(--sep-strong);
}

.signin-details { margin-top: 18px; font-size: 14px; }
.signin-details summary { cursor: pointer; color: var(--muted); margin-bottom: 12px; }
.signin-back { margin-top: 18px; text-align: center; font-size: 13px; }

/* Where passkeys.js says why a prompt did not finish. Empty, it takes no room. */
.passkey-status { margin-top: 8px; font-size: 13px; color: var(--red-ink); text-align: center; }
.passkey-status:empty { display: none; }

.signin-footer {
    margin-top: 20px;
    text-align: center;
    font-size: 12px;
    color: var(--muted);
}

/* ---------- Blazor error UI ------------------------------------------------ */

#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1000;
    background: var(--surface);
    border-top: 1px solid var(--sep-strong);
    padding: 14px var(--gutter);
    font-size: 14px;
    color: var(--text);
}

#blazor-error-ui .dismiss { cursor: pointer; float: right; }

/* ---------- Footer --------------------------------------------------------- */

.site-footer {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 8px;
    padding: 24px var(--gutter) 40px;
    font-size: 12px;
    color: var(--muted);
}

.site-footer .sep { color: var(--sep-strong); }

/* Cursor hint that the tooltip carries the full revision. */
.site-footer .version { font-family: var(--mono); cursor: help; }

/* ---------- Claim controls ------------------------------------------------- */

.claim-venue {
    min-height: 36px;
    min-width: 170px;
    border: 1px solid var(--sep-strong);
    border-radius: var(--r-control);
    padding: 0 10px;
    background: var(--surface);
    font-size: 14px;
    font-family: inherit;
    color: var(--text);
}

@media (min-width: 900px) {
    .claim-venue { min-height: 30px; font-size: 13px; }
}

/* ---------- Ledgers --------------------------------------------------------

   The reports page, and the one place in this product that deliberately looks like a
   spreadsheet.

   Everything else here separates with whitespace and spends a hairline only where a
   boundary has to be explicit. A ledger is the opposite job: forty rows of one cabinet
   each, read by somebody tracking a figure across the page with a finger, and the
   generous rhythm that makes a summary card calm makes a forty-row ledger something you
   have to scroll to compare the ends of. So the rows are tight, the numbers are tabular,
   nothing wraps, and alternating rows carry the faintest possible fill - which does the
   work the row hairline does elsewhere, without drawing forty lines.

   It is a modifier on table.grid rather than a table of its own, so the sort headers,
   the num alignment and the mono identifier cells all still apply. And it is never
   .stackable: these are twelve and fifteen columns wide by nature, the same way the audit
   log is, and stacking fifteen labelled rows per cabinet produces a page nobody can scan.
   Sideways is the honest answer. */

table.grid.ledger { font-size: 12.5px; width: auto; min-width: 100%; }

/* Pinned to the top of the scroller below, because a ledger is taller than the screen and
   a column of numbers with the heading scrolled off is not evidence of anything. The
   background is its own: a sticky cell is transparent by default and the rows would run
   underneath it. */
table.grid.ledger thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--surface);
    padding: 6px 10px 6px;
    font-size: 11px;
    line-height: 1.3;
    white-space: nowrap;
    box-shadow: inset 0 -1px 0 var(--sep-strong);
}

/* Every cell is one number, one label or one instant. Wrapping any of them would cost
   height on every row of forty to save width in one column. */
table.grid.ledger td {
    padding: 3px 10px;
    box-shadow: none;
    white-space: nowrap;
    vertical-align: baseline;
    line-height: 1.45;
}

/* The zebra, and it is as faint as the palette allows on purpose: it is there to keep an
   eye on one row across fifteen columns, not to stripe the table. */
table.grid.ledger tbody tr:nth-child(even) td { background: var(--surface-2); }
table.grid.ledger tbody tr:hover td { background: var(--surface-sunken); }

/* The cabinet pinned while the figures scroll past it, below the width where the ledger fits. On
   a phone the scroller showed Cabinet and Site and nothing else, and scrolling across to the money
   left no way to tell whose row it was (2026-10-30 walk). The meter grid pins its time the same
   way. Only the first heading row: a heading under a bracket is a figure's, not the cabinet's. */
@media (max-width: 899px) {
    table.grid.ledger tbody td:first-child,
    table.grid.ledger thead tr:first-child th:first-child {
        position: sticky;
        left: 0;
        z-index: 1;
        background: var(--surface);
        box-shadow: inset -1px 0 0 var(--sep);
    }

    table.grid.ledger thead tr:first-child th:first-child { z-index: 3; }
    table.grid.ledger tbody tr:nth-child(even) td:first-child { background: var(--surface-2); }
    table.grid.ledger tbody tr:hover td:first-child { background: var(--surface-sunken); }
}

/* Tabular figures everywhere, not only in .num. A cabinet label is often a number, and a
   column of asset numbers that do not line up is the one column somebody is scanning. */
table.grid.ledger td, table.grid.ledger th { font-variant-numeric: tabular-nums; }

/* The row the columns add up to. A rule above it and a weight on it, and it stays on the
   plain surface through the zebra - a total that happens to land on an even row and gets
   tinted reads as one more cabinet. */
table.grid.ledger tbody tr.ledger-total td,
table.grid.ledger tbody tr.ledger-total:hover td {
    background: var(--surface);
    font-weight: 600;
    border-top: 1px solid var(--sep-strong);
    padding-top: 6px;
    padding-bottom: 6px;
}

/* A heading that spans a block of columns - "Money in", "Money out" - sitting above the
   individual ones. Centred and faint: it is a bracket rather than a column of its own. */
table.grid.ledger thead th.ledger-span {
    text-align: center;
    color: var(--faint);
    box-shadow: none;
    padding-bottom: 2px;
}

/* The secondary line inside a cell: a serial under a label, a meter name under its code.
   Faint and one step smaller, and it is the only thing on a ledger row that adds height. */
.ledger-sub { color: var(--faint); font-size: 11px; }
.ledger-change { display: block; }

/* A cell whose figure is not merely zero but unheard-of. Same colour the meter grid uses
   for an hour nobody read, and for the same reason. */
.ledger-absent { color: var(--faint); }

/* A word after a cabinet's name: spaced off it, or "123no data" reads as one word. */
.ledger-tag { margin-left: 6px; }

/* The scroller. Bounded in height so the sticky header has something to stick to, and
   scrolling in both directions: down through the cabinets, across through the columns. */
.ledger-scroll {
    overflow: auto;
    -webkit-overflow-scrolling: touch;
    max-height: min(72vh, 900px);
}

/* ---------- Report chrome --------------------------------------------------- */

/* The report picker. The same object as .period-switch - a segmented control of mutually
   exclusive choices - but there are eight of them and they carry words rather than
   single-syllable spans, so it wraps rather than scrolling and each button keeps its
   whole label. */
.report-switch {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    padding: 3px;
    background: var(--surface-sunken);
    border-radius: var(--r-control);
}

.report-switch button {
    min-height: 34px;
    padding: 0 12px;
    border: 0;
    border-radius: 7px;
    background: none;
    color: var(--muted);
    font-family: inherit;
    font-size: 13px;
    cursor: pointer;
    white-space: nowrap;
}

.report-switch button:hover { color: var(--text); }

.report-switch button.active {
    background: var(--surface);
    color: var(--text);
    box-shadow: 0 1px 2px rgb(0 0 0 / .08);
}

@media (prefers-color-scheme: dark) {
    .report-switch button.active { box-shadow: none; background: var(--surface-2); }
}

/* .input and .input-sm, which have been written on the event report's two category and
   severity selects since that screen was built and matched nothing in this sheet - so both
   dropdowns have been rendering as raw browser controls next to styled buttons. Defined
   here because the reports page needs the same thing several times over, and defining it
   fixes the older screen on the way past. */

/* A row of controls styles its bare inputs and selects the same way, so a form written without
   the class does not render as browser controls a third the height of the button beside them
   (Admin's venue forms, Access's, Ticket reports' dates). Only the class-less ones: a control
   with a class of its own, such as .ledger-search, keeps it. */
.input,
:where(.grant-controls) input:where(:not([type="checkbox"], [type="radio"])):not([class]),
:where(.grant-controls) select:not([class]) {
    min-height: var(--tap);
    border: 1px solid var(--sep-strong);
    border-radius: var(--r-control);
    padding: 0 10px;
    background: var(--surface);
    font-family: inherit;
    font-size: 14px;
    color: var(--text);
}

.input-sm { min-height: 36px; font-size: 13px; }

@media (min-width: 900px) {
    .input-sm { min-height: 30px; }
}

/* The event-type checklist. In the flow on a phone, like the range picker, and a popover under
   its button from the tablet breakpoint up. */
.type-picker { position: relative; }

.type-picker-toggle { cursor: pointer; text-align: left; }

.type-pop {
    margin-top: 6px;
    border: 1px solid var(--sep);
    border-radius: var(--r-control);
    background: var(--surface);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}

.type-pop-head, .type-pop-foot {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 12px;
}

.type-pop-head { border-bottom: 1px solid var(--sep); }
.type-pop-foot { border-top: 1px solid var(--sep); }
.type-pop-head .ledger-search { min-height: 32px; max-width: none; }

.type-pop-body { max-height: 50vh; overflow-y: auto; padding: 4px 12px 8px; }

.type-group { padding: 6px 0; }
.type-group + .type-group { border-top: 1px solid var(--sep); }

.type-group-head, .type-option {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 28px;
    font-size: 13px;
    cursor: pointer;
}

.type-group-head { font-weight: 600; }
.type-group-head .page-sub { font-weight: 400; margin-left: auto; }
.type-option { padding-left: 22px; }
.type-option .mono { margin-left: auto; }

/* The notifications page's copy of the type checklist: inline in a card rather than in a popover,
   and scrolled on its own so the Save button under it stays in reach. */
.notify-types {
    max-height: 50vh;
    overflow-y: auto;
    margin-top: 10px;
    padding: 4px 12px 8px;
    border: 1px solid var(--sep);
    border-radius: var(--r-control);
}

/* "More", under the short list, with how many ticked types it is hiding. */
.notify-more { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 8px; }

/* It sits inside a .field, whose label rule would otherwise make each row a block. */
.notify-types .type-group-head, .notify-types .type-option {
    display: flex;
    margin-bottom: 0;
    font-size: 13px;
}

/* Short enough on a phone that Apply is on the screen with the list, not under it. */
@media (max-width: 639px) {
    .type-pop-body { max-height: 40vh; }
}

@media (min-width: 640px) {
    .type-pop {
        position: absolute;
        top: 100%;
        right: 0;
        z-index: 20;
        width: 420px;
    }
}

/* The filter row under the picker: a search box, a site, and whatever the chosen report
   needs. Wraps on a phone and sits on one line from the tablet breakpoint up. */
.report-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-top: 12px;
}

.report-filters .input, .report-filters .ledger-search { min-height: 36px; }

.report-filters label { font-size: 13px; color: var(--muted); }

.ledger-search {
    flex: 1 1 200px;
    max-width: 320px;
    border: 1px solid var(--sep-strong);
    border-radius: var(--r-control);
    padding: 0 12px;
    background: var(--surface);
    font-family: inherit;
    font-size: 14px;
    color: var(--text);
}

.ledger-search::placeholder { color: var(--faint); }

/* The line between the heading and the table saying what the report covers and what it
   cannot. Distinct from .page-sub because it is a paragraph rather than a caption. */
.ledger-note {
    padding: 10px 18px;
    font-size: 13px;
    color: var(--muted);
    border-top: 1px solid var(--sep);
}

.ledger-note strong { color: var(--text); font-weight: 500; }

/* What the report is, for whom and when - on the report, so it travels with a print or a
   screenshot. Label column in muted ink, the facts in full ink. */
.report-heading {
    padding: 14px 18px 12px;
    border-top: 1px solid var(--sep);
}

.report-heading h2 {
    margin: 0 0 6px;
    font-size: 17px;
    font-weight: 600;
}

.report-heading dl {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 2px 12px;
    margin: 0;
    font-size: 14px;
}

.report-heading dt { color: var(--muted); }
.report-heading dd { margin: 0; color: var(--text); overflow-wrap: anywhere; }

/* Ten pixels a side is room a wide screen can spare. On a tablet it is what pushed the
   ranking, Free play and Denominations ledgers 7-17 px past their card, a sideways scroll for
   the last column alone. */
@media (max-width: 1199px) {
    table.grid.ledger td,
    table.grid.ledger thead th { padding-left: 6px; padding-right: 6px; }
}

@media (min-width: 900px) {
    table.grid.ledger { font-size: 13px; }
    .report-switch button { min-height: 30px; }
    .report-filters .input, .report-filters .ledger-search { min-height: 32px; }
}

/* ---------- Venue map ------------------------------------------------------ */
/* Leaflet draws the map; everything here is ours. The basemap is greyscale (see venue-map.js),
   so the pins are the only colour on it and use the same four hues as every status pill. */
.venue-map {
    width: 100%;
    height: 340px;
    border-radius: var(--r-control);
    background: var(--surface-sunken);
    /* Leaflet's panes sit at z-index 400 and up; this keeps them under the sticky header. */
    isolation: isolate;
    z-index: 0;
}

.venue-map-compact { height: 180px; }

.venue-map .chart-empty { min-height: 100%; }

.venue-map.leaflet-container {
    font: 12px/1.4 var(--font);
    background: var(--surface-sunken);
}

.venue-pin span {
    display: block;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--faint);
    border: 3px solid var(--surface);
    box-shadow: 0 0 0 1px rgba(0, 0, 0, .18), 0 2px 6px rgba(0, 0, 0, .3);
    transition: transform .15s ease;
}

.venue-pin:hover span,
.venue-pin:focus span { transform: scale(1.25); }

.venue-pin-ok span { background: var(--green); }
.venue-pin-warn span { background: var(--orange); }
.venue-pin-down span { background: var(--red); }
.venue-pin-pick span { background: var(--blue); cursor: grab; }

/* Venues whose pins would cover each other at this zoom, drawn as one in the worst state among
   them, with how many. */
.venue-pin-group span {
    width: 26px;
    height: 26px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font: 700 11px/1 var(--font);
}

.venue-pin-idle.venue-pin-group span { background: var(--faint); }

.venue-popup-list {
    list-style: none;
    margin: 6px 0 0;
    padding: 0;
}

.venue-popup-list li { padding: 2px 0; }

.venue-popup-list i {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-right: 6px;
    background: var(--faint);
}

.venue-popup-list i.status-ok { background: var(--green); }
.venue-popup-list i.status-warn { background: var(--orange); }
.venue-popup-list i.status-down { background: var(--red); }

/* A pulse round a venue whose devices are all down: the one pin on the map that wants somebody. */
.venue-pin-down span { animation: venue-pin-pulse 1.8s ease-out infinite; }

@keyframes venue-pin-pulse {
    0%   { box-shadow: 0 0 0 1px rgba(0, 0, 0, .18), 0 0 0 0 color-mix(in srgb, var(--red) 55%, transparent); }
    100% { box-shadow: 0 0 0 1px rgba(0, 0, 0, .18), 0 0 0 14px transparent; }
}

@media (prefers-reduced-motion: reduce) {
    .venue-pin-down span { animation: none; }
}

.venue-map .venue-label {
    background: var(--surface);
    color: var(--text);
    border: 0;
    border-radius: var(--r-pill);
    padding: 2px 9px;
    font-weight: 600;
    box-shadow: 0 1px 4px rgba(0, 0, 0, .2);
}

.venue-map .venue-label::before { display: none; }

.venue-map .leaflet-popup-content-wrapper,
.venue-map .leaflet-popup-tip {
    background: var(--surface);
    color: var(--text);
    box-shadow: 0 4px 18px rgba(0, 0, 0, .25);
}

.venue-map .leaflet-popup-content-wrapper { border-radius: var(--r-control); }
.venue-map .leaflet-popup-content { margin: 12px 14px; }

.venue-popup strong { font-size: 14px; }
.venue-popup-sub { color: var(--muted); }

.venue-popup dl {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 2px 12px;
    margin: 8px 0;
}

.venue-popup dt { color: var(--muted); }
.venue-popup dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }
.venue-popup dd.status-ok { color: var(--green-ink); }
.venue-popup dd.status-warn { color: var(--orange-ink); }
.venue-popup dd.status-down { color: var(--red-ink); }
.venue-popup a { color: var(--blue-ink); font-weight: 600; text-decoration: none; }

.venue-map .leaflet-bar a {
    background: var(--surface);
    color: var(--text);
    border-bottom-color: var(--sep);
}

.venue-map .leaflet-control-attribution {
    background: color-mix(in srgb, var(--surface) 80%, transparent);
    color: var(--muted);
}

.venue-map .leaflet-control-attribution a { color: var(--muted); }

.venue-map-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 14px;
    font-size: 12px;
    color: var(--muted);
}

.venue-map-legend i {
    display: inline-block;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    margin-right: 5px;
    background: var(--faint);
}

.venue-map-legend .ok i { background: var(--green); }
.venue-map-legend .warn i { background: var(--orange); }
.venue-map-legend .down i { background: var(--red); }

@media (max-width: 767px) {
    .venue-map:not(.venue-map-compact) { height: 260px; }
}

.venue-location-address { flex: 1 1 280px; }
.venue-location-address input { width: 100%; }

.venue-location-candidates {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
    margin: 12px 0 0;
}

.venue-location-candidates .btn { text-align: left; white-space: normal; height: auto; }

.venue-location-map { height: 380px; margin-top: 14px; }

.venue-location-foot { margin-top: 12px; align-items: center; }
.venue-location-foot .spacer { margin-left: auto; }

/* ---------- Floor map ------------------------------------------------------ */

/* One tile per cabinet, coloured by what it is doing now. Each status sets two
   variables - --slot-ink for the icon, label and border, --slot-ground for the
   fill - so the tile and its legend lamp can never disagree. */

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

.floor-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 14px;
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: 12px;
    color: var(--muted);
}

.floor-legend i {
    display: inline-block;
    width: 9px;
    height: 9px;
    margin-right: 5px;
    border-radius: 3px;
    background: var(--slot-ink);
}

.floor-legend .slot-disable-sent i {
    background: repeating-linear-gradient(135deg, var(--muted) 0 2px, transparent 2px 4px);
    box-shadow: inset 0 0 0 1px var(--muted);
}

.floor-legend .slot-service i {
    border-radius: 50%;
    background: var(--orange-ink);
}

.floor-area + .floor-area { margin-top: 18px; }

.floor-area h3 {
    margin: 0 0 8px;
    font-size: 13px;
    font-weight: 600;
    color: var(--muted);
}

.floor-area h3 span { font-weight: 400; color: var(--faint); }

.floor-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(76px, 1fr));
    gap: 8px;
}

.slot {
    --slot-ink: var(--muted);
    --slot-ground: var(--surface-2);

    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    min-height: var(--tap);
    min-width: 0;
    padding: 8px 6px 7px;
    border: 1.5px solid color-mix(in srgb, var(--slot-ink) 45%, transparent);
    border-radius: var(--r-control);
    background: var(--slot-ground);
    color: var(--text);
    text-align: center;
    transition: transform .12s ease, box-shadow .12s ease;
}

.slot:hover,
.slot:focus-visible {
    transform: translateY(-1px);
    box-shadow: 0 3px 10px rgba(0, 0, 0, .12);
}

.slot-icon { width: 26px; height: 26px; color: var(--slot-ink); }

.slot-label {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 12px;
    font-weight: 600;
}

.slot-game {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 10.5px;
    color: var(--muted);
}

/* The period's net win: quiet, because the tile's colour is about now and this is not. */
.slot-win {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 10px;
    color: var(--faint);
}

.slot-win-loss { color: color-mix(in srgb, var(--red-ink) 70%, var(--faint)); }

.slot-flag {
    position: absolute;
    top: 3px;
    right: 5px;
    font-size: 11px;
    font-weight: 700;
    color: var(--slot-ink);
}

.slot-handpay   { --slot-ink: var(--red-ink);    --slot-ground: var(--red-tint); }
.slot-out       { --slot-ink: var(--red-ink);    --slot-ground: color-mix(in srgb, var(--red-tint) 55%, var(--surface)); }
.slot-power     { --slot-ink: var(--red-ink);    --slot-ground: color-mix(in srgb, var(--red-tint) 55%, var(--surface)); }
.slot-offline   { --slot-ink: var(--faint);      --slot-ground: var(--surface-sunken); }
.slot-not-answering { --slot-ink: var(--orange-ink); --slot-ground: var(--orange-tint); }
.slot-attention { --slot-ink: var(--orange-ink); --slot-ground: color-mix(in srgb, var(--orange-tint) 55%, var(--surface)); }
.slot-play      { --slot-ink: var(--green-ink);  --slot-ground: var(--green-tint); }
.slot-idle      { --slot-ink: var(--muted);      --slot-ground: var(--surface); }
.slot-never     { --slot-ink: var(--faint);      --slot-ground: transparent; }
.slot-no-device { --slot-ink: var(--faint);      --slot-ground: var(--surface-sunken); }

.slot.slot-offline,
.slot.slot-never,
.slot.slot-no-device { border-style: dashed; }
.slot.slot-never { color: var(--muted); }

/* The one tile on the floor that has a customer waiting at it. */
.slot.slot-handpay { border-color: var(--red); animation: slot-handpay 1.8s ease-out infinite; }

@keyframes slot-handpay {
    0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--red) 45%, transparent); }
    100% { box-shadow: 0 0 0 8px transparent; }
}

@media (prefers-reduced-motion: reduce) {
    .slot.slot-handpay { animation: none; }
}

/* A disable that was sent, never one that was confirmed: hatched over whatever the
   cabinet is observed to be doing, so the observed state still shows through. */
.slot.slot-disable-sent {
    background:
        repeating-linear-gradient(135deg,
            color-mix(in srgb, var(--muted) 18%, transparent) 0 4px,
            transparent 4px 9px),
        var(--slot-ground);
}

/* A printer or bill fault: a corner mark over the state, which it does not change. */
.slot-service-mark {
    position: absolute;
    top: 5px;
    left: 5px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--orange-ink);
    box-shadow: 0 0 0 2px var(--slot-ground);
}

@media (max-width: 767px) {
    .floor-grid { grid-template-columns: repeat(auto-fill, minmax(68px, 1fr)); gap: 6px; }
}

/* ---------- Sign-in security ----------------------------------------------- */

.security-lede { max-width: 68ch; margin: 12px 0 16px; }
.security-list { margin-bottom: 16px; }
.security-add { max-width: 26rem; }
.security-add .passkey-status { text-align: left; }

.authenticator-steps { margin: 12px 0 0; padding-left: 20px; display: grid; gap: 24px; }
.authenticator-steps form { max-width: 16rem; margin-top: 10px; }
.authenticator-qr { width: 200px; height: 200px; margin: 12px 0; }
.authenticator-qr svg { width: 100%; height: 100%; display: block; }
.authenticator-key { margin-top: 4px; font-size: 15px; color: var(--text); word-break: break-all; }

.recovery-codes {
    list-style: none;
    padding: 0;
    margin: 0 0 20px;
    display: grid;
    grid-template-columns: repeat(2, max-content);
    gap: 8px 32px;
    font-size: 16px;
}
