/* NexusGPS — phone and small-tablet layout
 * ---------------------------------------------------------------------------
 * Every rule in this file MUST sit inside a media query that cannot match a
 * desktop viewport. That is the whole contract: a rule that cannot match
 * desktop cannot change a desktop pixel, so the laptop UI is provably
 * untouched by anything here.
 *
 * tests/Frontend/mobile-css-guard.test.js enforces it. Put a bare rule at the
 * top level and CI fails.
 *
 * The three queries, and what each is for:
 *
 *   max-width: 768px   layout. The app's existing phone breakpoint — six
 *                      blocks in styles.css already use it, so matching it
 *                      means one layout switch rather than two fighting.
 *   max-width: 480px   small-phone tightening only. No new structure.
 *   pointer: coarse    hit targets and font size only. NEVER layout: an iPad
 *                      Pro in landscape is 1366px wide and reports coarse, so
 *                      switching shape on it hands a desktop-sized screen the
 *                      phone design. Width decides shape, coarse decides touch.
 *
 * Two more rules:
 *   - Colours come from var(--*), never literals. styles.css:36-47 redefines
 *     only seven tokens for dark theme, so a hard-coded #fff is a white panel
 *     in dark mode.
 *   - !important is allowed on .grid-2 / .grid-3 / .row-wrap and nowhere else.
 *     Those three exist to beat inline style= attributes in index.html, which
 *     no stylesheet can reach any other way. The guard test caps the count.
 */

/* ===========================================================================
 * Shell
 * ======================================================================== */

@media (max-width: 768px) {

    /* styles.css:831 uses 100vh. On iOS Safari that is taller than the visible
     * area, so the bottom of every page hides behind the URL bar.
     *
     * Every dvh in this file is preceded by the same property in vh. A browser
     * that does not know dvh drops that line whole, and without the one above it
     * would be left with no value at all — which is how the public share page
     * lost its min-height. The guard test enforces the pairing. */
    .app {
        height: 100vh;
        height: 100dvh;
    }

    /* The 260px rail has nowhere to sit, so it becomes a drawer over the
     * content. transform rather than left: it composites on the GPU and does
     * not fight the width transition at styles.css:843.
     *
     * Layering: above the tracking overlay (1100) and the chatbot (1090),
     * below .modal (2100). */
    .sidebar {
        position: fixed;
        inset: 0 auto 0 0;
        width: var(--sidebar-width);
        max-width: 84vw;
        z-index: 1200;
        transform: translateX(-100%);
        transition: transform 0.25s ease;
    }

    .sidebar.is-open {
        transform: none;
    }

    .sidebar-nav {
        overscroll-behavior: contain;
    }

    /* touch-action is what actually stops the page scrolling under a thumb —
     * body already has overflow:hidden (styles.css:424), so there is no body
     * scroll to lock. */
    .nav-backdrop {
        position: fixed;
        inset: 0;
        z-index: 1190;
        background: rgba(0, 0, 0, 0.5);
        touch-action: none;
    }

    .header {
        padding: 0 1rem;
    }

    .header-left {
        gap: 0.5rem;
    }

    .header-left h1 {
        font-size: 1rem;
    }

    /* The extra bottom padding is for the chatbot button: it is fixed at the
     * bottom right, so without slack the last row of a list — which is where
     * the action buttons are on a card — sits under it and cannot be tapped. */
    .page {
        padding: 1rem;
        padding-bottom: 5.5rem;
    }

    /* styles.css:1061 has no flex-wrap, so a header with three buttons (title +
     * Groups + Add Several + Add Vehicle on Vehicles) runs off the edge. One
     * rule pair, every page.
     *
     * .dashboard-header is the same thing under a different name — it is the
     * Dashboard's page header and nothing else uses it. */
    .page-header,
    .dashboard-header {
        flex-direction: column;
        align-items: stretch;
        gap: 0.75rem;
    }

    .dashboard-header {
        padding: 1rem;
    }

    .page-actions,
    .dashboard-actions {
        flex-wrap: wrap;
        gap: 0.5rem;
    }

    .page-actions > *,
    .dashboard-actions > * {
        flex: 1 1 auto;
        min-width: 0;
    }
}

@media (max-width: 480px) {

    /* Side by side these get too narrow to read the label. */
    .page-actions > *,
    .dashboard-actions > * {
        flex: 1 1 100%;
    }
}

@media (max-width: 768px) and (prefers-reduced-motion: reduce) {
    .sidebar {
        transition: none;
    }
}

/* ===========================================================================
 * Login
 * ======================================================================== */

@media (max-width: 480px) {

    /* styles.css:383 centres the footer across the full panel width, and the
     * chatbot button is fixed over the bottom right corner (styles.css:6140),
     * so "© 2026 NexusGPS" ran underneath it. Centre it in what is left. */
    .login-foot {
        right: 72px;
        gap: 10px;
    }
}

/* ===========================================================================
 * Forms, modals, tabs — shared by nearly every page
 * ======================================================================== */

@media (max-width: 768px) {

    /* styles.css:539 is 1fr 1fr with no override at any breakpoint, and it is
     * used forty times across the thirty modals. One line, every form. */
    .form-row {
        grid-template-columns: 1fr;
    }

    /* Seven containers in index.html set grid-template-columns inline, which no
     * stylesheet can reach. This is the only !important in the file, and the
     * guard test caps it. */
    .grid-2,
    .grid-3 {
        grid-template-columns: 1fr !important;
    }

    .grid-2 > *,
    .grid-3 > * {
        grid-column: 1 !important;
    }

    .row-wrap > * {
        flex: 1 1 100% !important;
    }

    /* Six tabs wrapping eats three rows of an 844px-tall screen, so scroll them
     * sideways instead. Overrides the flex-wrap:wrap at styles.css:4788 —
     * same specificity, later sheet. */
    .admin-tabs,
    .range-mode-tabs {
        flex-wrap: nowrap;
        overflow-x: auto;
        scroll-snap-type: x proximity;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
    }

    .admin-tabs::-webkit-scrollbar,
    .range-mode-tabs::-webkit-scrollbar {
        display: none;
    }

    .admin-tab,
    .range-mode-tabs .rm-tab {
        flex: 0 0 auto;
        scroll-snap-align: start;
        white-space: nowrap;
    }

    .range-mode-tabs {
        display: flex;
        max-width: 100%;
    }

    /* styles.css:1910 floors the enhancer's search box at 220px, which is most
     * of the width before the row can wrap sensibly. */
    .te-search {
        min-width: 0;
        flex: 1 1 100%;
    }
}

@media (max-width: 480px) {

    /* A centred card wastes the edges and puts the buttons of a tall form off
     * screen. A sheet anchored to the bottom is the phone idiom, and it lets
     * the footer stay put while the body scrolls. */
    .modal {
        align-items: flex-end;
    }

    .modal-content,
    .modal-content.modal-lg,
    .modal-content.modal-xl,
    .modal-content.modal-sm {
        width: 100%;
        max-width: none;
        max-height: 92vh;
        max-height: 92dvh;
        border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    }

    /* These two cap the body at 70vh by id (styles.css:5978, 6014), which
     * leaves a short sheet with a scrollbar inside a scrollbar. Equal
     * specificity, later sheet. */
    #deviceModal .modal-body,
    #deviceViewModal .modal-body {
        max-height: none;
    }

    .modal-header,
    .modal-body,
    .modal-footer {
        padding-left: 1rem;
        padding-right: 1rem;
    }

    /* Save has to stay reachable in an eighteen-field form. */
    .modal-footer {
        position: sticky;
        bottom: 0;
        background: var(--bg-secondary);
    }

    .modal-footer .btn {
        flex: 1 1 auto;
    }
}

/* Sizing only, never layout: an iPad Pro in landscape is 1366px wide and also
 * reports coarse. */
@media (pointer: coarse) {

    /* Below 16px, iOS Safari zooms the page when a field takes focus and never
     * zooms back. styles.css ships 0.875rem on .form-control, and about a dozen
     * narrower rules go smaller still.
     *
     * The :not() clauses are not filters, they are specificity. A plain `input`
     * selector (0,1) loses to `.form-group input` (1,1) and the fix would be
     * silently dead on the device it is for. Each :not() adds the weight of its
     * argument, which brings these to (1,1) and (2,0) — enough to match or beat
     * every rule in styles.css, and ties go to this file because it loads last.
     * Measured: 127 fields under 16px before, none after. */
    input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]),
    select:not([multiple]),
    textarea:not([hidden]),
    .form-control:not([type="hidden"]),
    .filter-select:not([multiple]),
    .search-input:not([type="hidden"]),
    #loginModal input:not([type="hidden"]) {
        font-size: 16px;
    }

    /* .btn-sm has no minimum height, so the table action buttons are ~24px. */
    .btn-sm {
        min-height: 36px;
    }
}

/* ===========================================================================
 * Live tracking and the map toolbars
 * ======================================================================== */

@media (max-width: 768px) {

    /* The map fills its page, so this one does not want the chatbot clearance
     * .page gets: there is no list whose last row could hide under the button,
     * and the slack made a full-bleed map page scroll by 72px. Padding and the
     * height below have to agree, or it comes back. */
    #trackingPage {
        padding-bottom: 1rem;
    }

    /* styles.css:2040 sizes this off 100vh. 2rem is #trackingPage's own padding,
     * top and bottom. */
    .tracking-layout {
        height: calc(100vh - var(--header-height) - 2rem);
        height: calc(100dvh - var(--header-height) - 2rem);
    }

    /* styles.css:1181 floors every map at 500px, which is taller than the area
     * it has to sit in once the header and page padding are taken out. */
    .full-map {
        min-height: 0;
    }

    /* Seven labelled buttons in a row that cannot wrap come to about 770px on a
     * 390px map. Icons only, with the label moved to the tooltip — the buttons
     * carry data-i18n-title, so it is translated.
     *
     * Wrap rather than scroll. overflow-x:auto makes this a scroll container,
     * and a visible axis computes to auto when the other is not visible — so
     * overflow-y goes with it and the toolbar clips its OWN dropdowns. Pin Label
     * and Find Places are absolutely positioned inside it, which is why
     * styles.css:2099 sets overflow:visible. Icon-only these strips are 277px
     * and 168px wide, so they fit and never needed to scroll anyway. */
    .map-mode-toggle,
    .geofence-poi-toolbar {
        max-width: calc(100vw - 20px);
        flex-wrap: wrap;
    }

    .map-mode-btn > span:not(.pin-label-count) {
        display: none;
    }

    .map-mode-btn {
        padding: 10px 12px;
        flex: 0 0 auto;
    }

    /* The right edge is where the toolbar's own dropdowns are anchored, so the
     * toolbar must not run under them. */
    .geofence-poi-toolbar {
        right: 10px;
    }

    /* Anchored to their trigger, so without a cap they hang off the screen. */
    .poi-search-menu,
    .pin-label-menu,
    .vehicle-action-menu,
    .column-picker-menu {
        min-width: 0;
        max-width: calc(100vw - 16px);
    }

    /* 36px is under the 44px a thumb needs. Position stays the desktop one
     * (styles.css:2055) — under the Leaflet layers control, top-right.
     *
     * The map toolbar's dropdowns (z-index 1010) are wide enough on a phone to
     * reach this corner, so the button drops below them and gets covered while
     * one is open, instead of floating on top of it. */
    .tracking-sidebar-toggle {
        width: 44px;
        height: 44px;
        z-index: 1005;
    }

    /* A bottom sheet rather than a right-hand overlay: it is the phone idiom for
     * a list you pull up over a map, and it keeps the top of the map visible
     * while you scroll it. */
    .tracking-sidebar {
        top: auto;
        left: 0;
        right: 0;
        bottom: 0;
        width: auto;
        max-width: none;
        height: 62vh;
        height: 62dvh;
        border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    }

    .tracking-sidebar.is-collapsed {
        transform: translateY(100%);
    }

    .leaflet-control-zoom a {
        width: 36px;
        height: 36px;
        line-height: 36px;
    }
}

/* ===========================================================================
 * Dashboard
 * ======================================================================== */

@media (max-width: 768px) {

    /* Widget spans already collapse to a single column (styles.css:5212). What
     * is left is the frame around them: styles.css:5128 reserves a screen's
     * worth of grid whether or not there are widgets to fill it, and adds side
     * padding on top of the padding .page already has. */
    .customizable-dashboard {
        min-height: 0;
        padding: 0;
    }

    /* 200 / 350 / 500px (styles.css:5177-5187). A large widget is most of an
     * 844px screen, so two of them are a whole session of scrolling. */
    .widget-h-small .widget-content {
        height: 180px;
    }

    .widget-h-medium .widget-content {
        height: 240px;
    }

    .widget-h-large .widget-content {
        height: 320px;
    }

    /* Reordering is HTML5 drag-and-drop (customizable-dashboard.js:415, 1420,
     * 1447) with no touch fallback, so on a phone these two buttons do nothing
     * at all. Hidden rather than left there to be pressed; the layout saved on
     * a desktop still applies. */
    #customizeDashboardBtn,
    #resetDashboardBtn {
        display: none;
    }
}

/* A grab cursor promising a drag that cannot happen on touch. */
@media (pointer: coarse) {
    .dashboard-widget,
    .widget-header {
        cursor: default;
    }
}

/* ===========================================================================
 * List tables become cards
 *
 * Only tables that asked for it — TableEnhancer adds .te-cards from a per-table
 * `cards: true`, so the wide admin tables keep their sideways scroll port.
 *
 * Every cell carries data-label, stamped from its own <th> by
 * table-enhancer.js:_stampLabels. Sort, search and pagination keep working
 * untouched: _render hides rows with an inline display:none and restores with
 * '', never 'table-row', so a visible row falls through to the display:block
 * below.
 * ======================================================================== */

@media (max-width: 768px) {

    .data-table.te-cards,
    .data-table.te-cards tbody {
        display: block;
    }

    .data-table.te-cards thead {
        display: none;
    }

    .data-table.te-cards tr.te-card {
        display: block;
        background: var(--bg-secondary);
        border: 1px solid var(--border-color);
        border-radius: var(--radius);
        margin-bottom: 0.75rem;
        padding: 0.25rem 0.875rem;
    }

    /* styles.css:1886 tints the row on hover, which on a card reads as the
     * whole card being pressed. */
    .data-table.te-cards tbody tr:hover {
        background: var(--bg-secondary);
    }

    /* Only stamped cells become label/value pairs. The "Loading…" and
     * "no matching rows" cells span every column and have no label, so they
     * are deliberately left alone below. */
    .data-table.te-cards td[data-label] {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: 1rem;
        padding: 0.5rem 0;
        border-bottom: 1px solid var(--border-color);
        text-align: right;
        white-space: normal;
    }

    .data-table.te-cards td[data-label]::before {
        content: attr(data-label);
        flex: 0 0 auto;
        color: var(--text-secondary);
        font-weight: 600;
        text-align: left;
    }

    .data-table.te-cards tr.te-card td:last-child {
        border-bottom: none;
    }

    /* Actions go in a row across the foot of the card, with no label. */
    .data-table.te-cards td.te-card-actions {
        justify-content: flex-end;
        flex-wrap: wrap;
        gap: 0.5rem;
        padding-top: 0.625rem;
    }

    .data-table.te-cards td.te-card-actions::before {
        content: none;
    }

    .data-table.te-cards td:not([data-label]) {
        display: block;
        text-align: center;
    }

    /* TableScroll still wraps the table (it runs before the card class is read)
     * and styles.css:1754 makes the last column sticky. Neither means anything
     * once the rows are cards, and a card stack inside a horizontal scroller
     * would scroll sideways for no reason. */
    .te-scrollframe:has(.te-cards) .te-scrollport,
    .te-scrollport:has(.te-cards) {
        overflow-x: visible;
    }

    .te-scrollframe.te-pin .data-table.te-cards th:last-child,
    .te-scrollframe.te-pin .data-table.te-cards td:last-child {
        position: static;
        box-shadow: none;
        background: transparent;
    }

    .te-scrollframe:has(.te-cards) .te-nav-layer {
        display: none;
    }
}

/* ===========================================================================
 * Reports, Geofences, Routes — the fixed-width split layouts
 * ======================================================================== */

@media (max-width: 768px) {

    /* styles.css:3301 is three columns and stops at two (:3686). It never
     * reaches one, so seven tiles share 390px: about 94px each, 64px of which
     * is the card's own padding. */
    .reports-grid {
        grid-template-columns: 1fr;
        gap: 0.75rem;
    }

    .report-card {
        padding: 1.25rem;
        text-align: left;
    }

    /* styles.css:3119 already collapses to one column at this width, but both
     * halves then share one fixed calc(100vh - …) and each gets half a screen
     * with the map unusable. Let them size to content and the page scroll. */
    .geofence-layout {
        height: auto;
    }

    .geofence-layout > * {
        min-height: 60vh;
    }

    /* styles.css:3132 centres the draw toolbar with translateX(-50%) and
     * :3149 stops it wrapping, so it bleeds off both edges. Pin it to both
     * sides instead and let it wrap. */
    .geofence-draw-toolbar {
        left: 10px;
        right: 10px;
        transform: none;
    }

    .draw-toolbar-content {
        flex-wrap: wrap;
        white-space: normal;
    }

    .re-geofence-panel {
        width: auto;
        max-width: calc(100vw - 32px);
    }

    /* Route replay: styles.css:7454 puts a 320px fixed sidebar beside the map
     * with no override anywhere, leaving about 70px for the map. */
    .page-report-inline .ri-body.route-replay-body {
        flex-direction: column;
        overflow: auto;
    }

    .page-report-inline .rr-map-col {
        min-height: 55vh;
    }

    .page-report-inline .rr-sidebar {
        width: auto;
        flex-shrink: 1;
        border-left: none;
        border-top: 1px solid var(--border-color);
    }

    .rr-time {
        min-width: 0;
    }
}

@media (max-width: 480px) {

    /* styles.css:5714 goes 5 -> 3 -> 2 columns and stops. Two stat cards, each
     * with a 56px icon, do not fit 390px — this is the last page that still
     * pushed its own content sideways. */
    .stats-row {
        grid-template-columns: 1fr;
    }

    /* styles.css:6462 is a 92vw modal holding a 380px sidebar that will not
     * shrink (:6476) plus a map, so the map never gets a pixel. Full screen,
     * and the two stack. */
    .route-editor-modal {
        width: 100%;
        max-width: none;
        height: 100vh;
        height: 100dvh;
        border-radius: 0;
    }

    .route-editor-body {
        flex-direction: column;
        overflow: auto;
    }

    .route-editor-sidebar {
        width: auto;
        flex-shrink: 1;
        border-right: none;
        border-bottom: 1px solid var(--border-color);
    }
}

@media (max-width: 768px) {

    /* The card view hides the <thead>, and those headers are the sort buttons —
     * so without this a phone is stuck with whatever sort was last saved on a
     * desktop. TableEnhancer builds the control with `hidden` set, which the UA
     * stylesheet honours everywhere except here. */
    .te-cardsort[hidden] {
        display: flex;
        align-items: center;
        gap: 0.4rem;
        white-space: nowrap;
    }

    .te-cardsort select {
        width: auto;
        min-width: 0;
    }
}
