/* ============================================================================
   mobile.css — phone/tablet viewport layer for AdvEntLotto.Web

   Loaded from Components/App.razor AFTER the Telerik theme (the last <head>
   stylesheet). Presentation only: Phases 0-6 change no Razor markup, no C#,
   no ids, no class names, no @ref targets. The only JS is one delegated click
   listener in App.razor (drawer auto-close), which never calls into .NET.

   ── EVERY RULE IN THIS FILE IS PREFIXED WITH `html` ─────────────────────────
   Not for style. The 31 inline <style> blocks in this app render in <body>,
   i.e. LATER in document order than this <head> link, and media queries add
   zero specificity. A plain `.content{…}` here therefore LOSES to `.content{…}`
   in a body block — and 10 pages already declare exactly that (Reports.razor:7
   plus all 9 Report*.razor at ~:12, all `.content{overflow-y:auto!important}`).
   `html .content` is (0,1,1) and beats (0,1,0) from any body block regardless
   of order, and it keeps `!important` in reserve as a separate lever.
   Do not drop the prefix.

   ── `screen and` IS MANDATORY ON EVERY QUERY ────────────────────────────────
   Without it a width query also evaluates against PAPER WIDTH when printing,
   and the 9 report pages have `@media print { .content { margin:0; padding:0 } }`
   blocks that would then fight these rules.

   ── BREAKPOINTS ────────────────────────────────────────────────────────────
   Phone   : @media screen and (max-width:  767.98px)   (Bootstrap 5 `md`)
   Drawer  : @media screen and (max-width: 1023.98px)   (also covers tablets)
   The 1024x768 POS terminal matches NEITHER (both are exclusive below 1024).
   The 14 legacy `(max-width:1024px) and (max-height:768px)` queries stay
   byte-for-byte untouched.

   ── THE LEGACY POS QUERY *DOES* FIRE ON SHORT PHONES ───────────────────────
   Verified against app.css:575-713. `(max-width:1024px) and (max-height:768px)`
   matches any viewport <=1024 wide AND <=768 tall — which includes 375x667
   (iPhone SE/8) and 414x736, but NOT 390x844 / 414x896 / 430x932, whose
   heights exceed 768. So the legacy density tweaks layer under this file on
   SHORT phones only. Every overlapping declaration below outranks it
   ((0,1,1)+ vs its (0,1,0)), so behaviour is consistent — but it is why the
   input-zoom and grid-font rules below are REQUIRED, not cosmetic: on a
   375x667 phone app.css:695 sets `.input-field{font-size:12px}`, which is a
   harder iOS auto-zoom trigger than the 14px base.

   ============================================================================
   HARD PROHIBITIONS — each of these breaks working functionality
   ============================================================================

   1. NEVER SET `display` ON `.row`.
      TicketSell.razor:44 and CashDrawerOperations.razor:204 define
      `.hidden { display: none }` at (0,1,0) and apply it to `.row` elements
      (TicketSell.razor:155 and :191 render `class="row @(_isSingle ? "" : "hidden")"`).
      Any mobile rule setting `display` on `.row` above (0,1,0) un-hides the
      inactive Single/Bulk panel. Looks routine; is a functional break.

   2. NO `color` OR `background` DECLARATIONS ANYWHERE.
      NotificationService.MapColour routes on raw hex literals
      (#dc3545 -> error, #198754 -> success). Normalizing a colour silently
      downgrades an error toast to info. Same applies to the OnRowRender
      semantic row classes (settlement-row-*, resettle-row-*,
      ticket-row-duplicate, reorder-alert-row, import-*-row) and
      BookManager.GetStatusCss.
      THE RULE APPLIES TO ELEMENTS THE APP ALREADY HAD. It is about never
      RESTYLING something whose colour already carries meaning. There are two
      classes of exception, both safe for the same underlying reason — nothing
      routes on them:

      (a) PSEUDO-ELEMENTS. A ::before/::after box exists nowhere in the app's
          own cascade. Used by the Phase 1b drawer scrim
          (`.app.sidebar-closed::after`) and the Phase 1d portrait-lock overlay
          (`body::before`).

      (b) PHASE 7 COMPONENTS. `.mobile-card`, `.mobile-sheet`, `.rpt-card` and
          friends are NEW DOM introduced under Components/Mobile. They did not
          exist when MapColour and the OnRowRender classes were written, so no
          hex literal is ever read back off them, and a card list is not
          buildable without giving the card a background. Declaring their own
          colours is correct — do not "fix" those rules to comply with this
          prohibition.

      WHERE (b) STILL HAS TEETH: MobileCardList forwards the OnRowRender
      semantic class onto `.mobile-card` via its RowClass parameter, so the
      card's own background must never outrank it. `html .mobile-card` is
      (0,1,1) and NOT !important, while `.settlement-row-*`, `.resettle-row-*`,
      `ticket-row-duplicate` and `reorder-alert-row` are all (0,1,0) !important
      — so those still win, which is what we want. Keep it that way: never add
      !important to a .mobile-card background.

      KNOWN GAP for whoever adopts MobileCardList on the import screens:
      `.import-failed-row td` and `.import-exists-row td` (ImportGames.razor:74
      and :79, ImportBooks.razor:82) are scoped to `td`. A card has no <td>, so
      those rows would render as plain white cards and silently lose the only
      signal that an import row failed. Add a card-shaped selector before using
      the card list there.

   3. NO RENAMES, NO ADDED OR REMOVED CLASSES, NO NEW DOM NODES.
      The drawer scrim is an ::after pseudo-element for exactly this reason.

   4. NEVER `display:none` ANYTHING HOSTING A `@ref`, AN `id` USED BY
      `getElementById`, OR A `tabindex`. A display:none element cannot receive
      .focus(), which silently kills the scanner and OTP flows.

   5. NEVER SET `max-height` ON `.modal-body`.
      ManageStoresModal.razor:457 uses `modal-dialog-scrollable`, whose whole
      mechanism is `.modal-content{max-height:100%}` + `.modal-body{overflow-y:auto}`.
   ============================================================================ */


/* ══════════════════════════════════════════════════════════════════════════
   PHASE 1a — Release the shell
   ══════════════════════════════════════════════════════════════════════════
   app.css:42-46 (.app) and app.css:48-56 (.content) are ONE constraint and
   must be released TOGETHER. `.content{height:100vh}` is only meaningful
   because `.app{height:100vh}`; changing one without the other yields either
   a still-clipped page or a capped flex container.

   THE vh / dvh / svh ASYMMETRY IS DELIBERATE:
     * `vh` on mobile is the LARGE viewport (URL bar hidden), so height:100vh
       is TALLER than the visible area — that is why the bottom of every page
       currently sits under the URL bar.
     * `min-height:100dvh` on the shell tracks the collapsing URL bar.
       Never `height:100dvh`, which re-locks it.
     * `height:60svh` on .grid-card because `svh` is the SMALL viewport and
       NEVER CHANGES. `dvh` there would resize the grid mid-scroll as the URL
       bar collapses, and Telerik re-measures on resize — visible jank plus
       needless work.
     * Always emit the `vh` line FIRST so pre-2022 browsers keep the fallback.

   SCROLL OWNERSHIP IS THE DOCUMENT BODY. `.content{overflow:visible}` lets the
   page grow so <body> scrolls natively: momentum scroll, URL-bar collapse, and
   correct scroll-into-view on keyboard focus. The alternative
   (`.content{overflow-y:auto}`, as the 10 report pages do) nests an inner
   scroller inside a 100dvh box — the classic double-scroll trap, and it kills
   URL-bar collapse.

   GRID HEIGHT TRADE: `Height="100%"` on a Telerik grid resolves against
   .flex-grid. If ANY ancestor becomes height:auto, 100% resolves to auto and
   all 22 grids collapse to their header. So we release the shell but hand each
   grid a DEFINITE box.                                                      */

@media screen and (max-width: 767.98px) {

    html .app {
        height: auto;
        min-height: 100vh;      /* fallback for pre-dvh browsers */
        min-height: 100dvh;
    }

    html .content {
        height: auto;
        min-height: 100vh;
        min-height: 100dvh;
        overflow: visible !important;   /* beats the 10 Report*.razor
                                           `.content{overflow-y:auto!important}` */
        padding: 10px;
        width: 100%;
        min-width: 0;
    }

    /* Swap `flex:1` (indefinite) for a definite viewport box.
       app.css:439 already gives .grid-card overflow:hidden + display:flex +
       min-height:0 — those stay as-is. Do NOT re-declare min-height:0 here;
       it would fight min-height:300px below. */
    html .grid-card {
        flex: 0 0 auto;
        height: 60vh;        /* fallback */
        height: 60svh;       /* small-viewport: STABLE, never changes size */
        min-height: 300px;
        max-height: 78svh;
    }

    /* Safety net for grids NOT inside a .grid-card
       (Settlement's TabStrip, PreviousSettlementModal, NewGameEntry). */
    html .grid-wrapper {
        min-height: 300px;
    }

    /* ── DEVICE FIX: the safety net above must NOT apply inside a .grid-card.
       app.css:550 gives .grid-wrapper `flex:1; min-height:0; overflow:hidden`.
       The min-height:0 is what lets it SHRINK inside a constrained flex
       column. Overriding it to 300px everywhere re-broke that: inside a card
       with a definite height (60svh, or 72svh on Settlement) the wrapper
       refused to shrink below 300px, overflowed its parent, and the
       `overflow:hidden` on .grid-card and on Settlement's .parent-container
       (Settlement.razor:475) clipped the result — which is why the top header
       row rendered as a sliver.

       A .grid-card already supplies the definite height, so the net is not
       needed there. (0,2,1) beats the (0,1,1) above. */
    html .grid-card .grid-wrapper {
        min-height: 0;
    }

    /* ── DEVICE FIX: inline `height:100%` cards spill their content ─────────
       CashDrawerOperations.razor:264 and :377 are
       `class="commonwhitecard" style="height:100%"`. That works on desktop,
       where the two col-md-6 cards sit on one flex line and stretch to a
       definite shared height. Phase 1a released the shell to height:auto, and
       below 768px each column is its own full-width flex line — so the 100%
       no longer resolves to anything that fits the form, and the content
       (ending with the Save Operations button) painted straight through the
       card's bottom edge onto the next card.

       Author !important outranks an inline declaration, which is the only way
       to reach those two style attributes without editing the markup.

       `:not(.grid-card)` IS LOAD-BEARING — several cards carry BOTH classes
       (BookSell.razor:171, TicketSell.razor:253, CashDrawerOperations.razor:429),
       and without the guard this would blow away the deliberate height:60svh
       set above and collapse every grid. */
    html .commonwhitecard:not(.grid-card) {
        height: auto !important;
    }

    /* app.css:550 (.grid-wrapper{flex:1;min-height:0}) and app.css:558
       (.flex-grid{flex:1;min-height:0}) stay UNCHANGED and still correct —
       .grid-card now supplies the definite height they resolve against.
       Only the top of the chain changed. */
}


/* ══════════════════════════════════════════════════════════════════════════
   PHASE 1b — Off-canvas drawer (zero JS, zero C# change)
   ══════════════════════════════════════════════════════════════════════════ */

@media screen and (max-width: 1023.98px) {

    html .sidebar {
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        width: 260px;
        max-width: 82vw;
        z-index: 1050;                 /* under Bootstrap .modal (1055) */
        transition: transform .22s ease;
        will-change: transform;
        /* The drawer is position:fixed at top:0, so with viewport-fit=cover it
           runs under the status bar and the home indicator. Pad both ends. */
        padding-top: env(safe-area-inset-top);
        padding-bottom: env(safe-area-inset-bottom);
    }

    /* ══ READ BEFORE EDITING ═══════════════════════════════════════════════
       MainLayout.razor:10 hardcodes `class="app sidebar-open"`. The hamburger
       eval (ContentHeader.razor:159-170 and its byte-identical twin at
       Settlement.razor:1233) flips between the two class names. Below 1024px
       we therefore read them as USER INTENT, not as state:

           .sidebar-open   = untouched default        -> drawer CLOSED
           .sidebar-closed = the user tapped the [=]  -> drawer OPEN

       One hardcoded literal, correct default on BOTH desktop and mobile,
       zero C# and zero JS change. This is NOT a naming bug. Do not "fix" it.

       Consequences, all of them correct:
         * MainLayout emits `sidebar-open`      -> drawer closed on load.
         * Every re-render re-emits it          -> drawer closes.
         * forceLoad:true navigation (ContentHeader.RefreshPage,
           ExecuteCloseShift, MainLayout.ValidateSessionVersionsAsync)
           resets to closed.
       ══════════════════════════════════════════════════════════════════════ */

    /* (0,3,1) — beats app.css:67 and app.css:72 at (0,3,0), no !important. */
    html .app.sidebar-open .sidebar {
        width: 260px;
        transform: translateX(-100%);
    }

    html .app.sidebar-closed .sidebar {
        width: 260px;
        transform: translateX(0);
    }

    /* Scrim without adding a DOM node — see prohibition 3. position:fixed
       takes the pseudo out of .app's flex flow, so it is not a stray flex
       item. The background here is one of the two sanctioned pseudo-element
       exceptions to prohibition 2. z-index 1040 sits under the drawer (1050)
       and under Bootstrap's .modal (1055), so an open modal still covers it. */
    html .app.sidebar-closed::after {
        content: "";
        position: fixed;
        inset: 0;
        background: rgba(0, 0, 0, .45);
        z-index: 1040;
    }

    html .app.sidebar-open::after {
        content: none;
    }

    /* ── DEVICE FIX: the user block was centred on iPhone ──────────────────
       app.css:584-588 (inside the legacy POS query) sets
       `.sidebar-top { text-align: center }`. As the file header records, that
       query DOES fire on short phones — 375x667 satisfies both max-width:1024
       and max-height:768 — so on an iPhone SE-class screen the drawer's user
       name inherited centring while the meta line below it did not, because
       .sl-umeta is a flex row and ignores text-align. Hence "Neha" floating in
       the middle with the store line hard left.

       Reversed here rather than in app.css so the 1024x768 POS terminal keeps
       its centred header byte-for-byte. Placed in the DRAWER query, not the
       phone one, so 768-1023px tablets running the drawer are covered too. */
    html .sidebar-top {
        text-align: left;
    }

    /* .app{display:flex} stays. With .sidebar position:fixed, .content{flex:1}
       already fills the row. */
    html .content {
        width: 100%;
        min-width: 0;
    }
}


/* ══════════════════════════════════════════════════════════════════════════
   PHASE 1d — Portrait lock
   ══════════════════════════════════════════════════════════════════════════
   A website in a browser tab CANNOT actually lock orientation:
     * screen.orientation.lock('portrait') requires fullscreen on Chrome/
       Android and iOS Safari does not implement it at all (NotSupportedError).
       Do not use it — it would also add JS to an app whose entire keyboard/
       OTP/scanner UX is focus-dependent.
     * A PWA manifest "orientation":"portrait" works only once INSTALLED, and
       iOS ignores the field. Optional future nicety, not a solution.
   The one mechanism that works everywhere is to detect landscape and cover
   the app with a "rotate your device" layer.

   KEYED ON max-height, NOT ORIENTATION ALONE: a desktop browser window
   resized narrow is also "landscape" and must never see this. Phone landscape
   heights are 375-430px (SE 375, 14 390, Pro Max 430); iPad landscape is 768+,
   the 1024x768 POS terminal is 768, and desktop is rarely under 600. 500px
   cleanly separates them.

   DELIBERATELY AN OVERLAY, NOT `display:none` ON .app. Hiding the app would
   break focus: a display:none element cannot receive .focus(), and
   TicketStockView's restrictDigits attaches on firstRender ONLY — so a rotate
   at the wrong moment would permanently kill the scan field. The app stays
   laid out and focusable underneath. Because it is a painted fixed box it
   receives pointer events, so it blocks interaction with no `pointer-events`
   juggling. Rotating back removes it with no state change anywhere.         */

@media screen and (orientation: landscape) and (max-height: 500px) {
    html body::before {
        content: "Please rotate your device\A This app works in portrait mode";
        white-space: pre-wrap;
        text-align: center;
        position: fixed;
        inset: 0;
        z-index: 2000;              /* above .modal (1055), the drawer (1050),
                                       and SweetAlert2's container (1060)    */
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 24px;
        background: #1e293b;        /* existing shell colour, app.css:61     */
        color: #fff;
        font: 600 17px/1.6 system-ui, sans-serif;
    }
}


/* ══════════════════════════════════════════════════════════════════════════
   PHASE 2 — Modals, forms, touch targets
   ══════════════════════════════════════════════════════════════════════════ */

@media screen and (max-width: 767.98px) {

    /* ── Modals: 58 roots, zero file edits ──────────────────────────────────
       Inline `style="max-width:600px"` beats every stylesheet rule EXCEPT an
       !important one (author-important outranks author-inline in the cascade).
       That is the only mechanism reaching all 58 without touching 24 files.

       Vertical scrolling already works: Bootstrap 5's .modal is
       position:fixed; height:100%; overflow-y:auto, forced visible by d-block.
       Modals already escape .content{overflow:hidden} today — they are only
       too WIDE.

       We add no classes, remove no classes, and never touch `display` on
       .modal, so ManageStoresModal.razor:1101's
       `document.querySelector('.modal.fade.show.d-block')` keeps matching. */
    html .modal .modal-dialog {
        max-width: 100% !important;   /* beats the inline max-width:NNNpx on all 58 */
        width: auto !important;
        margin: .5rem !important;
    }

    /* ── DO NOT re-add a .modal-body padding rule here ─────────────────────
       An earlier revision set `padding: 16px !important` on .modal .modal-body.
       That was wrong twice over:

       1. It bought nothing. Bootstrap 5.1.3's own `.modal-body` padding is
          already 1rem — the exact same 16px.
       2. It silently overrode every deliberate padding utility, because
          (0,2,1)+!important outranks a utility's (0,1,0)+!important:
              p-0  — Registration.razor:20, Login.razor:310,
                     GamesHistory.razor:417
              pb-0 — ForgotPassword.razor:21
              p-4  — GamesHistory.razor:376, :392
          Registration and Login zero their body padding on purpose because
          their inner panels supply it. Forcing 16px back in pushed every form
          field 16px right of the modal header — the visible symptom was the
          header icon and the input column no longer lining up on iOS.

       .modal-content's border-radius was dropped for the same reason: those
       modals set 20px inline as a design choice, and there was no mobile
       argument for overriding it.

       The .modal-dialog width rules ABOVE do earn their !important — they
       exist specifically to beat the inline max-width on all 58 modal roots.
       That is the test for any !important in this file: name the declaration
       it has to defeat. "Padding felt tight" is not one.

       And still never set max-height on .modal-body — see prohibition 5. */

    html .modal .modal-footer {
        flex-wrap: wrap;
        gap: .5rem;
    }

    html .modal .modal-footer > .btnCancel,
    html .modal .modal-footer > .btnCommon,
    html .modal .modal-footer > .btnExtra1,
    html .modal .modal-footer > .btnExtra2,
    html .modal .modal-footer > .btnExtra3 {
        flex: 1 1 140px;
        min-height: 44px;
    }

    /* ── iOS input zoom ────────────────────────────────────────────────────
       iOS Safari auto-zooms on focus of any input under 16px. app.css:200 sets
       .input-field to 14px, and app.css:695 (legacy POS query, which DOES fire
       on 375x667 — see file header) drops it to 12px. Single highest-impact
       rule in this file.

       `:not(.otp-box)` IS LOAD-BEARING. StoreConfig.razor:507,
       ManageStoresModal.razor:858 and Settlement.razor:1009 render
       `<input class="otp-box" type="text">` INSIDE a .modal, so they match
       `.modal input[type=text]` at (0,2,1) — which outranks `html .otp-box`
       at (0,1,1) and would override the deliberate OTP sizing below with
       16px/44px. Excluding them here is what keeps the two rule sets from
       fighting. Do not remove the :not().

       ── DEVICE FIX: SET VERTICAL PADDING ONLY, NEVER THE SHORTHAND ─────────
       An earlier revision used `padding: 10px 12px` here and destroyed the
       left padding that four pages rely on to clear a leading icon:
           Login.razor:181, Registration.razor.css:213,
           StoreConfig.razor:144, ManageStoresModal.razor:212
       all set `padding: 6px 8px 6px 35px` against an `.input-icon` absolutely
       positioned at left:12px. Collapsing that 35px to 12px put the icon
       straight through the placeholder — "envelope+nter email".

       Scoped CSS does not save it either: Registration.razor.css:213 compiles
       to (0,2,1), the same as `html .modal input[type=email]`, and the scoped
       bundle loads at App.razor:10 while this file loads at :15 — so on a tie
       document order hands the win to this file.

       Those icon inputs are BARE `input[type=...]` with no class (Login:316,
       StoreConfig:325), while `.input-field` is only ever used where there is
       no leading icon. The two sets are disjoint, so horizontal padding is
       safe to set on `.input-field` alone, below. */
    html .input-field,
    html .rpt-input,
    html .rpt-select,
    html .modal input[type=text]:not(.otp-box),
    html .modal input[type=number]:not(.otp-box),
    html .modal input[type=password],
    html .modal input[type=email],
    html .modal select,
    html .modal textarea,
    html .k-input-inner,
    html .k-picker .k-input-inner {
        font-size: 16px !important;
        min-height: 44px;
        padding-top: 10px;
        padding-bottom: 10px;
    }

    /* No leading icon on these, so horizontal padding is ours to set. */
    html .input-field,
    html .rpt-input,
    html .rpt-select {
        padding-left: 12px;
        padding-right: 12px;
    }

    /* ...but inside an .input-wrap there IS a trailing .search-icon
       (app.css:216, right:14px). app.css:199 only gives .input-field 8px of
       right padding, so long placeholders ran underneath the magnifier —
       visible on Book Inventory as "type Game# / Book#" colliding with it.
       Reserve room for it. */
    html .input-wrap .input-field {
        padding-right: 38px;
    }

    html .field-label {
        font-size: 14px;
    }

    /* ── DEVICE FIX: inline-block baseline overflow ─────────────────────────
       An <input> is inline-block with vertical-align:baseline, and an
       inline-block's baseline is the baseline of its own last line box — its
       internal text. Giving it min-height:44px above therefore adds height
       BELOW that baseline, which hangs outside the parent's line box. The
       parent div does not grow, so the NEXT element in the row paints on top
       of the input's lower edge.

       Observed on device as BookManager's "Reset" button overlapping the scan
       field, and ImportGames' "Select" button overlapping the file-path field.
       display:block removes the baseline from the equation entirely, so the
       container wraps the real height. Keep box-sizing explicit — the padding
       above would otherwise push these past 100% inside a col-*. */
    html .input-wrap {
        display: block;
    }

    html .input-field,
    html .rpt-input,
    html .rpt-select {
        display: block;
        box-sizing: border-box;
    }

    /* app.css:216-222 pins .search-icon with top:30%, and the legacy POS query
       (app.css:704-708) moves it to top:33%/right:6%. Both are percentage
       offsets calibrated to the old ~30px input. The rule above makes the
       input 44px tall, so a percentage top no longer centres it. Switch to
       transform centring, which is height-independent and fixes both cases.
       The <i> is decorative and has no id/@ref — safe to reposition. */
    html .input-wrap .search-icon {
        top: 50%;
        transform: translateY(-50%);
        right: 12px;
        font-size: 14px;
    }

    /* col-md-* already stacks below 768px: Bootstrap 5's `.row > *` gives every
       un-classed row child flex-shrink:0; width:100%. The 221 occurrences need
       no edit. Only the alignment utilities need reversing — and since both
       sides use !important, the win comes from specificity.
       NOTE: these set `justify-content` / `padding`, never `display`. See
       prohibition 1.

       ── DEVICE FIX: there are TWO label patterns in this codebase ──────────
       (a) utility ON the label   — BookManager.razor:146
           <label class="field-label col-md-3 justify-content-end d-flex …">
       (b) utility on a WRAPPER   — ImportGames.razor:115-116
           <div class="col-md-2 d-flex justify-content-end">
               <label class="field-label …">

       The original rule only covered (a), so ImportGames' "* File Path:" stayed
       right-aligned and ran off the edge on device. Both are covered below.

       The `.content` ancestor requirement was also dropped: modals are not
       always inside .content, and pattern (b) lives in a modal. `html .row >`
       is (0,2,1), still comfortably above any body-block rule. */
    html .row > .field-label.justify-content-end,
    html .row > .d-flex.justify-content-end,
    html .row > .justify-content-end {
        justify-content: flex-start !important;
    }

    html .row > .ps-0 {
        padding-left: calc(var(--bs-gutter-x) * .5) !important;
    }

    /* ── DEVICE FIX: stacked columns have NO vertical gap ──────────────────
       Bootstrap's `.row > *` sets horizontal padding only; the vertical
       gutter `--bs-gutter-y` defaults to 0. On desktop that is invisible
       because columns sit side by side. Once every column stacks to full
       width, consecutive fields touch edge to edge — on Ticket Sell the
       "Add Bulk Tickets" button sat flush against the End Ticket field with
       no separation at all.

       Setting the gutter variable is the idiomatic fix rather than adding
       margins by hand: Bootstrap already pairs `.row > * { margin-top: var(
       --bs-gutter-y) }` with a compensating negative margin-top on `.row`, so
       the first line is not pushed down, and items sharing a line all receive
       the same offset and stay aligned. Hand-rolled `+ sibling` margins break
       exactly that alignment on the two rows kept side by side (the
       ImportGames stat tiles and the row-cols-6 dashboard tiles below). */
    html .content .row {
        --bs-gutter-y: .75rem;
    }

    /* ── DEVICE FIX: `row-cols-N` is NOT breakpoint-scoped ──────────────────
       The plan's premise was that col-md-* already stacks below 768px, which
       is true. But Home.razor:215 uses `row row-cols-6 g-2`, and Bootstrap's
       UNSUFFIXED `.row-cols-6 > *` sets `width: 16.6666%` at EVERY width — it
       never stacks. At 360px that is ~60px per dashboard tile: every label
       wrapped to three lines and the sixth tile was cut off the screen.

       Verified as the only `row-cols-*` in the codebase, so overriding it here
       reaches exactly one place and cannot surprise another page. Two across
       keeps the tiles glanceable and the numbers large.

       Sets `width` on the CHILDREN, never `display` on `.row` — prohibition 1. */
    html .row.row-cols-6 > * {
        width: 50%;
    }

    /* app.css:123/136/149 pin these to padding:3px !important (~20px box);
       the legacy POS query tightens them further to 1px. */
    html .edit-icon,
    html .delete-icon,
    html .info-icon {
        padding: 10px !important;
        font-size: 15px !important;
        min-width: 44px;
        min-height: 44px;
        line-height: 1;
    }

    /* ── DEVICE FIX: inside a grid the binding constraint is WIDTH, not height.
       The plan sized these to 34px against the RowHeight="40" budget, but the
       command columns are narrow and hold THREE buttons:
           BookManager.razor:219   GridCommandColumn Width="100px"
           GameManager.razor:188   GridCommandColumn Width="120px"
       3 x 34px plus Telerik's own button margins exceeds 100px, so the third
       icon wrapped onto a second line — which then broke the 40px row budget
       and produced the two-row action cell seen on device.

       28px x 3 + 2px margins = 90px, which fits the narrowest (100px) column.
       Below the 44px touch guideline, but a wrapped row is worse, and the real
       fix for comfortable touch targets on these grids is the Phase 7 card
       list, not a wider column. nowrap makes the constraint explicit rather
       than relying on the arithmetic staying true. */
    html .k-grid td.k-command-cell,
    html .k-grid .k-table-td.k-command-cell {
        white-space: nowrap;
        padding: 2px 4px !important;
    }

    html .k-grid .edit-icon,
    html .k-grid .delete-icon,
    html .k-grid .info-icon {
        padding: 5px !important;
        font-size: 13px !important;
        min-width: 28px;
        min-height: 28px;
        margin: 0 1px !important;
        vertical-align: middle;
    }

    /* app.css:254-261 sets 40x35 — mixed widths caused per-page drift. */
    html .hamburger-btn {
        width: 44px !important;
        height: 44px !important;
        min-width: 44px;
        min-height: 44px;
        flex: 0 0 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        padding: 0 !important;
        line-height: 1;
        font-size: 20px;
        border-radius: 6px;
    }

    /* (0,3,1) beats the scoped .nav-item[b-xxxxx] at (0,2,0) — the only scoped
       selector needing an override anywhere in this file
       (Sidebar.razor.css:23 and :54). */
    html .sidebar .nav-item {
        padding: 13px 18px;
        font-size: 15px;
        min-height: 44px;
    }

    html .sidebar .logout-btn {
        min-height: 44px;
    }

    html .btnCancel,
    html .btnCommon,
    html .btnExtra1,
    html .btnExtra2,
    html .btnExtra3 {
        min-height: 44px;
        padding: 10px 16px;
        font-size: 14px;
    }

    /* ── DEVICE FIX: .toolbox button overlap ────────────────────────────────
       app.css / BookManager.razor:94 give .toolbox
       `display:flex; gap:15px; justify-content:space-between` with two
       children: a <label> and a plain <div> wrapping the Activate/Deactivate
       buttons (BookManager.razor:187-207).

       Those buttons are inline-block. Once min-height:44px above made them
       taller than the parent's line-height, wrapping stacked their LINE BOXES
       (~21px apart) rather than their 44px boxes — so Deactivate painted over
       Activate. Same root cause as the input fix above.

       Making the wrapper a flex container blockifies the buttons, so they lay
       out by real height. `:not(.hidden)` is defensive: CashDrawerOperations
       .razor:204 and TicketSell.razor:44 define `.hidden{display:none}` at
       (0,1,0), and a `display` declaration here at higher specificity would
       un-hide a panel. See prohibition 1 — same hazard, different element. */
    html .toolbox {
        flex-wrap: wrap;
        gap: 10px;
    }

    html .toolbox > div:not(.hidden) {
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
        align-items: center;
    }

    html .toolbox > label {
        flex: 1 1 auto;
    }

    /* ── DEVICE FIX: grid footer rows (count + submit) run off the card ─────
       TicketSell.razor:328 is `d-flex justify-content-between` holding a
       "Total Entries: N" heading and the Submit Sell button. justify-content
       :space-between never wraps, so at 360px the button was pushed hard
       against — and past — the card's right edge and clipped by the
       `overflow:hidden` that app.css:439 puts on .grid-card.

       Allowing the row to wrap lets the button drop to its own line instead of
       overflowing. min-width:0 lets the heading shrink rather than pinning the
       row at its natural width. Only flex properties are set here — never
       `display` — so `.hidden` panels stay hidden (prohibition 1). */
    html .grid-card > .d-flex.justify-content-between {
        flex-wrap: wrap;
        gap: 8px;
        align-items: center;
    }

    html .gridtotalcount {
        min-width: 0;
        margin-bottom: 0;
    }

    /* app.css:606 pins .page-title p to width:360px (inside the legacy POS
       query, so this bites at 375x667 but not at 390x844 — the !important
       normalises both). */
    /* The title block is the only flexible thing on the header row now, so it
       must be able to shrink below its intrinsic width — otherwise it pushes
       the icon strip off the line and the whole action block wraps (which is
       exactly what Settlement did). min-width:0 unlocks the shrink; the
       ellipsis keeps the result legible rather than clipped mid-letter. */
    html .page-title,
    html .header-left {
        min-width: 0;
    }

    html .page-title p {
        width: auto !important;
        max-width: 100%;
        font-size: 12.5px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    html .page-title h1 {
        font-size: 16px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    /* position:relative anchors the absolutely-positioned Refresh below.
       row-gap is 0 on purpose: the action block is always full width, so it
       always starts its own line, and the spacing between the title row and
       the buttons is carried by the buttons' own margin-top. That way a page
       whose ONLY action is Refresh — which is out of flow — adds no empty
       band under the title. */
    html .page-header {
        position: relative;
        padding: 8px 10px;
        column-gap: 8px;
        row-gap: 0;
        flex-wrap: wrap;
        align-items: center;
    }

    /* ── Header actions: INLINE WITH THE TITLE, ICON ONLY ──────────────────
       Requested behaviour. Previously `width:100%` forced the action block
       onto its own row below the title, which cost a whole band of vertical
       space on every page.

       .header-left must be allowed to shrink (min-width:0) or the title's
       intrinsic width would push the buttons off the row.

       max-width caps how much of the header the actions may claim, so a page
       with FIVE actions (Settlement.razor:384 — Resettle / Hold / Save /
       File Incident / Refresh) wraps them onto a second icon row instead of
       crushing the title to a few pixels. Pages with one to three actions
       (ContentHeader.razor:20) sit comfortably on one line. */
    html .header-left {
        flex: 1 1 auto;
        min-width: 0;
        /* Keep clear of the out-of-flow Refresh pinned at right:10px. */
        padding-right: 46px;
    }

    html .header-actions {
        width: 100%;
        flex: 1 1 100%;
        flex-wrap: wrap;
        justify-content: flex-start;
        column-gap: 8px;
        row-gap: 0;
    }

    /* ICON-ONLY MECHANISM: every header button renders an <i> followed by a
       BARE TEXT NODE — `<i class="fa-solid fa-arrow-rotate-right"></i> Refresh`
       (ContentHeader.razor:64-67, and the same shape at Settlement.razor:385+).
       A bare text node cannot be selected, so it cannot be hidden directly.
       Collapsing the button's font-size to 0 removes it, and the icon's size
       is restored on the child element below. This is why the two rules must
       stay together — deleting the second one hides the icons too. It is
       applied ONLY to .btn-refresh; every other action keeps its label. */

    /* REFRESH: icon-only, pinned to the title row.
       Taken out of flow so it sits beside the page title while the remaining
       actions lay out normally underneath. Anchored to .page-header, which is
       position:relative above. .header-left reserves matching right padding so
       a long title ellipsises before it can run under this button. */
    html .page-header .btn-refresh {
        position: absolute;
        top: 8px;
        right: 10px;
        width: 40px;
        min-width: 40px;
        height: 40px;
        min-height: 40px;
        margin: 0 !important;
        padding: 0 !important;
        font-size: 0 !important;
        line-height: 1;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: 0;
    }

    html .page-header .btn-refresh > i {
        font-size: 16px !important;
        margin: 0 !important;
        line-height: 1;
    }

    /* EVERY OTHER ACTION: icon AND label, two per row, comfortably legible.
       inline-flex + gap gives the icon real separation from its text — the
       markup relies on a single space character between them
       (ContentHeader.razor:39, Settlement.razor:422), which collapses to
       almost nothing once the button becomes a flex container. */
    html .header-actions > button:not(.btn-refresh) {
        flex: 1 1 calc(50% - 4px);
        min-width: 0;
        min-height: 44px;
        margin-top: 8px;
        padding: 10px 8px !important;
        font-size: 13.5px !important;
        line-height: 1.2;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: 5px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    html .header-actions > button:not(.btn-refresh) > i,
    html .header-actions > button:not(.btn-refresh) > .spinner-border {
        font-size: 14px !important;
        margin: 0 !important;
        flex: 0 0 auto;
    }

    /* app.css:448-470: 6 x 52px + 5 x .65rem gap = ~364px, which overflows a
       375px viewport once .content padding counts. Go fluid.
       The ids (otp-N, lotp-N, cfg-otp-N, mgt-otp-N, resettle-otp-N) are
       untouched, so every focus/auto-advance chain stays intact.
       app.css:469 sets padding:0px !important on .otp-box, which correctly
       survives — do not try to override it. */
    html .otp-row {
        gap: 6px;
    }

    html .otp-box {
        width: auto;
        flex: 1 1 0;
        min-width: 0;
        max-width: 52px;
        height: 56px;
        font-size: 1.4rem;
    }
}


/* ══════════════════════════════════════════════════════════════════════════
   PHASE 3 — Grid chrome
   ══════════════════════════════════════════════════════════════════════════
   app.css:569-573 already sets `.flex-grid .k-grid-content{overflow:auto!important}`
   on BOTH axes, and most columns already carry px widths — so once .content is
   narrower than the column sum, Kendo produces the horizontal scrollbar and
   syncs .k-grid-header's scrollLeft itself. 15 of 17 grid files need no Razor
   change at all. OnRowRender, SelectedItems, OnRowClick and RowHeight="40" are
   untouched because nothing about them is touched.

   RowHeight="40" IS A VIRTUALIZATION INPUT, written as an inline row height.
   If cell padding pushes content past 40px, the measured and declared heights
   disagree and scrolling glitches. Treat 40px as a hard budget for anything
   inside .k-grid.                                                           */

@media screen and (max-width: 767.98px) {

    /* The gap is the 30 width-less <GridColumn> tags, concentrated in
       BookSell.razor (7 of 7) and TicketSell.razor (8 of 8): with no widths
       Kendo stretches columns to the container, so they squash instead of
       scrolling. Remainder: Home 7/46, PreviousSettlementModal 3/15,
       Settlement 3/23, CashDrawerOperations 1/5, GamesHistory 1/4.

       HEADER AND BODY MUST GET THE SAME VALUE or columns drift while
       scrolling horizontally.

       RISK — HIGHEST-PROBABILITY VISUAL FAILURE IN THIS FILE: if Telerik
       8.1.1's DOM does not place header and body tables under these selectors,
       headers drift from cells. Inspect the real DOM on device before trusting
       it. Fallback: add Width="…px" to the 15 columns in BookSell.razor:175+
       and TicketSell.razor:266+ so Kendo's own <colgroup> drives alignment —
       but that ALSO changes desktop column widths on those two grids (today
       Kendo stretches them to fill), so only take that route if this CSS
       measurably misaligns, and size the widths to match current desktop
       rendering. */
    html .grid-wrapper .k-grid-header table,
    html .grid-wrapper .k-grid-content table,
    html .flex-grid .k-grid-header table,
    html .flex-grid .k-grid-content table {
        min-width: 640px;
    }

    /* Filter menus: BookManager:216, GameManager:183, Settlement:465 use
       FilterMenu.

       DO NOT CLAMP `left`/`top` ON .k-animation-container — Kendo positions
       popups with inline coordinates and has its own flip/fit collision
       handling; overriding position breaks every TelerikDropDownList and
       TelerikDatePicker too. Clamp WIDTH only, and verify on device. */
    html .k-animation-container {
        max-width: 96vw !important;
    }

    html .k-filter-menu,
    html .k-column-menu {
        max-width: 96vw !important;
        box-sizing: border-box;
    }

    html .k-filter-menu .k-input,
    html .k-filter-menu .k-picker,
    html .k-filter-menu .k-textbox {
        max-width: 100% !important;
    }

    /* Reverse the legacy POS query's shrink (app.css:673-676 drops cells to
       11.4px / 4px 5px, and it genuinely fires on 375x667) without busting the
       RowHeight="40" budget: 13px + 6px vertical padding ~= 30px.
       FONT AND PADDING ONLY — never `background`. The OnRowRender semantic row
       classes must keep painting their own colours. See prohibition 2. */
    html .k-grid td,
    html .k-grid .k-table-td {
        font-size: 13px !important;
        padding: 6px 10px !important;
    }

    html .k-column-title {
        font-size: 12.5px;
    }

    /* ── DEVICE FIX: grid FOOTER (aggregate) cells were unreadable ──────────
       TicketSell.razor:314 puts `Total: {price}` in a <FooterTemplate> on the
       Price column, which declares no Width. Kendo therefore sizes it from the
       remaining space, and at 360px that cell is ~50px — so Telerik's default
       `overflow:hidden; text-overflow:ellipsis` chopped the content into two
       stacked fragments, "T…" over "$…".

       nowrap stops the two words stacking, and letting the cell overflow lets
       the total spill across its neighbours — which are EMPTY footer cells, so
       nothing is obscured. This is the readable option without touching column
       widths, which would also change desktop.

       Footer cells are NOT bound by the RowHeight="40" virtualization budget;
       that governs body rows only. */
    html .k-grid .k-grid-footer .k-table-td,
    html .k-grid .k-grid-footer td,
    html .k-grid .k-table-tfoot .k-table-td,
    html .k-grid tfoot td {
        white-space: nowrap;
        overflow: visible;
        text-overflow: clip;
        font-size: 12.5px !important;
        padding: 8px 6px !important;
        font-weight: 600;
    }

    /* The footer table must not clip the spill either. */
    html .k-grid .k-grid-footer,
    html .k-grid .k-grid-footer-wrap {
        overflow: visible;
    }

    /* ── DEVICE FIX: multi-level (grouped) headers ──────────────────────────
       Settlement.razor:491 and :505 wrap nested <Columns> under
       Title="Game" and Title="Ticket #", so that grid's header is TWO rows
       deep, and PreviousSettlementModal does the same. app.css:563 makes
       .flex-grid .k-grid a flex column with .k-grid-content at flex:1, which
       leaves the header as the flexible sibling — under vertical pressure the
       browser squeezed the group row instead of the content, so the top row
       rendered as a clipped sliver.

       flex-shrink:0 pins the header at its natural height; the content area
       is already flex:1 and absorbs the pressure instead. */
    html .flex-grid .k-grid-header,
    html .grid-wrapper .k-grid-header {
        flex-shrink: 0;
    }

    /* Let grouped header titles WRAP rather than clip. Kendo defaults these to
       nowrap, which is right on a wide desktop column and wrong at 360px —
       "Scanned for Closing" cannot fit on one line here. Header rows are not
       bound by the RowHeight="40" virtualization budget (that governs BODY
       rows only), so two lines in a header is safe. */
    html .k-grid .k-grid-header .k-table-th,
    html .k-grid .k-grid-header .k-header {
        white-space: normal;
        vertical-align: middle;
        padding: 4px 6px !important;
    }

    /* app.css:164-168 pins `.k-grid .k-cell-inner > .k-link` to
       `padding: 8px 5px !important` and centres it. Keep the centring, trim
       the vertical padding so a two-row header does not eat the grid. */
    html .k-grid .k-grid-header .k-cell-inner > .k-link {
        padding: 5px 4px !important;
        white-space: normal;
        line-height: 1.2;
    }

    /* Deliberately NOT addressed: Resizable="true" puts ~4px drag handles in
       every header, which on touch sit on top of the sort target. Hiding
       .k-column-resizer would remove a capability — flag it, re-evaluate after
       device testing, do not do it pre-emptively. */
}


/* ══════════════════════════════════════════════════════════════════════════
   PHASE 4 — Report pages
   ══════════════════════════════════════════════════════════════════════════
   PRINT ISOLATION IS PROVABLE, NOT HOPEFUL. All 9 print methods do
   `var src = document.getElementById('rpt-print'); … html += src.innerHTML;`
   (verified at ReportTicketSales.razor:336/354 and 8 siblings). #rpt-print
   ITSELF IS NEVER COPIED — only its children — so styling #rpt-print is
   structurally invisible to print output. The popup additionally builds its
   whole stylesheet by string concatenation (ReportTicketSales.razor:339-352)
   and loads neither app.css nor mobile.css, so nothing in this query can reach
   print output. Expected diff of popup HTML against a pre-change capture:
   ZERO BYTES.

   KNOWN GAP — REPORT, DO NOT FIX HERE: all 9 print methods use
   `window.open('', '_blank', 'width=900,height=700')` + `setTimeout(win.print, 400)`.
   Mobile Safari and Chrome tab-ify or block popups, and 400ms may fire before
   layout. Printing is effectively desktop-only and this file does not change
   that — fixing it means changing the print handlers.                       */

@media screen and (max-width: 767.98px) {

    /* ── DEVICE FIX: scroll container was wrong parent ─────────────────────
       #rpt-print wraps .rpt-page.rpt-doc which wraps .rpt-table. The plan set
       overflow on #rpt-print only, but the table's min-width forces overflow
       INSIDE the card (width ~355px at 375vw), so #rpt-print (width 355) never
       scrolled — the card clipped. Seen on device as "e-wise Inventory"
       header cut and table spilling past card edge with no scroll hint.
       Move overflow to the actual card(s) themselves. */
    html #rpt-print,
    html .rpt-doc,
    html .rpt-page,
    html #rpt-print .rpt-doc,
    html #rpt-print .rpt-page {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        max-width: 100%;
        box-sizing: border-box;
    }

    html #rpt-print .rpt-table,
    html .rpt-doc .rpt-table,
    html .rpt-page .rpt-table {
        min-width: 620px;
    }

    /* .rpt-doc / .rpt-toolbar / .rpt-input live in each page's inline <style>
       block (body order) — which is exactly why these carry the html prefix. */
    html .rpt-doc {
        padding: 14px !important;
    }

    /* ── DEVICE FIX: report header spills ──────────────────────────────────
       .rpt-header is `justify-content:space-between` with store block left
       and title block right. At 355px the title "Game-wise Inventory Report"
       (18px bold) cannot fit beside the store name and is clipped mid-word —
       the "G" / "Gam" was under the status bar / off-canvas clip.
       Stack into a column on phone; left-align both blocks so the title
       wraps naturally. */
    html .rpt-header {
        flex-direction: column !important;
        align-items: flex-start !important;
        gap: 10px !important;
    }

    html .rpt-header > div:last-child {
        width: 100%;
        text-align: left !important;
    }

    html .rpt-title,
    html .rpt-period {
        text-align: left !important;
    }

    html .rpt-title {
        font-size: 16px !important;
    }

    /* ── DEVICE FIX: the toolbar is a FLAT flex row ────────────────────────
       ReportSalesSummary.razor:213-224 (and its 8 siblings) render label and
       field as SIBLINGS, not as grouped pairs:

           <button rpt-back>← Back</button>
           <span class="rpt-label">From:</span>
           <input class="rpt-input" type="date" />
           <span class="rpt-label">To:</span>
           <input class="rpt-input" type="date" />
           <button class="rpt-btn">Generate</button>

       The original `> *  { flex: 1 1 140px }` made the <span> labels 140px
       flex items too, so with flex-wrap:wrap they packed two-per-row and each
       label ended up beside the WRONG field — "From:" next to "← Back", the
       date input under it. Exactly what the device showed.

       Fix: labels take a full row so each one sits directly above its own
       field; fields take a full row; only the buttons share a row. */
    html .rpt-toolbar {
        gap: 8px;
    }

    html .rpt-toolbar > .rpt-label {
        flex: 0 0 100%;
        min-height: 0;        /* a label is not a touch target */
        margin-bottom: -2px;  /* pull it against its own field */
    }

    html .rpt-toolbar > .rpt-input,
    html .rpt-toolbar > .rpt-select {
        flex: 1 1 100%;
        min-height: 44px;
    }

    html .rpt-toolbar > .rpt-btn,
    html .rpt-toolbar > .rpt-btn-outline {
        flex: 1 1 130px;
        min-height: 44px;
    }

    /* ── Compact Back (user choice) ───────────────────────────────────────────
       Back stays intrinsic width, left-aligned — not 50%/100% stretched.
       Only Generate/Print share the remaining space. Prevents 100% vs 50%
       inconsistency across reports with/without Refresh+Print. */
    html .rpt-toolbar > .rpt-back {
        flex: 0 0 auto;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        line-height: 1;
        gap: 6px;
    }

    html .rpt-toolbar > .rpt-back > i {
        font-size: 12px !important;
        line-height: 1;
    }

    /* ReportGameInventory.razor:228 wraps multiple .rpt-page children in a bare
       <div id="rpt-print"> rather than a .rpt-doc, so the padding rule above
       does not reach them. Give .rpt-page the same treatment.
       Cosmetic caveat: a scroll container with padding does not honour its
       right padding past the scroll edge in Chrome. */
    html #rpt-print .rpt-page {
        padding: 14px !important;
    }
}


/* ══════════════════════════════════════════════════════════════════════════
   PHASE 5 — Public pages, safe areas, polish
   ══════════════════════════════════════════════════════════════════════════ */

@media screen and (max-width: 767.98px) {

    /* Notch / home-indicator insets. .content owns the page scroll now
       (Phase 1a), so it is the right place for these.

       The TOP inset is required, not optional: App.razor now sets
       viewport-fit=cover, which pulls the layout viewport up under the status
       bar. Without padding-top the page header would sit beneath the notch.
       These env() calls also only started returning non-zero once
       viewport-fit=cover was added — before that they silently resolved to 0
       and max() just picked the 10px. */
    html .content {
        padding-top: max(10px, env(safe-area-inset-top));
        padding-left: max(10px, env(safe-area-inset-left));
        padding-right: max(10px, env(safe-area-inset-right));
        padding-bottom: max(10px, env(safe-area-inset-bottom));
    }

    /* Public / unauthenticated pages (LandingPage, Login, Registration,
       ForgotPassword, OpenShift) render OUTSIDE the .content shell —
       MainLayout.razor:15-18 emits @Body directly for public pages — so none
       of the Phase 1a rules reach them. Their card containers are page-local
       classes in each page's own inline <style>; the one shared container is
       app.css:326's .commonwhitecard (padding:20px, tightened to 14px by the
       legacy POS query at app.css:653). */
    html .commonwhitecard {
        max-width: 100%;
        box-sizing: border-box;
        padding: 14px;
    }

    /* Settlement.razor:456's #iconsetting is a clickable <i> positioned by
       inline `position:absolute; left:5px; top:5px; z-index:10`, sitting ON
       TOP of the settlement grid's header row.

       An earlier revision enlarged its hit area to 34px for touch. On device
       that made it cover MORE of the already-cramped grid header — the first
       column title rendered as "ame" behind it. Enlarging an absolutely
       positioned overlay grows what it obscures, so the touch win is not worth
       it here; left at its natural size, i.e. no worse than desktop.

        Do NOT reposition it either: the plan's constraint holds, since moving
        it moves the click target. If this needs solving properly it belongs in
        Phase 7 alongside the column-settings UI, not in a CSS override. */
}


/* ══════════════════════════════════════════════════════════════════════════
   PHASE 7 — ViewportService + card lists (optional upgrade)
   Only worth doing after 0-6 shipped and real-device feedback says
   horizontal scroll is not enough for the widest grids (Settlement 25 cols,
   PreviousSettlementModal 16). Until then, StoreConfig / ManageStoresModal
   keep Ctrl+D as desktop-only entry points (see plan).

   This block provides the SHARED styles for the three Phase 7 components.
   They are scoped to `html` like every other rule, but NOT wrapped in a
   media query — the components themselves only render when Viewport.IsMobile
   is true (matchMedia max-width 767.98px), so the CSS gating is in C#.
   Keeping them un-media-queried avoids double-gating and lets DesktopOnlyHint
   appear on any viewport where it is mounted.

   OnRowRender catch (plan p.575): handlers mutate args.Class and cannot be
   passed to MobileCardList directly. Extract `string ClassifyRow(TModel row)`
   and let both the grid's OnRowRender and the card's RowClass call it.       */
html .mobile-card-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

html .mobile-card {
    background: #fff;
    border: 1px solid #e8e0d5;
    border-radius: 12px;
    padding: 12px 14px;
    box-shadow: 0 1px 4px rgba(26, 18, 8, 0.06);
}

html .mobile-card-selected {
    border-color: #F5C518;
    box-shadow: 0 0 0 2px rgba(245, 197, 24, 0.25);
}

/* Preserve OnRowRender semantic colors on cards by mapping the same class names.
   The grid row classes still paint the card's outer border/background. No color
   declarations elsewhere per prohibition 2 — these are the sanctioned mappings. */
html .mobile-card.settlement-row-soldout,
html .mobile-card.settlement-row-active,
html .mobile-card.settlement-row-scanned,
html .mobile-card.settlement-row-error,
html .mobile-card.settlement-row-selected { border-width: 1.5px; }

/* ── Action sheet (bottom sheet) ────────────────────────────────────────── */
html .mobile-sheet-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    z-index: 1060;
}

html .mobile-sheet {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1070;
    background: #fff;
    border-top-left-radius: 16px;
    border-top-right-radius: 16px;
    padding: 12px 16px max(16px, env(safe-area-inset-bottom));
    box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.18);
    max-height: 72vh;
    max-height: 72dvh;
    overflow-y: auto;
}

html .mobile-sheet-handle {
    width: 36px;
    height: 4px;
    border-radius: 2px;
    background: #e2e8f0;
    margin: 0 auto 10px;
}

html .mobile-sheet-title {
    font-size: 15px;
    font-weight: 700;
    color: #1a1208;
    margin-bottom: 10px;
}

html .mobile-sheet-body {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

html .mobile-sheet-close {
    margin-top: 12px;
    width: 100%;
    padding: 12px;
    border-radius: 10px;
    border: 1.5px solid #ede5d8;
    background: #faf7f2;
    font-weight: 600;
    font-size: 14px;
}

html .mobile-sheet-btn {
    width: 100%;
    padding: 14px 16px;
    border-radius: 10px;
    border: 1.5px solid #ede5d8;
    background: #faf7f2;
    font-weight: 600;
    font-size: 14px;
    text-align: left;
}

/* ── Desktop-only hint banner ───────────────────────────────────────────── */
html .desktop-only-hint {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    background: #fffbeb;
    border: 1px solid #fcd34d;
    border-radius: 10px;
    padding: 10px 12px;
    margin-bottom: 12px;
    font-size: 13px;
    color: #78350f;
}

html .desktop-only-hint-dismiss {
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    border: 1px solid rgba(26, 18, 40, 0.1);
    background: #FFFBF0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}

/* Report card fallback — for #rpt-print HTML tables where min-width:620 still
   demands scroll. Applied only when a report opts into the Viewport branch.
   Keep print isolation: these live outside #rpt-print's copied innerHTML. */
html .rpt-card-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

html .rpt-card {
    background: #fff;
    border: 1px solid #e8e0d5;
    border-radius: 12px;
    padding: 14px;
}

html .rpt-card-row {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    padding: 6px 0;
    border-bottom: 1px solid #f3ede4;
    font-size: 13px;
}

html .rpt-card-row:last-child { border-bottom: none; }
html .rpt-card-label { color: #6b5c45; font-weight: 600; font-size: 12px; }
html .rpt-card-value { color: #1a1208; font-weight: 500; text-align: right; }
html .rpt-card-value.num { font-variant-numeric: tabular-nums; }


/* ══════════════════════════════════════════════════════════════════════════
   PHASE 8 — camera scanning
   ══════════════════════════════════════════════════════════════════════════
   MobileScanButton renders nothing unless ViewportService.IsMobile, so none of
   this needs a media query to stay off desktop — the DOM simply is not there.
   Colours here fall under exception (b) in prohibition 2: these are new
   Phase 8 elements, nothing routes on them.

   The button sits BELOW the field rather than as a third icon inside
   .input-wrap, which already holds the magnifier (manual lookup) and the ✕
   (clear). Cramming a third 36px target into that corner would make the
   PRIMARY mobile input the hardest thing on screen to hit.                  */

html .mobile-scan-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
    min-height: 44px;
    margin-top: 8px;
    padding: 10px 16px;
    border: 1.5px solid #0f766e;
    border-radius: 8px;
    background: linear-gradient(135deg, #0f766e, #14b8a6);
    color: #fff;
    font-size: 14px;
    font-weight: 600;
    letter-spacing: .3px;
    line-height: 1;
    box-sizing: border-box;
    cursor: pointer;
}

html .mobile-scan-btn > i { font-size: 17px; }

html .mobile-scan-btn-disabled,
html .mobile-scan-btn:disabled {
    opacity: .55;
    cursor: not-allowed;
}

/* z-index 1080 clears SweetAlert2 (1060), Bootstrap .modal (1055) and the
   drawer (1050) — several scan fields live inside modals, so the camera must
   sit above its own host. Still below the portrait-lock overlay (2000), which
   must win over everything. */
html .mobile-scan-overlay {
    position: fixed;
    inset: 0;
    z-index: 1080;
    background: rgba(0, 0, 0, .82);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
}

html .mobile-scan-panel {
    width: 100%;
    max-width: 460px;
    border-radius: 14px;
    overflow: hidden;
    background: #10151f;
    box-shadow: 0 18px 48px rgba(0, 0, 0, .55);
}

html .mobile-scan-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 12px 14px;
    color: #fff;
    font-size: 15px;
    font-weight: 600;
}

html .mobile-scan-close {
    width: 36px;
    height: 36px;
    border: none;
    border-radius: 50%;
    background: rgba(255, 255, 255, .12);
    color: #fff;
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
}

html .mobile-scan-video-wrap {
    position: relative;
    width: 100%;
    aspect-ratio: 4 / 3;
    background: #000;
}

/* aspect-ratio only landed in Safari 15. On anything older the wrap collapses
   to zero height, the preview is invisible, and a camera that is in fact running
   perfectly reads as "the scanner is broken" — the same symptom as an actual
   decoder failure, which is why it is worth ruling out here rather than
   diagnosing it twice. The padding trick reserves the same 4:3 box. */
@supports not (aspect-ratio: 4 / 3) {
    html .mobile-scan-video-wrap {
        height: 0;
        padding-top: 75%;
    }

    html .mobile-scan-video {
        position: absolute;
        inset: 0;
    }
}

/* object-fit:cover stops the preview letterboxing when the camera's native
   ratio differs from 4:3 — the frame stays full-bleed so the reticle means
   what it looks like it means. */
html .mobile-scan-video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* Purely a sighting aid. Decoding reads the WHOLE frame, not this box — it is
   pointer-events:none so it can never intercept a tap. */
html .mobile-scan-reticle {
    position: absolute;
    left: 8%;
    right: 8%;
    top: 30%;
    bottom: 30%;
    border: 2px solid rgba(255, 255, 255, .85);
    border-radius: 10px;
    box-shadow: 0 0 0 100vmax rgba(0, 0, 0, .28);
    pointer-events: none;
}

html .mobile-scan-hint,
html .mobile-scan-error {
    padding: 12px 14px;
    font-size: 13px;
    line-height: 1.45;
}

html .mobile-scan-hint { color: rgba(255, 255, 255, .72); }
html .mobile-scan-error { color: #fecaca; background: rgba(220, 38, 38, .16); }

/* The configured game-book-ticket widths, shown so a store running on the
   5/6/3 fallback is visible rather than silently mis-splitting. */
html .mobile-scan-widths {
    display: block;
    margin-top: 3px;
    color: rgba(255, 255, 255, .55);
    font-variant-numeric: tabular-nums;
}

html .mobile-scan-again {
    display: inline-block;
    margin-top: 8px;
    padding: 8px 16px;
    min-height: 40px;
    border: 1px solid rgba(255, 255, 255, .35);
    border-radius: 8px;
    background: transparent;
    color: #fff;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}

/* Diagnostic readout for a symbol that decoded but was rejected. word-break is
   needed because the payload is a long unbroken digit run and would otherwise
   push the panel wider than the screen. */
html .mobile-scan-debug {
    padding: 10px 14px;
    border-top: 1px solid rgba(255, 255, 255, .12);
    color: rgba(255, 255, 255, .8);
    font-size: 12px;
    line-height: 1.5;
}

html .mobile-scan-debug code {
    display: block;
    margin-top: 4px;
    color: #fde68a;
    font-size: 13px;
    word-break: break-all;
}


/* ══════════════════════════════════════════════════════════════════════════
   PHASE 9 — Buttons that stop reading as desktop buttons
   ══════════════════════════════════════════════════════════════════════════
   Everything before this touched buttons REACTIVELY. The 44px normalisation
   at :630 fixed tap targets and the inline-block overlap bugs; the header
   rules at :810 handled the action strip. Nothing ever changed how a button
   SHAPES itself on a phone, and nothing ever gave it a press state — before
   this block the file contained no :hover, :active or tap-highlight rule at
   all. Three gaps, closed below. Presentation only, mobile breakpoint only:
   the 1024x768 POS terminal matches neither query and is untouched.
   ══════════════════════════════════════════════════════════════════════════ */

@media screen and (max-width: 767.98px) {

    /* ── 9a — A LONE ACTION BUTTON SPANS ITS COLUMN ────────────────────────
       Four buttons carry a width utility chosen for the DESKTOP column they
       sit in, where col-md-2 / col-md-3 is a fifth to a quarter of the row:

           BookSell.razor:172          btnExtra2 w-50  in col-md-3  Book Sell
           BookManager.razor:177       btnExtra1 w-50  in col-md-2  Reset
           GameManager.razor:173       btnExtra1 w-50  in col-md-2  Reset
           InventoryReceipt.razor:167  btnExtra2 w-75  in col-md-2  Save Book

       At >=768px `w-50` of a col-md-3 is about 110px, which is correct. Below
       md, Bootstrap gives every `.row > *` width:100%, so that SAME `w-50`
       resolves to half the phone — a small button pinned to the left edge of
       a full-width row with a dead gap beside it. It is the most obviously
       desktop-shaped thing left on these screens.

       :only-child is what lets this be stated once instead of per page. It
       fires only where a column exists to hold the button and nothing else,
       which is exactly those four; a column that also carries a label or a
       field is untouched, so nothing that was deliberately laid out beside
       something else gets stretched.

       THE !important IS EARNED — name the declaration it defeats:
       Bootstrap's `.w-50{width:50%!important}` and `.w-75{width:75%!important}`
       are themselves !important, so no author specificity can beat them.
       This is the only declaration in this file that exists to defeat a
       utility class.

       NO `.content` ANCESTOR, DELIBERATELY. The same shape occurs inside
       modal BODIES (a .row of fields ending in one action). Modal FOOTERS are
       not `.row > col` and keep their own rule at :372-384, so the two rule
       sets cannot collide.

       Restricted to the app's own action classes so a stray utility button
       sitting alone in a column is never ballooned to full width. */
    html .row > [class*="col-"] > button.btnCancel:only-child,
    html .row > [class*="col-"] > button.btnCommon:only-child,
    html .row > [class*="col-"] > button.btnCommon1:only-child,
    html .row > [class*="col-"] > button.btnExtra1:only-child,
    html .row > [class*="col-"] > button.btnExtra2:only-child,
    html .row > [class*="col-"] > button.btnExtra3:only-child {
        width: 100% !important;
    }

    /* ── 9d — btnCommon1 JOINS THE 44px NORMALISATION ──────────────────────
       OpenShift.razor:44 defines .btnCommon1 in a Razor inline <style>. Those
       blocks are GLOBAL, not scoped CSS, and they render in <body> — later in
       document order than this file. It is the primary action on the first
       screen of a shift (Continue / Start Shift, OpenShift.razor:150 and
       :209) and it was never in the list at :630, so it kept its desktop
       10px 26px / 1rem. Worse, on a 375x667 phone its own legacy POS query at
       OpenShift.razor:86-91 squeezes it further to 8px 26px / 14px.

       `html .btnCommon1` is (0,1,1) and outranks both of those (0,1,0) rules,
       so the later document position of the body block does not matter and no
       !important is needed.

       font-size is deliberately NOT set: 1rem already clears the 16px iOS
       zoom floor and the button hosts no input.

       Both call sites sit in `col-md-12 d-flex gap-3 justify-content-center`,
       already flex containers — so unlike the :630 batch this cannot
       reproduce the inline-block baseline overlap documented at :640.

       `.closeshiftbtn` (app.css:629) was the other orphan and is deliberately
       SKIPPED: it is declared there and applied to nothing anywhere in the
       solution, so a rule for it would be dead weight. */
    html .btnCommon1 {
        min-height: 44px;
        padding: 10px 20px;
    }

    /* ── 9b (part 1) — KILL THE DEFAULT TAP FLASH ──────────────────────────
       The grey rectangle the OS paints on tap is the wrong shape (it ignores
       border-radius) and it fights the press state defined below, which is
       the feedback we actually want. Scoped to the action classes plus the
       Phase 8 scan trigger — never global, or every link in the app loses its
       tap feedback too.

       This is NOT a prohibition 2 violation. That rule exists because
       NotificationService.MapColour and the OnRowRender classes route on hex
       literals read back off elements. -webkit-tap-highlight-color paints an
       OS overlay, is not `color` or `background`, and nothing reads it. */
    html .btnCancel,
    html .btnCommon,
    html .btnCommon1,
    html .btnExtra1,
    html .btnExtra2,
    html .btnExtra3,
    html .mobile-scan-btn {
        -webkit-tap-highlight-color: transparent;
    }
}

/* ── 9b (part 2) — A REAL PRESS STATE, AND NO HOVER THAT STICKS ────────────
   app.css:281-323 gives every variant a hover lift:

       .btnCancel:hover { transform: translateY(-1px); box-shadow: 0 8px 24px … }

   and OpenShift.razor:58 does the same for .btnCommon1. On a touch screen
   :hover LATCHES on tap and stays applied until you tap somewhere else, so
   every button used sits permanently raised afterwards. That, plus the total
   absence of a press state, is most of why these read as desktop controls.

   `(hover: none)` is the correct gate rather than width alone: a phone in
   desktop-site mode still reports no hover, and a touch laptop under 768px
   that genuinely has a pointer keeps its lift.

   ORDER IS LOAD-BEARING. `html .btnCancel:hover` and `html .btnCancel:active`
   are both (0,2,1), so the tie is broken by document order — :active must
   come after the hover reset or the press would be cancelled by it during
   the very tap that triggers it.

   transform + filter rather than colour: one pair of declarations then covers
   all five gradients plus btnCommon1's yellow without naming a single hex,
   and nothing in the app routes on either property (prohibition 2). The
   scale animates for free — app.css:269 already declares
   `transition: box-shadow .2s, transform .15s`. */
@media screen and (max-width: 767.98px) and (hover: none) {

    html .btnCancel:hover,
    html .btnCommon:hover,
    html .btnCommon1:hover,
    html .btnExtra1:hover,
    html .btnExtra2:hover,
    html .btnExtra3:hover {
        transform: none;
    }

    html .btnCancel:active,
    html .btnCommon:active,
    html .btnCommon1:active,
    html .btnExtra1:active,
    html .btnExtra2:active,
    html .btnExtra3:active,
    html .mobile-scan-btn:active {
        transform: scale(.97);
        filter: brightness(.93);
    }

    /* A disabled button must not appear to respond. The five action classes
       are plain <button>s and every busy path already sets `disabled`
       (_formBusy / _isBusy / _busy), so this is reachable on every one of
       them mid-request. (0,3,1) — two pseudo-classes — so it outranks the
       (0,2,1) press rule above on specificity, not merely on order. */
    html .btnCancel:disabled:active,
    html .btnCommon:disabled:active,
    html .btnCommon1:disabled:active,
    html .btnExtra1:disabled:active,
    html .btnExtra2:disabled:active,
    html .btnExtra3:disabled:active {
        transform: none;
        filter: none;
    }
}
