/* ==========================================================================
   mobile.css — global mobile overrides

   LOAD ORDER IS LOAD-BEARING.
   Must be the LAST stylesheet in <head>, after each page's inline <style>.
   Media query rules get no specificity boost, so a page-level rule beats a
   mobile.css rule at equal specificity if mobile.css is linked first.

   Single breakpoint: 768px. Do not add a ladder.
   ========================================================================== */

@media (max-width: 768px) {

  :root {
    --lsg-bar-h: 52px;   /* bottom tab bar height */
  }

  /* ======================================================================
     CHROME — sidebar rail becomes a bottom tab bar
     ----------------------------------------------------------------------
     The rail costs 56px of a ~360px viewport (15%) and is solid brand red,
     so it dominates visually as well as spatially. A bottom bar is the
     platform convention on both iOS and Android and reclaims the full
     width. Achieved purely in CSS — renderChrome is untouched, because the
     sidebar is already a flex column of icon buttons.
     ====================================================================== */

  html { height: 100dvh; }

  /* Topbar removed on chrome pages. It carried the logo, back/forward
     buttons and search — the device provides its own back/forward, search
     is stripped below, and 32px is not worth a logo when the bottom bar
     already anchors the app. The theme toggle is unaffected: it lives on
     the sidebar contrast icon.

     Gated like the body rule below: the public pages (register, tr-opp,
     warehousing) have their OWN topbar, which is their only branding and
     their only theme toggle. Hiding it there would be wrong. */
  body:has(#lsgSidebar) .topbar { display: none; }

  /* :has() gate — these two properties are only correct on pages that
     actually carry the chrome. Applied unconditionally they would break any
     standalone page (auth, public token pages) whose body is a centred flex
     container: it would lose 52px to a bar that isn't there.

     No padding-bottom: not every page sets box-sizing:border-box globally,
     so reserving the bar's space in `height` is the safe form. */
  body:has(#lsgSidebar) {
    padding-left: 0;
    margin-top: 0;
    height: calc(100dvh - var(--lsg-bar-h));
  }

  .lsg-sidebar {
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    width: auto;
    height: var(--lsg-bar-h);
    flex-direction: row;
    align-items: stretch;
  }
  /* Hairline moves from the right edge to the top edge. */
  .lsg-sidebar::after {
    top: 0;
    right: 0;
    bottom: auto;
    left: 0;
    width: auto;
    height: 0.3px;
  }

  /* The scoop joins a left rail to the topbar. Meaningless here. */
  #lsgCornerScoop { display: none; }

  .lsg-sb-nav {
    flex-direction: row;
    overflow: visible;
    padding: 0;
    gap: 0;
    justify-content: space-around;
    align-items: stretch;
  }
  .lsg-sb-row { flex: 1 1 0; min-width: 0; }
  .lsg-sb-strip { height: 100%; }

  /* Backloads dropped from the bottom bar — one fewer icon means the rest
     get a usable tap target at 360px. Targeted via the strip's href because
     .lsg-sb-row carries no key or data attribute; nav.js is untouched, so
     this reverses by deleting the rule. */
  .lsg-sb-row:has(.lsg-sb-strip[href="backlanes.html"]) { display: none; }

  /* Active indicator: left bar → top bar. */
  .lsg-sb-strip.lsg-sb-active::before {
    left: 6px;
    right: 6px;
    top: 0;
    bottom: auto;
    width: auto;
    height: 3px;
    border-radius: 0 0 2px 2px;
  }

  .lsg-sb-footer {
    flex-direction: row;
    align-items: stretch;
    padding: 0 4px;
  }
  .lsg-sb-footer .lsg-sb-strip { width: 44px; }

  /* ── Flyouts removed ──────────────────────────────────────────────────
     Every bar item is single-tap. Home / Opportunities / Suppliers /
     Site Map / Admin are <a href> strips that navigate directly; the theme
     strip is a <button> with its own handler. The avatar is the one item
     whose action lived inside its flyout, so nav.js attaches a direct
     click handler to it under this breakpoint (see wireUserPopout).

     Consequence: flyout children are unreachable on mobile — Groups,
     New Supplier, Register Form, New Warehousing Opp, New Transport Opp,
     and Available Lanes. All but the last are desktop-only work or have a
     mobile equivalent (quick-add, and the register link now on admin).
     -------------------------------------------------------------------- */
  .lsg-sb-flyout { display: none; }

  /* The avatar keeps its flyout, opened by click rather than hover (nav.js
     toggles .lsg-sb-flyout-open under this breakpoint). !important on the
     offsets because wireFlyouts() writes top/left inline from
     getBoundingClientRect, which for a bottom bar lands off-screen. */
  #lsgSbUserFlyout.lsg-sb-flyout-open {
    display: block;
    opacity: 1;
    pointer-events: auto;
    transform: none;
    left: 8px !important;
    right: 8px !important;
    top: auto !important;
    bottom: calc(var(--lsg-bar-h) + 8px) !important;
    margin-left: 0;
    min-width: 0;
  }
  #lsgSbUserFlyout .lsg-sb-flyout-sub { padding: 12px 14px; }

  #lsg-sb-toast {
    left: 16px;
    right: 16px;
    bottom: calc(var(--lsg-bar-h) + 16px);
    text-align: center;
  }

  /* Page-level toasts are `bottom: 28px`, which puts them behind the 52px
     tab bar — the "Added" confirmation after a quick-add was rendering
     under it and never seen. Gated to chrome pages: public pages have
     their own toasts and no bar. */
  body:has(#lsgSidebar) .toast {
    bottom: calc(var(--lsg-bar-h) + 16px);
    max-width: calc(100vw - 32px);
    white-space: normal;
    text-align: center;
  }

  /* ── Topbar ───────────────────────────────────────────────────────────
     Topbar is hidden wholesale above, so .lsg-tb-search goes with it. Kept
     as an explicit rule in case the topbar is ever restored on mobile: at
     260px fixed width the search box alone overflows the bar.
     -------------------------------------------------------------------- */
  .lsg-tb-search { display: none; }

  /* ======================================================================
     PAGE
     ====================================================================== */
  .page { padding: 20px 12px 24px; }
  .page-title { font-size: 22px; margin-bottom: 12px; }

  /* Per-page filter bars are stripped. The default filter state still
     applies (JS reads the controls, which remain in the DOM) — they are
     hidden, not removed. */
  .filters { display: none; }

  /* Escape hatch for pages where browsing without search is useless —
     a paginated list is unreachable past its first page otherwise.
     Container opts in with .mobile-search-only; the FIRST control in the
     FIRST row survives, everything else goes. Positional rather than a
     second marker class, because the container is the only thing a page
     has to change. */
  .filters.mobile-search-only {
    display: block;
    padding: 8px 10px;
    margin-bottom: 10px;
  }
  .filters.mobile-search-only > * { display: none; }
  .filters.mobile-search-only > .filters-row:first-child { display: block; }
  .filters.mobile-search-only > .filters-row:first-child > .filter-group { display: none; }
  .filters.mobile-search-only > .filters-row:first-child > .filter-group:first-child { display: flex; }
  .filters.mobile-search-only .field-label { margin-bottom: 4px; }

  /* iOS Safari auto-zooms on focus of any control under 16px and does not
     zoom back out. Explicit selectors, not a bare `input, select` — class
     rules like .qa-input would otherwise win on specificity. */
  .qa-input,
  .filter-input,
  .filter-select,
  input[type="text"],
  input[type="email"],
  input[type="tel"],
  input[type="number"],
  input[type="date"],
  select,
  textarea { font-size: 16px; }

  /* ── Results bar ── */
  .results-bar { flex-wrap: wrap; gap: 8px; }
  .results-bar > div { flex: 1 1 100%; }
  .quick-add-btn { flex: 1 1 0; padding: 8px 6px; }

  /* ======================================================================
     OPPORTUNITIES TABLE
     ----------------------------------------------------------------------
     Columns, in order:
       1 #        2 Title    3 Service   4 Client   5 Date
       6 Status   7 Priority 8 Details   9 Notes
     Keeping 2 / 3 / 4 / 6.

     nth-child is safe here ONLY because the quick-add row (which carries
     colspan="4") is lifted out of the table on mobile.
     ====================================================================== */
  .opp-table th:nth-child(1), .opp-table td:nth-child(1),
  .opp-table th:nth-child(5), .opp-table td:nth-child(5),
  .opp-table th:nth-child(7), .opp-table td:nth-child(7),
  .opp-table th:nth-child(8), .opp-table td:nth-child(8),
  .opp-table th:nth-child(9), .opp-table td:nth-child(9) { display: none; }

  /* THE FIX. `display:none` on a cell removes the cell, not the column —
     all nine <col> elements survive, so the table still reserves nine
     columns' worth of width. Hiding the whole colgroup is what actually
     drops them; `visibility:collapse` on <col> is not an option (Chrome
     ignores it) and neither is overriding col widths one by one.
     With the colgroup gone, table-layout:auto sizes from visible cells. */
  .opp-table colgroup { display: none; }
  .opp-table { table-layout: auto; width: 100%; }

  /* min-width is what forced the overflow, not width — .col-name declares
     min-width:280px, which survives any width override. These class rules
     match the <td>/<th> as well as the (now hidden) <col>. */
  .opp-table .col-name,
  .opp-table .col-service,
  .opp-table .col-client,
  .opp-table .col-status {
    min-width: 0;
    max-width: none;
  }
  .opp-table .col-service { width: 74px; }
  .opp-table .col-client  { width: 80px; }
  .opp-table .col-status  { width: 64px; }

  .opp-table thead th { font-size: 10px; padding: 7px 5px; white-space: normal; }
  .opp-table tbody td { padding: 9px 5px; font-size: 12px; vertical-align: top; }

  /* overflow-wrap, not word-break: break only when a word genuinely cannot
     fit, rather than mid-word on every line. */
  .opp-table .col-name,
  .opp-table .col-client,
  .cell-name-link { white-space: normal; overflow-wrap: break-word; }

  .opp-table .svc-badge { font-size: 10px; }

  .opp-table-wrap { overflow-x: hidden; }

  /* ======================================================================
     MODALS (global — .modal / .modal-backdrop are shared markup)
     ----------------------------------------------------------------------
     Desktop centres a 480–580px card with a 20px backdrop inset. At 360px
     that leaves a cramped 320px box floating mid-screen with its footer
     buttons at the top of the thumb's reach. Converted to a bottom sheet:
     full width, pinned to the bottom edge, rounded top corners.

     dvh not vh — a sheet sized in vh is partly under the iOS URL bar.
     ====================================================================== */
  .modal-backdrop {
    padding: 0;
    align-items: flex-end;
  }
  .modal {
    width: 100%;
    max-width: 100%;
    max-height: 92dvh;
    border-radius: 12px 12px 0 0;
    border-bottom: none;
  }
  .modal-header { padding: 16px 16px 12px; }
  .modal-body   { padding: 16px; }
  .modal-footer { padding: 12px 16px; gap: 8px; }
  /* theme.css .btn is inline-flex + align-items:center but sets no
     justify-content. Widening it with flex:1 left-aligned the text node. */
  .modal-footer .btn {
    flex: 1 1 0;
    justify-content: center;
    text-align: center;
    padding: 9px 10px;
    font-size: 0.8rem;
    line-height: 1;
  }

  /* Two-up field rows collapse. */
  .field-row-2 { grid-template-columns: 1fr; }
  /* Explicit rather than relying on the generic input rule above — .field
     input is equal specificity, so this only wins on source order. */
  .field input { font-size: 16px; }

  /* ======================================================================
     SUPPLIER PROFILE
     ----------------------------------------------------------------------
     Mobile flow, in DOM order (no reordering needed — hiding alone yields it):
       Title -> meta -> Services Offered -> Contacts -> Sites -> LSG Internal
     ====================================================================== */
  .page-decoration { display: none; }

  /* Centred stack: title -> group -> [edit, delete]. The pencil is moved
     into #profileHeaderActions by JS; that block is already the second
     child of .profile-header, so it lands below the meta row naturally. */
  .profile-header {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 6px;
    margin-bottom: 16px;
  }
  .profile-header > div { width: 100%; }
  .profile-title { display: block; line-height: 1.1; }
  .profile-meta { justify-content: center; margin-top: 2px; }

  #profileHeaderActions {
    display: flex;
    justify-content: center;
    gap: 10px;
    margin-top: 2px;
  }
  /* The pencil is a 16px glyph in an auto-sized button, the bin a 22px glyph
     in a 36x36 one — normalised so the pair reads as a matched set. */
  .title-edit-icon,
  .supplier-delete-btn {
    width: 30px; height: 30px;
    padding: 0;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .title-edit-icon svg,
  .supplier-delete-btn svg { width: 17px; height: 17px; }

  /* ── Meta row ─────────────────────────────────────────────────────────
     Group only. The items are classed by identity rather than hidden by
     position, because the Group item is omitted entirely for supplier
     logins — nth-child would target a different item per role.
     Restore any of these by deleting it from the list. */
  .profile-meta .verified-badge,
  .profile-meta .meta-contacts,
  .profile-meta .meta-quotes,
  .profile-meta .meta-added { display: none; }

  /* ── Tabs flattened ───────────────────────────────────────────────────
     The Warehousing / Transport / Admin tab bar goes, and every panel is
     forced open so the surviving sections read as one continuous page.
     switchTab() only toggles the .active class, so this override is stable
     — it is not fighting inline styles.

     Sections then hidden individually: everything transport, plus the two
     history/log sections. Note #lsgOnlySection is NOT touched here — its
     display is set by role at line ~2036 and must stay that way, so
     suppliers still never see it.
     -------------------------------------------------------------------- */
  .svc-tab-bar { display: none; }
  .svc-tab-panel { display: block; }

  #panelTransport,
  #regionsSection,
  #trailersSection,
  #capsSection,
  #quoteHistorySection,
  #answerHistorySection,
  #activityLogSection { display: none !important; }

  /* Bulk upload is a desktop/file-picker job. JS sets this button's inline
     display, so !important is required. */
  #bulkUploadSitesBtn { display: none !important; }

  /* The transport service pills at the top of the page stay — they are the
     only transport element retained. */

  /* Four region tiles per row is unreadable at 360px. Kept styled in case
     the section is ever restored on mobile. */
  .region-grid-display { grid-template-columns: repeat(2, 1fr); gap: 6px; }
  .region-tile { padding: 7px 9px; gap: 6px; }

  /* ── Contacts + Sites tables ──────────────────────────────────────────
     contacts: Name | Title/Role | Email | Services | Account | Options
               -> Name + Options only.
     sites:    Name | Address | Postcode | Capabilities | Contacts | Options
               -> Name + Postcode + Options.
     Cells are unclassed in both renderers, so this is nth-child. Safe: the
     only colspan in either is the empty-state row, whose single cell is
     nth-child(1) and therefore survives.

     Sites stays EDITABLE — the Options column is kept precisely so Edit /
     Archive / Delete remain reachable, and the site modal is handled by the
     sheet treatment above.
     -------------------------------------------------------------------- */
  .contacts-table th:nth-child(2), .contacts-table td:nth-child(2),
  .contacts-table th:nth-child(3), .contacts-table td:nth-child(3),
  .contacts-table th:nth-child(4), .contacts-table td:nth-child(4),
  .contacts-table th:nth-child(5), .contacts-table td:nth-child(5),
  .sites-table    th:nth-child(2), .sites-table    td:nth-child(2),
  .sites-table    th:nth-child(4), .sites-table    td:nth-child(4),
  .sites-table    th:nth-child(5), .sites-table    td:nth-child(5) { display: none; }

  /* The initials circle is redundant once the row is just a name. */
  .contact-avatar { display: none; }
  .contact-name-cell { gap: 0; }

  /* "Options" as a header label earns nothing above a ⋯ button. font-size:0
     rather than display:none — the <th> must survive to hold the column. */
  #contactActionsHeader,
  #sitesActionsHeader { font-size: 0; }

  /* theme.css puts a 2px border-right on every th/td. With two columns that
     reads as a divider splitting each row in half; without it the name and
     its menu read as one continuous row. */
  .contacts-table th, .contacts-table td,
  .sites-table th, .sites-table td { border-right: none; }

  /* Add Contact / Add Site sit beside a section title on a 360px row. */
  .section-header .btn { padding: 6px 9px; font-size: 0.72rem; }

  /* The surviving sites headers carry INLINE width/max-width attributes
     (width:200px on Name), which no stylesheet rule can beat without
     !important. Name must be free to take the remaining space. */
  .sites-table th:nth-child(1) {
    width: auto !important;
    max-width: none !important;
  }

  .contacts-table th, .sites-table th { font-size: 10px; padding: 7px 5px; white-space: normal; }
  .contacts-table td, .sites-table td { padding: 9px 5px; font-size: 12px; }
  .contacts-table td { overflow-wrap: break-word; }

  /* ── Row actions menu ─────────────────────────────────────────────────
     Contacts and Sites both render their actions as a three-dot menu on
     mobile only (actionsCellHtml branches on matchMedia), so these styles
     live here rather than in the page — nothing renders them on desktop.
     Mirrors the pattern already in suppliers.html.
     -------------------------------------------------------------------- */
  .actions-menu-btn {
    background: none; border: none; cursor: pointer;
    color: var(--text-secondary);
    font-size: 16px; line-height: 1;
    padding: 4px 6px; border-radius: 4px;
  }
  .actions-menu {
    position: fixed;
    background: var(--surface-2);
    border: 1px solid var(--border-strong);
    border-radius: 5px;
    box-shadow: var(--shadow-md);
    z-index: 10000;
    min-width: 150px;
    overflow: hidden;
    display: none;
  }
  .actions-menu.open { display: block; }
  .actions-menu-item {
    display: block; width: 100%; text-align: left;
    padding: 10px 14px;
    font-family: 'Barlow', sans-serif; font-size: 13px;
    color: var(--text-primary);
    background: none; border: none; cursor: pointer;
  }
  .actions-menu-item:hover { background: var(--surface-3); }
  .actions-menu-item.danger { color: var(--red); }

  /* ======================================================================
     ADMIN — All Users only
     ----------------------------------------------------------------------
     The tab bar and every panel except Users are hidden, along with the
     Invite form. What remains is the user control table plus the register
     link copy button (authored in admin.html as .mobile-only, revealed
     here). Nothing is removed from the desktop page.
     ====================================================================== */
  .tab-nav { display: none; }
  #tab-pending, #tab-enquiries, #tab-submissions, #tab-stats, #tab-settings { display: none !important; }
  #tab-users { display: block !important; }

  /* Invite needs an email, a role and a send — a desktop job. */
  #tab-users .section:has(.invite-form) { display: none; }

  .mobile-only { display: block; }

  /* #adminTitle sits OUTSIDE .page — each tab panel owns its own .page — so
     it reproduces that container's padding itself. Scoped by id: as a bare
     .page-title rule this also hit opportunities and suppliers, whose titles
     ARE inside .page, double-indenting them to 24px. */
  #adminTitle { padding: 20px 12px 0; }

  /* .section-title is 18px against a 22px page title — 4px of separation
     reads as no hierarchy at all on a narrow screen. Scoped to #tab-users
     rather than the bare class, which supplier-profile and register share. */
  #tab-users .section-header { padding: 0 2px 8px; gap: 8px; }
  #tab-users .section-title { font-size: 14px; letter-spacing: 0.05em; }
  #tab-users .section-meta { font-size: 10px; }

  /* .section-body has no padding by default (tables sit flush to the
     border), which left the copy button floating in an over-wide box. */
  #registerLinkSection .section-body { padding: 10px; }
  #copyRegisterLinkBtn { width: 100%; justify-content: center; }

  /* Users table, after the Supplier column was removed for desktop:
     Email | Role | Last Sign In | Invited | actions -> Email | Role | actions.
     Cells are unclassed, so nth-child. */
  #usersTable th:nth-child(3), #usersTable td:nth-child(3),
  #usersTable th:nth-child(4), #usersTable td:nth-child(4) { display: none; }

  #usersTable { table-layout: auto; width: 100%; }
  #usersTable th:nth-child(5) { width: auto; }
  #usersTable thead th { font-size: 10px; padding: 7px 5px; white-space: normal; }
  #usersTable tbody td { padding: 9px 5px; font-size: 12px; overflow-wrap: break-word; }
  .role-select { font-size: 12px; padding: 4px 6px; }

  #userSearch { width: 100%; }

  /* ======================================================================
     SITE MAP
     ----------------------------------------------------------------------
     Layout only. The supercluster config, jitter ring and zoom constants
     are deliberately untouched — they are empirically tuned.

     The page is already the most mobile-ready in the app: full-bleed map,
     floating absolute filter bar, no side panel, no table. Its body is
     `display:flex; flex-direction:column; height:100%` and .page-body is
     `flex:1`, so removing the topbar simply gives the map that height back.
     ====================================================================== */
  /* A map page must not scroll — nav.css sets body{overflow-y:auto}.

     Scoped to `.page-body > #map`, which is site-map.html's structure. A
     bare `body:has(#map)` also matched tr-opp.html, whose route-map tray
     contains its own #map — locking that page's body and making the whole
     opportunity unscrollable. */
  body:has(.page-body > #map) { overflow: hidden; }

  /* Filter bar reduced to the supplier search alone, centred across the top.
     Capabilities, Show archived and the site counter are all gone; the
     Reset view button and Google's zoom cluster are suppressed in
     site-map.html at map-init time rather than hidden here. */
  .filter-bar {
    top: 10px;
    left: 10px;
    right: 10px;
    max-width: none;
    justify-content: center;
    padding: 6px;
    gap: 0;
  }
  .filter-bar .field-label,
  #pillCaps,
  #popCaps,
  #toggleArchived { display: none; }

  .filter-search { width: 100%; }

  .map-counter { display: none; }

  /* ======================================================================
     HOME
     ----------------------------------------------------------------------
     home.html is outside the chrome system — no nav.js, no bottom bar, its
     own 280px sidebar (hidden for LSG roles) and its own theme toggle and
     sign-out. The chrome block above is gated on body:has(#lsgSidebar), so
     none of it reaches this page.

     Cards are the same design, reduced to four single-tap links matching the
     bottom bar. The expandable/popout cards and the dashboard tray are
     dropped in home.html by a matchMedia branch, not hidden here.
     ====================================================================== */
  /* Title and cards move down together as one block. The top padding is
     derived from the same 33dvh anchor: title height (~63px) plus its
     margin (~20px) is subtracted so the FIRST CARD still lands a third down
     the page, with the title sitting immediately above it.
     max() guards short viewports, where the subtraction would go negative. */
  .main { padding: max(20px, calc(33dvh - 84px)) 16px 40px; }
  .layout.no-sidebar .main { padding-left: 16px; }

  /* Desktop lays title and subtitle side by side; mobile stacks them, with
     the subtitle reading as a quiet label under the title rather than as a
     trailing note beside it. */
  .hub-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    margin-bottom: 20px;
  }
  .hub-title { font-size: 42px; }
  .hub-subtitle {
    margin-top: 0;
    font-size: 13px;
    font-weight: 300;
    letter-spacing: 0.04em;
    color: var(--text-tertiary);
  }

  /* Entrance animations off. The nth-child fadeUp rules leave cards at
     opacity:0 until their delay elapses, so both the animation and the
     initial state have to be reset or the grid starts invisible. */
  .hub-header,
  .nav-card,
  .nav-card:nth-child(1),
  .nav-card:nth-child(2),
  .nav-card:nth-child(3),
  .nav-card:nth-child(4),
  .nav-card:nth-child(5) {
    animation: none;
    opacity: 1;
    transform: none;
  }

  .cards-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 12px;
    max-width: none;
  }

  .nav-card {
    min-height: 0;
    /* Removes the grey flash Android/iOS paint over tapped links, which
       would otherwise fight the press effect below. */
    -webkit-tap-highlight-color: transparent;
    transition: transform 0.09s ease-out, box-shadow 0.09s ease-out, background 0.09s ease-out;
  }

  /* Press-and-soften. :active fires on touch-down and releases on lift, so
     the card physically depresses under the finger: it shrinks slightly and
     its shadow collapses, as though pushed toward the page. Kept fast
     (90ms) — anything slower reads as lag rather than as a button. */
  .nav-card.is-link:active {
    transform: scale(0.96);
    background: var(--surface-2);
    box-shadow: 0 1px 2px rgba(0,0,0,0.10);
  }

  /* Hover states are meaningless on touch and stick after a tap. */
  .nav-card:hover { background: var(--surface); border-color: var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.12), 0 1px 3px rgba(0,0,0,0.08); }
  .nav-card.is-link:hover { transform: none; }
  .nav-card.is-link:hover .card-arrow { opacity: 0; transform: translateX(-6px); }

  .card-head { padding: 18px 14px 16px; }
  .card-icon { margin-bottom: 12px; }
  .card-icon svg { width: 26px; height: 26px; }
  .card-label { font-size: 19px; }
  .card-arrow { display: none; }

  /* ======================================================================
     OPPORTUNITY (warehousing detail)
     ----------------------------------------------------------------------
     Mobile scope: title + meta, brief + questions (read and edit),
     responses, and the prospective link. Email Setup and Supplier Shortlist
     are hidden rather than restyled — they are desktop workflows, and
     squeezing them onto a phone invites half-finished sends.
     ====================================================================== */
  /* Email Setup and Supplier Shortlist are desktop workflows — hidden
     rather than restyled, since squeezing them onto a phone invites
     half-finished sends.

     The .content-panel WRAPPER has to go too, not just the section: each
     sits in its own panel, and an emptied panel still paints its grey
     surface and padding. That was the two dead containers above the
     prospective link. */
  #email-setup,
  #supplier-shortlist,
  .content-panel:has(> #email-setup),
  .content-panel:has(> #supplier-shortlist) { display: none !important; }

  .page-decoration { display: none; }

  /* ── Tabs flattened into one scrolling page ──
     Matches supplier-profile. setOppTab() only toggles the .active class,
     so forcing the panels open here is stable — no inline styles to fight.
     .page becomes a flex column purely so `order` can put the prospective
     link last: it lives inside #tab-send, which sits BEFORE #tab-responses
     in source, and no amount of restyling moves it without this. */
  .opp-tabs { display: none; }
  .tab-panel { display: block !important; }

  .page { display: flex; flex-direction: column; }
  .title-section  { order: 1; }
  #tab-brief      { order: 2; }
  #tab-responses  { order: 3; }
  #tab-send       { order: 4; }

  /* ── Title + meta, centred and stacked (as supplier-profile) ── */
  .title-section { margin-bottom: 18px; text-align: center; }
  #titleRead { display: flex; flex-direction: column; align-items: center; gap: 4px; }
  .title-name { display: inline; line-height: 1.1; }
  .title-ext-name { justify-content: center; }
  .title-meta { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }

  /* Status and type badges dropped — the status is evident from the page you
     came from, and the type is in the brief. Client and created date stay. */
  .title-meta .meta-badge[class*="status-"],
  .title-meta .meta-badge.type { display: none; }

  /* Edit + delete are collected into this row by renderMetaRow(); it does
     not exist on desktop. */
  .title-actions-row {
    display: flex;
    justify-content: center;
    gap: 10px;
    margin-top: 8px;
  }
  .title-actions-row .opp-delete-btn,
  .title-actions-row .title-edit-icon {
    position: static;
    width: 30px; height: 30px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .title-actions-row .opp-delete-btn svg,
  .title-actions-row .title-edit-icon svg { width: 16px; height: 16px; }

  /* ── Containers ──
     The grey .content-panel and the white .section-body card both stay —
     only the 3px coloured accent on the card goes, replaced by a plain
     border. Padding is trimmed on both rather than removed, since the
     nesting is what reads as structure.

     NOTE this is a GLOBAL rule inside the media query: .content-panel is
     also used by supplier-profile.html and home.html. */
  .content-panel { padding: 10px 10px 14px; margin-bottom: 14px; }
  .section > .section-body {
    padding: 10px;
    border-left: 1px solid var(--border);
  }
  #questionsSection > .section-body { padding: 8px; }
  .section { margin-bottom: 12px; }

  /* ── Question rows ──
     Desktop is 28px | 1fr | 140px | 28px with 8px gaps — 220px of overhead
     on a ~300px card, leaving the question itself about 80px. The type
     select drops to caret width and the fixed columns tighten, so the text
     gets nearly all the row.

     font-size:0 hides the select's chosen-value text while leaving the UA
     caret drawn — `appearance: none` would remove the caret too. The option
     list gets its size back so the menu is still readable when opened. */
  .question-row {
    grid-template-columns: 20px minmax(0, 1fr) 28px 20px;
    gap: 4px;
    padding: 4px 6px;
  }
  .question-row select.q-type {
    font-size: 0;
    padding: 5px 0;
    box-shadow: none;
    text-align: center;
  }
  .question-row select.q-type option { font-size: 14px; }
  .question-row input.q-text { font-size: 14px; padding: 5px 4px; }
  .question-order { font-size: 11px; }

  /* ── Sections ── */
  /* Actions sit at the right edge of the container rather than butting
     against the title. Only the FIRST button takes the auto margin —
     applying it to every button spaces them apart, which left Rate Analysis
     stranded mid-row with Export CSV pinned to the edge.

     wrap, not nowrap: a long title and a long button together exceed the
     row, and nowrap pushed the button off-screen entirely. Wrapping drops
     it onto its own line, still right-aligned by the auto margin. */
  .section-header { flex-wrap: wrap; gap: 8px; padding: 0 2px 8px; }
  .section-header .section-title { min-width: 0; }
  .section-header .btn { margin-left: auto; flex-shrink: 0; max-width: 100%; }
  .section-header .btn ~ .btn { margin-left: 0; }

  /* "+ Generate prospective link" is longer than the row can ever hold, so
     it drops onto its own line under the header — left-aligned, at its
     natural width, overriding the auto margin the general rule applies.

     Targeted by BUTTON id, not section id: opportunity.html wraps it in
     #prospective-suppliers but tr-opp.html's equivalent section has no id
     at all, so a section-scoped rule silently missed it there. */
  #generateProspectiveLinkBtn,
  #generateTokenBtn {
    flex: 0 0 auto;
    margin-left: 0 !important;
    margin-right: auto;
  }

  /* ── Responses ──
     Per-supplier accordion, not compare. renderResponses() reads wrView,
     which is initialised from the same breakpoint in opportunity.html, so
     hiding the toggle here leaves the right view rendered. */
  .wr-view-toggle { display: none; }

  /* "Warehousing Responses" is redundant on a warehousing opportunity.
     font-size:0 + ::after because the text is a node in the markup, which
     CSS cannot replace any other way. */
  #warehousing-responses > .section-header > .section-title { font-size: 0; letter-spacing: 0; }
  #warehousing-responses > .section-header > .section-title::after {
    content: 'Responses';
    font-family: 'Barlow Condensed', sans-serif;
    font-size: 16px; font-weight: 700;
    letter-spacing: 0.04em; text-transform: uppercase;
  }

  #warehousing-responses > .section-body {
    border-left: 1px solid var(--border);
    padding: 8px;
  }
  /* Rounded corners on the table edges, as supplier-profile. overflow-x:auto
     already establishes a clipping context, so the radius crops the table.

     The left edge moves onto the WRAPPER: the table draws it from
     th:first-child and .wr-type-cell, both of which the Type-column removal
     below hides, so the border vanished with them. On the wrapper it is
     immune to any column being hidden. */
  .wr-compare-wrap,
  .wr-supplier-table-wrap {
    background: transparent;
    border-radius: 6px;
    -webkit-overflow-scrolling: touch;
  }
  .wr-supplier-table-wrap { border-left: 2px solid var(--border); }

  /* Supplier header: the block title, so it stays legible, but the meta and
     profile link stop competing for the row. */
  .wr-supplier-header { flex-wrap: wrap; gap: 6px; padding: 10px; }
  .wr-supplier-name { font-size: 12.5px; font-weight: 600; min-width: 0; }
  .wr-supplier-meta { font-size: 10.5px; }

  /* Submitter attribution under each site heading — noise once the columns
     are this narrow, and it doubles the header height. */
  .wr-site-submitter { display: none; }

  /* theme.css sets white-space:nowrap on .col-header, which makes every
     column as wide as its longest heading. Wrapping them does more to bring
     the width down than any max-width, which cannot take effect until
     wrapping is allowed. */
  .wr-supplier-body .data-table th,
  .wr-supplier-body .data-table .col-header,
  #wrCompareTable th,
  #wrCompareTable .col-header { white-space: normal; }

  .wr-supplier-body .data-table {
    font-size: 11.5px;
    table-layout: fixed;
    width: 100%;
  }
  .wr-supplier-body .data-table th {
    padding: 4px 7px;
    line-height: 1.2;
    vertical-align: bottom;
  }
  .wr-supplier-body .data-table td {
    padding: 6px 7px;
    /* overflow-wrap alone: breaks only when a word genuinely cannot fit.
       word-break: break-word alongside it is more eager and splits ordinary
       words mid-way. */
    overflow-wrap: break-word;
    word-break: normal;
  }
  /* Type column removed. The type cell carries a rowspan and is emitted
     only on the first row of each group, so display:none is safe — rows
     without it are untouched, and the remaining columns close up.
     Widths shift accordingly: Question is now the first visible column. */
  .wr-supplier-body .data-table th:nth-child(1),
  .wr-type-cell { display: none; }
  .wr-supplier-body .data-table th:nth-child(2) { width: 38%; }
  .wr-caveat { font-size: 10.5px; }

  /* ── Prospective links ── */
  .prospective-row { flex-wrap: wrap; gap: 6px; }
  .prospective-row-url { word-break: break-all; min-width: 0; }

  /* ======================================================================
     TR-OPP (transport detail)
     ----------------------------------------------------------------------
     Same treatment as the warehousing detail page: tabs flattened into one
     scrolling list, title centred, Email Setup and Supplier Shortlist
     hidden. Page order: Brief, Project Specifics, Lanes, Quotes,
     Prospective — the first two share #tab-brief with Lanes, and
     Prospective sits in #tab-send, so `order` puts it last.

     Lanes go behind a full-screen sheet, as on tr-opp-public: the lanes
     table carries the opportunity's dynamic column_schema and will never
     fit inline. Quotes stay in the page but drop to Lane / Route / cost
     columns only, still split by supplier exactly as on desktop.
     ====================================================================== */

  /* tr-opp nests every panel inside #lsgView, so .page has only two flex
     children and `order` on the panels does nothing there — the flex
     container has to be the wrapper. (opportunity.html has its panels as
     direct children of .page, hence the two sets of rules.)
     #lsgView carries an inline display:none until init() clears it with
     style.display='', at which point this takes effect.

     #lsgView also needs an explicit order: it defaults to 0, which put the
     whole panel stack ABOVE .title-section (order 1) — the title rendered
     at the bottom of the page. */
  #lsgView { display: flex; flex-direction: column; order: 2; }
  #lsgView #tab-brief  { order: 1; }
  #lsgView #tab-quotes { order: 2; }
  #lsgView #tab-send   { order: 3; }

  /* tr-opp's shortlist is #shortlistSection, NOT #supplier-shortlist — the
     shared rule above only matches the warehousing page's id. */
  #shortlistSection,
  .content-panel:has(> #shortlistSection) { display: none !important; }

  /* ── Project specifics ──
     The desktop grid is four wide and drops the bottom border from the last
     four cells via :nth-last-child(-n+4). At two columns those cells span
     the last TWO rows, so the penultimate row loses its border as well.
     Restore, then remove it only from cells genuinely on the last row: the
     final cell always, and the second-to-last when it sits in the left
     column. Correct for odd and even counts alike. */
  .specs-grid { grid-template-columns: repeat(2, 1fr); }
  /* Read-only on mobile — the spec editor is a full field-by-field form. */
  #specsActions, #specEditor { display: none !important; }
  .spec-cell:nth-child(4n) { border-right: 1px solid var(--border); }
  .spec-cell:nth-child(2n) { border-right: none; }
  .spec-cell:nth-last-child(-n+4) { border-bottom: 1px solid var(--border); }
  .spec-cell:last-child { border-bottom: none; }
  .spec-cell:nth-last-child(2):nth-child(odd) { border-bottom: none; }

  /* Editing lanes is a desktop job — the toolbar and paste-import modal
     have no usable mobile form. */
  #lanesEditToolbar,
  #lanesEditWrap,
  #edPasteOverlay { display: none !important; }
  #lanesSection .section-actions { display: none; }

  /* Read table lives in the sheet; the button takes its place inline. */
  #lanesSection #lanesReadWrap { display: none; }
  /* theme.css sets .data-table to width:100%, so the browser squeezed every
     column to fit rather than letting the wrapper scroll.

     The earlier fix used min-width:max-content, which overcorrected:
     max-content means "as wide as the longest unbroken line", so headers
     stopped wrapping altogether and the custom columns — which carry no
     inline width — grew to fit their full heading on one line.

     table-layout:fixed with a width on EVERY column is the deterministic
     version. Under fixed layout the declared widths are authoritative, the
     table is their sum, headers wrap inside them, and the overflow goes to
     #lanesReadWrap, which scrolls. */
  .lane-sheet #lanesReadTable {
    font-size: 12px;
    table-layout: fixed;
    width: auto;
  }
  .lane-sheet #lanesReadTable th,
  .lane-sheet #lanesReadTable td {
    padding: 6px 8px;
    white-space: normal;
    /* Wrap at spaces only. break-word split headings mid-word — "Deliveries"
       becoming "Deliverie/s" — which is harder to read than a slightly wider
       column. A word longer than its column now overflows rather than
       breaking, so the widths below are set with that in mind. */
    overflow-wrap: normal;
    word-break: normal;
    hyphens: none;
  }
  .lane-sheet #lanesReadTable th .th-clamp { -webkit-line-clamp: 4; }
  .lane-sheet #lanesReadTable td:first-child { text-align: center; }

  /* !important throughout — the authored widths are inline styles.
     Sized to hold the longest single word in a typical heading now that
     mid-word breaking is off. */
  .lane-sheet #lanesReadTable .lr-num-th    { width: 34px !important; }
  .lane-sheet #lanesReadTable .lr-origin-th,
  .lane-sheet #lanesReadTable .lr-dest-th   { width: 92px !important; }
  .lane-sheet #lanesReadTable .lr-desc-th,
  .lane-sheet #lanesReadTable .lr-cap-th    { width: 110px; }
  .lane-sheet #lanesReadTable .lr-cost-th   { width: 92px !important; }

  /* ── Lanes sheet ──
     tr-opp-public.html carries its own copy of these (it is a public page
     and does not load this file). Without them here the sheet rendered as
     ordinary inline content on tr-opp.html and the trigger was an unstyled
     button. */
  .lanes-open-btn {
    display: flex;
    align-items: center; justify-content: center; gap: 8px;
    width: 100%; margin-top: 8px; padding: 12px;
    font-family: 'Barlow Condensed', sans-serif; font-size: 13px;
    font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
    background: var(--red); color: #fff;
    border: 1px solid var(--red); border-radius: 5px; cursor: pointer;
  }
  .lane-sheet {
    display: none;
    position: fixed; inset: 0; z-index: 1200;
    background: var(--black);
    flex-direction: column;
  }
  .lane-sheet.open { display: flex; }
  .lane-sheet-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px; padding: 12px 14px;
    border-bottom: 1px solid var(--border);
    background: var(--surface); flex-shrink: 0;
  }
  .lane-sheet-title {
    font-family: 'Barlow Condensed', sans-serif; font-size: 15px;
    font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
    color: var(--text-primary);
  }
  .lane-sheet-close {
    background: none; border: none; cursor: pointer;
    color: var(--text-secondary); font-size: 26px; line-height: 1;
    padding: 0 4px;
  }
  /* min-height:0 or the flex child refuses to shrink and overflows. */
  .lane-sheet-body {
    flex: 1 1 auto; min-height: 0;
    padding: 10px;
    display: flex;
    overflow: hidden;
  }
  /* The wrap is made the scroller in BOTH axes rather than letting the body
     scroll vertically and the wrap horizontally. Nesting two scroll
     contexts left the table unable to scroll down: overflow-x:auto forces
     the wrap's overflow-y to auto too, but with no height constraint it
     never became scrollable and the parent could not see past it. */
  .lane-sheet #lanesReadWrap {
    display: block;
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    -webkit-overflow-scrolling: touch;
  }

  /* ── Quotes ──
     Lane / Route / cost columns only. Hidden by class rather than
     nth-child: the cost column count comes from column_schema and the
     Submitted-by column is conditional, so positions are not stable. */
  .qt-notes-th,  .qt-notes-td,
  .qt-sub-th,    .qt-sub-td,
  .qt-status-th, .qt-status-td { display: none; }

  #quotesContainer .data-table { font-size: 11.5px; width: 100%; }
  #quotesContainer .data-table th,
  #quotesContainer .data-table td { padding: 6px 7px; white-space: normal; }
  /* !important because the th carries an inline style="width:80px" — without
     it the lane number was the widest column in the table. */
  #quotesContainer .qt-lane-th { width: 28px !important; }
  #quotesContainer .qt-lane-td { text-align: center; }
  /* 72px forced headings like "Curtain Sider - cost per lane" onto four
     lines, so the header block was taller than the rows beneath it. */
  #quotesContainer .qt-cost-th { width: 96px !important; }
  #quotesContainer .qt-route-td { overflow-wrap: break-word; }

  /* Give the table the same treatment as the responses table on
     opportunity.html: inset from the block edge, with its own border and
     rounded corners. .section-body.flush strips the padding for desktop's
     edge-to-edge table, so the inset is reinstated on the supplier body.
     border-collapse:separate is required or the radius is squared off by
     the cell borders. */
  .quotes-supplier-body { padding: 8px; }
  .quotes-supplier-body .data-table {
    border: 1px solid var(--border);
    border-radius: 6px;
    border-collapse: separate;
    border-spacing: 0;
  }
  /* theme.css gives cells border-right and border-bottom only, so the left
     edge has to come from somewhere. The table border alone proved
     unreliable here — drawing it on the first cell of every row is what the
     page itself does for the responses tables, and it survives whatever the
     collapse mode ends up being. */
  .quotes-supplier-body .data-table th:first-child,
  .quotes-supplier-body .data-table td:first-child {
    border-left: 1px solid var(--border);
  }
  .quotes-supplier-body .data-table thead th:first-child { border-top-left-radius: 6px; }
  .quotes-supplier-body .data-table thead th:last-child { border-top-right-radius: 6px; }

  /* Supplier header wraps rather than pushing its action buttons off. */
  .quotes-supplier-header { flex-wrap: wrap; gap: 6px; padding: 10px; }
  .quotes-supplier-name { font-size: 12.5px; font-weight: 600; min-width: 0; }
  .quotes-supplier-contact,
  .quotes-supplier-meta { font-size: 10.5px; }
  .quotes-supplier-submitter { display: none; }

  /* Accept / Reject / Reset removed. They sit inside the header, which is
     also the expand/collapse target, so any of them is a mis-tap away from
     a quote decision — and those are desktop decisions. */
  .quotes-supplier-actions { display: none; }

  /* The route map tray is a desktop affordance. */
  .map-tray-tab, .map-tray, .map-tray-backdrop { display: none !important; }

  /* ======================================================================
     DESKTOP-ONLY PAGES
     ----------------------------------------------------------------------
     new-opportunity, new-tr-opp, new-supplier, select-suppliers, backlanes,
     groups. These are multi-step builders and wide pickers; making them
     usable at 360px is a large job for workflows nobody runs from a phone.
     Left to render as-is they look broken rather than unsupported, so they
     get an explicit notice instead.

     Scoped with :has() so it only fires on pages carrying the notice — an
     unscoped `body > *` rule here would blank every page in the app,
     including the bottom bar. Hiding siblings rather than wrapping the
     page's markup means none of these six files needed structural edits.
     Base styling and the desktop `display: none` live in theme.css. */
  body:has(.desktop-only-notice) > * { display: none !important; }
  body:has(.desktop-only-notice) > .desktop-only-notice { display: flex !important; }

  /* ── Quick-add panel ──────────────────────────────────────────────────
     Mobile-only. On desktop buildQuickAddRow still returns a <tr> and
     #qaPanelHost stays empty (zero height, no layout cost).
     -------------------------------------------------------------------- */
  #qaPanelHost:empty { display: none; }

  .qa-panel {
    display: block;
    background: rgba(122, 161, 212, 0.06);
    border: 1px solid var(--border);
    border-left: 3px solid #7aa1d4;
    border-radius: 6px;
    padding: 12px;
    margin: 0 0 14px 0;
  }
  .qa-panel-head { margin-bottom: 10px; }
  .qa-panel-field { margin-bottom: 10px; }
  .qa-panel-field .field-label { margin-bottom: 4px; }

  /* .qa-actions is flex-direction:column on desktop (narrow notes cell). */
  .qa-panel .qa-actions { flex-direction: row; gap: 8px; }
  .qa-panel .qa-btn { flex: 1 1 0; padding: 10px; font-size: 11px; }

  /* ======================================================================
     SUPPLIERS TABLE
     ----------------------------------------------------------------------
     Every cell here carries a class, so hiding is by class rather than
     nth-child — immune to any future colspan. There is also no <colgroup>,
     so the phantom-column problem that bit opportunities.html doesn't
     arise: display:none on th+td genuinely removes the column.

     Keeping Company / Status / Service / Actions. Contacts, Addresses,
     Sites, Quotes and Last Activity are all available one tap away on the
     supplier profile. Actions stays because Quick Edit is the fastest
     route to "change supplier details" — its modal is already
     width:580px; max-width:100%, so it needs nothing further.
     ====================================================================== */
  .supplier-table .col-contacts,
  .supplier-table .col-addresses,
  .supplier-table .col-sites,
  .supplier-table .col-quotes,
  .supplier-table .col-activity { display: none; }

  /* table-layout:fixed does not give an `auto` column the leftover space the
     way auto layout does — the table collapsed to the sum of the explicit
     widths and left the wrap half empty, crushing Company to ~27px. */
  .supplier-table { table-layout: auto; width: 100%; }

  .supplier-table .col-sname   { width: auto; }
  .supplier-table .col-status  { width: 56px; }
  .supplier-table .col-svc     { width: 72px; }
  .supplier-table .col-actions { width: 32px; }

  /* Page CSS sets overflow-wrap:anywhere to stop long cells widening a
     fixed-layout column. In auto layout that is no longer needed and it
     breaks company names mid-word — relax it to whole-word wrapping. */
  .supplier-table td { overflow-wrap: break-word; word-break: normal; }

  .supplier-table thead th { font-size: 10px; padding: 7px 5px; white-space: normal; }
  .supplier-table tbody td { padding: 9px 5px; font-size: 12px; }
  .supplier-table .status-badge,
  .supplier-table .svc-pill-compact { font-size: 10px; }

  .supplier-table-wrap { overflow-x: hidden; }

  /* The actions menu is position:fixed and JS right-aligns it to its
     trigger (left = rect.right - menuWidth), so at 160px wide it lands
     inside the viewport unaided. Left alone deliberately. */
  /* ======================================================================
     ADMIN — per-user activity log
     ----------------------------------------------------------------------
     The log modal already inherits the bottom-sheet treatment from the
     MODALS block above (.modal-wide is a single class, so `.modal` there
     wins on source order). Only the row grid and the cell that opens it
     need work.
     ====================================================================== */

  /* The actions cell now carries two buttons — View log + Revoke — on a row
     that has already given most of its width to Email. Stacked full-width
     rather than dropped: the log is read-only and reads perfectly well as a
     sheet, so there is no reason to make it desktop-only. The desktop cell
     is nowrap, which has to be undone first. */
  #usersTable td:last-child { white-space: normal; }
  #usersTable td:last-child .btn {
    display: flex;
    width: 100%;
    justify-content: center;
  }
  #usersTable td:last-child .btn + .btn { margin-top: 4px; }

  /* .log-row is `8px | 1fr | auto` with a nowrap timestamp in the third
     column. At 360px that leaves the headline almost nothing. The timestamp
     moves under the text, which also puts it in reading order rather than
     competing with the headline for the same line. */
  .log-row {
    grid-template-columns: 8px minmax(0, 1fr);
    gap: 10px;
  }
  .log-when {
    grid-column: 2;
    margin-top: 4px;
  }

  /* .modal-body.log-body is two classes, so its `padding: 0` survives the
     generic .modal-body rule above — intentional, since .log-filters and
     .log-list carry their own. Only the height cap needs converting from
     vh (which sits under the iOS URL bar) to dvh. */
  .modal-body.log-body { max-height: 74dvh; }
  .log-filters { padding: 12px 16px; }
  .log-list    { padding: 4px 16px 16px; }
  .log-headline { font-size: 12.5px; }
  .log-detail   { font-size: 11.5px; }

  /* ======================================================================
     CLIENTS
     ----------------------------------------------------------------------
     Columns, in order:
       1 Name   2 Contact Name   3 Contact Email   4 Contact Phone   5 Type
     Keeping 1 / 3 / 5. The contact name is the weakest of the three contact
     fields on a narrow screen, and the phone is a tap-to-call link that the
     email column's row already implies.

     nth-child on the cells alone is enough here: clients.html's table has no
     <colgroup>, so nothing survives to reserve the hidden columns' width.
     Contrast .opp-table above, which needs `colgroup { display:none }` as
     well precisely because it has one.
     ====================================================================== */
  #clientsTable th:nth-child(2), #clientsTable td:nth-child(2),
  #clientsTable th:nth-child(4), #clientsTable td:nth-child(4) { display: none; }

  #clientsTable { table-layout: auto; width: 100%; }
  #clientsTable th:nth-child(5) { width: auto; }
  #clientsTable thead th { font-size: 10px; padding: 7px 5px; white-space: normal; }
  #clientsTable tbody td { padding: 9px 5px; font-size: 12px; overflow-wrap: break-word; }
  #clientsTable tbody td.muted { font-size: 11.5px; }
  #clientsTable .kind-badge { font-size: 10px; }

  /* clients.html wraps its content in .page-body rather than .page, so it
     gets none of the .page padding above — and .page-body is shared with
     site-map, whose full-bleed map must not gain padding. Scoped by id
     rather than by :has(), which reaches across the tree. */
  #clientsTitle { padding: 20px 12px 0; }
  #clientsPage  { padding: 0 12px 24px; }
  #clientsPage .section-header { padding: 0 2px 8px; gap: 8px; }
  #clientsPage .section-title { font-size: 14px; letter-spacing: 0.05em; }
  #clientsPage .section-meta { font-size: 10px; }

  /* 280px fixed. Also 13px, which trips iOS Safari's focus auto-zoom. */
  #clientSearch { width: 100%; font-size: 16px; }
}
