/* ============================================================================
   Rizzy Doors Quote System - Mobile Styles
   ============================================================================

   Loaded LAST in index.php so these rules win without !important.

   Every rule in this file is scoped inside @media (max-width: 768px) — the same
   breakpoint main.css already uses. Nothing here may affect desktop rendering.
   If you need a rule that applies at all widths, it belongs in main.css.

   See MOBILE-PLAN.md for phase breakdown.
   ============================================================================ */

/* ----------------------------------------------------------------------------
   DESKTOP-SIDE RULES (outside the media query)

   These two exist only so the mobile markup added to index.php is invisible at
   desktop widths. Nothing else in this file may live out here.
   ---------------------------------------------------------------------------- */

/* The wrapper added around .nav-links + .nav-links-right. display:contents
   removes it from layout entirely, so the header renders exactly as it did
   before the wrapper existed — .nav-links stays centred by .nav-container's
   justify-content, and .nav-links-right still positions absolutely against
   .nav-container (an unpositioned display:contents box is not a containing
   block, so the reference element is unchanged). */
.nav-menu {
    display: contents;
}

.nav-toggle,
.nav-backdrop {
    display: none;
}

/* ----------------------------------------------------------------------------
   THE MOBILE CONDITION

   Two clauses, OR'd (a comma in a media query list means OR):

     (max-width: 768px)
         Phones in portrait, matching the breakpoint main.css already used.

     (max-width: 950px) and (max-height: 500px)
         Phones in LANDSCAPE. A width-only breakpoint misses these entirely —
         an iPhone 15 rotated is 852x393, so 852 clears 768 and the whole
         mobile layer switches off on a device that is still very much a phone.

   Width alone cannot express this: phone-landscape widths (667-932) overlap
   iPad-portrait widths (744-834), and an iPad in portrait genuinely wants the
   desktop layout. Height is what separates them — a phone on its side is under
   ~430px tall, an iPad is 1024px+.

   The 950px ceiling on the second clause matters: without it, a short-but-wide
   desktop window (say 1400x450) would match on height alone and flip a real
   desktop into mobile mode.

   assets/js/mobile.js MUST keep this exact condition in sync — the two gates
   there (TableResizer, DraggableModal) have to agree with the CSS about which
   side of the line we are on.
   ---------------------------------------------------------------------------- */

@media (max-width: 768px), (max-width: 950px) and (max-height: 500px) {

    /* ------------------------------------------------------------------------
       PHASE 0 — BASELINE GUARDS
       ------------------------------------------------------------------------ */

    /* Nothing should be able to push the page wider than the viewport. Modules
       that genuinely need horizontal scroll (the door schedule entries table)
       scroll inside their own wrapper, not by widening the document. */
    html,
    body {
        max-width: 100%;
        overflow-x: hidden;
    }

    /* iOS Safari zooms the viewport whenever a focused input renders below 16px.
       The app's inputs are 0.8rem-1rem, so every tap into a field was a zoom.
       16px is the minimum that suppresses it. */
    input,
    select,
    textarea {
        font-size: 16px;
    }

    /* TableResizer is disabled on mobile (see table-resizer.js), which means it
       no longer applies the .widths-applied class through its normal path. It
       still adds the class defensively, but this is the belt-and-braces guard
       so a resizer failure can never leave a table invisible. */
    .table-container,
    .vendors-table-container,
    .customers-table-container,
    .products-table-container,
    .quotes-table-container,
    .takeoffs-table-container,
    .purchase-orders-table-container,
    .archived-orders-table-container {
        opacity: 1;
    }

    /* Resize handles and the column-reset menu are mouse-only affordances with
       no touch equivalent. Hiding them here rather than in JS keeps the resizer
       untouched if the viewport later crosses back over the breakpoint. */
    .resize-handle,
    .header-menu-wrapper {
        display: none;
    }

    /* ------------------------------------------------------------------------
       PHASE 2 — MODALS AS FULL-SCREEN SHEETS

       Desktop modals are fixed-width floating panels centred with a
       translateX(-50%). On a phone the only workable form is a sheet that owns
       the whole viewport. The existing markup is already a flex column of
       header / body / footer with the body set to flex:1 overflow-y:auto, so
       once the content box fills the screen the sticky header and sticky
       action bar come for free — no position:sticky needed.
       ------------------------------------------------------------------------ */

    /* The overlay must clear the site header. On desktop .modal-content starts
       at top:80px precisely so it sits below the nav bar, which means .modal
       (1000) being under .header (1001) never mattered. A full-screen sheet
       starts at top:0, so at 1000 the header paints straight over the sheet's
       own title bar — and its close button with it. */
    .modal {
        z-index: 1002;
    }

    .modal-content {
        position: fixed;
        top: 0;
        left: 0;
        right: auto;
        bottom: auto;
        width: 100%;
        height: 100vh;
        height: 100dvh; /* tracks the collapsing iOS URL bar; ignored if unsupported */
        min-width: 0;   /* desktop forces 600px — wider than the phone itself */
        max-width: none;
        min-height: 0;
        max-height: none;
        transform: none; /* desktop centres with translateX(-50%) */
        margin: 0;
        border-radius: 0;
        display: flex;
        flex-direction: column;
    }

    /* The order detail and door schedule modals are centred flex children of
       their overlay rather than fixed panels, so they need the overlay itself
       stretched. Selectors mirror main.css exactly; mobile.css is the later
       <link> so equal specificity resolves in our favour. */
    .order-detail-modal.modal[style*="display: block"],
    .order-door-schedule-modal.modal[style*="display: block"] {
        align-items: stretch;
        justify-content: stretch;
        padding-top: 0;
    }

    .order-detail-modal-content,
    .order-door-schedule-modal-content {
        width: 100%;
        min-width: 0;   /* door schedule forces 1000px on desktop */
        max-width: none;
        height: 100vh;
        height: 100dvh;
        max-height: none;
        border-radius: 0;
    }

    /* Header: square off the desktop corner radius and clear the notch. */
    .modal-header {
        border-radius: 0;
        padding: 0.75rem 1rem;
        padding-top: calc(0.75rem + env(safe-area-inset-top, 0px));
    }

    /* Desktop lays the header out as a 1fr auto 1fr grid: title | print | close.
       The centre column is auto-sized, so the quote and door-schedule modals —
       which put two full-text output buttons there ("📦 Output Delivery Slip")
       — blow past the viewport and carry the close button off the right edge
       with them.

       On mobile the header becomes two rows: title + close on the first, the
       action buttons spanning the full width of the second. */
    .modal-header {
        grid-template-columns: 1fr auto;
        row-gap: 0.5rem;
    }

    /* .modal-header is a grid and the close button is pinned to a column. A
       grid item defaults to min-width:auto, so a long title (quote and order
       modals interpolate project names) would refuse to shrink and push the
       close button past the right edge. */
    .modal-header h3 {
        font-size: 1rem;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* main.css pins this to column 3, which no longer exists. */
    .modal-header .close {
        grid-column: 2;
        grid-row: 1;
    }

    /* Both wrapper shapes used across the modals, plus print buttons that sit
       as direct children of the header. */
    .modal-header .output-buttons,
    .modal-header .header-actions,
    .modal-header > .btn-print {
        grid-column: 1 / -1;
        grid-row: 2;
        justify-content: flex-start;
        flex-wrap: wrap;
        gap: 0.5rem;
    }

    /* Share the row evenly and allow the labels to wrap — main.css sets
       white-space: nowrap, which would otherwise force overflow. The page-level
       #id-scoped rules only set padding/font-size, so they do not contend with
       any of these properties. */
    .modal-header .output-buttons .btn-print,
    .modal-header > .btn-print {
        flex: 1 1 0;
        min-width: 0;
        min-height: 44px;
        white-space: normal;
    }

    .modal-body,
    .view-modal-body,
    .edit-modal-body,
    .order-detail-modal-body,
    .order-door-schedule-modal-body {
        padding: 1rem;
        -webkit-overflow-scrolling: touch;
        /* Stop scroll momentum from chaining to the page behind the sheet. */
        overscroll-behavior: contain;
    }

    /* Most modal bodies carry a second class alongside .view-modal-body, and
       .data-view / .quote-data-view (main.css:1867) cap it at
       `max-height: calc(100vh - 200px)`. That was sized for a floating desktop
       panel; inside a 100dvh sheet it stops the body ~200px short, so the
       action bar lands short of the bottom with dead space beneath it. Affects
       the PO edit, product, both takeoff, and both quote modals.
       (.customer-data-view carries no such cap.) */
    .data-view,
    .quote-data-view {
        max-height: none;
    }

    /* Action bar pinned to the bottom of the sheet. The extra bottom padding
       clears the iOS home indicator, which otherwise sits over the buttons. */
    .modal-actions,
    .view-modal-actions {
        padding: 0.75rem 1rem;
        padding-bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
        gap: 0.5rem;
        flex-wrap: wrap;
    }

    .modal-actions .btn,
    .view-modal-actions .btn {
        min-height: 44px;
    }

    /* Groups of buttons inside the action bar share the row evenly rather than
       overflowing it. */
    .view-modal-buttons,
    .view-modal-action-buttons {
        display: flex;
        gap: 0.5rem;
        flex: 1 1 auto;
    }

    /* ------------------------------------------------------------------------
       PHASE 2 — TWO-COLUMN LAYOUTS INSIDE MODALS

       SPECIFICITY NOTE: the containers below are declared in <style> blocks
       inside api/pages/*.php. Those are injected into the DOM when the SPA
       loads a page, which places them AFTER this file in document order — so
       at equal specificity they win. The doubled class selectors below exist
       purely to outrank them without resorting to !important. Do not
       "simplify" them back to a single class.
       ------------------------------------------------------------------------ */

    /* Declared in main.css — a single class is enough here. */
    .info-grid-two-columns,
    .order-detail-grid {
        grid-template-columns: 1fr;
        gap: 1rem;
    }

    /* Declared in api/pages/{quotes,orders,purchase-orders}.php <style> blocks */
    .terms-pricing-container.terms-pricing-container,
    .quote-terms-pricing-container.quote-terms-pricing-container,
    .po-terms-pricing-container.po-terms-pricing-container,
    .hardware-groups-grid-view.hardware-groups-grid-view {
        grid-template-columns: 1fr;
        gap: 1rem;
        margin-top: 1rem;
    }

    /* Declared as `.modal .form-row` (0,2,0) in api/pages/quotes.php, so this
       needs three classes to win. */
    .modal .form-row.form-row {
        grid-template-columns: 1fr;
    }

    /* ------------------------------------------------------------------------
       PHASE 1 — APP SHELL: HEADER + NAVIGATION DRAWER
       ------------------------------------------------------------------------ */

    /* The desktop header centres .nav-links with the logo and .nav-links-right
       absolutely positioned on either side. On a phone all three overlap, so
       collapse to a simple two-item bar: logo left, hamburger right. */
    .nav-container {
        justify-content: space-between;
        padding: 0.5rem 1rem;
    }

    .logo {
        position: static;
    }

    .logo img {
        height: 32px;
    }

    /* Hamburger */
    .nav-toggle {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 44px;
        height: 44px;
        padding: 0;
        background: none;
        border: none;
        cursor: pointer;
        z-index: 1061; /* above the drawer, so it stays tappable to close */
        position: relative;
    }

    /* Three bars drawn from one element: the bar itself plus two pseudo bars. */
    .nav-toggle-bars,
    .nav-toggle-bars::before,
    .nav-toggle-bars::after {
        display: block;
        width: 24px;
        height: 2px;
        background: white;
        border-radius: 2px;
        transition: transform 0.25s ease, opacity 0.2s ease;
    }

    .nav-toggle-bars::before,
    .nav-toggle-bars::after {
        content: '';
        position: absolute;
        left: 10px;
    }

    .nav-toggle-bars::before {
        transform: translateY(-7px);
    }

    .nav-toggle-bars::after {
        transform: translateY(7px);
    }

    /* Morph to an X while open */
    body.nav-open .nav-toggle-bars {
        background: transparent;
    }

    body.nav-open .nav-toggle-bars::before {
        transform: translateY(0) rotate(45deg);
    }

    body.nav-open .nav-toggle-bars::after {
        transform: translateY(0) rotate(-45deg);
    }

    /* The drawer itself.

       NOTE ON Z-INDEX: .nav-menu and .nav-backdrop are both children of
       .header, which is position:relative + z-index:1001 and therefore a
       stacking context. The values below only order these two against each
       other; neither can paint above 1001 relative to the rest of the page.
       That is fine — 1001 already clears main content and modals (1000) — but
       it means the backdrop must stay inside .header. Moving it out makes its
       1055 outrank the whole header and it silently covers the drawer,
       intercepting every nav tap. */
    .nav-menu {
        display: flex;
        flex-direction: column;
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        width: min(80vw, 320px);
        background: var(--primary-color);
        box-shadow: var(--box-shadow-lg);
        padding-top: 4rem;
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
        z-index: 1060;
        transform: translateX(100%);
        transition: transform 0.25s ease;
    }

    body.nav-open .nav-menu {
        transform: translateX(0);
    }

    /* Visibility is driven purely by body.nav-open rather than the [hidden]
       attribute: [hidden] is a UA rule and would lose to the display:block
       needed here, leaving an invisible backdrop swallowing every tap. */
    .nav-backdrop {
        display: block;
        position: fixed;
        inset: 0;
        background: rgba(0, 0, 0, 0.45);
        z-index: 1055;
        opacity: 0;
        visibility: hidden;
        pointer-events: none;
        transition: opacity 0.25s ease, visibility 0s linear 0.25s;
    }

    body.nav-open .nav-backdrop {
        opacity: 1;
        visibility: visible;
        pointer-events: auto;
        transition: opacity 0.25s ease, visibility 0s linear 0s;
    }

    /* Lock the page behind the drawer. position:fixed is the only reliable
       scroll lock on iOS; mobile-nav.js preserves and restores scrollY around
       it, since fixing the body otherwise jumps the user to the top. */
    body.nav-open {
        position: fixed;
        width: 100%;
        overflow: hidden;
    }

    /* Undo every desktop positioning rule on the two lists now that they are
       stacked inside the drawer. */
    .nav-menu .nav-links,
    .nav-menu .nav-links-right {
        position: static;
        display: flex;
        flex-direction: column;
        min-width: 0;   /* kills the desktop min-width: 600px */
        width: 100%;
        margin: 0;
        padding: 0;
        gap: 0;
        transform: none;
    }

    .nav-menu .nav-links li,
    .nav-menu .nav-links-right li {
        margin: 0;
        width: 100%;
    }

    /* Settings + Logout get a rule above them to separate utility from modules */
    .nav-menu .nav-links-right {
        margin-top: 0.5rem;
        border-top: 1px solid rgba(255, 255, 255, 0.15);
        padding-top: 0.5rem;
    }

    .nav-menu .nav-link {
        display: flex;
        align-items: center;
        min-height: 48px;
        padding: 0 1.25rem;
        font-size: 1rem;
        border-bottom: none;   /* desktop uses a 2px underline for .active */
        border-left: 3px solid transparent;
    }

    /* The desktop rule nudges Takeoffs away from Dashboard; meaningless stacked. */
    .nav-menu .nav-link[href*="takeoffs"] {
        margin-left: 0;
    }

    .nav-menu .nav-link.active {
        background: rgba(255, 255, 255, 0.12);
        border-left-color: var(--accent-color);
        border-bottom-color: transparent;
    }

    /* ------------------------------------------------------------------------
       PHASE 1 — FILTERS ROW
       ------------------------------------------------------------------------ */

    /* Desktop reserves 300px on the right and absolutely positions the status
       filters into it. Stack the three sections instead. */
    .filters-row {
        flex-direction: column;
        align-items: stretch;
        gap: 0.75rem;
        margin: 1rem 0;
        padding-right: 0;
    }

    .status-filter-section {
        position: static;
        transform: none;
        top: auto;
        right: auto;
    }

    .add-button-section,
    .search-section {
        width: 100%;
    }

    .add-button-section .btn {
        width: 100%;
        min-height: 44px;
    }

    .search-bar {
        width: 100%;
    }

    /* 455px of forced width on a 390px screen — another overflow source. */
    .search-bar input {
        min-width: 0;
        flex: 1 1 auto;
        min-height: 44px;
        border-radius: 22px;
    }

    /* Status chips scroll horizontally rather than wrapping to two ragged rows */
    .status-filter-buttons {
        display: flex;
        gap: 0.5rem;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
        padding-bottom: 2px;
    }

    .status-filter-buttons::-webkit-scrollbar {
        display: none;
    }

    /* ------------------------------------------------------------------------
       PHASE 1 — TAP TARGETS

       Apple and Google both put the minimum comfortable touch target at ~44px.
       Everything below is well under it at desktop sizing.
       ------------------------------------------------------------------------ */

    .status-filter-btn {
        min-height: 44px;
        padding: 0.4rem 1rem;
        flex: 0 0 auto;
        white-space: nowrap;
    }

    .pagination-btn {
        min-height: 44px;
        min-width: 44px;
    }

    .modal-header .close {
        width: 44px;
        height: 44px;
        font-size: 1.75rem;
    }

    /* .btn-small is used for row actions across every module. Keep the compact
       font but give it a real hit area. */
    .btn-small {
        min-height: 44px;
        padding: 0.25rem 0.75rem;
    }

    /* ------------------------------------------------------------------------
       PHASE 3 — LIST TABLES AS CARDS

       Seven list tables share one rule block in main.css: fixed layout, cells
       clamped to max-width:0 with nowrap + ellipsis, and a 125px actions
       column. None of that survives a 390px viewport.

       Rather than render a second card template in JS (two code paths that
       will drift), the table elements are re-declared as blocks and each cell
       draws its own heading from a data-label attribute added to the existing
       <td> templates. One render path, one source of truth.

       A cell with no data-label renders as a plain full-width block — that is
       what the name/heading cells and the colspan empty-state rows rely on.
       ------------------------------------------------------------------------ */

    .products-table,      .products-table tbody,      .products-table tr,      .products-table td,
    .quotes-table,        .quotes-table tbody,        .quotes-table tr,        .quotes-table td,
    .takeoffs-table,      .takeoffs-table tbody,      .takeoffs-table tr,      .takeoffs-table td,
    .customers-table,     .customers-table tbody,     .customers-table tr,     .customers-table td,
    .vendors-table,       .vendors-table tbody,       .vendors-table tr,       .vendors-table td,
    .purchase-orders-table, .purchase-orders-table tbody, .purchase-orders-table tr, .purchase-orders-table td,
    .archived-orders-table, .archived-orders-table tbody, .archived-orders-table tr, .archived-orders-table td {
        display: block;
        width: auto;
    }

    /* Column headers are meaningless once each cell carries its own label. */
    .products-table thead,
    .quotes-table thead,
    .takeoffs-table thead,
    .customers-table thead,
    .vendors-table thead,
    .purchase-orders-table thead,
    .archived-orders-table thead {
        display: none;
    }

    /* Undo the shared list-table geometry from main.css. */
    .products-table,
    .quotes-table,
    .takeoffs-table,
    .customers-table,
    .vendors-table,
    .archived-orders-table {
        border-collapse: collapse;
        border-spacing: 0;
        table-layout: auto;
    }

    /* Each row becomes a card. */
    .products-table tbody tr,
    .quotes-table tbody tr,
    .takeoffs-table tbody tr,
    .customers-table tbody tr,
    .vendors-table tbody tr,
    .purchase-orders-table tbody tr,
    .archived-orders-table tbody tr {
        background: white;
        border: 1px solid var(--border-color);
        border-radius: 8px;
        box-shadow: var(--box-shadow);
        margin-bottom: 0.75rem;
        padding: 0.75rem 1rem;
    }

    /* Hover lift is meaningless on touch and leaves a stuck highlight. */
    .products-table tbody tr:hover,
    .quotes-table tbody tr:hover,
    .takeoffs-table tbody tr:hover,
    .customers-table tbody tr:hover,
    .vendors-table tbody tr:hover,
    .purchase-orders-table tbody tr:hover,
    .archived-orders-table tbody tr:hover {
        background: white;
    }

    /* Label / value row. main.css clamps these cells to max-width:0 with
       nowrap + ellipsis so every desktop row stays one line — all of which has
       to come off before the value can wrap. */
    .products-table tbody td,
    .quotes-table tbody td,
    .takeoffs-table tbody td,
    .customers-table tbody td,
    .vendors-table tbody td,
    .purchase-orders-table tbody td,
    .archived-orders-table tbody td {
        max-width: none;
        min-height: 0;
        padding: 0.3rem 0;
        border-bottom: none;
        font-size: 0.9rem;
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: 1rem;
    }

    /* SPECIFICITY: main.css clamps these cells to one line with
       `.customers-table tbody td:not(.buttons)` — (0,2,2). The rule above is
       only (0,1,2) and would lose, so the unclamping has to be declared at
       matching specificity. mobile.css is the later <link>, so an exact tie
       resolves here. */
    .products-table tbody td:not(.buttons),
    .quotes-table tbody td:not(.buttons),
    .takeoffs-table tbody td:not(.buttons),
    .customers-table tbody td:not(.buttons),
    .vendors-table tbody td:not(.buttons),
    .archived-orders-table tbody td:not(.buttons) {
        white-space: normal;
        overflow: visible;
        text-overflow: clip;
        overflow-wrap: break-word;
        word-wrap: break-word;
    }

    .products-table tbody td::before,
    .quotes-table tbody td::before,
    .takeoffs-table tbody td::before,
    .customers-table tbody td::before,
    .vendors-table tbody td::before,
    .purchase-orders-table tbody td::before,
    .archived-orders-table tbody td::before {
        content: attr(data-label);
        flex: 0 0 auto;
        font-size: 0.75rem;
        font-weight: 600;
        color: #6B7280;
        text-transform: uppercase;
        letter-spacing: 0.03em;
    }

    /* Cells with no data-label (the name heading, empty-state rows) render as
       plain blocks rather than an empty label plus a squeezed value. */
    .products-table tbody td:not([data-label]),
    .quotes-table tbody td:not([data-label]),
    .takeoffs-table tbody td:not([data-label]),
    .customers-table tbody td:not([data-label]),
    .vendors-table tbody td:not([data-label]),
    .purchase-orders-table tbody td:not([data-label]),
    .archived-orders-table tbody td:not([data-label]) {
        display: block;
    }

    .products-table tbody td:not([data-label])::before,
    .quotes-table tbody td:not([data-label])::before,
    .takeoffs-table tbody td:not([data-label])::before,
    .customers-table tbody td:not([data-label])::before,
    .vendors-table tbody td:not([data-label])::before,
    .purchase-orders-table tbody td:not([data-label])::before,
    .archived-orders-table tbody td:not([data-label])::before {
        content: none;
    }

    /* Primary cell — the record name, promoted to a card heading. The first
       cell is the record name in all seven tables, so :first-child generalises
       where the per-module class names do not (the takeoffs template gives its
       name cell no class at all). */
    .products-table tbody td:first-child,
    .quotes-table tbody td:first-child,
    .takeoffs-table tbody td:first-child,
    .customers-table tbody td:first-child,
    .vendors-table tbody td:first-child,
    .purchase-orders-table tbody td:first-child,
    .archived-orders-table tbody td:first-child {
        padding: 0 0 0.5rem 0;
        margin-bottom: 0.4rem;
        border-bottom: 1px solid var(--border-color);
        font-size: 1rem;
        font-weight: 600;
    }

    /* SPECIFICITY: main.css ellipsises these links at (0,2,2) — and the
       products one at (0,3,2). Selectors are copied verbatim from that rule so
       each ties and resolves here on source order. */
    .products-table tbody td.description .product-description-link,
    .quotes-table tbody td .quote-name-link,
    .takeoffs-table tbody td .takeoff-name-link,
    .customers-table tbody td .customer-name-link,
    .vendors-table tbody td .vendor-name-link,
    .purchase-orders-table tbody td .po-number-link,
    .archived-orders-table tbody td .view-order-link {
        font-size: 1rem;
        font-weight: 600;
        white-space: normal;
        overflow: visible;
        text-overflow: clip;
    }

    /* Actions become a full-width button row at the foot of the card. main.css
       locks this column to exactly 125px in two separate rule blocks. */
    .products-table tbody td.buttons,
    .quotes-table tbody td.buttons,
    .takeoffs-table tbody td.buttons,
    .customers-table tbody td.buttons,
    .vendors-table tbody td.buttons,
    .purchase-orders-table tbody td.buttons,
    .archived-orders-table tbody td.buttons {
        display: flex;
        width: auto;
        min-width: 0;
        max-width: none;
        gap: 0.5rem;
        padding: 0.75rem 0 0 0;
        margin-top: 0.5rem;
        border-top: 1px solid var(--border-color);
        white-space: normal;
    }

    .products-table tbody td.buttons::before,
    .quotes-table tbody td.buttons::before,
    .takeoffs-table tbody td.buttons::before,
    .customers-table tbody td.buttons::before,
    .vendors-table tbody td.buttons::before,
    .purchase-orders-table tbody td.buttons::before,
    .archived-orders-table tbody td.buttons::before {
        content: none;
    }

    .products-table tbody td.buttons .btn,
    .quotes-table tbody td.buttons .btn,
    .takeoffs-table tbody td.buttons .btn,
    .customers-table tbody td.buttons .btn,
    .vendors-table tbody td.buttons .btn,
    .purchase-orders-table tbody td.buttons .btn,
    .archived-orders-table tbody td.buttons .btn {
        flex: 1 1 auto;
        min-height: 44px;
    }

    /* The narrow single-glyph actions — destructive "x", the price-confirm
       tick, and the "⋮" overflow menu — keep their compact width instead of
       each claiming an equal third of the row. */
    .products-table tbody td.buttons .btn-delete,
    .quotes-table tbody td.buttons .btn-delete,
    .takeoffs-table tbody td.buttons .btn-delete,
    .customers-table tbody td.buttons .btn-delete,
    .vendors-table tbody td.buttons .btn-delete,
    .purchase-orders-table tbody td.buttons .btn-delete,
    .archived-orders-table tbody td.buttons .btn-delete,
    .products-table tbody td.buttons .btn-confirm-price,
    .products-table tbody td.buttons .btn-more,
    .quotes-table tbody td.buttons .btn-more,
    .takeoffs-table tbody td.buttons .btn-more {
        flex: 0 0 56px;
    }

    /* The "⋮" overflow menu is wrapped in a span, which is the actual flex
       item — the button inside it must not stretch the wrapper. */
    .products-table tbody td.buttons .dropdown-wrapper,
    .quotes-table tbody td.buttons .dropdown-wrapper,
    .takeoffs-table tbody td.buttons .dropdown-wrapper {
        flex: 0 0 56px;
    }

    .products-table tbody td.buttons .dropdown-wrapper .btn,
    .quotes-table tbody td.buttons .dropdown-wrapper .btn,
    .takeoffs-table tbody td.buttons .dropdown-wrapper .btn {
        width: 100%;
    }

    /* Empty / loading states span the full width with no card chrome. */
    .products-table tbody td.no-data,
    .products-table tbody td.loading-state,
    .quotes-table tbody td.no-data,
    .quotes-table tbody td.loading-state,
    .takeoffs-table tbody td.no-data,
    .takeoffs-table tbody td.loading-state,
    .customers-table tbody td.no-data,
    .customers-table tbody td.loading-state,
    .vendors-table tbody td.no-data,
    .vendors-table tbody td.loading-state,
    .purchase-orders-table tbody td.no-data,
    .archived-orders-table tbody td.no-items-found {
        display: block;
        text-align: center;
        padding: 1.5rem 0;
    }

    /* The containers hold the card list, so the desktop card chrome (white
       panel, radius, shadow) moves onto the rows themselves. */
    .products-table-container,
    .quotes-table-container,
    .takeoffs-table-container,
    .customers-table-container,
    .vendors-table-container,
    .archived-orders-table-container {
        background: transparent;
        box-shadow: none;
        border-radius: 0;
        margin-bottom: 1rem;
    }

    /* SPECIFICITY: the purchase-orders table is the one list table not covered
       by the shared groups in main.css — it is styled in the <style> block of
       api/pages/purchase-orders.php, which the SPA injects into the DOM after
       this file. Equal-specificity rules lose to it on source order, so the
       three properties it contends need doubled class selectors. Everything
       else above applies normally because that page never declares `display`. */
    .purchase-orders-table.purchase-orders-table {
        table-layout: auto;
        border-collapse: collapse;
        border-spacing: 0;
    }

    .purchase-orders-table.purchase-orders-table tbody td:not(.buttons) {
        white-space: normal;
        overflow: visible;
        text-overflow: clip;
        word-wrap: break-word;
        overflow-wrap: break-word;
    }

    .purchase-orders-table-container.purchase-orders-table-container {
        background: transparent;
        box-shadow: none;
        border-radius: 0;
        margin-bottom: 1rem;
    }

    /* ------------------------------------------------------------------------
       MODAL FORMS — shared by products, takeoffs, customers, vendors, quotes
       (plan items 22, 23)

       Every edit/view modal is built from .info-row: a flex row with the label
       on a 120px gutter and the value beside it. At 390px that leaves the
       field ~200px. Stack them.
       ------------------------------------------------------------------------ */

    .info-row,
    .pricing-row {
        flex-direction: column;
        align-items: stretch;
        gap: 0.2rem;
        padding: 0.5rem 0;
    }

    /* Pricing rows stay side-by-side — they are label/amount pairs, not inputs,
       and reading them as a column doubles the length of the totals block. */
    .pricing-row {
        flex-direction: row;
        align-items: center;
    }

    .info-row > label,
    .info-row > .info-value {
        width: 100%;
    }

    .info-row > .info-value input,
    .info-row > .info-value select,
    .info-row > .info-value textarea {
        width: 100%;
        min-height: 44px;
    }

    .info-row > .info-value textarea {
        min-height: 72px;
    }

    /* SPECIFICITY: page <style> blocks pin a few of these to desktop widths and
       are injected after this file. `.product-info-section .info-value:has(
       textarea)` is (0,2,1) — :has() takes the specificity of its argument — so
       a doubled class is needed to reach (0,3,1). */
    .product-info-section.product-info-section .info-value:has(textarea) {
        width: 100%;
    }

    /* Date inputs are pinned to 150px in takeoffs.php; fine in isolation but
       they read as broken next to full-width siblings. */
    .takeoff-info-section.takeoff-info-section .info-value input[type="date"] {
        width: 100%;
    }

    /* The $-prefixed money inputs are deliberately narrow (75px). Give them
       enough room to show a five-figure number at 16px type. */
    .info-value .input-with-symbol {
        width: 120px;
    }

    /* The "+ New Customer" button sits inline with the customer <select> in the
       takeoff and quote modals. */
    .customer-field-container {
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: 0.4rem;
    }

    .customer-field-container .btn-add-customer {
        min-height: 44px;
    }

    /* ------------------------------------------------------------------------
       TAKEOFFS — "quotes from this takeoff" popup (plan item 23)

       Its own overlay rather than the shared .modal, so the Phase 2 sheet rules
       do not reach it. Page-injected, hence the doubled classes.
       ------------------------------------------------------------------------ */

    .quotes-modal.quotes-modal[style*="display: flex"] {
        align-items: stretch;
        justify-content: stretch;
    }

    .quotes-modal-content.quotes-modal-content {
        width: 100%;
        max-width: none;
        max-height: none;
        height: 100dvh;
    }

    .quotes-modal-card.quotes-modal-card {
        max-height: none;
        height: 100%;
        border-radius: 0;
        border-left: none;
        border-right: none;
    }

    /* ------------------------------------------------------------------------
       PRODUCTS — URL entry + price check modals (plan item 22)
       ------------------------------------------------------------------------ */

    .url-entry-toolbar,
    .price-checks-toolbar {
        flex-wrap: wrap;
        gap: 0.5rem;
    }

    .url-entry-toolbar .url-vendor-select {
        flex: 1 1 100%;
        min-height: 44px;
    }

    /* ------------------------------------------------------------------------
       PURCHASE ORDER LINE ITEMS  (plan item 25)

       Seven columns totalling 432px of fixed width before the description gets
       any space. Rebuilt as a two-up card per line:

           [handle]                    [x]
           [ description             ]
           Qty  [__]        Unit  [__]
           Price[__]        Amount[__]

       SPECIFICITY: the whole table is styled in the <style> block of
       api/pages/purchase-orders.php, injected after this file. The doubled
       `.po-items-table.po-items-table` reaches (0,2,x), which also outranks the
       (0,1,0) per-column width rules in one go.
       ------------------------------------------------------------------------ */

    .po-items-table.po-items-table,
    .po-items-table.po-items-table tbody {
        display: block;
        width: auto;
    }

    .po-items-table.po-items-table thead {
        display: none;
    }

    .po-items-table.po-items-table tbody tr {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 0.4rem 0.75rem;
        align-items: end;
        padding: 0.75rem;
        margin-bottom: 0.75rem;
        background: white;
        border: 1px solid var(--border-color);
        border-radius: 8px;
    }

    .po-items-table.po-items-table tbody td {
        display: block;
        width: auto;
        padding: 0;
        border-bottom: none;
    }

    .po-items-table.po-items-table tbody td[data-label]::before {
        content: attr(data-label);
        display: block;
        font-size: 0.7rem;
        font-weight: 600;
        color: #6B7280;
        text-transform: uppercase;
        letter-spacing: 0.03em;
        margin-bottom: 2px;
    }

    /* Explicit placement — auto-flow would follow source order (handle, qty,
       unit, desc, price, amount, remove) and put the description third. */
    .po-items-table.po-items-table td.po-col-handle  { grid-row: 1; grid-column: 1; }
    .po-items-table.po-items-table td.po-col-remove  { grid-row: 1; grid-column: 2; justify-self: end; }
    .po-items-table.po-items-table td.po-col-desc    { grid-row: 2; grid-column: 1 / -1; }
    .po-items-table.po-items-table td.po-col-qty     { grid-row: 3; grid-column: 1; }
    .po-items-table.po-items-table td.po-col-unit    { grid-row: 3; grid-column: 2; }
    .po-items-table.po-items-table td.po-col-price   { grid-row: 4; grid-column: 1; }
    .po-items-table.po-items-table td.po-col-amount  { grid-row: 4; grid-column: 2; }

    .po-items-table.po-items-table td.po-col-handle {
        font-size: 20px;
        min-height: 40px;
        display: flex;
        align-items: center;
        text-align: left;
    }

    .po-items-table.po-items-table .po-item-remove {
        width: 36px;
        height: 36px;
        font-size: 18px;
    }

    .po-items-table.po-items-table input,
    .po-items-table.po-items-table textarea {
        min-height: 40px;
    }

    /* Same drag-follow trap as the kanban: a transition on the row would ease
       every inline transform Sortable writes to the follower clone. */
    .po-item-row.po-dragging,
    .po-item-row.sortable-fallback,
    .po-item-row.sortable-drag {
        transition: none;
    }

    /* ------------------------------------------------------------------------
       ORDER MODALS  (plan item 26b)
       ------------------------------------------------------------------------ */

    /* Inline style="max-height: 60vh" on the buyout body caps it well short of
       the sheet, stranding the action bar mid-screen with dead space beneath.
       An inline declaration can only be beaten by !important. */
    #buyout-preview-body {
        max-height: none !important;
    }

    .order-detail-tools-list a,
    .order-detail-tools-list button {
        min-height: 44px;
        display: flex;
        align-items: center;
    }

    /* Read-only line items keep their table shape and scroll inside their own
       container — .items-table-container is already overflow-x: auto, so this
       never widens the page. Converting these to cards needs data-labels in the
       quote templates and lands with plan item 24. */
    .items-table-container {
        -webkit-overflow-scrolling: touch;
    }

    /* ------------------------------------------------------------------------
       QUOTE EDIT MODAL — LINE ITEMS  (plan item 24)

       Seven columns: drag, description, qty, cost, unit price, amount, remove.
       Unlike every other table in the app this one CANNOT fall back to
       horizontal scroll — api/pages/quotes.php sets
       `#quote-edit-modal .items-table-container { overflow: visible }` on
       purpose, so the ProductSelector dropdown can escape the container. Cards
       are the only option here.

           [drag]                        [x]
           [ ProductSelector + notes     ]
           Qty   [__]        Cost   $0.00
           Price [__]        Total  $0.00

       SPECIFICITY: the items table is styled in the quotes.php <style> block,
       injected after this file, so the doubled `.items-data-table` is needed to
       reach (0,2,x). That also outranks the (0,1,0) per-column width rules.
       ------------------------------------------------------------------------ */

    #quote-edit-modal .items-data-table.items-data-table,
    #quote-edit-modal .items-data-table.items-data-table tbody {
        display: block;
        width: auto;
    }

    #quote-edit-modal .items-data-table.items-data-table thead {
        display: none;
    }

    #quote-edit-modal .items-data-table.items-data-table tbody tr.quote-item {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 0.4rem 0.75rem;
        align-items: end;
        padding: 0.75rem;
        margin-bottom: 0.75rem;
        background: white;
        border: 1px solid var(--border-color);
        border-radius: 8px;
    }

    /* Zebra striping reads as broken once rows are separated cards. */
    #quote-edit-modal .items-data-table.items-data-table tbody tr:nth-child(even),
    #quote-edit-modal .items-data-table.items-data-table tbody tr:hover {
        background: white;
    }

    #quote-edit-modal .items-data-table.items-data-table tbody td {
        display: block;
        width: auto;
        padding: 0;
        border-bottom: none;
    }

    /* Explicit placement — auto-flow would follow source order and drop the
       description into the second cell of row 1. */
    #quote-edit-modal .items-data-table.items-data-table td.item-drag-handle { grid-row: 1; grid-column: 1; }
    #quote-edit-modal .items-data-table.items-data-table td.item-actions     { grid-row: 1; grid-column: 2; justify-self: end; }
    #quote-edit-modal .items-data-table.items-data-table td.item-description { grid-row: 2; grid-column: 1 / -1; }
    #quote-edit-modal .items-data-table.items-data-table td.item-quantity    { grid-row: 3; grid-column: 1; }
    #quote-edit-modal .items-data-table.items-data-table td.item-cost        { grid-row: 3; grid-column: 2; }
    #quote-edit-modal .items-data-table.items-data-table td.item-price       { grid-row: 4; grid-column: 1; }
    #quote-edit-modal .items-data-table.items-data-table td.item-total       { grid-row: 4; grid-column: 2; }

    /* Headings for the numeric cells, which lose their column header. */
    #quote-edit-modal .items-data-table.items-data-table td.item-quantity::before { content: 'Qty'; }
    #quote-edit-modal .items-data-table.items-data-table td.item-cost::before     { content: 'Cost'; }
    #quote-edit-modal .items-data-table.items-data-table td.item-price::before    { content: 'Unit Price'; }
    #quote-edit-modal .items-data-table.items-data-table td.item-total::before    { content: 'Amount'; }

    #quote-edit-modal .items-data-table.items-data-table td.item-quantity::before,
    #quote-edit-modal .items-data-table.items-data-table td.item-cost::before,
    #quote-edit-modal .items-data-table.items-data-table td.item-price::before,
    #quote-edit-modal .items-data-table.items-data-table td.item-total::before {
        display: block;
        font-size: 0.7rem;
        font-weight: 600;
        color: #6B7280;
        text-transform: uppercase;
        letter-spacing: 0.03em;
        margin-bottom: 2px;
    }

    #quote-edit-modal .items-data-table.items-data-table td.item-drag-handle .drag-handle {
        display: flex;
        align-items: center;
        min-height: 40px;
        font-size: 20px;
    }

    #quote-edit-modal .items-data-table.items-data-table .btn-remove-item {
        width: 36px;
        height: 36px;
        font-size: 20px;
    }

    #quote-edit-modal .items-data-table.items-data-table input {
        min-height: 40px;
    }

    /* Division banners keep a single row: handle, name, remove. */
    #quote-edit-modal .items-data-table.items-data-table tbody tr.quote-division-row-edit {
        display: grid;
        grid-template-columns: auto 1fr auto;
        align-items: center;
        gap: 0.5rem;
        padding: 0.5rem 0.75rem;
        margin-bottom: 0.75rem;
        border-radius: 6px;
    }

    #quote-edit-modal .items-data-table.items-data-table td.division-edit-cell {
        grid-row: 1;
        grid-column: 2;
    }

    #quote-edit-modal .items-data-table.items-data-table .division-name-input {
        width: 100%;
        min-height: 40px;
    }

    /* The add-item buttons sit under the list. */
    #quote-edit-modal .add-item-section {
        display: flex;
        flex-wrap: wrap;
        gap: 0.5rem;
        margin-top: 0.5rem;
    }

    #quote-edit-modal .add-item-section .btn {
        flex: 1 1 auto;
        min-height: 44px;
    }

    /* ------------------------------------------------------------------------
       HARDWARE GROUP BUILDER  (plan item 24 — lives inside the quote edit modal)

       Includes the custom-items feature merged from main.
       ------------------------------------------------------------------------ */

    .hardware-groups-grid.hardware-groups-grid {
        grid-template-columns: 1fr;
        gap: 0.75rem;
    }

    .hardware-group-card.hardware-group-card {
        min-height: 0;
        padding: 0.75rem;
    }

    /* Same drag-follow trap as the kanban: .hardware-group-card carries
       `transition: all 0.2s ease`, which would ease every inline transform
       Sortable writes to the follower clone and make it oscillate. */
    .hardware-group-card.sortable-fallback,
    .hardware-group-card.sortable-drag {
        transition: none;
    }

    .hw-group-drag-handle {
        min-width: 26px;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        font-size: 18px;
    }

    /* Product rows inside a group: a 6-column grid totalling ~172px of fixed
       track plus the description. Split into two rows instead. */
    .compact-product-row.compact-product-row {
        grid-template-columns: 14px 1fr 20px;
        gap: 0.35rem 0.5rem;
        padding: 0.4rem;
        font-size: 0.85rem;
    }

    /* Qty / cost / price wrap onto a second line spanning the full row. */
    .compact-product-row.compact-product-row > *:nth-child(3),
    .compact-product-row.compact-product-row > *:nth-child(4),
    .compact-product-row.compact-product-row > *:nth-child(5) {
        grid-row: 2;
    }

    .compact-product-row.compact-product-row > *:nth-child(3) { grid-column: 2; }
    .compact-product-row.compact-product-row > *:nth-child(4) { grid-column: 2; }
    .compact-product-row.compact-product-row > *:nth-child(5) { grid-column: 3; }

    /* Custom-item editor: a 42px/1fr/1fr/auto grid does not fit at 390px. */
    .custom-item-fields.custom-item-fields {
        grid-template-columns: 1fr 1fr;
        gap: 0.5rem;
    }

    .custom-item-fields.custom-item-fields label {
        font-size: 0.7rem;
    }

    /* 0.75rem inputs trigger the iOS focus zoom; the global 16px rule is
       outranked by the page block's `.custom-item-form input`. */
    .custom-item-form.custom-item-form input {
        font-size: 16px;
        padding: 6px 8px;
        min-height: 40px;
    }

    .custom-item-form.custom-item-form {
        margin: 0.4rem 0 0;
    }

    .custom-item-form-actions.custom-item-form-actions {
        gap: 0.5rem;
    }

    .custom-item-form-actions.custom-item-form-actions .btn-mini {
        flex: 1 1 auto;
        min-height: 40px;
    }

    /* The mini product dropdown inside a group card. */
    .product-dropdown-mini {
        max-height: 260px;
    }

    /* ------------------------------------------------------------------------
       ORDERS KANBAN  (plan item 26)

       main.css already collapses .kanban-board to a single column at this
       breakpoint — one of the only two responsive rules that existed before
       this project. What it does not do is undo the desktop column sizing, so
       four stacked columns produced ~2000px of mostly-empty scroll.

       Vertical reorder within a column is preserved. Cross-column drag stays
       disallowed (group: columnName in orders.js) because a card's column is
       derived from its progress checklist, not from where it is dropped.
       ------------------------------------------------------------------------ */

    .kanban-board {
        gap: 1rem;
    }

    /* 500px per column is a desktop device for keeping four columns visually
       level. Stacked, it is just empty space to scroll past. */
    .kanban-column {
        min-height: 0;
        padding: 0.75rem;
    }

    .column-header {
        position: sticky;
        top: 0;
        z-index: 2;
        margin-bottom: 0.75rem;
    }

    /* The drag handle is the only thing Sortable listens to, so it needs a real
       target — but the target should be tall, not wide. A 44px-wide box centres
       the ~10px "⋮⋮" glyph with ~17px of dead space either side, which on a
       390px card is width stolen from the content for nothing.

       Narrow and left-aligned instead: the height still gives a comfortable
       press area, and the 200ms hold delay in orders.js means there is no
       accidental-activation risk that extra width would guard against. */
    .card-drag-handle {
        min-width: 0;
        width: 26px;
        min-height: 44px;
        justify-content: flex-start;
        padding: 0;
        font-size: 18px;
    }

    /* Tighter on the handle side so the glyph sits near the card edge. */
    .order-card {
        padding: 12px 12px 12px 8px;
    }

    /* Hover lift leaves a stuck transform after a tap on touch devices. */
    .order-card:hover {
        transform: none;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
        border-color: #ddd;
    }

    /* THE DRAG-FOLLOW FIX.

       .order-card carries `transition: all 0.2s ease` (main.css:3643). In
       fallback mode Sortable positions the follower clone by writing an inline
       transform on every touchmove — and that transition animates each write
       over 200ms with easing, so the clone never catches up. It eases toward
       the finger, overshoots, and settles: the "oscillating" card.

       Sortable does clear the transition, but only the INLINE one
       (`css(ghostEl, 'transition', '')`), which cannot defeat a stylesheet
       rule. Two classes beat the one-class .order-card rule, so no !important.

       .sortable-fallback is the follower clone; .sortable-drag is applied to
       whichever element is being dragged in either mode. */
    .order-card.sortable-fallback,
    .order-card.sortable-drag {
        transition: none;
    }

    /* ------------------------------------------------------------------------
       SETTINGS  (plan item 21)
       ------------------------------------------------------------------------ */

    /* The floating Save bar is anchored with
       `right: calc(50vw - 600px + 1rem)`, which assumes a viewport at least
       1200px wide. At 390px that resolves to right: -389px and the bar sits
       entirely off the right edge — settings could not be saved on a phone at
       all. Re-anchor it as a full-width bar across the bottom. */
    .settings-actions {
        left: 0;
        right: 0;
        bottom: 0;
        width: 100%;
        border-radius: 0;
        border-left: none;
        border-right: none;
        justify-content: stretch;
        padding: 0.75rem 1rem;
        padding-bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
    }

    .settings-actions .btn {
        flex: 1 1 auto;
        min-height: 44px;
    }

    /* Clear the now-fixed save bar so the last section is reachable. */
    .settings-section:last-of-type {
        margin-bottom: 6rem;
    }

    /* Quick-nav becomes a horizontal scroll strip rather than a wrapped block
       that pushes the first section below the fold. */
    .settings-nav {
        flex-direction: column;
        align-items: stretch;
        gap: 0.5rem;
        padding: 0.75rem 1rem;
    }

    .settings-nav .quick-nav-links {
        flex-wrap: nowrap;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
        padding-bottom: 2px;
    }

    .settings-nav .quick-nav-links::-webkit-scrollbar {
        display: none;
    }

    .settings-nav .quick-nav-link {
        flex: 0 0 auto;
        min-height: 36px;
        display: flex;
        align-items: center;
        white-space: nowrap;
    }

    /* Label above field instead of a fixed 180px gutter. */
    .settings-row {
        flex-direction: column;
        align-items: stretch;
        gap: 0.25rem;
    }

    .settings-label {
        flex: 0 0 auto;
        padding-top: 0;
    }

    .settings-section {
        padding: 1rem;
    }

    .settings-info-grid {
        grid-template-columns: auto 1fr;
        gap: 0.4rem 0.75rem;
    }

    /* Tax rates: a 1fr auto 160px grid does not fit. Stack, and let the action
       buttons form their own row. */
    .tax-rate-item {
        grid-template-columns: 1fr;
        gap: 0.5rem;
        padding: 0.75rem 1rem;
    }

    .tax-rate-actions {
        justify-content: flex-start;
        flex-wrap: wrap;
    }

    .tax-rate-actions .btn {
        min-height: 40px;
    }

    /* The 55px invisible spacer only exists to align the desktop grid. */
    .button-spacer {
        display: none;
    }

    /* Hardware categories: give the drag handle and remove button real hit
       areas. Touch drag itself is enabled in assets/js/settings.js. */
    .hw-category-item {
        padding: 0.5rem 0.75rem;
        gap: 0.5rem;
    }

    .hw-cat-drag-handle {
        min-width: 32px;
        min-height: 44px;
        display: flex;
        align-items: center;
        font-size: 1.1rem;
    }

    .hw-cat-label {
        min-width: 0;
    }

    .hw-cat-remove {
        min-width: 44px;
        min-height: 44px;
    }

    .hw-categories-add {
        flex-wrap: wrap;
    }

    .hw-categories-add input[type="text"] {
        max-width: none;
        flex: 1 1 100%;
        min-height: 44px;
    }

    .hw-categories-add .btn {
        flex: 1 1 auto;
        min-height: 44px;
    }

    /* ------------------------------------------------------------------------
       DASHBOARD  (plan item 19 — pulled ahead of Phase 2, it is the landing
       page and the cheapest fix in the project)
       ------------------------------------------------------------------------ */

    /* .dashboard-container sits inside .container, which already supplies 1rem
       of side padding. Its own 24px on top of that wasted 40px per side. */
    .dashboard-container {
        padding: 0;
    }

    /* Five quick-create buttons at repeat(5, 1fr) gave each ~62px on a 390px
       screen. Two columns leaves the last button spanning full width. */
    .quick-create-grid {
        grid-template-columns: repeat(2, 1fr);
        gap: 8px;
        margin-bottom: 20px;
    }

    .quick-create-button {
        min-height: 64px;
        padding: 8px;
    }

    /* The 5th button stays a normal half-width cell, leaving the right half of
       the last row empty. Spanning it full-width made it read as a different,
       more important action than the other four. */

    /* Three component columns is unreadable at phone width — stack them. */
    .dashboard-grid {
        grid-template-columns: 1fr;
        gap: 16px;
        margin-bottom: 20px;
    }

    .dashboard-component {
        padding: 16px;
    }

    /* .component-header uses negative margins to bleed its bar to the card
       edge. Those are hardcoded to the desktop 24px padding, so they must be
       re-stated to match the 16px above or the bar overhangs the card. */
    .component-header {
        margin: -16px -16px 12px -16px;
        padding: 12px 16px;
    }

    /* Activity rows: the 20px gap between title block and date badge squeezed
       the (already ellipsised) project name. */
    .activity-item {
        gap: 10px;
    }

    .view-toggle {
        min-height: 44px;
    }

    /* Left at two columns deliberately — each dashboard component is now full
       width, so these inner stat grids have ~360px to work with, which is
       comfortable for short numeric tiles. */
}

/* ============================================================================
   LANDSCAPE PHONES ONLY

   Everything above already applies here (the second clause of the mobile
   condition catches this viewport). This block only makes the trades that flip
   when the phone is on its side: width becomes plentiful — 852px on an iPhone
   15 — and HEIGHT becomes the scarce resource at ~393px.

   All three bounds are required. min-width keeps it off narrow portrait
   phones, max-height keeps it off tablets and desktops, and max-width stops a
   short-but-wide desktop window from matching.
   ============================================================================ */

@media (min-width: 600px) and (max-width: 950px) and (max-height: 500px) {

    /* The two-row header exists because 390px cannot fit title + output
       buttons + close on one line. At 852px it can, and on a 393px-tall screen
       that second row is ~50px of body content given away for nothing. */
    .modal-header {
        grid-template-columns: 1fr auto auto;
        row-gap: 0;
        padding: 0.5rem 1rem;
    }

    .modal-header .output-buttons,
    .modal-header .header-actions,
    .modal-header > .btn-print {
        grid-column: 2;
        grid-row: 1;
        flex-wrap: nowrap;
    }

    .modal-header .close {
        grid-column: 3;
        grid-row: 1;
    }

    /* Back to intrinsic width — stretching them made sense only when they
       owned a full row to themselves. */
    .modal-header .output-buttons .btn-print,
    .modal-header > .btn-print {
        flex: 0 0 auto;
        white-space: nowrap;
    }

    /* Reclaim vertical space in the sheet: with a ~393px viewport, header plus
       action bar can otherwise eat half the available height. */
    .modal-body,
    .view-modal-body,
    .edit-modal-body,
    .order-detail-modal-body,
    .order-door-schedule-modal-body {
        padding: 0.75rem 1rem;
    }

    .modal-actions,
    .view-modal-actions {
        padding: 0.5rem 1rem;
        padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px));
    }

    /* Dashboard: use the width rather than scrolling a single column through a
       393px-tall window. */
    .quick-create-grid {
        grid-template-columns: repeat(5, 1fr);
    }

    .dashboard-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    /* Two cards abreast — halves the scrolling on the shortest viewport we
       support. tbody is already display:block from the shared card rules, so
       it can take grid without disturbing the row/cell structure. */
    .products-table tbody,
    .quotes-table tbody,
    .takeoffs-table tbody,
    .customers-table tbody,
    .vendors-table tbody,
    .purchase-orders-table tbody,
    .archived-orders-table tbody {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 0 0.75rem;
        align-items: start;
    }
}
