/* ═══════════════════════════════════════════════════════════════════════════
 * SPARTAN CRM — js/responsive.css
 * A-668 · Foldable phone / phone / tablet layout
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * MUST BE THE LAST STYLESHEET IN index.html. Read on before editing.
 *
 * WHY THIS FILE USES !important
 * ─────────────────────────────
 * Almost every layout style in this app is an inline `style="..."` emitted by a
 * JS renderer into a template string (≈10,000 of them across 206 modules — the
 * top file alone, 22a-jobs-detail-v2.js, has 946). An inline style beats every
 * selector, so a stylesheet that does not use !important cannot change a single
 * one of those layouts. Rewriting all of them into classes is a multi-month
 * refactor of code that is otherwise working; overriding them from the last
 * position in the cascade is one file. So: !important is the deliberate design
 * of this layer, not sloppiness. Every rule here is also fenced inside a
 * viewport-tier / pointer / segment condition, so on a desktop session this file
 * contributes nothing at all.
 *
 * HOW IT SELECTS
 * ──────────────
 * `html[data-vp="xs"]` etc., stamped by modules/_lib/viewport.js (and, before
 * the first paint, by the inline snippet in index.html's <head>). Tiers:
 *
 *     xs  <  480px   Galaxy Z Fold COVER screen 344×882 · phones 412×915
 *     sm  480-639    large phones · phone landscape · Flip cover
 *     md  640-899    Z Fold UNFOLDED 673×841 / 717×845 · iPad portrait 820×1180
 *     lg  900-1279   tablet landscape 1024×768 · small laptops
 *     xl  ≥ 1280     desktop
 *
 * plus `[data-vp-nav="drawer"]` (xs|sm|md — the sidebar is an overlay),
 * `[data-vp-h="short"]` (viewport under 560px TALL — the Fold cover screen in
 * landscape is 882×344), `[data-vp-fold="h"|"v"]` (a real hinge seam, from the
 * Viewport Segments media features), `[data-vp-ptr="coarse"]` and
 * `[data-vp-orient]`.
 *
 * The attribute is used in preference to a plain media query because the SAME
 * tier is read by the JS renderers (SpartanVP.tier() decides drawer-vs-docked
 * nav, chrome heights, column counts, sheet-vs-dialog modals). A media query and
 * a JS breakpoint that drift apart is precisely how you get a docked sidebar
 * overlapping a full-width page. One source of truth, two consumers.
 *
 * A few rules below DO use `@media` — only where the thing being styled has no
 * JS counterpart and the query is genuinely about the device (pointer, hover,
 * reduced motion, print).
 *
 * PERFORMANCE
 * ───────────
 * CONTRACT-performance.md: a paint must stay under 120ms and the whole page is
 * rebuilt on every setState. Nothing here can affect that — CSS is applied by
 * the compositor, not by a renderer. The two things that COULD have hurt were
 * (a) re-rendering on every resize event and (b) renderers calling matchMedia
 * per row; both are solved in viewport.js (tier-change-only repaint, cached
 * reads). This file adds no JS.
 *
 * OPT-OUTS — for the handful of places where the generic rule is wrong:
 *     .vp-cols-keep    keep my grid-template-columns exactly as authored
 *     .vp-nowrap-keep  keep my white-space:nowrap
 *     .vp-wide         I am legitimately wider than the viewport; scroll me
 *     .vp-desktop-only display:none below lg
 *     .vp-narrow-only  display:none at lg and above
 * ═══════════════════════════════════════════════════════════════════════════ */


/* ═══════════════════════════════════════════════════════════════════════════
 * 1 · FOUNDATION — things that must be true at every size
 * ═══════════════════════════════════════════════════════════════════════════ */

/* The single most common cause of a horizontally-scrolling page on a phone is a
 * flex or grid CHILD refusing to shrink below its content's intrinsic width.
 * The default `min-width:auto` on a flex item means one long unbroken string —
 * a customer email address, a 30-character job reference, a Google Maps
 * attribution — makes its whole ancestor chain wider than the screen. Setting
 * min-width:0 on flex/grid children is the standard fix and is safe: it only
 * lets a box get SMALLER than its content, which is what `overflow` and
 * `text-overflow` are then there to handle.
 *
 * Scoped to #app so it can never touch a third-party widget's internals
 * (Google Maps' own panes, the Places Autocomplete shadow host), and applied at
 * every tier because a 1024px tablet overflows on a long email address too. */
#app :where(.card, .sp-card, .page-hero, .data-row, .kpi, .modal, .sp-modal, .panel) > * { min-width: 0; }

/* NOT a blanket `#app * { min-width: 0 }`. That was the first attempt and it was
 * wrong: combined with the wrap rule below it let a 6-column grid squeeze every
 * column to ~40px, and the Deal Detail stage tracker rendered "AWAITING MEASURE"
 * as one character per line down the screen. A page that scrolls sideways is
 * bad; a page of vertical text is worse. So the floor is dropped only on the
 * containers whose children genuinely need to shrink — the ones that hold a long
 * unbroken string next to something else. */
html[data-vp="xs"] #app :where(.data-row, .page-hero, .tb, .modbar, .sb, .modal-header, .modal-footer) > *,
html[data-vp="sm"] #app :where(.data-row, .page-hero, .tb, .modbar, .sb, .modal-header, .modal-footer) > *,
html[data-vp="md"] #app :where(.data-row, .page-hero, .tb, .modbar, .sb, .modal-header, .modal-footer) > * { min-width: 0; }

/* …with the exception of things whose whole job is to be a fixed size. */
/* …and these keep their intrinsic floor even inside those containers: a
 * notification count pill that can shrink below its own diameter stops being
 * round, and a 24px avatar that can shrink is just a smear. */
html[data-vp="xs"] #app :where(svg, img, .av, .sb-mark, .ico, .sp-dot, .brand-dot, .tb-dot, .bdg, .tag, .stage-bub, .map-pin-bub, .run-time, input[type="checkbox"], input[type="radio"]),
html[data-vp="sm"] #app :where(svg, img, .av, .sb-mark, .ico, .sp-dot, .brand-dot, .tb-dot, .bdg, .tag, .stage-bub, .map-pin-bub, .run-time, input[type="checkbox"], input[type="radio"]),
html[data-vp="md"] #app :where(svg, img, .av, .sb-mark, .ico, .sp-dot, .brand-dot, .tb-dot, .bdg, .tag, .stage-bub, .map-pin-bub, .run-time, input[type="checkbox"], input[type="radio"]) { min-width: auto; }

/* Never let an image or embed push the page wide. */
#app img, #app video, #app canvas, #app iframe, #app embed, #app object { max-width: 100%; }

/* Long unbroken strings — emails, URLs, reference numbers, base64 — wrap instead
 * of forcing a horizontal scroll.
 *
 * `break-word`, NOT `anywhere`, and the difference is the whole rule. Both break
 * a too-long word, but `anywhere` ALSO makes the element's min-content width one
 * character — so any flex or grid child inherits a min-content of a single glyph
 * and can be squeezed to a 12px column. That is exactly what happened on the
 * first pass: Deal Detail's six-stage tracker painted "AWAITING MEASURE" as one
 * letter per line, and a contact's name did the same. `break-word` leaves
 * min-content at the longest word, so a column can never collapse below a
 * readable width — it overflows its own box first, which the container rules
 * below then handle properly. */
html[data-vp="xs"] #app, html[data-vp="sm"] #app, html[data-vp="md"] #app { overflow-wrap: break-word; }

/* Email addresses and URLs are the one place `anywhere` is right, and it is safe
 * HERE specifically because a link is not a layout container — nothing nests
 * inside it that could then be squeezed to one glyph, which is the trap that made
 * a blanket `anywhere` unusable (see the note above). A customer's email is often
 * the single widest token on a detail page: samuel.thompson@example.test measured
 * 212px, and `break-word` will not break it because it treats the whole address
 * as one word with no break opportunity it is willing to take. */
html[data-vp="xs"] #app :where(a[href^="mailto:"], a[href^="tel:"], a[href^="http"], .sp-email, .sp-url),
html[data-vp="sm"] #app :where(a[href^="mailto:"], a[href^="tel:"], a[href^="http"], .sp-email, .sp-url),
html[data-vp="md"] #app :where(a[href^="mailto:"], a[href^="tel:"], a[href^="http"], .sp-email, .sp-url) {
  overflow-wrap: anywhere;
  white-space: normal !important;
  min-width: 0;
}

/* iOS zooms the whole page when a font-size < 16px input is focused, then
 * leaves it zoomed — the classic "the app got bigger and I can't get it back"
 * bug. 16px on the input is the only reliable prevention. Only on touch, so
 * desktop keeps the compact 13px fields. */
@media (pointer: coarse) {
  #app input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
  #app select,
  #app textarea { font-size: 16px !important; }
}

/* A <select> sizes itself to its LONGEST OPTION, and the author never sees that
 * width — it depends on the data. The Job Detail status picker holds "Awaiting
 * Service Work Material ⚠" and rendered 351px wide inside a 324px content box,
 * which was the last thing keeping that page overflowing after the two flex rows
 * above it were fixed. The label is truncated instead; the full text is still
 * there when the native picker opens, which on a phone is a full-screen list. */
/* Capped against the VIEWPORT, not against the parent. `max-width:100%` is
 * useless here: the parent is a flex row whose own min-content width comes from
 * this select, so it had already grown to 351px and 100% of 351 is 351. 100vw
 * minus the page padding is the only bound that does not depend on an ancestor
 * that the select itself is inflating. --app-pad is published on <main> by
 * 99-init and inherits down. */
html[data-vp="xs"] #app select,
html[data-vp="sm"] #app select,
html[data-vp="md"] #app select {
  max-width: calc(100vw - (2 * var(--app-pad, 24px)) - 8px) !important;
  min-width: 0 !important;
  text-overflow: ellipsis;
}
/* …and the row holding it must be allowed to stop growing too, or it keeps the
 * page wide even once the select inside it has been capped. */
html[data-vp="xs"] #app :where(.jdr-jh-status-block, .data-row) ,
html[data-vp="sm"] #app :where(.jdr-jh-status-block, .data-row) { max-width: 100%; }

/* Safe areas. index.html already ships viewport-fit=cover, which is what makes
 * env() non-zero — without the pad, content sits under the notch and the home
 * indicator on every modern phone. */
html[data-vp-nav="drawer"] body { padding-bottom: env(safe-area-inset-bottom); }


/* ═══════════════════════════════════════════════════════════════════════════
 * 2 · APP SHELL — module bar, topbar, sidebar drawer, main
 *
 * The JS already computes the right geometry (07-shared-ui.js _spSyncChrome /
 * _spSidebarGeom / _spNavIsDrawer, 99-init.js's <main>). The rules here are
 * (a) the parts that are pure presentation and (b) a BACKSTOP: if the module
 * loader ever drops modules/_lib/viewport.js, SpartanVP is undefined, the JS
 * falls back to the desktop numbers — and these rules still stop a 344px phone
 * from painting a page 220px off the left edge. Belt and braces, same reasoning
 * as the inline `background:#1a1a1a` backstop on the sidebar.
 * ═══════════════════════════════════════════════════════════════════════════ */

html[data-vp-nav="drawer"] #topbar { left: 0 !important; }
html[data-vp-nav="drawer"] .app-main { margin-left: 0 !important; }

/* ── Hamburger ─────────────────────────────────────────────────────────────
 * The only way into the drawer once it is shut. 44×44 — Apple's HIG minimum
 * and roughly the width of an adult fingertip; anything smaller is a mis-tap. */
.tb-burger {
  display: none;
  align-items: center; justify-content: center;
  width: 44px; height: 44px; margin-left: -8px;
  flex-shrink: 0;
  border: none; background: transparent; border-radius: 12px;
  color: #1a1a1a; cursor: pointer; padding: 0;
  -webkit-tap-highlight-color: transparent;
}
html[data-vp-nav="drawer"] .tb-burger { display: inline-flex; }
.tb-burger:active { background: rgba(196,18,48,.10); }
.tb-native .tb-burger { color: #fff; }

/* ── Module bar (the red SPARTAN / CRM-tab strip) ──────────────────────────
 * At xs it is not rendered at all (07-shared-ui.js renderModuleBar returns ''
 * once MODULE_BAR_HEIGHT hits 0) and its six tabs live in the drawer instead.
 * At sm and on short viewports it is 36px with horizontally scrollable tabs. */
html[data-vp="sm"] #moduleBar,
html[data-vp-h="short"] #moduleBar { height: 36px !important; padding: 0 10px !important; }
html[data-vp="sm"] #moduleBar .brand-sub,
html[data-vp="md"] #moduleBar .brand-sub,
html[data-vp-h="short"] #moduleBar .brand-sub { display: none; }
html[data-vp="sm"] #moduleBar .brand-name { font-size: 11px; }
html[data-vp="sm"] #moduleBar .mod-tabs,
html[data-vp="md"] #moduleBar .mod-tabs,
html[data-vp-h="short"] #moduleBar .mod-tabs {
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  /* Momentum-snap to a tab so a half-scrolled bar never rests mid-label. */
  scroll-snap-type: x proximity;
  flex: 1;
  min-width: 0;
}
html[data-vp="sm"] #moduleBar .mod-tabs::-webkit-scrollbar,
html[data-vp="md"] #moduleBar .mod-tabs::-webkit-scrollbar { display: none; }
html[data-vp="sm"] #moduleBar .mod-tab,
html[data-vp="md"] #moduleBar .mod-tab,
html[data-vp-h="short"] #moduleBar .mod-tab { white-space: nowrap; scroll-snap-align: start; }

/* ── Topbar ───────────────────────────────────────────────────────────────
 * At 344px the topbar has to hold: menu, search, notifications, profile. That
 * is already four things and the smallest of them (search) is the one people
 * actually use most, so everything else that was competing for the row is
 * dropped rather than squeezed. Each dropped control is still reachable: the
 * clock chip on the HR page, back/forward via the OS/browser gesture (and the
 * in-app history is unaffected), the branch picker inside the pages that filter
 * by branch. */
html[data-vp="xs"] #topbar { padding: 0 10px !important; gap: 6px !important; }
html[data-vp="sm"] #topbar { padding: 0 12px !important; gap: 8px !important; }
html[data-vp="md"] #topbar { padding: 0 16px !important; }

html[data-vp-nav="drawer"] #topbar .tb-navbtns,
html[data-vp-nav="drawer"] #topbar .hr-clock-chip,
html[data-vp="xs"] #topbar .tb-dev-badge { display: none !important; }

/* Search takes whatever is left of the row — and, crucially, YIELDS.
 *
 * `flex: 1 1 auto` was wrong: `flex-basis:auto` means the input's own declared
 * width is its starting size, so at 344px the search box held its ground and
 * pushed the notification bell, the email-tracking eye and the profile chip past
 * the right edge of the topbar — which sets overflow:hidden, so they were not
 * merely off-screen, they were CLIPPED and unreachable. The bell is how anyone
 * finds out a customer replied. `flex: 1 1 40px` makes the search the thing that
 * gives way, which is right: it can be a 40px icon-width field and still be
 * tapped, whereas a clipped bell cannot be tapped at all. */
html[data-vp-nav="drawer"] #topbar .sp-ts-wrap { max-width: none; flex: 1 1 40px; min-width: 0; }
html[data-vp="xs"] #topbar .sp-ts-input { min-width: 0; }

/* Every ambient indicator leaves the row at xs — unread voicemail, the maps
 * on/off toggle, Communication Breach, Unhappy Clients, check-measure holds, the
 * CM sheet review queue, email-tracking opens. Seven 36px lights plus a search
 * box, a branch filter, the bell and the profile do not fit in 324px, and what
 * actually happened is that the last few were pushed past the right edge of a
 * container with overflow:hidden — clipped, untappable, and invisible to a
 * scrollWidth check because the clip was doing its job.
 *
 * They are NOT lost: renderDrawerAlerts() (07-shared-ui.js) renders the same
 * components, from the same renderers with the same data-actions, in an "Alerts"
 * block at the top of the drawer. An escalation matters MORE to someone on a
 * phone, so hiding one would have been the wrong trade; moving it is the right
 * one. The bell and the profile keep their place on the row because they are the
 * two the user reaches for constantly. */
html[data-vp-nav="drawer"] #topbar .tb-ind,
html[data-vp-nav="drawer"] #topbar #etEye { display: none !important; }

/* ── The relocated indicators, in the drawer ─────────────────────────────── */
.sb-alerts { padding: 8px 4px 12px; margin-bottom: 8px; border-bottom: 1px solid rgba(255,255,255,.09); }
.sb-alerts-label {
  padding: 2px 8px 8px; font-size: 9px; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: #6b7280;
}
.sb-alerts-row { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 4px; }
/* The indicators were authored for a white topbar — dark glyphs on light. In the
 * ink-black drawer they would be invisible, so give them a light surface of
 * their own rather than restyling seven components. */
/* `:not(style):not(script)` is load-bearing, not defensive. Several of these
 * indicator components ship their own <style> block alongside their button
 * (renderCommsBreachButton injects @keyframes for its pulse), and a <style>
 * element is only invisible because its default is display:none. A blanket
 * `> * { display: inline-flex }` overrode that and printed 20 lines of raw CSS
 * source down the middle of the drawer. */
.sb-alerts-row > *:not(style):not(script) {
  background: rgba(255,255,255,.06);
  border: 1px solid rgba(255,255,255,.10);
  border-radius: 10px;
  color: rgba(255,255,255,.82);
  min-width: 40px; min-height: 40px;
  display: inline-flex; align-items: center; justify-content: center;
}
.sb-alerts-row > *:not(style):not(script) svg, .sb-alerts-row button { color: inherit; }
html[data-vp="xs"] #topbar .sp-ts-kbd,
html[data-vp="sm"] #topbar .sp-ts-kbd { display: none !important; }
html[data-vp="xs"] #topbar .sp-ts-input { padding-right: 10px !important; }
/* The results dropdown is a full-width sheet rather than a floating card —
 * a 460px popover anchored to a 324px input hangs off the screen. */
html[data-vp="xs"] #topbar .sp-ts-drop,
html[data-vp="sm"] #topbar .sp-ts-drop {
  position: fixed !important;
  left: 8px !important; right: 8px !important; width: auto !important;
  max-height: calc(100vh - 140px) !important;
  overflow-y: auto;
}

/* Branch pill: dot only at xs (the colour IS the branch — see
 * topbar-branch-pill.css, which gives every branch its own hue and pulse). */
html[data-vp="xs"] #topbar .branch-pill-label { display: none !important; }
html[data-vp="xs"] #topbar .branch-pill { padding: 6px 8px !important; }

/* Profile chip: avatar only at xs. `.tb-profile` is spartan-tokens.css's newer
 * chip; `.tb-profile-chip` is the one renderTopBar actually emits (it had no
 * class at all until A-668, which is why an earlier rule here matched nothing and
 * the name + role block kept pushing the notification bell off the row). The
 * dropdown INSIDE it must keep its size — it is what the chip is for. */
html[data-vp="xs"] #topbar :where(.tb-profile, .tb-profile-chip) { padding-left: 4px !important; border-left: none !important; margin-left: 0 !important; }
html[data-vp="xs"] #topbar :where(.tb-profile, .tb-profile-chip) > *:not(.av):not(svg):not([id$="Drop"]) { display: none !important; }
/* …and the chevron goes too: with no label beside it there is nothing to expand. */
html[data-vp="xs"] #topbar :where(.tb-profile, .tb-profile-chip) > svg { display: none !important; }
/* The dropdown is a 220px absolute panel anchored right:0 to a chip that now sits
 * at the very edge of the screen. Pin it to the viewport instead. */
html[data-vp="xs"] #profileDrop, html[data-vp="sm"] #profileDrop {
  position: fixed !important;
  right: 8px !important; left: auto !important;
  top: calc(var(--sp-chrome-h, 96px) + 4px) !important;
  max-width: calc(100vw - 16px) !important;
}

/* ── Sidebar as an off-canvas drawer ──────────────────────────────────────── */
.sb-drawer {
  /* Scroll the drawer's own nav, and never chain that scroll to the page
   * behind it — the "I scrolled the menu and the page moved" bug. */
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  max-width: 100vw;
}
/* Off-canvas means off the a11y tree too: a shut drawer must not be reachable
 * by keyboard or screen reader, or Tab from the hamburger walks into 20 hidden
 * nav rows. `visibility` is animatable-with-delay so it does not kill the
 * slide-out transition the way display:none would. */
.sb-drawer:not(.sb-drawer-open) { visibility: hidden; transition: transform .22s cubic-bezier(.4,0,.2,1), visibility 0s linear .22s; }
.sb-drawer.sb-drawer-open { visibility: visible; transition: transform .22s cubic-bezier(.4,0,.2,1), visibility 0s; }

.sb-scrim { animation: sbScrimIn .18s ease-out; }
@keyframes sbScrimIn { from { opacity: 0 } to { opacity: 1 } }

/* In drawer mode the sidebar's collapse chevron IS the drawer's close button —
 * the only control inside the panel that dismisses it. It renders at 22x32,
 * which was the smallest tap target left in the app after everything else was
 * floored, and it is the one a thumb reaches for first. */
html[data-vp-nav="drawer"] .sb-collapse-btn {
  min-width: 44px; min-height: 44px;
  align-items: center; justify-content: center;
  margin-right: -8px;
}

/* Drawer nav rows are touch targets, not mouse targets. */
html[data-vp-nav="drawer"] .sb-drawer .nav-item {
  min-height: 44px;
  padding: 10px 12px !important;
  font-size: 14px !important;
  border-radius: 10px;
}
html[data-vp-nav="drawer"] .sb-drawer .nav-item .ico { width: 18px; height: 18px; }

/* ── The module switcher inside the drawer (renderDrawerModuleNav) ────────── */
.sb-modnav { padding: 4px 4px 12px; margin-bottom: 8px; border-bottom: 1px solid rgba(255,255,255,.09); }
.sb-modnav-label {
  padding: 2px 8px 8px; font-size: 9px; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: #6b7280;
}
.sb-modnav-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.sb-modnav-btn {
  min-height: 40px; padding: 8px 10px;
  background: rgba(255,255,255,.05);
  border: 1px solid rgba(255,255,255,.09);
  border-radius: 10px;
  color: rgba(255,255,255,.72);
  font-family: inherit; font-size: 11.5px; font-weight: 600;
  text-align: left; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.sb-modnav-btn.on { background: #c41230; border-color: #c41230; color: #fff; }
.sb-modnav-btn:active { transform: scale(.98); }

/* Group flyouts are a hover idiom and cannot work on touch. In drawer mode the
 * group row toggles the panel on tap (07a's ui-sidebar-flyout-toggle already
 * handles click), but the panel is positioned as a floating card next to a
 * 64px rail — wrong when the drawer is 264px of a 344px screen. Dock it. */
html[data-vp-nav="drawer"] #sbFlyout {
  position: fixed !important;
  left: 0 !important; right: 0 !important;
  top: auto !important; bottom: 0 !important;
  width: auto !important; max-width: none !important;
  max-height: 62vh; overflow-y: auto;
  border-radius: 18px 18px 0 0 !important;
  z-index: 71 !important;
  padding-bottom: calc(env(safe-area-inset-bottom) + 8px) !important;
  box-shadow: 0 -10px 34px rgba(0,0,0,.4) !important;
}

/* ── <main> ───────────────────────────────────────────────────────────────── */
/* The 1400px reading measure is a desktop concern; below that it is a no-op
 * that only complicates the container. Make it explicit so a page can rely on
 * .app-page-wrap being exactly the content width at every tier. */
html[data-vp="xs"] .app-page-wrap,
html[data-vp="sm"] .app-page-wrap,
html[data-vp="md"] .app-page-wrap { max-width: none !important; width: 100% !important; }

/* Toasts: a 260px-min card pinned bottom-right is half off a 344px screen. */
html[data-vp-nav="drawer"] #toasts {
  left: 10px !important; right: 10px !important;
  bottom: calc(env(safe-area-inset-bottom) + 12px) !important;
  align-items: stretch !important;
}
html[data-vp-nav="drawer"] #toasts > div { min-width: 0 !important; width: 100% !important; }


/* ═══════════════════════════════════════════════════════════════════════════
 * 3 · LAYOUT PRIMITIVES — grids, rows, tables
 * ═══════════════════════════════════════════════════════════════════════════ */

/* ── Equal-column grids ───────────────────────────────────────────────────
 * A survey of the repo found 113 inline `1fr 1fr`, 40 `1fr 1fr 1fr`, 4
 * `1fr 1fr 1fr 1fr` and 64 `repeat(N,1fr)` — i.e. ~220 hardcoded "N cards
 * across" grids, none of which fit a phone. They are unclassed inline styles,
 * so the only way to reach them is the attribute-substring selector below.
 *
 * DELIBERATELY EXCLUDED:
 *   • repeat(auto-fit / auto-fill, minmax(…))  — 60 of these, and they are
 *     ALREADY responsive. Collapsing them would be a regression.
 *   • .vp-cols-keep — the explicit opt-out.
 * The `px`-track grids (`120px 1fr`, `1fr auto`, icon+label pairs) are left
 * alone on purpose: they are two-column *rows*, not card grids, and stacking
 * them would put a label above its own value. */
html[data-vp="xs"] .app-main [style*="grid-template-columns:1fr 1fr"]:not(.vp-cols-keep),
html[data-vp="xs"] .app-main [style*="grid-template-columns: 1fr 1fr"]:not(.vp-cols-keep),
html[data-vp="xs"] .app-main [style*="grid-template-columns:repeat("]:not([style*="auto-fit"]):not([style*="auto-fill"]):not(.vp-cols-keep),
html[data-vp="xs"] .app-main [style*="grid-template-columns: repeat("]:not([style*="auto-fit"]):not([style*="auto-fill"]):not(.vp-cols-keep),
html[data-vp="sm"] .app-main [style*="grid-template-columns:1fr 1fr 1fr"]:not(.vp-cols-keep),
html[data-vp="sm"] .app-main [style*="grid-template-columns: 1fr 1fr 1fr"]:not(.vp-cols-keep) {
  grid-template-columns: 1fr !important;
}
/* sm keeps two columns for 2-up grids (a 540px phone in landscape has room),
 * and drops 4+ down to two rather than one. */
html[data-vp="sm"] .app-main [style*="grid-template-columns:repeat("]:not([style*="auto-fit"]):not([style*="auto-fill"]):not(.vp-cols-keep),
html[data-vp="sm"] .app-main [style*="grid-template-columns: repeat("]:not([style*="auto-fit"]):not([style*="auto-fill"]):not(.vp-cols-keep),
html[data-vp="md"] .app-main [style*="grid-template-columns:repeat("]:not([style*="auto-fit"]):not([style*="auto-fill"]):not(.vp-cols-keep),
html[data-vp="md"] .app-main [style*="grid-template-columns: repeat("]:not([style*="auto-fit"]):not([style*="auto-fill"]):not(.vp-cols-keep),
html[data-vp="md"] .app-main [style*="grid-template-columns:1fr 1fr 1fr"]:not(.vp-cols-keep),
html[data-vp="md"] .app-main [style*="grid-template-columns: 1fr 1fr 1fr"]:not(.vp-cols-keep) {
  grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
}

/* The token grid helpers already collapse at 1100px (spartan-tokens.css:616).
 * Make the steps deliberate rather than all-or-nothing. */
html[data-vp="xs"] .sp-grid-2, html[data-vp="xs"] .sp-grid-3, html[data-vp="xs"] .sp-grid-4 { grid-template-columns: 1fr !important; gap: 10px !important; }
html[data-vp="sm"] .sp-grid-2 { grid-template-columns: 1fr !important; }
html[data-vp="sm"] .sp-grid-3, html[data-vp="sm"] .sp-grid-4,
html[data-vp="md"] .sp-grid-3, html[data-vp="md"] .sp-grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }

/* Anything already using auto-fit/auto-fill: shrink the minmax floor so it
 * actually reaches one column at 344px instead of overflowing by 20px. A
 * minmax(240px,1fr) grid inside a 324px content box fits one column — but a
 * minmax(320px,1fr) one does not, and CSS grid does NOT shrink below the floor;
 * it overflows. This is the single most common overflow in the codebase. */
html[data-vp="xs"] .app-main [style*="minmax("] { grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)) !important; }

/* ── Flex rows that must wrap ─────────────────────────────────────────────
 * Toolbars, filter bars, KPI strips and page-hero action rows are all
 * `display:flex` with 3-6 children and no flex-wrap. On a phone they either
 * overflow or squash every child to unreadability. Wrapping is almost always
 * the right answer for a toolbar — and where it is not (a table row, a
 * segmented control), .vp-nowrap-keep opts out.
 *
 * Scoped away from things where wrapping breaks the component: anything that
 * scrolls horizontally on purpose (the kanban, the module tabs), and the
 * fixed chrome. */
html[data-vp="xs"] .app-main [style*="display:flex"]:not([style*="flex-direction:column"]):not([style*="flex-wrap"]):not(.vp-nowrap-keep):not(.kanban):not(.runs),
html[data-vp="sm"] .app-main [style*="display:flex"]:not([style*="flex-direction:column"]):not([style*="flex-wrap"]):not(.vp-nowrap-keep):not(.kanban):not(.runs) {
  flex-wrap: wrap;
}
/* A row of controls authored `display:flex; …; gap:Npx; flex-shrink:0` is the
 * recurring shape of an action group — HR's per-user Edit/Deactivate/Delete
 * (431px), the detail views' header actions, a table row's buttons. flex-shrink:0
 * is right on a wide screen (Delete must never squash to "Dele…") and it is what
 * keeps the page wide on a narrow one: the row cannot shrink and nothing above it
 * can either.
 *
 * The `gap:` in the selector is what makes this safe. An avatar or icon with
 * `display:flex;flex-shrink:0` — the other common use, and one that MUST keep its
 * size — has no gap, because it has one child. A row of siblings has one. */
html[data-vp="xs"] .app-main [style*="display:flex"][style*="flex-shrink:0"][style*="gap:"],
html[data-vp="sm"] .app-main [style*="display:flex"][style*="flex-shrink:0"][style*="gap:"] {
  flex-shrink: 1 !important;
  flex-wrap: wrap;
  min-width: 0;
  row-gap: 8px;
}

/* …but a two-child row that is clearly "icon + text" or "label + value" should
 * not wrap its label onto a second line. Those are the rows with an explicit
 * gap under 12px and no justify-content:space-between; too subtle for a
 * selector, so instead: allow the wrap, and stop the SHRINKING that makes a
 * wrap look broken. */
html[data-vp="xs"] .app-main .page-hero,
html[data-vp="sm"] .app-main .page-hero { flex-wrap: wrap !important; gap: 12px !important; }
html[data-vp="xs"] .app-main .page-hero > * { flex: 1 1 100%; }
html[data-vp="xs"] .app-main .page-title { font-size: 20px !important; max-width: none !important; }
html[data-vp="sm"] .app-main .page-title { font-size: 22px !important; }
html[data-vp="xs"] .app-main .page-sub, html[data-vp="sm"] .app-main .page-sub { max-width: none !important; }

/* ── Tables ──────────────────────────────────────────────────────────────
 * There is no honest way to fit a 10-column job table into 344px. The two
 * real options are a card layout per row (a per-page rewrite) or horizontal
 * scroll. This gives every table horizontal scroll — content stays complete
 * and nothing is hidden — with a shadow cue so the user knows to swipe, and
 * the first column pinned so a scrolled row is still identifiable.
 *
 * `display:block` on the table is what makes it scrollable without a wrapper
 * div existing in the markup; the table keeps its own internal layout because
 * we set `width:max-content` on it. */
/* Applies through lg, not just the phone tiers. Measured: the Contacts table is
 * 771px of columns, which clips inside a 673px unfolded Fold, a 717px one, AND a
 * 1024px tablet in landscape (1024 − a 220px docked sidebar − padding = 804, and
 * the table grows to 813 there). Every one of those was CLIPPED, not scrolled —
 * the card wrapping it sets overflow:hidden for its corners — so Branch, Source
 * and the row actions were simply gone, with the page reporting no overflow at
 * all. xl is deliberately excluded: desktop is the control for this change. */
/* ⚠ THE TABLE MUST STAY ONE BOX. Do not reintroduce `table{display:block}`.
 *
 * The first version of this rule did exactly that, with
 * `thead,tbody,tfoot{display:table;width:max-content}` to make the rows lay out.
 * It scrolled beautifully and it was WRONG: two `display:table` children are two
 * independent table formatting contexts, so the header's column widths are
 * computed separately from the body's — and the header cells stop sitting over
 * the columns they label. Measured misalignments of 265px (the accounts Branch
 * Breakdown at 344px), 264px (commission's 12 columns), 203px (purchase orders'
 * 21) and 170px (payroll pay-lines), at md and lg as well as on a phone. A table
 * that scrolls but puts "Paid" over the Outstanding column is worse than one that
 * clips: clipping loses data, misalignment LIES about it, and someone reads a
 * number under the wrong heading and acts on it.
 *
 * The correct CSS-only fix makes the table's PARENT the scroller and leaves the
 * table itself a single table box, so column sizing, <colgroup>, table-layout and
 * position:sticky all keep working. `:has()` is what makes that reachable without
 * a wrapper div in the markup — Chrome 105+ / Safari 15.4+, which is every device
 * in the target list. xl is excluded: desktop is the control. */
html:not([data-vp="xl"]) .app-main :is(div, section, form, li, td, main):has(> table) {
  /* !important, because the card wrapping a table almost always carries an INLINE
   * `overflow:hidden` to clip its own rounded corners — `<div class="card"
   * style="overflow:hidden">` is the house idiom — and an inline declaration beats
   * any selector. Without it this rule matched and did nothing, and the tables on
   * Outstanding, Weekly In-vs-Out, Time Clock and Contacts stayed clipped: 543px
   * of columns inside a 324px card with no way to reach the rest.
   * Only the X axis is overridden, so the corner clipping that inline style exists
   * for still works vertically. */
  overflow-x: auto !important;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  max-width: 100%;
}
html[data-vp="xs"] .app-main table,
html[data-vp="sm"] .app-main table,
html[data-vp="md"] .app-main table,
html[data-vp="lg"] .app-main table {
  width: auto !important;
  min-width: 100%;
}
html[data-vp="xs"] .app-main :where(.th, .td, th, td),
html[data-vp="sm"] .app-main :where(.th, .td, th, td) { padding: 9px 10px !important; font-size: 12.5px !important; }
html[data-vp="md"] .app-main :where(.th, .td, th, td) { padding: 9px 11px !important; }

/* Any container the page has already marked as a horizontal scroller keeps
 * working, and gains touch momentum + a contained overscroll. */
html[data-vp-ptr="coarse"] .app-main [style*="overflow-x:auto"],
html[data-vp-ptr="coarse"] .app-main [style*="overflow-x: auto"],
html[data-vp-ptr="coarse"] .app-main [style*="overflow:auto"],
html[data-vp-ptr="coarse"] .app-main .vp-wide {
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}

/* ── Fixed-width blocks that are wider than a phone ───────────────────────
 * A `width:900px` or `min-width:760px` inline style overflows the page and
 * takes the whole document with it (the body then scrolls sideways, so every
 * fixed element judders). Cap them. The specific widths enumerated are the
 * ones actually present in the codebase — a substring match on "min-width:"
 * alone would also catch the legitimate 24px avatar floors. */
/* The list is EXACTLY the inline min-widths of 320px and up that exist in
 * modules/*.js today (surveyed, not guessed — a substring match on "min-width:"
 * alone would also zero the legitimate 24px avatar and 56px badge floors and
 * collapse them). If you add a wide inline min-width, add it here or, better,
 * put .vp-wide on the element and let it scroll itself. */
html[data-vp="xs"] .app-main :is(
  [style*="min-width:320px"], [style*="min-width:330px"], [style*="min-width:340px"],
  [style*="min-width:500px"], [style*="min-width:520px"], [style*="min-width:600px"],
  [style*="min-width:640px"], [style*="min-width:660px"], [style*="min-width:720px"],
  [style*="min-width:860px"], [style*="min-width:900px"], [style*="min-width:920px"],
  [style*="min-width:1080px"]
):not(.vp-wide),
html[data-vp="sm"] .app-main :is(
  [style*="min-width:500px"], [style*="min-width:520px"], [style*="min-width:600px"],
  [style*="min-width:640px"], [style*="min-width:660px"], [style*="min-width:720px"],
  [style*="min-width:860px"], [style*="min-width:900px"], [style*="min-width:920px"],
  [style*="min-width:1080px"]
):not(.vp-wide),
html[data-vp="md"] .app-main :is(
  [style*="min-width:900px"], [style*="min-width:920px"], [style*="min-width:1080px"]
):not(.vp-wide) { min-width: 0 !important; }

/* Same treatment for the fixed `width:NNNpx` blocks — dropdowns, popovers,
 * preview panes and side rails authored at a desktop width. `max-width:100%`
 * rather than `width:auto`, so a component that genuinely needs its intrinsic
 * width keeps it right up to the edge of the screen. */
/* NOTE THE ANCHORING. `[style*="width:400px"]` is WRONG here: it also matches
 * `min-width:400px` and `max-width:400px`, and there are dozens of the latter in
 * modules/*.js — forcing `width:100%` onto an element that only declared a
 * max-width changes a layout that was already fine. So each value is matched
 * only as the FIRST declaration (`^`) or immediately after a semicolon, which is
 * how this codebase writes inline styles (`a:b;c:d`, no spaces). */
html[data-vp="xs"] .app-main :is(
  [style^="width:400px"], [style*=";width:400px"],
  [style^="width:410px"], [style*=";width:410px"],
  [style^="width:420px"], [style*=";width:420px"],
  [style^="width:440px"], [style*=";width:440px"],
  [style^="width:480px"], [style*=";width:480px"],
  [style^="width:520px"], [style*=";width:520px"],
  [style^="width:560px"], [style*=";width:560px"],
  [style^="width:720px"], [style*=";width:720px"]
):not(.vp-wide),
html[data-vp="sm"] .app-main :is(
  [style^="width:520px"], [style*=";width:520px"],
  [style^="width:560px"], [style*=";width:560px"],
  [style^="width:720px"], [style*=";width:720px"]
):not(.vp-wide) { max-width: 100% !important; width: 100% !important; }

/* ═══════════════════════════════════════════════════════════════════════════
 * 4 · OVERLAYS — modals, drawers, popovers
 *
 * A centred 520px dialog with 20px of backdrop padding needs 560px. Below that
 * it is clipped, and its footer buttons — which is where Save lives — go off
 * the bottom. On a phone the right shape is a bottom sheet: anchored to the
 * bottom edge (where the thumb is), full width, its own scroll, and its action
 * row pinned above the home indicator.
 * ═══════════════════════════════════════════════════════════════════════════ */

html[data-vp="xs"] .modal-bg,
html[data-vp="sm"] .modal-bg {
  padding: 0 !important;
  align-items: flex-end !important;
  overflow: hidden !important;
}
html[data-vp="xs"] .modal-bg > .modal,
html[data-vp="sm"] .modal-bg > .modal,
html[data-vp="xs"] .modal-bg > .sp-modal,
html[data-vp="sm"] .modal-bg > .sp-modal {
  max-width: none !important;
  width: 100% !important;
  max-height: 92vh !important;
  border-radius: 18px 18px 0 0 !important;
  animation: vpSheetUp .22s cubic-bezier(.4,0,.2,1);
}
@keyframes vpSheetUp { from { transform: translateY(12%); opacity: .85 } to { transform: translateY(0); opacity: 1 } }
@media (prefers-reduced-motion: reduce) {
  html[data-vp="xs"] .modal-bg > .modal, html[data-vp="sm"] .modal-bg > .modal,
  html[data-vp="xs"] .modal-bg > .sp-modal, html[data-vp="sm"] .modal-bg > .sp-modal,
  .sb-scrim { animation: none !important; }
}
html[data-vp="md"] .modal-bg > .modal,
html[data-vp="md"] .modal-bg > .sp-modal { max-width: min(560px, calc(100vw - 32px)) !important; }

html[data-vp="xs"] .modal-header, html[data-vp="sm"] .modal-header { padding: 14px 16px !important; }
html[data-vp="xs"] .modal-body,   html[data-vp="sm"] .modal-body   { padding: 14px 16px !important; overscroll-behavior: contain; }
html[data-vp="xs"] .modal-footer, html[data-vp="sm"] .modal-footer {
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom)) !important;
  border-radius: 0 !important;
  flex-wrap: wrap;
  gap: 8px !important;
}
/* A footer's buttons go full-width and stack, primary action LAST (bottom) so
 * it lands under the thumb. Nothing reorders — the DOM order already puts the
 * primary action last in every modal in this app. */
html[data-vp="xs"] .modal-footer > button,
html[data-vp="xs"] .modal-footer > .btn-r,
html[data-vp="xs"] .modal-footer > .btn-w { flex: 1 1 100%; justify-content: center; min-height: 44px; }

/* The 40px close button must stay reachable and obvious — backdrop
 * click-to-close is disabled platform-wide (modal-backdrop-lock.js), so the X
 * is the ONLY pointer way out of a modal. Never let it shrink. */
html[data-vp-ptr="coarse"] .sp-modal-close,
html[data-vp-ptr="coarse"] .sc-modal-x-pulse { min-width: 40px; min-height: 40px; }

/* ── The .ovl / .panel edit-drawer family (lead / deal / contact / add-lead) ──
 * A right-anchored panel with an author-chosen px width. Full-screen it. */
html[data-vp="xs"] .ovl, html[data-vp="sm"] .ovl { align-items: stretch !important; }
html[data-vp="xs"] .ovl > .panel,
html[data-vp="sm"] .ovl > .panel {
  width: 100% !important;
  max-width: none !important;
  height: 100% !important;
  min-height: 0;
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
}
html[data-vp="md"] .ovl > .panel { width: min(560px, 92vw) !important; max-width: none !important; }

/* Any other fixed-position card sized for a desktop: keep it on screen. This
 * catches the popovers, flyaround menus and mini-panels that set their own
 * `position:fixed;width:NNNpx` inline — of which there are dozens. */
html[data-vp="xs"] #app [style*="position:fixed"]:not(#topbar):not(#moduleBar):not(#sidebar):not(#toasts):not(#bottomNav):not(.sb-scrim):not(.modal-bg):not(.ovl):not(.comms-dock):not(.comms-dock-mini):not(.vp-wide),
html[data-vp="sm"] #app [style*="position:fixed"]:not(#topbar):not(#moduleBar):not(#sidebar):not(#toasts):not(#bottomNav):not(.sb-scrim):not(.modal-bg):not(.ovl):not(.comms-dock):not(.comms-dock-mini):not(.vp-wide) {
  max-width: calc(100vw - 16px) !important;
  max-height: calc(100vh - 24px);
  overflow: auto;
}

/* ── Body-level fixed overlays ────────────────────────────────────────────
 * The signature nudge (12k-settings-my-signature.js) is appended to <body> at
 * `top:0; z-index:10500` — over the top of everything, which on a desktop means
 * over the 40px module bar and is fine. At 344px it wrapped to five lines and
 * buried the topbar, the page title, and the top of the nav drawer, including
 * the drawer's module switcher. Drop it below the chrome (whose height
 * _spSyncChrome publishes as --sp-chrome-h, because it changes per tier), take
 * it out of the drawer's and the modals' way, and let its own row wrap. */
/* Bottom-anchored on a phone, not top.
 *
 * Dropping it below the chrome stopped it burying the topbar, but it still sat on
 * top of the page hero — the h1 and the "7 active · 2 in Pre-Install" summary line
 * were behind it on every page. A position:fixed element appended to <body>
 * cannot participate in flow, so there is nothing to push down; the only real
 * choices are "overlay something" or "overlay nothing". At the bottom it overlays
 * nothing, which is also where every mobile app puts a nudge — within thumb reach
 * of both its call-to-action and its dismiss. */
html[data-vp-nav="drawer"] #sig-banner-root {
  top: auto !important;
  bottom: calc(env(safe-area-inset-bottom) + 8px) !important;
  left: 8px !important; right: 8px !important;
  z-index: 19 !important;              /* under the topbar (20), modals (60), drawer (70) */
  padding: 0;
}
/* …and the collapsed comms dock chip lifts above it, or the nudge covers the one
 * control that answers an incoming call. `:has()` means this only applies while
 * the banner is actually up — it disappears for good once a signature is saved. */
html[data-vp-nav="drawer"]:has(#sig-banner-root) .comms-dock-mini {
  bottom: calc(env(safe-area-inset-bottom) + 84px) !important;
}
html[data-vp-nav="drawer"] #sig-banner-root > * { flex-wrap: wrap; max-width: none !important; }
/* At xs the explainer line is what forces the five-line wrap. The headline plus
 * the call to action still says everything the nudge needs to say. */
html[data-vp="xs"] #sig-banner-root > * > div:nth-child(2) > div:nth-child(2) { display: none; }

/* Hover-only tooltips (.etrack-tip and friends) cannot be dismissed on touch
 * and a `white-space:nowrap` one hangs off the screen. Suppress them where
 * there is no hover; the information they carry is always also in the row. */
@media (hover: none) {
  .etrack .etrack-tip { display: none !important; }
}


/* ═══════════════════════════════════════════════════════════════════════════
 * 5 · TOUCH TARGETS + TYPOGRAPHY
 * ═══════════════════════════════════════════════════════════════════════════ */

@media (pointer: coarse) {
  /* 44px is the HIG minimum. Applied as min-height + centring rather than a
   * fixed height, so a button with wrapped label grows instead of clipping. */
  #app :where(.btn-r, .btn-w, .btn-g, .sp-btn, .pill, button.mod-tab, .nav-item, .tb-icon-btn, .tb-navbtn) {
    min-height: 40px;
  }
  html[data-vp="xs"] #app :where(.btn-r, .btn-w, .btn-g, .sp-btn, .pill) { min-height: 44px; padding-left: 14px; padding-right: 14px; }
  #app :where(.tb-icon-btn, .sp-btn-icon) { min-width: 40px; }
  /* The families above are the ones with a class. Everything else is a bare
   * <button> emitted inline, and a sweep at 344px with a touch pointer found 27
   * of them under a finger's width on the Sales dashboard alone — 24 of those
   * one component, the insights card-header icon buttons at 26×40. A floor on
   * the bare element catches all of it. It is a floor, not a size: a button
   * already larger keeps its own dimensions, and a wide-but-short button only
   * grows in the axis that was too small. */
  /* ⚠ min-HEIGHT only. A `min-width` here is not a floor, it is a REPLACEMENT:
   * `html[data-vp="xs"] #app button` outranks `.dash-seg-btn`, so setting
   * min-width:36px on every button SHRANK the dashboard branch selector's own
   * `min-width:128px` to 36 — the four segments collapsed to ~75px each, and
   * their nowrap sub-lines ("4 leads this wk · $3,334 won") overflowed and
   * printed on top of one another. It made a control unreadable in the name of
   * making it tappable.
   *
   * Height is safe to floor this way because almost nothing declares a
   * min-height on a button, and a taller button is never wrong. Width comes from
   * content plus padding, so where a button is genuinely too NARROW it is an
   * icon-only one, and those are handled by class above (.tb-icon-btn,
   * .sp-btn-icon) and per-component (.ins2-card-h-btn in js/insights-v2.css). */
  #app button { min-height: 32px; }
  html[data-vp="xs"] #app button, html[data-vp="sm"] #app button { min-height: 36px; }

  /* The width floor, wrapped so the WHOLE selector has zero specificity.
   *
   * `:where(...)` contributes nothing, so any authored rule — even a single class
   * — beats this. That is exactly what a default floor should do, and it is the
   * only way to express "36px unless the component says otherwise" in CSS.
   *
   * Written as a normal selector first, it did the opposite: `html[data-vp="xs"]
   * #app button` outranks `.dash-seg-btn`, so a min-width meant as a floor
   * REPLACED the dashboard branch selector's own `min-width:128px` with 36px. The
   * four segments collapsed to ~75px and their nowrap sub-lines ("4 leads this wk
   * · $3,334 won") overflowed and printed on top of one another — a control made
   * unreadable in the name of making it tappable. Removing the floor instead
   * traded that for 98 genuinely-too-narrow icon buttons across the app. Zero
   * specificity is the answer to both. */
  :where(#app button) { min-width: 32px; }
  :where(html[data-vp="xs"] #app button, html[data-vp="sm"] #app button) { min-width: 36px; }
  /* A 20px checkbox is a mis-tap; grow the hit area without growing the box. */
  #app input[type="checkbox"], #app input[type="radio"] { min-width: 20px; min-height: 20px; }
}

/* The app leans on 10-11px type for metadata. On a phone at arm's length that
 * is unreadable, so floor the smallest sizes — but ONLY inside the page body:
 * badges, pills and the eyebrow labels are deliberately tiny and floor at 11. */
html[data-vp="xs"] .app-main { font-size: 14px; }

/* The sizes below are set INLINE (252 uses of 9px, 84 of 9.5px, 1344 of 10px,
 * 292 of 10.5px), so a plain rule — even one using max() — cannot touch them:
 * an inline declaration beats any selector. Hence the attribute match, and
 * hence !important. Floored at 11px rather than 12: 11px is where this app's
 * own metadata type already sits (2237 uses), so raising the four smallest
 * sizes to it changes the reading experience without reflowing every dense row
 * that was designed around 11px. The badge/tag/eyebrow families are left at
 * their own size — they are labels, not text to read. */
html[data-vp="xs"] .app-main :is(
  [style*="font-size:9px"], [style*="font-size:9.5px"],
  [style*="font-size:10px"], [style*="font-size:10.5px"]
):not(.bdg):not(.tag):not(.eyebrow):not(.pill):not(.sp-pill):not(.conv-sentiment) { font-size: 11px !important; }
html[data-vp="xs"] .app-main :where(.bdg, .tag, .eyebrow) { font-size: 11px !important; }
html[data-vp="xs"] .app-main :where(h1, .num-xl) { font-size: 22px !important; }
html[data-vp="xs"] .app-main :where(h2, .num-lg) { font-size: 18px !important; }
html[data-vp="xs"] .app-main :where(h3) { font-size: 15px !important; }

/* Card padding: 20-24px a side inside a 324px content box leaves 276px. */
html[data-vp="xs"] .app-main :where(.card, .sp-card, .modal, .panel) { border-radius: 12px; }
/* Matched as a COMPLETE declaration only — `[style*="padding:24px"]` also matches
 * `padding:24px 20px`, and rewriting that to one value silently throws away the
 * asymmetry the author chose. Same for gap vs row-gap/column-gap. */
html[data-vp="xs"] .app-main :is(
  [style^="padding:24px;"], [style*=";padding:24px;"], [style$="padding:24px"]
):not(.vp-wide) { padding: 14px !important; }
html[data-vp="xs"] .app-main :is(
  [style^="padding:20px;"], [style*=";padding:20px;"], [style$="padding:20px"]
):not(.vp-wide) { padding: 13px !important; }
html[data-vp="xs"] .app-main :is(
  [style^="gap:24px;"], [style*=";gap:24px;"], [style$="gap:24px"]
) { gap: 12px !important; }
html[data-vp="xs"] .app-main :is(
  [style^="gap:20px;"], [style*=";gap:20px;"], [style$="gap:20px"]
) { gap: 12px !important; }


/* ═══════════════════════════════════════════════════════════════════════════
 * 6 · SHORT VIEWPORTS  (the Fold cover screen in LANDSCAPE is 882×344)
 *
 * 96px of fixed chrome on a 344px-tall viewport is 28% of the screen spent on
 * navigation before any content paints. _spSyncChrome already shrinks the bars;
 * these rules take the vertical air out of the page itself.
 * ═══════════════════════════════════════════════════════════════════════════ */

html[data-vp-h="short"] .app-main { padding-top: 10px !important; padding-bottom: 10px !important; }
html[data-vp-h="short"] .app-main .page-hero { margin-bottom: 10px !important; padding-bottom: 8px !important; }
html[data-vp-h="short"] .app-main .page-sub { display: none; }
html[data-vp-h="short"] .app-main :where(h1, .page-title) { font-size: 18px !important; }
/* A dialog on a 344px-tall screen has ~300px of usable height; make the body
 * the only thing that scrolls and keep the action row visible. */
html[data-vp-h="short"] .modal-bg > .modal,
html[data-vp-h="short"] .modal-bg > .sp-modal { max-height: calc(100vh - 16px) !important; }
html[data-vp-h="short"] .modal-header { padding-top: 10px !important; padding-bottom: 10px !important; }
html[data-vp-h="short"] .modal-footer { padding-top: 8px !important; padding-bottom: 8px !important; }
html[data-vp-h="short"] .kanban-col { max-height: calc(100vh - 120px) !important; }


/* ═══════════════════════════════════════════════════════════════════════════
 * 7 · THE HINGE
 *
 * On a book-style foldable held open there is a physical seam down the middle
 * of the viewport. Anything centred across it is bisected — a KPI number, a
 * button label, a photo. The Viewport Segments media features tell us the
 * viewport is split; env(viewport-segment-*) tells us where.
 *
 * The layout response is deliberately conservative: we do NOT try to build a
 * two-pane "spanning" layout (that needs per-page design work and gets it wrong
 * the moment the user rotates). We keep the single-pane layout and make sure
 * nothing important lands on the fold — the page gutters widen to the seam, and
 * multi-column grids get a gap wide enough to straddle it.
 *
 * Every value degrades safely: env(viewport-segment-*) resolves to its fallback
 * on every browser that does not implement it, and the whole block is fenced
 * behind a media query that only a real foldable matches.
 * ═══════════════════════════════════════════════════════════════════════════ */

@media (horizontal-viewport-segments: 2) {
  /* Width of the seam = where segment 1 starts minus where segment 0 ends. */
  :root {
    --vp-fold-left:  env(viewport-segment-right 0 0, 50%);
    --vp-fold-right: env(viewport-segment-left 1 0, 50%);
    --vp-fold-w: calc(var(--vp-fold-right) - var(--vp-fold-left));
  }
  /* Two-column content straddles the seam instead of being cut by it. */
  html[data-vp-fold="h"] .app-main [style*="grid-template-columns"],
  html[data-vp-fold="h"] .app-main .sp-grid-2,
  html[data-vp-fold="h"] .app-main .sp-grid-3,
  html[data-vp-fold="h"] .app-main .sp-grid-4 { column-gap: max(16px, var(--vp-fold-w)) !important; }
  /* A dialog sits inside ONE segment rather than being folded in half. */
  html[data-vp-fold="h"] .modal-bg > .modal,
  html[data-vp-fold="h"] .modal-bg > .sp-modal {
    max-width: calc(var(--vp-fold-left) - 16px) !important;
    margin-right: auto;
  }
  /* The drawer opens inside the left segment. */
  html[data-vp-fold="h"] .sb-drawer { width: calc(var(--vp-fold-left) - 24px) !important; }
}

@media (vertical-viewport-segments: 2) {
  :root {
    --vp-fold-top:    env(viewport-segment-bottom 0 0, 50%);
    --vp-fold-bottom: env(viewport-segment-top 0 1, 50%);
  }
  /* Flip-style fold — a horizontal seam across the middle. Keep the action row
   * of a sheet below it so the thumb half of the screen owns the controls. */
  html[data-vp-fold="v"] .modal-bg > .modal,
  html[data-vp-fold="v"] .modal-bg > .sp-modal { max-height: calc(100vh - var(--vp-fold-bottom)) !important; }
}


/* ═══════════════════════════════════════════════════════════════════════════
 * 8 · KNOWN WIDE SURFACES
 *
 * Boards and multi-pane layouts that are intentionally wider than a phone.
 * Each gets an explicit, considered treatment rather than being caught by a
 * generic rule — that is the difference between "it scrolls" and "it works".
 * ═══════════════════════════════════════════════════════════════════════════ */

/* ── Kanban boards (.kanban / .kanban-col, deals + production) ─────────────
 * A column is 264-300px, so at 344px exactly one fits. Snap-scroll it so a
 * swipe lands on a whole column rather than resting between two, and let the
 * column use the full width. */
html[data-vp="xs"] .kanban, html[data-vp="sm"] .kanban {
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  gap: 10px !important;
  padding-bottom: 6px;
}
html[data-vp="xs"] .kanban-col, html[data-vp="sm"] .kanban-col {
  scroll-snap-align: center;
  min-width: calc(100vw - 32px) !important;
  max-width: calc(100vw - 32px) !important;
  max-height: calc(100vh - 210px) !important;
}
html[data-vp="md"] .kanban-col { min-width: 264px !important; max-width: 264px !important; }
/* Drag-and-drop is a pointer idiom. On touch the board is read-only unless the
 * page provides its own long-press handler — so make the cards obviously
 * tappable-not-draggable and stop the browser hijacking the swipe. */
@media (hover: none) {
  .kanban-col, .kanban { touch-action: pan-x pan-y; }
}

/* ── The email client ──────────────────────────────────────────────────────
 * NOTE: there used to be a `.mail-grid` block here. It was DEAD — `.mail-grid` is
 * declared in spartan-tokens.css and emitted by no renderer; the real client
 * builds its own grid inline in 11-email-page.js. A dead rule that looks like
 * coverage is worse than no rule at all, because the next person reads it and
 * believes this surface is handled.
 *
 * The real handling is in the renderer: xs|sm get renderEmailMobile
 * (_spUsePhoneLayout), and md drops the 208px folder rail and runs two panes —
 * three panes need ~750px before the reading pane has any width. This rule just
 * removes the rail from the DOM flow to match the two-column track. */
html[data-vp="md"] #emailFolderRail { display: none !important; }

/* ── Month/week calendar grids (.cal-head / .cal-body, repeat(7,1fr)) ──────
 * Seven columns is kept — a month grid IS seven columns, and 344/7 = 49px per
 * cell, which is exactly what every phone calendar uses. What has to give is
 * the padding, the minimum body height and the event chips. */
html[data-vp="xs"] .cal-body, html[data-vp="sm"] .cal-body { min-height: 300px !important; }
html[data-vp="xs"] .cal-head > *, html[data-vp="sm"] .cal-head > * { padding: 6px 2px !important; font-size: 10px !important; }
html[data-vp="xs"] .cal-cell, html[data-vp="sm"] .cal-cell { padding: 3px !important; min-height: 44px; }
html[data-vp="xs"] .cal-evt, html[data-vp="sm"] .cal-evt { font-size: 9.5px !important; padding: 1px 3px !important; }

/* ── The shared detail-view action row (.ed-actions) ──────────────────────
 * Deal, Lead and Contact detail all render through renderEntityDetail, so this
 * one row is the header actions on all three. It is authored `flex-shrink:0`,
 * which is right on a wide screen (Delete must not squash into "Dele…") and
 * fatal on a narrow one. Given a full line of its own it fits comfortably. */
html[data-vp="xs"] .ed-actions,
html[data-vp="sm"] .ed-actions,
html[data-vp="md"] .ed-actions {
  flex: 1 1 100%;
  flex-shrink: 1 !important;
  flex-wrap: wrap;
  row-gap: 8px;
  min-width: 0;
}
html[data-vp="xs"] .ed-actions > *, html[data-vp="sm"] .ed-actions > * { min-width: 0; }

/* ── Stage/step trackers (.stages, .stage-bub + 48px .stage-link) ─────────
 * Seven 36px bubbles joined by 48px links is 588px. Wrap it and shorten the
 * links; the sequence still reads left-to-right, over two or three rows. */
html[data-vp="xs"] .stages, html[data-vp="sm"] .stages { flex-wrap: wrap !important; gap: 6px !important; }
html[data-vp="xs"] .stage-link, html[data-vp="sm"] .stage-link { width: 16px !important; }
/* A stage LABEL is up to "Not Proceeding" — 98px of nowrap text under a 36px
 * bubble, which is what actually decided the row's width once the bubbles were
 * allowed to wrap. Let it wrap onto two lines under its own bubble. */
html[data-vp="xs"] .stage-label, html[data-vp="sm"] .stage-label {
  white-space: normal;
  max-width: 76px;
  line-height: 1.25;
  text-align: center;
}

/* ── Settings tab rails ───────────────────────────────────────────────────
 * Job Settings stacks its own rail in the renderer (it needed the tab buttons to
 * become a horizontal strip, which CSS alone cannot do to a column of blocks).
 * The tab BUTTONS still need to stop being 100%-wide blocks once the rail is a
 * row, and a strip that is wider than the screen needs to scroll without a
 * visible scrollbar eating 4px of a 40px-tall control. */
html[data-vp="xs"] .app-main [style*="width:max-content"] > button,
html[data-vp="sm"] .app-main [style*="width:max-content"] > button {
  width: auto !important;
  white-space: nowrap;
  flex-shrink: 0;
}
html[data-vp="xs"] .app-main [style*="overflow-x:auto"]::-webkit-scrollbar,
html[data-vp="sm"] .app-main [style*="overflow-x:auto"]::-webkit-scrollbar { height: 0; }

/* ── The comms dock (floating call/chat panel) ─────────────────────────────
 * Sized as a desktop side panel. On a phone it becomes a bottom sheet, clear
 * of the home indicator and of the native wrapper's bottom nav (which
 * mobile-chrome.js already offsets — this must not fight it, so it only
 * applies where there is no bottom nav). */
html[data-vp="xs"] .comms-dock, html[data-vp="sm"] .comms-dock {
  left: 8px !important; right: 8px !important;
  width: auto !important; max-width: none !important;
  bottom: calc(env(safe-area-inset-bottom) + 8px) !important;
  max-height: 76vh !important;
  border-radius: 16px !important;
}
html[data-vp="xs"] .comms-dock-mini, html[data-vp="sm"] .comms-dock-mini {
  right: 10px !important; left: auto !important;
  bottom: calc(env(safe-area-inset-bottom) + 10px) !important;
}
/* .inbox-panel and .thread-panel are 400x580 side panels pinned `right: 96px` —
 * i.e. 496px of horizontal space before they even start, on a 344px screen. Same
 * bottom-sheet treatment as the dock itself. */
html[data-vp="xs"] :where(.inbox-panel, .thread-panel),
html[data-vp="sm"] :where(.inbox-panel, .thread-panel) {
  left: 8px !important; right: 8px !important;
  width: auto !important; max-width: none !important;
  bottom: calc(env(safe-area-inset-bottom) + 8px) !important;
  height: auto !important; max-height: 78vh !important;
  border-radius: 16px !important;
}
/* A-679 — but NOT when the comms dock is in its own phone mode.
 *
 * The block above and comms-dock.css's `.cd-mobile` rule are two competing
 * "make this phone-friendly" implementations of the same panel, and this one was
 * winning: every property here is `!important` and responsive.css is loaded LAST
 * by contract, so it beat the dock's full-screen sheet outright. The result on a
 * 412x915 handset, measured: left 8, width 396, height 714 — because
 * `left/right: 8px` gives 396, and `max-height: 78vh` of 915 gives 713.7. That is
 * the reported "call box is floating": an inset rounded card with the keypad's
 * bottom row clipped and the page showing underneath.
 *
 * Note this fired INSIDE the APK too — it keys on the `data-vp` tier, i.e. width,
 * not on native detection — so it was never only a mobile-web problem.
 *
 * The dock's own treatment is the better one for a phone (true full-screen, safe
 * areas, squared corners, slide-up), so where `.cd-mobile` is present this layer
 * stands down and hands the panel back. Everything else on xs/sm is untouched,
 * and desktop never matches either selector. Same `!important` weight is required
 * to undo the block above. */
html[data-vp="xs"] .cd-mobile :where(.inbox-panel, .thread-panel),
html[data-vp="sm"] .cd-mobile :where(.inbox-panel, .thread-panel) {
  left: 0 !important; right: 0 !important; top: 0 !important; bottom: 0 !important;
  width: 100vw !important; max-width: none !important;
  height: 100dvh !important; max-height: 100dvh !important;
  border-radius: 0 !important;
}

/* Its tab strip is `flex:1` per tab with 11px labels — five tabs in 328px is 65px
 * each, which truncates every label. Scroll instead. */
html[data-vp="xs"] .inbox-panel .ip-tabs,
html[data-vp="sm"] .inbox-panel .ip-tabs {
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
html[data-vp="xs"] .inbox-panel .ip-tabs::-webkit-scrollbar { height: 0; }
html[data-vp="xs"] .inbox-panel .ip-tab,
html[data-vp="sm"] .inbox-panel .ip-tab { flex: 0 0 auto !important; white-space: nowrap; }

/* The full-width active-call banner is a fixed flex row of five children with no
 * wrap (28-twilio.js #activeCallPanel). It is the surface someone uses WHILE on a
 * call, so its Hang Up must never be the thing that goes off the edge. */
html[data-vp="xs"] #activeCallPanel, html[data-vp="sm"] #activeCallPanel,
html[data-vp="xs"] #incomingCallBanner, html[data-vp="sm"] #incomingCallBanner {
  flex-wrap: wrap !important;
  gap: 8px !important;
  padding: 8px 10px !important;
  padding-top: calc(env(safe-area-inset-top) + 8px) !important;
}
html[data-vp="xs"] #activeCallPanel > *, html[data-vp="sm"] #activeCallPanel > * { min-width: 0; }

/* The V2 email composer header — five children, no wrap. */
html[data-vp="xs"] .ecx-head, html[data-vp="sm"] .ecx-head {
  flex-wrap: wrap; gap: 8px !important; padding: 10px 14px !important;
}

/* ── The top banner bars (email reminders, response watch, mgmt alerts) ────
 * Each is a full-width fixed strip of nowrap text with actions on the right.
 * Stacked, three of them eat 150px of a phone screen; and they are ambient
 * information, not tasks. Keep the highest-priority one (management alerts,
 * which is an escalation) and fold the other two away below md. */
html[data-vp="xs"] :where(.email-reminders-bar, .response-watch-bar),
html[data-vp="sm"] :where(.email-reminders-bar, .response-watch-bar) { display: none !important; }
html[data-vp="xs"] .management-alerts-bar, html[data-vp="sm"] .management-alerts-bar {
  flex-wrap: wrap !important;
  padding: 8px 10px !important;
  font-size: 12px !important;
}

/* ── Gantt / timeline / board scrollers ───────────────────────────────────
 * These set a large explicit width because the time axis IS wide. Let them
 * keep it, but make sure the scroll is contained to the element so swiping the
 * board never drags the page sideways underneath it. */
html[data-vp-ptr="coarse"] :where(.gantt, .gantt-scroll, .runs, .tline) {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}

/* ── Maps ─────────────────────────────────────────────────────────────────
 * A map with an inline `height:600px` on a 344×882 screen leaves no room for
 * the list beside it. Cap by viewport instead of by pixels, and — since a
 * one-finger drag on a map inside a scrolling page traps the page scroll —
 * require the standard two-finger gesture. */
/* Anchored for the same reason as the widths above — `max-height:500px` and
 * `min-height:480px` both exist in the codebase and must not be turned into a
 * fixed `height`. */
html[data-vp="xs"] .app-main :is(
  [style^="height:600px"], [style*=";height:600px"],
  [style^="height:560px"], [style*=";height:560px"],
  [style^="height:520px"], [style*=";height:520px"],
  [style^="height:500px"], [style*=";height:500px"],
  [style^="height:480px"], [style*=";height:480px"]
):not(.vp-wide) { height: min(52vh, 380px) !important; }


/* ═══════════════════════════════════════════════════════════════════════════
 * 9 · UTILITIES — for renderers that need to opt in or out explicitly
 * ═══════════════════════════════════════════════════════════════════════════ */

.vp-desktop-only { display: revert; }
html[data-vp="xs"] .vp-desktop-only,
html[data-vp="sm"] .vp-desktop-only,
html[data-vp="md"] .vp-desktop-only { display: none !important; }

.vp-narrow-only { display: none; }
html[data-vp="xs"] .vp-narrow-only,
html[data-vp="sm"] .vp-narrow-only,
html[data-vp="md"] .vp-narrow-only { display: revert !important; }

/* Mark a container as legitimately wider than the viewport: it scrolls itself
 * and is exempt from every clamp above. */
.vp-wide { overflow-x: auto; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; max-width: 100%; }

/* A "swipe me" cue for horizontal scrollers, so a table with columns off-screen
 * doesn't just look truncated. Pure CSS, no JS, no layout cost. */
html:not([data-vp="xl"]) .app-main :is(div, section, form):has(> table),
html[data-vp-ptr="coarse"] .vp-wide {
  background-image: linear-gradient(to left, rgba(10,10,10,.055), rgba(10,10,10,0) 26px);
  background-position: right center;
  background-repeat: no-repeat;
  background-size: 26px 100%;
  background-attachment: local, scroll;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * 10 · PRINT — unrelated to foldables, but this is the last stylesheet and
 * the chrome must come off a printed page. Quotes, invoices and pick lists
 * get printed from these screens every day.
 * ═══════════════════════════════════════════════════════════════════════════ */

@media print {
  #moduleBar, #topbar, #sidebar, .sb-scrim, #bottomNav, #toasts,
  .comms-dock, .comms-dock-mini, .tb-burger { display: none !important; }
  .app-main { margin: 0 !important; padding: 0 !important; }
  .app-page-wrap { max-width: none !important; }
}
