/* ============================================================================
   HotelTRS component styles - the Trs* primitives
   ----------------------------------------------------------------------------
   RULES ONLY. No variables, no literal colours - every value is a --trs-* token.

     variables                  -> css/tokens.css
     fonts, resets, focus       -> css/base.css
     app-specific overrides     -> app.css

   Loads AFTER app.css so .trs-* wins, and after Bootstrap 5.3.2 so the
   --bs-table-cell-padding-* override in .trs-grid takes effect.

   Why this file exists at all: the primitives are pure Blazorise components
   with zero raw HTML, so they have no elements for scoped .razor.css to attach
   to (Blazor's scoped-CSS rewriter stamps b-xxxxx onto HTML elements only,
   never onto child-component tags). Classes referenced via Class= are the only
   workable route.
   ============================================================================ */

/* ----------------------------------------------------------------------------
   TrsPageShell / TrsPageHeader
   ---------------------------------------------------------------------------- */

.trs-page {
    display: flex;
    flex-direction: column;
    gap: var(--trs-space-4);
}

.trs-page-header {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--trs-space-3) var(--trs-space-5);
    padding-bottom: var(--trs-space-3);
    border-bottom: 1px solid var(--trs-border);
}

.trs-page-header h1 {
    font-size: var(--trs-text-3xl);
    font-weight: var(--trs-weight-bold);
    letter-spacing: -.03em;
    line-height: 1.1;
    margin: 0;
    /* --trs-text-primary, not --trs-text: the page name is the one piece of type
       on a page that should carry the brand. p-700 light / p-600 dark, both of
       which the design system already certifies for TEXT rather than for fills -
       and at 34px this is large text, so it clears AA with room to spare.

       Applies to every TrsPageShell/TrsPageHeader page on purpose - 60 of them
       as of Sept 2026, up from 9 when this was written. A heading
       colour that differed per page would be worse than either choice. */
    color: var(--trs-text-primary);
}

.trs-page-subtitle {
    color: var(--trs-text-secondary);
    margin: var(--trs-space-1) 0 0;
}

.trs-page-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--trs-space-2);
}

/* ----------------------------------------------------------------------------
   TrsCard
   Not everything is a card - spend border, fill, radius and shadow by role.
   ---------------------------------------------------------------------------- */

.trs-card {
    border-radius: var(--trs-radius-lg);
    border: 1px solid var(--trs-border);
    background: var(--trs-surface);
    box-shadow: var(--trs-elevation-1);
}

.trs-card--flat {
    box-shadow: var(--trs-elevation-0);
}

.trs-card--raised {
    box-shadow: var(--trs-elevation-2);
}

.trs-card--sunken {
    background: var(--trs-surface-sunken);
    box-shadow: var(--trs-elevation-0);
}

.trs-card-icon {
    color: var(--trs-text-primary);
    margin-right: var(--trs-space-2);
}

/* ----------------------------------------------------------------------------
   TrsToolbar
   ---------------------------------------------------------------------------- */

.trs-toolbar {
    display: flex;
    flex-direction: column;
    gap: var(--trs-space-2);
    margin-bottom: var(--trs-space-2);
}

.trs-toolbar-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--trs-space-2);
    width: 100%;
}

.trs-toolbar-search {
    margin-left: auto;
}

.trs-filter-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--trs-space-2);
    width: 100%;
}

/* A bare <Select> dropped into the filter row.

   Blazorise renders .form-select at width:100%, and as a direct flex item with
   no basis that resolves to the WHOLE ROW - MEASURED on /TouristGuide, where the
   category filter ran the full page width with the search pill stranded above
   it. Shrink it to its content with a floor, so a long category name still fits.

   Direct children only. A Select nested inside a <Field> - the price grid's
   channel and rate pickers - takes its width from the Field's own min-width
   class, and must keep doing so. */
.trs-filter-row > .form-select {
    width: auto;
    min-width: 12rem;
}

/* ----------------------------------------------------------------------------
   A grid too wide for its column budget

   /Receipts carries fourteen columns and cannot be squeezed into the viewport
   without the numbers colliding, so it scrolls sideways inside its own frame
   rather than pushing the page out. That was two nested Divs carrying
   `Style="overflow-x: auto; width: 100%"` and `Style="min-width: 1250px"` at the
   call site - fixed geometry with no business being inline.

   1250px is the measured minimum at which the money columns stop wrapping; it is
   kept exactly as it was rather than re-derived.
   ---------------------------------------------------------------------------- */




/* ----------------------------------------------------------------------------
   TrsStatTile - the figure that heads a dashboard

   Replaces about fifty hand-built copies of icon + Heading.Is2 + muted caption
   in a centred Card. The tone is set once on the tile and inherited by the icon
   and the number, instead of being spelled out on each of them.

   NOT a Blazorise Card. A Card brings a header/body/footer structure this does
   not use, and its CardBody padding is sized for prose; a tile is one number.
   Tokens do the rest, so it follows the theme with no dark override.
   ---------------------------------------------------------------------------- */

.trs-stat-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--trs-space-1);
    padding: var(--trs-space-4);
    height: 100%;
    background: var(--trs-surface);
    border: 1px solid var(--trs-border);
    border-radius: var(--trs-radius-md);
    box-shadow: var(--trs-elevation-1);
    text-align: center;
}

.trs-stat-icon {
    font-size: 1.75rem;
    line-height: 1;
    color: var(--trs-text-secondary);
}

/* Tabular figures so a column of tiles does not jitter as the numbers change,
   and so 111 is visibly wider than 11 rather than accidentally the same width. */
.trs-stat-value {
    font-size: var(--trs-text-2xl);
    font-weight: var(--trs-weight-bold);
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
    color: var(--trs-text);
}

.trs-stat-label {
    font-size: var(--trs-text-sm);
    color: var(--trs-text-secondary);
}

/* The tone colours the icon and the figure together. Neutral leaves both at the
   defaults above - a count with no status attached should not be tinted. */
.trs-stat-tile--primary .trs-stat-icon,
.trs-stat-tile--primary .trs-stat-value {
    color: var(--trs-text-primary);
}

.trs-stat-tile--success .trs-stat-icon,
.trs-stat-tile--success .trs-stat-value {
    color: var(--trs-success);
}

.trs-stat-tile--info .trs-stat-icon,
.trs-stat-tile--info .trs-stat-value {
    color: var(--trs-info);
}

.trs-stat-tile--warning .trs-stat-icon,
.trs-stat-tile--warning .trs-stat-value {
    color: var(--trs-warning);
}

.trs-stat-tile--danger .trs-stat-icon,
.trs-stat-tile--danger .trs-stat-value {
    color: var(--trs-danger);
}

/* ----------------------------------------------------------------------------
   View switcher

   The dropdown that picks which collection a page is showing. It sits in the
   filter row beside the search, so it has to read as a control of the same
   family - and it has to stand off the page ground, which Color.Light did not:
   .btn-light is a near-white solid on a near-white --trs-bg.

   The stronger border rather than a fill does that work. A fill would put it in
   competition with the Create button next to it, and there is only one primary
   action per row.
   ---------------------------------------------------------------------------- */

/* THE SELECTOR IS FOUR CLASSES DEEP BECAUSE IT HAS TO BE. MEASURED TWICE.

   First attempt, `.trs-view-switcher` at (0,1,0): tied `.btn` and lost on
   document order, so the font came out 16px against the search field's 14px.

   Second attempt, `.trs-filter-row .trs-view-switcher` at (0,2,0): font and
   radius applied, but the border stayed near-black and the shadow never
   appeared. The reason is in blazorise.bootstrap5.css:

       .dropdown .btn.dropdown-toggle:not(.btn-outline) {
           border-color: currentcolor; box-shadow: none;
       }

   which is (0,4,0) - and `currentcolor` is why the border took --trs-text
   rather than any border token. Matching its shape and adding .trs-page puts
   this at (0,5,0), which wins outright rather than on load order. */
.trs-page .trs-filter-row .dropdown .btn.trs-view-switcher {
    display: inline-flex;
    align-items: center;
    gap: var(--trs-space-2);
    min-height: var(--trs-control-h);
    padding: var(--trs-space-2) var(--trs-space-4);
    color: var(--trs-text);
    background-color: var(--trs-surface);
    border: 1px solid var(--trs-border-strong);
    border-radius: var(--trs-radius-full);
    font-size: var(--trs-text-base);
    font-weight: var(--trs-weight-medium);
    /* The pill and the resting shadow are what make this read as a control
       rather than as text on the page ground - the flat hairline version
       disappeared into --trs-bg. */
    box-shadow: var(--trs-elevation-1);
    transition: border-color var(--trs-motion-fast) var(--trs-ease),
                background-color var(--trs-motion-fast) var(--trs-ease),
                box-shadow var(--trs-motion-fast) var(--trs-ease),
                color var(--trs-motion-fast) var(--trs-ease);
}

.trs-page .trs-filter-row .dropdown .btn.trs-view-switcher:hover,
.trs-page .trs-filter-row .dropdown .btn.trs-view-switcher:focus-visible,
.trs-page .trs-filter-row .dropdown .btn.trs-view-switcher[aria-expanded="true"] {
    color: var(--trs-text-primary);
    background-color: var(--trs-p-50);
    border-color: var(--trs-p-400);
    box-shadow: var(--trs-elevation-2);
}

/* Open state reads as held-down rather than hovered: the ring says "this menu
   belongs to this button" while the menu is on screen. */
.trs-page .trs-filter-row .dropdown .btn.trs-view-switcher[aria-expanded="true"] {
    background-color: var(--trs-p-100);
    box-shadow: 0 0 0 3px var(--trs-p-100);
}

/* The glyph leads the label and carries the brand tint, so the control has some
   colour in it at rest instead of being an outline around grey text. */
.trs-page .trs-filter-row .dropdown .btn.trs-view-switcher > .fas {
    color: var(--trs-text-primary);
}

@media (prefers-reduced-motion: reduce) {
    .trs-view-switcher {
        transition: none;
    }
}

/* ----------------------------------------------------------------------------
   SearchComponent

   Not a Trs* primitive - it lives in Components/Utilities with the app's other
   shared input widgets - but it renders on 42 pages, so its rules belong here
   rather than in any one feature sheet.

   Modelled on .trs-auth-input in auth.css, the verified pattern for "one field
   box with controls floated over it". What it replaces was a Bootstrap
   input-group: three abutting boxes with visible seams.

   WHY THE BOX IS PAINTED ON THE INPUT AND NOT ON THE WRAPPER

   dark.css is generated, is linked LAST, and blankets form fields with
   `:root[data-theme="dark"] .form-control { background-color; color;
   border-color }` at (0,3,0). Painting the box on the wrapper would mean
   neutralising the input's own background and border, and a `.trs-search >
   input` rule at (0,1,1) loses that fight - it would need a :root:root prefix
   on every declaration, the way pricing.css does. Painting the box ON the
   input means dark.css repaints exactly the three properties it should, to the
   SAME tokens used here, so light and dark agree with no prefix at all.
   ---------------------------------------------------------------------------- */

.trs-search {
    position: relative;
    display: flex;
    align-items: center;
    min-width: 220px;
}

/* A pill, deliberately. 999px is the universal search affordance, and it is the
   one control in this system where it beats the 11px signature radius - it
   separates search from every other control in a toolbar at a glance.

   min-height honours --trs-control-h instead of the two
   `max-height: 38px !important` inline styles this replaces, so a compact user
   gets 30px rather than a hardcoded 38. Still clears WCAG 2.2 SC 2.5.8.

   The horizontal padding makes room for the two floated controls, and it is
   symmetrical whether or not the clear button is showing - so the text does not
   jump sideways as that button appears and disappears mid-typing. */
.trs-search > input {
    width: 100%;
    min-height: var(--trs-control-h);
    padding: var(--trs-space-2) var(--trs-space-8);
    font-size: var(--trs-text-base);
    color: var(--trs-text);
    background: var(--trs-surface);
    border: 1px solid var(--trs-border-strong);
    border-radius: var(--trs-radius-full);
    /* A resting shadow, for the same reason the switcher has one: a flat
       hairline pill on --trs-bg reads as an outline drawn on the page rather
       than as a control sitting on it. */
    box-shadow: var(--trs-elevation-1);
    transition: border-color var(--trs-motion-fast) var(--trs-ease),
                box-shadow var(--trs-motion-fast) var(--trs-ease);
}

.trs-search > input:hover {
    border-color: var(--trs-text-muted);
    box-shadow: var(--trs-elevation-2);
}

.trs-search > input::placeholder {
    color: var(--trs-text-muted);
    opacity: 1;
}

/* :focus-within on the WRAPPER, at (0,2,0). Nothing in dark.css targets
   :focus-within, so this wins in both themes with no :root:root prefix.

   No focus ring of our own: base.css already gives every input an
   `outline: 2px solid var(--trs-focus)` on :focus-visible, and its comment
   records why a box-shadow ring cannot work here - Blazorise emits
   .form-control:focus from a style element inside the body, which beats any
   equal-specificity rule in a stylesheet. */
/* A brand-tinted glow, which is the modern focus treatment and is the one
   effect this field was missing.

   (0,2,1) - class + pseudo-class + type - so it beats `.form-control:focus`
   at (0,2,0) from the theme's in-body style element, which would otherwise
   overwrite the shadow. This is a box-shadow on the INPUT, which is fine;
   base.css's warning is about trying to replace the :focus-visible OUTLINE
   ring with one, and that ring is still doing its own job on top of this. */
.trs-search:focus-within > input {
    border-color: var(--trs-p-400);
    box-shadow: 0 0 0 3px var(--trs-p-100);
}

/* The magnifier picks up the brand tint while the field is focused, so the
   whole control responds rather than just its edge. */
.trs-search:focus-within > .trs-search-icon {
    color: var(--trs-text-primary);
}

/* Decorative and non-interactive. pointer-events:none is the point - the old
   AddonLabel wrapper made the magnifier look like a button. */
.trs-search-icon {
    position: absolute;
    left: var(--trs-space-3);
    top: 50%;
    transform: translateY(-50%);
    color: var(--trs-text-muted);
    pointer-events: none;
}

/* Centred against whatever --trs-control-h currently is. min-width/height hold
   the hit area at 24px even though the glyph is smaller, the same reasoning as
   .trs-auth-reveal. Rendered only when the field has text, so it stops being a
   dead affordance repeated on 42 pages. */
.trs-search-clear {
    position: absolute;
    right: var(--trs-space-2);
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    min-height: 24px;
    padding: 0;
    border-radius: var(--trs-radius-full);
}

/* ----------------------------------------------------------------------------
   TrsDataGrid - density

   Driven through Bootstrap's own variables rather than by out-specifying
   `.table > :not(caption) > * > *`, which is a (0,1,3) selector that would
   only be beaten on load order.
   ---------------------------------------------------------------------------- */

.trs-grid {
    --bs-table-cell-padding-y: var(--trs-cell-pad-y);
    --bs-table-cell-padding-x: var(--trs-cell-pad-x);
}

.trs-grid tbody tr {
    height: var(--trs-row-h);
}

/* ----------------------------------------------------------------------------
   The SELECTED datagrid row

   Blazorise's DataGrid does not use its own `.selected` rule for this. VERIFIED
   in the browser: it puts Bootstrap's contextual `.table-primary` on the row,
   which is stock #cfe2ff with `color: #000` hardcoded - a light blue band with
   black text, off-brand in light and unreadable in dark, where every other row
   is #202422 under near-white text.

   PAINTED ON THE CELLS, not the row, and with no reliance on --bs-table-*.
   Three separate things get between a <tr> and what you see:
     - --bs-table-bg is opaque (tokens.css has to keep it so, or the sticky
       header goes see-through), so a background on the row never shows;
     - dark.css hard-sets the cell background to --trs-surface at a higher
       specificity and ignores --bs-table-bg entirely;
     - MEASURED: overriding --bs-table-bg and color on the row left the CELLS at
       rgb(0,0,0) on rgb(204,217,212) anyway.
   Declaring the three properties on `> *` ends the argument.

   The 9999px inset shadow is Bootstrap's own trick for tinting a cell without
   losing its background - reused here so the tint composites the same way the
   stripes do. The bar on the first cell restates it because box-shadow does not
   merge: a second declaration replaces the list rather than adding to it.

   `table.b-datagrid` rather than a bare class: .table-striped's row rule is
   (0,2,2), and matching it would leave this decided by stylesheet order.
   .b-datagrid also scopes it to grids - table-primary is not hand-written
   anywhere in the app, but a Bootstrap contextual class is not ours to repaint
   globally. The `.selected` half of each pair covers a plain Blazorise <Table>
   with Selected rows, which does use that class.
   ---------------------------------------------------------------------------- */

table.b-datagrid tbody tr.table-primary > *,
table.table tbody tr:not(.table-group).selected > * {
    background-color: var(--trs-surface);
    box-shadow: inset 0 0 0 9999px var(--trs-selected);
    color: var(--trs-text);
    border-color: var(--trs-border);
}

table.b-datagrid tbody tr.table-primary > *:first-child,
table.table tbody tr:not(.table-group).selected > *:first-child {
    box-shadow: inset 0 0 0 9999px var(--trs-selected),
                inset 3px 0 0 0 var(--trs-fill);
}

/* Content that carries its own colour has to come with the row. MEASURED on the
   selected row in dark: the link read #7FB79C and the muted sub-label #8B938D
   against a #3A5E4F ground - 1.9:1 for the label, which is a sub-label you
   cannot read. Both take the row's text colour instead. Muting is dropped
   rather than softened on purpose: --trs-text-secondary is still only 3.2:1 on
   that ground, and the selected row is the one row where a secondary label has
   no reason to recede. */
table.b-datagrid tbody tr.table-primary .btn-link,
table.table tbody tr:not(.table-group).selected .btn-link {
    color: var(--trs-text);
}

/* A status pill loses its shape on the tint: MEASURED in dark, the Syncing
   badge is #3D6B52 sitting on a #3A5E4F row - same hue, same value, so the pill
   reads as plain text and the status stops being scannable. A 1px ring in the
   row's own surface colour redraws the edge without changing the badge's
   meaning-carrying fill. Both themes, because the brand and the success colour
   are the same family in each. */
table.b-datagrid tbody tr.table-primary .badge,
table.table tbody tr:not(.table-group).selected .badge {
    box-shadow: 0 0 0 1px var(--trs-surface);
}

/* !important, and only here. `.text-muted` is a Bootstrap UTILITY and ships as
   `color: ... !important`, so no amount of specificity reaches it - MEASURED:
   this rule at (0,3,3) still lost and the label stayed #8B938D. A utility that
   declares itself important can only be answered in kind. */
table.b-datagrid tbody tr.table-primary .text-muted,
table.table tbody tr:not(.table-group).selected .text-muted {
    color: var(--trs-text) !important;
}

/* ----------------------------------------------------------------------------
   THE GRID FILLS THE LEFTOVER HEIGHT - no reserve constant involved

   What this replaces: every grid capped itself with
   `max-height: calc(100vh - Npx)`, where N came from one of four constants in
   Constants.cs and had to account for the app header, the page chrome above the
   grid, the pager footer AND the EU funding footer. That number cannot be right,
   because the chrome differs per page. MEASURED across three pages sharing those
   constants:

       /Items      chrome 296  needed 392   reserve 400  -> fitted
       /AccessLog  chrome 358  needed 454   reserve 400  -> overflowed 54px
       /HotelStays chrome 213  needed 259   reserve 230  -> overflowed 74px

   Each fix broke another page, so the constant is the wrong mechanism.

   HOW THIS WORKS. `main.b-layout-content` is ALREADY `flex: 1 1 0%` inside the
   column `section.b-layout`, so the space between the app header and the EU
   footer is already computed for us - it just could not be handed down, because
   `main` carries `min-height: auto` (a flex item cannot shrink below its content
   unless told it may) and because every element between it and the scroll
   container is `display: block`.

   So: let `main` shrink, and turn the ancestors of the grid into flex columns
   that pass the leftover height down. The scroll container then takes exactly
   what is left, and `max-height` stops mattering.

   WHY `:has()`. The chain between `.content` and `.trs-grid-shell` is UNNAMED
   page markup - measured 3 levels deep on /Departments, with an anonymous Div
   wrapping the shell - and it differs per page, so it cannot be selected by
   class. `:has()` selects exactly the ancestors that contain a grid shell and
   nothing else. The app already depends on `:has()` for the layout header's
   hover states, so support is not a new assumption.

   SCOPED UNDER `.content` ON PURPOSE. Grids inside MODALS render outside
   `.content` and have no such height chain to inherit - if the max-height
   override reached them they would grow unbounded inside the dialog. They keep
   their constant.
   ---------------------------------------------------------------------------- */

/* `main` gets `min-height: 0` from the APP SHELL block further down, which is
   also what lets it shrink so the grid can take the leftover height. It is set
   there rather than here because every page needs it, not only grid pages. */

/* height: 100% is the load-bearing line, and leaving it out is what broke the
   first attempt at this. `main` IS height-definite - it is a flex item of the
   column layout with `flex: 1 1 0%`, so it computes to viewport minus header
   minus EU footer - but `.content` is a plain BLOCK child of it, so its own
   height was `auto` and it simply grew with its content. With no definite height
   anywhere in the chain, `flex: 1 1 auto` below has nothing to shrink against:
   MEASURED on /AccessLog, the scroll container inflated to its full 1307px of
   content and the page overflow went from 54px to 938px. */
.content:has(.trs-grid-shell) {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
}

/* Every ancestor of the shell, whatever it is called. flex-direction: column is
   safe here because these are page-content wrappers stacking a heading, a
   toolbar, tabs and the grid - if a page ever puts its grid inside a horizontal
   row, this is the rule that would need an exception. */
.content:has(.trs-grid-shell) *:has(.trs-grid-shell) {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

.content:has(.trs-grid-shell) .trs-grid-shell {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

/* Blazorise wraps the table in a classless form, so it is part of the chain -
   and like the scroll container it must SHRINK BUT NOT GROW.

   MEASURED: with `1 1 auto` here, fixing the scroll container alone was not
   enough. The container hugged its rows (114px) but the form still grew to fill
   the shell, and the pager - a sibling of the form - was pushed to the bottom of
   it, leaving the same 289px gap on /Items. Both levels have to hug. */
.content:has(.trs-grid-shell) .trs-grid-shell > form {
    display: flex;
    flex-direction: column;
    flex: 0 1 auto;
    min-height: 0;
}

/* The scroll container takes what is left. !important is unavoidable: Blazorise
   writes max-height as an INLINE style from FixedHeaderDataGridMaxHeight, and an
   inline declaration cannot be out-specified any other way. With this the four
   reserve constants are inert for every non-modal grid. */
/* `0 1 auto` - SHRINK BUT NEVER GROW - and the 0 is the whole point.

   With `1 1 auto` the scroll container grew to fill the space its ancestors had
   claimed, so a one-row table sat in a 386px box and the pager was pushed 289px
   below the last row. MEASURED on /Items; it is the same detached pager that
   `height: auto` was introduced to fix in the first place.

   grow 0 means a short table hugs its rows and the pager sits right under them,
   with the leftover height falling below the footer where nobody sees it.
   shrink 1 with min-height 0 means a long table gives way instead of pushing the
   page, and scrolls inside itself. */
.content:has(.trs-grid-shell) .table-fixed-header {
    flex: 0 1 auto;
    min-height: 0;
    max-height: none !important;
}

/* ----------------------------------------------------------------------------
   A GRID INSIDE A CARD OPTS OUT OF THE FILL

   The chain above assumes ONE grid owns the space between the app header and the
   EU footer. Where a page stacks two or three of them in Cards - the three
   dashboards - that leftover height is split between them and each gets half a
   screen. MEASURED on /Operations: 272px per table, which cuts a row in half and
   leaves two internal scrollbars on a page that has its own.

   A grid in a Card is a SECTION of a page, not the page. So if any grid here
   lives in one, the whole page falls back to sizing by content and scrolling
   normally. Four files match today, three of them those dashboards.

   Specificity, not source order: the extra .card takes these to (0,4,0) against
   the (0,3,0) above, so this holds wherever it sits in the file.
   ---------------------------------------------------------------------------- */

.content:has(.card .trs-grid-shell),
.content:has(.card .trs-grid-shell) *:has(.trs-grid-shell),
.content:has(.card .trs-grid-shell) .trs-grid-shell,
.content:has(.card .trs-grid-shell) .trs-grid-shell > form {
    display: block;
    height: auto;
    min-height: auto;
    flex: none;
}

/* The opt-out above is a blunt `display: block`, and the wildcard in its second
   selector reaches further than the unnamed wrappers it was written for: it also
   matches `.trs-page`, whose OWN layout is `display: flex` with
   `gap: var(--trs-space-4)`. Flattening it to block silently discards that gap,
   and the page header's `border-bottom` divider then sits FLUSH against whatever
   follows it - MEASURED on /Operations, where the header's bottom edge and the
   filter row's top edge were both at y=143.

   Restoring `display: flex` on `.trs-page` is the wrong repair. Flex gap does not
   collapse with the sections' own `mb-3`/`my-3` margins, so it adds 16px between
   EVERY section, not only under the header - measured, all four gaps went 16px ->
   32px. Only the header lost anything, so only the header gets it back, as a
   margin the block flow understands.

   Three pages match: the Operations, Superadmin and GDPR dashboards. */
.content:has(.card .trs-grid-shell) .trs-page > .trs-page-header {
    margin-bottom: var(--trs-space-4);
}

/* !important for the same reason the rule above needs it: Blazorise writes
   max-height as an INLINE style from FixedHeaderDataGridMaxHeight. */
.content:has(.card .trs-grid-shell) .table-fixed-header {
    max-height: none !important;
    flex: none;
}

/* ----------------------------------------------------------------------------
   Grid pager

   Blazorise emits the pager as a bare `row justify-content-between` under the
   table with nothing but a 0.75rem top margin, so it read as floating loose
   beneath the grid rather than belonging to it. This gives it the table's
   surface and border so the two read as one panel.

   PLACEMENT IS NOT DONE HERE. Centring the page buttons is
   PagerOptions.PaginationPosition = Center in TrsDataGrid - Blazorise's own API.
   An earlier version of this block hand-placed the pager's children in a
   three-track CSS grid, which fought the framework and broke as soon as the
   child count turned out to be three rather than two. Only chrome belongs here.

   SELECTED BY THE FORM ABOVE IT, not by the shell. `row justify-content-between`
   is a generic Bootstrap class used elsewhere in the app, so it needs a scope -
   but .trs-grid-shell is the wrong one, because only TrsDataGrid renders that
   wrapper. MEASURED: a raw <DataGrid> (WorkhoursSummary) got a transparent,
   unbordered pager sitting 12px below the table with its buttons 153px left of
   centre, while /Items looked right.

   Blazorise puts the table in a CLASSLESS <form> and the pager row immediately
   after it, in BOTH cases - verified on /Items (shell) and /WorkhoursSummary
   (raw). So `form:has(.trs-grid) + .row` identifies a grid footer precisely,
   with no wrapper needed, and one rule now serves both. (0,3,1) - :has() scores
   as its argument - so it still outranks what it replaced.
   ---------------------------------------------------------------------------- */

form:has(.trs-grid) + .row.justify-content-between {
    /* Overrides the 0.75rem !important top margin app.css puts on this exact
       selector - the footer has to touch the table to belong to it. */
    margin-top: 0 !important;
    align-items: center;
    padding: var(--trs-space-2) var(--trs-space-3);
    background-color: var(--trs-surface);
    border: 1px solid var(--trs-border);
    border-top: 0;
    /* --trs-radius-md, matching the wrapper's own radius from app.css:32
       (.table-fixed-header). It was radius-sm, which is why the footer's corners
       did not line up with the table's. */
    border-bottom-left-radius: var(--trs-radius-md);
    border-bottom-right-radius: var(--trs-radius-md);
}

/* The table above keeps its top corners and gives up its bottom ones, so the
   rounding happens once around the pair rather than twice in the middle.

   DESCENDANT, not a child selector. MEASURED: Blazorise wraps the table in a
   CLASSLESS <form>, so the scroll wrapper is a grandchild of the shell, not a
   child - `> .table-fixed-header` matched nothing and this rule was dead. The
   wrapper kept all four of its 11px corners while the footer added its own
   rounded bottom underneath, which is the double-rounded seam. */
/* A raw grid's wrapper is plain `.table-responsive`, which app.css:32 does not
   reach - MEASURED radius 0 on all four corners, so the table had no panel edge
   to share with the footer. Give it the same radius the fixed-header wrapper
   gets; the table's own `table-bordered` still draws the lines, and the
   wrapper's overflow does the clipping. */
form:has(.trs-grid) > .table-responsive {
    border-radius: var(--trs-radius-md);
}

/* Bottom corners go square only when a footer actually follows. A raw grid with
   ShowPager="false" (WorkhoursSummary uses a row-count condition) has nothing
   underneath, and squaring its bottom would leave two corners unrounded. */
form:has(.trs-grid):has(+ .row.justify-content-between) > .table-responsive {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}

/* PagerOptions puts the pagination in the centre slot, but it does not actually
   centre it, and this is the minimum needed to finish the job.

   MEASURED: Blazorise gives the centre slot `mx-auto` and the end slot
   `ms-auto`. That is THREE auto margins competing for the same free space in one
   flex row, so the browser splits it into three equal shares - 546.4px each -
   and the pagination landed at x=801 with its midpoint 318px left of the
   footer's. Auto margins cannot centre a child when the slots either side of it
   are different widths.

   So the two flanking slots are given equal flex instead, and the centre slot's
   auto margins are zeroed. Equal flanks means the middle is genuinely centred,
   and it stays flex rather than becoming a grid - the framework keeps owning the
   DOM and the wrapping behaviour. */
form:has(.trs-grid) + .row.justify-content-between > *:first-child,
form:has(.trs-grid) + .row.justify-content-between > *:last-child:not(:first-child) {
    flex: 1 1 0;
    min-width: 0;
}

form:has(.trs-grid) + .row.justify-content-between > *:not(:first-child):not(:last-child) {
    margin-left: 0;
    margin-right: 0;
}

/* Colour and size only. The class list Blazorise puts on this slot already
   carries justify-content-end, so the count stays hard right inside its flex
   track. */
form:has(.trs-grid) + .row.justify-content-between > *:last-child:not(:first-child) {
    color: var(--trs-text-secondary);
    font-size: var(--trs-text-sm);
}

/* A phone cannot hold the page buttons and the count on one line: the count got
   what was left of the equal flanks - MEASURED 0px at 390 - and was drawn over
   "Last". Below sm each flank takes a line of its own, and an empty one (no page
   sizes) takes none. Bootstrap's d-flex and justify-content-end are !important,
   hence these. */
@media (max-width: 575.98px) {
    form:has(.trs-grid) + .row.justify-content-between {
        row-gap: var(--trs-space-2);
    }

    form:has(.trs-grid) + .row.justify-content-between > *:first-child,
    form:has(.trs-grid) + .row.justify-content-between > *:last-child:not(:first-child) {
        flex-basis: 100%;
        justify-content: center !important;
    }

    form:has(.trs-grid) + .row.justify-content-between > *:first-child:empty {
        display: none !important;
    }

    /* Below sm Blazorise swaps the page buttons for a select, which flex shrank
       to 50px - all of it the chevron's padding, so the page number was not
       drawn. MEASURED at 390: four buttons 230px + select in a 275px slot. With
       the tighter padding the buttons take 198px and a two-digit page fits. */
    form:has(.trs-grid) + .row.justify-content-between .pagination .page-link {
        padding-inline: var(--trs-space-2);
    }

    form:has(.trs-grid) + .row.justify-content-between .pagination .form-select {
        flex-shrink: 0;
        min-width: 3.5rem;
        padding-inline: var(--trs-space-2) var(--trs-space-6);
        background-position: right var(--trs-space-2) center;
    }
}

/* Bootstrap gives `.table` a 1rem bottom margin, and it sits INSIDE the scroll
   wrapper - so it opened a 16px gap between the last row and the footer even
   though the footer itself was already flush with the wrapper (measured gap: 0).
   The table is that wrapper's whole content, so the margin has no job here.

   Was scoped to .trs-grid-shell; unscoped now so raw grids get it too. Selector
   is `.b-table.trs-grid` rather than bare `.trs-grid`, because bare would TIE
   `.table` at (0,1,0) and be decided by load order - including against the
   theme <style> Blazorise writes into <body>, which wins every equal-specificity
   tie. (0,2,0) leaves nothing to chance. */
.b-table.trs-grid {
    margin-bottom: 0;
}

/* ----------------------------------------------------------------------------
   TrsGridLoader - the in-table loading row

   Blazorise renders LoadingTemplate inside ONE full-width table row, and hides
   the data rows while it is showing: IsDisplayDataVisible is defined as
   `!IsLoadingTemplateVisible && !IsEmptyTemplateVisible`. So this is not an
   overlay - it is what the table body IS for the length of a fetch.

   That makes min-height load-bearing. Without it a 14-row table collapses to a
   single row and springs back on every pager click, and the pager moves with it
   (IsPagerVisible is gated on the same flag). Six rows rather than fourteen: a
   short grid should not GROW while it loads.
   ---------------------------------------------------------------------------- */

.trs-grid-loading {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--trs-space-2);
    min-height: calc(var(--trs-row-h) * 6);
    color: var(--trs-text-secondary);
}

/* ----------------------------------------------------------------------------
   Two heights that were inline styles

   Neither is data-driven, so neither had any business being written at the call
   site - they are just fixed sizes that nobody had given a name.
   ---------------------------------------------------------------------------- */

/* The instruction editor on a survey step. 25rem was chosen at the call site;
   keeping the exact value so nothing moves. */
.trs-step-instruction-editor {
    height: 25rem;
}

/* A country flag rendered inline with text - the hotel's country on
   HotelDetails. 18px matches the cap height of the 14px body text next to it;
   the header's own flag is 24px because it sits alone in a 40px button. */
.trs-country-flag {
    height: 18px;
    width: auto;
}

/* ----------------------------------------------------------------------------
   Date-range pill with a day stepper

   One control where HotelStays had seven: two independent DatePickers, each
   duplicated in markup so its "set" copy could be welded to a red clear button
   by a Bootstrap btn-group, plus Today and two day arrows.

   The group is drawn as ONE pill so the arrows read as part of the field rather
   than as page navigation, and it borrows the search box's exact tokens -
   --trs-control-h, --trs-radius-full, --trs-border-strong, --trs-elevation-1 -
   because the two sit side by side in the same row and any difference between
   them reads as a mistake. MEASURED before: the date inputs were 11px-radius on
   the sunken --trs-fill at 16px type, against the search pill's 999px on
   --trs-surface at 14px, which is why they read as bare text rather than fields.
   ---------------------------------------------------------------------------- */

.trs-daterange {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    /* An explicit height, because border-box on a container with no height set
       still adds the border OUTSIDE the content. MEASURED: the inner field is
       exactly --trs-control-h (38px) and the pill's own 0.74px top and bottom
       borders took the group to 39.5px - 1px taller than the search box it sits
       beside, and 1px higher up the row. With the height fixed here the borders
       are absorbed into it, and the field below is told not to fight it. */
    height: var(--trs-control-h);
    padding: 0 var(--trs-space-1);
    background: var(--trs-surface);
    border: 1px solid var(--trs-border-strong);
    border-radius: var(--trs-radius-full);
    box-shadow: var(--trs-elevation-1);
    transition: border-color var(--trs-motion-fast) var(--trs-ease),
                box-shadow var(--trs-motion-fast) var(--trs-ease);
}

/* The same brand glow the search field gets, on the whole group rather than on
   the input alone - the pill is the control, so the focus ring belongs to it. */
.trs-daterange:focus-within {
    border-color: var(--trs-p-400);
    box-shadow: 0 0 0 3px var(--trs-p-100);
}

/* DARK: inputs are SUNKEN, not surface. dark.css:87 gives every .form-control
   `background-color: var(--trs-surface-sunken)`, so a pill on --trs-surface
   read a full step lighter than the search box beside it - MEASURED #202422
   against the search field's #121513, matching in light and mismatched in dark.

   Both selectors, per the house pattern: the attribute form so an explicit
   choice wins, and the media form guarded with :not([data-theme="light"]) so
   the system default follows without overriding a user who picked light. */
:root[data-theme="dark"] .trs-daterange {
    background: var(--trs-surface-sunken);
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .trs-daterange {
        background: var(--trs-surface-sunken);
    }
}

/* Blazorise wraps flatpickr inputs in .flatpickr-wrapper, which is
   display: block and would otherwise take the full row width. */
.trs-daterange .flatpickr-wrapper {
    display: flex;
}

/* The field gives up all its own chrome to the pill around it. (0,2,0) to beat
   `.form-control` and the theme block Blazorise writes into <body>. */
/* Two fields share the pill now, so each takes half the width one range field
   used. */
.trs-daterange .form-control {
    width: 7rem;
    /* min-height: 0 so the pill's height wins - see the note above. */
    min-height: 0;
    height: 100%;
    padding: var(--trs-space-1) var(--trs-space-2);
    font-size: var(--trs-text-base);
    text-align: center;
    color: var(--trs-text);
    background: transparent;
    border: 0;
    border-radius: 0;
    box-shadow: none;
}

/* The pill owns the focus indication, so the field draws neither a shadow nor
   an outline of its own. MEASURED: the field was drawing a solid 1.48px
   --trs-p-500 outline at a 1.48px offset INSIDE the pill's 3px glow - and
   because the field's own radius is zeroed here, that outline rendered as a
   hard rectangle inside a 999px pill. Two rings, one of them the wrong shape.

   Removing it is not an accessibility regression: the replacement is on
   :focus-within, so it shows for keyboard focus exactly as it does for a
   click. */
/* `html` prefix to out-rank base.css's global focus ring, which is
   `html :is(..., .form-control, ...):focus-visible` - :is() carries the
   specificity of its most specific argument, so that rule is (0,2,1) and a plain
   `.trs-daterange .form-control:focus-visible` at (0,3,0) still lost to it on
   the cascade. This is (0,3,1). */
html .trs-daterange .form-control:focus,
html .trs-daterange .form-control:focus-visible {
    outline: none;
    box-shadow: none;
}

/* The en dash between the two fields. Muted, because it is punctuation rather
   than content. */
.trs-daterange-sep {
    color: var(--trs-text-muted);
    padding: 0 2px;
    user-select: none;
}

/* Small round arrows, sized off the control height so they always sit inside
   the pill rather than stretching it. */
.trs-daterange .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: calc(var(--trs-control-h) - var(--trs-space-2));
    height: calc(var(--trs-control-h) - var(--trs-space-2));
    padding: 0;
    border-radius: 50%;
}

/* ----------------------------------------------------------------------------
   Count overlay on a row-action button

   Four pages had copy-pasted the same
   `style="position: absolute; font-size: 0.6rem; right: -5px; top: -5px;"`
   onto a Badge inside an action button, each also needing
   Position="Position.Relative" on the button to give it a containing block.
   This is that pattern, once.

   The offsets are negative on purpose: the badge overhangs the button so it
   does not eat into the 40px circle the glyph is centred in.
   ---------------------------------------------------------------------------- */

.trs-row-actions .trs-action-badged {
    /* The containing block for the absolutely positioned badge. Without this
       the badge escapes to the nearest positioned ancestor - the table cell -
       and lands in the wrong corner. */
    position: relative;
}

.trs-row-actions .trs-action-badged .badge {
    position: absolute;
    top: -5px;
    right: -5px;
    font-size: 0.6rem;
    /* The glyph underneath must not shift the badge around, and a count of 1
       should read as the same shape as a count of 12. */
    line-height: 1;
    pointer-events: none;
}

/* ----------------------------------------------------------------------------
   The "recommended" star on the tourist guide

   A STATE TOGGLE, not a row action - it sits in its own column and its colour
   carries the state rather than the brand, so it does not take the ghost
   treatment .trs-row-actions applies.

   It used to be a clickable <span> with
   `style="cursor: pointer; font-size: 1.5rem; line-height: 1;"` on it: no
   button semantics, unreachable by keyboard, and two of the inline styles the
   ratchet counts. It is a real TrsButton now, so the size and the two state
   colours have to live here.

   `.trs-star.btn` rather than a bare class: .trs-btn-ghost sets a colour at
   (0,1,0) and would otherwise decide this on load order.
   ---------------------------------------------------------------------------- */

.trs-star.btn,
.trs-star {
    color: var(--trs-text-muted);
    font-size: 1.25rem;
    line-height: 1;
}

.trs-star.btn.trs-star--on,
.trs-star.trs-star--on {
    color: var(--trs-warning);
}

/* Hover only means anything on the pressable copy - the read-only star is an
   Icon, not a button. */
.trs-star.btn:hover,
.trs-star.btn:focus-visible {
    color: var(--trs-warning);
    background-color: transparent;
}

/* ----------------------------------------------------------------------------
   Row action cluster

   Replaces Blazorise's Buttons component in a datagrid actions column. Buttons
   renders a Bootstrap btn-group, which welds four or five outlined buttons into
   one heavy segmented pill - the noisiest element in every grid in the app. A
   plain flex row of icon-only ghost buttons carries the same affordances at a
   fraction of the ink.
   ---------------------------------------------------------------------------- */

/* Row actions: four bare icon buttons, spaced, no container.

   FOURTH ATTEMPT, and the previous three are worth recording so nobody repeats
   them. (1) Ghost buttons at 32px with MUTED GREY glyphs - read as inert
   decoration, "they look like icons, not buttons". (2) A border and a surface
   fill on each - read as clutter, and could not work anyway because the grid is
   STRIPED: any per-button fill matches its own row on half the rows. (3) One
   segmented pill containing all four - solved the striping but still looked
   flat and cramped at 32px with 14px glyphs.

   What was actually wrong in (1) was the COLOUR, not the absence of a box. A
   grey glyph on a table row is decoration; a brand-coloured one is an action.
   So: no container at all, 40px targets with real gaps between them, glyphs in
   --trs-text-primary, and a circular tint on hover. Chosen from a set of
   alternatives rather than guessed at again.
   ---------------------------------------------------------------------------- */

.trs-row-actions {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--trs-space-2);
    flex-wrap: nowrap;
}

/* Each TrsButton with a Tooltip is wrapped in a .b-tooltip div, so the row's
   direct children are those wrappers rather than the buttons themselves. */
.trs-row-actions > * {
    display: flex;
}

/* For a grid whose rows can carry more actions than its column fits: they go
   onto a second line instead of being clipped. */
.trs-row-actions--wrap {
    flex-wrap: wrap;
}

/* The stays grid shows each date in a read-only field; the field keeps room for
   a full "dd.MM.yyyy HH:mm" instead of shrinking with the column. */
.trs-stay-period .form-control {
    min-width: 9.5rem;
    font-size: var(--trs-text-sm);
}

/* Circles, not rounded squares - a 40px circle under a 18px glyph reads as a
   touch target rather than as a tile, and it is what distinguishes this from
   attempt (2). (0,2,0), so it beats both `.btn` from the theme's in-body style
   element and `.trs-btn-ghost`. */
.trs-row-actions .btn {
    width: 40px;
    height: 40px;
    padding: 0;
    border: none;
    border-radius: var(--trs-radius-full);
    background-color: transparent;
    box-shadow: none;
    color: var(--trs-text-primary);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background-color var(--trs-motion-fast) var(--trs-ease),
                color var(--trs-motion-fast) var(--trs-ease);
}

/* 18px in a 40px circle. 14px was the size in the first three attempts and it
   read as a speck. */
.trs-row-actions .btn > .fas {
    font-size: var(--trs-text-lg);
}

.trs-row-actions .btn:hover,
.trs-row-actions .btn:focus-visible {
    background-color: var(--trs-p-100);
}

/* SEMANTIC COLOURS SURVIVE THE RESHAPE.

   The 25 clusters rolled out across the app were not rewritten button by button
   - they still contain Blazorise `<Button Color="Color.X" Outline>`, and the
   rules above strip their fill and border and repaint them brand-green. That is
   right for a Primary action and WRONG for the rest: counted across the
   clusters there are 36 Danger, 15 Success, 6 Warning and 4 Info buttons, where
   the colour is the meaning - an approve tick and a reject cross must not be
   the same colour.

   So each variant keeps its own ink and its own hover wash. Bootstrap's variant
   class is (0,1,0); these are (0,2,0), and they sit after the generic rule so
   they win on order as well. */
.trs-row-actions .trs-btn-ghost--danger,
.trs-row-actions .btn-danger,
.trs-row-actions .btn-outline-danger {
    color: var(--trs-danger);
}

.trs-row-actions .trs-btn-ghost--danger:hover,
.trs-row-actions .trs-btn-ghost--danger:focus-visible,
.trs-row-actions .btn-danger:hover,
.trs-row-actions .btn-danger:focus-visible,
.trs-row-actions .btn-outline-danger:hover,
.trs-row-actions .btn-outline-danger:focus-visible {
    background-color: var(--trs-danger-bg);
    color: var(--trs-danger);
}

.trs-row-actions .btn-success,
.trs-row-actions .btn-outline-success {
    color: var(--trs-success);
}

.trs-row-actions .btn-success:hover,
.trs-row-actions .btn-success:focus-visible,
.trs-row-actions .btn-outline-success:hover,
.trs-row-actions .btn-outline-success:focus-visible {
    background-color: var(--trs-success-bg);
    color: var(--trs-success);
}

.trs-row-actions .btn-warning,
.trs-row-actions .btn-outline-warning {
    color: var(--trs-warning);
}

.trs-row-actions .btn-warning:hover,
.trs-row-actions .btn-warning:focus-visible,
.trs-row-actions .btn-outline-warning:hover,
.trs-row-actions .btn-outline-warning:focus-visible {
    background-color: var(--trs-warning-bg);
    color: var(--trs-warning);
}

.trs-row-actions .btn-info,
.trs-row-actions .btn-outline-info {
    color: var(--trs-info);
}

.trs-row-actions .btn-info:hover,
.trs-row-actions .btn-info:focus-visible,
.trs-row-actions .btn-outline-info:hover,
.trs-row-actions .btn-outline-info:focus-visible {
    background-color: var(--trs-info-bg);
    color: var(--trs-info);
}

/* A count bubble on a row action - several clusters carry one - is positioned
   against the 40px circle rather than a rectangular button, so it needs pulling
   back inside the round corner. */
.trs-row-actions .badge-count {
    top: 2px;
    right: 2px;
}

/* Unavailable, but still legibly a control. The tooltip saying WHY is on the
   wrapper and appears on hover. */
.trs-row-actions .btn:disabled {
    opacity: .4;
    cursor: default;
}

.trs-row-actions .btn:disabled:hover {
    background-color: transparent;
}

@media (prefers-reduced-motion: reduce) {
    .trs-row-actions .btn {
        transition: none;
    }
}

.trs-row-unread > td {
    background: var(--trs-p-50);
}

/* ----------------------------------------------------------------------------
   TrsEmptyState
   ---------------------------------------------------------------------------- */

.trs-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--trs-space-2);
    padding: var(--trs-space-10) var(--trs-space-4);
    text-align: center;
    color: var(--trs-text-secondary);
}

.trs-empty--compact {
    padding: var(--trs-space-5) var(--trs-space-3);
}

.trs-empty-icon {
    color: var(--trs-text-muted);
    font-size: var(--trs-text-2xl);
}

.trs-empty-title {
    color: var(--trs-text);
    font-weight: var(--trs-weight-bold);
    margin: 0;
}

.trs-empty-description {
    margin: 0;
    max-width: 46ch;
}

/* ----------------------------------------------------------------------------
   TrsStatusBadge - rack tones

   These six classes are the ONLY consumers of the --trs-status-* tokens.
   Semantic tones (Primary/Success/...) go through the Blazorise theme instead,
   which is already token-bridged.
   ---------------------------------------------------------------------------- */

/* The availability-grid badge.

   Two files - RoomAvailabilityTable and StaffAvailabilityTable - render a badge
   that fills its grid cell: centred, full width to a cap, bold, and raised. They
   were carrying that as Flex/Gap/Width/TextWeight parameters plus an interpolated
   Style string, which is the only inline style either file needed for it. The
   height came from a `private const double CellHeightPx`, so none of it was
   runtime geometry and all of it belongs here.

   The shadow becomes --trs-elevation-1 rather than the literal
   `0 2px 4px rgba(50,50,93,.1), 0 1px 2px rgba(0,0,0,.1)` it replaces. That
   literal was a LIGHT-MODE shadow that never flipped: on the dark grid it was
   painting a barely-visible haze tuned for a white page. The token is a touch
   softer and correct in both themes.

   Both grids render the badge at 40px - the room grid from its CellHeightPx const,
   the staff grid from a literal - so one height covers them. */
.trs-avail-badge {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--trs-space-1);
    width: 100%;
    max-width: 150px;
    min-height: 40px;
    font-weight: var(--trs-weight-bold);
    box-shadow: var(--trs-elevation-1);

    /* Bootstrap's .badge hard-sets white-space:nowrap, so a label wider than the
       column is CLIPPED rather than wrapped - and the column is flex: 0 0 140px, so
       it is the same width at every viewport. MEASURED: "Zahtijeva ciscenje" needs
       117px and gets 115, losing a sliver of the last letter on every screen. Same
       fix as .ota-health-cell .badge in pricing.css.

       min-height rather than height so a wrapped label grows the pill instead of
       spilling out of it. A single-line badge is unchanged at 40px. */
    white-space: normal;
    word-break: break-word;
    line-height: 1.15;
    text-align: center;
}


.trs-badge--occupied {
    background: var(--trs-status-occupied-bg);
    color: var(--trs-status-occupied-fg);
}

.trs-badge--arriving {
    background: var(--trs-status-arriving-bg);
    color: var(--trs-status-arriving-fg);
}

.trs-badge--departing {
    background: var(--trs-status-departing-bg);
    color: var(--trs-status-departing-fg);
}

.trs-badge--vacant-clean {
    background: var(--trs-status-vacant-clean-bg);
    color: var(--trs-status-vacant-clean-fg);
}

.trs-badge--vacant-dirty {
    background: var(--trs-status-vacant-dirty-bg);
    color: var(--trs-status-vacant-dirty-fg);
}

.trs-badge--ooo {
    background: var(--trs-status-ooo-bg);
    color: var(--trs-status-ooo-fg);
}

/* ----------------------------------------------------------------------------
   TrsSkeleton

   Blazorise Skeleton maps onto Bootstrap's .placeholder, which paints
   currentColor at 50% opacity. That inverts badly on the dark ground, so
   retint it onto a surface token.
   ---------------------------------------------------------------------------- */

.trs-skeleton .placeholder {
    background-color: var(--trs-surface-sunken);
    opacity: 1;
    border-radius: var(--trs-radius-sm);
}

/* ----------------------------------------------------------------------------
   Modal chrome

   These two rules are duplicated as a per-file <style> block in 60 and 52
   modal components respectively. Landing them here makes the Phase 4 modal
   sweep a pure deletion.
   ---------------------------------------------------------------------------- */

.modal-header {
    justify-content: center;
}

/* A modal body inside a modal body is a WRAPPER, not a second box.

   Blazorise's ModalProvider renders its own `.modal-body` around whatever content
   component it is showing, and every HotelTRS modal then renders one of its own -
   TrsModalShell does it now, and the hand-built modals did it before. So the
   padding applied twice: MEASURED on EditDepartmentModal, a 500px modal spent 32px
   of its width on two 16px gutters and the content got 467px.

   Pre-dates the modal sweep - the nesting is how the provider has always worked -
   and it is cheapest to fix here, because the outer element belongs to Blazorise
   and no component of ours can reach it.

   `:has(.modal-body)` rather than `:has(.modal-body-holder)` so it also covers the
   modals that pass BodyClass="@null" and render a bare inner body. The app already
   depends on :has() for the layout header and the grid fill chains. */
.modal-body:has(.modal-body) {
    padding: 0;
}

.modal-body-holder > div {
    padding-bottom: var(--trs-space-4);
}

/* Dense rate-entry modals. These carried 0.5em (7px against the 14px body)
   as their own rule; 8px is the nearest step on the 4px scale. Without this
   they would jump 9px per section on screens that already scroll. */
.modal-body-holder--tight > div {
    padding-bottom: var(--trs-space-2);
}

/* A modal body that supplies its own frame and wants none of ours.

   EditRecipeModal carries a full-bleed two-pane layout and had been holding this
   as an inline `Style` on its ModalBody - the exact debt the ratchet counts. It
   becomes a class here so the markup can name it, which is the only route a style
   is allowed to leave a .razor file.

   Both declarations need `!important` for the same reason the inline style did:
   Bootstrap's own `.modal-body` sets padding at equal specificity and wins on
   source order, since its sheet is linked before this one. */
.modal-body-flush {
    overflow: hidden !important;
    padding: 0 !important;
}

/* Modals close through their footer buttons, so the shell's own X is hidden
   everywhere. Was in MainLayout's <style> - rendered on every authenticated
   page, so already app-wide; it just says so now. */
.modal-header .btn-close {
    display: none;
}

/* ----------------------------------------------------------------------------
   CLICKABLE ROW

   Was `.cursor-pointer` in GDPRDashboardIndex - a name that says one thing and
   does two, since it also carried a hover tint. A utility called cursor-pointer
   has no business painting a background, and the name is generic enough that
   the next person to want a pointer cursor would have got the tint with it.
   ---------------------------------------------------------------------------- */

.trs-row-clickable {
    cursor: pointer;
}

.trs-row-clickable:hover {
    background-color: var(--trs-surface-sunken);
}

/* ----------------------------------------------------------------------------
   CENTRED STAGE

   A single message centred in the viewport - the access-denied page. Was
   `.container-div`, a name one letter away from Bootstrap's own .container.
   ---------------------------------------------------------------------------- */

.trs-center-stage {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
}

/* ----------------------------------------------------------------------------
   ORDER NUMBER

   An input group that reads as one control. Was a literal black border.
   ---------------------------------------------------------------------------- */

.order-number-holder {
    border: 1px solid var(--trs-border-strong);
    border-radius: var(--trs-radius-sm);
    padding: 0;
}

/* ----------------------------------------------------------------------------
   ACCORDION CHEVRON ON A FILLED HEADER

   Bootstrap draws the accordion chevron as a dark SVG background image, which
   disappears when the toggle is given a primary ground. Inverting it is the
   only way to recolour a background-image chevron. Zodomus is the one screen
   that fills its accordion headers, but the rule is Bootstrap chrome, so it
   lives with the rest of it rather than in a page.
   ---------------------------------------------------------------------------- */

.accordion-button.bg-primary::after,
.accordion-button.bg-primary:not(.collapsed)::after {
    filter: brightness(0) invert(1);
}

/* ----------------------------------------------------------------------------
   COOKIE BAR

   Pinned to the bottom of the viewport, above everything except modals. Was an
   #id selector in the component - the only rules in the app that could not be
   overridden by a class.
   ---------------------------------------------------------------------------- */

.trs-cookie-bar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1050;
    margin: 0;
    padding: var(--trs-space-4);
    background: transparent;
}

.trs-cookie-bar .alert {
    margin: 0 auto;
    max-width: 100%;
    box-shadow: 0 -4px 12px rgba(60, 65, 60, .15);
}

/* ----------------------------------------------------------------------------
   Count bubble

   There were FOUR definitions of .badge-count, not three. Three lived in
   component <style> blocks - HotelRoomsDatagrid at 0.6rem, HotelStaysDatagrid
   and ReceiptsIndex at 0.5rem - so the size of a bubble depended on which had
   rendered most recently. The fourth was in app.css, and it was the one
   actually deciding position, because it carried

       right: 1px !important; top: 1px !important;

   and !important beats specificity. That is why consolidating the other three
   here changed the size but not the placement: app.css was still in charge, and
   had been overriding layout.css's header bubble too. It is gone now, and this
   is the base rule with layout.css holding the header variant.

   One rule, at the 11px floor. 8px is below what this design system will put
   information at, and 11px is what the header bubble was already set to in
   layout.css when that file was migrated.

   Compound .badge.badge-count because a `.badge { position: relative }` rule
   ships in the stylesheet bundle; a single class would tie with it and be
   settled by document order. That tie is what the three !important flags in
   the originals were paying for.
   ---------------------------------------------------------------------------- */

.badge.badge-count {
    position: absolute;
    top: -2px;
    right: -2px;
    z-index: 1;
    font-size: var(--trs-text-2xs);
}

/* ----------------------------------------------------------------------------
   flatpickr popup

   Blazorise DatePicker/TimePicker with StaticPicker="false" - which is the
   default and what every call site uses - has flatpickr append its popup to
   document.body. It is therefore NOT inside the component that opened it, and
   these rules cannot be scoped to one.

   They were previously written unscoped inside two components' <style> blocks,
   which gave them a scope nobody chose: they applied to every picker in the
   app, but only while a department modal or the rooms datagrid happened to be
   rendered. Landing them here makes that consistent instead of incidental.

   CreateDepartmentModal also carried `.service-schedule-box .flatpickr-calendar`
   with a comment about "the inline/appended calendar". That selector could
   never match for the reason above, and is dropped rather than carried across.
   ---------------------------------------------------------------------------- */

/* These declarations are app.css's, moved verbatim rather than rewritten -
   including the !important flags and the 4em width. app.css had carried the
   same rules the whole time, so the two components that also declared them
   were writing a third and fourth copy of something already global, and the
   version this file first shipped quietly narrowed the time inputs from 4em to
   3.5em. Moving them unchanged keeps the computed result identical to what has
   been shipping; deciding whether the !important flags are still needed is a
   separate question from where the rules live. */

.flatpickr-wrapper > input {
    cursor: pointer;
}

.flatpickr-calendar {
    user-select: none !important;
    min-width: 320px;
}

.flatpickr-monthDropdown-month {
    text-align: center;
}

.flatpickr-innerContainer {
    justify-content: center;
    padding-bottom: 1em;
}

/* THE POPUP SHELL.

   These three were set from JavaScript until now - scripts.js wrote
   `1px solid #dee2e6`, `0.375rem` and a hardcoded shadow onto every calendar
   inline and !important, so no stylesheet could reach them in either theme.
   In dark that showed as a light grey rectangle around a #202422 popup; in both
   themes it was 6px round in an 11px application.

   `:root` rather than a bare class on purpose. blazorise.css declares
   `.flatpickr-calendar { border: 0 }` and arrives with a runtime nonce, so its
   position in document order is not something to rely on; (0,2,0) wins the tie
   whichever way it lands. The tokens flip with the theme, so this is the whole
   fix - no dark.css counterpart needed. */
:root .flatpickr-calendar {
    border: 1px solid var(--trs-border);
    border-radius: var(--trs-radius-md);
    box-shadow: var(--trs-elevation-2);
}

/* An INLINE calendar is embedded in a panel, not floating above one, so the
   elevation above is wrong for it - and TransferWorkdaysModal is the one place
   in the app that uses one (Inline on the DatePicker, always visible, no popup).
   MEASURED there in dark: the calendar and the modal body are both #202422, so
   a drop shadow between two identical surfaces reads as a smudge rather than a
   lift. Border and radius stay - they are what separates the two now. */
:root .flatpickr-calendar.inline {
    box-shadow: none;
}

/* Centre hour:minute in the time popup. */
.flatpickr-time {
    justify-content: center !important;
}

.flatpickr-time .numInputWrapper {
    flex: 0 0 auto !important;
    width: 4em;
}

.flatpickr-time .flatpickr-time-separator {
    flex: 0 0 auto !important;
    width: auto;
}

/* ----------------------------------------------------------------------------
   RICH TEXT EDITOR

   TouristGuideComposeModal and NoticeBoardComposeModal sized their Quill
   editor with identical rules, each keyed on its own element id. Both carried
   the same comment explaining why: styling .ql-editor unscoped stretches every
   datagrid cell that renders rich text to 250px the moment the modal opens.

   They were right, and the fix generalises - a class rather than two ids, so
   the next rich-text field gets the sizing by asking for it.
   ---------------------------------------------------------------------------- */

.trs-rich-editor .ql-container {
    min-height: 250px;
    max-height: calc(100vh - 40em);
    overflow: auto;
}

.trs-rich-editor .ql-editor {
    min-height: 250px;
}

/* ----------------------------------------------------------------------------
   FIXED-SIZE TEXT AREA

   A MemoEdit with no drag handle, for the note fields that used to carry
   Style="resize: none;" inline.
   ---------------------------------------------------------------------------- */

.trs-memo-no-resize {
    resize: none;
}

/* ----------------------------------------------------------------------------
   READ-ONLY rich text, and why it needs !important

   ExpandableMarkupStringComponent renders stored Quill HTML in a datagrid cell.
   That HTML is AUTHOR DATA, and content pasted from Word or a web page arrives
   carrying its own colours - VERIFIED in the browser on /TouristGuide:

       <span style="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255);">

   White-on-white is invisible on a white row, shows as a pale box on the grey
   striped rows, and in dark mode is a light box with black text in the middle of
   a dark table. Nothing in Quill's stylesheet or ours was doing it; the styles
   are ON THE ELEMENTS, and an inline style can only be beaten with !important.

   A table cell is not the place for author colour anyway - rich text there
   should read as table text - so both properties are handed back to the row.
   The EDITOR is deliberately untouched: authors keep their colour picker, and
   .trs-rich-editor / .ql-container never match this selector.

   The overflow came off the component's inline style attribute at the same time.
   ---------------------------------------------------------------------------- */

.trs-richtext-readonly {
    overflow: hidden;
}

.trs-richtext-readonly,
.trs-richtext-readonly * {
    color: inherit !important;
    background-color: transparent !important;
}

/* ----------------------------------------------------------------------------
   CONNECTION STATE INDICATOR

   The banner that appears when the SignalR circuit drops. Its two keyframes
   were named `slideIn` and `pulse` - keyframe names are global and the last
   definition in the document wins, so two of the most guessable names in CSS
   were sitting in a component that is mounted on every page. Prefixed, like
   trs-spin and ops-live-pulse before them.

   The colours were Bootstrap's amber and red with black and white text. They
   are now the audited warning and danger pairs, so the banner reads the same
   way a closed date or an out-of-order room does.
   ---------------------------------------------------------------------------- */

.connection-state-indicator {
    position: fixed;
    top: 70px;
    right: 20px;
    z-index: 1050;
    display: flex;
    align-items: center;
    padding: var(--trs-space-3) var(--trs-space-4);
    border-radius: var(--trs-radius-sm);
    box-shadow: var(--trs-elevation-2);
    animation: trs-connection-slide-in var(--trs-motion-slow) var(--trs-ease);
}

.connection-state-reconnecting {
    background-color: var(--trs-warning-bg);
    color: var(--trs-warning);
}

.connection-state-disconnected {
    background-color: var(--trs-danger-bg);
    color: var(--trs-danger);
}

.connection-state-indicator i {
    animation: trs-connection-pulse 2s infinite;
}

@keyframes trs-connection-slide-in {
    from {
        transform: translateX(400px);
        opacity: 0;
    }
    to {
        transform: translateX(0);
        opacity: 1;
    }
}

@keyframes trs-connection-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: .5; }
}

/* The banner is the only signal that the circuit has dropped, so it must not
   depend on motion to be noticed. */
@media (prefers-reduced-motion: reduce) {
    .connection-state-indicator,
    .connection-state-indicator i {
        animation: none;
    }
}

/* ----------------------------------------------------------------------------
   EDIT PAGES

   EditAccountIndex and EditHotelIndex are the same shape - a left nav list of
   sections, a card of fields, and a save bar that appears when the form is
   dirty - and both styled it from their own <style> block. EditAccountIndex's
   was written against the bare Bootstrap class:

       .list-group-item { border-left: 3px solid transparent; ... }

   so every list group in the application picked up a left border, a transition
   and cursor:pointer !important for as long as that page was open. Scoped to
   the nav it was written for.
   ---------------------------------------------------------------------------- */

.trs-nav-list .list-group-item {
    border-left: 3px solid transparent;
    cursor: pointer;
    user-select: none;
    transition: background-color var(--trs-motion-base) var(--trs-ease),
                border-color var(--trs-motion-base) var(--trs-ease);
}

.trs-nav-list .list-group-item:hover {
    background-color: var(--trs-surface-sunken);
    border-left-color: var(--trs-fill);
}

.trs-nav-list .list-group-item.active {
    border-left-color: var(--trs-fill);
}

/* A floor, not a fixed height: one section has three fields and another seven,
   so without it the card visibly shrinks on the shorter ones. Was written as
   `.container .card-body`, which is every card in every page using a Bootstrap
   container. */
.trs-edit-page .card-body {
    min-height: 0;
}

/* The save bar, separated from the fields by a rule rather than whitespace so
   it reads as an action strip and not one more row of the form. */
.trs-unsaved-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--trs-space-4);
    margin-top: var(--trs-space-6);
    padding-top: var(--trs-space-5);
    border-top: 1px solid var(--trs-border);
}

/* A soft pill rather than a saturated badge: this is a standing state for as
   long as the form is dirty, and a hard warning colour sitting there reads as
   an error. Was Bootstrap's amber at two alphas over a hand-mixed brown. */
.trs-unsaved-note {
    display: inline-flex;
    align-items: center;
    gap: var(--trs-space-2);
    padding: var(--trs-space-2) 0.9rem;
    border-radius: var(--trs-radius-full);
    background-color: var(--trs-warning-bg-soft);
    border: 1px solid var(--trs-warning-bg);
    color: var(--trs-warning);
    font-size: var(--trs-text-sm);
    font-weight: var(--trs-weight-medium);
    line-height: 1.2;
}

.trs-unsaved-note-icon {
    font-size: var(--trs-text-sm);
}

/* On a narrow screen the pill and the button stack; the button stays full width
   so it remains the obvious target rather than a small control floating right. */
@media (max-width: 575.98px) {
    .trs-unsaved-bar {
        justify-content: stretch;
    }

    .trs-unsaved-bar > .btn {
        width: 100%;
    }
}

/* ----------------------------------------------------------------------------
   IMAGE THUMBNAIL WITH REMOVE

   A photo with a round delete button in its corner. The button's class was
   .remove-button, a name TagPicker also defined globally - for a text button
   that its markup never used.
   ---------------------------------------------------------------------------- */

.trs-thumb {
    position: relative;
    display: inline-block;
    border: 1px solid var(--trs-border-strong);
    border-radius: var(--trs-radius-md);
    cursor: pointer;
}

.trs-thumb img {
    display: block;
    border-radius: var(--trs-radius-md);
}

/* .btn. prefix, not a bare class: the Blazorise theme emits `.btn-sm {
   border-radius: 6px }` from a <style> inside <body>, which ties on specificity
   and wins on document order. Measured in the browser - the circle was coming
   out a rounded square. */
.btn.trs-thumb-remove {
    position: absolute;
    top: 2px;
    right: 2px;
    width: 22px;
    height: 22px;
    padding: var(--trs-space-1);
    border: none;
    border-radius: 50%;
    font-size: var(--trs-text-sm);
    line-height: 1;
    transition: transform var(--trs-motion-fast) var(--trs-ease);
}

.btn.trs-thumb-remove:hover {
    transform: scale(1.1);
}

/* ----------------------------------------------------------------------------
   Design-system gallery only (/design-system)

   Swatches need one class per token because the gallery, like every other
   component, may not use inline styles or colour literals. Verbose on purpose.
   ---------------------------------------------------------------------------- */

.trs-sw {
    display: flex;
    align-items: flex-end;
    justify-content: center;
    height: 54px;
    flex: 1;
    min-width: 0;
    font-family: var(--trs-font-mono);
    font-size: var(--trs-text-2xs);
    padding-bottom: var(--trs-space-1);
}

.trs-sw-row {
    display: flex;
    border-radius: var(--trs-radius-md);
    overflow: hidden;
    width: 100%;
}

.trs-spec {
    background: var(--trs-p-300);
    border-radius: var(--trs-radius-sm);
    height: 16px;
}

.trs-elev-demo {
    background: var(--trs-surface);
    border: 1px solid var(--trs-border);
    border-radius: var(--trs-radius-md);
    padding: var(--trs-space-4);
    flex: 1 1 140px;
}

.trs-elev-0 { box-shadow: var(--trs-elevation-0); }
.trs-elev-1 { box-shadow: var(--trs-elevation-1); }
.trs-elev-2 { box-shadow: var(--trs-elevation-2); }

.trs-r-sm   { border-radius: var(--trs-radius-sm); }
.trs-r-md   { border-radius: var(--trs-radius-md); }
.trs-r-lg   { border-radius: var(--trs-radius-lg); }
.trs-r-full { border-radius: var(--trs-radius-full); }
.trs-sw-p50 { background: var(--trs-p-50); color: var(--trs-text); }
.trs-sw-p100 { background: var(--trs-p-100); color: var(--trs-text); }
.trs-sw-p200 { background: var(--trs-p-200); color: var(--trs-text); }
.trs-sw-p300 { background: var(--trs-p-300); color: var(--trs-text); }
.trs-sw-p400 { background: var(--trs-p-400); color: var(--trs-on-primary); }
.trs-sw-p500 { background: var(--trs-p-500); color: var(--trs-on-primary); }
.trs-sw-p600 { background: var(--trs-p-600); color: var(--trs-on-primary); }
.trs-sw-p700 { background: var(--trs-p-700); color: var(--trs-on-primary); }
.trs-sw-p800 { background: var(--trs-p-800); color: var(--trs-on-primary); }
.trs-sw-n50 { background: var(--trs-n-50); color: var(--trs-text); }
.trs-sw-n100 { background: var(--trs-n-100); color: var(--trs-text); }
.trs-sw-n200 { background: var(--trs-n-200); color: var(--trs-text); }
.trs-sw-n300 { background: var(--trs-n-300); color: var(--trs-text); }
.trs-sw-n400 { background: var(--trs-n-400); color: var(--trs-on-primary); }
.trs-sw-n500 { background: var(--trs-n-500); color: var(--trs-on-primary); }
.trs-sw-n600 { background: var(--trs-n-600); color: var(--trs-on-primary); }
.trs-sw-n700 { background: var(--trs-n-700); color: var(--trs-on-primary); }
.trs-sw-n800 { background: var(--trs-n-800); color: var(--trs-on-primary); }
/* The alias row carries labels like the ramps above it, so each needs a foreground
   that reads on its own fill. These were background-only, which is why the markup
   rendered them empty: a label would have been invisible on half of them - the
   `text` swatch is near-white in dark mode, and so is the inherited text colour.

   Three groups, by what the token IS rather than what it looks like:
     grounds and borders -> --trs-text, the colour that normally sits ON them
     foreground tokens   -> --trs-bg, the inverse, because here the FOREGROUND
                            colour is being used as a fill
     eucalyptus          -> --trs-on-primary, same as the p-400+ ramp steps */
.trs-sw-bg { background: var(--trs-bg); color: var(--trs-text); }
.trs-sw-surface { background: var(--trs-surface); color: var(--trs-text); }
.trs-sw-surface-sunken { background: var(--trs-surface-sunken); color: var(--trs-text); }
.trs-sw-text { background: var(--trs-text); color: var(--trs-bg); }
.trs-sw-text-secondary { background: var(--trs-text-secondary); color: var(--trs-bg); }
.trs-sw-text-muted { background: var(--trs-text-muted); color: var(--trs-bg); }
.trs-sw-text-primary { background: var(--trs-text-primary); color: var(--trs-on-primary); }
.trs-sw-fill { background: var(--trs-fill); color: var(--trs-on-primary); }
.trs-sw-border { background: var(--trs-border); color: var(--trs-text); }
.trs-sw-border-strong { background: var(--trs-border-strong); color: var(--trs-text); }

/* gallery: spacing scale bars */
.trs-sp-1 { width: var(--trs-space-1); }
.trs-sp-2 { width: var(--trs-space-2); }
.trs-sp-3 { width: var(--trs-space-3); }
.trs-sp-4 { width: var(--trs-space-4); }
.trs-sp-6 { width: var(--trs-space-6); }
.trs-sp-8 { width: var(--trs-space-8); }
.trs-sp-12 { width: var(--trs-space-12); }
.trs-sp-16 { width: var(--trs-space-16); }

/* ----------------------------------------------------------------------------
   Promoted from per-file modal style blocks (Phase 4 batch 5)

   Every class is trs- prefixed on the way in. The names these carried in the
   modals - .info-field, .inline-create-type - were fine while scoped to one
   modal's own style element, but become app-wide the moment they land here.
   ---------------------------------------------------------------------------- */

/* was .input-file-component-holder > label, in 4 modals.
   Selector unchanged: that class is bespoke to InputFileComponent and
   app.css already owns rules for it, so no markup has to change. */
.input-file-component-holder > label {
    width: 100%;
    margin-bottom: 0;
}

/* was .info-field, in 3 audit/GDPR detail modals */
.trs-info-field {
    padding: var(--trs-space-2);
    border-radius: var(--trs-radius-sm);
    background-color: var(--trs-surface-sunken);
}

.trs-info-field small {
    font-size: var(--trs-text-xs);
    text-transform: uppercase;
    letter-spacing: var(--trs-tracking-label);
    margin-bottom: var(--trs-space-1);
}

/* Audit detail fields hold ids, JSON fragments and user agents - values with no
   spaces to break at, which otherwise run out of the field. */
.trs-info-field--wrap {
    overflow-wrap: break-word;
    word-break: break-all;
}

/* was .bulk-target-room-row / -chip, in 3 bulk-change pricing modals */
.trs-chip-well {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--trs-space-1);
    max-height: 110px;
    overflow-y: auto;
    padding: var(--trs-space-2);
    border: 1px dashed var(--trs-border-strong);
    border-radius: var(--trs-radius-sm);
    background: var(--trs-surface-sunken);
}

.trs-chip {
    display: inline-flex;
    align-items: center;
    padding: var(--trs-space-1) var(--trs-space-3);
    background: var(--trs-p-50);
    border: 1px solid var(--trs-p-400);
    color: var(--trs-text-primary);
    border-radius: var(--trs-radius-full);
    font-size: var(--trs-text-sm);
    font-weight: var(--trs-weight-medium);
    line-height: 1.3;
    max-width: 260px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* was .inline-create-type, in the 2 hotel-room modals */
.trs-inline-panel {
    border: 1px solid var(--trs-border);
    border-radius: var(--trs-radius-sm);
    background-color: var(--trs-surface-sunken);
}

/* TrsButtonVariant.Ghost - a low-emphasis action with no fill and no border.
   Blazorise has no ghost colour, and Color.Light paints a solid that reads as
   a near-black block on a dark ground, so it is done here instead. */
.trs-btn-ghost {
    background-color: transparent;
    border-color: transparent;
    color: var(--trs-text-secondary);
}

.trs-btn-ghost:hover,
.trs-btn-ghost:focus {
    background-color: var(--trs-surface-sunken);
    border-color: transparent;
    color: var(--trs-text-primary);
}

/* The destructive ghost, for a datagrid row's delete.

   TrsButtonVariant.DangerGhost is danger-OUTLINE, which is the right thing for
   a modal's Cancel but draws a full border box; four bordered boxes in a table
   row is exactly the noise the row-action standard exists to remove. This is
   the quiet form - red glyph, no fill, no border, red wash on hover. */
.trs-btn-ghost--danger {
    color: var(--trs-danger);
}

.trs-btn-ghost--danger:hover,
.trs-btn-ghost--danger:focus {
    background-color: var(--trs-danger-bg);
    color: var(--trs-danger);
}

/* The pinned footer block at the bottom of the sidebar. Was an inline
   Style="flex-shrink: 0; background: white" - an inline style beats every
   stylesheet rule, so it stayed white in dark mode with the buttons on it
   measuring 2.29:1 and 1.90:1. */
.trs-nav-footer {
    flex-shrink: 0;
    background: var(--trs-surface);
}

/* The shared "(optional)" marker. Was an inline Style="color:darkgray",
   which is #A9A9A9 - 2.35:1 on white, well under AA, and it appears beside
   every optional field in the app. --trs-text-muted is the AA-verified
   equivalent at 5.20:1. */
.trs-optional-label {
    color: var(--trs-text-muted);
    font-size: var(--trs-text-xs);
}

.form-label + .trs-optional-label {
    margin-left: var(--trs-space-1);
}

/* ----------------------------------------------------------------------------
   Replacements for inline styles that hardcoded a light background.

   Each of these rendered a white or near-white block in dark mode, because an
   inline style beats every stylesheet rule - which is precisely why the
   ratchet counts them.
   ---------------------------------------------------------------------------- */

/* A generated password, token or URL shown for copying. Was
   Style="...background:#fff;border:1px solid #ddd" in three places. */
.trs-code-block {
    font-family: var(--trs-font-mono);
    font-size: var(--trs-text-base);
    padding: var(--trs-space-3);
    background: var(--trs-surface-sunken);
    border: 1px solid var(--trs-border);
    border-radius: var(--trs-radius-sm);
    word-break: break-all;
    margin-top: var(--trs-space-2);
}

/* A recessed panel - a card inside a card, a well. Stands in for Bootstrap's
   Background.Light, which is a fixed #f8f9fa and therefore a pale box on a dark
   surface. */
.trs-surface-sunken {
    background-color: var(--trs-surface-sunken);
}

/* Monospace where the CONTENT is a machine string - an API key, an id - and the
   character shapes matter. Was `Style="font-family: monospace"`, which also skips
   the loaded DM Mono for whatever the OS happens to have. */
.trs-mono {
    font-family: var(--trs-font-mono);
}

/* The small circled "i" beside a setting. The help cursor is the whole signal
   that it is explanatory rather than clickable, so it is not decoration. */
.trs-hint-icon {
    cursor: help;
}

/* A card header that expands and collapses its own body. Grows to fill the header
   so the whole strip is the hit target, not just the words. */
.trs-collapse-toggle {
    flex: 1 1 auto;
    cursor: pointer;
}

/* The floating chat panel. */
.trs-chat-popover {
    position: absolute;
    right: 0;
    top: 100%;
    width: 380px;
    max-height: 500px;
    z-index: 1050;
    background: var(--trs-surface);
}

/* The @-mention dropdown above the message box. */
.trs-scroll-panel {
    max-height: 150px;
    overflow-y: auto;
    background: var(--trs-surface);
}

/* Rate-grid "Needs attention" header and its detail rows. */
.trs-row-warning {
    background-color: var(--trs-warning-bg);
}

.trs-row-warning-soft {
    background-color: var(--trs-warning-bg-soft);
}

/* Floor-plan side panel. Capped to the viewport so a long reservation form
   cannot push its submit button out of reach - that behaviour is load-bearing,
   so it moves across intact. */
/* The panel is a column of the ops row, so its ceiling is that row - not the
   viewport. `calc(100vh - 150px)` was a second guess at the same chrome the row
   was already guessing at, and 50px adrift from it; with the row now sizing
   itself, 100% is exact and stays exact. */
.trs-side-panel {
    width: 360px;
    min-width: 360px;
    background: var(--trs-surface);
    border: 1px solid var(--trs-border);
    max-height: 100%;
    overflow-y: auto;
}

/* Unread/pending count bubble on a toolbar icon. Was defined identically in
   seven files, each with its own #dc3545. */
.trs-count-badge {
    position: absolute;
    top: -5px;
    right: -5px;
    background-color: var(--trs-danger);
    color: var(--trs-n-50);
    border-radius: var(--trs-radius-full);
    width: 20px;
    height: 20px;
    font-size: var(--trs-text-xs);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1;
}

/* Card hover lift, scoped to the page that opts in.

   Six dashboard pages each defined a bare `.card:hover { translateY(-2px) }`
   in their own style block. Promoted as-is that would lift every card in the
   application, modals included - so it is scoped to the page wrapper instead
   and only applies where TrsPageShell is given Class="trs-lift". */
.trs-lift .card {
    transition: transform var(--trs-motion-base) var(--trs-ease);
}

.trs-lift .card:hover {
    transform: translateY(-2px);
}

@media (prefers-reduced-motion: reduce) {
    .trs-lift .card,
    .trs-lift .card:hover {
        transition: none;
        transform: none;
    }
}


/* ----------------------------------------------------------------------------
   ELLIPSIS

   Thirteen files use .ellipsis; exactly two defined it, both inside their own
   <style> block (RoomAvailabilityTable and StaffAvailabilityTable). A <style>
   block only exists while its component is rendered, so the other eleven got
   truncation or did not depending on whether an availability table happened to
   be on the same page. Kept under its existing name - it is a plain utility
   and there are thirteen call sites.
   ---------------------------------------------------------------------------- */

.ellipsis {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ----------------------------------------------------------------------------
   LEGEND

   A bounded "badge -> what it means" key, sitting beside a page heading. The
   grid is two columns - badge, then explanation - so the badges right-align
   against a single edge however long the wording gets, which is what makes it
   scannable in a second language.
   ---------------------------------------------------------------------------- */

/* Sits beside the filters rather than in a band above them - see the comment in
   AccessLogDatagrid.razor. It keeps its natural width and does not stretch; the
   filters take what is left. */
.trs-filter-legend {
    display: flex;
    align-items: flex-start;
    flex-wrap: wrap;
    gap: var(--trs-space-3);
}

/* The basis is the width below which putting these side by side stops helping -
   under it the pair wraps and the legend goes back under the filters. */
.trs-filter-legend-main {
    flex: 1 1 440px;
    min-width: 0;
}

.trs-filter-legend > .trs-legend {
    flex: 0 0 auto;
}

/* Was `border rounded bg-light px-3 py-2 small` in the markup - Bootstrap spacing
   and surface utilities standing in for tokens. Correcting an earlier note here:
   bg-light is NOT broken in dark mode. tokens.css retargets Bootstrap's own
   --bs-light-rgb per theme and dark.css overrides .bg-light on top of that, so it
   follows the theme correctly. This swap is for consistency with the rest of the
   design system, not a bug fix. */
.trs-legend {
    max-width: 480px;
    padding: var(--trs-space-2) var(--trs-space-3);
    border: 1px solid var(--trs-border);
    border-radius: var(--trs-radius-md);
    background-color: var(--trs-surface-sunken);
    font-size: var(--trs-text-sm);
}

.trs-legend-title {
    font-size: var(--trs-text-xs);
    letter-spacing: .06em;
    text-transform: uppercase;
}

.trs-legend-grid {
    display: grid;
    grid-template-columns: max-content 1fr;
    column-gap: var(--trs-space-3);
    row-gap: .35rem;
    align-items: center;
}

/* ----------------------------------------------------------------------------
   THE APP SHELL: THE CONTENT SCROLLS, THE FRAME DOES NOT

   The sider was already outside the scroll; the header and the EU footer were
   inside it and rode up with the content. Pinning them with `position: sticky`
   fixed the pages that were checked and left the ones that were not, because
   sticky only holds while the element the browser is actually scrolling is the
   one the bar sits in. Any page that ends up scrolling something ELSE - the
   document, or `main` itself - scrolls the bar with it. That is a promise the
   layout cannot keep, so the layout is changed instead of the bars.

   Blazorise already builds the right shape: `section.b-layout` is a flex column
   holding header, main and footer, with the two bars at `flex: 0 0 auto` and
   `main` at `flex: 1 1 0%`. The only thing wrong was WHICH element owned the
   overflow. Moving it from the column to `main` means the bars are not pinned
   over a scrolling area - there is no scrolling area under them at all. They
   cannot move because nothing they are in moves.

   This also collapses the double-scrollbar class of bug for good. There is one
   scroll container per page by construction: on a grid page the fill-height chain
   hands the leftover to the grid's own scroller and `main` fits exactly, so it
   never scrolls; on every other page `main` scrolls and nothing else does.

   `min-height: 0` on `main` is what allows both. A flex item will not shrink
   below its content while min-height is auto, and without shrinking there is
   nothing for `overflow` to act on - the content would push the footer off the
   bottom of a column that can no longer scroll to reach it.

   Nothing scripts this scroller: the scrollTop/scrollIntoView calls in
   scripts.js and modalScripts.js all target chat containers and modal bodies,
   which are unaffected by where the page scroll lives. */
section.b-layout:not(.b-layout-root) {
    overflow: hidden;
}

main.b-layout-content {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
}

/* Neither bar may shrink to make room for content - being squeezed is the other
   way a fixed frame fails. */
.b-layout-header,
.b-layout-footer {
    flex: 0 0 auto;
}

/* THE BACKGROUNDS ARE NOT COSMETIC. Both bars compute to rgba(0,0,0,0) and only
   ever looked solid because the page ground showed through them; with content
   scrolling under them they have to paint. --trs-bg, not --trs-surface: surface
   is white, which is also the nav's colour, and painting them that turned the nav
   and header into one L-shaped slab. Dark mode paints both from dark.css, which
   loads after this file and still wins. */
.b-layout-header {
    background-color: var(--trs-bg);
    border-bottom: 1px solid var(--trs-border);
}

.b-layout-footer {
    background-color: var(--trs-bg);
    border-top: 1px solid var(--trs-border);
}

.b-layout-sider {
    border-right: 1px solid var(--trs-border);
}

/* ----------------------------------------------------------------------------
   BOOTSTRAP 4'S HOVER DARKEN, WHICH IS NOT THIS APP'S PALETTE

   App.razor loads Bootstrap 4.6.1 AND 5.3.2. Bootstrap 4 shipped a set of rules
   that darken a background utility when it is on a link or a button:

       button.bg-primary:hover { background-color: #0062cc; }

   The colour is a hardcoded Bootstrap 4 blue - not a variable, so nothing in
   tokens.css reaches it - and Bootstrap 5 dropped the rule entirely. At (0,2,1)
   it out-specifies the (0,1,0) `.bg-primary` that Blazorise and Bootstrap 5 both
   write, so it wins on every one of the eight variants.

   MEASURED on /Zodomus: the accordion headers are `button.accordion-button
   .bg-primary`, and hovering one turned the whole brand-green bar rgb(0,98,204),
   Bootstrap 4 blue. It applies to any <a> or <button> carrying a bg- class, so
   this is not one page's problem.

   Restoring Bootstrap 5's behaviour - no change on hover - by pointing each rule
   back at the same value the base class uses. Reading the --bs-*-rgb variables
   rather than repeating a literal means these also follow the theme, which the
   Blazorise-generated literals do not.

   !important IS REQUIRED, and a first pass without it did nothing. Bootstrap 4
   marks these hover rules important, and Blazorise marks its own `.bg-primary`
   important too - so at rest the (0,1,0) important from Blazorise wins and the
   bar is green, and on hover Bootstrap 4's (0,2,1) important outranks it and the
   bar turns blue. Matching both the specificity and the priority is the only way
   in; this file loads after Bootstrap, so an equal-weight rule wins on order.

   A real hover affordance on the accordion header would be a separate,
   deliberate addition. */
a.bg-primary:hover,
a.bg-primary:focus,
button.bg-primary:hover,
button.bg-primary:focus {
    background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity, 1)) !important;
}

a.bg-secondary:hover,
a.bg-secondary:focus,
button.bg-secondary:hover,
button.bg-secondary:focus {
    background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity, 1)) !important;
}

a.bg-success:hover,
a.bg-success:focus,
button.bg-success:hover,
button.bg-success:focus {
    background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity, 1)) !important;
}

a.bg-info:hover,
a.bg-info:focus,
button.bg-info:hover,
button.bg-info:focus {
    background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity, 1)) !important;
}

a.bg-warning:hover,
a.bg-warning:focus,
button.bg-warning:hover,
button.bg-warning:focus {
    background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity, 1)) !important;
}

a.bg-danger:hover,
a.bg-danger:focus,
button.bg-danger:hover,
button.bg-danger:focus {
    background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity, 1)) !important;
}

a.bg-light:hover,
a.bg-light:focus,
button.bg-light:hover,
button.bg-light:focus {
    background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity, 1)) !important;
}

a.bg-dark:hover,
a.bg-dark:focus,
button.bg-dark:hover,
button.bg-dark:focus {
    background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity, 1)) !important;
}

/* ----------------------------------------------------------------------------
   PAGE MEASURE

   A settings page is a column of forms, and a form field stretched across a
   1900px monitor is unreadable and unpleasant to fill in - so these pages have
   always been capped. Each one did it with its own inline `max-width`, and no two
   agreed: 1000, 1100 four times, 1200 twice, 1400. Eight arbitrary numbers, eight
   inline styles the ratchet counts, and eight pages that lined up differently as
   you moved between them.

   Two measures, on the page shell. 1100 is a column of form fields - the common
   case. The wide one is for a page whose content is a TABLE, which needs the room:
   dropping the cap entirely instead, as a first pass did, left eVisitor Compliance
   running the full 2329px of an ultrawide, which is worse than the 1400 it had. */
/* ONE measure for the whole admin section, on Marko's call, because the section
   was drifting: the settings pages each had their own inline max-width (1000 to
   1400) and Booking Channels had a fourth at 1600, so the content column moved
   every time you navigated. A second `-wide` step was tried and dropped - two
   steps still means two widths, and predictability was the point.

   1400 is the compromise. It costs Booking Channels 200px, whose rooms table was
   measured to need the room; if that table starts scrolling, this number is the
   thing to revisit, not the page.

   CENTRED, which is what these pages did before the measure existed. A
   left-aligned pass came first, to keep the h1 from shifting between a full-width
   page and a capped one - the wrong trade, because on a 2560px monitor it left
   about 1200px of dead space down one side. A page that looks broken is worse
   than a heading that moves. */
.trs-page-measure {
    max-width: 1400px;
    margin-inline: auto;
}

/* ----------------------------------------------------------------------------
   CONTEXT BAR

   The line under a statistics page's filters that says what is being counted -
   the date range, the hotel, the staff member. On all six pages it was an
   <Alert Color="Color.Light">.

   Correcting an earlier note here, which claimed dark.css did not reach alert
   colours: it does. dark.css restyles .alert-light from tokens, so the alert was
   themed correctly. The reason to change it was never the theme.

   The reason is that it is not an alert. Nothing is wrong and there is nothing to
   dismiss; it
   is a caption for the charts below it. A rule and quiet text say that, and stop
   it competing with the toolbar above and the cards below. */
/* Block, not flex. The line reads as a sentence - "Showing data for: 10.08.2026
   - 09.09.2026 | Hotel Adriatic" - so the dates, the dash and the pipes have to
   stay in normal inline flow; as flex items each one became its own gapped box
   and the range fell apart into three. */
.trs-context-bar {
    padding: var(--trs-space-2) 0;
    line-height: 1.6;
    border-top: 1px solid var(--trs-border);
    border-bottom: 1px solid var(--trs-border);
    color: var(--trs-text-secondary);
    font-size: var(--trs-text-sm);
}

/* The values themselves - the dates, the hotel name - are the part being read. */
.trs-context-bar strong {
    color: var(--trs-text);
    font-weight: var(--trs-weight-semibold);
}

.trs-context-bar .fa,
.trs-context-bar .fas,
.trs-context-bar .fa-solid,
.trs-context-bar svg {
    color: var(--trs-text-muted, var(--trs-text-secondary));
}

/* ----------------------------------------------------------------------------
   CHARTS

   Chart.js sizes a canvas from its aspect ratio, not from the box it is in. The
   six statistics pages all asked for AspectRatio 1.5 and left MaintainAspectRatio
   at its default of true, which means the WIDTH decides the height: in a
   half-width card on a 1600px screen a chart came out 466px tall, and two rows of
   them made a page nearly three screens long with the numbers that matter pushed
   under the fold. On an ultrawide it was worse - the wider the monitor, the taller
   the chart.

   Turning MaintainAspectRatio off inverts that. The canvas then fills its parent,
   so the parent has to be the one with a height, and it has to be positioned -
   Chart.js measures the nearest positioned ancestor and will grow without bound
   inside a static one. That is what this wrapper is for. Height is fixed rather
   than a ratio so a row of charts lines up whatever each one is plotting, and so
   the page is the same length on a laptop and on an ultrawide. */
.trs-chart {
    position: relative;
    height: 260px;
    width: 100%;
}

/* A pie carries its legend inside the same canvas, so the plot itself only gets
   what the legend leaves. At 260px the circle was under 140px across with five
   slices in it. */
.trs-chart-pie {
    height: 300px;
}

/* A horizontal bar chart's height is its category count - twelve rooms in 260px
   is a 12px band per row with a label that does not fit between the ticks. */
.trs-chart-tall {
    height: 340px;
}

/* CARDS IN A ROW MATCH.

   The three heights above are MINIMUMS, not the final answer. Two cards sitting
   side by side rarely want the same one - a pie needs 300 and the bar beside it
   needs 260, and their headers differ too (one carries a Details button, one does
   not) - so /OccupancyStatistics had a 376px card next to a 341px one and a 416
   next to a 336. Two cards of different height in one row read as two unrelated
   things rather than one comparison.

   Bootstrap already stretches the columns to the tallest in the line, so the fix
   is to let the card fill the column it is in and hand the leftover down to the
   chart. The tallest MINIMUM in the row then sets the row, and every canvas in it
   comes out the same height - which is also what makes two bar charts actually
   comparable, since a bar's length only means something against a consistent
   frame.

   Scoped with :has() to cards that contain a chart: a card holding a table or a
   form should still be as tall as its content. */
/* EVERY card on the line, not only the ones holding a chart. /AuditDashboard puts
   all six cards in one wrapping row, and its last line pairs a 416px chart with a
   207px two-figure summary - the same mismatch, between a chart card and a card
   that simply has less in it. `.row:has(.trs-chart)` catches the whole row. */
.row:has(.trs-chart) > [class*="col"] > .trs-card {
    height: 100%;
    display: flex;
    flex-direction: column;
}

.row:has(.trs-chart) > [class*="col"] > .trs-card > .card-body {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

/* A card with no chart has nothing inside it that grows, so its content would sit
   at the top of a box sized by the chart beside it. Centred, the extra height
   reads as breathing room rather than as a card that failed to fill. */
.row:has(.trs-chart) > [class*="col"] > .trs-card:not(:has(.trs-chart)) > .card-body {
    justify-content: center;
}

/* height: auto is load-bearing - the fixed height above would win over flex-grow
   and the card would keep its own size inside a taller row. The min-height is
   what stops a chart collapsing when it is the only thing in its row. */
.trs-card > .card-body > .trs-chart {
    flex: 1 1 auto;
    height: auto;
    min-height: 260px;
}

.trs-card > .card-body > .trs-chart-pie {
    min-height: 300px;
}

.trs-card > .card-body > .trs-chart-tall {
    min-height: 340px;
}

/* Nothing to plot. Every update method already refuses to draw an empty series,
   so without this the card held a BLANK WHITE BOX the size of a chart - four of
   them on /MyStatistics for a member of staff with a quiet month, which reads as
   the page having failed rather than as the period being empty. Same height as
   the chart so the row keeps its shape. */
.trs-chart-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px dashed var(--trs-border);
    border-radius: var(--trs-radius-md, 8px);
    background: var(--trs-surface-sunken);
}

/* ----------------------------------------------------------------------------
   NARROW SCREENS: the grid could not scroll, so it squeezed

   THIS BLOCK MUST STAY LAST IN THE FILE. A media query adds no specificity, so
   these rules only beat the fill-the-height chain above on source order - placed
   earlier they matched, computed, and lost silently.

   Two rules in app.css cancel each other out. TrsDataGrid sets Responsive by
   default, which wraps the table in Bootstrap's .table-responsive - a horizontal
   scroller. But app.css:129 gives every .b-datagrid table
   `table-layout: fixed; width: 100%`, so the table can never be WIDER than that
   wrapper, and a scroller round something that always fits never appears.

   What happens instead is app.css:135 - `overflow: hidden; text-overflow:
   ellipsis` on every cell. On a phone, five columns divide 380px between them
   and each one ellipsises. On /Operations the Error column, which is the entire
   reason the page exists, renders as "Fail...".

   Below the layout's own breakpoint, let the table size to its content and give
   it a floor wider than any phone. The wrapper then has something to scroll, and
   reading sideways beats reading nothing. Above 992px nothing changes.
   ---------------------------------------------------------------------------- */

@media (max-width: 991.98px) {
    /* `table.b-datagrid`, NOT `.b-datagrid table`. The class is ON the table, so
       the descendant form matches nothing - app.css:129 has the same mistake and
       gets away with it because the `.table-responsive > table` beside it does
       the work. Checked in the browser after the first attempt computed to
       min-width:100% and I went looking for why. */
    .table-responsive > table,
    .table-responsive > table.b-datagrid {
        table-layout: auto;
        /* (0,2,1). app.css:676 sets `width: auto; min-width: 100%` on the same
           element at (0,2,0), so anything less than this computes to 100% of a
           433px container and the columns are back to one word per line - which
           is what /SuperadminDashboard's address column did on the first pass. */
        width: auto;
        min-width: 720px;
    }

    /* Except when there is nothing to scroll TO. An empty grid renders its
       TrsEmptyState in one full-width cell, so a 720px floor centred "No data
       available" in 720px and pushed it off a 390px screen - you had to scroll
       sideways to read that there was nothing there. MEASURED on /GDPRAdmin.

       (0,2,1) on purpose: app.css:680 sets `min-width: 100%` at (0,2,0), so this
       needs the extra element to win rather than relying on load order. */
    table.b-datagrid:has(.trs-empty) {
        table-layout: fixed;
        /* app.css:676 sets `width: auto` at (0,2,0), which content-sizes the table
           even under fixed layout - so clearing min-width alone left it at 514px on
           a 433px screen. Both have to be said. */
        width: 100%;
        min-width: 0;
    }

    /* A grid that hides its secondary columns below md (d-none d-md-table-cell)
       has already chosen what a phone shows, and the 720px floor would still make
       it scroll sideways past them. MEASURED on the lock credentials modal at
       390: four columns left, table 720px in a 340px wrapper. */
    .table-responsive > table.b-datagrid.trs-grid--fit {
        min-width: 100%;
    }

    /* The ellipsis was only there to cope with the squeeze. With room to lay out,
       a cell should wrap rather than silently drop its content. */
    .b-datagrid td,
    .b-datagrid th {
        overflow: visible;
        text-overflow: clip;
        white-space: normal;
    }

    /* AND THE PAGE SCROLLS AGAIN.

       The fill-the-leftover-height chain above assumes one grid is the page's
       main content and that there is leftover height to give it. Neither holds
       on a phone. MEASURED on /Operations at 390px: four stat tiles take 480px
       of an 844px viewport, the two grid Cards below them get what is left,
       `min-height: 0` lets them shrink to satisfy it - and both collapsed to
       1px while `main` overflowed by 360px with nothing scrollable. The tables
       were not cramped, they were GONE.

       Below the breakpoint the model is simply wrong: you scroll a phone, you do
       not fit a table into the space left over. So the chain is switched off and
       the document scrolls, which is what every other page on a phone does. */
    /* `main` keeps min-height: 0 and its overflow here - it is the scroller in
       both models now, so a phone scrolls main rather than the document. What
       does switch off is the chain BELOW it: the grid stops trying to fit the
       leftover height and simply grows, and main scrolls to reach it. Setting
       min-height: auto here, as an earlier pass did, would stop main shrinking
       and push the content into a column that can no longer scroll to it. */

    .content:has(.trs-grid-shell),
    .content:has(.trs-grid-shell) *:has(.trs-grid-shell),
    .content:has(.trs-grid-shell) .trs-grid-shell,
    .content:has(.trs-grid-shell) .trs-grid-shell > form {
        display: block;
        height: auto;
        min-height: auto;
        flex: none;
    }

    /* !important for the same reason the desktop rule needs it: Blazorise writes
       max-height as an INLINE style from FixedHeaderDataGridMaxHeight. */
    .content:has(.trs-grid-shell) .table-fixed-header {
        max-height: none !important;
        min-height: auto;
        flex: none;
    }

    /* A stat tile is one number. Stacked four deep on a phone the centred layout
       spent 480px saying four things, so it turns on its side: icon, figure and
       label on one line at a third of the height. */
    .trs-stat-tile {
        flex-direction: row;
        align-items: center;
        justify-content: flex-start;
        gap: var(--trs-space-3);
        padding: var(--trs-space-3) var(--trs-space-4);
        text-align: left;
    }

    .trs-stat-label {
        margin-left: auto;
        text-align: right;
    }
}

/* ----------------------------------------------------------------------------
   PAGER  —  PaginationComponent

   One control, one size, everywhere it is used: pickers, workdays, kitchen
   orders, the notice board, the availability tables and the floor plan list.

   It shipped with Padding.Is2 and Bootstrap's default link metrics, which is a
   chunky control to put in a dialog footer. The sizing lives here rather than
   at any one call site, so the eight screens that use it cannot drift apart.

   Blazorise's own DataGrid pager is NOT covered - that one is its own control
   and is left alone.
   ---------------------------------------------------------------------------- */

.trs-pager {
    margin: 0;
    justify-content: center;
}

.trs-pager .page-item {
    margin: 0 1px;
}

.trs-pager .page-link {
    padding: 0.1rem 0.45rem;
    min-width: 1.6rem;
    font-size: var(--trs-text-sm);
    line-height: 1.6;
    text-align: center;
    border: none;
    border-radius: var(--trs-radius-sm);
    background: transparent;
    color: var(--trs-text-secondary);
}

.trs-pager .page-link:hover {
    background: var(--trs-surface-sunken);
    color: var(--trs-text);
}

/* The current page is the one thing in a pager that needs weight. */
.trs-pager .page-item.active .page-link {
    background: var(--trs-selected);
    color: var(--trs-text);
    font-weight: var(--trs-weight-semibold);
}

.trs-pager .page-item.disabled .page-link {
    background: transparent;
    color: var(--trs-text-muted);
    opacity: 0.5;
}

/* ----------------------------------------------------------------------------
   eVISITOR

   The check-in review modal scrolls inside itself rather than growing past the
   viewport: it stacks a verdict bar, the per-guest strip and every guest's full
   form, and reception needs the footer buttons in reach while working down it.
   ---------------------------------------------------------------------------- */

.trs-evisitor-review-body {
    max-height: 74vh;
    overflow-y: auto;
}

.trs-evisitor-guest-col {
    width: 45%;
}

/* Wide enough for the longest translated status ("Awaiting confirmation" /
   "Čeka potvrdu") without the select truncating it. */
.trs-evisitor-status-filter {
    min-width: 200px;
}

/* A request or response body from an eVisitor call log. Capped and scrolled so
   one large payload cannot push the rest of the activity list off screen, and
   wrapped so a long single-line JSON string does not force horizontal scroll. */
.trs-evisitor-payload {
    max-height: 16rem;
    overflow: auto;
}

.trs-evisitor-payload pre {
    white-space: pre-wrap;
    word-break: break-word;
}
