/*
    The phone, taken seriously.

    Most of the people who use this product are standing next to a vehicle holding a phone, not
    sitting at a desk. The shell already reflowed below 768 px — a bottom tab bar, an off-canvas
    rail, stacked .rows — but the reflow stopped at the shell. Everything inside it was still a
    desktop screen made narrow: seven-column tables squeezed to 40 px per column, dialogs the size
    of a sheet of A4 dropped into a 360 px viewport, form fields at 14 px that make iOS zoom the
    page every time somebody taps one, and a tablet between 768 and 1080 px that lost its rail
    altogether and got twenty wrapped links across the top of every page instead.

    This file is the layer that finishes the job. It is loaded last, after garagex-layouts.css, so
    it can correct rules those files state at wider breakpoints without either of them having to
    know it exists.

    Three widths, and only three:

      >= 1024 px  the desk. Nothing here applies.
      768–1023    a tablet. The rail comes back as a rail — it is 208 px of a 768 px screen, which
                  is affordable, and it is the same navigation the desk has.
      <= 767.98   a phone. Drawer, tab bar, stacked tables, sheet dialogs, thumb-sized targets.

    Everything below is written `@media screen and ...`. A sheet of A4 is 794 CSS px wide and a
    thermal roll is far narrower than that, so a bare `max-width` query is a query that can fire on
    the printer — which is how the tab bar once came out on a customer's bill.
*/

/* ==========================================================================
   1. Foundations — true at every width
   ========================================================================== */

:root {
    /* The notch, the home indicator and the rounded corners, named once. `env()` resolves to 0 on
       everything that has none of them, so these are safe to use unconditionally. */
    --gx-safe-t: env(safe-area-inset-top, 0px);
    --gx-safe-b: env(safe-area-inset-bottom, 0px);
    --gx-safe-l: env(safe-area-inset-left, 0px);
    --gx-safe-r: env(safe-area-inset-right, 0px);

    /* The height of the tab bar, so the things that must clear it — the page, the toast, a sheet —
       all clear it by the same amount and stay in agreement when it changes. */
    --gx-tabbar: 60px;
}

html {
    /* Safari on iPhone inflates text in the narrower column of a page it has decided is a desktop
       layout. The product is responsive; it does not want the help, and the help is what makes one
       paragraph on a screen a size nobody chose. */
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}

/* A grey flash on every tap is what a web page does. The product says what it is doing with its
   own :active states instead, which is what an app does. */
a, button, [role="button"], summary, label.check, .chip-f, .bizcard {
    -webkit-tap-highlight-color: transparent;
}

/* Nothing with an intrinsic width may set the width of the page. */
img, svg, video, canvas { max-width: 100%; }

/* ==========================================================================
   2. The tablet — 768 to 1023.98
   ========================================================================== */
/*
    Two older rules flattened the shell well before a phone: servicedesk-app.css turns `.shell`
    into a block at 1080 px, and servicedesk-design-system.css un-sticks the rail at 900 px. Both
    were written when the rail was the only navigation there was, and between them they meant an
    800 px tablet — the screen a service advisor actually carries — rendered every page with the
    entire rail wrapped across the top of it and the work pushed below the fold.

    A 208 px rail on a 768 px screen leaves 560 px for the work, which is wider than any phone this
    product is expected to run on. So the tablet keeps the rail, and there is one fewer layout in
    the product rather than one more.
*/
@media screen and (min-width: 768px) and (max-width: 1023.98px) {
    .shell { display: flex; align-items: stretch; }

    #app { grid-template-columns: 208px 1fr; }

    .shell > .rail,
    #app > .rail {
        position: sticky;
        top: 0;
        height: 100vh;
        height: 100dvh;
        width: 208px;
        flex: 0 0 208px;
        transform: none;
        visibility: visible;
    }

    /* The rail is a list of links again, not a wrapped row of them, and its group headings come
       back — they are the only thing that says which of twenty destinations belong together. */
    .rail-nav { display: block; overflow: auto; padding: 0 6px; }
    .rail-nav .set { margin-bottom: 14px; }
    .rail-nav .grp { display: block; }
    .rail-nav a { width: auto; padding: 7px 9px; font-size: 13px; }

    /* Narrower than the desk's 244 px, so the padding comes in with it rather than eating the
       label. */
    .rail-head { padding: 15px 12px 12px; }
    .rail-branch { margin: 0 9px 12px; }

    .content { padding: 18px 18px 36px; }

    /* Tiles two across rather than one. At 900 px the design system drops every grid to a single
       column, which for a row of figures leaves half the screen empty.

       `.g2` is deliberately left at one column: it is the two-pane shape of a detail screen, and
       the panes hold tables. Two of those side by side on a tablet is two squeezed tables. */
    .g4, .g3 { grid-template-columns: 1fr 1fr; }
    .board { grid-template-columns: repeat(2, minmax(0, 1fr)); }

    /* There is no drawer at this width, so there must be no button for one and nothing to dim. */
    .tabbar { display: none; }
    .rail-scrim { display: none; }
    .shell-main { padding-bottom: 0; }
}

/* ------------------------------------------------------- the tablet, inside the work itself */
/*
    The section above reflows the shell for a tablet and stops there. Everything inside it was still
    laid out for a desk, and a tablet is not a small desk: 768 px less a 208 px rail and 36 px of
    padding leaves 524 px of work — narrower than several of the fixed measurements the screens
    write into their own markup.

    Three shapes break at that width, and the phone's own answers to them start 0.02 px too late:

      * a list header written `1fr 150px 130px 150px 110px 130px 130px 90px`, which is 890 px of
        columns in 524 px of card. It did not stack and it did not scroll — it simply ran off the
        right of the screen, taking the money columns with it;
      * a two-pane screen written `1fr 380px`, where the 380 px pane leaves 130 px for the pane that
        matters;
      * a three-across form row, which at 524 px gives each field 165 px — narrower than the date
        box inside it.

    The phone answers the first by stacking, because 360 px cannot hold a table in any arrangement.
    A tablet can: 524 px shows three or four columns of a list at a readable size, and the rest is
    reached by pushing it sideways. So the list scrolls inside its own card rather than stacking,
    which keeps a tablet a tablet — and the page under it never moves, which is the whole point.
*/
@media screen and (min-width: 768px) and (max-width: 1023.98px) {

    /* The card is the frame; the list inside it is what moves. One scroll container for the
       heading row and every row under it, so the columns stay lined up with their headings. */
    .rows { overflow-x: auto; -webkit-overflow-scrolling: touch; }
    .rows > .rh,
    .rows > .rr { min-width: max-content; }

    /* Nothing else inside the work may widen the page. A fixed pixel column is a desk's
       measurement wherever it was written, and at this width there is no room for it. `.rh` and
       `.rr` are exempt: they were just given somewhere to scroll instead. */
    .content .grid,
    .content [style*="grid-template-columns"]:not(.rh):not(.rr):not(.board),
    #modalWrap [style*="grid-template-columns"]:not(.rh):not(.rr):not(.board) {
        grid-template-columns: minmax(0, 1fr) !important;
    }

    /* A pane written inline with a desk's floor under it. */
    .card [style*="min-width"]:not(.rh):not(.rr) { min-width: 0 !important; }

    /* Three fields across becomes two. Not one: a tablet has the room for a pair, and a column of
       single boxes is a phone's form on a screen twice its width. */
    .r3 { grid-template-columns: 1fr 1fr; }

    /* The same wrapping the phone gives a row of controls written inline into a heading. */
    .phead > *, .card > .hd > * { min-width: 0; }
    .phead [style*="display:flex"],
    .card > .hd,
    .card > .hd [style*="display:flex"] { flex-wrap: wrap; }

    /* Two panes side by side is the shape of a detail screen, and there is no room for two. */
    .grid-3, .jdetail { display: block; }
    .grid-3 > .col, .grid-3 > .col-side { width: auto; }
    .grid-3 > * + * { margin-top: 12px; }

    /* And a long unbroken figure breaks rather than widening what holds it. */
    .mono { overflow-wrap: anywhere; }
}

/* --------------------------------------------------- narrower than the desk, phone or tablet */
/*
    The top bar is a breadcrumb, a search box, a layout picker, a theme picker, three dots and an
    avatar. That is a desk's worth of controls, and below 1024 px they stop fitting: the breadcrumb
    wrapped onto three lines and ran underneath the search box.

    The search box is what the bar is for. The two pickers are a preference set once and then left
    alone, and the breadcrumb repeats the heading of the page directly beneath it — so those are
    what go, and they go at the same width on a phone and on a tablet rather than at two.
*/
@media screen and (max-width: 1023.98px) {
    .appbar .crumb { display: none; }
    .appbar-dots { display: none; }
    .design-pick { display: none; }
    .appbar-search { flex: 1; }

    /* The stat tiles are flex children with no basis of their own, so four of them share a tablet
       four ways and "In the workshop" comes out on three lines above a two-digit figure. */
    .stats > .stat { flex: 1 1 calc(50% - 8px); min-width: 0; }
}

/* ==========================================================================
   3. The phone — the shell
   ========================================================================== */

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

    /* The page ends above the tab bar, and the tab bar ends above the home indicator. Stated once
       here from the two variables at the top, rather than as the 68 px that used to be guessed. */
    .shell-main { padding-bottom: calc(var(--gx-tabbar) + var(--gx-safe-b) + 8px); }

    .tabbar {
        min-height: calc(var(--gx-tabbar) + var(--gx-safe-b));
        padding-bottom: var(--gx-safe-b);
        padding-left: var(--gx-safe-l);
        padding-right: var(--gx-safe-r);
    }

    .tabbar a, .tabbar .tab-menu { min-height: var(--gx-tabbar); }

    /* Pressed states, because a bar with no feedback reads as a picture of a bar. */
    .tabbar a:active, .tabbar .tab-menu:active { background: var(--card-2); }

    /* The tab you are on says so with a mark as well as a colour — colour alone is the one signal
       that is gone under bright sun with a phone held at arm's length. */
    .tabbar a { position: relative; }
    .tabbar a.on::before {
        content: '';
        position: absolute;
        top: 0;
        left: 50%;
        width: 26px;
        height: 3px;
        transform: translateX(-50%);
        border-radius: 0 0 3px 3px;
        background: var(--gx-brand);
    }

    /* The top bar and the rail take the notch into account. Without this the search box sits under
       the status bar in a home-screen install, and under the rounded corner in landscape. */
    .appbar {
        padding-top: calc(10px + var(--gx-safe-t));
        padding-left: calc(12px + var(--gx-safe-l));
        padding-right: calc(12px + var(--gx-safe-r));
    }

    .content {
        padding-left: calc(12px + var(--gx-safe-l));
        padding-right: calc(12px + var(--gx-safe-r));
    }

    /* The drawer, on a phone that is 640 px tall in landscape: it has to scroll to its own end, and
       the page behind it must not take the scroll when it gets there. */
    #app > .rail {
        padding-top: var(--gx-safe-t);
        padding-bottom: var(--gx-safe-b);
        overscroll-behavior: contain;
    }

    /* Rubber-banding the whole document to reveal the browser's own background is the clearest tell
       that a thing is a web page. Contained, the scroll stops where the content stops. */
    body { overscroll-behavior-y: contain; }

    /* garagex-layouts.css clips these to stop sideways scroll. `hidden` on one axis forces the
       other to `auto`, which quietly turns every card into a scroll container — enough to kill a
       sticky table head inside it and to trap a focused field. `clip` does the same job without
       creating a scroll box at all. */
    .content, .card, .rows { overflow-x: clip; }

    /*
        The drawer holds the rail, and the rail has to still be a rail inside it.

        servicedesk-app.css turns `.rail-nav` into a wrapping row at 1080 px and hides the group
        headings, which was right when the rail was being flattened across the top of the page. In
        a drawer it is not: twenty destinations came out two to a line with no headings over them,
        so "Spare parts" and "Bills" shared a row and nothing said which of them was Store and which
        was Money. It is a list again, and the headings that group it come back.
    */
    #app > .rail .rail-nav { display: block; overflow: auto; }
    #app > .rail .rail-nav .set { margin-bottom: 14px; }
    #app > .rail .rail-nav .grp { display: block; }
    #app > .rail .rail-nav a { width: auto; min-height: 44px; }
}

/* ==========================================================================
   4. The phone — typing
   ========================================================================== */
/*
    Safari on iOS zooms the page whenever a field with a font smaller than 16 px takes focus, and it
    does not zoom back out. Every form in this product had 14 px fields, so every tap on one left
    somebody looking at a page 1.15x too wide with the rest of the form off the right edge — on a
    counter screen where the next field is always to the right, that is the whole form.

    16 px is not a style choice here. It is the number that stops the zoom.
*/
@media screen and (max-width: 767.98px) {
    input, select, textarea,
    .field input, .field select, .field textarea,
    .rows.edit .rr input, .lotform input, .search input {
        font-size: 16px;
    }

    /* Checkboxes and radios have no text to zoom to, and 16 px would only make them bigger than the
       label beside them. They get the target size instead. */
    input[type="checkbox"], input[type="radio"] {
        font-size: inherit;
        width: 20px;
        height: 20px;
        accent-color: var(--orange);
    }

    /* Room for a thumb, on the fields and on the buttons alike. 44 px is the figure both Apple and
       Google publish, and it is the one a gloved hand at a service bay actually needs. */
    .field input, .field select, .field textarea { min-height: 44px; padding: 10px 12px; }
    .field textarea { min-height: 88px; }

    /* Both spellings. The design system says .btn.pri and the views were written .btn-primary;
       site.css resolves them to the same button rather than rewriting every view, so anything said
       about a button here has to be said twice or it is true on half the screens. */
    .btn, .btn-primary, .btn-quiet, .btn-danger { min-height: 44px; padding: 10px 15px; }
    /* 44 like the rest, not 38. `.btn.sm` is the row action — Open, Record it, Remove a photo —
       and a row action is the thing a thumb reaches for most often on this screen. It was the one
       button in the product still under the floor everything else here holds to. */
    .btn.sm { min-height: 44px; padding: 8px 12px; font-size: 13px; }

    /* A row of form buttons at the end of a screen — Save, Cancel, sometimes Delete. Each takes a
       share of the width rather than huddling at the left in 13 px type. */
    .form-actions { gap: 8px; }
    .form-actions > .btn,
    .form-actions > .btn-primary,
    .form-actions > .btn-quiet,
    .form-actions > .btn-danger { flex: 1 1 130px; justify-content: center; }

    /* A tick box in a list of twenty rights is a 13 px square with a sentence beside it. The whole
       row becomes the target instead. */
    label.check {
        display: flex;
        align-items: flex-start;
        gap: 10px;
        min-height: 44px;
        padding: 8px 4px;
    }

    /* The search box is the width of the screen or it is nothing: 230 px inside a wrapping row of
       filters leaves a box with a scrollbar's worth of the customer's name in it. */
    .search, .search input { width: 100%; }

    /* The page heading and its actions: the heading reads, the actions are pressed. Side by side at
       360 px they are neither. */
    .phead { flex-direction: column; align-items: stretch; }
    .phead .acts { width: 100%; }
    .phead .acts > .btn { flex: 1 1 auto; justify-content: center; }
    .acts { flex-wrap: wrap; }
}

/* ==========================================================================
   5. The phone — a table becomes a list of cards
   ========================================================================== */
/*
    Twelve screens draw a real <table>. A seven-column table on a 360 px screen has two outcomes and
    both are bad: `width:100%` squeezes every column to 40 px and wraps "Transfer stock between
    branches" down five lines, or the wrapper scrolls sideways and the column headings leave the
    screen before the numbers under them do — so the reader is looking at a grid of ticks with no
    way to tell which column is which.

    A list table therefore stops being a table on a phone and becomes one card per row, with each
    cell carrying the heading it belonged to. That is exactly what `.rows` already does, so the two
    kinds of list in this product now read the same way on a phone rather than one of them being a
    stack of labelled facts and the other a squeezed grid.

    Which tables those are is decided in mobile.js and stamped as `data-tbl`, not guessed here. A
    bill, a salary slip and a sale invoice are documents rather than lists — their columns mean
    something in relation to each other, and stacking them would destroy the document — so they are
    left alone and pan inside their frame. So is the attendance calendar, and so is anything a
    screen has explicitly marked `.tbl-wide`.

    Without JavaScript nothing is stamped, no rule below matches, and every table renders exactly as
    it did before. Stacking is worthless without the headings anyway, and the headings are copied by
    the same script.
*/
@media screen and (max-width: 767.98px) {

    /* The wrapper stops scrolling for a table that no longer needs to be scrolled, and stops
       forcing a minimum width onto one that is now a column of cards. */
    .tblwrap:has(> table[data-tbl="stack"]),
    .tablewrap:has(> table[data-tbl="stack"]) { overflow: visible; contain: none; }
    .tablewrap > table[data-tbl="stack"] { min-width: 0; }

    table[data-tbl="stack"],
    table[data-tbl="stack"] > tbody,
    table[data-tbl="stack"] > tfoot,
    table[data-tbl="stack"] > tbody > tr,
    table[data-tbl="stack"] > tfoot > tr { display: block; }

    /* The headings are on each cell now, so the row of them at the top would be the same words
       twice. */
    table[data-tbl="stack"] > thead { display: none; }

    table[data-tbl="stack"] > tbody > tr,
    table[data-tbl="stack"] > tfoot > tr {
        border: 1px solid var(--line);
        border-radius: var(--r2);
        background: var(--card);
        box-shadow: var(--sh);
        margin-bottom: 9px;
        overflow: hidden;
    }

    table[data-tbl="stack"] > tbody > tr:hover { background: var(--card); }

    table[data-tbl="stack"] td {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: 14px;
        width: auto;
        padding: 9px 13px;
        border-bottom: 1px solid var(--line-2);
        text-align: right;
        white-space: normal;
    }

    table[data-tbl="stack"] tr > td:last-child { border-bottom: 0; }

    /* The heading the cell belonged to, on the left of the value it explains. */
    table[data-tbl="stack"] td[data-label]:not([data-label=""])::before {
        content: attr(data-label);
        flex: 0 0 auto;
        max-width: 45%;
        text-align: left;
        font-size: 10.5px;
        font-weight: 700;
        line-height: 1.5;
        letter-spacing: .05em;
        text-transform: uppercase;
        color: var(--slate);
    }

    /* The first cell is the row's name — the customer, the part, the person. It reads as a title
       across the top of the card rather than as another labelled fact. */
    table[data-tbl="stack"] > tbody > tr > td:first-child {
        display: block;
        text-align: left;
        background: var(--card-2);
        font-weight: 600;
        font-size: 15px;
        padding: 11px 13px;
    }
    table[data-tbl="stack"] > tbody > tr > td:first-child::before { display: none; }
    table[data-tbl="stack"] > tbody > tr > td:first-child a { font-size: 15px; }

    /* A cell the desktop left blank has no fact to state. `data-blank` is stamped by mobile.js,
       which sees whitespace as blank; `:empty` catches the trailing cells of a spanning total row,
       which the script deliberately leaves unlabelled. */
    table[data-tbl="stack"] td[data-blank],
    table[data-tbl="stack"] td:empty { display: none; }

    /* The actions. On the desk they are a small button at the right end of the row; on a card they
       are what the card is for, so they take the full width at the bottom of it — whatever the
       column above them happened to be called. */
    table[data-tbl="stack"] td[data-act] {
        justify-content: stretch;
        gap: 8px;
        background: var(--card-2);
    }
    table[data-tbl="stack"] td[data-act]::before { display: none; }
    table[data-tbl="stack"] td[data-act] > .btn { flex: 1; justify-content: center; }
    table[data-tbl="stack"] td[data-act] > form { flex: 1; display: flex; }
    table[data-tbl="stack"] td[data-act] > form > .btn { flex: 1; justify-content: center; }

    /* A total line spanning the table keeps spanning it. Written to the same specificity as the
       first-cell rule above and after it, because the spanning cell is usually also the first one
       and must not be drawn as the row's title. */
    table[data-tbl="stack"] > tbody > tr > td[colspan],
    table[data-tbl="stack"] > tfoot > tr > td[colspan] {
        display: block;
        background: none;
        font-size: inherit;
        font-weight: inherit;
        text-align: left;
    }

    /* A spanning cell holding a sentence is a total and reads from the right. One holding a panel —
       a part's costed lots open inside the row that owns them — is a screen, and a screen that has
       been right-aligned is a mistake. */
    table[data-tbl="stack"] > tbody > tr > td[colspan]:not(:has(div)):not(:has(table)),
    table[data-tbl="stack"] > tfoot > tr > td[colspan]:not(:has(div)):not(:has(table)) {
        text-align: right;
        font-weight: 700;
    }

    table[data-tbl="stack"] td[colspan]::before { display: none; }

    /* A number keeps its monospace and its right edge; it just no longer has a column to sit in. */
    table[data-tbl="stack"] td.num { text-align: right; }

    /* The nested tables — a part's costed lots open inside the row that owns them — stack too, and
       must not draw a card inside a card. */
    table[data-tbl="stack"] td[colspan] table[data-tbl="stack"] > tbody > tr { box-shadow: none; }
}

/* ==========================================================================
   6. The phone — a table that stays a table
   ========================================================================== */
/*
    The two kinds that cannot stack: a document, whose columns are a statement about each other, and
    a matrix, whose whole content is which column a tick is in.

    Both pan sideways instead — but panning is only readable if the two things that say what you are
    looking at stay put. So the heading row sticks to the top of the scroll and the first column
    sticks to the left of it, which means the role names stay above the ticks and the right stays
    beside them however far across you have swiped.
*/
@media screen and (max-width: 767.98px) {
    .tblwrap:has(> table[data-tbl="wide"]),
    .tablewrap:has(> table[data-tbl="wide"]) {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        overscroll-behavior-x: contain;
    }

    /* Squeezing is the failure this is here to prevent, so the table is given a width it cannot
       fall below and the box around it scrolls. */
    table[data-tbl="wide"]:not(.att) { min-width: 560px; }
    table[data-tbl="wide"]:not(.att) th,
    table[data-tbl="wide"]:not(.att) td { padding: 8px 11px; }

    /* The attendance calendar is the one wide table that is already as narrow as it can be: it is
       31 day columns at 24 px, and a floor of 560 px or a padding of 11 px would each make it wider
       rather than more readable. What it needs instead is the staff name held at the left edge —
       a grid of coloured squares with the row's name scrolled off is a grid of coloured squares. */
    .att td.att-name, .att th.att-name {
        position: sticky;
        left: 0;
        z-index: 1;
        min-width: 116px;
        padding: 7px 9px;
        background: var(--card);
        box-shadow: 1px 0 0 var(--line-2);
    }
    .att th.att-name { z-index: 3; background: var(--card-2); }

    table[data-tbl="wide"] > thead th {
        position: sticky;
        top: 0;
        z-index: 2;
        background: var(--card-2);
    }

    /* The row's own name, held at the left edge. */
    table.tbl-wide > tbody > tr > td:first-child,
    table.tbl-wide > thead > tr > th:first-child {
        position: sticky;
        left: 0;
        z-index: 1;
        background: var(--card);
        box-shadow: 1px 0 0 var(--line-2);
        text-align: left;
    }
    table.tbl-wide > thead > tr > th:first-child { z-index: 3; background: var(--card-2); }

    /* A bill, a slip and a sale invoice pan inside the grey mount they are printed on. That mount is
       `.paper-frame`, and garagex-layouts.css already scrolls it; the table inside keeps the width
       the document was laid out at rather than being given a second, larger one. */
    .bill table { min-width: 0; }
}

/* ==========================================================================
   7. The phone — a dialog is a sheet
   ========================================================================== */
/*
    Every add and edit in this product happens in a dialog over the list. On a desk that is a card
    640 px wide floating in the middle of the screen. Dropped on a phone it was a card with 26 px of
    dark either side of it, a body that scrolled at 66 % of the viewport height while the page behind
    it scrolled too, and Save at the bottom of that inner scroll — reachable only by scrolling the
    dialog, in a product where Save is the last thing anybody does.

    On a phone it becomes what a phone uses for the same job: a sheet that comes up from the bottom,
    held to the screen, with its title fixed at the top and its buttons fixed at the bottom above the
    home indicator. Nothing about the dialog's markup changes.
*/
@media screen and (max-width: 767.98px) {
    #modalWrap.on {
        display: flex;
        align-items: flex-end;
        padding: 0;
        overflow: hidden;
    }

    #modalWrap .modal {
        width: 100%;
        /* !important, because three dialogs state a width of their own inline, and a sheet that is
           860 px wide on a 360 px screen is not a sheet. */
        max-width: none !important;
        margin: 0;
        border: 0;
        border-radius: var(--r) var(--r) 0 0;
        /* dvh, not vh: with the browser's own chrome showing, 100vh is taller than the screen and
           the footer of the sheet sits under the address bar. */
        max-height: 92dvh;
        display: flex;
        flex-direction: column;
        animation: gx-sheet .2s cubic-bezier(.22, .61, .36, 1);
    }

    #modalWrap .modal .mh {
        position: relative;
        flex: 0 0 auto;
        padding: 20px 16px 13px;
    }

    /* The grab handle. It is not a control — the sheet is dismissed by its own close button and by
       the scrim — but it is the thing that says "this came up from the bottom and can go back". */
    #modalWrap .modal .mh::before {
        content: '';
        position: absolute;
        top: 7px;
        left: 50%;
        width: 36px;
        height: 4px;
        transform: translateX(-50%);
        border-radius: 4px;
        background: var(--line);
    }

    #modalWrap .modal .mh h3 { font-size: 16.5px; }

    #modalWrap .modal .mb {
        flex: 1 1 auto;
        max-height: none;
        padding: 16px;
        overscroll-behavior: contain;
        -webkit-overflow-scrolling: touch;
    }

    #modalWrap .modal .mf {
        flex: 0 0 auto;
        flex-wrap: wrap;
        gap: 8px;
        padding: 12px 16px calc(12px + var(--gx-safe-b));
        border-radius: 0;
    }

    /* Every button in the footer takes an equal share of the width, so Save is a thumb-width target
       at the bottom of the screen rather than a 58 px one in the corner. Both spellings, because a
       dialog lifted out of a screen carries whichever that screen was written with. */
    #modalWrap .modal .mf > .btn,
    #modalWrap .modal .mf > .btn-primary,
    #modalWrap .modal .mf > .btn-quiet,
    #modalWrap .modal .mf > .btn-danger { flex: 1 1 120px; justify-content: center; }
    #modalWrap .modal .mf > form { flex: 1 1 120px; display: flex; }
    #modalWrap .modal .mf > form > .btn,
    #modalWrap .modal .mf > form > .btn-primary,
    #modalWrap .modal .mf > form > .btn-danger { flex: 1; justify-content: center; }

    /* A screen lifted into a dialog brings its own card with it — the form is a `.panel`, which is
       a bordered, padded, shadowed box. Inside a sheet that is a card drawn inside a card, with the
       fields inset twice from an edge that is already only 16 px away. */
    #modalWrap .modal .panel {
        border: 0;
        border-radius: 0;
        box-shadow: none;
        background: none;
        padding: 0;
        margin: 0;
    }

    /* The close control in the title bar, at a size a thumb can hit. */
    #modalWrap .modal .mh .x {
        min-width: 40px;
        min-height: 40px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    /* The confirm dialog is the same shape, for the same reason — it is the one dialog where
       pressing the wrong thing cannot be undone. */
    .ask { align-items: flex-end; padding: 0; }
    .ask-box {
        width: 100%;
        max-width: none;
        border-radius: var(--r) var(--r) 0 0;
        padding-bottom: calc(18px + var(--gx-safe-b));
        animation: gx-sheet .2s cubic-bezier(.22, .61, .36, 1);
    }
    .ask-foot { flex-wrap: wrap; gap: 8px; }
    .ask-foot > .btn { flex: 1 1 120px; justify-content: center; min-height: 44px; }

    /* The command palette takes the whole screen. A 12 px margin around a search field is a desktop
       overlay wearing a phone's clothes. */
    .find { padding: 0; align-items: stretch; }
    .find-box {
        width: 100%;
        max-width: none;
        height: 100dvh;
        border-radius: 0;
        display: flex;
        flex-direction: column;
    }
    .find-in { padding-top: calc(12px + var(--gx-safe-t)); }
    .find-in input { font-size: 16px; }
    .find-in kbd { display: none; }
    .find-out { flex: 1; max-height: none; overscroll-behavior: contain; }

    /* The toast clears the tab bar rather than being drawn behind it. */
    #toast {
        right: 12px;
        left: 12px;
        top: auto;
        bottom: calc(var(--gx-tabbar) + var(--gx-safe-b) + 12px);
        max-width: none;
        transform: translateY(160%);
    }
    #toast.on { transform: none; }

    #saveDot { bottom: calc(var(--gx-tabbar) + var(--gx-safe-b) + 12px); }
}

@keyframes gx-sheet {
    from { transform: translateY(14%); opacity: .6; }
    to   { transform: none; opacity: 1; }
}

/* ==========================================================================
   8. The phone — the support band
   ========================================================================== */
/*
    A platform visit is announced across the top of every page, and it has to keep being announced:
    the whole point is that neither side forgets whose garage this is. But at 360 px the desktop band
    took 145 px — a fifth of the screen — before the page began, on every screen, for the whole
    visit. It says the same thing in three lines instead of six.
*/
@media screen and (max-width: 767.98px) {
    .visitband-in {
        padding: calc(8px + var(--gx-safe-t)) calc(12px + var(--gx-safe-r)) 8px calc(12px + var(--gx-safe-l));
        gap: 8px;
        font-size: 11.5px;
        line-height: 1.35;
        align-items: center;
    }

    /* The badge reads as the first words of the sentence rather than as a block above it. */
    .visitband-in > b { padding: 2px 8px; font-size: 10px; }

    .visitband-in > span { flex: 1 1 100%; }

    .visitband-in form { margin: 0; width: auto; }
    .visitband-out { width: auto; padding: 6px 12px; min-height: 36px; font-size: 12px; }
}

/* ==========================================================================
   9. The phone — signing in
   ========================================================================== */
/*
    The signed-out shell is a dark half that sells the product and a light half that asks for a
    number. Stacked on a phone, the selling half comes first: a logo, a headline, a paragraph and
    five numbered claims — roughly a screen and a half of scrolling before the person who already has
    an account can reach the field they came for.

    Somebody opening this on a phone at 8 a.m. is a mechanic signing in, not a visitor being
    convinced. The pitch keeps its headline and loses its list.
*/
@media screen and (max-width: 767.98px) {
    #login { min-height: 100dvh; }

    #login .left {
        padding: calc(24px + var(--gx-safe-t)) 20px 22px;
        justify-content: flex-start;
    }
    #login .left h1 { font-size: 26px; }
    #login .left p { margin: 10px 0 0; font-size: 13px; }
    #login .left .feat { display: none; }
    #login .left:after { display: none; }
    #login .lg-img { width: 190px; margin-bottom: 16px; }

    #login .right { padding: 20px 16px calc(24px + var(--gx-safe-b)); align-items: start; }
    .loginbox { max-width: none; width: 100%; }
    .authtabs a, .authtabs button { min-height: 44px; display: grid; place-items: center; }
}

/* ==========================================================================
   10. The phone — the platform panel
   ========================================================================== */
/*
    The panel is administered from a desk almost all of the time, and almost all is not always: the
    one occasion somebody opens it on a phone is the occasion something is wrong at nine on a Sunday.
    It gets the same treatment as the product — a compact bar that stays put, tabs that scroll, rows
    that stack — rather than being the one corner where the old behaviour survived.

    It shares the design system with the garage screens, so sections 4 to 7 above already apply to
    everything inside it. This is only the shell.
*/
@media screen and (max-width: 767.98px) {
    .adminshell { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto 1fr; }

    .adminrail {
        min-width: 0;
        position: sticky;
        top: 0;
        z-index: 30;
        height: auto;
        border-right: 0;
        border-bottom: 1px solid #232830;
        padding-top: var(--gx-safe-t);
    }

    /* Title and sign-out on one line: two full-width blocks for a wordmark and a button is most of a
       phone screen spent on neither the data nor the navigation. */
    .adminrail .mark { padding: 10px 13px; border-bottom: 0; }
    .adminrail .mark img { height: 22px; max-width: 26px; }
    .adminrail .mark .wm { font-size: 13px; }

    .adminfoot {
        position: absolute;
        top: calc(6px + var(--gx-safe-t));
        right: 12px;
        padding: 0;
        border-top: 0;
        gap: 8px;
    }
    .adminwho { display: none; }

    .adminnav {
        display: flex;
        align-items: center;
        gap: 4px;
        overflow-x: auto;
        overscroll-behavior-x: contain;
        -webkit-overflow-scrolling: touch;
        flex: none;
        min-width: 0;
        padding: 0 12px 9px;
    }
    .adminnav .grp { display: none; }
    .adminnav a.tab {
        white-space: nowrap;
        flex: 0 0 auto;
        min-height: 38px;
        display: grid;
        place-items: center;
    }
    .adminnav a.tab.on:before { display: none; }

    .adminwrap {
        padding: 14px calc(13px + var(--gx-safe-r)) calc(32px + var(--gx-safe-b)) calc(13px + var(--gx-safe-l));
    }

    /* A fact is a label and a figure on one line, and on a phone the figure is the half that must
       not wrap. */
    .fact { gap: 10px; font-size: 13px; }
}

/* ==========================================================================
   11. The phone — a row that opens where it stands
   ========================================================================== */
/*
    garagex-layouts.css stacks `.rows .rr` and draws each cell's heading above it. That works for
    the ordinary row, whose cells are its own children — but the salary run and the error log are
    rows that open, so the row is a <details> and the seven columns live inside its <summary>. The
    grid is written on the summary, inline, and the stacking rule never reached it: the salary
    screen laid out at 904 px on a 390 px phone, clipped at the edge with five of its seven columns
    on the other side of it.

    The cells are labelled by mobile.js, which looks inside the summary for exactly this shape.
*/
@media screen and (max-width: 767.98px) {
    .rows details.rr > summary {
        grid-template-columns: 1fr !important;
        gap: 4px !important;
        padding: 12px 14px !important;
    }

    .rows details.rr > summary > * {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        flex-wrap: wrap;
        gap: 2px 12px;
        min-width: 0;
        text-align: right !important;
        overflow-wrap: anywhere;
    }

    .rows details.rr > summary > *[data-label]:not([data-label=""])::before {
        content: attr(data-label);
        flex: 0 0 auto;
        text-align: left;
        color: var(--gx-ink-faint);
        font-size: 10.5px;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: .05em;
    }

    /* The first cell names the row — the person on a salary run — and reads as its title. */
    .rows details.rr > summary > :first-child { display: block; text-align: left !important; font-size: 15px; }
    .rows details.rr > summary > :first-child::before { display: none !important; }
}

/* ==========================================================================
   12. The phone — odds and ends the screens leave behind
   ========================================================================== */
@media screen and (max-width: 767.98px) {
    /* A card with 16 px of padding on a 360 px screen leaves 328 px of content. */
    .card > .hd { padding: 13px 14px; }
    .card > .bd { padding: 14px; }

    /*
        The screens in this product write a good deal of their layout inline — `min-width:220px` on
        a pane, `grid-template-columns:1fr 380px` on a two-column settings page, `display:flex` on a
        row of actions in a page heading. Every one of those is a desktop measurement, an inline
        declaration outranks a stylesheet, and there are far too many to chase one at a time.

        So they are addressed by shape rather than one by one. Not pretty, and the alternative is
        either editing sixty views or leaving a 380 px column on a 360 px screen.
    */
    .card [style*="min-width"] { min-width: 0 !important; }

    /* A two-column page whose second column is a fixed 380 px. */
    .content .grid { grid-template-columns: 1fr !important; }

    /*
        And every other inline grid: `1fr 84px 124px auto` for the counter's add-a-part line,
        `1fr 150px 130px 150px` for a list header, `1fr 380px` for a settings page. Fixed pixel
        columns are a desk's measurement — at 360 px the counter's part picker came out 30 px wide
        with "Add a part" wrapped down three lines beside a full-size Add button.

        One column, everywhere. It reads as a stack of labelled fields, which is what a form on a
        phone is. `.rows` is unaffected: its heading row is already hidden here and its data rows
        already carry this same declaration.
    */
    .content [style*="grid-template-columns"]:not(.board),
    #modalWrap [style*="grid-template-columns"]:not(.board) { grid-template-columns: 1fr !important; }

    /*
        The board is the exception, at both widths.

        Its columns are written inline by the view because only the server knows how many lanes the
        garage has, so it matched the rule above and every lane was stacked into one column — while
        the inline `min-width` for five lanes stayed, leaving a tall column a phone had to be panned
        sideways to read. It is the workshop's home screen.

        What it wants instead is already written in garagex-layouts.css: the lanes stay side by
        side, one lane is 82vw so the next peeks in at the edge, and `.boardwrap` scrolls with a
        snap. That is how a board is read on a phone — swiped, like the lanes are pages.
    */

    /* A row of controls written inline in a page heading or a card header: it wraps rather than
       running off the edge, and nothing in it insists on its full width. */
    .phead > *, .card > .hd > * { min-width: 0; }
    .phead [style*="display:flex"],
    .card > .hd,
    .card > .hd [style*="display:flex"] { flex-wrap: wrap; }

    /* The typed date range on the reports screen: two date boxes and Apply, which at 16 px type is
       wider than the screen. Each box takes a share of the line and the button takes the next. */
    .daterange { flex-wrap: wrap; }
    .daterange input[type=date] { flex: 1 1 120px; min-width: 0; }
    .daterange > .btn { flex: 1 1 100%; justify-content: center; }

    /* Two panes side by side is the shape of a detail screen everywhere in the product. */
    .grid-3, .jdetail, .r2, .r3 { display: block; }
    .grid-3 > .col, .grid-3 > .col-side { width: auto; }
    .grid-3 > * + * { margin-top: 12px; }

    /* The search row of the filter bar. `.field` carries a 220 px floor for the desk, and a floor
       plus a select plus a button is 460 px of things that will not shrink on a 390 px screen. Each
       one takes a line of its own instead. */
    .filterbar-find { width: 100%; }
    .filterbar-find .field { min-width: 0; flex: 1 1 100%; }
    .filterbar-find select, .filterbar-find button { flex: 1 1 130px; min-width: 0; }

    /* A row of filters wraps rather than scrolls, which is right — but each control in it needs to
       be pressable, and a date box at 4 px of vertical padding is not. */
    .daterange { flex: 1 1 100%; justify-content: space-between; }
    .daterange input[type=date] { min-height: 40px; font-size: 16px; }
    /* The filter chip is pressed more than any other control in the product — Below minimum, Ready
       for delivery, Out of stock — and it sat at 36. */
    .chip-f { min-height: 44px; display: inline-flex; align-items: center; }

    /* The stat tiles. Two across is already set; the figure inside them is a 30 px display face that
       overflows its own tile at 360 px once it is a lakh figure with grouping in it. */
    .stat { padding: 13px; }
    .stat .v { font-size: 23px; word-break: break-word; }

    /* The empty state's 56 px of vertical padding is half a phone screen of nothing. */
    .empty { padding: 34px 18px; }

    /* Anything monospaced and long — a plate, a bill number, a stack trace — breaks rather than
       widening the page under it. */
    .mono { overflow-wrap: anywhere; }
    .logdetail pre { font-size: 10.5px; }
}

/* ==========================================================================
   13. The phone — a record's header and its tabs
   ========================================================================== */
/*
    A record's screen opened with its number, a date line squeezed beside it, and the actions in a
    wrapping row where "Send the job card" broke over four lines and the last button ran off the
    edge. The owner's picture of it is the shape below: the date line, the number, what it is and
    where it stands, then the actions as equal tiles, then the tabs as one segmented control.
*/
@media screen and (max-width: 767.98px) {
    /* Heading, date line and identity each take a line of their own. A screen with an eyebrow keeps
       its order; one without puts the date line above the number, as the owner's picture has it. */
    .phead > div:first-child > h1 { flex-basis: 100%; }
    .phead > div:first-child:not(:has(> .eyebrow)) > .sub { order: -1; flex-basis: 100%; }

    /* The actions: equal tiles, two to a row, each word on one line. */
    .phead .acts {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
        gap: 8px;
    }
    .phead .acts > .btn,
    .phead .acts > form > .btn {
        width: 100%;
        justify-content: center;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    .phead .acts > form { display: grid; gap: 4px; margin: 0; }

    /* Words a button can do without on a phone — "the", a long bill number. */
    .wide-only { display: none; }

    /* The tabs: one segmented control, the open part raised out of it. */
    .pagetabs {
        gap: 4px;
        padding: 4px;
        border-bottom: 0;
        border-radius: 12px;
        background: var(--line-2);
    }
    .pagetabs a {
        flex: 1 1 0;
        justify-content: center;
        min-height: 40px;
        margin: 0;
        padding: 8px 10px;
        border-bottom: 0;
        border-radius: 9px;
    }
    .pagetabs a:hover { border-bottom-color: transparent; }
    .pagetabs a.on { background: var(--card); box-shadow: var(--sh); }
}
