/* ── Responsive layer (#779) ─────────────────────────────────────────────────────────────────────────
 *
 * Every small-screen rule for the portal as a whole lives in this one file. _Layout loads it LAST — after
 * each page's own stylesheet — so a rule here beats a page rule of equal specificity without a fight.
 * Page-specific small-screen rules stay with their page; this file is for what every page shares.
 *
 * Built from the #789 audit (all six roles × every page at 390 / 768 / 1024):
 *   §1 header      — every role's header was wider than 1024 (Reseller 1,509px) → every page scrolled
 *                    sideways at tablet and small-laptop widths (#791)
 *   §2 fit shell   — below 768 a viewport-fit page scrolls as a page instead of pinning a list (#790)
 *   §3 tables      — below 768 a data table becomes a stack of labelled cards (#792)
 *   §4 components  — stat tiles, cards, forms, modals (#793)
 *   §5 dashboard   — the 2000px drag canvas becomes one column (#793)
 *
 * Breakpoints: only the four on the #715 scale (480 / 768 / 1024 / 1440), max-width, desktop-first —
 * CssBreakpointScaleTests fails the build on any other value. Desktop above 1440 is untouched.
 * ─────────────────────────────────────────────────────────────────────────────────────────────────── */


/* ── §1 Header: collapse the menu at 1440 (#791) ─────────────────────────────────────────────────────
   _Layout uses Bootstrap's `navbar-expand` (always expanded) and collapses it HERE, because Bootstrap's
   own collapse points (576/768/992/1200/1400) aren't on the #715 scale. Bootstrap's expanded state is
   `display: flex !important` on the collapse, so the hidden state must be !important to beat it.
   `.collapsing` is Bootstrap's mid-animation class — left visible so the open/close still animates. */
header .navbar .navbar-toggler {
    border-color: rgba(255, 255, 255, 0.55);
}
header .navbar .navbar-toggler-icon {
    /* navbar-light draws a dark icon — invisible on the blue gradient. */
    filter: invert(1) brightness(2);
}
/* The ring follows the menu, not focus (owner, 2026-10-01): Bootstrap's focus ring stayed on after a tap until
   you tapped elsewhere, even once the menu was closed again. Highlighted while open; a keyboard user still gets
   a focus ring (focus-visible), a tap or click doesn't. */
header .navbar .navbar-toggler:focus,
header .navbar .navbar-toggler:active {
    box-shadow: none;
    outline: none;
}
header .navbar .navbar-toggler:focus-visible {
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.75);
}
header .navbar .navbar-toggler[aria-expanded="true"] {
    border-color: #ffffff;
    background: rgba(255, 255, 255, 0.12);
}

@media (max-width: 1440px) {
    header .navbar.navbar-expand {
        flex-wrap: wrap;
    }
    header .navbar.navbar-expand > .container-fluid {
        flex-wrap: wrap;
    }
    header .navbar.navbar-expand .navbar-toggler {
        display: block;
        margin-right: 10px;
    }
    header .navbar.navbar-expand .navbar-collapse {
        flex-basis: 100%;
        flex-direction: column;
        align-items: stretch;
    }
    header .navbar.navbar-expand .navbar-collapse:not(.show):not(.collapsing) {
        display: none !important;
    }
    header .navbar.navbar-expand .navbar-collapse.show,
    header .navbar.navbar-expand .navbar-collapse.collapsing {
        display: flex !important;
        padding: 8px 0 12px;
    }
    header .navbar.navbar-expand .navbar-nav {
        flex-direction: column;
        margin-left: 0 !important;
    }
    /* Tap-sized rows, and the active marker moves from an underline to a left bar in a vertical list. */
    header .navbar.navbar-expand .navbar-nav .nav-link {
        padding-top: 10px;
        padding-bottom: 10px;
        min-height: 44px;
    }
    header .navbar.navbar-expand .navbar-nav .nav-link.active {
        border-bottom: none;
        box-shadow: inset 3px 0 0 #60a5fa;
    }
    /* In a collapsed menu a dropdown opens INLINE, pushing the rows below it down. */
    header .navbar.navbar-expand .navbar-nav .dropdown-menu {
        position: static;
        margin: 0 8px 6px;
    }
    header .navbar.navbar-expand .navbar-nav .dropdown-item {
        padding-top: 10px;
        padding-bottom: 10px;
    }
    /* The open menu can be taller than a phone: it scrolls rather than running off the bottom. */
    header .navbar.navbar-expand .navbar-collapse.show {
        max-height: calc(100vh - 80px);
        overflow-y: auto;
    }
    /* The name / Settings / Logout block becomes a profile row at the foot of the open menu (owner,
       2026-10-01): it kept its desktop shape — right-aligned text, avatar far right — and read as a separate
       box. Divider above; avatar first, then name and role left-aligned like the menu items; Settings and
       Logout as two small outlined buttons instead of underlined links split by a bar. */
    header .navbar .nav-user {
        justify-content: flex-start !important;
        flex-wrap: wrap;
        gap: 12px !important;
        padding: 12px 8px 4px !important;
        margin-top: 8px;
        border-top: 1px solid rgba(255, 255, 255, 0.2);
    }
    header .navbar .nav-user-avatar {
        order: -1;
    }
    header .navbar .nav-user .user-avatar {
        width: 42px;
        height: 42px;
    }
    header .navbar .nav-user-text {
        flex: 1 1 auto;
        min-width: 0;
        text-align: left !important;
        display: flex;
        align-items: center;
        justify-content: space-between;
        flex-wrap: wrap;
        gap: 10px;
    }
    header .navbar .nav-user-links {
        margin-top: 0 !important;
        display: flex;
        gap: 8px;
    }
    header .navbar .nav-user-sep {
        display: none;
    }
    header .navbar .nav-user-links form {
        display: inline-flex !important;
    }
    header .navbar .nav-user-links a,
    header .navbar .nav-user-links button {
        text-decoration: none !important;
        border: 1px solid rgba(255, 255, 255, 0.55) !important;
        border-radius: 6px;
        padding: 6px 14px !important;
        font-size: 13px !important;
        line-height: 1.2;
    }
    /* The bell sits beside the hamburger (moved there by site.js), the pair pushed to the right of the logo. */
    header .navbar.navbar-expand .alert-bell--bar {
        margin-left: auto;
        margin-right: 4px;
    }
    header .navbar.navbar-expand .alert-bell--bar .alert-bell-btn {
        font-size: 20px;
        padding: 8px 10px;
    }
}

@media (max-width: 768px) {
    /* Logo, bell and hamburger share one row: the 55px logo with margins on both sides made the trio a few
       pixels wider than a phone, so the hamburger wrapped (owner, 2026-10-01). Logo flush left and a little
       smaller; the header's side padding tightens to match the page's 8px gutter. */
    header .navbar > .container-fluid {
        padding-left: 8px;
        padding-right: 8px;
    }
    header .navbar .navbar-brand {
        margin: 0 !important;
        padding: 4px 0 !important;
    }
    header .navbar .navbar-brand img {
        height: 42px !important;
    }
    header .navbar.navbar-expand .navbar-toggler {
        margin-right: 0;
    }
    header .navbar.navbar-expand .alert-bell--bar .alert-bell-btn {
        padding: 6px 8px;
    }
    /* The alert panel hangs off the bell, which now sits at the LEFT of the open menu — anchored right:0
       it ran off the left edge. Pinned to the viewport instead. */
    .alert-bell-panel {
        position: fixed;
        top: 72px;
        left: 8px;
        right: 8px;
        width: auto;
        max-width: none;
    }
}


/* ── §2 Viewport-fit shell on small screens (#790) ───────────────────────────────────────────────────
   The #180 shell pins a page to the viewport and scrolls a list INSIDE it. On a phone that leaves a sliver
   of list under whatever sits above it (Pallets' filter form took most of the screen). Below 768 the page
   scrolls as a page: every level takes its natural height and nothing clips. The legacy fixed caps on
   table wrappers go too — a 250px scroll box inside a scrolling page is a trap for a thumb. */
@media (max-width: 768px) {
    .pv-fit {
        height: auto !important;
        overflow: visible !important;
    }
    .pv-fit-fill,
    .pv-fit-scroll {
        flex: none !important;
    }
    .pv-fit-scroll {
        overflow-y: visible !important;
    }
    .portal-wrapper {
        height: auto !important;
        overflow: visible !important;
    }
    .portal-table-wrapper,
    .scrollable-table-container,
    .portal-card-body.scrollable-table-container,
    .table-container,
    .hw-table-wrap {
        max-height: none !important;
        overflow: visible !important;
    }
}


/* ── §3 Tables become labelled cards (#792) ──────────────────────────────────────────────────────────
   Three table classes carry the portal's lists: .portal-table, .hw-table (Hardware, Stock) and
   .fulfil-table (Fulfilment). Below 768 each row becomes a card and each cell a "LABEL  value" line; the
   labels are the column headers, copied onto every cell as data-label by labelStackTables() in site.js
   (which also re-labels rows a page renders later). A table where cards make no sense opts out with
   .pv-no-stack and keeps scrolling sideways inside its card. Header sorting is unavailable while stacked:
   the header row is hidden. */
@media (max-width: 768px) {
    table.portal-table:not(.pv-no-stack),
    table.hw-table:not(.pv-no-stack),
    table.fulfil-table:not(.pv-no-stack) {
        display: block;
        width: 100% !important;
        min-width: 0 !important;
        border: none;
    }
    table.portal-table:not(.pv-no-stack) thead,
    table.hw-table:not(.pv-no-stack) thead,
    table.fulfil-table:not(.pv-no-stack) thead {
        display: none;
    }
    table.portal-table:not(.pv-no-stack) tbody,
    table.hw-table:not(.pv-no-stack) tbody,
    table.fulfil-table:not(.pv-no-stack) tbody {
        display: block;
        padding: 8px;
    }
    table.portal-table:not(.pv-no-stack) tr,
    table.hw-table:not(.pv-no-stack) tr,
    table.fulfil-table:not(.pv-no-stack) tr {
        display: block;
        background: #fff;
        border: 1px solid #e2e8f0;
        border-radius: 8px;
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
        margin-bottom: 10px;
        padding: 6px 0;
    }
    table.portal-table:not(.pv-no-stack) td,
    table.hw-table:not(.pv-no-stack) td,
    table.fulfil-table:not(.pv-no-stack) td {
        display: block;
        position: relative;
        width: auto !important;
        min-width: 0 !important;
        max-width: none !important;
        padding: 6px 12px 6px 42%;
        border-bottom: none !important;
        text-align: left !important;
        white-space: normal !important;
        overflow-wrap: anywhere;
        min-height: 30px;
    }
    table.portal-table:not(.pv-no-stack) td::before,
    table.hw-table:not(.pv-no-stack) td::before,
    table.fulfil-table:not(.pv-no-stack) td::before {
        content: attr(data-label);
        position: absolute;
        left: 12px;
        top: 7px;
        width: calc(42% - 20px);
        font-size: 11px;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.2px;
        color: #64748b;
        /* Between words only — `anywhere` split SUBSCRIPTIONS into SUBSCRIPTION / S (owner, 2026-09-29). */
        overflow-wrap: normal;
        word-break: normal;
    }
    /* A cell with no header (checkbox / actions column) and a full-width cell (empty-state "No results"
       row, colspan) take the whole card width with no label. */
    table.portal-table:not(.pv-no-stack) td:not([data-label]),
    table.portal-table:not(.pv-no-stack) td[data-label=""],
    table.portal-table:not(.pv-no-stack) td[colspan],
    table.hw-table:not(.pv-no-stack) td:not([data-label]),
    table.hw-table:not(.pv-no-stack) td[data-label=""],
    table.hw-table:not(.pv-no-stack) td[colspan],
    table.fulfil-table:not(.pv-no-stack) td:not([data-label]),
    table.fulfil-table:not(.pv-no-stack) td[data-label=""],
    table.fulfil-table:not(.pv-no-stack) td[colspan] {
        padding-left: 12px;
    }
    table.portal-table:not(.pv-no-stack) td:not([data-label])::before,
    table.portal-table:not(.pv-no-stack) td[data-label=""]::before,
    table.portal-table:not(.pv-no-stack) td[colspan]::before,
    table.hw-table:not(.pv-no-stack) td:not([data-label])::before,
    table.hw-table:not(.pv-no-stack) td[data-label=""]::before,
    table.hw-table:not(.pv-no-stack) td[colspan]::before,
    table.fulfil-table:not(.pv-no-stack) td:not([data-label])::before,
    table.fulfil-table:not(.pv-no-stack) td[data-label=""]::before,
    table.fulfil-table:not(.pv-no-stack) td[colspan]::before {
        content: none;
    }
    /* A row a filter hid stays hidden — display:block above would otherwise resurrect it. */
    table.portal-table:not(.pv-no-stack) tr[style*="display: none"],
    table.hw-table:not(.pv-no-stack) tr[style*="display: none"],
    table.fulfil-table:not(.pv-no-stack) tr[style*="display: none"],
    table.portal-table:not(.pv-no-stack) tr[hidden],
    table.hw-table:not(.pv-no-stack) tr[hidden],
    table.fulfil-table:not(.pv-no-stack) tr[hidden] {
        display: none;
    }
    table.portal-table:not(.pv-no-stack) tr:hover td {
        background: transparent;
    }
    /* Opted-out tables: scroll sideways inside their own box instead of widening the page. */
    table.pv-no-stack {
        display: block;
        overflow-x: auto;
    }
}


/* ── §4 Components (#793) ────────────────────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
    /* Stat tiles two-up; the page header's title and its buttons wrap instead of squeezing. */
    .stat-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    .portal-page-header {
        align-items: flex-start;
    }
    .portal-card-header {
        flex-wrap: wrap;
    }
    .info-grid,
    .form-row {
        grid-template-columns: 1fr;
    }
    .form-actions,
    .filter-actions {
        flex-wrap: wrap;
    }
    /* 16px stops iOS Safari zooming the page every time a field gets focus. */
    input.form-control,
    select.form-select,
    textarea.form-control,
    .form-group input,
    .form-group select,
    .form-group textarea,
    .list-filter,
    .filter-group input,
    .filter-group select {
        font-size: 16px !important;
    }
    .list-filter {
        flex: 1 1 100%;
    }
    /* Kendo windows and Bootstrap modals never wider than the screen. Kendo centres a window from its
       measured width, so capping max-width is enough to keep it on-screen. */
    .k-window {
        max-width: calc(100vw - 16px) !important;
    }
    .k-window .k-window-content {
        overflow: auto;
    }
    .modal-dialog {
        margin: 8px;
    }
    .modal-footer {
        flex-wrap: wrap;
    }
}

@media (max-width: 480px) {
    .stat-grid {
        grid-template-columns: 1fr;
    }
}


/* ── §5 Dashboard canvas (#793) ──────────────────────────────────────────────────────────────────────
   The dashboard is a 2000×2000px free-form canvas with absolutely-positioned widgets — on a phone the
   second column of tiles was simply off-screen. Below 768 the widgets stack in one column in reading
   order (top-to-bottom, then left-to-right, set as `order` by Dashboard/Index), small tiles two-up.
   Rearranging is off here: the add-widget sidebar and its tab are hidden, drag/resize are disabled, and
   saveCanvasLayout refuses to save while stacked, so a phone can never overwrite the desktop layout. */
@media (max-width: 768px) {
    /* dashboard.css locks the page (`body { overflow: hidden }`) — right for a desktop canvas that scrolls
       inside itself, fatal for a stacked column: on a phone only the first screenful was reachable
       (owner walkthrough, 2026-09-29: "Dashboard doesn't really work at all"). Scoped with :has so no
       other page is touched. */
    body:has(#canvas-layout) {
        overflow: auto !important;
    }
    .demo-section.wide {
        height: auto !important;
        overflow: visible !important;
    }
    #canvas-mode-container.layout-container {
        height: auto;
        overflow: visible;
        box-shadow: none;
        background: transparent;
    }
    #canvas-layout {
        min-width: 0 !important;
        min-height: 0 !important;
        height: auto !important;
        overflow: visible !important;
        display: flex;
        flex-wrap: wrap;
        gap: 12px;
        padding: 0 12px 16px;
        background: transparent !important;
    }
    #canvas-layout .canvas-widget {
        position: relative !important;
        left: auto !important;
        top: auto !important;
        width: 100% !important;
        cursor: default;
        z-index: auto !important;
    }
    /* No scroll areas INSIDE a stacked widget: a finger landing on a table widget scrolled the table, not
       the page, so the page could only be scrolled from the screen's edge (owner walkthrough, 2026-09-29).
       Every widget except a chart grows to its content and the page does all the scrolling. Charts keep
       their height: they size themselves from it at init and never scroll. Kendo grids get their height
       from script, so the override is !important; a wide grid still pans sideways inside its widget
       (overflow-y hidden — a horizontal-only scroller passes vertical swipes through to the page). */
    #canvas-layout .canvas-widget:not(:has(.widget-chart-wrapper)) {
        height: auto !important;
    }
    /* Charts get one height on a phone rather than whatever the desktop canvas saved (some were ~200px,
       which left a sliver of plot under the legend). The chart reads this height when it draws. */
    #canvas-layout .canvas-widget:has(.widget-chart-wrapper) {
        height: 300px !important;
    }
    #canvas-layout .canvas-widget:not(:has(.widget-chart-wrapper)) .widget-root,
    #canvas-layout .canvas-widget .k-card-body {
        height: auto !important;
    }
    #canvas-layout .widget-grid-content {
        flex: none !important;
        max-height: none !important;
        overflow: visible !important;
    }
    /* The license widget is a plain table, not a Kendo grid, so the grid rules below miss it. Same deal:
       full height, sideways pan only, and a width floor so the columns have something to pan across
       (owner, 2026-09-30: it squeezed and wouldn't scroll left/right). */
    #canvas-layout .widget-license-table-wrapper {
        flex: none !important;
        max-height: none !important;
        overflow-x: auto !important;
        overflow-y: hidden !important;
    }
    #canvas-layout .widget-license-table {
        min-width: 560px;
    }
    #canvas-layout .widget-license-table td,
    #canvas-layout .widget-license-table th {
        white-space: nowrap;
    }
    #canvas-layout .k-grid,
    #canvas-layout .k-grid-content {
        height: auto !important;
        max-height: none !important;
    }
    #canvas-layout .k-grid-content {
        overflow-x: auto !important;
        overflow-y: hidden !important;
    }
    /* ...but a Kendo grid without column widths squeezes every column into the widget, so there was
       nothing to pan. A floor on the table width keeps columns readable and gives the sideways swipe
       something to do; Kendo scrolls the header in step with the body. */
    #canvas-layout .k-grid-header table,
    #canvas-layout .k-grid-content table {
        min-width: 560px;
    }
    #canvas-layout .k-grid td,
    #canvas-layout .k-grid th {
        white-space: nowrap;
    }
    /* THE cause of "only scrolls from the screen edge": jQuery UI's stylesheet gives every drag handle
       `touch-action: none`, and a widget's handle is its whole .widget-root — so a finger anywhere on a
       widget could never pan the page. Disabling the draggable doesn't remove the class. Stacked widgets
       don't drag, so they get normal touch behaviour back. */
    #canvas-layout .ui-draggable-handle,
    #canvas-layout .ui-draggable,
    #canvas-layout .ui-resizable {
        touch-action: auto !important;
    }
    #canvas-layout .canvas-widget.cw-small {
        width: calc(50% - 6px) !important;
    }
    /* A count tile two-up is ~170px wide: icon + 32px number + a long uppercase label side by side didn't
       fit, so the icon hung off the side or the text ran out of the grey box (owner walkthrough,
       2026-09-29). Stacked vertically and centred instead; the label wraps between words. The sizes are
       !important because the desktop resize handler scales them inline. */
    #canvas-layout .canvas-widget.cw-small .info-container {
        flex-direction: column;
        justify-content: center;
        gap: 8px;
        padding: 14px 10px;
        text-align: center;
    }
    #canvas-layout .canvas-widget.cw-small .info-holder {
        align-items: center;
        flex: none;
        min-width: 0;
        max-width: 100%;
    }
    #canvas-layout .canvas-widget.cw-small .widget-icon-circle {
        width: 42px !important;
        height: 42px !important;
    }
    #canvas-layout .canvas-widget.cw-small .widget-icon-circle i {
        font-size: 18px !important;
    }
    #canvas-layout .canvas-widget.cw-small .item-values {
        font-size: 26px !important;
    }
    #canvas-layout .canvas-widget.cw-small .text-indicator {
        text-align: center;
        font-size: 11px;
        line-height: 1.3;
        overflow-wrap: break-word;
    }
    #canvas-layout .canvas-widget .ui-resizable-handle,
    #canvas-layout .canvas-widget .k-close-button {
        display: none !important;
    }
    #sidebar-container,
    .sidebar-toggle-btn {
        display: none !important;
    }
}


/* ── §6 Collapsible filters (#794) ───────────────────────────────────────────────────────────────────
   A filter section marked data-pv-collapse-filters gets a "Filters" button from site.js. Below 768 the
   section folds away until the button opens it; above that the button is hidden and nothing changes. */
.pv-filter-toggle {
    display: none;
}

@media (max-width: 768px) {
    .pv-filter-toggle {
        display: flex;
        align-items: center;
        gap: 8px;
        width: 100%;
        min-height: 44px;
        padding: 10px 16px;
        border: none;
        border-bottom: 1px solid #e9ecef;
        background: #f8f9fa;
        color: #1e3c72;
        font-size: 14px;
        font-weight: 600;
        text-align: left;
    }
    .pv-filter-toggle-count {
        margin-left: auto;
        min-width: 22px;
        height: 22px;
        padding: 0 7px;
        border-radius: 11px;
        background: #1e3c72;
        color: #fff;
        font-size: 12px;
        line-height: 22px;
        text-align: center;
    }
    [data-pv-collapse-filters]:not(.pv-open) {
        display: none !important;
    }
    /* Every field full width and allowed to shrink. The open panel ran slightly wider than a phone: an
       Android date field won't go below ~150px, so two of them plus "to" didn't fit one row — the range
       now wraps, one date per line when it has to. */
    [data-pv-collapse-filters] .filter-group input,
    [data-pv-collapse-filters] .filter-group .k-picker,
    [data-pv-collapse-filters] .filter-group .k-dropdownlist {
        width: 100% !important;
        max-width: 100%;
        min-width: 0;
        box-sizing: border-box;
    }
    [data-pv-collapse-filters] .daterange-inputs {
        flex-wrap: wrap;
    }
    [data-pv-collapse-filters] .daterange-inputs input[type="date"] {
        flex: 1 1 140px;
    }
    [data-pv-collapse-filters] .filter-grid {
        grid-template-columns: minmax(0, 1fr);
    }
}


/* ── §7 Walkthrough fixes (owner, 2026-09-29) ────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
    /* Buttons in a stacked card cell: a row of them wraps, and wrapped rows get air between them — they
       sat touching (Account Details users + facilities), and a nowrap flex row pushed Delete off the card
       (Sales Team, when Resend appears). The flex override needs !important to beat inline nowrap. */
    table.portal-table:not(.pv-no-stack) td > .portal-btn,
    table.portal-table:not(.pv-no-stack) td > .btn,
    table.portal-table:not(.pv-no-stack) td > button,
    table.hw-table:not(.pv-no-stack) td > .portal-btn,
    table.hw-table:not(.pv-no-stack) td > button,
    table.fulfil-table:not(.pv-no-stack) td > .portal-btn,
    table.fulfil-table:not(.pv-no-stack) td > button {
        margin: 0 4px 6px 0;
    }
    table.portal-table:not(.pv-no-stack) td > div,
    table.hw-table:not(.pv-no-stack) td > div,
    table.fulfil-table:not(.pv-no-stack) td > div {
        flex-wrap: wrap !important;
        row-gap: 6px;
    }
    /* Account Details (reseller + affiliate) tab row: wraps so the last tab — Billing — isn't off the
       right edge behind a sideways scroll. */
    .account-tabs {
        flex-wrap: wrap;
        row-gap: 2px;
    }
    /* Fewer boxes, more colour (owner, 2026-09-29: "a lot of white"). A stacked list read as card > search
       > grey outline > grey card per row. The inner outline (.scrollable-table-container draws its own
       border) goes; the cards sit on a light slate ground; and each card's first cell — the record's name
       or id — becomes a title bar in the portal's header blue. The body stays white so blue links and
       coloured badges keep their contrast. Phones only: desktop tables are untouched. */
    .scrollable-table-container,
    .portal-card-body.scrollable-table-container {
        border: none !important;
        border-radius: 0 !important;
    }
    table.portal-table:not(.pv-no-stack) tbody,
    table.hw-table:not(.pv-no-stack) tbody,
    table.fulfil-table:not(.pv-no-stack) tbody {
        background: #eef2f7;
        border-radius: 8px;
    }
    table.portal-table:not(.pv-no-stack) tr,
    table.hw-table:not(.pv-no-stack) tr,
    table.fulfil-table:not(.pv-no-stack) tr {
        padding: 0 0 6px;
        overflow: hidden;
        border-color: #d7dee8;
    }
    table.portal-table:not(.pv-no-stack) td:first-child:not([colspan]),
    table.hw-table:not(.pv-no-stack) td:first-child:not([colspan]),
    table.fulfil-table:not(.pv-no-stack) td:first-child:not([colspan]) {
        background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%) !important;
        color: #fff !important;
        font-weight: 700;
        padding: 10px 12px !important;
        margin-bottom: 6px;
    }
    table.portal-table:not(.pv-no-stack) td:first-child:not([colspan])::before,
    table.hw-table:not(.pv-no-stack) td:first-child:not([colspan])::before,
    table.fulfil-table:not(.pv-no-stack) td:first-child:not([colspan])::before {
        content: none;
    }
    /* Text and links go white on the blue bar; anything with its own colours (Rented / Purchased tags and
       other badges, which set a background) keeps them — forcing every descendant white made those tags
       white-on-colour and unreadable (owner, 2026-10-01). */
    table.portal-table:not(.pv-no-stack) td:first-child:not([colspan]) :is(a, strong, b, small, em),
    table.hw-table:not(.pv-no-stack) td:first-child:not([colspan]) :is(a, strong, b, small, em),
    table.fulfil-table:not(.pv-no-stack) td:first-child:not([colspan]) :is(a, strong, b, small, em) {
        color: #fff !important;
    }
    /* Less white between the layers (owner, 2026-09-29, Onboarding: margin > outer card > padding > inner
       card > padding > tinted list > record — five layers of white before any content).
       1. Page side margins 24px → 8px (pages set their own padding inline, hence !important).
       2. A card that only WRAPS other cards (a page shell like Onboarding's) keeps its header — the tabs
          live there — but loses its border, fill and body padding.
       3. A card whose body is just a list loses the body padding, so the tinted ground runs edge to edge
          under the card's blue header; the search row keeps its own inset. */
    .content-wrapper > .container-fluid,
    .content-wrapper > div > .container-fluid,
    .content-wrapper > .container-fluid.pv-fit {
        padding-left: 8px !important;
        padding-right: 8px !important;
        /* Some pages set a large inline padding-bottom (Affiliate Accounts: 100px) — on a phone that was a
           white band between the list and the footer (owner, 2026-10-01). */
        padding-bottom: 12px !important;
    }
    .portal-card:has(.portal-card-body .portal-card) {
        border: none;
        box-shadow: none;
        background: transparent;
        overflow: visible;
    }
    .portal-card:has(.portal-card-body .portal-card) > .portal-card-header {
        border-radius: 10px;
        margin-bottom: 8px;
    }
    .portal-card:has(.portal-card-body .portal-card) > .portal-card-body {
        padding: 0 !important;
    }
    .portal-card-body:has(> table.portal-table:not(.pv-no-stack)),
    .portal-card-body:has(> .scrollable-table-container > table.portal-table:not(.pv-no-stack)),
    .portal-card-body:has(> .portal-table-wrapper > table.portal-table:not(.pv-no-stack)) {
        padding: 0 !important;
    }
    /* ...everything else in that body (search box, "4 of 4") keeps an inset. */
    .portal-card-body:has(> table.portal-table:not(.pv-no-stack)) > :not(table):not(.scrollable-table-container):not(.portal-table-wrapper),
    .portal-card-body:has(> .scrollable-table-container > table.portal-table:not(.pv-no-stack)) > :not(table):not(.scrollable-table-container):not(.portal-table-wrapper),
    .portal-card-body:has(> .portal-table-wrapper > table.portal-table:not(.pv-no-stack)) > :not(table):not(.scrollable-table-container):not(.portal-table-wrapper) {
        margin-left: 8px;
        margin-right: 8px;
    }
    .portal-card-body:has(> table.portal-table:not(.pv-no-stack)) > :first-child:not(table):not(.scrollable-table-container):not(.portal-table-wrapper),
    .portal-card-body:has(> .scrollable-table-container > table.portal-table:not(.pv-no-stack)) > :first-child:not(table):not(.scrollable-table-container):not(.portal-table-wrapper),
    .portal-card-body:has(> .portal-table-wrapper > table.portal-table:not(.pv-no-stack)) > :first-child:not(table):not(.scrollable-table-container):not(.portal-table-wrapper) {
        margin-top: 8px;
    }
    /* Prose in such a body (Auto-Renew's explanation) needs more air than a search box: 8px sat it against
       the card's edge (owner, 2026-10-01). */
    .portal-card-body:has(> table.portal-table:not(.pv-no-stack)) > p,
    .portal-card-body:has(> .scrollable-table-container > table.portal-table:not(.pv-no-stack)) > p,
    .portal-card-body:has(> .portal-table-wrapper > table.portal-table:not(.pv-no-stack)) > p {
        margin: 12px 14px !important;
    }
    .portal-card-body > table.portal-table:not(.pv-no-stack) tbody,
    .portal-card-body > .scrollable-table-container > table.portal-table:not(.pv-no-stack) tbody,
    .portal-card-body > .portal-table-wrapper > table.portal-table:not(.pv-no-stack) tbody {
        border-radius: 0;
    }
    /* Same flush treatment when the list sits in a .pv-fit-scroll wrapper inside a padded body (Sales Team):
       the tinted ground stopped short of the card edges there, unlike Accounts (owner, 2026-10-01). */
    .portal-card-body:has(> .pv-fit-scroll > table.portal-table:not(.pv-no-stack)) {
        padding: 0 !important;
    }
    .portal-card-body:has(> .pv-fit-scroll > table.portal-table:not(.pv-no-stack)) > :not(.pv-fit-scroll) {
        margin-left: 8px;
        margin-right: 8px;
    }
    .portal-card-body:has(> .pv-fit-scroll > table.portal-table:not(.pv-no-stack)) > :first-child:not(.pv-fit-scroll) {
        margin-top: 8px;
    }
    .portal-card-body > .pv-fit-scroll > table.portal-table:not(.pv-no-stack) tbody {
        border-radius: 0;
    }
    /* Sales Team → Assignment Manager: each row put the customer beside a 220px dropdown that may not shrink,
       inside 24px padding, so on a phone the name got about one letter (owner, 2026-10-01). The row wraps:
       name on its own line, dropdown full width beneath. Rows are built by script with inline styles. */
    #manageAssignmentsModal .assignment-row {
        flex-wrap: wrap;
        gap: 8px !important;
        padding: 12px 16px !important;
    }
    #manageAssignmentsModal .assignment-row > div {
        flex: 1 1 100%;
    }
    #manageAssignmentsModal .assignment-row .assignment-select {
        width: 100% !important;
        min-width: 0 !important;
    }
    #manageAssignmentsModal #assignmentsContent > div[style*="position: sticky"] {
        padding: 12px 16px !important;
    }
    /* A totals row (<tfoot>) stacks like the body — it stayed a narrow, unlabelled table row squeezed to the
       left (Reseller Billing, owner 2026-10-01). Its first cell ("Total") takes the blue title bar like any
       card's; cells with nothing in them are dropped rather than shown as a bare label. */
    table.portal-table:not(.pv-no-stack) tfoot,
    table.hw-table:not(.pv-no-stack) tfoot,
    table.fulfil-table:not(.pv-no-stack) tfoot {
        display: block;
        padding: 0 8px 8px;
        background: #eef2f7;
    }
    table.portal-table:not(.pv-no-stack) td:empty,
    table.hw-table:not(.pv-no-stack) td:empty,
    table.fulfil-table:not(.pv-no-stack) td:empty {
        display: none;
    }
    /* Lists a page builds by script as body > <div style="overflow-x:auto"> > table (Affiliate Account
       Details: facilities, machines, licenses, users) get the same flush treatment — the tinted ground sat small
       in the middle with white on every side (owner, 2026-10-01). */
    .portal-card-body:has(> div:only-child > table.portal-table:not(.pv-no-stack)) {
        padding: 0 !important;
    }
    .portal-card-body > div:only-child:has(> table.portal-table:not(.pv-no-stack)) {
        overflow: visible !important;
    }
    .portal-card-body > div:only-child > table.portal-table:not(.pv-no-stack) tbody {
        border-radius: 0;
    }
    /* The shared hardware list (_HardwareTable: Stock, every Hardware page) — search row, then .hw-table-wrap >
       table.hw-table — gets the same flush treatment (owner, 2026-10-01). */
    .portal-card-body:has(> .hw-table-wrap > table.hw-table:not(.pv-no-stack)) {
        padding: 0 !important;
    }
    .portal-card-body:has(> .hw-table-wrap > table.hw-table:not(.pv-no-stack)) > :not(.hw-table-wrap) {
        margin-left: 8px;
        margin-right: 8px;
    }
    .portal-card-body:has(> .hw-table-wrap > table.hw-table:not(.pv-no-stack)) > :first-child:not(.hw-table-wrap) {
        margin-top: 8px;
    }
    .portal-card-body > .hw-table-wrap > table.hw-table:not(.pv-no-stack) tbody {
        border-radius: 0;
    }
    /* General case: the list inside any wrapper <div> in a padded body (Damages' and Claims' containers are the
       latest of several shapes — owner, 2026-10-01). The body loses its padding and the body's other children
       keep an inset. */
    .portal-card-body:has(> div > table.portal-table:not(.pv-no-stack)) {
        padding: 0 !important;
    }
    .portal-card-body:has(> div > table.portal-table:not(.pv-no-stack)) > :not(:has(> table.portal-table)) {
        margin-left: 8px;
        margin-right: 8px;
    }
    .portal-card-body:has(> div > table.portal-table:not(.pv-no-stack)) > :first-child:not(:has(> table.portal-table)) {
        margin-top: 8px;
    }
    .portal-card-body > div > table.portal-table:not(.pv-no-stack) tbody {
        border-radius: 0;
    }
    /* ...and two wrappers deep: Damages/Claims render body > #damagesTableContainer > #damages-grid > table,
       which the one-level rule above didn't reach (owner, 2026-10-01). */
    .portal-card-body:has(> div > div > table.portal-table:not(.pv-no-stack)) {
        padding: 0 !important;
    }
    .portal-card-body:has(> div > div > table.portal-table:not(.pv-no-stack)) > :not(:has(table.portal-table)) {
        margin-left: 8px;
        margin-right: 8px;
    }
    .portal-card-body:has(> div > div > table.portal-table:not(.pv-no-stack)) > :first-child:not(:has(table.portal-table)) {
        margin-top: 8px;
    }
    .portal-card-body > div > div > table.portal-table:not(.pv-no-stack) tbody {
        border-radius: 0;
    }
    /* Tabbed card bodies (.tab-content.portal-card-body: superadmin Billing — Active Subscriptions, Revenue by
       Customer — and every Fulfilment tab): the list sits body > pane > wrapper > table, three levels down, and
       Fulfilment's is a .fulfil-table. The body loses its padding; within each pane everything except the
       list's wrapper keeps an inset; the wrapper stops scrolling on its own (owner, 2026-10-01). */
    .portal-card-body.tab-content:has(table.portal-table:not(.pv-no-stack), table.fulfil-table:not(.pv-no-stack), table.hw-table:not(.pv-no-stack)) {
        padding: 0 !important;
    }
    /* !important: Fulfilment's notes set their margin inline (e.g. margin: -6px 0 12px), which beat a plain
       inset and left "X free to allocate" and the packing note against the border (owner, 2026-10-01). */
    .portal-card-body.tab-content > .tab-pane > :not(:has(table)):not(table) {
        margin-left: 8px !important;
        margin-right: 8px !important;
    }
    .portal-card-body.tab-content > .tab-pane > :first-child:not(:has(table)):not(table) {
        margin-top: 8px;
    }
    .portal-card-body.tab-content > .tab-pane > :has(> table) {
        overflow: visible !important;
        max-height: none !important;
        min-height: 0 !important;
    }
    .portal-card-body.tab-content table.portal-table:not(.pv-no-stack) tbody,
    .portal-card-body.tab-content table.fulfil-table:not(.pv-no-stack) tbody {
        border-radius: 0;
    }
    /* Damages' two lists were capped at calc(100vh - 350px) inline — a box inside a scrolling page. */
    #damagesTableContainer,
    #claimsTableContainer {
        max-height: none !important;
        overflow: visible !important;
    }
}


/* ── §8 Modals on phones (owner audit, 2026-10-01) ─────────────────────────────────────────────────────
   72 Bootstrap modals, most with 24-30px of padding written inline on header, body and footer — on a 390px
   phone that is ~15% of the width spent on air before any field. One shared treatment instead of 72 edits:
   tighter padding, footers that wrap with even gaps (Bootstrap's per-button margins removed), button labels
   allowed to wrap, two-column field rows single-column on the narrowest phones, and any plain table pans
   sideways inside the modal instead of widening it. A modal that needs more (Add License) has its own rules,
   which win by specificity. Inline padding is why these are !important. */
@media (max-width: 768px) {
    .modal-header {
        padding: 14px 16px !important;
    }
    .modal-body {
        padding: 16px !important;
    }
    .modal-footer {
        padding: 12px 16px !important;
        flex-wrap: wrap;
        gap: 8px;
    }
    .modal-footer > * {
        margin: 0 !important;
    }
    .modal-footer .btn,
    .modal-footer .portal-btn {
        white-space: normal !important;
    }
    .modal-body table:not(.portal-table):not(.hw-table):not(.fulfil-table) {
        display: block;
        max-width: 100%;
        overflow-x: auto;
    }
}

@media (max-width: 480px) {
    .modal-body [style*="grid-template-columns: 1fr 1fr"],
    .modal-body [style*="grid-template-columns:1fr 1fr"] {
        grid-template-columns: minmax(0, 1fr) !important;
    }
}
