/* ============================================================================
   Converse Desk — phone layout (<=720px)

   Loaded AFTER styles.css so it can override without !important wars.

   Scope discipline: everything here is behind @media (max-width:720px) AND
   scoped to #mainTabs / the Converse panels / body[data-cdmobile="on"], so it
   cannot leak into Contacts, Deals, Email or anything else that happens to
   share a class name. data-cdmobile is "on" whenever the Converse module is
   active (any width) — the media query is what makes these rules phone-only.
   .cv-wrap in particular is SHARED WITH THE EMAIL DESK — rules that touch it
   are scoped to #panel-inbox / #inboxWrap on purpose.

   720 is the app's one "narrow". 900 stays for the module chrome. The old 820
   was folded into 720 on 2026-08-17 so a tablet no longer lands in a band where
   the thread list has stacked but the bottom bar has not appeared.

   THE PHONE QUERY IS NOT WIDTH ALONE (2026-08-20). A phone turned sideways is
   ~932px WIDE and ~430px TALL: on width alone every rule in this file switched
   off mid-session and the desk fell back to the desktop card — whose fixed
   height cannot fit a 430px screen, so the thread simply froze ("turned
   sideways it doesn't allow us to scroll", Sam). The phone query is therefore
   `(max-width:720px), (max-height:560px) and (pointer:coarse)` — narrow, OR
   short AND touch. 560 clears a FULL-SCREEN iPad landscape (768) while catching
   an iPhone landscape (~430); pointer:coarse keeps a short desktop window out.
   (An iPad in Stage Manager or Split View CAN be under 560 tall, and then it
   gets the phone presentation at ~1180px wide. Accepted: a window that short is
   closer to a phone than a desk.) Any rule keyed to the phone layout must use
   BOTH halves or the two orientations disagree — and a rule whose premise is
   genuinely about WIDTH (crushed controls at 390px) needs its own nested
   `@media (max-width:720px)` so it stops firing on a 932px landscape screen.

   2026-08-17 (second pass): the whole phone presentation now follows the PWA
   mockup (artifact 3fec2043 "Converse Desk, Desk to Pocket") rather than
   squeezing the desktop panels. That means: the global topbar hides on the
   phone desk and #mainTabs becomes the app header band; thread rows carry
   avatar discs; the Contacts/Leads tables restack into tappable person-rows;
   tab strips become pill segments; Queue/Compose/Settings render as cards on
   the page background; and type inside the desk uses the native system stack
   (the mockup's font) instead of Inter/Space Grotesk.
   ========================================================================= */

/* ============================================================================
   THE FOUR-STAGE TAB ROW (desktop / tablet)

   The mockup's desktop chrome is the same four destinations as the phone's
   bottom bar: 01 Inbox · 02 Modules · 03 Queue · 04 Settings. The seven real
   .tab-btn elements stay in the DOM as the single definition of each stage —
   they are hidden, and these four click them.

   Scoped to data-view="standard". In Classic the .cd-tabrow is display:contents
   and IS the tab row, so hiding it there would leave no tabs at all.
   ========================================================================= */
:root[data-view="standard"] #mainTabs .cd-tabrow { display: none; }

/* NO look defined here. Sam, 2026-08-17: "use pills to match the rest". The
   segmented-pill styling already exists in styles.css for .cd-tabrow, built from
   the earlier unified design mockup — so .cd-fourtabs was ADDED to those same
   selectors rather than given a copy of them here. One definition, and the two
   rows cannot drift apart.

   The button reset below is the one thing the pill rules do not cover — it used to
   come from the base .tab-btn class, which these deliberately no longer carry (that
   class made `#mainTabs .tab-btn.active` match a button with no data-tab, and the
   app lost track of which stage was open). */
.cd-fourtabs button {
  background: none; color: var(--ink-dim);
  font: inherit; cursor: pointer;
}
.cd-fourtabs button:hover { color: var(--ink); }

/* #mainTabs is in the selector for SPECIFICITY, not scoping: the pill look comes
   from `:root[data-view="standard"] .cd-fourtabs { display:flex }` in styles.css
   (0,3,0), and a bare `.cd-fourtabs { display:none }` LOSES to it — which is how
   the four-pill row showed up on Sam's phone stacked on top of the bottom bar. */
@media (max-width: 720px), (max-height: 560px) and (pointer: coarse) { #mainTabs .cd-fourtabs { display: none; } }

/* Module ▾ and More sit top-right of the page header at EVERY width, as drawn. */
:root[data-view="standard"] #mainTabs .cd-actions {
  margin-left: auto; display: flex; gap: 6px; align-items: center; flex: 0 0 auto;
}

/* ============================================================================
   HEADER BAND WHITESPACE — Sam, 2026-08-20: "what the actual fuck is with all the
   white empty space in the top menu bar". Desktop-sized margins stacked at phone
   width: every header row carried 10-16px of gap. Halved across the band; rows keep
   their structure, they just stop hoarding air.
   ========================================================================= */
@media (max-width: 720px) {
  body[data-cdmobile="on"] .ll-head { margin-bottom: 4px !important; gap: 4px !important; }
  body[data-cdmobile="on"] #viewsBar { margin: 2px 0 4px !important; }
  body[data-cdmobile="on"] .views-bar { gap: 4px; }
  body[data-cdmobile="on"] #ctToolSlotA, body[data-cdmobile="on"] #ctToolSlotB { gap: 4px; }
  body[data-cdmobile="on"] .ct-tools, body[data-cdmobile="on"] .ll-tools { margin: 2px 0 4px !important; gap: 4px !important; }
  body[data-cdmobile="on"] #addContactBtn, body[data-cdmobile="on"] .ll-addrow { margin-bottom: 6px !important; }
  body[data-cdmobile="on"] #ctReachBar { margin: 0 0 4px !important; }
  /* The toolbar and the reach tabs sit directly under each other (Sam: "remove the
     white space between list/kanban and need to reach"). The 16px the tabs carry
     inline separates them from the cooldown INSIDE the band — halved on a phone. */
  body[data-cdmobile="on"] .ct-tools { margin-bottom: 2px !important; }
  body[data-cdmobile="on"] .ct-headactions { margin: 0 !important; gap: 4px !important; }
  body[data-cdmobile="on"] #listViewTabs { margin-bottom: 6px !important; }
  /* The New-contact split, compact, on the views row. */
  body[data-cdmobile="on"] #viewsBar .addc-split .btn { padding: 5px 10px; font-size: 12.5px; }
}
/* Narrowest screens: the label goes, the ＋ stays — that is what fits beside the
   dropdown and the two icons on one 375px row. */
@media (max-width: 480px) {
  /* !important: the Standard view's label-shower outranks a bare id selector. */
  #viewsBar .addc-split .ct-lbl-std, #viewsBar .addc-split .ct-lbl-classic { display: none !important; }
  #viewsBar .addc-split .ct-plus { margin: 0; }
  #viewsBar .addc-split #manualBtn { padding: 5px 9px !important; }
  /* (batch-button sizing lives in the 720 block — rules here lost the cascade) */
  body[data-cdmobile="on"] #panel-contacts .card, body[data-cdmobile="on"] #cmView .card { padding-top: 6px; }
}

/* ============================================================================
   MODULE LIST CONTROL STACK — compressed on a phone.
   Sam's 2026-08-20 circle: cooldown row, batch picker, pager and selection bar
   stood 242px tall (the pager alone wrapped to three lines, 112px) before the
   first contact appeared. Nothing is removed — advice text hides, labels the
   controls restate go, everything else tightens onto single wrapped lines.
   The classes are shared by Contacts, Leads and every custom module, so this
   cleans the whole Modules station at once.
   ========================================================================= */
@media (max-width: 720px) {
  /* the explainer sentence is advice, not a control — the input says it all */
  body[data-cdmobile="on"] .ll-cooldown .callout { display: none; }
  body[data-cdmobile="on"] .ll-cooldown { margin-bottom: 6px !important; gap: 6px !important; font-size: 12px; }
  body[data-cdmobile="on"] .ll-cooldown label { font-size: 12px; }
  body[data-cdmobile="on"] .ll-cooldown input { width: 50px !important; padding: 4px 6px !important; font-size: 13px; }

  body[data-cdmobile="on"] .batch-picker { margin-bottom: 4px !important; gap: 4px !important; font-size: 11px; }
  body[data-cdmobile="on"] .batch-picker > span { font-size: 11px; }
  /* Slim enough that the label + all six numbers hold LINE ONE — otherwise the
     "100" chip spills onto the buttons' line and shoves Clear selection down. */
  body[data-cdmobile="on"] .batch-picker .batch-btn { padding: 3px 6px; font-size: 11px; }
  /* All four selection buttons on ONE line (Sam): tight enough that Select all shown ·
     Show selected · Select all · Clear selection hold a 375px row together, so the
     orphaned Clear-selection line dies and the pager and list ride up. */
  body[data-cdmobile="on"] .batch-picker .btn { padding: 3px 6px !important; font-size: 11px !important; margin-left: 0 !important; }

  /* pager: the select IS the "records per page" statement; the label repeated it.
     One wrapped line instead of three. */
  body[data-cdmobile="on"] .pager-bar { margin-bottom: 6px !important; gap: 6px !important; font-size: 11px !important; justify-content: flex-start !important; }
  body[data-cdmobile="on"] #pagerBar .row > span:first-child { display: none; }
  body[data-cdmobile="on"] .pager-bar select { padding: 3px 6px !important; font-size: 11px !important; }
  body[data-cdmobile="on"] .pager-bar .btn { padding: 3px 8px !important; font-size: 11px !important; }
  body[data-cdmobile="on"] .cm-listbar .pg-lbl, body[data-cdmobile="on"] .dt-bar .pg-lbl { display: none; }
  body[data-cdmobile="on"] .cm-listbar { gap: 6px; font-size: 11px; }
  body[data-cdmobile="on"] .cm-listbar .btn { padding: 3px 8px; font-size: 11px; }

  /* selection bar: same buttons, tighter suit */
  body[data-cdmobile="on"] .ct-selbar { padding: 7px 10px !important; gap: 6px !important; font-size: 12px !important; }
  body[data-cdmobile="on"] .ct-selbar .btn { padding: 3px 9px !important; font-size: 11.5px !important; }
  body[data-cdmobile="on"] .dt-bulk { padding: 7px 10px !important; gap: 6px !important; font-size: 12px !important; }
  body[data-cdmobile="on"] .dt-bulk .btn { padding: 3px 9px !important; font-size: 11.5px !important; }

  body[data-cdmobile="on"] .sort-indicator { font-size: 10px; margin-bottom: 4px; opacity: .85; }
}

/* ============================================================================
   COMPOSE — "what this send will actually do"
   Cooldown, Do Not Text and missing numbers all quietly shrink a send. Every
   width, not just the phone: the surprise is the same on a desktop.
   ========================================================================= */
.cs-summary {
  background: var(--panel-raised); border: 1px solid var(--panel-border);
  border-radius: var(--radius, 8px); padding: 10px 12px; margin: 0 0 12px;
}
.cs-path { display: block; margin-bottom: 8px; }
.cs-path b { display: block; font-size: 13px; font-weight: 600; }
.cs-path span { display: block; font-size: 11.5px; color: var(--ink-dim); line-height: 1.4; margin-top: 2px; }
.cs-path.cs-green b { color: var(--confirm); }
.cs-path.cs-amber b { color: var(--signal); }
.cs-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 5px 0; font-size: 12.5px; border-top: 1px solid var(--panel-border);
}
.cs-row span { color: var(--ink-dim); }
.cs-row b { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.cs-row b.cs-green { color: var(--confirm); }
.cs-row b.cs-amber { color: var(--signal); }
.cs-row b.cs-grey  { color: var(--ink-dim); font-family: inherit; font-weight: 400; }

/* The back ✕ on an open thread. Hidden by default; the phone query below shows it.
   Ported from cvInjectMobileStyles(), which never runs — see the note further down. */
#cvHead .cv-back {
  display: none;
  align-items: center; justify-content: center;
  width: 30px; height: 30px; flex: none; padding: 0;
  border: 1px solid var(--panel-border); border-radius: 8px;
  background: var(--panel-raised); color: var(--ink);
  font-size: 15px; line-height: 1; cursor: pointer;
}
#cvHead .cv-back:active { opacity: .6; }

/* The bar is hidden by DEFAULT, outside the media query. It used to rely on a
   `body:not([data-cdmobile="on"])` rule that lived INSIDE the query — so above
   720px nothing hid it at all and it rendered in normal flow at the bottom of
   every desktop page. Base state off; the query below turns it on. */
.cd-mobilebar { display: none; }

/* The thread rows' avatar disc (renderThreads / cvSearch emit it at every width).
   Off by default: the desktop desk keeps its shipped compact rows. The phone
   block below turns it on. */
.cv-t .cv-av { display: none; }

/* #cvTabsHost (the filter pills' wrapper, created by renderThreads) is
   display:contents by DEFAULT — on desktop the tabs row must stay a direct
   grid child of .cv-wrap, byte-for-byte as before the wrapper existed. The
   phone block turns it into a real band. OUTSIDE the media query on purpose. */
#cvTabsHost { display: contents; }

/* ---- Conversation pinning (every width: pins sync across devices) ---- */
/* The pinned strip: initials discs, left to right, iMessage-style. Emitted
   ONLY when something is pinned, so an empty pin list costs zero pixels.
   Scrolls sideways if Sam pins more than fit. */
.cv-pinrow {
  display: flex; gap: 12px; align-items: center;
  padding: 10px 12px;
  background: var(--bg);
  border-bottom: 1px solid var(--panel-border);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.cv-pinrow::-webkit-scrollbar { display: none; }
.cv-pinav {
  position: relative; flex: 0 0 auto;
  background: none; border: 0; padding: 0; cursor: pointer;
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none;
}
.cv-pinav-disc {
  display: grid; place-items: center;
  width: 46px; height: 46px; border-radius: 50%;
  background: var(--signal-dim);
  color: var(--signal-deep, var(--signal));
  font-size: 15px; font-weight: 650;
}
.cv-pinav-disc.cv-pinav-unread {
  background: var(--signal);
  color: var(--on-signal, #fff);
}
.cv-pinav-badge {
  position: absolute; top: -3px; right: -5px;
  background: var(--alert); color: #fff;
  font-family: var(--font-mono); font-size: 9.5px; line-height: 1;
  padding: 2.5px 5px; border-radius: 999px;
  border: 1.5px solid var(--bg);
}
.cv-pin-ic { font-size: 10px; line-height: 1; }
/* The long-press / right-click row menu. */
.cv-rowmenu {
  position: fixed; z-index: 300;
  background: var(--bg);
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  box-shadow: var(--shadow-menu, 0 12px 28px rgba(15, 23, 42, .18));
  overflow: hidden;
  min-width: 140px;
}
.cv-rowmenu button {
  display: block; width: 100%; text-align: left;
  background: none; border: 0; cursor: pointer;
  color: var(--ink); font: inherit; font-size: 14.5px;
  padding: 12px 18px;
}
.cv-rowmenu button:hover { background: var(--panel-raised); }

/* ---------------------------------------------------------------- Settings rows
   Used by renderConverseSettings() at EVERY width — the Settings stage exists on
   the desktop four-tab row too, and these previously sat inside the media query,
   so desktop rendered them as unstyled full-width buttons. */
.cd-setlist {
  background: var(--panel); border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg); overflow: hidden;
}
.cd-setrow {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 14px; width: 100%; text-align: left;
  background: none; border: 0; border-bottom: 1px solid var(--panel-border);
  color: var(--ink); font: inherit; cursor: pointer;
  min-height: 44px;
}
.cd-setrow:last-child { border-bottom: 0; }
.cd-setrow:hover { background: var(--panel-raised); }
.cd-setrow .cd-setic {
  width: 32px; height: 32px; border-radius: 8px; flex: 0 0 auto;
  background: var(--signal-dim); color: var(--signal);
  display: grid; place-items: center; font-size: 15px;
}
.cd-setrow .cd-setbody { flex: 1; min-width: 0; }
.cd-setrow .cd-setbody b { display: block; font-size: 14px; font-weight: 600; }
.cd-setrow .cd-setbody span {
  display: block; font-size: 12px; color: var(--ink-dim); line-height: 1.35;
}
.cd-setrow .cd-setright { flex: 0 0 auto; color: var(--ink-dim); font-size: 12px; }

@media (max-width: 720px), (max-height: 560px) and (pointer: coarse) {

  /* The mockup's type. The desk on a phone reads as a native app, and native
     apps set the system face — Inter/Space Grotesk stay everywhere else. */
  body[data-cdmobile="on"] {
    --cd-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, Roboto,
               "Helvetica Neue", Arial, sans-serif;
  }

  /* ------------------------------------------------------------ architecture
     The mockup's phone frame has NO global topbar: the app header band + the
     bottom bar are the whole chrome. The topbar (brand, module strip, quick-add)
     hides on the phone desk; every one of its destinations is still reachable
     from a desktop or any other module. syncTopbarHeight() only writes
     --topbar-h when the measured height is > 0, so hiding it here cannot zero
     the offset other modules' sticky rails depend on. */
  body[data-cdmobile="on"] .topbar { display: none; }
  body[data-cdmobile="on"] .app { padding: 0; max-width: none; }
  /* When iOS wedges the layout viewport short (the stuck-keyboard bug), the area
     beyond it composites as a raw band behind the vv-pinned bars. Paint the root
     in the PANEL colour — the shade the desk actually wears. var(--bg) here was
     the dark app token and letterboxed Sam's screen in black. */
  html:has(body[data-cdmobile="on"]) { background: var(--panel); }

  /* #mainTabs becomes the mockup's .apphead: a full-width band, panel-coloured,
     stuck to the top. It already holds .cd-pagehead + .cd-actions, and it is
     already shown only on Converse Desk. */
  body[data-cdmobile="on"] #mainTabs {
    display: block;
    position: sticky; top: 0; z-index: 40;
    background: var(--panel);
    border-bottom: 1px solid var(--panel-border);
    /* viewport-fit=cover means the status bar overlays the page in the installed
       app — without the top inset the clock sat ON the header (Sam's orange
       circle, 2026-08-18). The bottom bar already does this; the top band must
       too. Falls back to 0 in a plain browser tab. */
    /* LEFT/RIGHT matter now that landscape is a phone layout (2026-08-20): a
       notched iPhone on its side puts the notch over one edge and the home
       indicator over the other. Both resolve to 0 in portrait, so this is
       landscape-only in effect. */
    padding: calc(5px + env(safe-area-inset-top, 0px))
             calc(14px + env(safe-area-inset-right, 0px))
             6px
             calc(14px + env(safe-area-inset-left, 0px));
    margin: 0;
  }
  body[data-cdmobile="on"] #mainTabs,
  body[data-cdmobile="on"] .panel,
  body[data-cdmobile="on"] .cd-mobilebar,
  body[data-cdmobile="on"] #mainTabs button,
  body[data-cdmobile="on"] .panel :is(h2, h3, h4, button, input, select, label) {
    font-family: var(--cd-sans);
  }

  /* ---------------------------------------------------------------- tab bar
     Four destinations, per Sam 2026-08-17: Inbox · Modules · Queue · Settings,
     Settings pinned far right. Injected by app.js (cdMobileBar) rather than
     added to index.html, so it can't render on modules that don't want it. */
  /* Shown only at phone width AND only on Converse Desk. */
  body[data-cdmobile="on"] .cd-mobilebar {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 60;                       /* above .compose-bar (50) */
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    background: var(--panel);
    border-top: 1px solid var(--panel-border);
    /* The inset resolves to a real number now that the viewport meta carries
       viewport-fit=cover. On a device without a home indicator it is 0. */
    padding: 6px
             calc(4px + env(safe-area-inset-right, 0px))
             calc(6px + env(safe-area-inset-bottom))
             calc(4px + env(safe-area-inset-left, 0px));
    box-shadow: 0 -4px 20px rgba(15, 23, 42, 0.08);
  }
  .cd-mobilebar button {
    background: none;
    border: 0;
    color: var(--ink-dim);
    font: inherit;
    font-family: var(--cd-sans);
    font-size: 10.5px;
    line-height: 1.2;
    padding: 4px 2px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    position: relative;
    cursor: pointer;
    /* 44px is the smallest thing a thumb reliably hits. */
    min-height: 44px;
    justify-content: center;
  }
  .cd-mobilebar button .cdm-ic { font-size: 18px; line-height: 1; }
  .cd-mobilebar button.active { color: var(--signal); font-weight: 600; }
  .cd-mobilebar .cdm-badge {
    position: absolute;
    top: 1px; left: calc(50% + 6px);
    background: var(--alert); color: #fff;
    font-family: var(--font-mono); font-size: 9.5px; line-height: 1;
    padding: 2px 5px; border-radius: 999px;
    border: 1.5px solid var(--panel);
  }
  .cd-mobilebar .cdm-badge.hidden { display: none; }

  /* -------------------------------------------------- room for the fixed bar
     The bar is ~62px + the home indicator. Anything that scrolls or pins to the
     bottom has to clear it or the last row is permanently unreachable. */
  body[data-cdmobile="on"] {
    --cd-barh: calc(62px + env(safe-area-inset-bottom));
  }
  body[data-cdmobile="on"] .panel.active {
    padding: 12px 14px calc(var(--cd-barh) + 14px);
  }
  /* The Inbox runs edge to edge like the mockup's list — no page gutters. */
  body[data-cdmobile="on"] #panel-inbox.active {
    padding: 0 0 var(--cd-barh);
  }

  /* Panels' outer .card was desktop chrome. On the phone it becomes a
     transparent wrapper — each screen's content carries its own cards, which is
     the mockup's .pad structure. */
  body[data-cdmobile="on"] .panel > .card {
    background: none; border: 0; border-radius: 0; padding: 0; box-shadow: none;
  }

  /* The selection bar already existed, already fixed, already asking for the
     safe-area inset. It just sat at bottom:0, which is now under the tab bar.
     Lift it, and let the bar below own the inset. */
  body[data-cdmobile="on"] .compose-bar {
    bottom: var(--cd-barh);
    padding-bottom: 12px;
    z-index: 55;
    background: var(--signal-dim);
    border-top: 1px solid var(--signal);
  }

  /* ------------------------------------------------- the app header's content
     Six numbered tabs do not fit at 390px, and the bottom bar replaces them. */
  body[data-cdmobile="on"] #mainTabs .cd-tabrow { display: none; }
  /* The open conversation's header carries the name, + Task, + Deal, Pin, Unread and the
     close button on a 390px row. Pin and Unread keep their icons and drop their words at
     phone width: the NAME is the one thing on that row nobody can do without, and it is
     the first casualty when the buttons grow. Desktop keeps the words. */
  body[data-cdmobile="on"] #cvHead .cv-btxt { display: none; }
  body[data-cdmobile="on"] #cvHead .cv-headacts .btn { padding: 3px 7px; }
  body[data-cdmobile="on"] .cd-pagehead {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0;
    /* 34px reserved a second line for a subtitle this screen does not print:
       #cdPageSub is empty on the desk stages, so the slack showed as grey under
       the title (Sam, 2026-09-02). 26px still clears the 17px title comfortably. */
    min-height: 26px;
  }
  body[data-cdmobile="on"] .cd-titlewrap { min-width: 0; flex: 1; }
  body[data-cdmobile="on"] .cd-pagetitle {
    font-size: 17px; font-weight: 650; letter-spacing: -.015em; margin: 0;
  }
  body[data-cdmobile="on"] .cd-pagesub {
    font-size: 11.5px; margin: 1px 0 0;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* Module ▾ and More live top-right of the header, per Sam's layout. */
  body[data-cdmobile="on"] .cd-actions {
    margin-left: auto;
    display: flex; gap: 6px; align-items: center; flex: 0 0 auto;
  }
  /* The mockup's .hbtn: a quiet pill. */
  body[data-cdmobile="on"] .cd-actions .btn {
    background: var(--panel-raised);
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    color: var(--ink-dim);
    font-size: 11.5px;
    padding: 5px 11px;
  }

  /* ------------------------------------------------------------- pill tabs
     Every tab strip inside the desk (All/Unread/Sent, the audience tabs, the
     Not-contacting source/state tabs) renders as the mockup's .seghead pills
     instead of the desktop's uppercase display-font tabs. */
  body[data-cdmobile="on"] .panel .tab-btn,
  body[data-cdmobile="on"] :is(#cvThreads, #cvTabsHost) .tab-btn {
    background: var(--panel-raised);
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    color: var(--ink-dim);
    font-family: var(--cd-sans);
    font-size: 12.5px;
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
    padding: 6px 13px;
    white-space: nowrap;
  }
  body[data-cdmobile="on"] .panel .tab-btn.active,
  body[data-cdmobile="on"] :is(#cvThreads, #cvTabsHost) .tab-btn.active {
    background: var(--signal);
    border-color: var(--signal);
    color: var(--on-signal, #fff);
    font-weight: 600;
  }
  /* The count trails the label ("All 200"), as the mockup draws it — the DOM
     emits the .n span first, so the pill flexes and reorders it. */
  body[data-cdmobile="on"] .panel .tab-btn,
  body[data-cdmobile="on"] :is(#cvThreads, #cvTabsHost) .tab-btn {
    display: inline-flex; align-items: center; gap: 5px;
  }
  body[data-cdmobile="on"] .panel .tab-btn .n,
  body[data-cdmobile="on"] :is(#cvThreads, #cvTabsHost) .tab-btn .n {
    font-family: var(--font-mono);
    font-size: 10.5px;
    font-variant-numeric: tabular-nums;
    text-transform: none;
    margin: 0;
    order: 2;
    color: inherit; opacity: .75;
  }

  /* ------------------------------------------------------ Conversations
     NO HEIGHT RULE HERE, ON PURPOSE. In Standard view the card is already
     `display:flex` with fitPanelToViewport() setting its real pixel height (it
     measures where the card actually starts, which CSS cannot know), and
     `.cv-wrap` is `height:auto; flex:1 1 auto` so it fills that card. A height
     here overrides the auto and breaks the fill — measured at 375px, the card
     ran 177px underneath the tab bar and off the bottom of the screen.
     The tab bar is accounted for in fitPanelToViewport() instead, which is the
     one place that already knows the real geometry. */

  /* The mockup's list is full-bleed: the desk card's border and radius go. */
  body[data-cdmobile="on"] #inboxWrap {
    border: 0; border-radius: 0; margin-top: 0;
  }

  /* NO document scroll lock here. Tried (`body:has(.cv-thread-open)
     overflow:hidden`, 2026-08-17) and REVERTED the same hour: with the page
     unscrollable, iOS pans the whole layout under the fixed chrome instead —
     headers slid off-screen, the reply box hid behind the keyboard until a
     keystroke forced a repaint, and scroll gestures grew a white void below
     the document. Measured on Sam's phone; do not re-try the lock. */

  /* THE OPEN THREAD IS A FIXED LAYER — header band to tab bar, glued.
     Sam, after five sizer iterations: "can we just get rid of these
     spaces/gaps in all?" The gaps were the JS sizers (card fit + wrap floor)
     measuring mid-transition and baking stale inline values. A fixed layer has
     no measurements to get wrong: top is the header band's measured height
     (--cd-headh, set by cdSyncMobileBar), bottom is the tab bar, and the
     !importants neutralize every inline height/min-height the sizers may have
     left on the wrap — ending the sizer war for this screen by construction.
     Messages scroll inside; the composer sits at the flex end, ON the bar. */
  /* The WHOLE inbox screen is the fixed layer — list and thread alike. The
     list had the same top strip and bottom gap the thread had (Sam's circles,
     5:25pm), from the same sizer slack; gluing the layer header-to-bar removes
     them the same way. The grid the list already uses lays out inside it. */
  body[data-cdmobile="on"] #inboxWrap {
    position: fixed;
    top: var(--cd-headh, 118px);
    /* The bar's MEASURED height (set by emFitToViewport via cdBarHeight) — the
       estimate constant over-reserved by ~15px on Sam's phone and left a strip
       above the bar. The calc is only the pre-measurement fallback. */
    bottom: var(--cd-barh-real, calc(var(--cd-barh) - 5px));
    left: 0; right: 0;
    z-index: 45;
    background: var(--bg);
    height: auto !important;
    min-height: 0 !important;
    margin: 0;
  }
  body[data-cdmobile="on"] #inboxWrap.cv-thread-open {
    display: flex;
    flex-direction: column;
  }
  /* #cvMsg (the send-status line) sits BELOW the reply box and carries an 8px
     top margin even when empty — the strip above the bar that survived the
     measured-bar fix (harness: replyBottom 950, layer end 958, all of it this
     element). Collapse it when empty; it returns when it has something to say. */
  body[data-cdmobile="on"] #inboxWrap.cv-thread-open .modal-msg:empty { display: none; }
  body[data-cdmobile="on"] #inboxWrap.cv-thread-open .cv-reply { margin: 0; }
  /* The phone NEVER splits list and thread: without .cv-thread-open the layer
     is strictly search + list. Explicit, because the shipped grid keys off
     :has(#cvHead .cv-none) — and a thread left un-reset on the way out (bottom
     bar, not ✕) kept its name in #cvHead, so the grid held a third row open:
     Sam's white band between the search bar and the pills. */
  body[data-cdmobile="on"] #inboxWrap:not(.cv-thread-open) {
    /* search band · pills band (#cvTabsHost) · the list */
    grid-template-rows: auto auto 1fr !important;
  }
  body[data-cdmobile="on"] #inboxWrap:not(.cv-thread-open) .cv-thread { display: none !important; }
  body[data-cdmobile="on"] #inboxWrap:not(.cv-thread-open) .cv-threads { display: block !important; }
  /* The pills band: its own grid row glued under the search band (Sam's call —
     it had been a sticky first child INSIDE the scroller, where its slot opened
     white and it "slid up and down"). display:contents on desktop keeps the
     tabs row a direct grid child there, byte-for-byte as before. */
  body[data-cdmobile="on"] #cvTabsHost {
    display: block;
    background: var(--panel);
    border-bottom: 1px solid var(--panel-border);
    /* Explicit row: the shipped rules pin .cv-headrow to row 1 and .cv-threads
       to row 2, so an auto-placed band fell into the leftover bottom row —
       measured in the harness as the pills rendering BELOW the list. */
    grid-row: 2;
  }
  body[data-cdmobile="on"] #inboxWrap:not(.cv-thread-open) > .cv-threads {
    grid-row: 3 !important;
  }
  body[data-cdmobile="on"] #inboxWrap.cv-thread-open #cvTabsHost { display: none; }
  /* Camouflage for the other strip: the page behind the fixed layer wears the
     header band's tint, so a transiently mis-measured layer top reads as a
     slightly deeper header instead of a white gash. */
  body[data-cdmobile="on"] { background: var(--panel); }
  body[data-cdmobile="on"] #inboxWrap.cv-thread-open .cv-thread {
    flex: 1 1 auto; min-height: 0;
  }
  /* The messages ARE the scroller inside the layer — explicit, not inherited
     from the Standard rules, and contained so reaching either end can't chain
     the gesture through to the dead document behind the layer. */
  body[data-cdmobile="on"] #inboxWrap.cv-thread-open .cv-msgs {
    overflow-y: auto;
    min-height: 0;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }
  body[data-cdmobile="on"] #inboxWrap.cv-thread-open .cvr-body { min-height: 0; }
  /* The list is the other scroller in the layer; contained the same way. */
  body[data-cdmobile="on"] #cvThreads { overscroll-behavior: contain; }
  /* With the thread layer up, the header band is the only exposed strip of the
     background page — dragging it scrolled that dead page (Sam: "the only
     thing that scrolls is the top row"). Pans off; its buttons still tap. */
  body[data-cdmobile="on"]:has(#inboxWrap.cv-thread-open) #mainTabs { touch-action: none; }
  /* Keyboard up: the bar is hidden, the layer runs to the screen bottom, and
     the composer floats on the keyboard (cdKbFit). */
  body[data-cdkb="on"][data-cdmobile="on"] #inboxWrap.cv-thread-open { bottom: 0; }

  /* Filter row: selection lives behind long-press/swipe on a phone, so the
     checkbox column goes; the pills take its place hard left. */
  body[data-cdmobile="on"] :is(#cvThreads, #cvTabsHost) .cv-selall { display: none; }
  body[data-cdmobile="on"] #cvThreads .cv-pick { display: none; }
  body[data-cdmobile="on"] :is(#cvThreads, #cvTabsHost) .cv-tabs-row {
    gap: 6px; padding: 8px 10px !important;
    border-bottom: 1px solid var(--panel-border);
  }
  /* One scrolling strip (Sam rejected the two-row wrap: "I want the swipe
     capability"). Native horizontal panning dies on this screen — the PWA's gesture
     glue owns horizontal touches — so the strip is dragged by cvWireTabsetDrag in
     app.js, which moves scrollLeft straight from the pointer events the glue cannot
     starve. The CSS here only has to make the strip a shrinkable scroller. */
  body[data-cdmobile="on"] :is(#cvThreads, #cvTabsHost) .cv-tabset {
    display: flex; gap: 6px; flex: 1 1 auto; min-width: 0;
    overflow-x: auto; touch-action: pan-x; -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
  }

  /* Rows: the mockup's .trow — avatar disc, name + mono time on the baseline,
     a two-line preview under it, unread in colour. */
  body[data-cdmobile="on"] #cvThreads .cv-t {
    display: flex; gap: 11px; align-items: flex-start;
    padding: 11px 12px;
    /* Long-press opens the row menu — without these iOS starts selecting the
       preview text mid-hold instead. */
    -webkit-user-select: none; user-select: none;
    -webkit-touch-callout: none;
  }
  body[data-cdmobile="on"] .cv-t .cv-av {
    display: grid; place-items: center;
    width: 36px; height: 36px; border-radius: 50%;
    background: var(--signal-dim);
    color: var(--signal-deep, var(--signal));
    font-family: var(--cd-sans);
    font-size: 13px; font-weight: 650;
    flex: 0 0 auto;
    margin-top: 1px;
  }
  body[data-cdmobile="on"] .cv-t .cv-av.cv-av-unread {
    background: var(--signal); color: var(--on-signal, #fff);
  }
  body[data-cdmobile="on"] .cv-t-main { flex: 1 1 auto; min-width: 0; }
  body[data-cdmobile="on"] .cv-t-name {
    font-size: 14.5px; font-weight: 600; letter-spacing: -.01em;
  }
  body[data-cdmobile="on"] .cv-t-when {
    font-family: var(--font-mono); font-size: 10.5px;
  }
  /* The preview grows to two clamped lines. white-space needs !important only
     because the span carries an inline nowrap for the desktop's one-line row. */
  body[data-cdmobile="on"] .cv-t-prev > span:first-child {
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden; white-space: normal !important;
    font-size: 12.5px; line-height: 1.4;
  }
  body[data-cdmobile="on"] .cv-t.cv-tunread .cv-t-prev > span:first-child {
    color: var(--ink);
  }

  /* Thread view: the mockup's bubbles — bigger radius, roomier type, mono
     timestamps — and an iMessage-shaped reply row. */

  /* THE CLIPPED-THREAD FIX (Sam's phone, 2026-08-17). .cv-thread is a grid item,
     so its automatic minimum size is its content's min width — and the header row
     (name + the nowrap opted-out pill + Task/Deal/✕) can't shrink. The track went
     wider than the screen and .cv-wrap's overflow:hidden CLIPPED the pane: the ✕
     was off-screen and every outbound bubble was cut at the right edge.
     min-width:0 lets the pane stay at screen width; the header then ellipsizes. */
  body[data-cdmobile="on"] #inboxWrap .cv-thread,
  body[data-cdmobile="on"] #inboxWrap .cvr-body,
  body[data-cdmobile="on"] #inboxWrap .cv-msgs { min-width: 0; }
  body[data-cdmobile="on"] #cvHead > div { min-width: 0; }
  body[data-cdmobile="on"] #cvHead > div > span:first-child { min-width: 0; }
  /* The mockup's back control sits LEFT of the name, like every phone app. */
  body[data-cdmobile="on"] #cvHead .cv-back { order: -1; }

  body[data-cdmobile="on"] #panel-inbox .cv-head {
    background: var(--panel);
    display: flex; align-items: center; gap: 10px;
    font-size: 14.5px;
  }
  body[data-cdmobile="on"] #panel-inbox .cv-b {
    max-width: 78%;
    padding: 9px 13px;
    border-radius: 16px;
    font-size: 14px; line-height: 1.42;
  }
  body[data-cdmobile="on"] #panel-inbox .cv-b.in {
    border: 1px solid var(--panel-border);
    border-bottom-left-radius: 5px;
  }
  body[data-cdmobile="on"] #panel-inbox .cv-b.out {
    border-bottom-right-radius: 5px;
  }
  body[data-cdmobile="on"] #panel-inbox .cv-when {
    font-family: var(--font-mono); font-size: 10px;
  }
  body[data-cdmobile="on"] #panel-inbox .cv-reply {
    background: var(--panel);
    padding: 9px 12px;
    align-items: flex-end;
  }
  body[data-cdmobile="on"] #panel-inbox .cv-reply textarea {
    /* Beats the @900 `flex:1 1 100%` that gave the textarea a whole line and
       dropped the send button underneath it — the mockup's reply row is one
       line: textarea + circular send. */
    flex: 1 1 auto; min-width: 0; width: auto;
    border-radius: 18px;
    padding: 9px 14px;
    background: var(--bg);
    min-height: 38px;
    font-family: var(--cd-sans);
  }
  /* THE COMPOSER GIVES ITS SPACE BACK WHILE YOU TYPE.
     Sam, 2026-08-21, pointing at the ^ v Done bar: "remove that … and remove the space
     which will bring down the reply box and send button area." That bar is iOS's own
     keyboard accessory and no web page can remove it — but the row WE spend on the
     template picker is ours, and it is worth most exactly when the keyboard has taken
     half the screen. Choosing a template replaces the draft anyway, so it is a
     before-you-start control, not a mid-sentence one.
     Keyed on data-cdkb, the keyboard flag the bars already ride, so it is one source of
     truth rather than a second guess at whether the keyboard is up. */
  /* The template picker STAYS while the keyboard is up. It was hidden here for an
     afternoon (2026-08-21) to give the composer a row back — my trade, not Sam's ask —
     and he immediately missed it: "why do we lose the template function when keyboard
     opens?" Templates get reached for mid-conversation, not just before typing starts.
     The reply box's growth never needed this row anyway: the composer is pinned above
     the keyboard and grows UPWARD into the thread. If the space-vs-function preference
     flips again, that's the signal to give him a setting rather than re-trading
     ([[toast-and-offer-followup]] rule). */
  /* The reply box grows with the text (cvAutoGrow sets the height); this keeps the
     rounded shape and stops the flex row stretching it back to one line. */
  body[data-cdmobile="on"] #panel-inbox .cv-reply textarea {
    align-self: flex-end;
  }
  /* The runtime-injected template picker rides its own line above the reply
     (the @900 rule already gives it flex-basis 100%); it just gets the pill look. */
  body[data-cdmobile="on"] #cvTemplateSel {
    background: var(--bg);
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    padding: 6px 12px;
    font-size: 16px;
    color: var(--ink-dim);
  }
  /* The send button becomes the mockup's circular ↑. font-size:0 swallows the
     "Send" label; the glyph is drawn back on. */
  body[data-cdmobile="on"] #cvSendBtn {
    width: 38px; height: 38px; border-radius: 50%;
    padding: 0; font-size: 0; flex: 0 0 auto;
    display: grid; place-items: center;
  }
  body[data-cdmobile="on"] #cvSendBtn::after { content: "↑"; font-size: 16px; }
  body[data-cdmobile="on"] #cvAttachBtn {
    width: 38px; height: 38px; border-radius: 50%;
    padding: 0; flex: 0 0 auto;
    display: grid; place-items: center;
  }

  /* The full-screen-thread rules below are PORTED from cvInjectMobileStyles()
     in app.js, which builds them as a <style> tag — and which never ran, because
     its only caller openInbox() is itself never called from anywhere. So on a
     phone the thread opened, .cv-thread-open went on, and nothing consumed it:
     the list stayed on screen underneath. Static CSS belongs in a stylesheet,
     where it cannot depend on an init function firing.

     Scoped to #inboxWrap, NOT the bare .cv-wrap the original used — the Email
     desk (#emWrap) is also a .cv-wrap with its own .cv-back and its own rules,
     and it is not part of this pass. */
  #inboxWrap.cv-thread-open { grid-template-rows: 1fr !important; }
  #inboxWrap.cv-thread-open > .cv-headrow { display: none !important; }
  #inboxWrap.cv-thread-open .cv-threads { display: none !important; }
  /* grid-row is the load-bearing part, not the hiding. With the first two children
     hidden the thread still auto-placed into row 3, leaving an empty 1fr row 1 to
     absorb ~250px of a 572px card — measured at 375x812. Pinning it to the single
     track means it gets the whole card however the siblings are counted. */
  #inboxWrap.cv-thread-open .cv-thread {
    display: flex !important;
    grid-row: 1 / -1 !important;
    grid-column: 1 / -1 !important;
  }
  #cvHead .cv-back { display: inline-flex; }
  #cvBody { font-size: 16px; }

  /* ------------------------------------------- the search / scope toolbar
     Measured on Sam's phone at 375px: the scope dropdown took 193px and left
     the search box 85px, so its placeholder read "Search messag". The row is
     justify-content:flex-end on desktop, which is fine when there is room and
     wrong when there isn't. Let search take the slack and cap the dropdown.
     Styled as the mockup's sticky search band: pill controls on the panel tint. */
  #panel-inbox .cv-headrow-slim {
    justify-content: stretch;
    /* A stronger shipped rule keeps re-applying the .row bottom margin — 8px
       of the wrap's white showed between the search band and the pills band
       (harness-measured, twice: the bare margin:0 lost the specificity fight). */
    margin: 0 !important;
    background: var(--panel);
    border-bottom: 1px solid var(--panel-border);
    padding: 8px 10px;
    margin: 0;
  }
  #panel-inbox .cv-headtools { display: flex; gap: 6px; width: 100%; align-items: center; }
  #panel-inbox #cvSearch {
    flex: 1 1 auto; min-width: 0;
    margin-right: 0 !important;   /* beats the inline style on the element */
    background: var(--bg);
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    padding: 7px 14px;
  }
  #panel-inbox #cvScope {
    flex: 0 1 auto; max-width: 46%; min-width: 0;
    margin-right: 0 !important;
    background: var(--bg);
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    padding: 6px 10px;
  }

  /* ------------------------------------------------------------- typing
     iOS zooms the whole viewport when you focus an input under 16px, and it
     does NOT zoom back out. #cvBody is already handled above; these are the
     controls it doesn't cover. Do not tidy them back to 13/14px. */
  #panel-inbox input[type="search"],
  #panel-compose textarea,
  #panel-compose input[type="text"],
  .cd-setlist input,
  .cd-setlist textarea { font-size: 16px; }

  /* --------------------------------------------------------------- Modules
     The mockup's phone list: "Table gone. Name, phone and chips stack into one
     tappable row." Both tables restack into person-rows via grid — the markup
     (and every handler bound to it) is untouched.

     CONTACTS. The row grid: [checkbox] [name over phone] [chips over cooldown].
     Cells are placed by what they CONTAIN (:has), not by column position,
     because the visible columns are workspace configuration, not a constant. */
  body[data-cdmobile="on"] #panel-contacts > .card:not(.list-card) { display: none; }
  body[data-cdmobile="on"] #panel-contacts .ll-sub,
  body[data-cdmobile="on"] #panel-contacts .ct-pagehead,
  body[data-cdmobile="on"] #panel-contacts .ct-importbtn,
  body[data-cdmobile="on"] #panel-contacts #clearAllContactsBtn,
  body[data-cdmobile="on"] #panel-contacts .filter-builder { display: none; }
  /* The filter-count line: "27 match" is the phone's whole story — the
     don't/total halves are desktop detail. */
  body[data-cdmobile="on"] #panel-contacts .fc-rest { display: none; }
  body[data-cdmobile="on"] #panel-contacts .ll-filters input,
  body[data-cdmobile="on"] #panel-contacts .ll-filters select {
    background: var(--bg);
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    padding: 7px 14px;
    font-size: 16px;
  }

  /* ------------------------------------------------------------- Sort by
     The phone's replacement for clickable column headers (the thead is hidden
     below). Shown ONLY on the Converse Desk phone layout — every other surface
     still has its headers, so the button would be a second way to do the same
     thing. The button reports the current sort, because the desktop's
     "Sorted by…" indicator is part of the table chrome the phone drops. */
  body[data-cdmobile="on"] .cd-sortbtn {
    display: inline-flex; align-items: center; gap: 6px;
    align-self: flex-start;
    padding: 7px 14px;
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    background: var(--bg);
    color: var(--ink);
    font: inherit; font-size: 13px; font-weight: 600;
    cursor: pointer;
  }
  body[data-cdmobile="on"] .cd-sortbtn:active { background: var(--panel-raised); }

  /* ------------------------------------------------ record page section nav
     Details / Deals / Tasks / Activity is position:sticky for the desk, where
     it rides a tall column beside the record. Stacked on the phone it pinned
     part-way down the screen and floated over the fields (Sam's landscape
     record screenshot, 2026-08-20: "menus bar floats"). The phone desk already
     has ONE sticky layer — the app header band — and a second one inside a
     single-column record has nothing sensible to pin against. In flow it
     scrolls with the record, like every other section on the phone. */
  body[data-cdmobile="on"] .deal-nav { position: static; z-index: auto; }

  body[data-cdmobile="on"] #panel-contacts .contact-table-wrap {
    overflow-x: hidden;
    border: 1px solid var(--panel-border);
    border-radius: var(--radius-lg);
    background: var(--bg);
  }
  body[data-cdmobile="on"] #panel-contacts table.contact-table { display: block; min-width: 0; width: 100%; }
  body[data-cdmobile="on"] #panel-contacts table.contact-table thead { display: none; }
  body[data-cdmobile="on"] #panel-contacts table.contact-table tbody { display: block; }
  body[data-cdmobile="on"] #panel-contacts tr.contact-trow {
    display: grid;
    /* check · first name · last name (1fr) · chips — a workspace showing First
       and Last as separate columns gets both on the name line (Sam's ask); a
       combined name column leaves the 1fr track empty and reads identically. */
    /* Column 4 is BOUNDED, not `auto`. A workspace class reads "Broker - Distressed
       Properties"; as an auto track that pill took the row and squeezed the 1fr name
       track to nothing (Sam's screenshot, 2026-09-08 — the last name was gone and the
       pill still overflowed). 45% leaves the majority to the person's name, which is
       what the row is for.

       ⚠️ fit-content(45%), NOT minmax(0, 45%). minmax RESERVES the 45% rather than
       capping it — a non-flexible track takes free space up to its growth limit before
       the 1fr track expands — so a 40px "Group C" chip claimed 165px anyway and cut the
       surname track from 256px to 131px on ordinary rows. Measured, and measured a
       SECOND time: the first pass read each td's own box, which for a `justify-self:
       end` grid item is not the track width, and showed no difference at all. Read
       getComputedStyle(row).gridTemplateColumns when a track is the question.
       fit-content sizes to content and clamps: 40px for a short chip, 165px for
       "Broker - Distressed Properties". */
    grid-template-columns: auto auto minmax(0, 1fr) fit-content(45%);
    gap: 2px 6px;
    align-items: center;
    padding: 11px 12px;
    border-bottom: 1px solid var(--panel-border);
  }
  body[data-cdmobile="on"] #panel-contacts tr.contact-trow:last-child { border-bottom: 0; }
  body[data-cdmobile="on"] #panel-contacts tr.contact-trow.selected { background: var(--signal-dim); }
  body[data-cdmobile="on"] #panel-contacts tr.contact-trow > td {
    display: none; padding: 0; border: 0; background: none;
  }
  /* tr.contact-trow must be in this selector: the hide-all rule above carries
     (id,2,3) specificity and a bare td.col-select loses to it. */
  body[data-cdmobile="on"] #panel-contacts tr.contact-trow > td.col-select {
    display: flex; grid-column: 1; grid-row: 1 / span 2; align-items: center;
  }
  body[data-cdmobile="on"] #panel-contacts .rowCheck {
    width: 20px; height: 20px; accent-color: var(--signal); margin: 0;
  }
  body[data-cdmobile="on"] #panel-contacts tr.contact-trow > td:has(.contact-name-link) {
    display: block; grid-column: 2; grid-row: 1;
    font-size: 14.5px; font-weight: 600;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* First + Last as separate columns: the SECOND name cell rides beside the
     first ("show first name and last name on the list" — Sam); a third+ stays
     hidden. A combined single name column leaves the 1fr track empty. */
  body[data-cdmobile="on"] #panel-contacts tr.contact-trow > td:has(.contact-name-link) ~ td:has(.contact-name-link) {
    display: block; grid-column: 3; grid-row: 1; justify-self: start;
    font-size: 14.5px; font-weight: 600;
    /* The same shrink-to-fit trap as the chip cell, mirrored: justify-self makes this
       item size to its content, so "Vandermeersch-Ngo" stayed 136px wide inside a 96px
       track and hung out to the RIGHT, under the chip. ellipsis cannot fire while the
       box is as wide as the text. */
    min-width: 0; max-width: 100%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  body[data-cdmobile="on"] #panel-contacts tr.contact-trow > td:has(.contact-name-link) ~ td:has(.contact-name-link) ~ td:has(.contact-name-link) { display: none; }
  body[data-cdmobile="on"] #panel-contacts tr.contact-trow > td:has(.pc-cell) {
    display: block; grid-column: 2 / span 2; grid-row: 2;
    font-size: 11.5px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  body[data-cdmobile="on"] #panel-contacts tr.contact-trow > td:has(.pc-cell) ~ td:has(.pc-cell) { display: none; }
  body[data-cdmobile="on"] #panel-contacts tr.contact-trow > td:has(.class-pill, .choice-pill) {
    display: flex; gap: 4px; flex-wrap: wrap; justify-content: flex-end;
    grid-column: 4; grid-row: 1; justify-self: end;
    /* ⚠️ max-width: 100% IS THE ONE THAT DOES THE WORK, and it is 100% of the GRID AREA
       — i.e. the track. Without it the cell is still sized to its content, because
       `justify-self: end` makes a grid item shrink-to-fit rather than stretch: the box
       stayed 203px wide inside a 152px track and, pinned by its right edge, hung out of
       the track to the LEFT and landed on top of the surname. overflow:hidden clips
       INSIDE a box; it does not shrink one. min-width:0 only removes the automatic
       minimum, which is necessary here but not sufficient. */
    min-width: 0; max-width: 100%; overflow: hidden;
  }
  /* ⚠️ ONE FAMILY, NOT TWO LISTS. These two rules used to hide a class-pill cell after
     a class-pill cell and a choice-pill cell after a choice-pill cell — and never a
     CHOICE cell after a CLASS cell, which is the ordinary case: Contact Class is a
     .class-pill and Group (or any other dropdown column) is a .choice-pill. Both then
     claimed column 4 / row 1 and painted ON TOP OF EACH OTHER — "Estate Sale Company"
     and "Group C" as one unreadable smear (Sam, 2026-09-08). Only the FIRST pill cell
     of either kind survives; matching on the union is what makes that true. */
  body[data-cdmobile="on"] #panel-contacts tr.contact-trow > td:has(.class-pill, .choice-pill) ~ td:has(.class-pill, .choice-pill) { display: none; }
  /* A single long class must ellipsis rather than run under its neighbour. .class-pill
     does not carry nowrap (only .choice-pill does), so both are stated here. */
  body[data-cdmobile="on"] #panel-contacts tr.contact-trow > td :is(.class-pill, .choice-pill) {
    max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  body[data-cdmobile="on"] #panel-contacts tr.contact-trow > td:has(.cooldown-badge) {
    display: block; grid-column: 4; grid-row: 2; justify-self: end;
    font-size: 11px;
    /* justify-self again — bound it before a longer stamp repeats the trick. */
    min-width: 0; max-width: 100%; overflow: hidden;
  }
  /* The inline editor row opens full-width under its person-row. */
  body[data-cdmobile="on"] #panel-contacts tr.contact-edit-row { display: block; }
  body[data-cdmobile="on"] #panel-contacts tr.contact-edit-row > td.contact-edit-cell { display: block; padding: 10px 12px; }

  /* LEADS and every engine module. Same grid; the hooks here are the engine's
     own cell classes: .cm-v-first is the leading field, .cm-v-phone is any
     phone-typed field, choice fields already contain .choice-pill. */
  body[data-cdmobile="on"] #panel-custom .cm-pagehead { display: none; }
  body[data-cdmobile="on"] #panel-custom .cm-scroll {
    overflow-x: hidden;
    border: 1px solid var(--panel-border);
    border-radius: var(--radius-lg);
    background: var(--bg);
  }
  body[data-cdmobile="on"] #panel-custom table.cm-table { display: block; min-width: 0; width: 100%; }
  body[data-cdmobile="on"] #panel-custom table.cm-table thead { display: none; }
  body[data-cdmobile="on"] #panel-custom table.cm-table tbody { display: block; }
  body[data-cdmobile="on"] #panel-custom tr.cmrec-row {
    display: grid;
    /* Same bounded chip track as Contacts. .choice-pill carries white-space:nowrap from
       styles.css, so a long dropdown value ("Contacted - Awaiting Seller Response") took
       the row as an `auto` track and cut the record title from 278px to 119px — the same
       symptom Sam reported on Contacts, one module over, found by the ui-guardian review
       of that fix on 2026-09-08. */
    grid-template-columns: auto minmax(0, 1fr) fit-content(45%);
    gap: 2px 11px;
    align-items: center;
    padding: 11px 12px;
    border-bottom: 1px solid var(--panel-border);
  }
  body[data-cdmobile="on"] #panel-custom tr.cmrec-row:last-child { border-bottom: 0; }
  body[data-cdmobile="on"] #panel-custom tr.cmrec-row.selected { background: var(--signal-dim); }
  body[data-cdmobile="on"] #panel-custom tr.cmrec-row > td { display: none; padding: 0; border: 0; background: none; }
  body[data-cdmobile="on"] #panel-custom tr.cmrec-row > td.dt-check {
    display: flex; grid-column: 1; grid-row: 1 / span 2; align-items: center; width: auto;
  }
  body[data-cdmobile="on"] #panel-custom .cmCheck {
    width: 20px; height: 20px; accent-color: var(--signal); margin: 0;
  }
  body[data-cdmobile="on"] #panel-custom tr.cmrec-row > td.cm-td.cm-v-phone {
    display: block; grid-column: 2; grid-row: 2;
    font-size: 11.5px; color: var(--ink-dim);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  body[data-cdmobile="on"] #panel-custom tr.cmrec-row > td.cm-td.cm-v-phone ~ td.cm-td.cm-v-phone { display: none; }
  /* AFTER the phone rule: if the leading field IS the phone, it renders as the
     row title, not as the meta line. */
  body[data-cdmobile="on"] #panel-custom tr.cmrec-row > td.cm-td.cm-v-first {
    display: block; grid-column: 2; grid-row: 1;
    font-size: 14.5px; font-weight: 600; color: var(--ink);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  body[data-cdmobile="on"] #panel-custom tr.cmrec-row > td.cm-td:has(.choice-pill):not(.cm-v-first) {
    display: flex; gap: 4px; flex-wrap: wrap; justify-content: flex-end; align-content: center;
    grid-column: 3; grid-row: 1 / span 2; justify-self: end;
    min-width: 0; max-width: 100%; overflow: hidden;
  }
  body[data-cdmobile="on"] #panel-custom tr.cmrec-row > td.cm-td:has(.choice-pill):not(.cm-v-first) ~ td.cm-td:has(.choice-pill) { display: none; }
  /* min-width:0 above is what makes the fit-content clamp hold: without it the nowrap
     pill sets the cell's automatic minimum and pushes the track back open. */
  body[data-cdmobile="on"] #panel-custom tr.cmrec-row > td .choice-pill {
    max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  }

  /* The list toolbar shrinks to what a phone needs: the count and the pager. */
  body[data-cdmobile="on"] #panel-custom .cm-listbar { gap: 6px; flex-wrap: wrap; font-size: 12px; }
  body[data-cdmobile="on"] #panel-custom #cmColsBtn,
  body[data-cdmobile="on"] #panel-custom #cmSelAllShown3,
  body[data-cdmobile="on"] #panel-custom #cmClearSel3 { display: none; }

  /* --------------------------------------------------------------- Queue
     The mockup's "Sending now" card: a slim progress bar in a panel card, then
     person-rows. The per-recipient tickets keep their editable message and
     buttons — that is function the mockup's static rows don't draw. */
  /* The card's own "Send queue" h2 duplicates the app header band (it sits
     inside a .row, not as a direct child). The Back-to-compose button stays. */
  body[data-cdmobile="on"] #panel-queue .card h2 { display: none; }
  body[data-cdmobile="on"] #panel-queue .card > p.sub,
  body[data-cdmobile="on"] #panel-queue .card > .callout { display: none; }

  /* "Send all remaining" follows you down the queue (Sam, 2026-09-02: on a
     30-card queue it scrolled off the top and he had to go all the way back up
     to reach it).

     It sticks to the TOP, not the bottom, because the bottom already belongs to
     the four-destination tab bar — a second fixed bar down there would either
     sit under it or crowd the thumb zone.

     ⚠ top is NOT 0. #mainTabs is `position:sticky; top:0` on this layout, so a
     second sticky element at offset 0 lands in the same place and the two
     overlap. --cd-headh is the header band's measured BOTTOM edge in viewport
     coordinates — the same anchor the fixed inbox layer uses (see the thread
     layer above), re-measured on the fit cadence, so it tracks the real header
     instead of a hardcoded guess. */
  body[data-cdmobile="on"] #panel-queue .q-sendall {
    position: sticky;
    top: var(--cd-headh, 118px);
    z-index: 30;             /* over the ticket cards, under the header band (40) */
    /* The resting fill is a translucent blue tint, which is fine sitting still
       on the card and wrong once cards scroll UNDERNEATH it. Painting the same
       rgba over an opaque --panel keeps the exact colour and stops the tickets
       showing through. */
    background: linear-gradient(rgba(47, 128, 237, .08), rgba(47, 128, 237, .08)),
                var(--panel);
    /* Reads as a layer above the list rather than a card that stopped moving. */
    box-shadow: 0 4px 12px rgba(15, 23, 42, .10);
  }
  body[data-cdmobile="on"] #panel-queue .queue-progress {
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: var(--radius-lg);
    padding: 12px;
    display: flex; flex-direction: column; gap: 8px;
  }
  body[data-cdmobile="on"] #panel-queue .queue-bar {
    /* flex:0 0 auto beats the shipped `flex:1 1 100%`, which in this COLUMN
       container acts on the vertical axis and collapses the bar to 0px. */
    flex: 0 0 auto; width: 100%;
    height: 7px; border-radius: 999px; background: var(--panel-raised); overflow: hidden;
  }
  body[data-cdmobile="on"] #panel-queue .queue-bar-fill {
    background: var(--signal); border-radius: 999px;
  }
  body[data-cdmobile="on"] #panel-queue .queue-count {
    font-family: var(--font-mono); font-size: 12.5px; color: var(--ink-dim);
  }
  body[data-cdmobile="on"] #panel-queue .ticket {
    background: var(--bg);
    border: 1px solid var(--panel-border);
    border-radius: var(--radius-lg);
    padding: 11px 12px;
  }
  body[data-cdmobile="on"] #panel-queue .ticket-name { font-size: 14.5px; font-weight: 600; }
  body[data-cdmobile="on"] #panel-queue .ticket-meta {
    font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-dim);
  }
  body[data-cdmobile="on"] #panel-queue .ticket textarea {
    font-family: var(--cd-sans); font-size: 16px; border-radius: 9px;
  }

  /* --------------------------------------------------------------- Compose
     Single column of the mockup's cards: quiet uppercase card labels, a 16px
     message box, pill channel toggle, and a full-width primary send. */
  body[data-cdmobile="on"] #panel-compose .grid-2 {
    display: grid; grid-template-columns: 1fr; gap: 12px;
  }
  body[data-cdmobile="on"] #panel-compose .grid-2 > .card {
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: var(--radius-lg);
    padding: 12px;
    margin: 0;
  }
  body[data-cdmobile="on"] #panel-compose .card h2 {
    font-size: 11px; letter-spacing: .09em; text-transform: uppercase;
    color: var(--ink-dim); font-weight: 650; margin: 0 0 8px;
  }
  body[data-cdmobile="on"] #panel-compose .card p.sub { display: none; }
  body[data-cdmobile="on"] #panel-compose .channel-toggle { display: flex; gap: 6px; overflow-x: auto; }
  body[data-cdmobile="on"] #panel-compose .channel-toggle button {
    background: var(--panel-raised);
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    color: var(--ink-dim);
    font-size: 12.5px;
    padding: 6px 13px;
    white-space: nowrap;
  }
  body[data-cdmobile="on"] #panel-compose .channel-toggle button.active {
    background: var(--signal); border-color: var(--signal);
    color: var(--on-signal, #fff); font-weight: 600;
  }
  body[data-cdmobile="on"] #panel-compose .preview-card { font-family: var(--cd-sans); }
  body[data-cdmobile="on"] #panel-compose #bodyInput {
    background: var(--bg);
    border: 1px solid var(--panel-border);
    border-radius: 9px;
    font-family: var(--cd-sans);
    font-size: 16px; line-height: 1.5;
  }
  body[data-cdmobile="on"] #panel-compose #buildQueueBtn,
  body[data-cdmobile="on"] #panel-compose #startBlastBtn {
    width: 100%;
    border-radius: 10px;
    font-size: 15px; font-weight: 600;
    padding: 12px;
  }

  /* -------------------------------------------------------- Not contacting
     One card per person: name, mono phone, what they said, the matched rule in
     red, then Put back / Ignore. Same :has placement pattern as the lists —
     the checkbox cell is omitted on locked rows, so position can't be trusted. */
  body[data-cdmobile="on"] #panel-notcontacting .dnt-tablewrap {
    border: 1px solid var(--panel-border);
    border-radius: var(--radius-lg);
    background: var(--bg);
    overflow: hidden;
  }
  body[data-cdmobile="on"] #panel-notcontacting table.dnt-table { display: block; min-width: 0; width: 100%; }
  body[data-cdmobile="on"] #panel-notcontacting table.dnt-table thead { display: none; }
  body[data-cdmobile="on"] #panel-notcontacting table.dnt-table tbody { display: block; }
  /* One stacked card per person. Not a grid: the checkbox cell is OMITTED on
     locked rows, so explicit placement can't be trusted — the cells simply
     stack in source order and the checkbox pins to the card's top-right. */
  body[data-cdmobile="on"] #panel-notcontacting #dntRows tr {
    display: block;
    position: relative;
    padding: 12px;
    border-bottom: 1px solid var(--panel-border);
  }
  body[data-cdmobile="on"] #panel-notcontacting #dntRows tr:last-child { border-bottom: 0; }
  body[data-cdmobile="on"] #panel-notcontacting #dntRows td {
    display: block; padding: 0; border: 0; min-width: 0; margin-top: 3px;
  }
  body[data-cdmobile="on"] #panel-notcontacting #dntRows td:has([data-dntpick]) {
    /* width:auto beats an inherited 100% — full-width it stretches across the
       card and the checkbox lands on the name. */
    position: absolute; top: 12px; right: 12px; margin: 0;
    width: auto; left: auto;
  }
  body[data-cdmobile="on"] #panel-notcontacting #dntRows [data-dntpick] {
    width: 20px; height: 20px; accent-color: var(--signal); margin: 0;
  }
  /* The name cell leads the card, clear of the pinned checkbox. */
  body[data-cdmobile="on"] #panel-notcontacting #dntRows td:has(.dnt-name) {
    font-size: 14.5px; font-weight: 600; margin-top: 0; padding-right: 34px;
  }
  body[data-cdmobile="on"] #panel-notcontacting #dntRows .dnt-name {
    font-size: 14.5px; font-weight: 600;
  }
  /* The card's own heading + count duplicate the app header band. */
  body[data-cdmobile="on"] #panel-notcontacting .card h2,
  body[data-cdmobile="on"] #panel-notcontacting #dntSub { display: none; }
  body[data-cdmobile="on"] #panel-notcontacting #dntRows td.mono {
    font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-dim);
  }
  body[data-cdmobile="on"] #panel-notcontacting #dntRows td:has(.dnt-said) {
    font-size: 13px; color: var(--ink-dim);
  }
  body[data-cdmobile="on"] #panel-notcontacting #dntRows td.dnt-sub {
    font-size: 11px; color: var(--ink-dim);
  }
  body[data-cdmobile="on"] #panel-notcontacting #dntRows [data-dnt] {
    border-radius: 999px; padding: 6px 14px; margin-top: 4px;
  }

  /* ------------------------------------------------------ keyboard open
     data-cdkb is set from a visualViewport listener in app.js. The keyboard
     replaces the tab bar (as in a native app): iOS otherwise floats the fixed
     bar above the keyboard while the layout still reserves its strip — a dead
     white band between the reply box and the bar. These sit AFTER the rules
     they override; same specificity, later wins. */
  body[data-cdkb="on"][data-cdmobile="on"] .cd-mobilebar { display: none; }
  body[data-cdkb="on"][data-cdmobile="on"] .panel.active { padding-bottom: 14px; }
  body[data-cdkb="on"][data-cdmobile="on"] #panel-inbox.active { padding-bottom: 0; }
  body[data-cdkb="on"][data-cdmobile="on"] .compose-bar { bottom: 0; }

  /* Toasts must clear the fixed tab bar or "Select at least one contact
     first" pops up BEHIND it — which reads as the send button being dead
     (Sam: "send buttons are not working sometimes"). */
  body[data-cdmobile="on"] #toastHost { bottom: calc(var(--cd-barh) + 10px); }

  /* Blast modal recipient rows: the 26px/160px/1fr grid left the per-person
     message ~1 character wide at phone width ("H/e"). Checkbox + name on the
     first line, the message spanning the full second line. Width-gated for the
     same reason as the record header above: a 932px landscape screen has ample
     room for the name column and should keep it. The 16px textarea is NOT
     gated — it exists to stop iOS zooming on focus, which it does in either
     orientation. */
  @media (max-width: 720px) {
    .br-row.br-edit-row { grid-template-columns: 26px 1fr; }
  }
  .br-row.br-edit-row textarea { grid-column: 1 / -1; font-size: 16px; }

  /* The blast modal fills the phone. As a floating card it used half the screen
     and gave the recipient list its own inner scrollbar inside that half (Sam,
     2026-08-18: "have it fill the phone screen length first"). One column, one
     scroller: the header rows take what they need, the recipient list takes
     every remaining pixel, the buttons stay reachable above the home indicator. */
  body[data-cdmobile="on"] #blastModal {
    padding: 0;
    align-items: stretch;
    overflow: hidden;                /* the LIST scrolls, not the page */
  }
  body[data-cdmobile="on"] #blastModal .modal-card {
    max-width: none;
    width: 100%;
    height: 100dvh;
    margin: 0;
    border-radius: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding: calc(10px + env(safe-area-inset-top, 0px)) 14px 0;
  }
  body[data-cdmobile="on"] #blastModal h2 { margin: 0 0 2px; font-size: 17px; }
  body[data-cdmobile="on"] #blastModal .modal-sub { margin: 0 0 8px; font-size: 12px; }
  body[data-cdmobile="on"] #blastModal .blast-audit { margin: 0 0 8px; }
  body[data-cdmobile="on"] #blastModal #blastEdit {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  body[data-cdmobile="on"] #blastModal #beList {
    flex: 1 1 auto;
    min-height: 0;
    height: auto;                    /* beats the desktop min(46vh, 420px) cap */
    max-height: none;
  }
  body[data-cdmobile="on"] #blastModal .modal-section {
    margin-top: 10px;
    padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
  }

  /* ---------------------------------------------------- record page header
     Five controls share one non-wrapping row; at 390px the back button was
     crushed clean off the screen ("no back button" — Sam, on a contact record
     opened from the desk). Wrap the row and lead it with the back button.
     NOT gated on data-cdmobile: a phone-width record page has this problem
     from any module. It IS gated on width, though — the premise is literally
     "390px of room". Once the outer query started matching short-and-wide
     screens too, a 932px landscape record header split onto two lines for no
     reason, so the width test it was always describing is now explicit. */
  @media (max-width: 720px) {
    .cr-headrow { flex-wrap: wrap; }
    .cr-headrow #contactBackBtn { order: -1; flex: 0 0 auto; }
    .cr-headrow .cr-titlewrap { flex: 1 1 100%; order: 0; min-width: 0; }
  }

  /* --------------------------------------------------------- Settings tab
     The rows themselves are styled outside the media query (they exist on
     desktop too). Here: drop the duplicate card heading — the app header band
     already says Settings. */
  body[data-cdmobile="on"] #panel-settings .card > h2,
  body[data-cdmobile="on"] #panel-settings .card > p.sub { display: none; }
}

/* ============================================================================
   CHOICE FIELDS RENDER AS CHIPS — every width, not just the phone.
   Sam, 2026-08-17: "both". This is the standing request from 2026-08-15.
   Deliberately OUTSIDE the media query.
   ========================================================================= */
.f-chip {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11.5px; line-height: 1.5; font-weight: 600;
  padding: 2px 9px; border-radius: 999px;
  border: 1px solid var(--panel-border); color: var(--ink-dim);
  background: var(--panel-raised); white-space: nowrap;
}
.f-chip .f-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex: 0 0 auto;
}
.f-chip.f-blue  { color: var(--signal);  border-color: var(--signal);  background: var(--signal-dim); }
.f-chip.f-green { color: var(--confirm); border-color: var(--confirm); background: transparent; }
.f-chip.f-red   { color: var(--alert);   border-color: var(--alert);   background: transparent; }
.f-chip.f-grey  { color: var(--ink-dim); border-color: var(--panel-border); }

/* Choice values carry COLOUR, not just a pill outline. The engine list already
   wrapped select fields in .cm-chip, but every value rendered the same neutral
   grey — so New, Working, Qualified and Dead were indistinguishable at a glance,
   which is the whole point of a chip.
   Colours come from the tag palette (.tagc-*, a --tag-wash/--tag-ink pair) so
   choices and tags look like one system rather than two. */
.choice-pill[class*="tagc-"],
.f-chip[class*="tagc-"] {
  background: var(--tag-wash);
  color: var(--tag-ink);
  border-color: transparent;
  font-weight: 600;
}

/* The record takeovers (a contact, a task, a deal opened full-screen) are not .panel
   elements, so they never got the bar reserve above — their last card sat under the
   bar and could not be reached (Sam, 2026-09-15, the Deals card on a contact:
   "Can’t reach deals on the mobile phone"). Same reserve as a panel. */
body[data-cdmobile="on"] #contactRecordView,
body[data-cdmobile="on"] #taskRecordView,
body[data-cdmobile="on"] #dealEditView,
body[data-cdmobile="on"] #searchResultsView { padding-bottom: calc(var(--cd-barh) + 14px); }
