/* ═════════════════════════════════════════════════════════════════════════════════════
   MOBILE OFFICE — PHONE SHELL FOR ADMIN / DISPATCHER / BOOKKEEPER (Phases 0–2)
   ─────────────────────────────────────────────────────────────────────────────────────
   Design + phasing: docs/mobile-office-view-proposal-2026-08-27.md (approved 2026-08-27).
   Mockups the visual language comes from: docs/mobile-office-mockups/.

   SCOPING CONTRACT — the technician layer's contract (styles.css "MOBILE — TECHNICIAN
   FIELD WORKFLOW" header), mirrored for the second audience:

     1. ONE media query. Every rule in this file lives inside @media (max-width: 767.98px)
        — the same boundary the technician layer owns, chosen deliberately so the two
        audiences share one definition of "phone". At ≥768px this file is inert; the
        <link> tag also carries media="(max-width: 767.98px)" so desktop sessions don't
        even block on it.
     2. TWO body classes, both required on every rule:
          body.mobile-office  — the AUDIENCE. Written by _applyRoleBodyClass() (auth.js)
                                for signed-in non-technician roles only; cleared on any
                                auth screen. Role-driven, never viewport-driven — the
                                exact shape of body.role-technician.
          body.mo-active      — the SHELL IS MOUNTED. Added/removed only by
                                mobile-office.js once it has verified the audience class,
                                the viewport, and the per-device opt-out, and has rendered
                                #mo-root. Keying the chrome-hiding rules off mo-active
                                (not off audience+width alone) means a failed script load
                                fails SAFE: the user gets today's desktop-at-phone-width,
                                never a hidden chrome with no shell behind it.
        A technician can carry neither class: the writer toggles mobile-office off for
        role === 'technician' in the same statement that toggles role-technician on.
     3. NO shared selector is restyled — the shell hides desktop chrome (display:none,
        no !important needed at 0,3,1 specificity) and renders its OWN #mo-root DOM.
        The desktop surfaces this file touches beyond hiding are ADOPTIONS, geometry
        only: the universal search modal (#gh-search-modal, full-screen — body-level
        and shared by design), the ticket panel (Phase 1's takeover, below), the six
        dispatch dialogs Phase 2's guided flows raise (#bm-dest-modal /
        #bm-confirm-modal / #day-note-modal / #confirmed-route-change-modal /
        #route-changes-modal / #lock-reassign-modal, bottom-sheeted), the bulk bar's
        undo state, and the multi-tab banner (suppressed: a phone plus a desk tab is
        this audience's NORMAL state, and the banner is warn-only by design). The
        three customer-notify buttons those dialogs carry are hidden here and refused
        engine-side — the SMS ceiling, pinned in mobile-office-gate.test.js §6.
     4. TOKENS ONLY. Every color is a var(--…) from styles.css's core token block, so
        the Carbon dark theme (body[data-theme='dark'] remaps the tokens) applies to the
        shell with zero rules here — same discipline the dark theme documents for itself.
     5. z-index: the shell owns the 60–199 band (nav 190, sheet scrim 180). Everything
        the app already draws above 200 — dropdowns, settings, header, modals, toasts,
        boot loader — paints over the shell. "Modal wins" is the house convention.

   Tests that pin this file's contract: tests/mobile-office-gate.test.js (class writers,
   scoping), tests/e2e/layout/mobile-office/*.spec.js (rendered geometry at 390×844 and
   320×780, light and dark).
   ═══════════════════════════════════════════════════════════════════════════════════ */

@media (max-width: 767.98px) {
  /* ── Chrome swap: desktop shell out, #mo-root in ─────────────────────────────────
     Hidden, not unmounted: the desktop views keep rendering into display:none DOM so
     switchView / realtime / ss_last_view semantics stay byte-identical underneath. */
  body.mobile-office.mo-active .global-header,
  body.mobile-office.mo-active #app-body .sidebar,
  body.mobile-office.mo-active #app-body .main-content {
    display: none;
  }
  /* The multi-tab presence banner (multi-tab.js injects #multi-tab-warning with inline
     styles — display:flex in style.cssText — so only !important can override it). It is
     warn-only by contract, and a phone plus a desk tab is this audience's NORMAL state;
     suppressing it here is the proposal's §7.4 decision, scoped to the mounted shell. */
  body.mobile-office.mo-active #multi-tab-warning {
    display: none !important;
  }

  /* The update notice (#version-drift-bar, version-watch.js) is NOT suppressed here — unlike the
     multi-tab banner it is not warn-only, and the phone office shell is exactly the kind of tab
     that stays open for days. It only has to move: app-wide the card rests at bottom:24px, and
     this shell owns the bottom of the screen with .mo-nav (56px of tab bar plus the safe-area
     inset, absolutely positioned inside #mo-root). A card at 24px would sit on the tabs.
     version-watch.js writes its own position inline — it must render with no stylesheet at all —
     so the anchors it reads are custom properties, which is the one thing a rule here can still
     re-point underneath an inline style. Full width, because at this size a 344px card anchored
     to one edge reads as a stray toast. */
  body.mobile-office.mo-active {
    --vw-notice-left: 12px;
    --vw-notice-right: 12px;
    --vw-notice-w: auto;
    --vw-notice-bottom: calc(68px + env(safe-area-inset-bottom, 0px));
  }

  body.mobile-office.mo-active #mo-root {
    display: flex;
    flex-direction: column;
    position: fixed;
    inset: 0;
    z-index: 60;
    background: var(--gray-100);
    font-size: 14px;
    color: var(--text-primary);
    overflow: hidden;
  }

  /* ── App bar (the global header's 48px language) ───────────────────────────────── */
  body.mobile-office.mo-active .mo-appbar {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
    height: calc(48px + env(safe-area-inset-top, 0px));
    padding: env(safe-area-inset-top, 0px) 8px 0 16px;
    background: var(--surface-header, var(--surface-base));
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
    position: relative;
    z-index: 2;
  }
  body.mobile-office.mo-active .mo-appbar-title {
    flex: 1;
    min-width: 0;
    font-size: 16px;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--gray-900);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  body.mobile-office.mo-active .mo-brand {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 1;
    min-width: 0;
  }
  body.mobile-office.mo-active .mo-brand-badge {
    width: 24px;
    height: 24px;
    border-radius: 6px;
    background: var(--blue);
    color: #fff;
    display: grid;
    place-items: center;
    flex-shrink: 0;
  }
  body.mobile-office.mo-active .mo-brand-badge svg {
    width: 14px;
    height: 14px;
  }
  /* The company's own uploaded mark, in the badge's box. object-fit:contain (never cover) so a
     wordmark or a non-square logo is shown whole rather than cropped — the same treatment
     .gh-brand-logo gets in the desktop header. No background of its own: a logo brings one. */
  body.mobile-office.mo-active .mo-brand-logo {
    width: 24px;
    height: 24px;
    border-radius: 6px;
    object-fit: contain;
    display: block;
    flex-shrink: 0;
  }
  body.mobile-office.mo-active .mo-brand-name {
    font-size: 13px;
    font-weight: 700;
    color: var(--gray-900);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  body.mobile-office.mo-active .mo-iconbtn {
    position: relative;
    width: 44px;
    height: 44px;
    display: grid;
    place-items: center;
    border: 0;
    border-radius: var(--radius);
    background: transparent;
    color: var(--gray-600);
    cursor: pointer;
    flex-shrink: 0;
  }
  body.mobile-office.mo-active .mo-iconbtn svg {
    width: 20px;
    height: 20px;
  }
  body.mobile-office.mo-active .mo-iconbtn:active {
    background: var(--gray-100);
  }
  body.mobile-office.mo-active .mo-badge {
    position: absolute;
    top: 5px;
    right: 5px;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    border-radius: 8px;
    background: var(--red);
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    line-height: 16px;
    text-align: center;
  }
  body.mobile-office.mo-active .mo-avatar {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: var(--blue);
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    display: grid;
    place-items: center;
  }

  /* ── Scroll region ─────────────────────────────────────────────────────────────── */
  /* overscroll-behavior-y: contain — MEASURED as `auto` on html, body AND this
     element (2026-09-07). With the chain open, dragging past the top of any list
     reaches the ROOT scroller, and on Android Chrome that means pull-to-refresh:
     a full SPA reload — a cold boot, every store re-fetched, whatever the user was
     part-way through gone. From the user's side that reads as "the app randomly
     restarted", which is why it never got reported as a scroll bug. .mo-dp-sheet
     already contained its own overscroll; the surface a phone spends all day
     scrolling did not. */
  body.mobile-office.mo-active .mo-scroll {
    flex: 1;
    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior-y: contain;
    -webkit-overflow-scrolling: touch;
    padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px) + 16px);
  }

  /* ── Bottom navigation (the sidebar's active language, rotated to a tab bar) ───── */
  body.mobile-office.mo-active .mo-nav {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 190;
    display: flex;
    background: var(--surface-base);
    border-top: 1px solid var(--border);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06);
  }
  body.mobile-office.mo-active .mo-nav-btn {
    flex: 1;
    height: 56px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    border: 0;
    background: transparent;
    color: var(--gray-500);
    font-family: inherit;
    font-size: 10.5px;
    font-weight: 600;
    cursor: pointer;
    position: relative;
  }
  body.mobile-office.mo-active .mo-nav-btn svg {
    width: 22px;
    height: 22px;
  }
  /* --blue, NOT --blue-dark. Both tokens are themed, but only one of them clears 4.5:1 on the
     nav surface in BOTH themes, and this label is 10.5px/600 — normal text, so 4.5 is the bar.
     Measured in a real browser at 375px:
       --blue-dark  light #1d4ed8 on #fff 6.70:1 PASS | dark #2563eb on #18181b 3.43:1 FAIL
       --blue       light #2563eb on #fff 5.17:1 PASS | dark #3b82f6 on #18181b 4.80:1 PASS
     "blue-dark" is a blue darkened for a LIGHT background; as text on a dark surface it is the
     wrong end of the ramp, which is why the ACTIVE tab read DARKER than its inactive neighbours
     (--gray-500, 5.29:1) — the one control that must read as selected was the least legible in
     the bottom bar. Fixed with a token swap rather than a [data-theme] block: this sheet
     deliberately contains none, and re-stating a colour under a theme selector is exactly how a
     dark override gets inverted. ::before below already uses --blue, so the active label and its
     indicator now come from one token. */
  body.mobile-office.mo-active .mo-nav-btn.active {
    color: var(--blue);
  }
  body.mobile-office.mo-active .mo-nav-btn.active::before {
    content: '';
    position: absolute;
    top: 0;
    width: 32px;
    height: 3px;
    border-radius: 0 0 3px 3px;
    background: var(--blue);
  }
  body.mobile-office.mo-active .mo-nav-btn .mo-badge {
    top: 4px;
    right: calc(50% - 22px);
  }

  /* ── Section headers, labels, notes ────────────────────────────────────────────── */
  body.mobile-office.mo-active .mo-section {
    padding: 18px 16px 8px;
    display: flex;
    align-items: baseline;
    gap: 8px;
  }
  body.mobile-office.mo-active .mo-section h2 {
    margin: 0;
    font-size: 13px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-secondary);
  }
  body.mobile-office.mo-active .mo-count {
    font-size: 12px;
    font-weight: 600;
    color: var(--gray-400);
  }
  body.mobile-office.mo-active .mo-note {
    font-size: 12px;
    color: var(--text-faint);
    line-height: 1.5;
  }

  /* ── Cards / list rows ─────────────────────────────────────────────────────────── */
  body.mobile-office.mo-active .mo-card {
    background: var(--surface-base);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
    margin: 0 16px 10px;
  }
  body.mobile-office.mo-active .mo-card.accent {
    border-left: 4px solid var(--mo-accent, var(--blue));
  }
  body.mobile-office.mo-active .mo-row {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: 52px;
    padding: 10px 14px;
    border-top: 1px solid var(--gray-100);
    background: transparent;
    border-left: 0;
    border-right: 0;
    border-bottom: 0;
    width: 100%;
    font-family: inherit;
    font-size: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
  }
  body.mobile-office.mo-active .mo-row:first-child {
    border-top: 0;
  }
  /* ── .mo-grow IS THE SHRINKABLE HALF OF EVERY FLEX ROW — INCLUDING THE ONES OUTSIDE
        .mo-row ────────────────────────────────────────────────────────────────────────
     This rule used to be scoped `.mo-row .mo-grow`, and three other flex containers draw
     a .mo-grow too: `.mo-alert` (the Today route cards and every alert card), `.mo-profile`
     (the client header) and `.mo-dp-cand` (the assign picker). Those got neither half of
     it, so they kept the flex-item default `min-width: auto` — which refuses to shrink a
     box below its MIN-CONTENT width.

     The Today route card is where that bites, because its `.mo-row-sub` line is
     `white-space: nowrap` ("Stop 1 of 1 · Next: <customer> · 8:00 AM"). A nowrap line's
     min-content is the entire string, so `.mo-grow` computed a 248px minimum inside a
     206px slot and simply hung off the right of its card. `.mo-scroll` is
     `overflow-x: hidden`, so nothing scrolled sideways and the document stayed clean —
     the text was just CLIPPED and unreachable.

     AND IT WAS NOT A NARROW-FLOOR CURIOSITY. It first surfaced as "11px at 320px", which
     is what the layout harness's deliberately SHORT fixture names produce. Measured with a
     real-length customer name: 250px lost at 320px, and 180px at 390px — the design
     viewport — where the "Next:" line and the route progress bar ran off the right of the
     card, cut mid-word with no ellipsis. `.mo-row-sub` already carries `overflow: hidden;
     text-overflow: ellipsis`, so once the box may shrink it ellipsises on its own;
     `min-width: 0` was the only thing missing.

     Unscoped deliberately: every `.mo-grow` the shell renders was enumerated in a real
     browser across all seven screens plus the dispatch sub-screens, and all 16 of them sit
     in a `display: flex` parent. Pinned at 320px and 390px by
     tests/e2e/layout/mobile-office/mobile-office-real-app-walkthrough.spec.js. */
  body.mobile-office.mo-active .mo-grow {
    flex: 1;
    min-width: 0;
  }
  body.mobile-office.mo-active .mo-row-title {
    font-size: 14px;
    font-weight: 600;
    color: var(--text-primary);
  }
  body.mobile-office.mo-active .mo-row-sub {
    font-size: 12px;
    color: var(--text-secondary);
    margin-top: 2px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  body.mobile-office.mo-active .mo-chev {
    color: var(--gray-400);
    flex-shrink: 0;
  }
  body.mobile-office.mo-active .mo-chev svg {
    width: 18px;
    height: 18px;
  }

  /* ── KPI tiles (the metrics-card 2px top-border language) ──────────────────────── */
  body.mobile-office.mo-active .mo-kpis {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    padding: 4px 16px 0;
  }
  body.mobile-office.mo-active .mo-kpi {
    background: var(--surface-base);
    border: 1px solid var(--gray-200);
    border-top: 2px solid var(--mo-accent, var(--blue));
    border-radius: var(--radius-lg);
    padding: 12px 14px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  }
  body.mobile-office.mo-active .mo-kpi .mo-kpi-v {
    font-size: 26px;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--gray-900);
    font-variant-numeric: tabular-nums;
  }
  body.mobile-office.mo-active .mo-kpi .mo-kpi-l {
    font-size: 11px;
    font-weight: 600;
    color: var(--text-secondary);
    margin-top: 2px;
  }

  /* ── Pills: live status, location, status chips (board vocabulary) ─────────────── */
  body.mobile-office.mo-active .mo-pill-live {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    height: 22px;
    padding: 0 9px;
    border-radius: 99px;
    font-size: 11px;
    font-weight: 700;
    white-space: nowrap;
  }
  body.mobile-office.mo-active .mo-pill-live .mo-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
  }
  body.mobile-office.mo-active .mo-pill-live.mo-onjob {
    background: var(--green-light);
    color: var(--green);
  }
  body.mobile-office.mo-active .mo-pill-live.mo-ahead {
    background: var(--blue-light);
    /* --link-color, NOT --blue-dark: same finding as the bottom-nav AA fix —
       blue-dark is a blue darkened for LIGHT grounds and lands 3.04:1 on the
       dark theme's --blue-light tint. --link-color is the token whose two
       values are picked for text duty (measured: 5.2:1 light, 6.2:1 dark). */
    color: var(--link-color);
  }
  body.mobile-office.mo-active .mo-pill-live.mo-behind {
    background: var(--amber-light);
    color: var(--amber);
  }
  body.mobile-office.mo-active .mo-pill-live.mo-idle {
    background: var(--surface-inset);
    color: var(--text-secondary);
  }
  body.mobile-office.mo-active .mo-loc {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    height: 20px;
    padding: 0 8px;
    border-radius: 99px;
    font-size: 10px;
    font-weight: 700;
    color: #fff;
    background: var(--mo-loc-color, var(--blue));
    white-space: nowrap;
  }

  /* ── Alert stack (Needs attention) ─────────────────────────────────────────────── */
  body.mobile-office.mo-active .mo-alert {
    display: flex;
    gap: 12px;
    padding: 13px 14px;
  }
  body.mobile-office.mo-active .mo-alert .mo-alert-ic {
    width: 34px;
    height: 34px;
    border-radius: 8px;
    display: grid;
    place-items: center;
    flex-shrink: 0;
  }
  body.mobile-office.mo-active .mo-alert .mo-alert-ic svg {
    width: 18px;
    height: 18px;
  }
  body.mobile-office.mo-active .mo-alert h3 {
    margin: 0;
    font-size: 13.5px;
    font-weight: 700;
    color: var(--text-primary);
  }
  body.mobile-office.mo-active .mo-alert p {
    margin: 2px 0 0;
    font-size: 12.5px;
    color: var(--text-secondary);
    line-height: 1.45;
  }
  body.mobile-office.mo-active .mo-alert .mo-alert-band {
    display: inline-block;
    background: var(--gray-800);
    border-radius: 8px;
    color: var(--surface-base);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.08em;
    padding: 3px 8px;
  }

  /* ── Today: route rows ─────────────────────────────────────────────────────────── */
  body.mobile-office.mo-active .mo-avatar-lg {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: var(--mo-avatar-color, var(--blue));
    color: #fff;
    font-size: 14px;
    font-weight: 700;
    display: grid;
    place-items: center;
    flex-shrink: 0;
  }
  body.mobile-office.mo-active .mo-route-top {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    row-gap: 4px;
  }
  body.mobile-office.mo-active .mo-prog {
    display: flex;
    gap: 3px;
    margin-top: 7px;
  }
  body.mobile-office.mo-active .mo-prog i {
    height: 5px;
    flex: 1;
    border-radius: 3px;
    background: var(--gray-200);
  }
  body.mobile-office.mo-active .mo-prog i.mo-done {
    background: var(--green);
  }
  body.mobile-office.mo-active .mo-prog i.mo-now {
    background: var(--blue);
  }
  body.mobile-office.mo-active .mo-prog i.mo-utc {
    background: var(--red);
  }

  /* ── Today: hello block ────────────────────────────────────────────────────────── */
  body.mobile-office.mo-active .mo-hello {
    padding: 18px 16px 6px;
  }
  body.mobile-office.mo-active .mo-hello .mo-hello-date {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--blue);
  }
  body.mobile-office.mo-active .mo-hello h1 {
    margin: 3px 0 0;
    font-size: 22px;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--gray-900);
  }

  /* ── Alerts screen ─────────────────────────────────────────────────────────────── */
  /* ── P7: THE ROW HAS TO SAY IT CONTINUES ────────────────────────────────────────────
     Production TL_TABS carries ELEVEN tabs and roughly two and a half fit a phone. The
     row already scrolled — but it was cut flush at the viewport edge with no fade, no
     snap and no indicator, so the eight or nine groups past the edge were discoverable
     only by accident.

     The mask is applied to the SCROLLER's own box (a mask paints against the element's
     border box, not against the content that moves under it), so it stays pinned to the
     edges while the chips slide beneath it. --mo-fl / --mo-fr are 0 by default and the
     shell's scroll handler (_moChipFade) raises whichever edge actually has content
     beyond it: a permanent right-hand fade on a row already scrolled to its end would
     read as a rendering fault, which is the failure mode of the naive version of this.

     scroll-snap-type: proximity, NOT mandatory — mandatory would fight a deliberate
     small nudge and snap the row somewhere the user did not ask for. */
  body.mobile-office.mo-active .mo-chiprow {
    display: flex;
    gap: 8px;
    overflow-x: auto;
    padding: 12px 16px 10px;
    scrollbar-width: none;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    scroll-padding-left: 16px;
    --mo-fl: 0px;
    --mo-fr: 0px;
    -webkit-mask-image: linear-gradient(
      90deg,
      transparent 0,
      #000 var(--mo-fl),
      #000 calc(100% - var(--mo-fr)),
      transparent 100%
    );
    mask-image: linear-gradient(
      90deg,
      transparent 0,
      #000 var(--mo-fl),
      #000 calc(100% - var(--mo-fr)),
      transparent 100%
    );
  }
  body.mobile-office.mo-active .mo-chiprow.mo-fade-l {
    --mo-fl: 26px;
  }
  body.mobile-office.mo-active .mo-chiprow.mo-fade-r {
    --mo-fr: 34px;
  }
  body.mobile-office.mo-active .mo-chiprow::-webkit-scrollbar {
    display: none;
  }
  body.mobile-office.mo-active .mo-chip {
    flex-shrink: 0;
    scroll-snap-align: start;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 32px;
    padding: 0 12px;
    border-radius: 99px;
    border: 1px solid var(--border-strong);
    background: var(--surface-base);
    font-family: inherit;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--text-secondary);
    white-space: nowrap;
    cursor: pointer;
  }
  body.mobile-office.mo-active .mo-chip .mo-chip-n {
    font-size: 11px;
    font-weight: 700;
    background: var(--gray-100);
    border-radius: 8px;
    padding: 1px 6px;
  }
  body.mobile-office.mo-active .mo-chip.mo-on {
    background: var(--blue-light);
    border-color: var(--blue);
    color: var(--link-color); /* the mo-ahead pill's AA rationale, same pair */
  }
  body.mobile-office.mo-active .mo-chip.mo-on .mo-chip-n {
    background: var(--blue);
    color: #fff;
  }
  body.mobile-office.mo-active .mo-notif {
    display: flex;
    gap: 12px;
    padding: 12px 14px;
    align-items: flex-start;
    border-top: 1px solid var(--gray-100);
    width: 100%;
    background: transparent;
    border-left: 0;
    border-right: 0;
    border-bottom: 0;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
  }
  body.mobile-office.mo-active .mo-notif:first-child {
    border-top: 0;
  }
  body.mobile-office.mo-active .mo-notif .mo-notif-ic {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    flex-shrink: 0;
    background: var(--blue-light);
    color: var(--blue-dark);
  }
  body.mobile-office.mo-active .mo-notif .mo-notif-ic svg {
    width: 16px;
    height: 16px;
  }
  body.mobile-office.mo-active .mo-notif .mo-notif-txt {
    flex: 1;
    min-width: 0;
    font-size: 13px;
    color: var(--text-primary);
    line-height: 1.45;
  }
  body.mobile-office.mo-active .mo-notif .mo-notif-t {
    display: block;
    font-size: 11px;
    color: var(--text-faint);
    margin-top: 3px;
  }
  body.mobile-office.mo-active .mo-notif .mo-notif-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--blue);
    margin-top: 6px;
    flex-shrink: 0;
  }
  body.mobile-office.mo-active .mo-notif.mo-read {
    opacity: 0.62;
  }

  /* ── Buttons ───────────────────────────────────────────────────────────────────── */
  body.mobile-office.mo-active .mo-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    height: 44px;
    padding: 0 18px;
    border-radius: 7px;
    font-family: inherit;
    font-size: 14px;
    font-weight: 600;
    border: 0.5px solid var(--gray-300);
    background: var(--surface-base);
    color: var(--gray-700);
    cursor: pointer;
  }
  body.mobile-office.mo-active .mo-btn.mo-sm {
    height: 34px;
    padding: 0 12px;
    font-size: 12.5px;
  }
  body.mobile-office.mo-active .mo-btn.mo-primary {
    background: linear-gradient(180deg, var(--blue), var(--blue-dark));
    border: 0;
    color: #fff;
    box-shadow: 0 2px 6px rgba(37, 99, 235, 0.35);
  }
  body.mobile-office.mo-active .mo-btn:active {
    transform: translateY(1px);
    box-shadow: none;
  }

  /* ── Handoff screens (Tickets / Dispatch until their phases land) ──────────────── */
  body.mobile-office.mo-active .mo-handoff {
    text-align: center;
    padding: 40px 24px 28px;
  }
  body.mobile-office.mo-active .mo-handoff .mo-handoff-ic {
    width: 56px;
    height: 56px;
    border-radius: 16px;
    display: grid;
    place-items: center;
    margin: 0 auto 14px;
    background: var(--blue-light);
    color: var(--blue-dark);
  }
  body.mobile-office.mo-active .mo-handoff .mo-handoff-ic svg {
    width: 26px;
    height: 26px;
  }
  body.mobile-office.mo-active .mo-handoff h2 {
    margin: 0 0 6px;
    font-size: 17px;
    font-weight: 700;
    color: var(--gray-900);
  }
  body.mobile-office.mo-active .mo-handoff p {
    margin: 0 auto;
    max-width: 34ch;
    font-size: 13px;
    color: var(--text-secondary);
    line-height: 1.55;
  }
  body.mobile-office.mo-active .mo-desktop-tag {
    display: inline-block;
    margin-left: auto;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.05em;
    color: var(--text-faint);
    border: 1px solid var(--gray-300);
    border-radius: 99px;
    padding: 2px 8px;
    text-transform: uppercase;
    flex-shrink: 0;
  }

  /* ── More screen ───────────────────────────────────────────────────────────────── */
  body.mobile-office.mo-active .mo-profile {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px;
  }
  body.mobile-office.mo-active .mo-profile .mo-profile-nm {
    font-size: 15px;
    font-weight: 700;
    color: var(--text-primary);
  }
  body.mobile-office.mo-active .mo-profile .mo-profile-rl {
    font-size: 12px;
    color: var(--text-secondary);
    margin-top: 2px;
  }
  body.mobile-office.mo-active .mo-more-ic {
    width: 34px;
    height: 34px;
    border-radius: 8px;
    display: grid;
    place-items: center;
    background: var(--surface-inset);
    color: var(--gray-600);
    flex-shrink: 0;
  }
  body.mobile-office.mo-active .mo-more-ic svg {
    width: 17px;
    height: 17px;
  }
  body.mobile-office.mo-active .mo-signout .mo-row-title {
    color: var(--red);
  }
  body.mobile-office.mo-active .mo-version {
    text-align: center;
    font-size: 11px;
    color: var(--text-faint);
    padding: 16px 0 4px;
  }

  /* ── Refresh hint (resume-resync affordance) ───────────────────────────────────── */
  body.mobile-office.mo-active .mo-refreshing {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 8px 16px 0;
    font-size: 11.5px;
    font-weight: 600;
    color: var(--text-faint);
  }

  /* ── Universal search modal, full-screen (restyles the EXISTING #gh-search-modal;
        body-level, shared by design — the one desktop surface the shell reuses) ───── */
  body.mobile-office.mo-active .gh-search-overlay {
    padding: 0;
    align-items: stretch;
  }
  body.mobile-office.mo-active .gh-search-box {
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: none;
    height: 100%;
    max-height: none;
    border-radius: 0;
    padding-top: calc(env(safe-area-inset-top, 0px) + 8px);
  }
  body.mobile-office.mo-active .gh-search-input-row input {
    font-size: 16px; /* iOS focus-zoom guard — the technician layer's own rule */
    height: 44px;
  }
  body.mobile-office.mo-active .gh-search-results {
    max-height: none;
    flex: 1;
  }

  /* ── Phase 1: the ticket queue ─────────────────────────────────────────────────── */
  body.mobile-office.mo-active .mo-tk-search {
    padding: 10px 16px 0;
  }
  body.mobile-office.mo-active .mo-tk-searchbox {
    display: flex;
    align-items: center;
    gap: 8px;
    height: 44px;
    padding: 0 12px;
    border: 1px solid var(--input-border);
    border-radius: 8px;
    background: var(--input-bg);
    color: var(--text-faint);
  }
  body.mobile-office.mo-active .mo-tk-searchbox input {
    flex: 1;
    min-width: 0;
    border: 0;
    outline: none;
    background: transparent;
    font-family: inherit;
    font-size: 16px; /* iOS focus-zoom guard — the technician layer's own rule */
    color: var(--text-primary);
  }
  body.mobile-office.mo-active .mo-tk-group {
    position: sticky;
    top: 0;
    z-index: 3;
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 6px 16px 8px;
    padding: 7px 12px;
    background: var(--surface-inset);
    border-radius: 8px;
  }
  body.mobile-office.mo-active .mo-tk-group b {
    font-size: 12px;
    font-weight: 700;
    color: var(--text-primary);
  }
  /* ── OFF-SCREEN CARDS COST NOTHING ─────────────────────────────────────────────────
     content-visibility: auto lets the engine skip layout AND paint for a card that is
     not in (or near) the viewport. Measured on the real bundle at 412x915 with 2,679
     tickets, 4x CPU throttling — the profile of a mid-range phone: a Tickets rebuild
     went 180ms -> 114ms, a 37% cut, from these two declarations alone.

     contain-intrinsic-size is not optional decoration: without a size to stand in for
     a skipped card the scrollbar and the scroll offset would jump as cards enter and
     leave. `auto 118px` seeds it with the measured card height and then lets the engine
     remember each card's REAL height once it has rendered it a first time, so scrolling
     back up lands exactly where it left. */
  body.mobile-office.mo-active .mo-tk-cardwrap {
    content-visibility: auto;
    contain-intrinsic-size: auto 118px;
  }
  body.mobile-office.mo-active .mo-tk-card {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 0 6px 0 0;
  }
  body.mobile-office.mo-active .mo-tk-main {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 3px;
    padding: 12px 8px 12px 14px;
    border: 0;
    background: transparent;
    font-family: inherit;
    font-size: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
  }
  body.mobile-office.mo-active .mo-tk-top {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    row-gap: 3px;
  }
  body.mobile-office.mo-active .mo-tk-name {
    font-size: 14.5px;
    font-weight: 700;
    color: var(--text-primary);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  body.mobile-office.mo-active .mo-tk-addr {
    font-size: 12.5px;
    color: var(--text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  body.mobile-office.mo-active .mo-tk-meta {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 5px;
    font-size: 11.5px;
    color: var(--text-faint);
    flex-wrap: wrap;
  }
  body.mobile-office.mo-active .mo-tk-num {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 10.5px;
  }
  body.mobile-office.mo-active .mo-tk-call {
    width: 44px;
    height: 44px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: var(--green-light);
    color: var(--green);
    flex-shrink: 0;
  }
  body.mobile-office.mo-active .mo-tk-call svg {
    width: 19px;
    height: 19px;
  }
  body.mobile-office.mo-active .mo-tk-more {
    display: flex;
    width: calc(100% - 32px);
    margin: 2px 16px 12px;
  }

  /* ── Phase 1: the ticket-panel TAKEOVER (body.mo-panel-open) ─────────────────────
     The shell adopts the REAL desktop ticket panel (#tl-side-panel) as the phone's
     ticket detail. The class is written only by mobile-office.js's panel observer,
     the panel lives inside .main-content (index.html:787→4105), and the shell never
     forces a panel MODE — these rules override the GEOMETRY of all four modes
     (docked/floating/modal/fullscreen apply whatever the user's desktop preference
     stored), so ss_ticket_panel_mode_* is never written and desktop keeps its shape.
     !important is used exactly where a mode writes INLINE styles (floating's
     left/top/width/height) or its own !important (fullscreen's width/height). */
  body.mobile-office.mo-active.mo-panel-open #app-body .main-content {
    display: block; /* the chrome-swap exception: the panel's ancestor must render */
  }
  body.mobile-office.mo-active.mo-panel-open .tl-side-panel {
    position: fixed !important;
    inset: 0 !important;
    width: auto !important;
    height: auto !important;
    min-width: 0 !important;
    max-width: none !important;
    z-index: 195; /* above #mo-root (60), below dropdowns (200) and modals (220) */
    border-radius: 0;
    border-left: none;
    box-shadow: none;
    animation: none !important;
  }
  /* Desktop-only panel chrome: window management has no meaning on a phone. */
  body.mobile-office.mo-active.mo-panel-open #tsp-collapse-btn,
  body.mobile-office.mo-active.mo-panel-open #tsp-resize-handle,
  body.mobile-office.mo-active.mo-panel-open #tsp-float-resize,
  body.mobile-office.mo-active.mo-panel-open #tsp-mode-dd-btn,
  body.mobile-office.mo-active.mo-panel-open #tsp-next-ticket,
  body.mobile-office.mo-active.mo-panel-open #tsp-modal-backdrop {
    display: none !important;
  }
  /* ── THE CUSTOMER HEADER, ON A PHONE ──────────────────────────────────────────────
     Measured at 390×844 before this block: .tsp-header was 195px — 23% of the viewport,
     and ~29% of an iPhone SE — before a single field of the ticket was on screen. It
     broke down as 55px of utility row + 90px of customer block + a 2px rule + the 36px
     tab strip, and the 55px was the waste: a band holding three icon buttons and nothing
     else, its whole middle empty, stacked ABOVE a customer block whose own right-hand end
     (Call, ticket number) was likewise sitting against empty space.

     So the utility strip stops being a band. It shares the customer block's grid cell and
     settles into its bottom-right corner — space that block was already leaving empty —
     and the lines that then share its y give up width rather than gaining a second line.
     Measured after: 132px / 15.6% at 390×844, 161px / 20.6% at 320×780, and 24.1% for a
     37-character name on the SE. Around a third off, at every size.

     What that bought is space, not affordances. Activity, move-to-group, Call, the profile
     link, the ticket number and the required-fields chip are all still here and all still
     at 44px. The single exception is the close ✕, which is dropped only while the action
     bar's own Back button — the same closeTicketPanel() call — is genuinely on screen; the
     :has() rule below is what makes "genuinely" true rather than assumed.

     Deliberately not "hide the header Call button because the action bar has one": the two
     do not resolve the same number. #tsp-header-call-btn goes through _availCallNumber
     (tickets.js) — primary, or the alternate when there is no primary, read from the LIVE
     panel input — while #mo-panel-bar's Call is built from the saved t.phone. On a ticket
     that carries only an alternate number the header button is the ONLY way to dial, so
     hiding it would take a working affordance away from exactly the tickets that need it. */
  /* A one-column grid, so the utility strip can be put in the SAME CELL as the customer
     block and settle into its bottom-right corner. Sharing a cell is what makes this work:
     a flex sibling — or the same strip as its own row — takes width or height from the
     customer block, and at 390px the name and its status chip need every pixel of the line
     they are on or they wrap and the header grows back. Overlapping instead costs nothing,
     and the row's height is still max(block, strip), so the icons can never be clipped.

     `position: relative` is kept: .tsp-caught-up-popup is an absolutely-positioned child
     that anchors to this box (top: calc(100% + 8px)), and it must go on doing so. */
  body.mobile-office.mo-active.mo-panel-open .tsp-header {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
  }
  body.mobile-office.mo-active.mo-panel-open .tsp-header-util {
    grid-area: 1 / 1;
    justify-self: end;
    align-self: end;
    z-index: 3;
    width: auto;
    padding: 0 2px 4px;
    gap: 0;
    /* Was flex-wrap:wrap + row-gap:4px, to stop the row clipping at 390px. Sharing a cell
       and holding only what a phone shows (close, activity, move-to-group — the rest is
       display:none'd above), it has nothing left to wrap: wrapping now would push a second
       line of icons UP across the customer block instead. */
    flex-wrap: nowrap;
    row-gap: 0;
  }
  body.mobile-office.mo-active.mo-panel-open .tsp-header-separator {
    grid-area: 2 / 1;
  }
  body.mobile-office.mo-active.mo-panel-open .tsp-tabs {
    grid-area: 3 / 1;
  }
  body.mobile-office.mo-active.mo-panel-open .tsp-header-util button {
    min-height: 44px;
    min-width: 44px;
  }
  /* min-height reserves a band at the bottom of the cell that belongs to the icon strip
     alone, so a header configured down to a single row still cannot put a field's text
     under an icon. 48px of strip plus one line of fields. */
  body.mobile-office.mo-active.mo-panel-open .tsp-header-info {
    grid-area: 1 / 1;
    padding: 4px 12px 8px;
    min-height: 80px;
  }
  body.mobile-office.mo-active.mo-panel-open .tsp-header-rows {
    gap: 3px;
  }
  /* ── The close ✕, and the ONE affordance it is safe to drop ──────────────────────────
     #tsp-close calls closeTicketPanel(). So does #mo-panel-bar's Back button, which this
     shell renders unconditionally for every open panel and pins to the bottom of the
     screen — a duplicate in the literal sense, unlike the header Call button above.
     Dropping it is worth 44px of the width the first header line has to keep clear, and
     that width is what decides whether the customer name and its status chip fit on one
     line at 320px or take two.

     :has(), so the ✕ can only disappear while its replacement is genuinely on screen.
     _moPanelAdopt (mobile-office.js) adds body.mo-panel-open UNCONDITIONALLY and only
     then fills the bar `if (bar)` — so keying on the class alone would, in a session
     where #mo-panel-bar was missing, leave a full-screen panel with no way out of it.
     The bar carries [hidden] until that same function has populated and revealed it,
     which makes ":the bar is showing" exactly the right question to ask. */
  body.mobile-office.mo-active.mo-panel-open:has(#mo-panel-bar:not([hidden])) #tsp-close {
    display: none !important;
  }
  /* The strip sits in the cell's bottom-right, so it is the lines BELOW the first that
     share its y — the first line is the one that must keep its full width, because the
     customer name and its status chip are what wrap when it does not. 2 × 44px buttons
     plus the strip's own padding. The fields that end up here are the short, truncating
     ones (tags, address, dates), which lose an ellipsis rather than a line. */
  body.mobile-office.mo-active.mo-panel-open .tsp-header-rows .tsp-header-row:not(:first-child) {
    padding-right: 96px;
  }
  /* …and those lines then take the reserve out of their WIDTH rather than out of a second
     line: shrink-and-ellipsise beats wrapping for meta text, which is what a configured
     row below the first almost always is (address, location, dates, the ticket's own
     numbers). Excluding any row holding TAGS, by :has() — tag pills are discrete objects
     that must wrap among themselves, and squeezing them onto one clipped line would hide
     whole tags rather than the tail of a string. (:has() is used throughout styles.css;
     this is the same mechanism.) */
  body.mobile-office.mo-active.mo-panel-open
    .tsp-header-rows
    .tsp-header-row:not(:first-child):not(:has(.tsp-tags-display)):not(
      :has(.tsp-account-tags-display)
    ),
  body.mobile-office.mo-active.mo-panel-open
    .tsp-header-rows
    .tsp-header-row:not(:first-child):not(:has(.tsp-tags-display)):not(
      :has(.tsp-account-tags-display)
    )
    .tsp-hf {
    /* The wrapper wraps too, or the squeeze just moves inside it: measured, "E Dundee"
       broke away from its own leading divider and made a 47px-wide field 35px tall. */
    flex-wrap: nowrap;
  }
  body.mobile-office.mo-active.mo-panel-open
    .tsp-header-rows
    .tsp-header-row:not(:first-child)
    .tsp-hf {
    min-width: 0;
    overflow: hidden;
  }
  /* A squeezed field ends in an ellipsis, not mid-letter. On .tsp-hf itself text-overflow
     does nothing (it is an inline-flex box, so the property has no line box to act on) —
     it has to go on the span holding the text, which is one of these two classes for every
     meta field _updateHeaderMeta renders, plus the address's own class. */
  body.mobile-office.mo-active.mo-panel-open .tsp-header-address,
  body.mobile-office.mo-active.mo-panel-open .tsp-header-meta-item {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 100%;
  }
  /* Touch + iOS-zoom floors, the technician layer's own numbers: real form
     controls at 16px/44px (a sub-16px input zooms the whole page on focus). The
     mention composer is a contenteditable — no zoom risk — but 13px thumbs-width
     prose is unreadable; 15px matches the tech layer's field text. */
  body.mobile-office.mo-active.mo-panel-open
    .tl-side-panel
    input:not([type='checkbox']):not([type='radio']),
  body.mobile-office.mo-active.mo-panel-open .tl-side-panel select,
  body.mobile-office.mo-active.mo-panel-open .tl-side-panel textarea {
    font-size: 16px;
    min-height: 44px;
  }
  body.mobile-office.mo-active.mo-panel-open .tl-side-panel .tsp-mention-input {
    font-size: 15px;
  }
  body.mobile-office.mo-active.mo-panel-open .tl-side-panel .tsp-tab {
    font-size: 13px;
    padding: 10px 12px;
  }
  /* Room for the action bar at the bottom of the panel's own scroll region. */
  body.mobile-office.mo-active.mo-panel-open .tl-side-panel .tsp-body {
    padding-bottom: calc(76px + env(safe-area-inset-bottom, 0px));
  }
  /* The three grids the seam audit flagged as having no narrow escape. */
  body.mobile-office.mo-active.mo-panel-open .tl-side-panel.tsp-mode-fullscreen .tsp-grid-2col {
    grid-template-columns: 1fr;
  }
  body.mobile-office.mo-active.mo-panel-open .tl-side-panel .tsp-fin-2col {
    grid-template-columns: 1fr;
  }
  body.mobile-office.mo-active.mo-panel-open .tl-side-panel .tsp-fin-stats {
    grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
    gap: 6px;
  }

  /* ── The action bar (#mo-panel-bar — sibling of #mo-root; see index.html) ──────── */
  body.mobile-office.mo-active.mo-panel-open #mo-panel-bar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 197; /* above the adopted panel (195), below dropdowns (200) */
    display: flex;
    gap: 8px;
    padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
    background: var(--surface-base);
    border-top: 1px solid var(--border);
    box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
  }
  body.mobile-office.mo-active.mo-panel-open .mo-pb-btn {
    flex: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    height: 44px;
    border-radius: 7px;
    border: 0.5px solid var(--gray-300);
    background: var(--surface-base);
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    color: var(--gray-700);
    text-decoration: none;
    cursor: pointer;
  }
  body.mobile-office.mo-active.mo-panel-open .mo-pb-btn:active {
    transform: translateY(1px);
  }
  body.mobile-office.mo-active.mo-panel-open .mo-pb-call {
    background: var(--green);
    border: 0;
    color: #fff;
  }
  body.mobile-office.mo-active.mo-panel-open .mo-pb-back {
    flex: 0 0 auto;
    padding: 0 14px;
  }
  body.mobile-office.mo-active.mo-panel-open .mo-pb-btn:focus-visible {
    outline: 2px solid var(--focus-ring, var(--blue-dark));
    outline-offset: 2px;
  }

  /* ═══════════════════════════════════════════════════════════════════════════════
     PHASE 2 — DISPATCH DAY BOARD, TECH-DAY TIMELINE, GUIDED MOVE, TRAY SHEET
     Shell-rendered presentation over the same stores the desktop board draws.
     The guided-move dialogs are the DESKTOP'S OWN (#bm-dest-modal /
     #bm-confirm-modal, plus the day-note and confirmed-route dialogs) — this
     section only re-geometries them for a phone, exactly as the panel takeover
     above re-geometries #tl-side-panel. Every write control that could text a
     customer is hidden here AND refused engine-side (bmCommitMove / _crcCommit /
     _routeChangesConfirm each check body.mo-active) — the pair is pinned in
     tests/mobile-office-gate.test.js §6.
     ═══════════════════════════════════════════════════════════════════════════════ */

  /* ── Week row + date strip ─────────────────────────────────────────────────────── */
  body.mobile-office.mo-active .mo-dp-weekrow {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 8px 8px 0;
  }
  body.mobile-office.mo-active .mo-dp-weeklabel {
    flex: 1;
    min-width: 0;
    font-size: 13px;
    font-weight: 700;
    color: var(--gray-900);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    padding-left: 4px;
  }
  body.mobile-office.mo-active .mo-dp-chevflip svg {
    transform: rotate(180deg);
  }
  body.mobile-office.mo-active .mo-dp-days {
    display: flex;
    gap: 6px;
    padding: 8px 16px 4px;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  body.mobile-office.mo-active .mo-dp-days::-webkit-scrollbar {
    display: none;
  }
  body.mobile-office.mo-active .mo-dp-day {
    position: relative;
    flex: 1 0 48px;
    min-width: 48px;
    min-height: 62px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1px;
    padding: 6px 2px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface-base);
    cursor: pointer;
    font-family: inherit;
  }
  body.mobile-office.mo-active .mo-dp-day .mo-dp-dw {
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: var(--text-faint);
  }
  body.mobile-office.mo-active .mo-dp-day .mo-dp-dn {
    font-size: 15px;
    font-weight: 700;
    color: var(--gray-800);
  }
  body.mobile-office.mo-active .mo-dp-day .mo-dp-dc {
    font-size: 10.5px;
    font-weight: 600;
    color: var(--text-secondary);
  }
  body.mobile-office.mo-active .mo-dp-day.mo-on {
    border-color: var(--blue);
    background: var(--blue-light);
  }
  body.mobile-office.mo-active .mo-dp-day.mo-on .mo-dp-dn,
  body.mobile-office.mo-active .mo-dp-day.mo-on .mo-dp-dc {
    color: var(--link-color); /* the vetted on-tint AA pair — the Phase 1 chip lesson */
  }
  body.mobile-office.mo-active .mo-dp-day.mo-over .mo-dp-dc,
  body.mobile-office.mo-active .mo-dp-day.mo-full .mo-dp-dc {
    color: var(--amber);
    font-weight: 700;
  }
  body.mobile-office.mo-active .mo-dp-flag {
    position: absolute;
    top: 5px;
    right: 6px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--amber);
  }

  /* ── Banner extras, location headers, tech cards ────────────────────────────────── */
  body.mobile-office.mo-active .mo-dp-override {
    display: inline-block;
    margin-top: 6px;
    font-size: 10.5px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: var(--amber);
  }
  body.mobile-office.mo-active .mo-dp-noterow {
    margin-top: 8px;
  }
  body.mobile-office.mo-active .mo-dp-note {
    display: block;
    width: 100%;
    text-align: left;
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
    font-family: inherit;
  }
  body.mobile-office.mo-active .mo-dp-loc {
    padding: 12px 20px 4px;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: var(--text-faint);
  }
  body.mobile-office.mo-active .mo-dp-techwrap {
    padding: 0;
  }
  body.mobile-office.mo-active .mo-dp-tech {
    display: block;
    width: 100%;
    text-align: left;
    background: none;
    border: 0;
    padding: 14px 16px;
    cursor: pointer;
    font-family: inherit;
    font-size: inherit;
    color: inherit;
  }
  body.mobile-office.mo-active .mo-dp-tech:active {
    background: var(--surface-inset);
  }

  /* ── Unassigned tray pill (floats above the nav) ────────────────────────────────── */
  /* Bordered-chip anatomy (.tl-status-badge's): red ink + red border on the BASE
     surface. Measured, not reasoned — white on the dark --red is 3.48:1 and the
     red-on-red-light tint is 4.41:1 in light; ink-on-base clears 4.5 both themes. */
  body.mobile-office.mo-active .mo-dp-tray {
    position: fixed;
    left: 12px;
    right: 12px;
    bottom: calc(56px + env(safe-area-inset-bottom, 0px) + 12px);
    z-index: 150;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-height: 44px;
    padding: 10px 14px;
    border: 1px solid var(--red);
    border-radius: 12px;
    background: var(--surface-base);
    color: var(--red);
    font-family: inherit;
    font-size: 13px;
    font-weight: 700;
    box-shadow: var(--shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.2));
    cursor: pointer;
  }
  /* The pill while the §7.3 fresh-baseline pull is in flight. Same box, same place, same
     count — only the message and the spinner change, so nothing under the finger moves. */
  body.mobile-office.mo-active .mo-dp-tray-busy {
    border-color: var(--border);
    color: var(--text-secondary);
    box-shadow: var(--shadow-sm, 0 1px 3px rgba(0, 0, 0, 0.1));
    cursor: progress;
  }
  body.mobile-office.mo-active .mo-dp-tray-busy .mo-dp-tray-n {
    border-color: var(--border);
    color: var(--text-secondary);
  }
  body.mobile-office.mo-active .mo-dp-tray-spin {
    width: 14px;
    height: 14px;
    flex: none;
    border-radius: 50%;
    border: 2px solid var(--border);
    border-top-color: var(--text-secondary);
    animation: gw-spin 0.7s linear infinite;
  }
  body.mobile-office.mo-active .mo-dp-tray-n {
    display: grid;
    place-items: center;
    min-width: 22px;
    height: 22px;
    padding: 0 5px;
    border-radius: 11px;
    border: 1px solid var(--red);
    font-size: 12px;
  }

  /* ── Tech-day: summary + timeline ──────────────────────────────────────────────── */
  body.mobile-office.mo-active .mo-dp-sum {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    padding: 12px 20px 8px;
  }
  body.mobile-office.mo-active .mo-dp-sumstats {
    font-size: 12px;
    color: var(--text-secondary);
  }
  body.mobile-office.mo-active .mo-dp-line {
    position: relative;
    padding: 4px 16px 8px 0;
  }
  body.mobile-office.mo-active .mo-dp-line::before {
    content: '';
    position: absolute;
    left: 64px;
    top: 14px;
    bottom: 14px;
    width: 2px;
    background: var(--border);
  }
  body.mobile-office.mo-active .mo-dp-stop {
    position: relative;
    display: grid;
    grid-template-columns: 64px 1fr;
    margin: 8px 0;
  }
  body.mobile-office.mo-active .mo-dp-rail {
    position: relative;
  }
  body.mobile-office.mo-active .mo-dp-time {
    display: block;
    padding: 12px 14px 0 0;
    text-align: right;
    font-size: 11px;
    font-weight: 600;
    color: var(--text-secondary);
    white-space: nowrap;
  }
  body.mobile-office.mo-active .mo-dp-dot {
    position: absolute;
    left: 64px;
    top: 16px;
    transform: translateX(-50%);
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--gray-400);
    border: 2px solid var(--gray-100);
    z-index: 1;
  }
  body.mobile-office.mo-active .mo-dp-stop.mo-done .mo-dp-dot {
    background: var(--green);
  }
  body.mobile-office.mo-active .mo-dp-stop.mo-now .mo-dp-dot {
    background: var(--blue);
    box-shadow: 0 0 0 3px var(--blue-light);
  }
  body.mobile-office.mo-active .mo-dp-stop.mo-utc .mo-dp-dot {
    background: var(--red);
  }
  body.mobile-office.mo-active .mo-dp-stopcard {
    margin-left: 14px;
    background: var(--surface-base);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 10px 12px;
    box-shadow: var(--shadow-sm);
  }
  body.mobile-office.mo-active .mo-dp-stop.mo-now .mo-dp-stopcard {
    border-color: var(--blue);
  }
  body.mobile-office.mo-active .mo-dp-stop.mo-utc .mo-dp-stopcard {
    border-color: var(--red);
  }
  body.mobile-office.mo-active .mo-dp-gap {
    padding: 2px 0 2px 88px;
    font-size: 11px;
    color: var(--text-faint);
  }
  body.mobile-office.mo-active .mo-dp-stopmain {
    display: block;
    width: 100%;
    text-align: left;
    background: none;
    border: 0;
    padding: 0;
    font-family: inherit;
    font-size: inherit;
    color: inherit;
    cursor: pointer;
  }
  body.mobile-office.mo-active .mo-dp-c-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
  }
  body.mobile-office.mo-active .mo-dp-c-name {
    font-size: 13.5px;
    font-weight: 700;
    color: var(--gray-900);
    min-width: 0;
    overflow-wrap: anywhere;
  }
  body.mobile-office.mo-active .mo-dp-c-sub {
    display: block;
    margin-top: 2px;
    font-size: 12px;
    color: var(--text-secondary);
    overflow-wrap: anywhere;
  }
  body.mobile-office.mo-active .mo-dp-c-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 4px;
    font-size: 11.5px;
    color: var(--text-faint);
  }
  body.mobile-office.mo-active .mo-dp-strike {
    text-decoration: line-through;
    color: var(--text-faint);
  }
  body.mobile-office.mo-active .mo-dp-utclabel {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-bottom: 4px;
    font-size: 10.5px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: var(--red);
  }
  body.mobile-office.mo-active .mo-dp-chip-done {
    color: var(--green);
    border-color: var(--green);
    background: transparent;
  }
  body.mobile-office.mo-active .mo-dp-chip-eta {
    color: var(--link-color);
    border-color: var(--link-color);
    background: transparent;
  }
  body.mobile-office.mo-active .mo-dp-c-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 10px;
  }
  body.mobile-office.mo-active .mo-dp-mini {
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 0.5px solid var(--gray-300);
    background: var(--surface-base);
    color: var(--gray-700);
    cursor: pointer;
  }
  body.mobile-office.mo-active .mo-dp-call {
    background: var(--green);
    border: 0;
    color: #fff;
  }
  body.mobile-office.mo-active .mo-dp-movebtn {
    margin-left: auto;
    min-height: 40px; /* measured 34px as a bare .mo-btn.mo-sm — below the touch floor */
  }
  body.mobile-office.mo-active .mo-dp-assignbtn {
    min-height: 40px;
  }
  body.mobile-office.mo-active .mo-dp-reason {
    margin-left: auto;
    font-size: 11.5px;
    color: var(--text-faint);
    text-align: right;
  }
  body.mobile-office.mo-active .mo-dp-footer {
    display: flex;
    gap: 8px;
    padding: 8px 20px 16px;
  }
  body.mobile-office.mo-active .mo-dp-footer .mo-btn {
    flex: 1;
  }

  /* ── Tray sheet + assign picker (shell z-band: scrim 191, sheet 192 — real
        modals at 220 still win, "modal wins" convention) ──────────────────────────── */
  body.mobile-office.mo-active .mo-dp-scrim {
    position: fixed;
    inset: 0;
    z-index: 191;
    background: rgba(24, 24, 27, 0.55);
    border: 0;
    padding: 0;
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.2s ease;
  }
  body.mobile-office.mo-active .mo-dp-scrim.mo-in {
    opacity: 1;
  }
  /* Reduced motion keeps the scrim — the dimming is INFORMATION (the board behind
     is not tappable), not decoration — and only drops the fade to it. */
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-scrim {
    opacity: 1;
    transition: none;
  }
  /* ── WHERE THE SHEET STOPS ─────────────────────────────────────────────────────────────
     It used to be bottom:0 — over the top of the bottom nav, which it outranks (192 > 190).
     With a tray of any real length that produced the reported "renders partially cut off":
     the scroll region ran flush to the last pixel of the viewport, so the row at the fold
     was sliced through the middle against nothing, with no edge, no nav and no other sign
     that the list continued. The sheet's own padding-bottom could not help — it is INSIDE
     the scroller, so it is only ever on screen once you have already reached the end.

     Ending above the nav gives the sheet a real bottom edge, so a half-visible row reads as
     "there is more" instead of as a broken card, and leaves the app's primary navigation on
     screen (dimmed by the scrim above it, which still takes the tap and dismisses — the
     tray is a panel with a scrim, not a modal that should swallow the whole chrome).
     max-height comes down by the same amount, so the sheet keeps its scrim margin at the
     top rather than growing into it. */
  body.mobile-office.mo-active .mo-dp-sheet {
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(56px + env(safe-area-inset-bottom, 0px));
    z-index: 192;
    max-height: 72dvh;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    background: var(--gray-100);
    border-radius: 14px 14px 0 0;
    border-bottom: 1px solid var(--border);
    padding-bottom: 12px;
    box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.3);
    /* ── P5: IT ARRIVES AND IT LEAVES ─────────────────────────────────────────────
       This sheet used to TELEPORT: the stylesheet carried one transition in 2,055
       lines and no entrance at all, so a panel that covers three-quarters of the
       screen simply existed on the next frame. The shell adds .mo-in one frame after
       inserting the sheet, which is what the transition runs to; removing it plays
       the exit, and _moDpCloseTray waits for that before un-rendering.

       TRANSFORM, never height/top: a transform is composited, costs no layout on any
       of the rows inside the sheet, and cannot shift the board underneath. The curve
       is the platform's own sheet easing — fast out of the gate, long settle. */
    transform: translateY(101%);
    transition: transform 0.26s cubic-bezier(0.32, 0.72, 0, 1);
    will-change: transform;
  }
  body.mobile-office.mo-active .mo-dp-sheet.mo-in {
    transform: translateY(0);
  }
  /* While a finger is on the grab handle the sheet must track it EXACTLY — a
     transition here would make it lag behind the thumb, which is the single most
     obvious way a drag-to-dismiss reads as fake. */
  body.mobile-office.mo-active .mo-dp-sheet.mo-dragging {
    transition: none;
  }
  /* Reduced motion: no slide at all. The sheet is simply there — which is exactly
     what it did before this block existed, so the fallback is the shipped
     behaviour rather than a degraded guess at one. */
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-sheet {
    transform: none;
    transition: none;
    will-change: auto;
  }
  /* ── The grab handle is now a real control ────────────────────────────────────
     It drew the universal "drag me down to dismiss" pill and did nothing — an
     affordance with no behaviour behind it, which is worse than no affordance.
     The visible pill is unchanged (::after, same 36x4 at the same optical
     position); what changes is that the ELEMENT is a full-width 26px band, so
     there is something a thumb can actually land on, and touch-action: none is
     what hands the vertical gesture to the shell's pointer handlers instead of to
     the sheet's own scroller. */
  body.mobile-office.mo-active .mo-dp-grab {
    width: 100%;
    height: 26px;
    display: grid;
    place-items: center;
    margin: 0;
    background: transparent;
    touch-action: none;
    cursor: grab;
  }
  body.mobile-office.mo-active .mo-dp-grab::after {
    content: '';
    width: 36px;
    height: 4px;
    border-radius: 2px;
    background: var(--gray-300);
  }
  body.mobile-office.mo-active .mo-dp-grab:active {
    cursor: grabbing;
  }
  body.mobile-office.mo-active .mo-dp-grab:active::after {
    background: var(--gray-500);
    width: 44px;
  }
  body.mobile-office.mo-active .mo-dp-sheet-title {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 4px 12px 8px 20px;
  }
  body.mobile-office.mo-active .mo-dp-sheet-title h2 {
    flex: 1;
    min-width: 0;
    font-size: 15px;
    font-weight: 700;
    color: var(--gray-900);
  }
  body.mobile-office.mo-active .mo-dp-trayrow {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0 16px 8px;
    padding: 12px 14px;
    background: var(--surface-base);
    border: 1px solid var(--border);
    border-radius: 10px;
  }
  body.mobile-office.mo-active .mo-dp-trayrow .mo-dp-stopmain {
    flex: 1;
    min-width: 0;
  }
  body.mobile-office.mo-active .mo-dp-assignbtn {
    flex-shrink: 0;
  }
  body.mobile-office.mo-active .mo-dp-cand {
    display: flex;
    align-items: center;
    gap: 12px;
    width: calc(100% - 32px);
    margin: 0 16px 8px;
    padding: 10px 14px;
    min-height: 56px;
    text-align: left;
    background: var(--surface-base);
    border: 1px solid var(--border);
    border-radius: 10px;
    font-family: inherit;
    font-size: inherit;
    color: inherit;
    cursor: pointer;
  }
  body.mobile-office.mo-active .mo-dp-cand:active {
    background: var(--surface-inset);
  }
  body.mobile-office.mo-active .mo-dp-cand.mo-overcap {
    opacity: 0.75;
  }
  body.mobile-office.mo-active .mo-dp-fits {
    flex-shrink: 0;
    font-size: 11px;
    font-weight: 700;
    color: var(--green);
  }
  body.mobile-office.mo-active .mo-dp-over {
    flex-shrink: 0;
    font-size: 11px;
    font-weight: 700;
    color: var(--amber);
  }
  body.mobile-office.mo-active .mo-dp-mismatch {
    font-size: 11px;
    font-weight: 600;
    color: var(--amber);
  }

  /* ── The adopted dispatch dialogs: phone geometry only ──────────────────────────
        Bottom-sheet the six dialogs the Phase 2 flows raise. Behavior, wiring and
        content are untouched — the same rule as the panel takeover. ───────────────── */
  body.mobile-office.mo-active #bm-dest-modal.modal-backdrop,
  body.mobile-office.mo-active #bm-confirm-modal.modal-backdrop,
  body.mobile-office.mo-active #day-note-modal.modal-backdrop,
  body.mobile-office.mo-active #confirmed-route-change-modal.modal-backdrop,
  body.mobile-office.mo-active #route-changes-modal.modal-backdrop,
  body.mobile-office.mo-active #lock-reassign-modal.modal-backdrop {
    padding: 0;
    align-items: flex-end;
  }
  body.mobile-office.mo-active #bm-dest-modal .modal,
  body.mobile-office.mo-active #bm-confirm-modal .modal,
  body.mobile-office.mo-active #day-note-modal .modal,
  body.mobile-office.mo-active #confirmed-route-change-modal .modal,
  body.mobile-office.mo-active #route-changes-modal .modal,
  body.mobile-office.mo-active #lock-reassign-modal .modal {
    /* 100% of the backdrop, NOT 100vw — the app keeps a stable scrollbar gutter,
       so vw units resolve 7px short of the backdrop's real edge-to-edge. */
    width: 100%;
    max-width: none;
    max-height: 92dvh;
    margin: 0;
    border-radius: 14px 14px 0 0;
    display: flex;
    flex-direction: column;
  }
  body.mobile-office.mo-active #bm-dest-modal .modal-body,
  body.mobile-office.mo-active #bm-confirm-modal .modal-body,
  body.mobile-office.mo-active #day-note-modal .modal-body,
  body.mobile-office.mo-active #confirmed-route-change-modal .modal-body,
  body.mobile-office.mo-active #route-changes-modal .modal-body,
  body.mobile-office.mo-active #lock-reassign-modal .modal-body {
    flex: 1;
    min-height: 0;
    overflow: auto;
    -webkit-overflow-scrolling: touch;
  }
  body.mobile-office.mo-active #bm-dest-modal .modal-footer,
  body.mobile-office.mo-active #bm-confirm-modal .modal-footer,
  body.mobile-office.mo-active #day-note-modal .modal-footer,
  body.mobile-office.mo-active #confirmed-route-change-modal .modal-footer,
  body.mobile-office.mo-active #route-changes-modal .modal-footer,
  body.mobile-office.mo-active #lock-reassign-modal .modal-footer {
    flex-wrap: wrap;
    gap: 8px;
    padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  }
  body.mobile-office.mo-active #bm-dest-modal .modal-footer button,
  body.mobile-office.mo-active #bm-confirm-modal .modal-footer button,
  body.mobile-office.mo-active #day-note-modal .modal-footer button,
  body.mobile-office.mo-active #confirmed-route-change-modal .modal-footer button,
  body.mobile-office.mo-active #route-changes-modal .modal-footer button,
  body.mobile-office.mo-active #lock-reassign-modal .modal-footer button {
    min-height: 44px;
  }
  /* iOS zoom guard — the tech layer's 16px input rule, for the note textarea. */
  body.mobile-office.mo-active #day-note-modal textarea,
  body.mobile-office.mo-active #day-note-modal input {
    font-size: 16px;
  }
  /* Touch sizing inside the destination sheet; the day strip stays one row. */
  body.mobile-office.mo-active #bm-dest-modal .bm-tech-row {
    min-height: 56px;
  }
  body.mobile-office.mo-active #bm-dest-modal .bm-day-tabs {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    flex-wrap: nowrap;
  }
  body.mobile-office.mo-active #bm-dest-modal .bm-seg button {
    min-height: 40px;
  }

  /* ── THE SMS CEILING (presentation half — the engine half lives in
        bmCommitMove / _crcCommit / _routeChangesConfirm, each refusing under
        mo-active). Anything that texts a customer is desktop-only. ────────────────── */
  body.mobile-office.mo-active #bm-confirm-notify,
  body.mobile-office.mo-active #btn-crc-confirm-notify,
  body.mobile-office.mo-active #btn-rc-confirm-notify {
    display: none;
  }

  /* ── The bulk bar: selection state is desktop chrome (the shell's own sheets
        carry the flow) — but the post-commit UNDO state is kept, above the nav. ────── */
  body.mobile-office.mo-active #bm-bar-host .bm-bar {
    display: none;
  }
  body.mobile-office.mo-active #bm-bar-host .bm-bar--done {
    display: flex;
    left: 12px;
    right: 12px;
    transform: none;
    bottom: calc(56px + env(safe-area-inset-bottom, 0px) + 12px);
    z-index: 198;
    white-space: normal;
  }
  body.mobile-office.mo-active #bm-bar-host .bm-bar--done .tl-bulk-btn {
    min-height: 40px;
  }

  /* ═══════════════════════════════════════════════════════════════════════════════
     PHASE 3 — METRICS ADOPTION (read-only) + CLIENTS / FINANCE + DEPARTMENTS
     Metrics is the panel-takeover pattern applied to a whole view: switchView
     renders the COMPANY'S REAL BOARD into #view-metrics (active), and
     body.mo-vw-metrics — written only by mobile-office.js while its metrics
     screen is up — shows it between the shell's appbar and nav, reflowed to one
     column. Read-only is two-layer like the SMS ceiling: these rules hide every
     board-edit entry point, and _mwSave (metrics.js) refuses guard-first under
     mo-active. Both halves pinned in tests/mobile-office-gate.test.js §7.
     ═══════════════════════════════════════════════════════════════════════════════ */

  /* ── The adopted view's frame ──────────────────────────────────────────────────
     .main-content becomes a fixed viewport strip between appbar (48px) and nav
     (56px). z 55 sits BELOW #mo-root (60): the shell's own chrome paints over
     the board, and the root goes click-through so the strip receives touches.
     The ticket panel takeover (195) and every real modal still win — a drill
     row's ticket tap lands on the Phase 1 panel exactly as everywhere else. */
  body.mobile-office.mo-active.mo-vw-metrics #app-body .main-content {
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    top: calc(48px + env(safe-area-inset-top, 0px));
    bottom: calc(56px + env(safe-area-inset-bottom, 0px));
    z-index: 55;
    background: var(--gray-100);
    overflow: hidden;
  }
  body.mobile-office.mo-active.mo-vw-metrics #mo-root {
    pointer-events: none;
    background: transparent;
  }
  body.mobile-office.mo-active.mo-vw-metrics #mo-root .mo-appbar,
  body.mobile-office.mo-active.mo-vw-metrics #mo-root .mo-nav {
    pointer-events: auto;
  }
  /* The unavailable-fallback card must not ghost behind a live board; when the
     adoption failed the class is dropped and the scroll region shows normally. */
  body.mobile-office.mo-active.mo-vw-metrics #mo-root .mo-scroll {
    visibility: hidden;
  }

  /* ═══════════════════════════════════════════════════════════════════════════════
     SCHEDULE ADOPTION — the field workflow inside the shell
     The second view-adoption screen, and the same four rules as Metrics directly
     above, for the same reason: switchView('my-schedule') renders the technician's
     REAL five-step workflow into #view-my-schedule, and body.mo-vw-schedule — written
     only by mobile-office.js while its Schedule screen is up — shows it between the
     shell's appbar and nav.

     WHAT IS NOT HERE IS THE POINT. There is no layout, no type scale and no touch
     sizing below, because #view-my-schedule is ALREADY phone-shaped: the ~270
     body.role-technician rules in styles.css do that, and auth.js puts that class on
     the body for exactly the roles this tab is offered to (a field-technician role
     that also holds an office tab — see _applyRoleBodyClass). Re-stating any of that
     geometry here would fork it, and the fork would drift the first time the field
     workflow changes shape. This block only decides WHERE the view sits.

     The Metrics block's read-only rules have no counterpart either, deliberately: the
     technician workflow is not a read-only surface. Its writes are the whole point of
     the screen, they are the desktop path executing unmodified (tech-workflow-sync's
     own service-role seam), and every one of them is already scoped to the signed-in
     technician's own stops — so there is nothing here to take away.
     ═══════════════════════════════════════════════════════════════════════════════ */
  body.mobile-office.mo-active.mo-vw-schedule #app-body .main-content {
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    top: calc(48px + env(safe-area-inset-top, 0px));
    bottom: calc(56px + env(safe-area-inset-bottom, 0px));
    z-index: 55;
    background: var(--gray-100);
    overflow: hidden;
  }
  body.mobile-office.mo-active.mo-vw-schedule #mo-root {
    pointer-events: none;
    background: transparent;
  }
  body.mobile-office.mo-active.mo-vw-schedule #mo-root .mo-appbar,
  body.mobile-office.mo-active.mo-vw-schedule #mo-root .mo-nav {
    pointer-events: auto;
  }
  /* The unavailable-fallback card must not ghost behind a live workflow; when the
     adoption failed the class is dropped and the scroll region shows normally. */
  body.mobile-office.mo-active.mo-vw-schedule #mo-root .mo-scroll {
    visibility: hidden;
  }
  /* The desktop view fills the strip. #view-my-schedule is a flex column whose
     .ms-body is the scrollport the workflow's sticky step CTA sticks to, so the
     .view itself must be allowed to BE that column inside a fixed parent rather
     than inheriting the desktop's page-level height. min-height:0 is what lets
     .ms-body scroll instead of the strip growing past the nav. */
  body.mobile-office.mo-active.mo-vw-schedule #app-body .main-content #view-my-schedule.active {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    width: 100%;
  }

  /* ── THE JOB SCREEN IS A TAKEOVER, AND THE STRIP TRAPS IT ──────────────────────
     `.msm-job` (styles.css, the Option C field flow) is `position: fixed; inset: 0;
     z-index: 215` — deliberately between the global header (210) and the modal band
     (220), because the Job screen is a full-screen takeover with its own back button.
     That is the screen the built-in Technician gets, and this file's header calls
     everything over 200 "paints over the shell".

     IT COULD NOT GET THERE, AND THE REASON IS NOT ITS OWN z-index. The strip above is
     `position: fixed`, and a fixed box ALWAYS establishes a stacking context — with no
     z-index at all, it still paints at the 0 level of the root context. So 215 was
     resolved INSIDE the strip and the whole takeover sat under #mo-root (60). Measured
     on the unmodified bundle at 390×844: dropping the strip to `z-index: auto` changed
     nothing, and forcing #mo-root down to 1 changed nothing either — only lifting the
     STRIP moved the paint, which is the proof that the strip is the context.

     The box itself still spanned the viewport (nothing here is a containing block for a
     fixed child), so the shell chrome painted across BOTH of its ends: the appbar (0–48)
     buried `.msm-back`, and the tab bar (787–844) buried `.msm-unable` — 788–832, inside
     it to the pixel, with `elementFromPoint` at that button's centre returning the nav.
     So "Unable to Complete is not showing" was literally true, and it was the ONE
     control a blocked technician has: the primary CTA is the footer's first child and
     cleared the bar, the escape hatch beneath it did not.

     ONLY a field-technician role that ALSO holds an office tab can reach this — the
     built-in Technician never mounts this shell (_moCanMount refuses a pure field
     technician), which is why it went unseen until a custom role opened the Job screen.

     215 IS NOT A NEW NUMBER, it is `.msm-job`’s own, restated at the level that now
     carries it. Lifting the strip to the takeover’s band reproduces the built-in
     layer’s relationships exactly: over the shell (60) and the global header (210),
     under the Unable modal and every other .modal-backdrop (220) and the photo viewer.
     Scoped by `:has(.msm-job)` so it applies only while the takeover is mounted — the
     Day Overview keeps the strip at 55 with the shell chrome over it, which is the
     adoption design, and tapping Back restores it with no JavaScript. */
  body.mobile-office.mo-active.mo-vw-schedule #app-body .main-content:has(.msm-job) {
    z-index: 215;
  }

  /* ── READ-ONLY: every board-editing entry point disappears ─────────────────────
     Add Widget, the drag grip, the resize corner and the ⋮ actions menu are all
     board mutations (each commits through _mwSave — the engine guard behind
     this). The filter button stays VISIBLE as state ("these numbers are
     filtered") but stops being a door to the filter editor. */
  body.mobile-office.mo-active #view-metrics #btn-add-chart,
  body.mobile-office.mo-active #view-metrics .mw-grip,
  body.mobile-office.mo-active #view-metrics .mw-resize,
  body.mobile-office.mo-active #view-metrics .mw-actions {
    display: none !important;
  }
  body.mobile-office.mo-active #view-metrics .mw-title-filter {
    pointer-events: none;
  }

  /* ── Reflow: the board at 390px ────────────────────────────────────────────────
     The shell appbar already titles the screen, so the view's own topbar (title
     + the hidden Add Widget) yields its 48px. Cards carry inline PIXEL widths
     from the desktop model — width:100% !important is the one honest way to a
     single column; heights stay the model's own (ApexCharts drew for them). */
  body.mobile-office.mo-active #view-metrics .view-topbar {
    display: none;
  }
  body.mobile-office.mo-active #view-metrics .mw-toolbar {
    flex-wrap: wrap;
    gap: 10px;
    padding: 10px 14px 4px;
  }
  body.mobile-office.mo-active #view-metrics .mw-tabbar {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    flex-wrap: nowrap;
  }
  body.mobile-office.mo-active #view-metrics .mw-tab {
    min-height: 44px;
    flex-shrink: 0;
  }
  body.mobile-office.mo-active #view-metrics .mw-grid {
    padding: 12px 14px calc(24px + env(safe-area-inset-bottom, 0px));
    gap: 12px;
  }
  body.mobile-office.mo-active #view-metrics .mw-card {
    width: 100% !important;
    min-width: 0;
  }
  /* iOS zoom guard on the toolbar's real inputs (TomSelect renders its own). */
  body.mobile-office.mo-active #view-metrics .mw-select,
  body.mobile-office.mo-active #view-metrics .ts-control,
  body.mobile-office.mo-active #view-metrics .ts-control input {
    font-size: 16px;
  }
  /* The drill-down: full-width sheet from the right; companion offsets are a
     desktop-panel geometry and never apply under the full-bleed phone panel. */
  body.mobile-office.mo-active .mw-drill {
    width: 100%;
    max-width: 100%;
    border-radius: 0;
    right: 0 !important;
    left: auto !important;
  }

  /* ── Clients (shell-native cards + read-only profile) ──────────────────────────── */
  body.mobile-office.mo-active .mo-cl-avatar {
    width: 32px;
    height: 32px;
    font-size: 12px;
    --mo-avatar-color: var(--blue);
  }
  body.mobile-office.mo-active .mo-cl-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
    margin-top: 6px;
  }
  body.mobile-office.mo-active .mo-cl-tag {
    display: inline-flex;
    align-items: center;
    height: 20px;
    padding: 0 8px;
    border-radius: 99px;
    border: 1px solid var(--border-strong);
    background: var(--surface-inset);
    font-size: 10.5px;
    font-weight: 600;
    color: var(--text-secondary);
    white-space: nowrap;
  }

  /* ── Finance (shell-native grouped balance list, read-only) ────────────────────── */
  body.mobile-office.mo-active .mo-fin-sub {
    padding: 8px 20px 2px;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-secondary);
  }
  body.mobile-office.mo-active .mo-fin-group {
    display: flex;
    align-items: center;
    gap: 8px;
    width: calc(100% - 32px);
    margin: 10px 16px 8px;
    padding: 9px 12px;
    min-height: 44px;
    border: 0;
    border-left: 4px solid var(--mo-accent, var(--blue));
    border-radius: 8px;
    background: var(--surface-inset);
    font-family: inherit;
    text-align: left;
    cursor: pointer;
  }
  body.mobile-office.mo-active .mo-fin-group b {
    font-size: 12.5px;
    font-weight: 700;
    color: var(--text-primary);
  }
  body.mobile-office.mo-active .mo-fin-group .mo-fin-chev {
    margin-left: auto;
  }
  body.mobile-office.mo-active .mo-fin-group .mo-fin-chev svg {
    transition: transform 0.15s ease;
    transform: rotate(0deg);
  }
  body.mobile-office.mo-active .mo-fin-group .mo-fin-chev.mo-open svg {
    transform: rotate(90deg);
  }
  body.mobile-office.mo-active .mo-fin-row {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 5px;
    width: 100%;
    padding: 12px 14px;
    border: 0;
    background: transparent;
    font-family: inherit;
    font-size: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
  }
  body.mobile-office.mo-active .mo-fin-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
  }
  body.mobile-office.mo-active .mo-fin-amt {
    font-size: 15px;
    font-weight: 700;
    color: var(--gray-900);
    font-variant-numeric: tabular-nums;
    flex-shrink: 0;
  }
  body.mobile-office.mo-active .mo-fin-amt.mo-fin-zero {
    color: var(--text-faint);
    font-weight: 600;
  }
  body.mobile-office.mo-active .mo-fin-mid {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
  }
  /* Stage tags: the bordered-chip anatomy (ink + border on the base surface) —
     the ONE chip shape that measured ≥4.5:1 in both themes in Phase 2. */
  body.mobile-office.mo-active .mo-fin-tag {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    height: 20px;
    padding: 0 8px;
    border-radius: 99px;
    border: 1px solid currentColor;
    background: transparent;
    font-size: 10.5px;
    font-weight: 700;
    white-space: nowrap;
  }
  body.mobile-office.mo-active .mo-fin-tag-deposit,
  body.mobile-office.mo-active .mo-fin-tag-remainder {
    color: var(--amber);
  }
  body.mobile-office.mo-active .mo-fin-tag-full {
    color: var(--red);
  }
  body.mobile-office.mo-active .mo-fin-tag-chasing {
    color: var(--text-secondary);
  }
  body.mobile-office.mo-active .mo-fin-tag-paid {
    color: var(--green);
  }
  body.mobile-office.mo-active .mo-fin-tag-none {
    color: var(--text-secondary);
  }

  /* ── Departments (More) ────────────────────────────────────────────────────────── */
  body.mobile-office.mo-active .mo-dept-on {
    color: var(--green);
    flex-shrink: 0;
  }

  /* ═══════════════════════════════════════════════════════════════════════════════════
     TOUCH FEEDBACK — THE APP ANSWERS THE FINGER
     ───────────────────────────────────────────────────────────────────────────────────
     THE DEFECT. This stylesheet carried 18 `cursor: pointer` declarations and FIVE
     `:active` rules. The thirteen without one included every bottom tab, every ticket
     card, every filter chip, every list row, every notification, every dispatch row and
     every finance row — that is, the twelve most-tapped things in the product (the
     thirteenth, .mo-dp-scrim, is a dismiss scrim and correctly needs none).

     The browser was doing its part the whole time: a pressed card genuinely matched
     :active. The page simply had nothing to say back, so the screen was pixel-identical
     from the moment the finger landed until the action completed. Pair that with the
     160–229ms the old whole-screen rebuild took to produce a result and, for a fifth of
     a second, a tap that registered and a tap that was dropped looked exactly the same.
     A user who taps again is reading the screen correctly. THAT is the "I have to click
     things twice" report — and reproducing it never required a single tap to actually be
     lost (56/56 taps registered under a realtime update firing mid-press).

     WHY transform: scale AND NOT padding / border / margin. A scale is composited: it
     shifts no neighbour, triggers no layout, and cannot move the control out from under
     the finger that is pressing it. Any pressed state built from box metrics does all
     three, and on a list that is how a press ends up opening the row below.

     -webkit-tap-highlight-color goes transparent HERE and only here — the platform flash
     it replaces is Android-only, absent on iOS for composite card buttons, and would
     otherwise double up with the real state below.

     THE ONE THING A REAL HANDSET STILL HAS TO CONFIRM: Chrome delays applying :active
     inside a scrollable container while it decides whether the gesture is a scroll or a
     tap. That delay could not be measured here — CDP-synthesized touch does not drive
     Chrome's :active state machine at all (proved on a bare page: synthesized MOUSE down
     applies :active, synthesized TOUCH down never does). If the delay proves noticeable
     on the ticket list, the fallback is a pointerdown-driven class on the delegated root,
     which is unconditional. This is nine declarations and entirely reversible; it is the
     right thing to try first. */
  body.mobile-office.mo-active .mo-nav-btn,
  body.mobile-office.mo-active .mo-iconbtn,
  body.mobile-office.mo-active .mo-btn,
  body.mobile-office.mo-active .mo-chip,
  body.mobile-office.mo-active .mo-tk-main,
  body.mobile-office.mo-active .mo-tk-call,
  body.mobile-office.mo-active .mo-row,
  body.mobile-office.mo-active .mo-notif,
  body.mobile-office.mo-active .mo-dp-day,
  body.mobile-office.mo-active .mo-dp-tech,
  body.mobile-office.mo-active .mo-dp-cand,
  body.mobile-office.mo-active .mo-dp-note,
  body.mobile-office.mo-active .mo-dp-tray,
  body.mobile-office.mo-active .mo-dp-mini,
  body.mobile-office.mo-active .mo-dp-stopmain,
  body.mobile-office.mo-active .mo-fin-group,
  body.mobile-office.mo-active .mo-fin-row,
  body.mobile-office.mo-active.mo-panel-open .mo-pb-btn {
    /* Kills the 300ms tap delay and the ghost click that rides on it — the exact
       treatment styles.css gives the TECHNICIAN layer, which the office layer never
       got. `touch-action` appeared ZERO times in this file before this line. */
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
    transition:
      background-color 0.12s ease,
      transform 0.12s ease;
  }

  /* Rows and cards — a small, calm compression that reads instantly at arm's length
     without the row appearing to move. */
  body.mobile-office.mo-active .mo-tk-main:active,
  body.mobile-office.mo-active .mo-row:active,
  body.mobile-office.mo-active .mo-notif:active,
  body.mobile-office.mo-active .mo-dp-stopmain:active,
  body.mobile-office.mo-active .mo-dp-note:active,
  body.mobile-office.mo-active .mo-dp-tray:active,
  body.mobile-office.mo-active .mo-fin-group:active,
  body.mobile-office.mo-active .mo-fin-row:active {
    background: var(--surface-inset);
    transform: scale(0.985);
  }
  /* .mo-dp-tech and .mo-dp-cand already had the background half of this; they get the
     movement so the whole shell answers a press the same way. */
  body.mobile-office.mo-active .mo-dp-tech:active,
  body.mobile-office.mo-active .mo-dp-cand:active {
    transform: scale(0.985);
  }
  /* Small targets — tabs, chips, icon buttons, day cells — take a deeper scale
     because at 32–56px a 1.5% compression is not perceptible. */
  body.mobile-office.mo-active .mo-nav-btn:active,
  body.mobile-office.mo-active .mo-chip:active,
  body.mobile-office.mo-active .mo-dp-day:active,
  body.mobile-office.mo-active .mo-tk-call:active,
  body.mobile-office.mo-active .mo-dp-mini:active {
    background: var(--surface-inset);
    transform: scale(0.94);
  }
  body.mobile-office.mo-active .mo-iconbtn:active,
  body.mobile-office.mo-active .mo-btn:active,
  body.mobile-office.mo-active.mo-panel-open .mo-pb-btn:active {
    transform: scale(0.94);
  }
  /* A disabled control must not pretend to answer. The tray pill goes disabled for
     the duration of its fresh-baseline pull and has its own busy state to show. */
  body.mobile-office.mo-active .mo-btn:disabled:active,
  body.mobile-office.mo-active .mo-dp-tray:disabled:active,
  body.mobile-office.mo-active .mo-iconbtn:disabled:active {
    transform: none;
  }

  /* REDUCED MOTION — keep the colour change (that is the feedback), drop the movement.
     Driven by body.mo-reduce-motion, which mobile-office.js publishes from
     matchMedia('(prefers-reduced-motion: reduce)'), rather than by a nested @media:
     this sheet's contract is exactly ONE media query (the phone boundary), the JS has
     to make the same decision for the sheet transitions anyway, and one source of
     truth cannot drift from itself. */
  body.mobile-office.mo-active.mo-reduce-motion .mo-nav-btn,
  body.mobile-office.mo-active.mo-reduce-motion .mo-iconbtn,
  body.mobile-office.mo-active.mo-reduce-motion .mo-btn,
  body.mobile-office.mo-active.mo-reduce-motion .mo-chip,
  body.mobile-office.mo-active.mo-reduce-motion .mo-tk-main,
  body.mobile-office.mo-active.mo-reduce-motion .mo-tk-call,
  body.mobile-office.mo-active.mo-reduce-motion .mo-row,
  body.mobile-office.mo-active.mo-reduce-motion .mo-notif,
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-day,
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-tech,
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-cand,
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-note,
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-tray,
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-mini,
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-stopmain,
  body.mobile-office.mo-active.mo-reduce-motion .mo-fin-group,
  body.mobile-office.mo-active.mo-reduce-motion .mo-fin-row,
  body.mobile-office.mo-active.mo-reduce-motion.mo-panel-open .mo-pb-btn {
    transition: background-color 0.12s ease;
  }
  body.mobile-office.mo-active.mo-reduce-motion .mo-nav-btn:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-iconbtn:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-btn:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-chip:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-tk-main:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-tk-call:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-row:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-notif:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-day:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-tech:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-cand:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-note:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-tray:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-mini:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-dp-stopmain:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-fin-group:active,
  body.mobile-office.mo-active.mo-reduce-motion .mo-fin-row:active,
  body.mobile-office.mo-active.mo-reduce-motion.mo-panel-open .mo-pb-btn:active {
    transform: none;
  }

  /* ── Focus visibility (app convention: :focus-visible only) ────────────────────── */
  body.mobile-office.mo-active .mo-nav-btn:focus-visible,
  body.mobile-office.mo-active .mo-iconbtn:focus-visible,
  body.mobile-office.mo-active .mo-btn:focus-visible,
  body.mobile-office.mo-active .mo-chip:focus-visible,
  body.mobile-office.mo-active .mo-row:focus-visible,
  body.mobile-office.mo-active .mo-notif:focus-visible,
  body.mobile-office.mo-active .mo-dp-day:focus-visible,
  body.mobile-office.mo-active .mo-dp-tech:focus-visible,
  body.mobile-office.mo-active .mo-dp-stopmain:focus-visible,
  body.mobile-office.mo-active .mo-dp-mini:focus-visible,
  body.mobile-office.mo-active .mo-dp-tray:focus-visible,
  body.mobile-office.mo-active .mo-dp-cand:focus-visible,
  body.mobile-office.mo-active .mo-dp-note:focus-visible,
  body.mobile-office.mo-active .mo-fin-group:focus-visible,
  body.mobile-office.mo-active .mo-fin-row:focus-visible,
  body.mobile-office.mo-active .mo-tk-main:focus-visible {
    outline: 2px solid var(--focus-ring, var(--blue-dark));
    outline-offset: 2px;
  }
}
