/* ==========================================================================
   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%;
}

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

/* ---------- 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; }

.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-sub { color: var(--muted); font-size: 13px; }

/* 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)); }
}

/* 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;
}

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

/* ---------- 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; }

    table.stackable td {
        box-shadow: none;
        display: flex;
        justify-content: space-between;
        /* Baseline, not the default stretch: a cell holding text plus a badge would
           otherwise pull the badge to full cell height and render a pill as a tall oval. */
        align-items: baseline;
        gap: 14px;
        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);
        font-weight: 400;
        color: var(--muted);
        text-align: left;
        flex: 0 0 45%;
    }

    /* 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]) {
        justify-content: flex-start;
        text-align: left;
    }

    table.stackable td:not([data-label])::before { content: none; }

    /* 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; }

    table.stackable td {
        box-shadow: none;
        display: flex;
        justify-content: space-between;
        /* Baseline, not the default stretch: a cell holding text plus a badge would
           otherwise pull the badge to full cell height and render a pill as a tall oval. */
        align-items: baseline;
        gap: 14px;
        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);
        font-weight: 400;
        color: var(--muted);
        text-align: left;
        flex: 0 0 45%;
    }

    /* 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]) {
        justify-content: flex-start;
        text-align: left;
    }

    table.stackable td:not([data-label])::before { content: none; }

    /* 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; }
    .btn-sm { min-height: 28px; 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); }

/* 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);
}

/* ---------- 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); }

.field input, .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; }

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

    .filter-bar .field input, .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; }

.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 {
    font-size: 12px;
    line-height: 1.25;
}

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

/* ---------- 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. */
.capability { padding: 12px 0; 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; }

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

.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); }

/* 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; }

/* 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); }

/* 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. */

.input {
    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 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; }

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