/**
 * Clientra CRM — Mobile Design Layer
 * =================================================================
 * Loads AFTER main.css. Purely additive: this file overrides, it never
 * requires an edit to variables.css / components.css / main.css.
 *
 * Design language: Apple's fluid-interface model translated to the web.
 *   response       — feedback on pointer-down, never on release
 *   direct         — content tracks the finger 1:1, honouring grab offset
 *   interruptible  — every motion can be grabbed and reversed mid-flight
 *   material       — translucent layers carry hierarchy; content flows under
 *   restraint      — motion serves comprehension, never decoration
 *
 * Motion itself lives in assets/js/mobile-ui.js (springs are stateful and
 * velocity-aware; CSS transitions cannot be interrupted, so gesture-driven
 * elements here deliberately declare NO transition on transform).
 *
 * HOUSE RULE (HANDBOOK, "Lessons paid for"):
 *   Settings > Appearance sets `zoom` on <body> and mirrors it into
 *   --zoom-factor. Under zoom, vh/vw resolve against the UNZOOMED viewport
 *   and are then scaled down. So: never vh/vw inside a fixed overlay.
 *   Use a percentage of the overlay, or divide by --zoom-factor, or let
 *   mobile-ui.js measure the real box with getBoundingClientRect().
 * =================================================================
 */

/* =================================================================
   00 · TOKENS — mobile-only, scoped so desktop is untouched
   ================================================================= */
@media (max-width: 768px) {
    :root {
        /* --- Material: translucent layers, weight encodes hierarchy --- */
        --mat-thin-bg:      rgba(10, 10, 10, 0.62);
        --mat-regular-bg:   rgba(10, 10, 10, 0.78);
        --mat-thick-bg:     rgba(18, 18, 18, 0.90);
        --mat-thin-blur:    blur(20px) saturate(180%);
        --mat-regular-blur: blur(30px) saturate(180%);
        --mat-thick-blur:   blur(40px) saturate(190%);
        /* Bright top edge = light catching the surface of a real material */
        --mat-edge:         rgba(255, 255, 255, 0.10);
        --mat-edge-strong:  rgba(255, 255, 255, 0.16);

        /* --- Depth: bigger surfaces read as thicker --- */
        --lift-1: 0 1px 2px rgba(0, 0, 0, .30);
        --lift-2: 0 4px 16px rgba(0, 0, 0, .34), 0 1px 2px rgba(0, 0, 0, .28);
        --lift-3: 0 12px 34px rgba(0, 0, 0, .44), 0 2px 6px rgba(0, 0, 0, .30);
        --lift-4: 0 -14px 44px rgba(0, 0, 0, .55), 0 -1px 0 var(--mat-edge);

        /* --- Geometry: nested radii stay concentric --- */
        --r-chip:   999px;
        --r-ctl:    12px;
        --r-card:   18px;
        --r-sheet:  26px;
        --r-tile:   14px;

        /* --- Rhythm --- */
        --gutter: 16px;
        --tabbar-h: 54px;

        /* env() is a viewport-derived length, so inside the zoomed subtree it
           renders at inset x zoom — the same defect as vh, invisible to a vh
           grep. Compensate once, here, and every consumer is correct. */
        --safe-b: calc(env(safe-area-inset-bottom, 0px) / var(--zoom-factor, 1));
        --safe-t: calc(env(safe-area-inset-top, 0px) / var(--zoom-factor, 1));
    }

    [data-theme="light"] {
        --mat-thin-bg:      rgba(255, 255, 255, 0.66);
        --mat-regular-bg:   rgba(255, 255, 255, 0.80);
        --mat-thick-bg:     rgba(252, 252, 253, 0.92);
        --mat-edge:         rgba(255, 255, 255, 0.75);
        --mat-edge-strong:  rgba(255, 255, 255, 0.95);
        --lift-1: 0 1px 2px rgba(16, 24, 40, .06);
        --lift-2: 0 4px 16px rgba(16, 24, 40, .09), 0 1px 2px rgba(16, 24, 40, .05);
        --lift-3: 0 12px 34px rgba(16, 24, 40, .14), 0 2px 6px rgba(16, 24, 40, .06);
        --lift-4: 0 -14px 44px rgba(16, 24, 40, .18), 0 -1px 0 rgba(16, 24, 40, .05);
    }
}

/* =================================================================
   01 · BASE — viewport, safe areas, scroll behaviour
   ================================================================= */
@media (max-width: 768px) {

    html {
        -webkit-text-size-adjust: 100%;
        text-size-adjust: 100%;
    }

    body {
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        /* Kill the grey tap flash — we draw our own press state, instantly. */
        -webkit-tap-highlight-color: transparent;
    }

    /* Chrome must not be selectable on long-press. */
    .sidebar,
    .header,
    .mobile-tabbar,
    .btn,
    .sidebar__link,
    .sheet-grabber {
        -webkit-user-select: none;
        user-select: none;
        -webkit-touch-callout: none;
    }

    /* Content is the exception — Arabic names and refs must stay copyable. */
    .page p,
    .page td,
    .page .dtc-field,
    .page .card-body {
        -webkit-user-select: text;
        user-select: text;
    }

    .app {
        min-height: calc(100dvh / var(--zoom-factor, 1));
    }

    /* Bottom gutter clears the tab bar + the home indicator.
       --tabbar-space is 0 until a tab bar is actually mounted. */
    .page {
        padding: var(--gutter);
        /* !important because addons/mobile-app/mobile.css also declares this
           with !important and is injected into <head> AFTER our <link>. */
        padding-bottom: calc(var(--gutter) + var(--tabbar-space, 0px) + var(--safe-b)) !important;
    }
}

/* =================================================================
   02 · TYPOGRAPHY — tracking and leading are size-specific
   -----------------------------------------------------------------
   A single letter-spacing value is wrong somewhere. Large text reads
   too loose as it grows (negative tracking); small text needs a touch
   of positive tracking to stay legible. Leading moves inversely.
   ================================================================= */
@media (max-width: 768px) {

    body {
        font-size: 16px;
        line-height: 1.45;
        letter-spacing: 0;
    }

    /* Page title — the largest type on a phone screen.
       Scoped through .page-header because main.css already declares
       `.page-header .page-title { font-size: 22px; letter-spacing: -0.3px }`
       at this breakpoint. A bare `.page-title` (0,1,0) loses to it on size
       and tracking while still winning on line-height — a partial override,
       which is worse than none: the type ends up at someone else's size with
       our leading. Match the specificity and own all three. */
    .page-header .page-title,
    .page-title,
    .header__title {
        font-size: 26px;
        line-height: 1.12;
        letter-spacing: -0.021em;
        font-weight: 700;
        font-optical-sizing: auto;
    }

    .page-subtitle {
        font-size: 14px;
        line-height: 1.42;
        letter-spacing: -0.003em;
        color: var(--color-text-secondary);
    }

    /* Section / card titles */
    .card-title,
    .modal-header h3,
    .page h2 {
        font-size: 19px;
        line-height: 1.24;
        letter-spacing: -0.014em;
        font-weight: 650;
    }

    .page h3,
    .card-header h3 {
        font-size: 16px;
        line-height: 1.3;
        letter-spacing: -0.009em;
        font-weight: 620;
    }

    /* Body copy sits at zero tracking — the size the face was cut for. */
    .page p,
    .card-body {
        font-size: 15px;
        line-height: 1.5;
        letter-spacing: 0;
    }

    /* Footnote / metadata — slight positive tracking buys legibility back */
    .text-muted,
    .form-hint,
    .dtc-field,
    .badge,
    small {
        font-size: 13px;
        line-height: 1.4;
        letter-spacing: 0.004em;
    }

    /* Caption / all-caps labels need the most tracking of all */
    .form-label,
    .mobile-tabbar__label {
        font-size: 11px;
        line-height: 1.32;
        letter-spacing: 0.055em;
        text-transform: uppercase;
        font-weight: 640;
    }

    /* Emphasis comes from weight, not size — presence without extra space. */
    .dtc-head,
    .kpi-value,
    strong {
        font-weight: 660;
        letter-spacing: -0.011em;
    }

    /* Numerals in lists must align vertically — tabular figures, always. */
    .dtc-amount,
    .dtc-date,
    .kpi-value,
    td[data-label*="Price"],
    td[data-label*="Value"],
    td[data-label*="Amount"] {
        font-variant-numeric: tabular-nums;
        font-feature-settings: "tnum" 1;
    }

    /* Mixed Arabic/Latin content: never let one unbroken ref blow out a card. */
    .page .dtc-field,
    .page .card-body,
    .page td,
    .page .lmc-row {
        overflow-wrap: anywhere;
    }
}

/* =================================================================
   03 · RESPONSE — feedback lands on pointer-down, never on release
   -----------------------------------------------------------------
   The moment lag appears the sense of directness falls off a cliff.
   `.is-pressed` is applied by mobile-ui.js on `pointerdown` (iOS does
   not fire :active reliably), and removed on up / cancel / drag-away.
   ================================================================= */
@media (pointer: coarse) {

    .btn,
    .sidebar__link,
    .mobile-tabbar__item,
    .card.is-tappable,
    .lead-mcard,
    .chip,
    .header__toggle,
    [role="button"] {
        /* Compositor-only, so a press never costs a layout pass. */
        transition: transform 110ms cubic-bezier(.22, .61, .36, 1),
                    opacity   110ms cubic-bezier(.22, .61, .36, 1),
                    background-color 110ms cubic-bezier(.22, .61, .36, 1);
    }

    /* Small controls: a crisp scale reads as a physical press. */
    .btn.is-pressed,
    .header__toggle.is-pressed,
    .chip.is-pressed,
    .lmc-act.is-pressed,
    .mobile-tabbar__item.is-pressed {
        transform: scale(0.94);
        opacity: 0.78;
    }

    /* Large surfaces: the same 6% would read as a jolt — go gentler and let
       the background carry more of the signal. */
    .card.is-tappable.is-pressed,
    .lead-mcard.is-pressed,
    .sidebar__link.is-pressed,
    tr.is-pressed {
        transform: scale(0.985);
        background-color: var(--color-bg-card-hover);
    }

    /* Hover is a lie on touch — a sticky :hover must never look like state. */
    .btn:hover,
    .card:hover,
    .sidebar__link:hover,
    .header__toggle:hover {
        background-color: inherit;
    }

    /* Minimum comfortable target, everywhere. */
    .btn,
    .form-input,
    .form-select,
    .sidebar__link,
    .header__toggle,
    .lmc-act {
        min-height: 44px;
    }

    /* Icon buttons keep their visual size but grow their hit area (~10px). */
    .lmc-act,
    .header__toggle,
    .btn-icon {
        position: relative;
    }
    .lmc-act::after,
    .header__toggle::after,
    .btn-icon::after {
        content: '';
        position: absolute;
        inset: -6px;
    }
}

/* =================================================================
   04 · MATERIALS — the header as a translucent layer
   -----------------------------------------------------------------
   Chrome is a floating functional layer, not an opaque strip that eats
   a fixed band of the screen. Content scrolls UNDER it.
   ================================================================= */
@media (max-width: 768px) {

    .header {
        position: sticky;
        top: 0;
        z-index: 50;
        height: auto;
        min-height: 52px;
        padding: calc(var(--safe-t) + 6px) 12px 6px;
        gap: 8px;

        background: var(--mat-thin-bg);
        -webkit-backdrop-filter: var(--mat-thin-blur);
        backdrop-filter: var(--mat-thin-blur);

        /* No hard 1px rule. The separation appears only once content is
           actually behind the bar — see .is-scrolled below. */
        border-bottom: 1px solid transparent;
        box-shadow: none;
        transition: background-color 220ms ease,
                    border-color     220ms ease,
                    box-shadow       220ms ease,
                    -webkit-backdrop-filter 220ms ease,
                    backdrop-filter  220ms ease;
    }

    /* Scroll edge effect: the material thickens and a soft edge fades in,
       only when something is genuinely underneath it. */
    .header.is-scrolled {
        background: var(--mat-regular-bg);
        -webkit-backdrop-filter: var(--mat-regular-blur);
        backdrop-filter: var(--mat-regular-blur);
        border-bottom-color: color-mix(in srgb, var(--color-border) 60%, transparent);
        box-shadow: 0 8px 22px -14px rgba(0, 0, 0, .55);
    }

    /* Vibrancy: over a blurred surface, flat grey text dissolves. Lift the
       contrast, add a touch of weight and tracking instead. */
    .header .header__title,
    .header .header__toggle {
        color: var(--color-text-primary);
        font-weight: 640;
        letter-spacing: 0.002em;
    }

    .header__toggle {
        width: 40px;
        height: 40px;
        border-radius: var(--r-ctl);
        background: transparent;
    }

    /* The desktop hamburger is meaningless here; the mobile pair is shown
       by app.js. Keep the row tight so the title owns the space. */
    .header__left,
    .header__right {
        gap: 6px;
    }
}

/* =================================================================
   05 · DRAWER — the sidebar as a grabbable panel
   -----------------------------------------------------------------
   NO transition on transform: mobile-ui.js drives it with a spring so
   it can be caught mid-flight and reversed. A CSS transition here would
   make the panel refuse the finger until it finished animating.
   ================================================================= */
@media (max-width: 768px) {

    .sidebar {
        z-index: 100;
        width: min(84%, 320px);
        height: calc(100dvh / var(--zoom-factor, 1));
        padding-top: var(--safe-t);
        padding-bottom: var(--safe-b);

        background: var(--mat-thick-bg);
        -webkit-backdrop-filter: var(--mat-thick-blur);
        backdrop-filter: var(--mat-thick-blur);
        border-right: 1px solid var(--mat-edge);

        /* No shadow while it is shut.
         *
         * The panel is parked at -315px, but a 60px blur spreading 18px to
         * the RIGHT of it still lands inside the viewport - so every page on
         * a phone carried a permanent dark smear down its left edge. The
         * shadow only has a job when the drawer is actually over the page. */
        box-shadow: none;

        /* CSS drives this now, not a spring.
         *
         * The drawer used to be positioned by an inline transform written
         * every frame by a spring in mobile-ui.js. That gave it a gesture and
         * a nice curve, and it also gave it five ways to get stuck open: a
         * frozen rAF, a throttled watchdog timer, a pointer capture from a
         * stray drag, a stale open flag, and a reduced-motion rule that nulled
         * the very transform it depended on.
         *
         * A drawer has one job. `.open` is now the only state, the transform
         * lives entirely in the stylesheet, and the browser animates between
         * them - which it cannot fail to do. */
        transition: transform 260ms cubic-bezier(.32, .72, 0, 1);
        will-change: transform;

        /* The nav scrolls vertically; the panel is dragged horizontally.
           Declaring pan-y lets the browser own one axis and us the other,
           so both gestures are recognised in parallel and neither has to
           wait on the other to be disambiguated. */
        touch-action: pan-y;
    }

    /* While a gesture or spring is running, promote to its own layer. */
    .sidebar.is-animating,
    .sidebar.is-dragging {
        will-change: transform;
    }

    .sidebar-backdrop {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 90;
        background: rgba(0, 0, 0, 0.45);
        -webkit-backdrop-filter: blur(3px);
        backdrop-filter: blur(3px);
        /* Opacity is written every frame by the drawer spring so the scrim
           tracks the finger continuously — it hints where the panel is going
           instead of appearing only once the gesture completes. */
        opacity: 0;
        visibility: hidden;
        transition: visibility 0s linear 260ms;
    }

    .sidebar.open {
        box-shadow: 18px 0 60px rgba(0, 0, 0, .5);
    }

    .sidebar-backdrop.open {
        visibility: visible;
        /* opacity was written inline by the spring; with the spring gone the
           class has to carry it or the scrim stays invisible. */
        opacity: 1;
        transition: opacity 260ms ease, visibility 0s;
    }

    /* A drawer has no collapsed mode — but `.collapsed` is persisted in
       localStorage from the desktop rail and re-applied on load, and every
       `.sidebar.collapsed …` rule in main.css is (0,2,0) while the phone
       width rule is (0,1,0). A media query adds no specificity, so today a
       user who ever collapsed the rail gets an 80px drawer with every label
       hidden and the page shoved 80px right. Neutralise the whole state. */
    .sidebar,
    .sidebar.collapsed {
        width: min(84%, 320px);
    }

    .sidebar.collapsed .sidebar__logo-text,
    .sidebar.collapsed .sidebar__section-title,
    .sidebar.collapsed .sidebar__link-text,
    .sidebar.collapsed .sidebar__user-info,
    .sidebar.collapsed .sidebar__group-chevron {
        display: revert;
    }

    .sidebar.collapsed ~ .main-content,
    .main-content {
        margin-left: 0;
    }

    /* An edge-swipe target that never blocks content: 18px of screen edge,
       only live when the drawer is closed. */
    .drawer-edge {
        position: fixed;
        top: 0;
        bottom: 0;
        inset-inline-start: 0;
        width: 18px;
        z-index: 80;
        touch-action: pan-y;
    }

    .sidebar__link {
        border-radius: var(--r-ctl);
        margin-inline: 8px;
        font-size: 15px;
        letter-spacing: -0.006em;
    }

    /* The login screen has no navigation at all. */
    .app.auth-mode .sidebar-backdrop,
    .app.auth-mode .drawer-edge,
    .app.auth-mode .mobile-tabbar {
        display: none !important;
    }
}

/* =================================================================
   06 · BOTTOM SHEET — every modal, on a phone
   -----------------------------------------------------------------
   The old rules in main.css targeted `.modal-content`, a class the
   markup never uses (every modal is `.modal-overlay > .modal`), so they
   have never applied. These do.
   ================================================================= */
@media (max-width: 768px) {

    .modal-overlay {
        align-items: flex-end !important;
        justify-content: center;
        padding: 0 !important;
        background: rgba(0, 0, 0, 0.55);
        /* Written per-frame by the sheet spring while dragging. */
        transition: opacity 240ms cubic-bezier(.22, .61, .36, 1),
                    visibility 0s linear 240ms;
    }

    .modal-overlay.active {
        transition: opacity 260ms cubic-bezier(.22, .61, .36, 1),
                    visibility 0s;
    }

    /* Geometry only. Applied to ANY first child, because not every overlay in
       this codebase wraps its content in `.modal`.
       Deliberately NO `transform: translateY(100%)` here: the off-screen
       start state belongs to JS. If mobile-ui.js ever fails to load, a CSS
       hidden-state would strand every modal below the fold with no way back
       — the sheet must degrade to a plain, visible panel, not to nothing. */
    .modal-overlay > * {
        width: 100% !important;
        max-width: 100% !important;
        margin: 0 !important;
        border-radius: var(--r-sheet) var(--r-sheet) 0 0 !important;
        border-inline: none;
        border-bottom: none;
        border-top: 1px solid var(--mat-edge);
        box-shadow: var(--lift-4);

        /* max-height is overwritten in px by mobile-ui.js from a measured
           getBoundingClientRect() of the overlay — never from vh, which lies
           under body zoom. This percentage is only the pre-JS fallback. */
        /* !important: 22 JS files hand-roll an inline `max-height:NNvh` on
           their modal child. Without it the sheet silently reverts to the
           broken vh cap on most real modals. mobile-ui.js then overwrites
           this with a measured px value (inline vs inline, last write wins). */
        max-height: 92% !important;
        overflow: hidden;
    }

    /* Layout assumptions only for the standard modal shape. A custom child
       (image viewer, PDF canvas, re-modal-content) keeps its own layout. */
    .modal-overlay > .modal {
        display: flex;
        flex-direction: column;
        transition: none;
    }

    /* Both states neutralise components.css `transform: scale(.9)/scale(1)`,
       which would fight the sheet transform JS writes inline. */
    .modal-overlay > .modal,
    .modal-overlay.active > .modal {
        transform: none;
    }

    .modal-overlay.is-dragging > * {
        will-change: transform;
    }

    /* While the sheet is being dragged, its scrim opacity is written every
       frame by the spring. A CSS transition on the same property turns each
       of those writes into a fresh 260ms interpolation, so the scrim becomes
       a low-pass filter of the finger instead of tracking it — the backdrop
       lags roughly 80ms behind the sheet and is still drifting after release.
       The drawer's scrim (.sidebar-backdrop) never had this problem because
       it declares no opacity transition at all; the sheet needs one for the
       tap-to-open path, so it is cancelled for the duration of the gesture. */
    .modal-overlay.is-dragging,
    .modal-overlay.active.is-dragging {
        transition: none !important;
    }

    /* The grabber: the affordance that says this surface can be thrown away.
       Injected by mobile-ui.js into the header of every sheet. */
    .sheet-grabber {
        position: relative;
        flex: 0 0 auto;
        height: 22px;
        display: flex;
        align-items: center;
        justify-content: center;
        touch-action: none;
        cursor: grab;
    }

    .sheet-grabber::before {
        content: '';
        width: 38px;
        height: 5px;
        border-radius: 999px;
        background: color-mix(in srgb, var(--color-text-secondary) 45%, transparent);
        transition: background-color 140ms ease, width 140ms ease;
    }

    .modal-overlay.is-dragging .sheet-grabber::before {
        background: color-mix(in srgb, var(--color-text-primary) 60%, transparent);
        width: 48px;
    }

    /* A sheet the user has typed into refuses to be thrown away by a gesture.
       The refusal has to be VISIBLE, not just felt — a control that silently
       ignores you reads as broken, one that visibly holds its ground reads as
       protective. Colour + weight, no movement, so it survives reduced-motion. */
    .modal-overlay.is-refusing .sheet-grabber::before {
        background: var(--color-warning);
        width: 56px;
        height: 6px;
    }

    .modal-header {
        padding: 8px var(--gutter) 12px;
        border-bottom: 1px solid transparent;
        transition: border-color 200ms ease;
        flex: 0 0 auto;
    }

    /* Same scroll edge treatment as the app header, inside the sheet. */
    .modal-header.is-scrolled {
        border-bottom-color: color-mix(in srgb, var(--color-border) 70%, transparent);
    }

    .modal-body {
        padding: 4px var(--gutter) var(--gutter) !important;
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
        /* A sheet body must not hand its overscroll to the page behind it. */
        overscroll-behavior: contain;
        flex: 1 1 auto;
        min-height: 0;
    }

    /* A sheet with no footer must still clear the home indicator itself. */
    .modal-body:last-child {
        padding-bottom: calc(var(--gutter) + var(--safe-b)) !important;
    }

    /* Footer actions ride the material, above the home indicator. */
    .modal-footer {
        position: sticky;
        bottom: 0;
        flex: 0 0 auto;
        padding: 12px var(--gutter) calc(12px + var(--safe-b));
        background: var(--mat-thick-bg);
        -webkit-backdrop-filter: var(--mat-regular-blur);
        backdrop-filter: var(--mat-regular-blur);
        border-top: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
        display: flex;
        gap: 10px;
    }

    .modal-footer .btn {
        flex: 1 1 0;
        justify-content: center;
    }

    /* Dim to focus: a modal task pushes the app back and down a little, so
       the sheet reads as arriving on top of a real, receding surface. */
    .app.sheet-open > .main-content {
        transform: scale(0.976) translateY(-6px);
        transform-origin: 50% 0;
        border-radius: 14px;
        overflow: hidden;
        transition: transform 320ms cubic-bezier(.32, .72, 0, 1);
    }

    /* The drawer recedes only when it is ALREADY open, and keeps its
       translateX when it does.
     *
     * `.app.sheet-open > .sidebar` used to be in the rule above. For
     * .main-content the transform is decoration; for the drawer it is
     * POSITION - main.css parks the closed panel at translateX(-100%) and
     * slides it to translateX(0) with `.open`. At (0,3,0) the shared rule
     * outranked both (0,1,0) and (0,2,0) and replaced them with a transform
     * carrying no translateX at all, so the panel jumped to x:0. Opening any
     * modal on a phone dragged the whole sidebar on screen beside it, and
     * closing the modal took both away together.
     *
     * This is the same trap the prefers-reduced-motion block below documents
     * and exempts `.sidebar` from: the exemption was applied there and missed
     * here. Scoping to `.sidebar.open` (0,4,0) means a CLOSED drawer matches
     * nothing here and keeps main.css's translateX(-100%). */
    .app.sheet-open > .sidebar.open {
        transform: translateX(0) scale(0.976) translateY(-6px);
        transform-origin: 50% 0;
        border-radius: 14px;
        overflow: hidden;
        transition: transform 320ms cubic-bezier(.32, .72, 0, 1);
    }

    .app:not(.sheet-open) > .main-content {
        transition: transform 320ms cubic-bezier(.32, .72, 0, 1);
    }
}

/* =================================================================
   07 · TAB BAR — wayfinding: where am I, where can I go
   -----------------------------------------------------------------
   Built by mobile-ui.js from the links the sidebar actually rendered,
   so it inherits the permission filter and can never show a route the
   user is not allowed to open.
   ================================================================= */
@media (max-width: 768px) {

    .mobile-tabbar {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 85;

        /* Counter-zoom, exactly as .modal-overlay does in components.css.
           <body> carries zoom:var(--zoom-factor) (Settings > Appearance,
           85% by default here). A position:fixed child of a zoomed element
           resolves its offsets in the ZOOMED coordinate space, so bottom:0
           lands at 85% of the viewport height - the bar floats well above
           the bottom of the screen, over the content, which is exactly what
           it was doing. Dividing the zoom back out puts it on the real
           viewport edge where a tab bar belongs.

           This was the only fixed overlay in the app that had not been given
           this treatment; the sidebar already divides its height by the same
           factor. */
        zoom: calc(1 / var(--zoom-factor, 1));
        display: flex;
        align-items: stretch;
        justify-content: space-around;
        gap: 2px;
        padding: 4px 6px calc(4px + var(--safe-b));

        background: var(--mat-regular-bg);
        -webkit-backdrop-filter: var(--mat-regular-blur);
        backdrop-filter: var(--mat-regular-blur);
        border-top: 1px solid var(--mat-edge);
        box-shadow: 0 -8px 28px -18px rgba(0, 0, 0, .6);

        /* Slides away under a sheet / drawer, spring-driven. */
        transition: none;
    }

    .mobile-tabbar__item {
        flex: 1 1 0;
        min-width: 0;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 3px;
        padding: 6px 2px;
        min-height: var(--tabbar-h);
        border: none;
        background: transparent;
        border-radius: var(--r-ctl);
        color: var(--color-text-muted);
        text-decoration: none;
        cursor: pointer;
    }

    .mobile-tabbar__icon {
        display: block;
        width: 23px;
        height: 23px;
        flex: 0 0 auto;
    }

    .mobile-tabbar__icon svg {
        width: 100%;
        height: 100%;
        stroke-width: 1.9;
    }

    .mobile-tabbar__label {
        font-size: 10px;
        line-height: 1.1;
        letter-spacing: 0.03em;
        text-transform: none;
        font-weight: 600;
        max-width: 100%;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .mobile-tabbar__item.is-active {
        color: var(--color-primary);
    }

    /* Selection is carried by colour and weight, not a box that competes
       with the content above it. */
    .mobile-tabbar__item.is-active .mobile-tabbar__label {
        font-weight: 680;
    }

    /* A live count belongs on the icon it describes. */
    .mobile-tabbar__badge {
        position: absolute;
        top: 3px;
        inset-inline-end: 22%;
        min-width: 16px;
        height: 16px;
        padding: 0 4px;
        border-radius: 999px;
        background: var(--color-danger);
        color: #fff;
        font-size: 10px;
        font-weight: 700;
        line-height: 16px;
        text-align: center;
        font-variant-numeric: tabular-nums;
    }

    .mobile-tabbar__item {
        position: relative;
    }

    /* When a sheet or the drawer owns the screen, the tab bar is not a
       reachable destination — get it out of the way. */
    .mobile-tabbar.is-hidden {
        pointer-events: none;
    }
}

/* =================================================================
   08 · LISTS & CARDS — the record is the unit of the phone screen
   ================================================================= */
@media (max-width: 768px) {

    .card {
        border-radius: var(--r-card);
        box-shadow: var(--lift-2);
        border: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
    }

    .card-body {
        padding: 14px !important;
    }

    .page-header {
        flex-direction: column;
        align-items: stretch;
        gap: 10px;
        margin-bottom: 14px;
    }

    /* NOTHING here about the dashboard KPIs — deliberately.
       Dashboard.js already injects a considered phone treatment: below 768px
       the KPI row becomes a horizontally scrollable rail of stat chips, with
       its own card padding, radius and .kpi-value size, alongside a .mob-hero
       and a .mob-quick action grid. That block is injected into <head> at
       render time, i.e. AFTER this stylesheet, so at equal specificity it
       wins anyway — and it should. A third opinion on the same selectors is
       how the .filters-row contradiction in main.css/components.css happened.
       The dashboard owns its own phone layout. Leave it alone. */

    /* Entity cards get a real press target and concentric corners. */
    .lead-mcard,
    .property-card,
    .mobile-card-table tbody tr {
        border-radius: var(--r-card);
    }

    /* Empty states: never leave the screen blank and silent. */
    .empty-state {
        padding: 40px 24px;
        text-align: center;
        color: var(--color-text-secondary);
    }
}

/* =================================================================
   09 · CHIPS & FILTER RAILS — momentum with snap
   ================================================================= */
@media (max-width: 768px) {

    .filters-row,
    .page-header .page-actions,
    .sc-chips,
    .prop-search-chips {
        scroll-snap-type: x proximity;
        scroll-padding-inline-start: 2px;
        overscroll-behavior-x: contain;
        /* A rail that can scroll must say so — fade its trailing edge. */
        -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 22px), transparent 100%);
        mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 22px), transparent 100%);
    }

    .filters-row > *,
    .page-header .page-actions > *,
    .sc-chips > *,
    .prop-search-chips > * {
        scroll-snap-align: start;
    }

    /* The mask must not clip a rail that has nothing to scroll. */
    .filters-row.is-complete,
    .page-header .page-actions.is-complete,
    .sc-chips.is-complete,
    .prop-search-chips.is-complete {
        -webkit-mask-image: none;
        mask-image: none;
    }
}

/* =================================================================
   10 · FORMS — 16px minimum, generous targets, honest validation
   ================================================================= */
@media (max-width: 768px) {

    .form-input,
    .form-select,
    .form-textarea,
    textarea.form-input,
    textarea,
    input[type="text"],
    input[type="search"],
    input[type="email"],
    input[type="tel"],
    input[type="number"],
    input[type="date"] {
        /* 16px is the floor below which iOS Safari zooms the page on focus —
           it has ignored user-scalable=no for that heuristic since iOS 10.
           Body zoom multiplies on top, so a flat 16px renders ~13.6px at the
           85% interface size and re-triggers it. Divide it back out. */
        font-size: calc(16px / var(--zoom-factor, 1)) !important;
        border-radius: var(--r-ctl);
        padding: 11px 13px;
    }

    /* Filter rails set 13px on their controls; keep the type at the floor and
       take the compactness out of padding and height instead. */
    .filters-row .form-input,
    .filters-row .form-select,
    .page-header + .card.mb-4 select,
    .page-header + .card.mb-4 input {
        font-size: calc(16px / var(--zoom-factor, 1)) !important;
        padding-block: 6px;
    }

    .form-group {
        margin-bottom: 14px;
    }

    .form-label {
        margin-bottom: 6px;
        color: var(--color-text-secondary);
    }

    .form-input:focus,
    .form-select:focus {
        outline: 2px solid color-mix(in srgb, var(--color-primary) 55%, transparent);
        outline-offset: 1px;
    }

    /* Field grids collapse to one column. components.css only does this below
       600px, so between 600 and 768 a modal still shows two columns of AED
       inputs — unusable on any phone in landscape.
       The gap is kept: .form-group's own margin does not apply to grid items,
       so zeroing it would run the labels straight into the field above. */
    .form-row,
    .form-grid {
        display: grid !important;
        grid-template-columns: 1fr !important;
        gap: var(--spacing-3) !important;
    }
}

/* =================================================================
   11 · TOASTS — above the tab bar, never under the thumb
   ================================================================= */
@media (max-width: 768px) {

    .toast-container,
    #toastContainer {
        left: 10px;
        right: 10px;
        bottom: calc(var(--tabbar-space, 0px) + var(--safe-b) + 10px);
        top: auto;
        align-items: stretch;
    }

    .toast {
        width: 100%;
        border-radius: var(--r-ctl);
        background: var(--mat-thick-bg);
        -webkit-backdrop-filter: var(--mat-regular-blur);
        backdrop-filter: var(--mat-regular-blur);
        border: 1px solid var(--mat-edge);
        box-shadow: var(--lift-3);
    }
}

/* =================================================================
   12 · (reserved)
   -----------------------------------------------------------------
   A pull-to-refresh indicator lived here and was removed with its
   gesture. See the note in mobile-ui.js section 13: there is no safe
   re-render entry point to call, so the control was cut rather than
   shipped as a convincing shell. No dead selectors left behind.
   ================================================================= */

/* =================================================================
   13 · ACCESSIBILITY — three independent user signals
   -----------------------------------------------------------------
   Reduced motion does not mean no feedback. It means a gentler,
   non-vestibular equivalent: cross-fades instead of slides, no
   overshoot, no parallax. Comprehension cues stay.
   ================================================================= */
@media (prefers-reduced-motion: reduce) {

    /* .sidebar is deliberately NOT in this list.
     *
     * For everything else here the transform is decoration and nulling it
     * removes an unwanted slide. For the drawer it is POSITION: the closed
     * state IS translateX(-100%), and the panel is moved by an inline
     * transform the spring writes. `transform: none !important` overrode
     * both, so with Reduce Motion switched on the drawer sat at left:0 -
     * permanently visible, and impossible to close by any means, because
     * every transform written to it was being discarded.
     *
     * The preference is still honoured, in the place that can honour it
     * without breaking the component: mobile-ui.js jumps the drawer to its
     * target instead of springing when reduced() is true, so there is no
     * slide - it is simply open or shut.
     */
    .modal-overlay > *,
    .mobile-tabbar,
    .app.sheet-open > .main-content {
        transition: opacity 180ms ease !important;
        transform: none !important;
    }

    /* The drawer keeps its transform - that is its POSITION - and loses only
       the animation between the two states. */
    .sidebar,
    .app.sheet-open > .sidebar.open,
    .sidebar-backdrop {
        transition: none !important;
    }

    .btn.is-pressed,
    .chip.is-pressed,
    .header__toggle.is-pressed,
    .mobile-tabbar__item.is-pressed,
    .card.is-tappable.is-pressed,
    .lead-mcard.is-pressed,
    .sidebar__link.is-pressed,
    tr.is-pressed {
        /* Keep the feedback, drop the movement. */
        transform: none;
        opacity: 0.72;
    }

    .header,
    .modal-header {
        transition: none;
    }
}

@media (prefers-reduced-transparency: reduce) {

    .header,
    .header.is-scrolled,
    .sidebar,
    .mobile-tabbar,
    .modal-footer,
    .toast,
    .modal-overlay > * {
        -webkit-backdrop-filter: none !important;
        backdrop-filter: none !important;
    }

    .header,
    .header.is-scrolled { background: var(--color-bg-sidebar); }
    .sidebar,
    .mobile-tabbar,
    .modal-footer,
    .toast { background: var(--color-bg-card); }

    .sidebar-backdrop {
        -webkit-backdrop-filter: none !important;
        backdrop-filter: none !important;
        background: rgba(0, 0, 0, 0.68);
    }
}

@media (prefers-contrast: more) {

    .header,
    .sidebar,
    .mobile-tabbar,
    .modal-overlay > *,
    .card,
    .toast {
        background: var(--color-bg-card);
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
        border: 1px solid var(--color-text-secondary);
    }

    .mobile-tabbar__item.is-active {
        outline: 2px solid var(--color-primary);
        outline-offset: -2px;
    }

    .text-muted,
    .form-hint,
    .page-subtitle {
        color: var(--color-text-secondary);
    }
}

/* =================================================================
   14 · CORRECTIONS to rules that were never reaching anything
   -----------------------------------------------------------------
   main.css lines ~1352-1382 style `.modal-content`. Nothing in the app
   renders that class — every modal is `.modal-overlay > .modal` — so
   those bottom-sheet rules have never applied on any phone. Section 06
   above replaces them against the real markup.

   They also carried `max-height: 92vh` inside a fixed overlay, which is
   the exact defect the handbook records four rounds of debugging on.
   Section 06 uses a percentage, and mobile-ui.js overwrites it with a
   measured pixel value.
   ================================================================= */
@media (max-width: 768px) {
    .modal-content {
        /* Neutralised: if this class is ever introduced, it inherits the
           sheet language above rather than a second, conflicting one. */
        position: static !important;
        max-height: none !important;
        animation: none !important;
    }
}

/* =================================================================
   15 · COEXISTENCE — the fixed furniture that was already here
   -----------------------------------------------------------------
   A bottom tab bar is the first thing this app has ever put on the
   bottom edge, so every element that already claimed that edge has to
   be lifted. These are not cosmetic: each one currently renders ON TOP
   of the tab bar and swallows its taps.
   ================================================================= */
@media (max-width: 768px) {

    /* The bulk-action bar is appended to <body> by six list pages with an
       identical inline `position:fixed; bottom:24px; z-index:999`. One id,
       six call sites — lift it once. */
    #bulkActionBar {
        bottom: calc(var(--tabbar-space, 0px) + var(--safe-b) + 12px) !important;
        left: 12px !important;
        right: 12px !important;
        transform: none !important;
        width: auto !important;
        max-width: none !important;
        border-radius: var(--r-ctl);
    }

    /* Developer Dashboard's compare bar claims the same edge. */
    .dev-cmpbar {
        bottom: calc(var(--tabbar-space, 0px) + var(--safe-b) + 12px) !important;
    }

    /* Leads hides its view toggle below 768px, but its phone card renderer
       only runs in TABLE mode — so anyone whose saved viewMode is 'grid' is
       stranded on the desktop grid with no control to get back. Give the
       toggle back rather than leaving a dead end. */
    .view-toggle {
        display: inline-flex !important;
    }

    /* Leads injects <style id="leads-page-styles"> into <head> at render
       time — i.e. after this file — so equal specificity loses. Scope through
       .page to stay above it, matching what main.css already does. */
    .page .lead-mcards .lmc-act,
    .page .lead-mcards .lmc-open {
        min-height: 40px;
        border-radius: var(--r-ctl);
    }
}

/* =================================================================
   16 · PRE-AUTH — nothing navigational may flash on the login screen
   -----------------------------------------------------------------
   There are TWO gates, not one. `html.pre-auth` hides the shell before
   the router has run at all; `.app.auth-mode` hides it once the router
   resolves /login. An element covered by only one of them appears for a
   moment on every cold load.
   ================================================================= */
html.pre-auth .mobile-tabbar,
html.pre-auth .drawer-edge,
.app.auth-mode .mobile-tabbar,
.app.auth-mode .drawer-edge {
    display: none !important;
}


/* =================================================================
   12 - RAILS NEED A MOUSE ESCAPE HATCH
   ================================================================= */

/* The primary action must never be the one off the edge.
 *
 * .page-actions is a scroll rail and the primary button is rendered LAST,
 * after Columns, Save Filters, Load Filters and Bulk Actions. Measured at
 * 375px: "Add Lead" sat 295px past the right edge, "Add Property" 320px. The
 * one button a page exists for was the hardest of the five to reach.
 *
 * Reordering rather than restyling: the rail keeps its behaviour, the DOM keeps
 * its order for anything reading it, and the button that matters is simply
 * first. Desktop is untouched - there is nothing to scroll there.
 */
@media (max-width: 768px) {
    .page-header .page-actions .btn-primary {
        order: -1;
    }
}

/* A rail assumes a thumb. Give a mouse somewhere to go.
 *
 * .filters-row and .page-actions scroll horizontally with the scrollbar
 * deliberately hidden (main.css: scrollbar-width:none and a display:none
 * ::-webkit-scrollbar) - right on a phone, where you swipe and the trailing
 * fade is the affordance.
 *
 * A NARROW DESKTOP WINDOW gets neither: no touch to swipe with, no scrollbar to
 * drag. Measured at 375px the filters rail ran 1158px past the edge, so most of
 * it was simply unreachable with a mouse - which is the "cut off on the right"
 * reported from a resized browser window.
 *
 * pointer:fine is a mouse or trackpad, so phones keep the rail exactly as it
 * was and only the case that CANNOT scroll gets wrapping instead.
 */
@media (max-width: 768px) and (pointer: fine) {
    .filters-row,
    .page-header .page-actions {
        flex-wrap: wrap !important;
        overflow-x: visible;
        -webkit-mask-image: none;
        mask-image: none;
    }
}


/* -----------------------------------------------------------------
   SPECIFICITY MATCHED — the filter console

   main.css escalates the same component to
       .page-header + .card.mb-4 .filters-row
   with flex-wrap:nowrap !important. That is (0,3,1); the plain
   .filters-row above is (0,1,0), so it loses on specificity no matter
   which file loads last - which is why .page-actions wrapped and the
   filter row did not, from the very same rule.

   Matched here rather than escalated further: the same selector, so the
   two are decided by source order, and mobile.css is loaded last on
   purpose. Adding a third, longer selector is what produced this mess.

   The rail itself stays for touch. It is a good phone pattern - Mail
   and Photos both use one - and the trailing fade is its affordance.
   Only the case that CANNOT scroll it, a mouse at phone width, wraps.
   ----------------------------------------------------------------- */
@media (max-width: 768px) and (pointer: fine) {
    .page-header + .card.mb-4 .filters-row,
    .page-header + .card.mb-4 > .card-body > div[style*="display:grid"],
    .page-header + .card.mb-4 > .card-body > div[style*="display:flex"] {
        flex-wrap: wrap !important;
        overflow-x: visible;
        -webkit-mask-image: none;
        mask-image: none;
    }

    /* The stage pills are the same story: a rail nobody can scroll. */
    .lead-stage-pills,
    .sc-chips,
    .prop-search-chips {
        flex-wrap: wrap !important;
        overflow-x: visible;
        -webkit-mask-image: none;
        mask-image: none;
    }

    /* A wrapped rail must not keep a fixed-width search box that forces
       a second scroll axis: 82vw was sized for a single-line rail. */
    .page-header + .card.mb-4 .search-input {
        flex: 1 1 100% !important;
        min-width: 0 !important;
    }
}


/* -----------------------------------------------------------------
   THE FILTER CONSOLE COLLAPSES

   Measured on Leads at 492px: the console was 400px tall and the first
   lead began at 667px - 75% of the screen spent on filters before a
   single record. A list screen should show the list.

   So it collapses to what you actually arrive wanting: the search box,
   and one button that says how many filters are on. Everything else is
   one tap away. This is the Mail / Files pattern, and it is the same
   console on every page that has one, which is the point - one design,
   not one per page.

   Collapsed state is a CLASS on the card, and the rows are hidden with
   CSS rather than removed from the DOM. Pages re-render this console on
   every filter change; anything that restructured the markup would be
   destroyed and have to be rebuilt on a mutation observer, and would
   fight the page for ownership of its own DOM.

   grid-template-rows 0fr -> 1fr animates to the content's real height
   without anyone having to measure it.
   ----------------------------------------------------------------- */
@media (max-width: 768px) {

    .m-filter-toggle {
        display: inline-flex;
        align-items: center;
        gap: 7px;
        flex: 0 0 auto;
        min-height: 40px;
        padding: 0 15px;
        border: 1px solid var(--color-border);
        border-radius: var(--r-chip);
        background: var(--color-bg-input);
        color: var(--color-text);
        font-size: 13.5px;
        font-weight: 600;
        letter-spacing: 0;
        cursor: pointer;
        -webkit-tap-highlight-color: transparent;
    }

    /* Feedback on the press, not on release. */
    .m-filter-toggle:active {
        transform: scale(0.97);
        transition: transform 100ms ease-out;
    }

    .m-filter-toggle[aria-expanded="true"] {
        background: var(--color-primary);
        border-color: var(--color-primary);
        color: #fff;
    }

    /* The count is the whole reason the button can replace the console:
       it answers "is anything filtered?" without expanding it. */
    .m-filter-count {
        min-width: 18px;
        height: 18px;
        padding: 0 5px;
        border-radius: 999px;
        background: var(--color-primary);
        color: #fff;
        font-size: 11px;
        font-weight: 700;
        line-height: 18px;
        text-align: center;
    }
    .m-filter-toggle[aria-expanded="true"] .m-filter-count {
        background: rgba(255, 255, 255, .25);
    }
    .m-filter-count:empty,
    .m-filter-count[data-n="0"] { display: none; }

    /* Collapsed by hiding rows, NOT by wrapping them.
     *
     * A wrapper would need the page's markup restructured, and every page
     * re-renders this console whenever a filter changes - the wrapper
     * would be destroyed and have to be rebuilt from a mutation
     * observer, with the page and this file each thinking they own the
     * DOM. A class plus one injected button survives a re-render because
     * there is nothing structural to lose.
     *
     * The cost is no height animation. Worth it: a filter panel that is
     * reliably correct beats one that slides prettily and sometimes
     * comes back empty.
     */
    .card.m-collapsed .lead-stage-wrap,
    .card.m-collapsed .view-toggle,
    .card.m-collapsed .filters-row > *:not(.search-input):not(.m-filter-toggle) {
        display: none !important;
    }

    /* Search takes the width it is no longer sharing. */
    .card.m-collapsed .filters-row .search-input {
        flex: 1 1 auto !important;
        min-width: 0 !important;
    }

}


/* -----------------------------------------------------------------
   THE CARD IS A NEUTRAL SURFACE; STATUS IS THE ONLY COLOUR

   main.css tints the whole lead card amber - background, row borders,
   row fills, the footer, the icons - because amber is the LEAD entity's
   colour. With a status-coloured spine added, a New lead read as an
   amber card with a teal edge and a teal pill: two colour systems on one
   object, and neither one legible.

   Identity stays amber but shrinks to where identity belongs: the LEAD
   eyebrow and the avatar. Everything structural goes back to the plain
   card surface, so the only colour left carrying meaning is the status.

   Same reasoning as a well-made list row anywhere: the surface is quiet
   so the one thing that changes per row can be seen at a glance.
   ----------------------------------------------------------------- */
@media (max-width: 768px) {

    .page .lead-mcards .lead-mcard {
        background: var(--color-bg-card);
        box-shadow: var(--lift-2);
    }

    .page .lead-mcards .lmc-row {
        border-color: var(--color-border);
        background: var(--color-bg-input);
    }

    /* The contact icons were amber on an amber field - invisible twice
       over. Muted is correct: they label the row, they are not the point
       of it. */
    .page .lead-mcards .lmc-row svg {
        color: var(--color-text-muted);
    }

    .page .lead-mcards .lmc-actions {
        border-top-color: var(--color-border);
        background: transparent;
    }

    /* The spine earns a little more presence now that it is the only
       colour on the card carrying state. */
    .page .lead-mcards .lead-mcard::before {
        width: 4px;
    }
}


/* -----------------------------------------------------------------
   THE DASHBOARD KPI RAIL, ON THE SAME RULE AS EVERY OTHER RAIL

   Dashboard.js injects its own phone treatment into <head> at render
   time, so it lands after this stylesheet and wins at equal
   specificity - hence !important here rather than a longer selector.
   That injected block is one of the per-page mobile designs this file
   is replacing; until it is folded in, this keeps the KPI rail
   behaving like the filter rails instead of being the one exception.

   Measured: 6 cards x 121px = 761px of rail inside a 460px screen, with
   the scrollbar hidden. A thumb can swipe it. A mouse cannot.
   ----------------------------------------------------------------- */
@media (max-width: 768px) and (pointer: fine) {
    .kpi-row {
        display: grid !important;
        grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
        gap: 10px !important;
        overflow-x: visible !important;
        -webkit-mask-image: none;
        mask-image: none;
    }

    /* The cards were sized for a rail; in a grid they take the column. */
    .kpi-row .kpi-card {
        min-width: 0 !important;
        width: auto !important;
        flex: initial !important;
    }
}


/* =================================================================
   17 - CENTRED CARD, NOT A BOTTOM SHEET

   Section 06 above presents every modal as a sheet rising from the
   bottom edge. The owner tried it and does not want it - not for Lead
   Details, not for Edit Lead. This section converts that geometry to a
   centred card, and it is deliberately placed LAST so it wins on source
   order at the same specificity rather than by escalating selectors.

   Section 06 is left intact rather than deleted: everything else it
   provides is still wanted - the measured max-height that avoids the vh
   trap, the scroll edge under the header, overscroll containment, the
   16px form minimum. Only the placement changes.

   The card is capped at 460px so it does not stretch into a letterbox
   on a large phone or a small tablet, and centred in what is left.
   ================================================================= */
@media (max-width: 768px) {

    .modal-overlay {
        align-items: center !important;
        justify-content: center;
        padding: var(--gutter) !important;
    }

    .modal-overlay > * {
        width: 100% !important;
        max-width: 460px !important;
        margin: 0 auto !important;

        /* All four corners: a card floats, so it has four of them.
           Section 06 rounds only the top two, which is what makes a
           sheet read as attached to the bottom edge. */
        border-radius: var(--r-card) !important;
        border: 1px solid var(--color-border) !important;
        box-shadow: var(--lift-3) !important;
    }

    /* The grabber is the affordance for throwing a sheet away. There is
       no gesture to afford on a card, and a handle that does nothing is
       worse than no handle - it promises an interaction that is not
       there. mobile-ui.js also stops wiring the drag; this hides the
       control for any sheet built before that change took effect. */
    .sheet-grabber {
        display: none !important;
    }

    /* Without the grabber the header needs its own top padding back. */
    .modal-overlay > * > .modal-header {
        padding-top: 14px;
    }
}


/* -----------------------------------------------------------------
   NO ENTRANCE ANIMATION ON A PHONE

   The modal should be there when it opens, not arrive. Three separate
   things were animating it, so all three are stopped here rather than
   one of them being fixed and the other two left to surprise someone:

     1. mobile-ui.js sprang the panel up from a panel-height below -
        removed in Sheet.present(), which now jumps straight to rest.
     2. QuickViewModal.js injects
            animation: modalSlideIn 0.3s ease-out forwards !important
        into <head> at render time, so it lands after this file and
        needs !important to be cancelled.
     3. components.css transitions .modal from scale(.9) to scale(1),
        and the overlay fades its opacity over 240-260ms.

   Dismissal is deliberately left alone: the ask was about opening, and
   a modal that fades out as it goes still reads as "this is leaving"
   rather than as a frame drop.
   ----------------------------------------------------------------- */
@media (max-width: 768px) {

    .modal-overlay,
    .modal-overlay.active {
        transition: none !important;
    }

    .modal-overlay > *,
    .modal-overlay > .modal,
    .modal-overlay.active > .modal,
    .quick-view-modal {
        animation: none !important;
        transition: none !important;
        transform: none !important;
    }
}


/* -----------------------------------------------------------------
   NOTHING IN A MODAL PANS OR ZOOMS

   main.css already sets touch-action: pan-x pan-y on html, body, .app
   and the two overlays. It stops at the overlay: the panel and the body
   inside it were left at touch-action:auto, which is what let a pinch
   that started on the form itself be treated as a zoom.

   pan-y only, not pan-x pan-y: there is nothing to pan sideways to
   inside a card that is already the width of the screen, so allowing
   the axis only lets the content be dragged off-centre and left there.

   The rails INSIDE a modal - the tab strip, any chip row - re-enable
   their own axis below, because those genuinely do scroll sideways and
   are the one place horizontal movement means something.
   ----------------------------------------------------------------- */
@media (max-width: 768px) {

    .modal-overlay,
    .quick-view-overlay,
    .modal-overlay > *,
    .quick-view-modal,
    .modal-overlay .modal-body,
    .modal-overlay .modal-header,
    .modal-overlay .modal-footer {
        touch-action: pan-y;
        overscroll-behavior: contain;
    }

    /* The one exception: strips that really are horizontal scrollers. */
    .modal-overlay .modal-header__tabs,
    .modal-overlay .sc-chips,
    .modal-overlay .filters-row {
        touch-action: pan-x;
    }

    /* A card is centred by the overlay; it must never be draggable out of
       that position by a stray gesture. */
    .modal-overlay > * {
        position: relative;
        left: auto !important;
        right: auto !important;
        inset-inline: auto !important;
    }
}
