/* iOS Safari resolves 100vh to the LARGEST viewport — the one with the URL bar
   retracted — so a shell sized in vh is taller than what you can actually see
   and the composer hides under the browser chrome until you scroll. This file
   governs every width above 768px (css/mobile.css is gated to <=768px in
   index.html), so these were the iPad and phone-landscape cases. dvh is already
   the house unit inside mobile.css; these four were simply never migrated.
   Only the UNIT changed — the subtracted constants are a separate layout
   question, measured separately. (2026-08-27) */
/* ═══════════════════════════════════════════════════════════════════════════
   Messages — Interior Moderna team channels
   Design language: architectural, restrained, newspaper-typographic.
   Two-column layout (channel rail | thread surface).
   Brand palette only: cream bg, bronze accent, hairline borders.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─── Page shell ──────────────────────────────────────────────────────────── */

.msg-shell {
  display: grid;
  /* DEFAULT (no thread selected): channel rail | wide thread list. The detail
     column only appears once a thread is opened (.msg-has-detail), so the list
     gets the full width for better previews until then. */
  grid-template-columns: 240px 1fr;
  gap: 0;
  /* Fixed height (not min-height) so each column scrolls INTERNALLY and
     independently — scrolling the thread list must not move the rail/detail.
     2026-09-04 (Cole, against WhatsApp: "we're able to see a full page of
     content, and it doesn't waste a lot of space"): the reserve was a HARD-CODED
     200px guess at "app header + page header + gutter". Measured at 1512x900 it
     was 69px too generous — the shell ended at y=831 in a 900px viewport and the
     conversation got 523 of 900 pixels. `_msgSyncShellHeight()` now measures the
     shell's own top and writes --msg-shell-reserve, so the surface ends on the
     viewport floor at every width, font size and header-wrap. The 200px literal
     survives only as the fallback for the frame before the first measurement. */
  height: calc(100dvh - var(--msg-shell-reserve, 200px));
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}

.msg-shell--no-rail {
  grid-template-columns: 1fr;  /* warehouse single-channel: wide thread list */
}
.msg-shell.msg-rail-collapsed {
  grid-template-columns: 0 1fr;
}

/* Thread selected → make room for the 3rd column: rail | 340px list | detail. */
.msg-shell.msg-has-detail {
  grid-template-columns: 240px 340px 1fr;
}
.msg-shell--no-rail.msg-has-detail {
  grid-template-columns: 340px 1fr;
}
.msg-shell.msg-rail-collapsed.msg-has-detail {
  grid-template-columns: 0 340px 1fr;
}

.msg-shell.msg-rail-collapsed .msg-rail {
  display: none;
}

/* ─── 3rd column: the open thread (only shown once a thread is selected) ───── */
.msg-detail-col {
  display: none;
  flex-direction: column;
  min-width: 0;        /* let long content shrink instead of overflowing the grid */
  min-height: 0;       /* allow the conversation to scroll internally */
  border-left: 1px solid var(--border);
  overflow: hidden;
}
.msg-shell.msg-has-detail .msg-detail-col {
  display: flex;
}
.msg-detail-empty {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  color: var(--text-muted);
  text-align: center;
  padding: 40px;
}
.msg-detail-empty-icon { opacity: 0.5; }
.msg-detail-empty-text { font-size: 13.5px; margin: 0; }

/* The thread detail was built as a full-screen card; inside the column it should
   fill the cell with no extra chrome (the column already has a left border) and
   scroll its replies internally. */
.msg-detail-col > .msg-detail-shell {
  flex: 1;
  min-height: 0;
  height: 100%;
  border: none;
  border-radius: 0;
  box-shadow: none;
}

/* Selected thread in the list (3-column mode) */
.msg-thread-row--active {
  background: var(--surface-2);
  box-shadow: inset 3px 0 0 var(--accent);
}

/* Rail re-open handle — only visible when the rail is collapsed */
.msg-rail-handle { display: none; }
/* ...and never on the chats view, where there is no rail to re-open at any
   width (2026-09-06). `.msg-shell.msg-rail-collapsed .msg-rail-handle` below is
   two classes and outranks a lone `--chats`, so a collapse preference stored
   before the rail was removed put a live `›` arrow beside the list pointing at
   nothing. Three classes, and stated here rather than inside a media query
   because it is true in every band. */
.msg-shell.msg-shell--chats.msg-rail-collapsed .msg-rail-handle { display: none; }
.msg-shell.msg-rail-collapsed .msg-rail-handle {
  display: flex;
  align-items: center;
  justify-content: center;
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 18px;
  height: 52px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-left: none;
  border-radius: 0 8px 8px 0;
  cursor: pointer;
  z-index: 4;
  color: var(--text-muted);
}
.msg-shell { position: relative; }

/* In 3-column mode the thread list stays visible, so the detail's "Back"
   button is redundant — hide it on wide screens (kept on narrow/full-screen). */
/* Close (×) button — closes the detail column in 3-column mode. Hidden by
   default (narrow uses the full-screen "Back" button instead); shown on wide. */
.msg-detail-close {
  display: none;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  padding: 6px;
  border-radius: var(--radius-sm);
  align-items: center;
}
.msg-detail-close:hover { background: var(--surface-2); color: var(--text); }

@media (min-width: 1100px) {
  .msg-detail-back { display: none; }
  .msg-detail-close { display: inline-flex; }
}

/* Below the 3-column breakpoint: never grid-in the detail column; the thread
   opens full-screen (replacing the view) exactly as before — even if a thread
   is selected (.msg-has-detail). */
@media (max-width: 1099px) {
  .msg-shell,
  .msg-shell.msg-has-detail {
    grid-template-columns: 240px 1fr;
  }
  .msg-shell--no-rail,
  .msg-shell--no-rail.msg-has-detail {
    grid-template-columns: 1fr;
  }
  .msg-detail-col,
  .msg-shell.msg-has-detail .msg-detail-col {
    display: none;
  }
}

/* ─── Split View ──────────────────────────────────────────────────────────────
   Four columns in two halves: LEFT half = all teams' messages list + the open
   message's conversation; RIGHT half = all teams' tasks list + the open task's
   conversation. Each half opens its own thread independently. The shell is a
   vertical flex (toolbar on top, the two halves below). */
.msg-shell--split {
  display: flex;
  flex-direction: column;
}
.msg-split-toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.msg-split-cols {
  flex: 1;
  min-height: 0;
  display: flex;
}
/* Each half = one list column + one detail column, splitting the width 50/50. */
.msg-split-half {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
}
.msg-split-half + .msg-split-half { border-left: 1px solid var(--border); } /* 2026-09-01 mega-audit polish */
/* The list fills its half until a thread opens, then it shrinks to make room for
   that half's conversation. */
.msg-split-half .msg-split-col {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}
.msg-split-half.has-detail .msg-split-col { flex: 0 0 clamp(220px, 32%, 300px); }
.msg-split-detail {
  flex: 1 1 0;
  min-width: 0;
  min-height: 0;
  display: none;
  flex-direction: column;
  overflow: hidden;
  border-left: 1px solid var(--border);
}
.msg-split-half.has-detail .msg-split-detail { display: flex; }
/* The per-pane detail reuses .msg-detail-shell; let it fill its column. */
.msg-split-detail > .msg-detail-shell { flex: 1; min-height: 0; height: 100%; border: none; border-radius: 0; box-shadow: none; }
.msg-split-detail .msg-detail-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; color: var(--text-muted); text-align: center; padding: 40px; }
/* Team label in a pane's detail topbar (replaces the channel "back" affordance). */
.msg-split-detail-team {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}
.msg-split-head {
  display: flex;
  align-items: baseline;
  gap: 9px;
  padding: 12px 18px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.msg-split-head-title {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--text);
}
.msg-split-head-sub {
  font-size: 11px;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
/* Column-header unread total — a RED notification badge (like the nav count) so
   the top of each list tells you instantly how many unread there are. */
.msg-split-head-unread {
  margin-left: auto;
  flex-shrink: 0;
  min-width: 19px;
  height: 19px;
  padding: 0 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 11.5px;
  font-weight: 700;
  color: #fff;
  background: var(--red, #dc2626);
  border-radius: 10px;
}
.msg-split-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}
.msg-split-group-head {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  background: var(--surface-2);
  padding: 6px 18px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border);
}
/* Per-team unread count — a RED notification badge so each team tells you, at a
   glance, how many unread it has (messages OR tasks). */
.msg-split-group-unread {
  flex-shrink: 0;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 700;
  color: #fff;
  background: var(--red, #dc2626);
  border-radius: 9px;
}
.msg-split-empty { padding: 44px 24px; text-align: center; }
/* Muted "No messages/tasks yet" line under a team that exists but is empty (roster
   mode — so a team like Factory still appears even with nothing in it yet). */
.msg-split-group-empty {
  padding: 9px 18px 11px;
  font-size: 12px;
  font-style: italic;
  color: var(--text-muted);
}
/* Task-column filter bar (assignee / waiting-on / status / priority).
   UI standard (docs/ui-conventions.md): ONE inline row when there's room, and
   collapse into a "Filters" button + popover when the column is narrow — never
   wrap, never crush the controls to slivers. `.is-collapsed` is toggled in JS
   from the live column width. */
.msg-split-filterbar {
  position: relative;
  padding: 8px 14px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  flex-shrink: 0;
}
.msg-split-filters {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 5px;
}
.msg-split-filter-sel {
  font-size: 11.5px;
  padding: 4px 6px;
  height: auto;
  flex: 1 1 0;
  min-width: 0;       /* allow shrinking below intrinsic width so all fit one row */
}
.msg-split-filter-clear {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11.5px;
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text-secondary);
  cursor: pointer;
  flex: 0 0 auto;     /* keep the Clear button at its natural width */
  white-space: nowrap;
}
/* The "Filters" toggle — only shown when the bar is collapsed. */
.msg-split-filterbar-toggle {
  display: none;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text-secondary);
  cursor: pointer;
}
.msg-split-filterbar-toggle:hover { color: var(--text); border-color: var(--accent); }
.msg-filterbar-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--accent);
  color: #fff;
  font-size: 10px;
  font-weight: 600;
}
/* Collapsed: show the toggle, turn the inline row into a floating popover. */
.msg-split-filterbar.is-collapsed > .msg-split-filterbar-toggle { display: inline-flex; }
.msg-split-filterbar.is-collapsed > .msg-split-filters {
  display: none;
  position: absolute;
  top: calc(100% - 1px);
  left: 14px;
  right: 14px;
  z-index: 30;
  flex-wrap: wrap;
  gap: 6px;
  padding: 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow-md, 0 8px 24px rgba(0, 0, 0, 0.14));
}
.msg-split-filterbar.is-collapsed.is-open > .msg-split-filters { display: flex; }
/* In the popover the controls get room to be readable (wrap, ~2 per row). */
.msg-split-filterbar.is-collapsed > .msg-split-filters .msg-split-filter-sel { flex: 1 1 130px; }
.msg-split-filterbar.is-collapsed > .msg-split-filters .msg-split-filter-clear { flex: 1 1 100%; justify-content: center; }
.msg-split-filter-clear:hover { color: var(--text); border-color: var(--accent); }
/* "Waiting on <name>" badge on a task row — distinct from the assignee chip. */
.msg-task-waiting {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  padding: 1px 7px 1px 6px;
  border-radius: 10px;
  background: var(--yellow-dim);
  color: var(--yellow-strong);
  border: 1px solid var(--border);
  white-space: nowrap;
}
/* The Split toggle button shares the .refresh-btn pill; active state is inlined. */
.msg-split-toggle.is-active { font-weight: 600; }

/* Below the 4-column breakpoint, stack the two halves vertically (each still
   shows its list + any open conversation side by side, just half-height). */
@media (max-width: 1199px) {
  .msg-split-cols { flex-direction: column; }
  .msg-split-half { flex: 1 1 0; min-height: 0; }
  .msg-split-half + .msg-split-half { border-left: none; border-top: 1px solid var(--border); } /* 2026-09-01 mega-audit polish */
}

/* The Messages view is a fixed-height app surface, not a scrolling document:
   the shell already ends on the viewport floor, so `.main`'s 80px bottom gutter
   (css/modern-theme.css) is 80px of nothing beneath the composer. Scoped by
   :has() to the two roots this view mounts (Cole 2026-09-04). Phones keep their
   own gutter — css/mobile.css is a separate stylesheet gated to <=768px. */
#mainContent:has(> .msg-shell) {
  padding-bottom: 14px;
}

/* Page header — tighter, more deliberate than the default page-header */
.msg-page-header {
  display: flex;
  align-items: center;          /* compact: title + actions on one tight row */
  justify-content: space-between;
  gap: 12px 16px;
  padding: 0 0 8px 0;
  margin-bottom: 10px;
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
  /* Frozen: stays pinned to the top of the content area while anything scrolls. */
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--bg);
}

.msg-page-header .msg-title-block {
  display: flex;
  flex-direction: row;          /* title + subtitle inline to save vertical space */
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 10px;
}

.msg-page-header .msg-title {
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
}

.msg-page-header .msg-title svg {
  color: var(--accent);
}

.msg-page-header .msg-subtitle {
  font-size: 12px;
  color: var(--text-muted);
  margin: 0;
}

.msg-header-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.msg-header-actions .form-input {
  font-size: 12px;
  max-width: 140px;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
}

/* ─── Channel rail (left) ─────────────────────────────────────────────────── */

.msg-rail {
  background: var(--bg);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  padding: 18px 0;
  min-height: 0;
  overflow-y: auto;  /* scroll the channel list independently */
}

/* ── Team Members (presence + custom status) ─────────────────────────────── */
.msg-members-section-label { margin-top: 16px; }
.msg-members-list { padding: 2px 0 8px; }
.msg-member-item {
  display: flex; align-items: center; gap: 9px;
  padding: 6px 18px 6px 20px; font-size: 13px; color: var(--text-muted);
}
.msg-member-item.is-self { cursor: pointer; }
.msg-member-item.is-self:hover { background: var(--surface-2); }
.msg-member-avatar {
  position: relative; flex-shrink: 0; width: 26px; height: 26px; border-radius: 50%;
  background: var(--accent, #b08d57); color: #fff; font-size: 10px; font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center;
}
.msg-member-dot {
  position: absolute; right: -1px; bottom: -1px; width: 9px; height: 9px;
  border-radius: 50%; border: 2px solid var(--bg); background: var(--text-dim);
}
.msg-member-dot.is-online { background: var(--green-bright); }
.msg-member-body { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
.msg-member-name { font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.msg-member-you { color: var(--text-muted); font-weight: 400; font-size: 11px; }
.msg-member-status { font-size: 11px; /* 2026-09-01 mega-audit polish */ color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 165px; }
.msg-member-setstatus { color: var(--text-muted); font-style: italic; }
.msg-member-active { color: var(--green-bright); font-weight: 600; }
.msg-member-item.is-clickable { cursor: pointer; }
.msg-member-item.is-clickable:hover { background: var(--surface-2); }
/* 2026-09-01 mega-audit: press feedback — a tap on a member row gave nothing
   back until the profile opened (same reasoning as .msg-thread-row:active). */
.msg-member-item.is-clickable:active { background: color-mix(in srgb, var(--text) 7%, var(--surface-2)); }
/* Direct Messages rail section */
.msg-dm-section-label { margin-top: 16px; }
.msg-dm-avatar {
  position: relative; flex-shrink: 0; width: 22px; height: 22px; border-radius: 50%;
  background: var(--surface-2); color: var(--text-secondary); font-size: 10px; font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center;
}
.msg-dm-avatar .msg-member-dot { right: -2px; bottom: -2px; }
.msg-dm-item { gap: 8px; }
/* Status picker modal */
.msg-status-presets { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 4px; }
.msg-status-preset { border: 1px solid var(--border); background: var(--surface); color: var(--text); border-radius: 999px; padding: 5px 11px; font-size: 12.5px; cursor: pointer; }
.msg-status-preset:hover { border-color: var(--accent); color: var(--accent); }
.msg-status-row { display: flex; gap: 10px; }
.msg-status-field { display: block; font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-top: 10px; }
.msg-status-field .form-input { width: 100%; margin-top: 4px; }
.msg-status-hint { font-size: 11.5px; color: var(--text-muted); margin-top: 10px; }
.msg-status-active-btn {
  display: flex; align-items: center; gap: 8px; width: 100%;
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
  border-radius: 8px; padding: 9px 12px; font-size: 13.5px; font-weight: 600;
  cursor: pointer; margin-bottom: 12px;
}
.msg-status-active-btn:hover { border-color: var(--green-bright); }
.msg-status-active-sub { font-weight: 400; font-size: 11.5px; color: var(--text-muted); }

.msg-rail-section-label {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--text-muted);
  padding: 0 18px 8px;
}

.msg-rail-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
}

.msg-channel-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 18px 9px 22px;
  cursor: pointer;
  border: none;
  background: transparent;
  width: 100%;
  text-align: left;
  font-family: inherit;
  font-size: 13.5px;
  color: var(--text-muted);
  transition: background 0.12s ease, color 0.12s ease;
  border-radius: 0;
}

/* Was a '#' prefix — now a per-team emoji rendered in .msg-channel-ico (JS). */
.msg-channel-item::before { content: none; }

.msg-channel-item .msg-channel-ico {
  flex-shrink: 0;
  font-size: 14.5px;
  line-height: 1;
}

.msg-channel-item .msg-channel-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 500;
}

.msg-channel-item:hover {
  background: var(--surface-2);
  color: var(--text);
}

.msg-channel-item:hover::before {
  color: var(--text-muted);
}

.msg-channel-item.is-active {
  background: var(--surface);
  color: var(--text);
  font-weight: 600;
  box-shadow: inset 3px 0 0 var(--accent);
}

.msg-channel-item.is-active::before {
  color: var(--accent);
  font-weight: 600;
}

.msg-channel-item.is-active .msg-channel-name {
  font-weight: 600;
}

.msg-channel-item.is-muted {
  color: var(--text-muted);
}

.msg-channel-item.is-muted::before {
  color: var(--text-muted);
}

.msg-channel-unread-pill {
  flex-shrink: 0;
  background: var(--accent);
  color: #fff;
  font-size: 10.5px;
  font-weight: 700;
  padding: 1px 7px;
  border-radius: 10px;
  letter-spacing: 0.01em;
  min-width: 20px;
  text-align: center;
}

.msg-channel-item.is-muted .msg-channel-unread-pill {
  background: var(--text-dim);
}

.msg-rail-footer {
  margin-top: auto;
  padding: 16px 18px 0;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.msg-rail-footer-label {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--text-muted);
}

.msg-rail-footer .form-input {
  width: 100%;
  font-size: 12.5px;
  padding: 6px 10px;
}

/* ─── Surface (right column) ──────────────────────────────────────────────── */

.msg-surface {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;   /* allow the inner thread list to scroll instead of growing the page */
  background: var(--surface);
}

/* Channel header — name, description, meta */
.msg-channel-header {
  padding: 22px 32px 18px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.msg-channel-header-meta {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  flex: 1;
}

.msg-channel-header-name {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin: 0;
}

/* Was a '#' prefix — the per-team emoji is now prepended to the title text (JS). */
.msg-channel-header-name::before {
  content: none;
}

.msg-channel-header-desc {
  font-size: 13px;
  color: var(--text-muted);
  margin: 0;
  line-height: 1.5;
}

.msg-channel-header-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

.msg-channel-header-actions .btn {
  font-size: 13px;
  padding: 7px 14px;
}

/* Mobile-only affordances — hidden on desktop, revealed in css/mobile.css.
   The rail-drawer toggle (the phone channel switcher) + its backdrop only make
   sense once the rail collapses to an off-canvas drawer below 768px. */
.msg-rail-mobile-toggle { display: none; }
.msg-rail-backdrop { display: none; }

/* Filter strip */
.msg-filter-strip {
  padding: 14px 24px; /* 2026-09-01 mega-audit polish: 24px detail gutter (base) */
  border-bottom: 1px solid var(--border);
  background: var(--bg);
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

/* WhatsApp-style Chats/Tasks toggle bar — hidden by default; shown in chat mode
   (`.msg-shell--chats`, all widths) and on the phone main mount (css/mobile.css).
   Component styles live here (not the mobile media query) so the toggle renders
   correctly on the desktop 2-pane layout too. */
.msg-mobile-bar { display: none; }
.msg-shell--chats .msg-mobile-bar.is-active {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}
/* ── Quick scope filter: All chats / Team / Factories ──────────────────────
   Cole 2026-08-27: "filters that are quick and already there as buttons instead
   of a drop-down." The point of the request is that the cut is ALWAYS VISIBLE —
   so this row sits above the search, outside the `<details>` popover, and takes
   a full row of its own rather than competing with search + compose for width.
   The type dropdown stays inside Filters for the long tail. */
.msg-scope-row {
  flex: 1 1 100%;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
}
.msg-scope-btn {
  /* Content-sized, then share the slack: equal 1 1 0 columns gave "All" the
     same width as "Factories 35" and truncated the long ones first. */
  flex: 1 1 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  min-width: 0;
  font: inherit;
  font-size: 11.5px;
  font-weight: 600;
  line-height: 1;
  /* Coarse-pointer height up front, not bolted on in a media query — this row
     is thumb-sized on a phone and the same control on a desktop. */
  min-height: 32px;
  padding: 7px 8px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  white-space: nowrap;
  transition: background 160ms var(--ease-standard), color 160ms var(--ease-standard);
}
.msg-scope-btn:hover { color: var(--text); }
.msg-scope-btn.is-on {
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-sm);
  /* The lifted pill reads by CONTRAST in light mode (white on grey) but
     surface-vs-surface-2 is a small delta in dark, where --shadow-sm is a black
     glow on a near-black row and adds nothing. An inset hairline gives the
     active segment an edge in both themes rather than only the one it was
     eyeballed in. */
  box-shadow: var(--shadow-sm), inset 0 0 0 1px var(--border);
}
/* Press feedback on pointer-DOWN, not on click — the control should answer the
   finger, not the event that fires after it lifts. */
.msg-scope-btn:active { transform: scale(0.97); }
.msg-scope-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.msg-scope-count {
  /* 2026-09-01 mega-audit polish: shared count-chip metrics + softer inactive ground */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  font-size: 10.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  padding: 0 5px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--text) 8%, transparent);
  color: var(--text-secondary);
}
.msg-scope-btn.is-on .msg-scope-count { background: var(--accent-dim, var(--surface-2)); color: var(--accent); }
@media (prefers-reduced-motion: reduce) {
  .msg-scope-btn { transition: none; }
  .msg-scope-btn:active { transform: none; }
}

/* Chat-mode toolbar: keep search and compose primary; tuck occasional filters
   into a lightweight popover so the conversation list remains the focus. */
.msg-mobile-tools-row {
  flex: 1 1 100%;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto 38px;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.msg-mobile-search-wrap {
  position: relative;
  display: flex;
  align-items: center;
  min-width: 0;
}
.msg-mobile-search-wrap svg {
  position: absolute;
  left: 10px;
  color: var(--text-muted);
  pointer-events: none;
}
.msg-mobile-search-input {
  width: 100%;
  font-size: 13px;
  padding: 8px 12px 8px 32px;
  border-radius: var(--radius-sm);
  box-sizing: border-box;
}
.msg-mobile-search-input.has-clear {
  padding-right: 30px;
}
.msg-mobile-filters {
  position: relative;
}
.msg-mobile-filters > summary {
  box-sizing: border-box;
  min-height: 36px;
  padding: 0 11px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  list-style: none;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-muted);
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  user-select: none;
}
.msg-mobile-filters > summary::-webkit-details-marker { display: none; }
.msg-mobile-filters > summary:hover,
.msg-mobile-filters[open] > summary {
  color: var(--text);
  background: var(--surface);
  border-color: var(--border);
}
.msg-mobile-filters > summary:focus-visible,
.msg-mobile-new:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.msg-mobile-filters.is-active > summary { color: var(--accent); }
.msg-mobile-filters > summary i {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
}
.msg-mobile-filter-row {
  position: absolute;
  z-index: 12;
  top: calc(100% + 7px);
  right: 0;
  width: min(270px, calc(100vw - 28px));
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  box-shadow: var(--shadow-lg);
}
/* Both menus: override the global `.form-input { width:100% }` (dashboard.css)
   so flex — not that rule — controls their width; each takes half the row. */
.msg-mobile-type-select,
.msg-mobile-sort-select {
  flex: 1 1 0;
  width: auto;
  min-width: 0;
  font-size: 12.5px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
}
/* Highlight the Type filter when it's narrowing the list. */
.msg-mobile-type-select.is-active {
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}
/* In chat mode the old per-channel header + filter strip are replaced by the
   toggle bar — hide them (all widths). */
.msg-shell--chats .msg-channel-header,
.msg-shell--chats .msg-filter-strip { display: none; }
.msg-mobile-new {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  cursor: pointer;
}
.msg-mobile-new svg { color: #fff; }
.msg-mobile-new:hover { filter: brightness(1.06); }

/* ── Split View + Chat Dock, rehoused in the list bar (Cole 2026-09-04) ──────
   The page header that used to hold them is deleted on the chats view. These
   are square icon buttons in the row that already exists, so they add no
   vertical space: the row's height is set by the 38px compose button. The
   active look lives HERE rather than in an inline style written by
   `_msgToggleChatDock` — one home for the state, two homes for the button. */
.msg-bar-view-row {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}
/* The Translate control in the View row. It is a label + select, not a
   `.msg-bar-tool` button, so it needs its own alignment; `min-width: 0` keeps
   the select from forcing the wrapping row wider than the bar. */
.msg-bar-lang {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.msg-bar-lang select {
  font-size: 12px;
  padding: 3px 6px;
  min-width: 0;
  max-width: 150px;
}
.msg-bar-view-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-right: 2px;
}
.msg-bar-tool {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  justify-content: center;
  min-height: 30px;
  padding: 4px 10px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 500;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
  color: var(--text-muted);
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.msg-bar-tool:hover { background: var(--surface-2); color: var(--text); }
@media (max-width: 900px) {
  /* ux-polish.css's touch floor, applied to a control it does not name. */
  .msg-bar-tool { min-height: 44px; }
}
.msg-bar-tool:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.msg-bar-tool.is-active {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.msg-bar-tool.is-active svg { color: #fff; }
.msg-chat-row.is-active { background: var(--accent-dim); }

/* ⛔ THE TEAM RAIL IS GONE ON THE CHATS VIEW IN THE MIDDLE BAND TOO.
   Cole 2026-09-06, with a screenshot of the column: "Let's remove this column
   from the messages tab. We don't need it."

   It was already hidden at BOTH ENDS of the width range — under 769px by
   css/mobile.css (the merged list replaces team switching) and at 1100px+ by
   the 2-pane rule below — which left 769-1099px as the one band where it came
   back: a 240px slab of twenty rows all reading "Factory Suppliers" (they share
   one group_key), with the conversation squeezed into what was left. A rule
   that hides something at both ends and not in the middle reads as "hidden"
   right up until someone works at that size, which is the desktop app's own
   window width.

   Team switching survives it: the list is already merged across teams and the
   Filters disclosure carries the per-team filter. The handle and the backdrop
   go with it — a reopen affordance for a column that no longer exists is worse
   than the column.

   ⚠️ Bounded at 1099px on purpose. These selectors carry MORE classes than the
   1100px block's, so letting them reach that far would outrank it and undo the
   2-pane grid. The band that needs fixing is the only band this touches. */
@media (min-width: 769px) and (max-width: 1099px) {
  .msg-shell--chats .msg-rail,
  .msg-shell--chats .msg-rail-handle,
  .msg-shell--chats .msg-rail-backdrop { display: none; }
  /* One column in every state: this band hides `.msg-detail-col` outright (the
     conversation opens in the overlay, see _msgUseMobileOverlay), so a second
     track would reserve width for an element that is never painted. Enumerated
     against `.msg-rail-collapsed` and `.msg-has-detail` because both are two-
     class selectors in the block above and would otherwise win. */
  .msg-shell.msg-shell--chats,
  .msg-shell.msg-shell--chats.msg-has-detail,
  .msg-shell.msg-shell--chats.msg-rail-collapsed,
  .msg-shell.msg-shell--chats.msg-rail-collapsed.msg-has-detail {
    grid-template-columns: 1fr;
  }
}

/* Desktop WhatsApp 2-pane: [chat list | conversation], team rail hidden, the
   conversation column always present (empty state until a chat is picked). */
@media (min-width: 1100px) {
  .msg-shell--chats,
  .msg-shell--chats.msg-has-detail,
  .msg-shell--chats.msg-rail-collapsed,
  .msg-shell--chats.msg-rail-collapsed.msg-has-detail {
    grid-template-columns: 360px 1fr;
  }
  .msg-shell--chats .msg-rail,
  .msg-shell--chats .msg-rail-handle { display: none; }
  .msg-shell--chats .msg-detail-col { display: flex; }
}

/* Narrow-screen "open a conversation" overlay — a flow child of #mainContent
   that holds the open thread while the list shell is hidden underneath (never
   destroyed), so Back is instant. Subtle rise-in so it doesn't just pop. */
/* Fade only — NO translate (2026-07-30). The overlay is pinned to the visual
   viewport now, and any transform on it (a) offsets the whole conversation and
   its composer by however far the entrance has got, and (b) makes the overlay a
   containing block for fixed descendants. A stuck mid-transition left the
   composer 5px below the fold in testing. Opacity alone cannot misplace it. */
.msg-mobile-detail {
  opacity: 0;
  transition: opacity 0.18s ease;
}
.msg-mobile-detail.is-in { opacity: 1; }

/* ⛔ THE OVERLAY'S GEOMETRY MUST LIVE HERE, NOT ONLY IN css/mobile.css.
   ---------------------------------------------------------------------------
   Cole 2026-09-02, on a narrowed DESKTOP window: "I can't even scroll up and
   down on it. It's really bad. Nothing seems to work to scroll or do anything.
   There's not even a key bar to type into."

   Two boundaries disagreed, and the gap between them was a dead conversation:

     `_msgUseMobileOverlay()`  opens this overlay below **1100px** (!_msgIsWide)
     `css/mobile.css`          is linked `media="(max-width: 768px), …"`

   So from 769px to 1099px on a fine-pointer screen the overlay was CREATED and
   then styled by nothing. Without `position: fixed` and a definite `height` it
   is an ordinary static block: the stream has no bounded box to scroll inside,
   and the composer — its last child — flows off the bottom of the document.
   Exactly the two symptoms reported, and both follow from the same missing box.

   The JS never had this bug: `_msgSyncMobileViewport` publishes
   `--msg-mobile-detail-top/height` whenever the overlay EXISTS (its only guard
   is `if (!ov) return`), and already anchors below the live app-header bottom,
   so these variables are correct at every width.

   This block matches `_msgIsWide()` deliberately — it is the same boundary the
   JS uses to decide the overlay is in play, so the two cannot drift again. The
   phone rules in css/mobile.css carry `!important` and still win below 768px;
   nothing about a phone changes. The `(max-height: 480px) and (pointer: coarse)`
   arm of the stylesheet's media query is the landscape-phone case and is
   likewise unaffected — it loads the phone rules on top of these.

   Related: the TODO(device) note above `_msgUseMobileOverlay` in
   js/views/messages.js flagged the LANDSCAPE half of this same disagreement.
   The desktop-narrow half went unnoticed because no one drags a desktop window
   that narrow. */
@media (max-width: 1099px) {
  #mainContent .msg-mobile-detail {
    position: fixed;
    left: 0;
    right: 0;
    top: var(--msg-mobile-detail-top, 0px);
    height: var(--msg-mobile-detail-height, 100dvh);
    min-height: 0;
    /* Above the app header (z 30); below the nav drawer (z 100), so the sidebar
       can still be opened over an open conversation. Same stack as the phone. */
    z-index: 60;
    overflow: hidden;
    overscroll-behavior: none;
  }
  /* The shell is the flex column that gives the stream its scrollable box and
     keeps the composer pinned as the last child. Without a definite height it
     collapses to content and takes the composer with it. */
  #mainContent .msg-mobile-detail .msg-detail-shell {
    min-height: 0;
    height: 100%;
  }
  /* Nothing behind a full-screen conversation may scroll. */
  body:has(#msgMobileDetail) #mainContent,
  body:has(#msgMobileDetail) .main {
    overflow: hidden;
  }
  /* 2026-09-03 UX audit round 4 — the font-size half of the 769–1099px gap
     above: without it, tapping the composer on a tablet zooms the whole
     conversation in and it stays zoomed after the keyboard closes.
     css/mobile.css only loads at <=768px (index.html), so between 769px and
     1099px a coarse-pointer tablet gets this overlay with no 16px guard. Same
     rule the inline edit box states at `textarea.msg-edit-input` ("16px
     prevents iOS zoom-on-focus — do not lower"). Scoped to `pointer: coarse`
     so the 14.5px desktop density base is untouched, and nested inside this
     block so it shares `_msgIsWide()`'s boundary and cannot drift again. */
  @media (pointer: coarse) {
    #mainContent .msg-reply-composer textarea,
    #mainContent .msg-mobile-search-input,
    #mainContent .msg-filter-search-input {
      font-size: 16px;
    }
  }
}
@media (prefers-reduced-motion: reduce) {
  .msg-mobile-detail { transition: none; }
}

/* ── WhatsApp-style chat rows (one row per team/person in the mobile Chats list) ── */
.msg-chat-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 14px;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
}
.msg-chat-row:active { background: var(--surface-2); }
.msg-chat-row:hover { background: color-mix(in srgb, var(--surface-2) 68%, transparent); }
.msg-chat-row:focus-visible {
  outline: 2px solid var(--accent, #1478ff);
  outline-offset: -2px;
  background: var(--surface-2);
}
.msg-chat-avatar {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
}
.msg-chat-avatar-team {
  background: var(--surface-2);
  border: 1px solid var(--border);
}
/* The DRAWN shipping container (js/utils.js `icon('container')`), used where
   every sibling marker is an emoji. Two things an emoji gets for free and a
   stroked SVG does not:
     · a COLOUR. `currentColor` on `--surface-2` would inherit whatever `color`
       the row happens to set — including #fff on the urgent-red variant right
       below, which would erase it. It takes `--text` explicitly, and the red
       row keeps its own inherited white by being MORE specific.
     · a BASELINE. An inline SVG sits on the text baseline and rides ~3px low
       next to a 15px emoji; `vertical-align: -0.18em` puts the two on the same
       optical line. */
.msg-ico-container { color: var(--text); vertical-align: -0.18em; }
.msg-chat-avatar-team .msg-ico-container { vertical-align: 0; }
/* Image avatar (mig 746 — supplier product photos, container illustration).
   White backing so transparent-bg product webps read cleanly in dark mode. */
.msg-chat-avatar-img {
  object-fit: cover;
  background: white;
  border: 1px solid var(--border);
}
.msg-chat-row-body { flex: 1; min-width: 0; }
.msg-chat-row-top {
  display: flex;
  /* `baseline` until 2026-09-02. The member bubbles moved onto this row, and a
     baseline has no meaning for a graphic — the discs sat low against the
     title. Centering is what aligns text to a circle. */
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
/* Cole 2026-09-02: "I also want all the bubbles that say who is in what chat to
   show to the right of the title of the chat, in between that and the time of
   the last message. That way, the most recent message can take the whole line
   below it. If the bubbles get cut off, then that's fine."

   So the title no longer claims the whole row (`flex: 1`): it takes what it
   needs and ellipsizes, the bubbles take the rest and are CLIPPED rather than
   ellipsized, and the time never shrinks. Both shrink proportionally when the
   row is tight, which is what "cut off by the time" means — a few discs stay
   visible instead of the strip vanishing the moment a title runs long. */
.msg-chat-row-title {
  display: flex;
  align-items: center;
  gap: 7px;
  flex: 0 1 auto;
  min-width: 0;
}
.msg-chat-row-name {
  min-width: 0;
  font-size: 14.5px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.msg-chat-row-time {
  font-size: 11px;
  color: var(--msg-meta);
  flex-shrink: 0;
}
.msg-chat-row-preview {
  flex: 1;
  min-width: 0;
  line-height: 1.3; /* 2026-09-01 mega-audit polish */
  font-size: 12.5px;
  color: var(--text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The preview now owns this line outright — the bubbles moved up to the title
   row (Cole 2026-09-02), which is the whole point of moving them. */
.msg-chat-row-subline {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  margin-top: 3px;
}
.msg-chat-participants {
  display: inline-flex;
  align-items: center;
  /* Takes the space LEFT OVER between title and time, and clips. No ellipsis:
     a "…" after a row of discs reads as a broken avatar, not as "there are
     more".

     `flex-basis: 0` is the load-bearing part. With `auto`, the discs' natural
     width joins the shrink calculation and the TITLE gets ellipsized to make
     room for them — measured in the browser, "Jianzhi — USA Support" came back
     as "Jianzhi — USA Sup…". Cole's rule is the other way round: "If the
     bubbles get cut off, then that's fine." A zero basis means the strip only
     ever consumes space nothing else claimed. The `min-width` floor keeps two
     or three discs visible rather than letting a long title erase the strip
     entirely — cut off, not gone. */
  flex: 1 1 0;
  min-width: 30px;
  overflow: hidden;
  padding-left: 3px;
  /* A disc sliced down the middle reads as a rendering fault. Fading the last
     10px says "this continues" with the same honesty and none of the alarm. */
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 10px), transparent 100%);
  mask-image: linear-gradient(to right, #000 calc(100% - 10px), transparent 100%);
}
.msg-chat-participant {
  width: 15px;
  height: 15px;
  /* Inside a shrinking flex line these would squash into ovals. */
  flex: none;
  margin-left: -3px;
  border: 1.5px solid var(--surface);
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  background: #6b7280;
  font-size: 10px;
  font-weight: 700; /* 2026-09-01 mega-audit polish */
  line-height: 1;
  box-sizing: border-box;
}
.msg-edit-attachments { display:flex;flex-wrap:wrap;gap:8px;margin-top:10px; }
.msg-edit-attachment { position:relative;display:inline-flex;align-items:center;max-width:220px;padding:6px;border:1px solid var(--border);border-radius:10px;background:var(--surface); } /* 2026-09-01 mega-audit polish: radius */
.msg-edit-attachment img { width:92px;height:72px;object-fit:cover;border-radius:6px; }
.msg-edit-attachment > span { overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px; }
.msg-edit-attachment button { position:absolute;top:-7px;right:-7px;width:22px;height:22px;border:0;border-radius:50%;background:var(--red);color:var(--surface);cursor:pointer;font-size:16px;line-height:20px; }
.msg-chat-participant:nth-child(6n + 1) { background: #7c3aed; }
.msg-chat-participant:nth-child(6n + 2) { background: #2563eb; }
.msg-chat-participant:nth-child(6n + 3) { background: #0f8a73; }
.msg-chat-participant:nth-child(6n + 4) { background: #b45309; }
.msg-chat-participant:nth-child(6n + 5) { background: #be185d; }
.msg-chat-participant:nth-child(6n) { background: #4f46e5; }
.msg-chat-row.is-active .msg-chat-participant {
  border-color: color-mix(in srgb, var(--accent) 16%, var(--surface));
}
/* 2026-09-03 (Cole): the discs moved out of the scrolling list and into the
   conversation header, beside the title. Here they are a roster, not a
   hint, so they get real size, no fade-out and no shrink-to-fit. */
.msg-chat-participants-slot { display: inline-flex; align-items: center; flex: none; min-width: 0; }
.msg-chat-detail-title .msg-chat-participants {
  flex: none;
  min-width: 0;
  overflow: visible;
  padding-left: 6px;
  -webkit-mask-image: none;
  mask-image: none;
}
.msg-chat-detail-title .msg-chat-participant {
  width: 20px;
  height: 20px;
  font-size: 10px;
  margin-left: -4px;
  border-width: 2px;
}
/* `--msg-meta` (#6b6c64), not `--text-dim` (#7a7b72). Measured against the row
   background on a phone: the dim token puts these at 4.03:1 and 4.28:1, under
   the 4.5:1 that small text needs, and this is 11.5px text read one-handed in
   whatever light the warehouse or a loading dock happens to have. The meta
   token is already the established "small quiet text" role on this surface —
   the bubble timestamp beside it uses it and clears 5.3:1 — so this is one
   role being applied consistently, not a new colour. */
.msg-chat-row-by { color: var(--msg-meta); }
.msg-chat-row-empty { color: var(--msg-meta); font-style: italic; }
.msg-chat-row.is-unread .msg-chat-row-name { font-weight: 700; }
.msg-chat-row.is-unread .msg-chat-row-preview { color: var(--text); font-weight: 500; }
/* The unread time is the one bit of accent-coloured TEXT in the list, at 10px
   on a phone: raw `--accent` measures 4.08:1. `--accent-strong` is the same
   hue carrying enough weight to be read. */
.msg-chat-row.is-unread .msg-chat-row-time { color: var(--accent-strong); font-weight: 600; }
.msg-chat-row-unread {
  /* 2026-09-01 mega-audit polish: shared count-chip metrics (18px pill, 10.5/700, tabular) */
  flex-shrink: 0;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--red);
  color: #fff;
  font-size: 10.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 18px;
  text-align: center;
}
/* ── Pinned + archived chats (mig 860) ────────────────────────────────────────
   A pinned row gets a faint accent ground so the block at the top of the list
   reads as deliberate rather than as "these happen to be recent". */
.msg-chat-row.is-pinned { background: color-mix(in srgb, var(--accent) 5%, transparent); }
.msg-chat-row.is-pinned:hover { background: color-mix(in srgb, var(--accent) 9%, transparent); }

/* ── Procurement / USA Support row tint (2026-08-17) ──────────────────────────
   Cole: "if a chat is a Procurement chat with just me lucy and supplier,
   highlight the preview bar scrolling for it light gray. if its a USA Support
   chat, highlight it light blue." `:not(.is-active)` so the selected-row accent
   (below) always wins over this — the tint says what KIND of room this is,
   the active state says which one you're looking at right now, and the two
   must not compete for the same background. --text-dim is theme-aware (a muted
   gray in both light/dark, see modern-theme.css); --blue likewise carries its
   own light/dark values, so this needs no separate dark-mode override. */
.msg-chat-row--procurement:not(.is-active) { background: color-mix(in srgb, var(--text-dim) 10%, transparent); }
.msg-chat-row--procurement:not(.is-active):hover { background: color-mix(in srgb, var(--text-dim) 16%, transparent); }
.msg-chat-row--support:not(.is-active) { background: color-mix(in srgb, var(--blue) 8%, transparent); }
.msg-chat-row--support:not(.is-active):hover { background: color-mix(in srgb, var(--blue) 14%, transparent); }
.msg-chat-row-pin {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  color: var(--text-muted);
}
/* ── Task boards in the chats list ───────────────────────────────────────────
   Paired "* Task Requests" channels are lists of assignable work, not
   conversations, so they sit in a labelled group above the chats and open a
   board pane instead of a message stream. */
.msg-chat-group-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 12px 16px 6px; /* 2026-09-01 mega-audit polish: border-bottom dropped */
}
.msg-chat-group-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}
.msg-board-row .msg-chat-row-preview { color: var(--text-muted); }
.msg-board-row.is-active { background: color-mix(in srgb, var(--accent) 9%, transparent); }

/* The board pane itself — header + filters pinned, task rows scrolling. */
.msg-task-board { display: flex; flex-direction: column; min-height: 0; }
.msg-task-board .msg-detail-topbar { justify-content: space-between; }
.msg-task-board-filters {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 10px;
}
.msg-board-mine-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
/* Urgent Tasks board (mig 962). Urgent customer asks live on their OWN board —
   Customer Tasks carries 25+ items and an urgent ask inside that list is still
   something you have to find. These rules make the board itself unmissable in
   the rail; the board is also force-pinned to the top in _msgTaskBoardRowModel,
   since boards there sort by recency and a quiet urgent board would sink. */
.msg-chat-row.msg-board-row--urgent {
  border-left: 3px solid var(--red);
  background: color-mix(in srgb, var(--red) 5%, transparent);
}
.msg-chat-row.msg-board-row--urgent .msg-chat-row-name { color: var(--red); font-weight: 700; }
.msg-chat-row.msg-board-row--urgent .msg-chat-avatar-team {
  background: var(--red);
  color: #fff;
}
/* …and the drawn glyph inside it, which pins its own `--text` colour and would
   otherwise stay dark on the red disc. */
.msg-chat-row.msg-board-row--urgent .msg-chat-avatar-team .msg-ico-container {
  color: #fff;
}
/* "New urgent task" on a non-urgent board — red outline so it is clearly a
   different action from the neutral New task beside it, not a second copy. */
.btn.msg-new-urgent-btn {
  border-color: var(--red);
  color: var(--red);
  font-weight: 600;
}
.btn.msg-new-urgent-btn:hover {
  background: var(--red);
  color: #fff;
}
.msg-task-board-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 4px 22px 18px;
}
/* On a phone the shared filter styling stretches .msg-status-toggle to the full
   row; match it so "Assigned to me" reads as the same control, not an offcut. */
@media (max-width: 768px) {
  .msg-task-board-body { padding: 4px 12px 18px; }
  .msg-task-board-filters .msg-board-mine-btn {
    flex: 1 1 100%;
    justify-content: center;
    min-height: 38px;
    font-size: 13px;
  }
}

/* The one row that stands for every archived chat. */
.msg-archived-toggle { color: var(--text-muted); }
.msg-archived-toggle .msg-chat-row-name { color: var(--text-muted); font-weight: 600; }
.msg-archived-toggle .msg-chat-avatar-team { background: var(--surface-2); color: var(--text-muted); }
.msg-archived-count {
  flex-shrink: 0;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text-secondary);
  font-size: 11.5px;
  font-weight: 700;
  line-height: 20px;
  text-align: center;
}

/* Chat detail header title (channel/person name + avatar) */
.msg-chat-detail-title {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  min-width: 0;
}
.msg-chat-detail-title .msg-chat-avatar,
.msg-chat-detail-title .msg-reply-avatar { width: 26px; height: 26px; font-size: 12.5px; }
/* Name over a presence line, the way a chat app stacks them. */
.msg-chat-detail-titles,
.msg-chat-detail-copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-width: 0;
  line-height: 1.15;
}
/* …but BESIDE it from tablet up (Cole 2026-09-04: the chat header is "just one
   thin row"). The stack cost the topbar a second text line — ~19px on every
   conversation, forever. The subtitle already ellipsizes (.msg-convo-sub below),
   so inlining truncates rather than wraps. Kept off the phone deliberately:
   at 375px the name eats the width and the presence line truncates to nothing,
   and css/mobile.css does NOT re-declare flex-direction here, so a base-rule
   change would have reached the phone with no override to catch it. */
@media (min-width: 769px) {
  .msg-chat-detail-titles,
  .msg-chat-detail-copy {
    flex-direction: row;
    align-items: baseline;
    gap: 8px;
    line-height: 1.2;
  }
}
.msg-convo-sub {
  font-size: 11.5px;
  font-weight: 400;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.msg-convo-sub:empty { display: none; }
.msg-convo-online { color: var(--green); font-weight: 600; }

/* ── Search within the open conversation ──────────────────────────────────── */
.msg-convo-search-btn {
  margin-left: auto;
  flex: none;
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-muted);
  border-radius: var(--radius-sm);
  padding: 5px 7px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
}
.msg-convo-search-btn:hover { color: var(--text); background: var(--surface-2); }
.msg-convo-search-btn[aria-expanded="true"] { color: var(--accent); border-color: var(--border); }

.msg-convo-search {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-muted);
}
.msg-convo-search-input {
  flex: 1;
  min-width: 0;
  border: none;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 13px;
  outline: none;
}
.msg-convo-search-input::-webkit-search-cancel-button { display: none; }
/* Two X's (Cole 2026-08-06). `<input type="search">` draws its OWN clear button
   in WebKit, and these fields also render `.msg-search-clear-btn` — so a search
   with text in it showed two different-looking clears side by side, one styled
   and one not. The convo search above already suppressed the native one; the
   list searches never did. Ours stays: it is themed, has an aria-label, and is
   the one wired to `msg-clear-search` (which also reloads the thread list —
   the native button only empties the field, leaving stale results on screen). */
.msg-filter-search-input::-webkit-search-cancel-button,
.msg-mobile-search-input::-webkit-search-cancel-button { display: none; }
.msg-convo-search-count {
  font-size: 11.5px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.msg-convo-search-nav {
  flex: none;
  border: none;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  padding: 3px;
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
}
.msg-convo-search-nav:hover { color: var(--text); background: var(--surface-2); }

/* Whole-bubble highlight — the content element belongs to the translate
   toggle, so rewriting its innerHTML to wrap matches would fight it. */
.msg-replies-container .msg-reply.is-search-hit {
  outline: 2px solid color-mix(in srgb, var(--yellow) 55%, transparent);
  outline-offset: 1px;
}
.msg-replies-container .msg-reply.is-search-current {
  outline: 2px solid var(--yellow);
  outline-offset: 1px;
  box-shadow: 0 0 0 4px var(--yellow-dim);
}
.msg-chat-detail-copy > span {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* The copy is a flex-start column, so this span is max-content wide unless
     told otherwise — nowrap + ellipsis never engaged and the name painted past
     the title box, under the copy button, once the phone let the title shrink
     (2026-09-07). Same fix .msg-convo-sub already carries. */
  max-width: 100%;
}
.msg-chat-detail-copy small {
  color: var(--text-muted);
  font-size: 10px;
  font-weight: 500;
}
.msg-search-section-label {
  padding: 10px 16px 5px;
  color: var(--text-muted);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
}

/* Small team/DM label chip prefixed on a thread-row title when the channel
   context isn't otherwise on screen (Split View + the phone merged list). Only
   renders when `opts.teamLabel` is passed, so the desktop channel/combined views
   never show it. */
.msg-thread-row-team {
  display: inline-block;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-muted);
  background: var(--surface-2);
  border-radius: 5px;
  padding: 1px 6px;
  margin-right: 6px;
  vertical-align: middle;
}

/* Header row above the flat cross-channel search results list. */
.msg-search-results-head {
  padding: 9px 16px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border);
}

/* Chat / Tasks tab switcher for a team (migration: team-tabs nav consolidation) */
.msg-team-tabs {
  display: flex;
  gap: 8px;
  padding: 10px 32px 0;
  background: var(--surface);
}
.msg-team-tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 16px;
  border: 1px solid var(--border);
  border-bottom: none;
  background: var(--bg);
  color: var(--text-muted);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  border-radius: 8px 8px 0 0;
  position: relative;
  top: 1px;
}
.msg-team-tab:hover { color: var(--text); }
.msg-team-tab.is-active {
  color: var(--accent);
  border-color: var(--border);
  background: var(--surface);
  border-bottom: 2px solid var(--accent);
}
.msg-team-tab svg { opacity: 0.8; }
.msg-team-tab-badge {
  background: var(--red, #e5484d);
  color: #fff;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 700;
  min-width: 17px;
  height: 17px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 5px;
}

/* Collapsed "Task activity" strip on a team's general channel — recent
   updates from the paired -tasks channel, faded, click-through to the task. */
.msg-task-activity-strip {
  padding: 6px 32px 0;
  background: var(--surface);
}
.msg-task-activity-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
/* Collapsed strip is ONE row: the newest activity line rides beside the toggle
   instead of on a second row (Cole 2026-08-12 — the two-row strip plus the
   composer left barely any conversation visible on a phone keyboard). */
.msg-task-activity-inline {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
}
.msg-task-activity-inline .msg-task-activity-line {
  flex: 1 1 auto;
  min-width: 0;
  padding: 3px 2px;
}
.msg-task-activity-toggle {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 3px 2px;
  /* Shares its row with the collapsed inline line — the LINE ellipsizes, the
     label never wraps to two lines. */
  flex: 0 0 auto;
  white-space: nowrap;
}
.msg-task-activity-toggle:hover { color: var(--text); }
.msg-task-activity-toggle svg { opacity: 0.75; }
.msg-task-activity-count {
  background: var(--accent);
  color: var(--surface);
  border-radius: 999px;
  font-size: 10px;
  font-weight: 700;
  min-width: 15px;
  height: 15px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 4px;
}
.msg-task-activity-body {
  display: flex;
  flex-direction: column;
  padding: 2px 0 6px;
}
.msg-task-activity-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  background: none;
  border: none;
  text-align: left;
  cursor: pointer;
  padding: 3px 2px;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-family: inherit;
  font-size: 11.5px;
  line-height: 1.35;
  /* The line renders as a <button> (opens a task in place) OR an <a> (deep-links
     to Team Projects / the Support Inbox) OR a <div> (nothing to open). Reset
     the anchor's underline + link colour so all three read identically. */
  text-decoration: none;
}
.msg-task-activity-line:hover { background: var(--bg); color: var(--text); }
.msg-task-activity-line:hover .msg-task-activity-line-text strong,
.msg-task-activity-line:hover .msg-task-activity-line-text em { color: var(--text); }
/* Events with NO destination (a sweep, a presence ping, a project the reader
   cannot open) — same look, deliberately no affordance: no pointer, no hover
   highlight, not focusable. Anything that does nothing must not look clickable. */
.msg-task-activity-line--static { cursor: default; }
.msg-task-activity-line--static:hover { background: none; color: var(--text-muted); }
/* "· 82 times" on a collapsed line — a count, not part of the sentence, so it
   recedes. Never the first thing clipped: it sits inside the ellipsised text. */
.msg-activity-repeat { color: var(--text-muted); }
.msg-task-activity-line-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-task-activity-line-text strong { color: var(--text); font-weight: 600; }
.msg-task-activity-line-text em { font-style: normal; color: var(--text); }
.msg-task-activity-line:not(.msg-task-activity-line--static) .msg-task-activity-line-text em {
  color: var(--accent-strong, var(--accent));
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--accent) 45%, transparent);
  text-underline-offset: 2px;
}
.msg-task-activity-line:not(.msg-task-activity-line--static):hover .msg-task-activity-line-text em {
  color: var(--accent-strong, var(--accent));
  text-decoration-color: currentColor;
}
.msg-task-activity-line:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.msg-task-activity-line-time { flex-shrink: 0; color: var(--text-muted); }
/* The "goes somewhere" chevron (2026-08-21). The accent-underlined title is the
   primary affordance, but a dashboard-action line has no title to underline —
   "Maria closed ticket #1a02…" is one flat sentence — so a real <a href> looked
   exactly like an inert row. On touch there is no hover or cursor either, which
   left the phone with no signal at all. Rendered ONLY when the row has a
   destination, so it stays a positive marker rather than decoration. */
.msg-task-activity-line-go {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  color: var(--accent);
  opacity: 0.55;
}
.msg-task-activity-line:hover .msg-task-activity-line-go { opacity: 1; }
/* Two classes, deliberately. `.msg-task-activity-line--stream svg` fades every
   glyph in a stream row to 0.6 and is declared LATER in this file, so a
   single-class rule here loses the tie and the chevron compounds to 0.33 —
   a ghost, on the one surface (touch) where it is the ONLY affordance.
   Measured; the Playwright case asserts the product of both opacities. */
.msg-task-activity-line .msg-task-activity-line-go svg { display: block; opacity: 1; }
/* Placed inside the WhatsApp-style channel-chat detail (msgChannelChatTaskActivity)
   instead of the channel header — narrower padding + a bottom border since it
   sits directly above the reply stream rather than under the header. */
.msg-task-activity-strip--chat {
  padding: 6px 24px 4px;
  border-bottom: 1px solid var(--border);
}

/* ── Keyboard up on a phone: give the room back to the conversation ────────
   Measured on 375x812 with the keyboard open: the header (~100px) plus the task
   activity strip (~140px, head + newest line) left about ONE readable message
   above the composer. Cole, 2026-08-08, arriving from a notification: "i can't
   read any of the above messages or anything theres too much wasted space."

   `--compact` is set by _msgSyncMobileViewport whenever the visual viewport
   shrinks, and cleared when the keyboard goes away — so nothing is permanently
   removed. The activity strip and the header's second line are one tap away
   again the moment you stop typing, which is when you have room for them.

   Deliberately NOT hidden: the conversation name and the back affordance. Losing
   either would leave you typing into a room you can't identify or leave. */
.msg-mobile-detail--compact .msg-task-activity-strip { display: none; }
.msg-mobile-detail--compact .msg-convo-sub { display: none; }
.msg-mobile-detail--compact .msg-channel-header {
  padding-top: 4px;
  padding-bottom: 4px;
}
/* The same event rendered INSIDE the chat stream, interleaved with messages —
   dimmer than chat text so it reads as a task event, not a message; hover
   restores full strength as the click-affordance. */
.msg-task-activity-line--stream {
  padding: 3px 18px;
  align-items: center;
  color: var(--text-muted);
  font-size: 11px;
}
.msg-task-activity-line--stream svg { flex-shrink: 0; opacity: 0.6; }
.msg-task-activity-line--stream .msg-task-activity-line-text strong,
.msg-task-activity-line--stream .msg-task-activity-line-text em { color: var(--text-muted); }
.msg-task-activity-line--stream:hover { background: var(--bg); }
.msg-task-activity-line--stream:hover .msg-task-activity-line-text strong,
.msg-task-activity-line--stream:hover .msg-task-activity-line-text em { color: var(--text); }

.msg-status-toggle {
  display: inline-flex;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 2px;
  gap: 0;
}

.msg-status-toggle .toggle-btn {
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-size: 12.5px;
  font-weight: 500;
  padding: 5px 14px;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
  font-family: inherit;
}

.msg-status-toggle .toggle-btn:hover {
  color: var(--text);
}

.msg-status-toggle .toggle-btn.active {
  background: var(--text);
  color: var(--surface);
  font-weight: 600;
}

.msg-filter-spacer {
  flex: 1;
}

.msg-filter-search-wrap {
  position: relative;
  display: flex;
  align-items: center;
}

.msg-filter-search-wrap svg {
  position: absolute;
  left: 10px;
  color: var(--text-muted);
  pointer-events: none;
}

.msg-filter-search-input {
  font-size: 12.5px;
  padding: 6px 12px 6px 32px;
  min-width: 220px;
  border-radius: var(--radius-sm);
}

.msg-filter-search-input.has-clear {
  padding-right: 40px;
}

.msg-search-clear-btn {
  position: absolute;
  right: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  border-radius: 50%;
}
.msg-search-clear-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  color: var(--text);
}

.msg-search-clear-btn:hover {
  color: var(--text);
  background: var(--hover-bg, rgba(0, 0, 0, 0.06));
}

.msg-filter-tagged-select {
  font-size: 12.5px;
  padding: 6px 10px;
  min-width: 140px;
  border-radius: var(--radius-sm);
}

/* Compose */
.msg-compose-area {
  padding: 0 32px;
}

.msg-compose-box {
  margin: 16px 0;
  padding: 18px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-xs);
  /* Tall task-compose forms must never push Cancel/Create off-screen: cap the
     box to the viewport and scroll its body, with the actions pinned (below). */
  display: flex;
  flex-direction: column;
  max-height: calc(100dvh - 230px);
  overflow-y: auto;
  overflow-x: hidden;
}

.msg-compose-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}

.msg-compose-title {
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text);
}

.msg-compose-title .ch-name {
  color: var(--accent);
}

.msg-compose-hint {
  font-size: 11px;
  color: var(--text-muted);
}

.msg-compose-tag-row {
  margin-top: 10px;
}

.msg-compose-tag-row-label {
  font-size: 11.5px;
  color: var(--text-muted);
  margin-bottom: 6px;
  font-weight: 500;
}

.msg-compose-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  align-items: center;
  /* Pinned to the bottom of the (scrollable) compose box so Cancel + Create
     stay visible no matter how tall the task form gets. */
  position: sticky;
  bottom: -18px;                 /* cancel the box's 18px bottom padding */
  margin: 14px -18px -18px;      /* bleed to the box edges */
  padding: 12px 18px;
  background: var(--bg);
  border-top: 1px solid var(--border);
  border-bottom-left-radius: var(--radius);
  border-bottom-right-radius: var(--radius);
}

/* Thread list */
.msg-thread-list {
  flex: 1;
  overflow-y: auto;
  min-height: 0;
  padding: 8px 0 24px;
  /* Reaching the end of the list must not hand the scroll to the page behind
     it. Without this, the last flick in a long conversation list carries on
     into the dashboard body — and on iOS it can reach the top of the document
     and arm pull-to-refresh, which reloads the whole app mid-read. */
  overscroll-behavior: contain;
}

.msg-thread-row {
  display: grid;
  grid-template-columns: auto 1fr auto;  /* status dot | body | read-toggle */
  align-items: start;
  gap: 10px; /* 2026-09-01 mega-audit polish: tighter grid gap */
  padding: 12px 16px; /* 2026-09-01 mega-audit polish: 16px list gutter (base) */
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background 0.12s ease;
  position: relative;
}

.msg-thread-row:hover {
  background: var(--bg);
}

/* A phone has no hover, so without this a tap on a conversation gives nothing
   back until the detail paints — the row you pressed and the row beside it look
   identical for the whole round trip. Feedback belongs on the press, not on the
   result (the sibling .msg-chat-row already worked this way; the task/thread
   list did not). */
.msg-thread-row:active {
  background: var(--surface-2);
}

.msg-thread-row::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: transparent;
  transition: background 0.12s ease;
}

.msg-thread-row:hover::before,
.msg-thread-row:active::before {
  background: var(--accent);
}

/* Leading dot = UNREAD status (the thing you scan for). Filled accent dot with a
   glow = unread; a faint hollow ring = read/seen. (Open/closed is conveyed by the
   "Closed" pill in the row meta + the Open/Closed/All filter, not this dot.) */
.msg-thread-unread-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
  margin-top: 5px;  /* align with the first line of the wrapped title */
  box-sizing: border-box;
}
.msg-thread-unread-dot.is-unread {
  background: var(--red); /* 2026-09-01 mega-audit polish: bare token, tokenized halo */
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 18%, transparent);
}
.msg-thread-unread-dot.is-read {
  background: transparent;
  border: 1.5px solid var(--border);
}
/* Per-row unread MESSAGE count — a red badge ("3") in the leading slot, like a
   chat app. Replaces the dot when there are unread messages to count. */
.msg-thread-unread-badge {
  /* 2026-09-01 mega-audit polish: shared count-chip metrics */
  flex-shrink: 0;
  margin-top: 2px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 10.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: #fff;
  background: var(--red);
  border-radius: 999px;
}
/* Combined-team list sections (channel view): "Messages" then "Tasks", with a
   clear gap so the two categories are obviously distinct. */
.msg-thread-section--tasks { margin-top: 22px; } /* 2026-09-01 mega-audit polish: real gap instead of a border spacer */
.msg-thread-section-head {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 22px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  font-size: 10.5px; /* 2026-09-01 mega-audit polish: match .msg-chat-group-label micro-label metrics */
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}
.msg-thread-section-title { display: inline-flex; align-items: center; gap: 6px; }
.msg-thread-section-unread {
  /* 2026-09-01 mega-audit polish: shared count-chip metrics */
  flex-shrink: 0;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 10.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: #fff;
  background: var(--red);
  border-radius: 999px;
}
.msg-thread-section-new {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 10.5px;
  font-weight: 600;
  text-transform: none;
  letter-spacing: 0;
  padding: 2px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text-secondary);
  cursor: pointer;
}
.msg-thread-section-new:hover { color: var(--accent); border-color: var(--accent); }
.msg-thread-section-empty {
  padding: 14px 22px;
  font-size: 12.5px;
  font-style: italic;
  color: var(--text-muted);
}

/* Headline row: title on the left, WhatsApp-style last-activity time on the right. */
.msg-thread-row-headline {
  display: flex;
  align-items: flex-start;
  gap: 8px;
}
.msg-thread-row-headline .msg-thread-row-title { flex: 1; min-width: 0; }
.msg-thread-row-time {
  flex-shrink: 0;
  margin-top: 2px;
  font-size: 11px; /* 2026-09-01 mega-audit polish */
  color: var(--text-muted);
  white-space: nowrap;
}
.msg-thread-row--unread .msg-thread-row-time {
  color: var(--red); /* 2026-09-01 mega-audit polish: bare token */
  font-weight: 600;
}
/* Latest-message preview line ("Name: text") — one truncated line, iMessage-style. */
.msg-thread-row-preview {
  font-size: 12.5px;
  color: var(--text-secondary);
  margin-top: 1px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.msg-thread-row-preview-by { font-weight: 600; color: var(--text-muted); }
/* Unread rows: keep the latest message at full strength so it reads as "new". */
.msg-thread-row--unread .msg-thread-row-preview { color: var(--text); }

.msg-thread-row-body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.msg-thread-row-title {
  font-size: 14.5px; /* 2026-09-01 mega-audit polish: was 16px; the density pass value is now the base */
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.01em;
  /* Titles are short headlines now (see _msgShortTitle / server _auto_title), so
     they read as a clear, prominent label. Clamp to 2 lines for the rare long
     one; the latest-message preview sits below. */
  white-space: normal;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  line-height: 1.3;
  word-break: break-word;
}

.msg-thread-row-meta {
  font-size: 12px;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

/* ── Read vs unread (email-client style) ──────────────────────────────────
   Read rows are de-emphasized (lighter weight + dimmer title); unread rows are
   bright + bold with a leading accent dot. Non-task unread rows also get a soft
   tint + a persistent left accent bar (task rows keep their priority stripe, so
   we don't add a bar there). Makes "did I read this?" obvious at a glance. */
.msg-thread-row--read .msg-thread-row-title {
  font-weight: 500;
  color: var(--text-muted);
}
/* 2026-09-01 mega-audit polish: whole-row opacity dimmed avatars/pills/times
   into a fog; read rows now recede through their TEXT instead (dim title above,
   dim preview here). */
.msg-thread-row--read .msg-thread-row-preview {
  color: var(--text-muted);
}
.msg-thread-row--unread .msg-thread-row-title {
  font-weight: 700; /* 2026-09-01 mega-audit polish: 800 read as a different face */
  color: var(--text);
}
.msg-thread-row--unread:not(.msg-task-row) {
  background: color-mix(in srgb, var(--accent) 14%, transparent);  /* stronger accent wash — 2026-09-01 mega-audit polish: tokenized */
}
.msg-thread-row--unread:not(.msg-task-row)::before {
  background: var(--accent);
  width: 4px;  /* thicker persistent left accent bar (overrides 3px hover default) */
}
/* Unread task rows: keep the priority stripe + status tint, but lift them above
   their dimmed read siblings with a soft accent ring drawn INSIDE the row so it
   never collides with the left priority box-shadow. */
.msg-thread-row--unread.msg-task-row {
  outline: 1.5px solid color-mix(in srgb, var(--accent) 45%, transparent); /* 2026-09-01 mega-audit polish: tokenized */
  outline-offset: -1.5px;
}

/* Per-row read/unread toggle (envelope / check). Hidden until row hover; always
   faintly visible on unread rows so the action is discoverable. */
.msg-thread-readtoggle {
  align-self: center;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  padding: 5px 7px;
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  opacity: 0;
  transition: opacity 0.12s ease, color 0.12s ease, background 0.12s ease;
}
.msg-thread-row:hover .msg-thread-readtoggle { opacity: 0.75; }
.msg-thread-row--unread .msg-thread-readtoggle { opacity: 0.85; color: var(--accent); }
.msg-thread-readtoggle:hover { opacity: 1; color: var(--accent); background: var(--surface-2); }
/* Its touch treatment lives in the ONE canonical `@media (hover: none)` block
   further down (next to the bubble-action rules) — a second hover:none block
   here would fragment the touch contract across the file. */

.msg-thread-row-meta .author {
  color: var(--text);
  font-weight: 500;
}

.msg-thread-row-meta .replies {
  color: var(--text-muted);
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.msg-thread-row-meta .dot {
  color: var(--text-muted);
}

.msg-tag-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 9px;
  background: var(--accent-dim);
  color: var(--accent);
  font-size: 11px;
  font-weight: 600;
  border-radius: 10px;
  border: 1px solid transparent;
}

/* Empty + loading */
.msg-empty {
  padding: 80px 32px;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}

.msg-empty-icon {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--accent-dim);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 4px;
}

.msg-empty-title {
  font-size: 17px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.01em;
  margin: 0;
}

.msg-empty-body {
  font-size: 13.5px;
  color: var(--text-muted);
  max-width: 380px;
  line-height: 1.55;
  margin: 0;
}

.msg-empty-cta {
  margin-top: 8px;
}

.msg-empty-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
}

.msg-load-more-row {
  display: flex;
  justify-content: center;
  padding: 20px 32px 8px;
}

/* ─── Thread detail ───────────────────────────────────────────────────────── */

.msg-detail-shell {
  display: flex;
  flex-direction: column;
  /* Same measured reserve as .msg-shell (2026-09-04) — this is the phone/tablet
     full-screen conversation, which the mobile overlay then overrides to
     height:100% inside its fixed box. */
  min-height: calc(100dvh - var(--msg-shell-reserve, 200px));
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  position: relative;
}

/* ─── Conversation + Files rail ───────────────────────────────────────────────
   The shell body splits into the conversation column (left, flexible) and a
   files rail (right, fixed) that surfaces every attachment in the thread so
   they aren't buried in scrollback. The rail is a wide-desktop affordance —
   hidden below 1280px so narrower layouts keep the full conversation width. */
.msg-detail-body {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: stretch;
}
.msg-detail-convo {
  flex: 1;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.msg-detail-files {
  flex-shrink: 0;
  width: 254px;
  border-left: 1px solid var(--border);
  background: var(--bg);
  display: none;  /* shown only on wide desktop (see @media below) */
  /* Files + Updates are each their own scroll region (Cole 2026-07-16: "scroll
     up and down windows within that section") — the aside itself never
     scrolls, so the whole sidebar always fits its column with no page-level
     scroll needed to reach the bottom. */
  flex-direction: column;
  overflow: hidden;
}
@media (min-width: 1280px) {
  .msg-shell.msg-has-detail .msg-detail-files { display: flex; }
}
/* 254px was sized for a list of links. The media grid wants width, and a wide
   screen has it to spare — the transcript keeps every pixel below 1600px. */
@media (min-width: 1600px) { .msg-detail-files { width: 320px; } }
@media (min-width: 1920px) { .msg-detail-files { width: 368px; } }
.msg-seko-bol-btn {
  font-size: 12.5px;
  padding: 6px 10px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.msg-seko-bol-backdrop {
  position: fixed;
  inset: 0;
  z-index: 4000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 18px;
  background: rgba(0, 0, 0, 0.42);
}
.msg-seko-bol-modal {
  width: min(520px, 100%);
  max-height: min(720px, calc(100dvh - 36px));
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--card-bg);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.28);
  padding: 16px;
}
.msg-seko-bol-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 14px;
}
.msg-seko-bol-title {
  font-size: 16px;
  font-weight: 700;
  color: var(--text);
}
.msg-seko-bol-sub {
  margin-top: 3px;
  font-size: 12.5px;
  color: var(--text-muted);
}
.msg-seko-bol-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
.msg-seko-bol-grid label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 12px;
  color: var(--text-secondary);
}
.msg-seko-bol-drop {
  width: 100%;
  margin-top: 12px;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 1px dashed var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
}
.msg-seko-bol-drop:hover {
  border-color: var(--accent);
  background: var(--surface-2);
}
.msg-seko-bol-staging { margin-top: 10px; }
.msg-seko-bol-note {
  margin-top: 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  padding: 9px 10px;
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--text-secondary);
}
.msg-seko-bol-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 16px;
}
@media (max-width: 640px) {
  .msg-seko-bol-backdrop { align-items: flex-end; padding: 10px; }
  .msg-seko-bol-grid { grid-template-columns: 1fr; }
}
/* Files half — bounded, own scrollbar. When Updates isn't present (DM, or a
   channel with no interleaved activity) it grows to fill the whole column. */
.msg-files-section {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: 14px 14px 10px;
}
.msg-files-section--solo { flex: 1 1 auto; }
.msg-files-scroll { flex: 1; min-height: 0; overflow-y: auto; padding-right: 2px; }
.msg-task-rail {
  /* Grows twice as fast as the Updates section below it: tasks are the reason
     the rail exists now, updates are the ambient feed beside them. */
  flex: 2 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: 14px;
}
/* Every rail section is the same disclosure (Cole 2026-08-06). Closed, it is a
   single header row; open, it becomes a bounded scroll region. The aside itself
   must never scroll — see .msg-detail-files — so closed sections collapse to
   `flex:0 0 auto` and hand their height to whichever ones are open. */
.msg-rail-section {
  display: flex;
  flex-direction: column;
  min-height: 0;
  border-top: 1px solid var(--border);
}
.msg-rail-section:first-child { border-top: 0; }
.msg-rail-section-toggle {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  min-height: 34px;
  padding: 10px 14px;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: 11.5px;
  font-weight: 750;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-align: left;
  cursor: pointer;
}
.msg-rail-section-label { flex: 1; min-width: 0; }
.msg-rail-section-toggle:hover { color: var(--text); }
.msg-rail-section-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.msg-rail-section-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: 0 14px 12px;
  /* 2026-09-02 (Cole: "weird and glitchy overlap with the tasks in the
     media"): with two sections open in a short rail this body shrank to 0
     but its non-scrolling children (the hint paragraph, the compose form)
     kept painting, so the next section's header sat on top of the Tasks
     text. The body clips now, and an OPEN section keeps a floor so it is
     never squeezed to a sliver. */
  overflow: hidden;
}
.msg-rail-section.is-open { flex: 1 1 0; min-height: 96px; }
.msg-rail-section:not(.is-open) { flex: 0 0 auto; }
.msg-rail-section-head { position: relative; z-index: 1; background: var(--bg); }
/* The scrollers inside each section body. */
.msg-rail-section .msg-task-rail-list,
.msg-rail-section .msg-updates-list,
.msg-rail-section .msg-files-scroll { flex: 1; min-height: 0; overflow-y: auto; }

/* Media rail scrolling. Three separate causes of the judder Cole reported
   2026-08-27 ("the media scrolling is really glitchy"):

   1. Every thumbnail carries a `.msg-files-jump` button with
      `backdrop-filter: blur(6px)`. A backdrop filter forces the compositor to
      read back the pixels behind it on every frame, and there is one PER TILE
      in a scrolling grid — the single most expensive thing that can be
      attached to a repeating list item. It is now an opaque background, which
      looks the same over a photo and costs nothing.
   2. No scroll containment, so reaching either end chained the scroll to the
      page behind the rail — the "glitch" of the whole layout lurching.
   3. No layout containment on the cells, so a decoding image invalidated
      layout for the entire grid. (The tiles already declare `aspect-ratio`,
      so their boxes are correct before the bytes arrive; containment keeps
      that locality.) */
.msg-files-scroll {
  overscroll-behavior: contain;
  scrollbar-gutter: stable;
}
.msg-files-cell { contain: layout; }


.msg-task-rail-sub {
  margin: 10px 2px 8px;
  color: var(--text-muted);
  font-size: 11px;
  line-height: 1.35;
}
.msg-task-rail-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 2px 2px 2px 1px;
}
.msg-task-rail-item {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 100%;
  min-height: 58px;
  padding: 9px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  text-align: left;
  cursor: pointer;
  box-shadow: var(--shadow-xs, none);
}
.msg-task-rail-item:hover {
  border-color: var(--accent-dim);
  background: var(--surface-2);
}
.msg-task-rail-item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.msg-task-rail-item-title {
  display: -webkit-box;
  overflow: hidden;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  font-size: 12.5px;
  font-weight: 650;
  line-height: 1.3;
}
.msg-task-rail-item-meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  color: var(--text-muted);
  font-size: 10.5px;
  line-height: 1.3;
}
.msg-task-rail-item-event {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.msg-task-rail-item-time {
  flex-shrink: 0;
  color: var(--text-muted);
}
.msg-files-count {
  background: var(--surface-2);
  color: var(--text-secondary);
  border-radius: 999px;
  padding: 1px 7px;
  font-size: 11px;
  font-weight: 700;
}

/* ── Media / Links / Docs tabs ────────────────────────────────────────────────
   Replaces the single "Files" heading. Three equal segments so the rail reads
   as a gallery with sections rather than one undifferentiated list. */
.msg-gallery-tabs {
  display: flex;
  gap: 2px;
  padding: 0 0 10px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 10px;
}
.msg-gallery-tab {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 5px 4px;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  cursor: pointer;
  white-space: nowrap;
}
.msg-gallery-tab:hover { background: var(--surface-2); color: var(--text); }
.msg-gallery-tab.is-active { background: var(--accent-dim); color: var(--accent-strong); }
.msg-gallery-tab.is-active .msg-files-count { background: var(--surface); }
.msg-gallery-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
/* The link rows reuse .msg-files-item; only the glyph needs centring for an
   SVG rather than an emoji. */
.msg-files-item .msg-files-glyph svg { display: block; opacity: 0.7; }
.msg-files-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  text-align: center;
  color: var(--text-muted);
  padding: 32px 12px;
  font-size: 12.5px;
}
.msg-files-empty p { margin: 0; line-height: 1.4; }
.msg-files-empty svg { opacity: 0.45; }
/* Media grid. Was a fixed 2 columns of 1:1 tiles, which showed about four
   photos at a time in a 254px rail (Cole 2026-08-20: "such a tight space right
   now, can't see much at once"). auto-fill lets the same markup pack 2 columns
   in the narrow rail and 4-5 once the rail widens on a large screen, and it
   needs no breakpoint of its own — the track count follows the container. */
.msg-files-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(68px, 1fr));
  gap: 6px;
  margin-bottom: 12px;
}
.msg-files-cell { position: relative; min-width: 0; }
.msg-files-thumb {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: 8px;
  border: 1px solid var(--border);
  cursor: zoom-in;
  display: block;
  background: var(--surface-2);
}
/* Jump-to-message. The tile keeps its one-click preview; this corner button is
   the second intent. Hidden until hover/focus so a wall of thumbnails stays a
   wall of thumbnails — but always present for keyboard and touch, where there
   is no hover to reveal it. */
/* The background is opaque, NOT `backdrop-filter: blur(6px)`. A backdrop filter
   makes the compositor read back the pixels behind it every frame, and there is
   one of these per TILE in a scrolling grid — the most expensive thing that can
   ride a repeating list item. Over a photo the blur was invisible anyway.
   Kept OUTSIDE the rule body on purpose: tests/test_messages_media_gallery_density.py
   reads the first 900 characters after `.msg-files-jump {` to prove the
   `@media (hover: none)` touch fallback below is present, so prose inside the
   block pushes that proof out of the window and fails a test that is about
   reachability, not about comments. */
.msg-files-jump {
  position: absolute;
  top: 4px;
  right: 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: color-mix(in srgb, var(--bg) 88%, var(--surface-2));
  color: var(--text);
  cursor: pointer;
  opacity: 0;
  transition: opacity 120ms ease, background 120ms ease;
}
.msg-files-cell:hover .msg-files-jump,
.msg-files-jump:focus-visible { opacity: 1; }
.msg-files-jump:hover { background: var(--surface-2); }
.msg-files-jump:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
@media (hover: none) {
  .msg-files-jump { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .msg-files-jump { transition: none; }
}
/* Copy-for-Claude on a gallery tile — same box and the same reveal rules as
   the jump button beside it, one slot to its left, so the pair reads as one
   control group and neither can drift (2026-09-05). The touch fallback below
   matters most here: on a phone there is no hover, and this is the surface
   people reach for when the photo is old enough to have scrolled out. */
.msg-files-ref {
  position: absolute;
  top: 4px;
  right: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: color-mix(in srgb, var(--bg) 88%, var(--surface-2));
  color: var(--text);
  cursor: pointer;
  opacity: 0;
  transition: opacity 120ms ease, background 120ms ease;
}
.msg-files-cell:hover .msg-files-ref,
.msg-files-ref:focus-visible { opacity: 1; }
.msg-files-ref:hover { background: var(--surface-2); }
.msg-files-ref:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
@media (hover: none) {
  .msg-files-ref { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .msg-files-ref { transition: none; }
}
.msg-files-cap {
  display: block;
  margin-top: 3px;
  font-size: 10px;
  line-height: 1.25;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.msg-files-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.msg-files-item {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  text-decoration: none;
  color: var(--text);
  transition: background 0.12s ease, border-color 0.12s ease;
}
.msg-files-item:hover { background: var(--surface-2); border-color: var(--border-hover); }
.msg-files-item--dead { cursor: default; opacity: 0.6; }
.msg-files-glyph { font-size: 18px; line-height: 1; flex-shrink: 0; }
.msg-files-info { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
.msg-files-name {
  font-size: 12.5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.msg-files-meta { font-size: 11px; color: var(--text-muted); }

/* Drag-and-drop-to-attach overlay — covers the whole thread pane, only
   visible while a file is being dragged over it. */
.msg-drop-overlay {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: none;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font-size: 16px;
  font-weight: 600;
  color: var(--accent);
  background: var(--accent-dim);
  border: 2px dashed var(--accent);
  border-radius: var(--radius-lg);
  pointer-events: none;
}

.msg-drop-overlay.is-active {
  display: flex;
}

/* Live camera capture modal */
.msg-camera-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal, 1000);
  background: rgba(0, 0, 0, 0.82);
  display: flex;
  align-items: center;
  justify-content: center;
}

.msg-camera-panel {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  max-width: 92vw;
}

.msg-camera-video {
  max-width: 92vw;
  max-height: 72vh;
  border-radius: var(--radius-lg);
  background: #000;
}

.msg-camera-close {
  position: absolute;
  top: -14px;
  right: -14px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.msg-camera-controls {
  display: flex;
  justify-content: center;
}

.msg-camera-shutter {
  width: 62px;
  height: 62px;
  border-radius: 50%;
  background: #fff;
  border: 4px solid rgba(255, 255, 255, 0.4);
  cursor: pointer;
}
/* 2026-09-01 mega-audit: transform transition gated on motion preference
   (same pattern as the mention-dropdown entrance). The :active scale still
   applies under reduce — it just snaps. */
@media (prefers-reduced-motion: no-preference) {
  .msg-camera-shutter { transition: transform 0.1s ease; }
}

.msg-camera-shutter:active {
  transform: scale(0.92);
}

.msg-detail-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 24px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
  flex-wrap: wrap;
}

.msg-detail-back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-size: 13px;
  font-weight: 500;
  padding: 6px 10px;
  margin-left: -10px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-family: inherit;
  transition: background 0.12s ease, color 0.12s ease;
}

.msg-detail-back:hover {
  background: var(--surface-2);
  color: var(--text);
}

.msg-detail-back .back-channel {
  color: var(--accent-strong);
  font-weight: 600;
}

.msg-detail-header {
  /* 2026-09-01 mega-audit polish: unify detail-pane gutter to 24px (base;
     the winning polish pass below carries the same 24px horizontal). */
  padding: 18px 24px 14px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}

.msg-detail-title-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: nowrap;
  min-width: 0;
  margin-bottom: 8px;
}

.msg-detail-title {
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.02em; /* 2026-09-01 mega-audit polish: tracking ramp */
  margin: 0;
  line-height: 1.3;
  /* One line + ellipsis so the header stays compact and leaves more room for
     the conversation in the 3-column layout. */
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ─── People dropdown (tagged users) — used in the thread list + detail ───── */
.msg-thread-row-people { margin-top: 6px; }

.msg-people { display: inline-block; }
.msg-people-summary {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  color: var(--text-muted);
  padding: 2px 9px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  user-select: none;
  white-space: nowrap;
}
.msg-people-summary::-webkit-details-marker { display: none; }
.msg-people-summary:hover { color: var(--text); }
.msg-people[open] > .msg-people-summary { color: var(--text); border-color: var(--accent); }
.msg-people-menu {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 6px;
}
.msg-people-item {
  font-size: 12px;
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text);
  white-space: nowrap;
}

.msg-detail-title-edit-btn {
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-muted);
  cursor: pointer;
  padding: 4px 6px;
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}

.msg-detail-title-edit-btn:hover {
  color: var(--text);
  background: var(--surface-2);
  border-color: var(--border);
}

.msg-detail-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 12.5px;
  color: var(--text-muted);
}

.msg-detail-meta .author {
  color: var(--text);
  font-weight: 600;
}

.msg-detail-meta .dot {
  color: var(--text-muted);
}

.msg-detail-meta .translate-status {
  color: var(--accent);
  font-style: italic;
}

.msg-replies-container {
  flex: 1;
  overflow-y: auto;
  padding: 4px 0 24px;
  min-height: 0;
  /* Same reason as .msg-thread-list: hitting the top of the history (which is
     where "load older" lives, so it happens constantly) must not chain the
     scroll into the page and arm pull-to-refresh. */
  overscroll-behavior: contain;
  /* 2026-09-01 mega-audit: JS owns scroll anchoring in this stream (paging,
     fresh-message pinning). With native overflow anchoring also active the two
     double-corrected each other and produced visible jumps. */
  overflow-anchor: none;
}

/* Reply blocks — newspaper-style stacked, no bubbles */
.msg-reply {
  padding: 18px 32px;
  border-bottom: 1px solid var(--border);
  position: relative;
}

.msg-reply:last-child {
  border-bottom: none;
}

/* Consecutive messages from the same author group into one block: the
   continuation sits tight under its predecessor (Cole 2026-07-17).
   2026-09-01 mega-audit: the `:has(+ .msg-reply--cont) { border-bottom: none }`
   rule that lived here was dead — the later bubble restyle re-declares the full
   `border` shorthand at equal specificity and wins by source order — so only
   its :has() matching cost survived. Deleted. */
/* Doubled class to out-specify the later density `.msg-reply { padding: … }`
   overrides so the continuation actually sits tighter under its predecessor. */
.msg-reply.msg-reply--cont {
  padding-top: 3px;
}

.msg-reply-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 12.5px;
  font-weight: 700;
  flex-shrink: 0;
  letter-spacing: 0.02em;
}

.msg-reply-author {
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text);
}

.msg-reply-time {
  font-size: 11.5px;
  color: var(--text-muted);
  margin-left: 2px;
}

.msg-reply-actions {
  margin-left: auto;
  display: flex;
  gap: 2px;
  opacity: 0.58;
  transition: opacity 0.12s ease;
}

.msg-reply:hover .msg-reply-actions,
.msg-reply-actions:focus-within {
  opacity: 1;
}

.msg-reply-actions button {
  background: transparent;
  border: 1px solid transparent;
  cursor: pointer;
  padding: 4px 6px;
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
  color: var(--text-muted);
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}

.msg-reply-actions button:hover {
  background: var(--surface-2);
  border-color: var(--border);
}

.msg-reply-actions button[data-action="msg-recall-reply"]:hover {
  color: var(--red);
  border-color: var(--red-dim);
  background: var(--red-dim);
}

.msg-reply-content {
  font-size: 14.5px;
  color: var(--text);
  line-height: 1.45; /* 2026-09-01 mega-audit polish: one bubble line-height */
  padding-left: 42px;
  word-break: break-word;
  white-space: pre-wrap;
}

/* ── Rich markdown in message/task bodies (tables, images, lists, headings) ──
   Only bodies with block markdown get a .msg-md wrapper; it overrides the
   parent's white-space:pre-wrap so blocks lay out normally. Inline-only bodies
   (bold/links/inline images) stay under pre-wrap and are unaffected. */
.msg-reply-content .msg-md {
  white-space: normal;
}
.msg-md > *:first-child { margin-top: 0; }
.msg-md > *:last-child { margin-bottom: 0; }
.msg-md-p {
  margin: 0 0 8px;
  white-space: normal;
}
.msg-md-h {
  margin: 12px 0 6px;
  font-weight: 700;
  line-height: 1.3;
}
.msg-md-h1 { font-size: 1.15em; }
.msg-md-h2 { font-size: 1.08em; }
.msg-md-h3 { font-size: 1.02em; }
.msg-md-h4 { font-size: 1em; color: var(--text-secondary); }
.msg-md-list {
  margin: 4px 0 8px;
  padding-left: 22px;
}
.msg-md-list li { margin: 2px 0; }
.msg-md-quote {
  margin: 6px 0 8px;
  padding: 4px 12px;
  border-left: 3px solid var(--border-hover);
  color: var(--text-secondary);
}
.msg-md-hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 12px 0;
}
.msg-md-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.88em;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 1px 5px;
}
.msg-md-tablewrap {
  overflow-x: auto;
  margin: 8px 0;
  -webkit-overflow-scrolling: touch;
}
.msg-md-table {
  border-collapse: collapse;
  font-size: 13px;
  width: auto;
  max-width: 100%;
}
.msg-md-table th,
.msg-md-table td {
  border: 1px solid var(--border);
  padding: 5px 10px;
  text-align: left;
  vertical-align: top;
  white-space: nowrap;
}
.msg-md-table th {
  background: var(--surface-2);
  font-weight: 600;
  color: var(--text);
}
.msg-md-table tbody tr:nth-child(even) td {
  background: color-mix(in srgb, var(--surface-2) 45%, transparent);
}
.msg-md-img {
  /* 2026-09-01 mega-audit: markdown images had no reserved box, so every
     decode reflowed the stream. Fixed 320px-wide 4:3 box reserved before
     decode; object-fit letterboxes the photo inside it. The old max-height
     was dropped — with the aspect-ratio box the height is 240px by
     construction, and keeping both invited a conflicting cap. */
  width: 320px;
  max-width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: contain;
  height: auto;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  display: block;
  margin: 6px 0;
  cursor: zoom-in;
}

.msg-reply-own .msg-reply-avatar {
  background: var(--accent) !important;
}

/* Jarvis reads PURPLE everywhere — the avatar, the sender name, the mention
   picker row, and the bubble. One colour, so "this was written by the
   assistant, not a person" is answered before you read a word.
   (Cole 2026-08-03: "is the jarvis chat bubble a diff color from everyone
   elses? lets do that so ppl instantly know its jarvis.")

   This rule USED to paint a 4%-opacity brown wash here and did nothing at all:
   the chat layout's `.msg-replies-container .msg-reply` (two classes) outranks
   a bare `.msg-reply-jarvis` (one), so the bubble background always won. Kept
   for the flat/legacy list rendering; the bubble variant is below, at matching
   specificity. */
.msg-reply-jarvis {
  background: var(--purple-dim);
}

.msg-reply-jarvis .msg-reply-author::after {
  content: 'AI';
  margin-left: 6px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  background: var(--purple);
  color: #fff;
  padding: 1px 5px;
  border-radius: 3px;
  vertical-align: middle;
}

.msg-reply-deleted .msg-reply-content {
  color: var(--text-muted);
  font-style: italic;
}

/* Optimistic send states — local echo before/if the server confirms */
.msg-reply-pending {
  opacity: 1;
}
.msg-reply-pending .msg-reply-content,
.msg-reply-pending .msg-reply-author,
.msg-reply-pending .msg-reply-time { opacity: 0.72; }
.msg-reply-pending .msg-reply-attachments,
.msg-reply-pending .msg-attach-video { opacity: 1; filter: none; }

.msg-reply-failed .msg-reply-content {
  opacity: 0.8;
}

.msg-reply-status {
  padding-left: 42px;
  margin-top: 4px;
  font-size: 11px;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  gap: 4px;
}

.msg-reply-sending {
  display: inline-flex;
  align-items: center;
  opacity: 0.7;
}

.msg-send-spinner {
  width: 10px;
  height: 10px;
  border: 1.5px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: msgSendSpin 0.7s linear infinite;
}

@keyframes msgSendSpin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .msg-send-spinner {
    animation: none;
    border-right-color: currentColor;
    opacity: 0.55;
  }
}

.msg-reply-retry {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--red-dim);
  color: var(--red);
  border: 1px solid var(--red-dim);
  border-radius: 10px;
  padding: 2px 8px;
  font-size: 11px;
  font-weight: 600;
  cursor: pointer;
  transition: opacity 0.12s ease;
}

.msg-reply-retry:hover {
  opacity: 0.8;
}

/* Read-receipt checkmarks — own outgoing messages only (WhatsApp-style) */
.msg-receipt {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  cursor: default;
}

/* WhatsApp-style read receipt: blue double-check once everyone in the
   conversation has seen the message. Grey (single or double) until then. */
.msg-receipt-seen {
  color: var(--blue);
}

.msg-receipt-sent {
  color: var(--text-muted);
}

.msg-mention {
  background: var(--accent-dim);
  color: var(--accent);
  padding: 0 4px;
  border-radius: 3px;
  font-weight: 600;
}

.msg-translated-badge {
  display: inline-block;
  margin-top: 8px;
  /* 2026-09-03 mega-audit: this was a click-only <div> with cursor:pointer, so
     the zh-paired team could not reach "Show original" from the keyboard at
     all. It is a <button> now; inherit the type it used to draw with instead
     of falling back to the UA button font. */
  font-family: inherit;
  font-size: 11px;
  color: var(--accent);
  cursor: pointer;
  font-style: italic;
  padding: 2px 8px;
  border: 1px solid var(--accent-dim);
  border-radius: 10px;
  background: var(--accent-dim);
  transition: opacity 0.12s ease;
}

.msg-translated-badge:hover {
  opacity: 0.75;
}

.msg-translated-badge:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.msg-typing-indicator {
  /* 2026-09-01 mega-audit: JS toggles this element's inline display, and as an
     in-flow sibling of the stream every appearance/disappearance resized the
     scroller and shifted the conversation. No parent slot exists to reserve,
     so it is taken out of flow instead: absolutely anchored just above the
     composer (same --msg-composer-h contract as .msg-jump-latest), where the
     display toggle can no longer move anything. It paints on the paper surface,
     so it carries the paper colour as its own backdrop. */
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--msg-composer-h, 80px);
  z-index: 3;
  pointer-events: none;
  background: var(--msg-paper, var(--bg));
  padding: 4px 16px; /* 2026-09-01 mega-audit polish: align with bubble column */
  font-size: 12px;
  color: var(--text-muted);
  font-style: italic;
}

/* Reply composer — pinned at bottom */
.msg-reply-composer {
  /* 2026-09-01 mega-audit polish: the base padding declared here was dead —
     fully overridden at every width by the later polish pass
     (`padding: 12px max(24px, …) 16px`), which is where the safe-area work
     lives. Deleted rather than maintained as a decoy. */
  border-top: 1px solid var(--border);
  background: var(--bg);
  position: sticky;
  bottom: 0;
}

/* Composer markdown formatting toolbar (reply, split-pane & new-thread) */
.msg-format-bar {
  display: flex;
  align-items: center;
  gap: 2px;
  flex-wrap: wrap;
  margin-bottom: 6px;
}
.msg-fmt-btn {
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  cursor: pointer;
  font-size: 12px;
  line-height: 1;
  padding: 4px 7px;
  min-width: 26px;
  transition: background 0.1s ease, border-color 0.1s ease, color 0.1s ease;
}
.msg-fmt-btn:hover {
  background: var(--surface-2);
  border-color: var(--border);
  color: var(--text);
}
.msg-fmt-btn-b { font-weight: 700; }
.msg-fmt-btn-i { font-style: italic; }

/* The markdown button row is collapsed behind "Aa" on EVERY width now, not just
   ≤768px. A chat composer that leads with seven formatting buttons reads like a
   document editor; the row is one tap away and most messages never format
   anything. (mobile.css keeps its own sizing overrides for the same elements.) */
.msg-format-bar .msg-fmt-btn { display: none; }
.msg-format-bar.is-open .msg-fmt-btn { display: inline-flex; align-items: center; justify-content: center; }

.msg-fmt-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  cursor: pointer;
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
  padding: 4px 8px;
}
.msg-fmt-toggle[aria-expanded="true"] {
  background: var(--surface-2);
  color: var(--text);
  border-color: var(--accent-dim, var(--border));
}

.msg-reply-composer-inner {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
  box-shadow: var(--shadow-xs);
  /* Stable two-part editor: the writing area always owns the full width, while
     formatting/media/send controls share a predictable toolbar below it. */
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 6px 8px;
}

.msg-reply-composer-inner:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-dim);
}

.msg-attach-btn {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px; /* 2026-09-01 mega-audit polish: 34x34 like its siblings */
  height: 34px;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  color: var(--text-muted);
  transition: background 0.12s ease, color 0.12s ease;
}
.msg-attach-btn:hover { background: var(--surface-2); color: var(--text); }

.msg-composer-actions {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
  min-height: 34px; /* 2026-09-01 mega-audit polish: match 34px controls */
}

.msg-composer-media-actions {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
}

.msg-reply-composer .msg-format-bar {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
}

/* Reply-composer secondary actions: closed by default behind one stable +.
   The popover is part of the composer rather than a free-floating toolbar, so
   it cannot be left over a different chat or become detached from its input. */
/* 2026-08-14 — deliberately NOT the popover's containing block. It is 34px
   wide, so anchoring an absolutely-positioned popover to it means the popover's
   only horizontal bound is the viewport, and it grows leftward across whatever
   sits beside the composer. `.msg-reply-composer-inner` is already
   `position: relative` and spans the pane, so it becomes the containing block
   instead and a percentage max-width finally means "fits the composer".
   Mobile pins the popover with `position: fixed` and re-declares
   `.msg-composer-tools { position: relative }`, so it is untouched by this. */
.msg-composer-tools { position: static; flex: 0 0 auto; }
.msg-composer-more-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm); /* 2026-09-01 mega-audit polish */
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: 24px;
  font-weight: 400;
  line-height: 1;
  cursor: pointer;
}
.msg-composer-more-toggle:hover,
.msg-composer-more-toggle[aria-expanded="true"] { background: var(--surface-2); color: var(--text); }
.msg-composer-tools-popover[hidden] { display: none !important; }
/* 2026-08-14 — THE POPOVER IS BOUNDED BY ITS PANE, NOT BY THE VIEWPORT.
   Reported as "can't upload anything, just showing emojis": the + popover
   showed only the trailing 😊 and 🎤, with the format bar, 📎 and 📷 gone.
   Nothing was hidden — `display` computed to `flex` on all eleven controls.
   The popover is `position:absolute; right:0`, so it grows LEFTWARD from the
   + button, and as one non-wrapping row it measured 441px. The old cap read
   `min(560px, 100vw - 28px)`, which asks the VIEWPORT how much room there is;
   the real constraint is the conversation pane, narrower than the viewport by
   the whole thread-list column. Measured in a harness at 1440px: the popover
   laid out at x = -267, i.e. 267px past its own left edge, which is precisely
   the strip the thread list covers.
   A wider max-width would only move the cliff. Instead both children take a
   full row and the box wraps — the same two-row shape css/mobile.css already
   gives this popover — so its width is bounded by the cap rather than by the
   sum of its controls, and every control stays inside the pane at any width. */
.msg-composer-tools-popover {
  position: absolute;
  right: 6px;
  bottom: calc(100% + 8px);
  z-index: 12;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  width: max-content;
  /* Percentage now resolves against the composer's padding box, so the cap is
     the pane's real width rather than the viewport's. `100vw` stays as a
     second bound for the narrow-window case. */
  max-width: min(calc(100% - 12px), calc(100vw - 28px));
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow-md);
}
.msg-composer-tools-popover .msg-format-bar {
  display: flex;
  flex: 1 1 100%;
  flex-wrap: wrap;
  margin: 0;
}
.msg-composer-tools-popover .msg-format-bar .msg-fmt-btn { display: inline-flex; align-items: center; justify-content: center; }
/* Its own row now, so the divider that separated it from the format bar has to
   move with it: a border-LEFT on a full-width row draws a stray tick. */
.msg-composer-tools-popover .msg-composer-media-actions {
  display: flex;
  align-items: center;
  flex: 1 1 100%;
  justify-content: flex-end;
  border-left: 0;
  border-top: 1px solid var(--border);
  padding: 4px 0 0;
}

.msg-reply-composer textarea {
  flex: 1;
  min-width: 0;
  resize: none;
  padding: 7px 8px; /* 2026-09-01 mega-audit polish */
  font-size: 14.5px;
  font-family: inherit;
  border: none;
  background: transparent;
  outline: none;
  color: var(--text);
  line-height: 1.5;
  min-height: 34px;
  max-height: 220px;
}

.msg-reply-composer-inner > textarea {
  width: 100%;
  flex: 0 1 auto;
  box-sizing: border-box;
}

.msg-reply-composer textarea::placeholder {
  color: var(--text-muted);
}

.msg-reply-composer .send-btn {
  flex-shrink: 0;
  align-self: center;
  margin: 0;
  font-size: 12.5px;
  padding: 6px 14px;
  background: var(--accent-strong);
  color: var(--surface);
  border: 1px solid var(--accent-strong);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-family: inherit;
  font-weight: 600;
  transition: background 0.12s ease, opacity 0.12s ease;
}

.msg-reply-composer .send-btn:hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
  filter: brightness(0.92);
}

.msg-reply-composer .send-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Round icon send — the chat-app shape. The label moved to aria-label/title, so
   the button still announces itself; only the visible text is gone. */
.msg-reply-composer .send-btn--round {
  width: 34px;
  height: 34px;
  padding: 0;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.msg-reply-composer .send-btn--round svg { display: block; }

/* ── Composer emoji picker ────────────────────────────────────────────────────
   On document.body, positioned above the button, so the composer's own
   overflow/stacking can't clip it. */
.msg-emoji-pop {
  position: fixed;
  z-index: 1000;
  width: 296px;
  max-height: 232px;
  overflow-y: auto;
  padding: 8px;
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 2px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
.msg-emoji-cell {
  border: none;
  background: transparent;
  border-radius: var(--radius-sm);
  font-size: 19px;
  line-height: 1;
  padding: 5px 0;
  cursor: pointer;
}
.msg-emoji-cell:hover,
.msg-emoji-cell:focus-visible { background: var(--surface-2); outline: none; }
.msg-emoji-cell:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
.msg-emoji-btn[aria-expanded="true"] { color: var(--accent); }

/* ── Voice notes ──────────────────────────────────────────────────────────────
   Recording strip sits ABOVE the composer row so the input never shifts under
   the cursor mid-thought, and the live-mic state is unmissable. */
.msg-voice-bar {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 10px;
  margin-bottom: 5px;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--red) 10%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--red) 30%, var(--border));
  font-size: 12.5px;
}
.msg-voice-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--red);
  flex: none;
  animation: msgVoicePulse 1.1s ease-in-out infinite;
}
@keyframes msgVoicePulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
@media (prefers-reduced-motion: reduce) { .msg-voice-dot { animation: none; } }
.msg-voice-time { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--text); }
.msg-voice-hint { color: var(--text-muted); flex: 1; min-width: 0; }
.msg-voice-cancel, .msg-voice-stop {
  flex: none;
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  padding: 4px 10px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.msg-voice-cancel { border: 1px solid var(--border); background: transparent; color: var(--text-muted); }
.msg-voice-cancel:hover { color: var(--text); border-color: var(--border-hover); }
.msg-voice-stop { border: 1px solid var(--accent); background: var(--accent); color: #fff; }
.msg-voice-btn.is-recording { color: var(--red); }

/* Playback in a bubble. Native <audio controls> — keyboard-accessible and wired
   to the OS media session for free. */
.msg-voice-note {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-top: 6px;
  max-width: 100%;
}
.msg-voice-note > svg { flex: none; opacity: 0.7; }
.msg-voice-audio { min-width: 0; flex: 1; height: 34px; max-width: 260px; }
.msg-voice-dur {
  flex: none;
  font-size: 11px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* The assistant launcher is useful elsewhere, but should never sit over an
   active desktop reply field. Mobile hides both global floaters for the whole
   conversation in css/mobile.css. */
body:has(#mainContent .msg-reply-composer:focus-within) .jarvis-assistant-btn {
  opacity: 0 !important;
  visibility: hidden !important;
  pointer-events: none !important;
}

/* Status pills (lists + sometimes detail) */
.msg-status-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 9px;
  font-size: 11px;
  font-weight: 600;
  border-radius: 11px;
  letter-spacing: 0.01em;
}

.msg-status-pill.msg-status-closed {
  background: var(--surface-2);
  color: var(--text-muted);
}

.msg-status-pill::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

/* Title edit inline form */
.msg-title-edit-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.msg-title-edit-input {
  flex: 1;
  min-width: 200px;
  max-width: 540px;
  font-size: 19px;
  font-weight: 700;
  padding: 8px 12px;
}

/* Mention dropdown — stays similar to before but cleaner */
.msg-mention-dropdown {
  position: absolute;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  max-height: 200px;
  overflow-y: auto;
  z-index: 100;
  min-width: 180px;
}

.msg-mention-dropdown.msg-dropdown-down {
  top: 100%;
  left: 0;
  margin-top: 4px;
}

.msg-mention-dropdown.msg-dropdown-up {
  bottom: 100%;
  left: 0;
  margin-bottom: 4px;
}

/* ─── Responsive — collapse rail on narrower screens ──────────────────────── */

@media (max-width: 1024px) {
  .msg-shell {
    grid-template-columns: 200px 1fr;
  }
  /* A member with no team channels still has the company-wide DM picker.
     Keep that single surface full-width instead of reserving an empty rail. */
  .msg-shell.msg-shell--no-rail {
    grid-template-columns: 1fr;
  }
  .msg-channel-header,
  .msg-filter-strip,
  .msg-reply,
  .msg-thread-row,
  .msg-reply-composer,
  .msg-compose-area,
  .msg-detail-header {
    padding-left: 22px;
    padding-right: 22px;
  }
  .msg-reply-content {
    padding-left: 42px;
  }
}

@media (max-width: 768px) {
  /* Mobile is fully handled by css/mobile.css — these rules below help
     when desktop CSS still applies in the in-between range. */
  .msg-shell {
    grid-template-columns: 1fr;
  }
  .msg-rail {
    display: none;
  }
}

/* ─── Dark mode adjustments (auto via existing tokens) ────────────────────── */

[data-theme="dark"] .msg-reply-composer .send-btn:hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}

[data-theme="dark"] .msg-thread-row:hover {
  background: var(--surface-2);
}

/* ─── Compact pass (2026-06-18) ────────────────────────────────────────────
   Smaller, non-huge headers (normal size, still bold); section descriptions
   removed; denser rail + thread rows; longer message preview. Overrides above
   by source order. */

/* Top page header ("Messages" + New Thread) — small + tight, drop the subtitle */
.msg-page-header { padding-bottom: 10px; margin-bottom: 10px; }
.msg-page-header .msg-title { font-size: 16px; }
.msg-page-header .msg-subtitle { display: none; }

/* Channel header (thread-list column) — compact, no description */
.msg-channel-header { padding: 10px 16px; }
.msg-channel-header-name { font-size: 14.5px; }
.msg-channel-header-desc { display: none; }

/* Open-thread header (3rd column) — prominent title */
.msg-detail-header { padding: 12px 18px; }
.msg-detail-title { font-size: 18px; font-weight: 700; letter-spacing: -0.01em; }

/* Channel rail — denser, names tight to the left */
.msg-rail { padding: 12px 0; }
.msg-rail-section-label { padding: 0 14px 6px; font-size: 10px; }
.msg-channel-item { padding: 6px 12px 6px 14px; gap: 6px; }
.msg-channel-name { font-size: 13px; }

/* Thread rows — compact, with a bigger, clearer title headline */
.msg-thread-row { padding: 7px 16px; gap: 8px; } /* 2026-09-01 mega-audit polish: 16px list gutter (winning density pass) */
.msg-thread-row-title { font-weight: 700; line-height: 1.28; -webkit-line-clamp: 1; } /* 2026-09-01 mega-audit polish: font-size now lives on the base rule */
.msg-thread-row-meta { font-size: 11px; }


/* ─── Column 2: 2-line rows by default, expand the preview + reveal people on
       hover ─────────────────────────────────────────────────────────────── */
/* Titles are short headlines now — 2 lines is plenty; the latest-message
   preview + meta line sit below. */
.msg-thread-row-title { -webkit-line-clamp: 1; }
/* 2026-09-01 mega-audit: the hover reveal used to flip the title clamp 1→2 and
   the people row display:none→flex — both grow the hovered row, reflowing every
   row below it under the moving pointer (list jitter). Chosen variant: the
   people strip stays in the DOM but out of flow — an absolutely positioned
   overlay pinned to the row's bottom-right (the row is already
   position:relative), faded in with opacity/visibility only, so no row ever
   changes height. Its background matches the row's hover background so it sits
   ON the row, not over its text illegibly. The hover clamp expansion is
   dropped for the same reason; the full title is shown in column 3 on open.
   The info stays reachable — nothing is removed, only re-anchored. */
.msg-thread-row-people {
  display: flex;
  position: absolute;
  right: 8px;
  bottom: 4px;
  max-width: calc(100% - 16px);
  overflow: hidden;
  margin-top: 0;
  padding: 2px 4px;
  border-radius: 6px;
  background: var(--bg);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.12s ease;
}
.msg-thread-row:hover .msg-thread-row-people {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}

/* ─── Column 3: tighter message spacing ───────────────────────────────────── */
.msg-replies-container { padding: 2px 0 14px; }
.msg-reply { padding: 9px 18px; }
.msg-reply-avatar { width: 28px; height: 28px; }
.msg-reply-content { line-height: 1.45; padding-left: 36px; font-size: 14px; } /* 2026-09-01 mega-audit polish */

/* ─── Col 2 filter strip: "All people" dropdown sits on the status line, right-
       aligned + small; search drops to its own full-width line ────────────── */
.msg-filter-strip { padding: 10px 24px; gap: 8px; row-gap: 8px; } /* 2026-09-01 mega-audit polish: 24px detail gutter (winning density pass) */
.msg-filter-tagged-select {
  min-width: 0;
  max-width: 124px;
  font-size: 11.5px;
  padding: 3px 8px;
}
/* Push search onto its own row, full width, under the status + dropdown line. */
.msg-filter-search-wrap { flex-basis: 100%; }
.msg-filter-search-wrap .msg-filter-search-input { width: 100%; }

/* ─── Col 3: ~20% tighter messages ────────────────────────────────────────── */
.msg-replies-container { padding: 2px 0 10px; }
.msg-reply { padding: 7px 18px; }
.msg-reply-avatar { width: 24px; height: 24px; }
.msg-reply-content { line-height: 1.45; padding-left: 31px; font-size: 13.5px; } /* 2026-09-01 mega-audit polish */

/* ─── Image lightbox (attached pictures open here, not a browser tab) ──────── */
.msg-img-lightbox {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: rgba(0, 0, 0, 0.82);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  /* Edge tokens, not a flat 40px: this is `position: fixed; inset: 0`, so on a
     phone its bottom row — now the paging strip — sat in the home-indicator
     band and its sides ran into the display's corner arc. */
  padding: max(40px, var(--sa-t, 0px)) var(--edge-r, 40px)
           max(24px, var(--edge-b, 40px)) var(--edge-l, 40px);
  cursor: zoom-out;
}
.msg-img-lightbox-img {
  /* `flex: 0 1 auto` + `min-height: 0` so the photo SHRINKS to make room for the
     caption and the paging strip below it. At a flat 84vh the strip was pushed
     outside the overlay's padding box and off the bottom of the screen —
     measured with the nav shown, not assumed. */
  flex: 0 1 auto;
  min-height: 0;
  max-width: 92vw;
  max-height: 84vh;
  object-fit: contain;
  border-radius: 8px;
  box-shadow: 0 10px 50px rgba(0, 0, 0, 0.6);
  cursor: default;
}
.msg-img-lightbox-close {
  position: absolute;
  top: 16px;
  right: 20px;
  width: 44px;
  height: 44px;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.msg-img-lightbox-close:hover { background: rgba(255, 255, 255, 0.28); }
/* Reply to THIS photo (mig 1242). Same glass as the close button, top-left so
   it never collides with close/paging; 44px tall for the thumb. */
.msg-img-lightbox-reply {
  position: absolute;
  top: 16px;
  left: 20px;
  height: 44px;
  padding: 0 16px 0 12px;
  border: none;
  border-radius: 22px;
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.msg-img-lightbox-reply:hover { background: rgba(255, 255, 255, 0.28); }
.msg-img-lightbox-reply:active { scale: 0.96; }
.msg-img-lightbox-reply:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .msg-img-lightbox-reply:active { scale: 1; } }
.msg-img-lightbox-close:focus-visible,
[data-action="msg-open-image"][role="button"]:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 3px;
}
.msg-img-lightbox-cap { color: #fff; font-size: 13px; max-width: 92vw; text-align: center; opacity: 0.9; }

/* Paging the shared-media set (2026-08-27). Opening the 12th photo and wanting
   the 13th used to mean closing the viewer and re-aiming at a 71px tile — every
   reference messenger pages with arrow keys. Hidden entirely for a single image
   (a bubble thumbnail or the thread-info panel), so those keep the plain viewer
   they always had.

   The strip sits at the BOTTOM rather than as two edge-anchored chevrons: the
   photo is pinch-zoomable, and edge chevrons over a zoomed image swallow the
   pan gesture exactly where you want to drag. Bottom-centre also keeps both
   controls and the counter inside one thumb arc on a phone. */
.msg-img-lightbox-nav {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.45);
}
.msg-img-lightbox-nav[hidden] { display: none; }
.msg-img-lightbox-prev,
.msg-img-lightbox-next {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.14);
  color: #fff;
  cursor: pointer;
}
.msg-img-lightbox-prev:hover:not(:disabled),
.msg-img-lightbox-next:hover:not(:disabled) { background: rgba(255, 255, 255, 0.28); }
/* Disabled rather than hidden at the ends: a control that vanishes moves the
   one beside it, and the counter already says which end you are at.
   NO opacity/cursor rule here on purpose — css/modern-theme.css declares
   `button:disabled { cursor: not-allowed !important; opacity: 0.45 !important }`
   globally, so any value written here is dead text no matter how it is scoped.
   Measured in the browser (computed 0.45, not the 0.32 first written here);
   the global reads correctly as disabled, so this defers to it. */
.msg-img-lightbox-prev:focus-visible,
.msg-img-lightbox-next:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.msg-img-lightbox-count {
  min-width: 56px;
  text-align: center;
  color: #fff;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  opacity: 0.9;
}

/* ─── Col-2 filter: make sure the status toggle + 'All people' fit one line ── */
.msg-status-toggle .toggle-btn { padding: 3px 9px; font-size: 11.5px; }
.msg-filter-tagged-select { max-width: 112px; }

/* ═══ Task Requests: status pills only ════════════════════════════════════════
   Deliberately NO row background tint, NO priority stripe, NO due-date color —
   those read as confusing noise. Priority is conveyed by sort order (high first);
   status keeps a labelled pill; the due date is a neutral chip. */
.msg-thread-row-taskpills { display: flex; flex-wrap: wrap; gap: 5px; margin: 3px 0 1px; }
.msg-tpill {
  display: inline-flex; align-items: center;
  font-size: 10px; font-weight: 700; letter-spacing: 0.04em; /* 2026-09-01 mega-audit polish */
  padding: 1px 8px; border-radius: 999px; white-space: nowrap;
}
.msg-tpill.msg-tstat-new               { background: var(--blue-dim);   color: var(--blue); }
.msg-tpill.msg-tstat-in_progress       { background: var(--yellow-dim); color: var(--yellow-strong); }
.msg-tpill.msg-tstat-awaiting_response { background: var(--orange-dim); color: var(--orange-strong); }
.msg-tpill.msg-tstat-resolved          { background: var(--green-dim);  color: var(--green-strong); }
.msg-tpill.msg-tstat-cancelled         { background: var(--orange-dim); color: var(--orange-strong); }
/* Urgency pill IS colored (High=Red, Medium=Yellow, Low=Blue) — only the small
   pill, never the whole row. */
.msg-tpill.msg-tprio-high   { background: var(--red-dim);    color: var(--msg-red-strong); }
/* Urgent ask (mig 957) — solid, not the dim tint the priority pills use. These
   share the Customer Tasks board with ordinary work and are pinned to the top of
   it, so the chip has to carry the reason at a glance. */
.msg-tpill.msg-turgent {
  background: var(--red);
  color: #fff;
  font-weight: 700;
}
.msg-thread-row.msg-task-row--urgent {
  border-left: 3px solid var(--red);
  background: color-mix(in srgb, var(--red) 5%, transparent);
}
.msg-tpill.msg-tprio-medium { background: var(--yellow-dim); color: var(--yellow-strong); }
.msg-tpill.msg-tprio-low    { background: var(--blue-dim);   color: var(--blue); }

.msg-compose-urgency-row { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.msg-compose-urgency-select { max-width: 150px; font-size: 13px; padding: 5px 8px; border-radius: var(--radius-sm); }

.msg-task-details {
  margin-top: 10px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--bg);
  overflow: hidden;
}
.msg-task-details[open] {
  border-color: color-mix(in srgb, var(--accent) 42%, var(--border));
  background: var(--surface);
}
.msg-task-details-summary {
  min-height: 44px;
  padding: 8px 10px;
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.msg-task-details-summary::-webkit-details-marker { display: none; }
.msg-task-details-summary:hover { background: var(--surface-2); }
.msg-task-details-summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.msg-task-details-heading {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--text);
  font-size: 12px;
  font-weight: 700;
}
.msg-task-summary-pills {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 5px;
  overflow: hidden;
}
.msg-task-summary-owner,
.msg-task-summary-due {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-secondary);
  font-size: 11.5px;
}
.msg-task-summary-due {
  flex: 0 0 auto;
  color: var(--text-muted);
}
.msg-task-details-chevron {
  flex: 0 0 auto;
  display: inline-flex;
  color: var(--text-muted);
}
/* 2026-09-01 mega-audit: the chevron flip transition is motion-gated; under
   reduce it rotates instantly (the [open] transform itself is state, kept). */
@media (prefers-reduced-motion: no-preference) {
  .msg-task-details-chevron { transition: transform 0.15s ease; }
}
.msg-task-details[open] .msg-task-details-chevron { transform: rotate(180deg); }

.msg-detail-task {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(135px, 1fr));
  gap: 10px;
  margin: 0;
  padding: 10px;
  border-top: 1px solid var(--border);
}
.msg-detail-task-field {
  display: flex; flex-direction: column; gap: 3px;
  min-width: 0;
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.02em; color: var(--text-muted);
}
.msg-task-select {
  width: 100%;
  min-height: 38px;
  font-size: 12.5px;
  font-weight: 600;
  padding: 7px 30px 7px 10px;
  border-radius: 9px;
  border: 1px solid var(--border);
}
.msg-task-select.msg-tstat-new               { background: var(--blue-dim);   color: var(--blue); }
.msg-task-select.msg-tstat-in_progress       { background: var(--yellow-dim); color: var(--yellow-strong); }
.msg-task-select.msg-tstat-awaiting_response { background: var(--orange-dim); color: var(--orange-strong); }
.msg-task-select.msg-tstat-resolved          { background: var(--green-dim);  color: var(--green-strong); }
.msg-task-select.msg-tstat-cancelled         { background: var(--orange-dim); color: var(--orange-strong); }
.msg-task-select.msg-tprio-high   { background: var(--red-dim);    color: var(--msg-red-strong); }
.msg-task-select.msg-tprio-medium { background: var(--yellow-dim); color: var(--yellow-strong); }
.msg-task-select.msg-tprio-low    { background: var(--blue-dim);   color: var(--blue); }

/* 'blocked' status (migration 593 — merged Tasks & Requests tab) */
.msg-tpill.msg-tstat-blocked      { background: var(--red-dim); color: var(--msg-red-strong); }
.msg-task-select.msg-tstat-blocked{ background: var(--red-dim); color: var(--msg-red-strong); }

/* Assignee chip + due-date pill on task thread rows (migration 593) */
.msg-task-assignee { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: var(--text-secondary); }
.msg-task-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border-radius: 50%; background: var(--accent, #c8a97e);
  color: #fff; font-size: 10px; font-weight: 700; line-height: 1;
}
/* Unassigned task: muted chip + dashed placeholder avatar. */
.msg-task-assignee--none { color: var(--text-muted); font-style: italic; }
.msg-task-avatar--none { background: transparent; color: var(--text-muted); border: 1px dashed var(--border); }
/* Assignee filter dropdown (task channels) — match the tagged-people select. */
.msg-filter-assignee-wrap { display: inline-flex; }
.msg-filter-assignee-select { font-size: 12px; max-width: 170px; padding: 6px 10px; border-radius: var(--radius-sm); }
/* Due-date chip is NEUTRAL — no due-based color/stripe on the preview row. */
.msg-tpill.msg-due-pill { background: var(--surface-2, rgba(255,255,255,0.06)); color: var(--text-secondary); }
input.msg-task-select.msg-due-overdue { border-color: var(--red); color: var(--red); }
.msg-activity-line strong { color: var(--text-secondary); font-weight: 600; }

.msg-activity-line {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 4px 18px;
  padding: 6px 10px;
  border-left: 2px solid var(--border);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  color: var(--text-muted);
  font-size: 11.5px;
  line-height: 1.35;
}
.msg-activity-line > svg {
  flex: 0 0 auto;
  opacity: 0.7;
}
.msg-activity-line-text {
  flex: 1;
  min-width: 0;
  overflow-wrap: anywhere;
}
.msg-activity-line-detail::before {
  content: " · ";
  color: var(--text-muted);
}
.msg-activity-line-detail { color: var(--text-secondary); }
.msg-activity-line-time {
  flex: 0 0 auto;
  color: var(--text-muted);
  font-size: 10.5px;
  white-space: nowrap;
}

.msg-thread-status-btn {
  min-height: 34px;
  padding: 6px 14px;
  font-size: 12.5px;
}
.msg-thread-status-btn--close { color: var(--green-strong); }
.msg-thread-status-btn--reopen { color: var(--msg-blue-strong); }

.msg-customer-link-btn {
  min-height: 32px;
  padding: 4px 6px;
  border: 0;
  background: none;
  color: var(--accent-strong);
  cursor: pointer;
  font: 600 12px/1.3 inherit;
}
.msg-customer-link-btn:hover { text-decoration: underline; }
.msg-customer-link-btn--change {
  margin-left: auto;
  color: var(--text-muted);
  font-size: 11px;
}

.msg-task-context {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  padding-top: 8px;
}
.msg-task-supporting:has(.msg-task-context) {
  display: grid;
  grid-template-columns: minmax(260px, 0.7fr) minmax(380px, 1.3fr);
  gap: 8px;
  align-items: start;
}
.msg-task-context-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  min-height: 28px;
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font-size: 11.5px;
}
.msg-task-context-label {
  color: var(--text-muted);
  font-size: 10px;
  font-weight: 600;
}

/* Forwarded Inbox link ("context_link" attachment) — one thin line, opens the
   source thread in a side panel on click (see _msgOpenInboxContextPanel). */
.msg-context-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 5px 10px;
  border: 1px solid var(--accent);
  border-radius: 8px;
  background: var(--accent-dim);
  color: var(--text);
  font-size: 12px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.msg-context-chip:hover {
  opacity: 0.85;
}
.msg-context-chip-icon {
  font-size: 13px;
  line-height: 1;
  flex-shrink: 0;
}
.msg-context-chip-label {
  overflow: hidden;
  text-overflow: ellipsis;
}
.msg-context-chip-arrow {
  flex-shrink: 0;
  font-weight: 400;
  color: var(--text-secondary);
}

.msg-customer-link {
  margin-top: 8px;
}
.msg-customer-card,
.msg-customer-link--empty {
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg);
}
.msg-customer-card {
  padding: 9px 10px;
}
.msg-customer-card-head {
  display: flex;
  align-items: center;
  gap: 8px;
}
.msg-customer-card-icon {
  width: 28px;
  height: 28px;
  flex: 0 0 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  background: var(--accent-dim);
  color: var(--accent-strong);
}
.msg-customer-card-person {
  min-width: 0;
  display: flex;
  flex-direction: column;
  line-height: 1.2;
}
.msg-customer-card-person small {
  color: var(--text-muted);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.msg-customer-card-person strong {
  color: var(--text);
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.msg-customer-link--empty .msg-customer-link-btn {
  width: 100%;
  min-height: 48px;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 9px;
  text-align: left;
}
.msg-customer-link--empty .msg-customer-link-btn > span:nth-child(2) {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.msg-customer-link--empty .msg-customer-link-btn small {
  color: var(--text-muted);
  font-size: 10.5px;
  font-weight: 500;
}
.msg-customer-inbox {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  color: var(--text-secondary);
  font-size: 11.5px;
}
.msg-customer-inbox-row {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
.msg-customer-inbox-status {
  display: inline-flex;
  align-items: center;
  min-height: 23px;
  padding: 3px 7px;
  border-radius: 999px;
  font-weight: 700;
}
.msg-customer-inbox-status.is-us {
  background: var(--red-dim);
  color: var(--msg-red-strong);
}
.msg-customer-inbox-status.is-customer {
  background: var(--green-dim);
  color: var(--green-strong);
}
.msg-customer-inbox-status.is-neutral {
  background: var(--surface-2);
  color: var(--text-secondary);
}
.msg-customer-inbox-when {
  color: var(--text-muted);
  font-size: 10.5px;
}
.msg-customer-inbox-open {
  margin-left: auto;
  min-height: 30px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--accent-strong);
  cursor: pointer;
  font: 700 11px/1.2 inherit;
}
.msg-customer-inbox-open:hover {
  background: var(--accent-dim);
}
.msg-customer-inbox-summary {
  margin: 6px 0 0;
  color: var(--text-secondary);
  font-size: 11.5px;
  line-height: 1.4;
}

@media (prefers-reduced-motion: reduce) {
  .msg-task-details-chevron { transition: none; }
}

/* ─── Thread Info side panel (info button in the thread topbar) ────────── */
.msg-info-media-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.msg-info-media-thumb {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  cursor: zoom-in;
}

.msg-info-file-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 4px;
  border-bottom: 1px solid var(--border);
  text-decoration: none;
  color: var(--text);
}

.msg-info-file-row:last-child {
  border-bottom: none;
}

.msg-info-file-glyph {
  font-size: 20px;
  line-height: 1;
  flex-shrink: 0;
}

.msg-info-file-meta {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.msg-info-file-name {
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.msg-info-file-sub {
  font-size: 11.5px;
  color: var(--text-muted);
}

.msg-ask-ai-row {
  display: flex;
  gap: 8px;
}

.msg-ask-ai-row .form-input {
  flex: 1;
}

.msg-ask-ai-answer {
  margin-top: 10px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--border);
  font-size: 13px;
  line-height: 1.55;
  white-space: pre-wrap;
}

.msg-ask-ai-answer.is-loading {
  color: var(--text-muted);
  font-style: italic;
}

.msg-ask-ai-answer.is-error {
  color: var(--red);
  background: var(--red-dim);
  border-color: var(--red-dim);
}

/* Inline plain-text URL links + order-number refs (#1234) in message content */
.msg-link {
  color: var(--accent);
  word-break: break-all;
}

.msg-order-link {
  color: var(--accent);
  font-weight: 600;
  cursor: pointer;
  border-bottom: 1px dotted var(--accent);
}

.msg-order-link:hover {
  opacity: 0.75;
}

/* SKU references in message bodies (2026-08-17). Cole: "the SKUs should
   always link on desktop and mobile both to show us." Same markup on both
   surfaces (js/utils.js imSkuLinkHtml), so this one rule covers the desktop
   view; the dock re-declares it in its own inline CSS because it is a
   self-contained bundle with no external stylesheet.
   Monospace because a SKU is a code, not a word — and it is what makes
   "C-42-DIA" scannable in a wall of prose. Deliberately quieter than
   .msg-order-link: SKUs appear far more often than order refs, and a bold
   accent-coloured run every other line reads as noise. */
.msg-sku-link {
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 0.94em;
  color: var(--accent);
  cursor: pointer;
  border-bottom: 1px dotted color-mix(in srgb, var(--accent) 55%, transparent);
}

.msg-sku-link:hover,
.msg-sku-link:focus-visible {
  opacity: 0.75;
  border-bottom-style: solid;
}

/* Links section rows in the Thread Info panel */
.msg-info-link-row {
  display: block;
  padding: 8px 4px;
  border-bottom: 1px solid var(--border);
  text-decoration: none;
}

.msg-info-link-row:last-child {
  border-bottom: none;
}

.msg-info-link-url {
  display: block;
  font-size: 12.5px;
  color: var(--accent);
  word-break: break-all;
}

/* Order detail side panel (opened from a #1234 ref in a message) */
.msg-order-summary {
  font-size: 13.5px;
  margin-bottom: 12px;
}

.msg-order-totals {
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-size: 13px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  margin-bottom: 12px;
}

.msg-order-shipping {
  margin-bottom: 12px;
}

.msg-order-line {
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}

.msg-order-line:last-of-type {
  border-bottom: none;
}

.msg-order-line-main {
  display: flex;
  gap: 8px;
  font-size: 13px;
  margin-bottom: 2px;
}

.msg-order-product-link {
  appearance: none;
  align-items: baseline;
  background: transparent;
  border: 0;
  color: var(--text);
  cursor: pointer;
  display: inline-flex;
  flex-wrap: wrap;
  gap: 8px;
  min-width: 0;
  padding: 0;
  text-align: left;
}

.msg-order-product-link:hover .msg-order-product-title,
.msg-order-product-link:focus-visible .msg-order-product-title {
  color: var(--accent);
  text-decoration: underline;
}

.msg-order-product-link:focus-visible {
  border-radius: var(--radius-sm);
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.msg-order-product-sku {
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
}

/* SKU drawer. Identity leads; the picture supports it.
   Cole 2026-08-20: "make the image a little smaller on desktop, no need for it
   to be so big. SKU and barcode could be bigger." The hero used to be a
   full-width 4:3 block — ~440px of mirror above the two strings anyone actually
   came for. It is now a fixed thumb beside the title, and the SKU/barcode are
   the largest type in the panel. */
.msg-sku-head { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 10px; }
.msg-sku-panel-hero {
  flex: 0 0 76px;
  width: 76px;
  aspect-ratio: 1 / 1;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface-2);
}
.msg-sku-panel-hero img { width: 100%; height: 100%; object-fit: contain; display: block; }
.msg-sku-headtext { min-width: 0; padding-top: 2px; }
.msg-sku-panel-title { font-size: 15px; font-weight: 650; line-height: 1.25; }
.msg-sku-panel-sub { font-size: 12px; color: var(--text-muted); margin-top: 3px; }

/* The two strings people read aloud, type into Shopify, and scan. */
.msg-sku-identwrap {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  padding: 8px 10px;
  margin-bottom: 10px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2);
}
.msg-sku-ident { min-width: 0; flex: 1 1 auto; }
.msg-sku-ident-k {
  display: block;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.msg-sku-ident-v {
  display: block;
  font-family: var(--font-mono, monospace);
  font-size: 17px;
  line-height: 1.2;
  font-weight: 600;
  margin-top: 2px;
  overflow-wrap: anywhere;
  user-select: all;          /* one click selects the whole code to copy */
}
/* Every fact uses the identity card's shape, one size down: label above
   value. auto-fit means the same markup is a 2-3 column grid in the 440px
   desktop drawer and a clean 2-up on a 375px phone, with no breakpoint of its
   own — the panel goes full-width on mobile (css/mobile.css Fix 2), so a fixed
   column count would have stretched or squeezed at every size between. */
.msg-sku-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
  gap: 5px;
}
.msg-sku-cell {
  min-width: 0;
  padding: 4px 7px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface-2);
}
.msg-sku-cell.is-wide { grid-column: 1 / -1; }
.msg-sku-cell.is-w2 { grid-column: span 2; }
.msg-sku-cell-k {
  display: block;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.msg-sku-cell-v {
  display: block;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.2;
  margin-top: 1px;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

/* Mobile overrides MUST come after the base rules above — same specificity,
   so source order decides. They were written before them and silently lost:
   the panel reported 14.5px cells at 375px wide while the identity card,
   whose base rule sits higher in the file, correctly grew. */
@media (max-width: 640px) {
  /* The panel is full-width here (css/mobile.css Fix 2), so the thumb can
     breathe and the type can grow — a phone is held closer but read faster. */
  .msg-sku-panel-hero { flex-basis: 108px; width: 108px; }
  .msg-sku-ident-v { font-size: 18px; }
  .msg-sku-panel-title { font-size: 17px; }
  .msg-sku-cell-v { font-size: 15.5px; }
  .msg-sku-grid { grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); }
  /* Thumbs are worth more room on a phone than a 96px chip. */
  .msg-sku-head { gap: 14px; }
}

/* Group headings need air ABOVE them, not below — otherwise each one reads as
   a label for the row it is sitting on top of rather than the block it opens.
   :first-of-type keeps the first heading tight to the identity card. */
.msg-sku-panel .sp-section-title { margin-top: 10px; margin-bottom: 6px; }
.msg-sku-panel .sp-section-title:first-of-type { margin-top: 4px; }

.msg-order-panel-footer {
  display: flex;
  gap: 8px;
  margin-top: 12px;
}

/* Forwarded-Inbox conversation side panel (opened from a .msg-context-chip) */
.msg-inbox-ctx-thread {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.msg-inbox-ctx-bubble {
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg);
}
.msg-inbox-ctx-bubble.is-customer {
  border-color: var(--accent);
  background: var(--accent-dim);
}
.msg-inbox-ctx-bubble-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 4px;
  font-size: 12.5px;
}
.msg-inbox-ctx-bubble-body {
  font-size: 12.5px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}
.msg-inbox-ctx-photos {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}
.msg-inbox-ctx-photo {
  width: 84px;
  height: 84px;
  object-fit: cover;
  border-radius: 8px;
  border: 1px solid var(--border);
  cursor: zoom-in;
}
.msg-inbox-ctx-files {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}

/* AI customer overview + mini inbox history (order panel, admin/internal only) */
.msg-order-ai-overview {
  margin-top: 12px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--border);
  font-size: 13px;
  line-height: 1.55;
}

.msg-order-ai-overview.is-loading,
.msg-order-inbox-history.is-loading {
  color: var(--text-muted);
  font-style: italic;
}

.msg-order-ai-overview.is-error,
.msg-order-inbox-history.is-error {
  color: var(--text-muted);
  font-style: italic;
}

.msg-order-ai-overview-narrative {
  white-space: pre-wrap;
}

.msg-order-ai-overview-stats {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 8px;
  font-size: 12px;
  color: var(--text-muted);
}

.msg-order-inbox-history {
  margin-top: 4px;
}

.msg-order-inbox-row {
  display: block;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  text-decoration: none;
  color: inherit;
}

.msg-order-inbox-row:last-child {
  border-bottom: none;
}

.msg-order-inbox-row:hover .msg-order-inbox-subject {
  color: var(--accent);
  text-decoration: underline;
}

.msg-order-inbox-subject {
  font-size: 13px;
  font-weight: 600;
}

.msg-order-inbox-meta {
  font-size: 11.5px;
  color: var(--text-muted);
  margin: 1px 0 2px;
}

.msg-order-inbox-excerpt {
  font-size: 12.5px;
  color: var(--text-muted);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.pill.hold {
  background: var(--red-dim);
  color: var(--red);
}

/* ─── Mobile top-header quick access button ─────────────────────────────────
   Always-reachable "jump to Messages" icon in the fixed mobile header — the
   sidebar (where Messages normally lives) is behind the hamburger on phones.
   Hidden on desktop (sidebar nav item + Team Chat dock already cover it);
   shown + sized in css/mobile.css. */
.header .mobile-messages-btn.icon-btn { display: none !important; }
.header .mobile-messages-btn { position: relative; }
.mobile-messages-badge {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 15px;
  height: 15px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--red, #ef4444);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  line-height: 15px;
  text-align: center;
  box-shadow: 0 0 0 2px var(--surface, #1e1e1e);
}

/* ─── New-message banner (WhatsApp-style pop-up) ────────────────────────────
   Fired by index.html's _imMessageAlert() whenever the unread total rises
   while the tab is focused (see skills/messages-map.md §4b). Desktop: stacks
   bottom-right, above the Team Chat dock launcher pill. Mobile position is
   overridden in css/mobile.css to slide down from the top of the screen. */
/* Updates section in the info panel (Files aside) — scrollable activity list. */
.msg-updates-list { display:flex; flex-direction:column; gap:4px; flex:1; min-height:0; overflow-y:auto; padding-right:2px; }
.msg-updates-list .msg-task-activity-line { width:100%; text-align:left; }

#imMsgBannerWrap {
  position: fixed;
  bottom: 84px;
  right: 16px;
  z-index: var(--z-toast, 9999);
  display: flex;
  flex-direction: column-reverse;
  gap: 8px;
  width: 320px;
  max-width: calc(100vw - 32px);
  pointer-events: none;
}
@keyframes imMsgBannerIn {
  from { opacity: 0; transform: translateY(12px) scale(.98); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

/* Ambient activity bar (js/action-toasts.js) — "someone just did X".
   This was
   a stack of up to three floating cards at bottom:84px right:16px, i.e. right
   on top of the Messages composer and its send button. It is now ONE slim strip
   bounded on desktop and anchored below the live header on mobile, so ambient awareness never lands on the thing
   someone is actually using. Hidden (translated out, pointer-events off) until
   an event arrives; body.im-activity-bar-on marks it visible for any layout
   that wants to reserve the strip. */
#imActivityBar {
  position: fixed;
  top: var(--im-activity-bar-top, 60px);
  bottom: auto;
  left: var(--im-activity-bar-left, 192px);
  right: 0;
  z-index: 9998;
  display: flex;
  align-items: center;
  gap: 9px;
  height: 34px;
  padding: 0 14px;
  /* 2026-09-01 mega-audit: was translucent + backdrop-filter blur(6px). A
     fixed full-width backdrop filter makes the compositor read back the pixels
     behind the bar on every frame of every scroll. Opaque color-mix instead —
     same pattern as .msg-files-jump (see the note there). */
  background: color-mix(in srgb, var(--surface, #1c1c1a) 96%, var(--bg, #141412));
  border-bottom: 1px solid var(--border, #313130);
  box-shadow: 0 4px 14px rgba(22, 19, 15, 0.09);
  transform: none;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.16s ease;
}
#imActivityBar.is-visible {
  transform: translateY(0);
  opacity: 1;
  pointer-events: auto;
}
@media (prefers-reduced-motion: reduce) {
  #imActivityBar { transition: opacity 0.01s linear; transform: none; }
}
/* On a wide screen the strip does not need the whole bottom edge (Cole
   2026-08-06: "these notifications dont need to take up the whole bottom here
   on desktop mode"). A 1600px-wide rule for one short sentence reads as a
   system-level banner rather than the ambient aside it is, and it draws the eye
   across the entire viewport every time anyone does anything.
   Bounded to a corner pill instead: same slot, same behaviour, a fraction of
   the footprint. Phones keep the full-width treatment — there the screen IS the
   pill, and the mobile.css override above still wins on `left`. */
@media (min-width: 1024px) {
  #imActivityBar {
    right: auto;
    max-width: min(560px, 42vw);
    border-right: 1px solid var(--border, #313130);
    border-bottom-right-radius: 10px;
  }
}
/* The whole strip is the click target (it routes to what the event describes),
   so it needs to look like one. */
#imActivityBar.is-visible { cursor: pointer; }
#imActivityBar.is-visible:hover { background: var(--surface-2, #2a2a2a); }
#imActivityBar:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.im-activity-bar-icon { flex: none; font-size: 13px; line-height: 1; }
.im-activity-bar-body {
  flex: 1;
  min-width: 0;
  font-size: 12px;
  line-height: 1.3;
  color: var(--text-muted, #999);
  /* One line, always — the bar must never grow into the page. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.im-activity-bar-who { color: var(--text, #eee); font-weight: 700; }
/* "+2" — the rest of this poll's burst, counted rather than queued. */
.im-activity-bar-more {
  flex: none;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--surface-2, #2a2a2a);
  border: 1px solid color-mix(in srgb, var(--border, #313130) 70%, transparent);
  font-size: 10.5px;
  font-weight: 700;
  color: var(--text-dim, #999);
}
.im-activity-bar-close {
  flex: none;
  border: 0;
  background: transparent;
  color: var(--text-dim, #999);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  padding: 3px 6px;
  border-radius: 6px;
}
.im-activity-bar-close:hover { background: var(--surface-2, #2a2a2a); color: var(--text, #eee); }
.im-activity-bar-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.im-msg-banner {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  background: var(--surface, #1e1e1e);
  border: 1px solid var(--border, #3334);
  border-left: 3px solid var(--accent, #b08d57);
  border-radius: 12px;
  padding: 11px 12px;
  box-shadow: 0 10px 28px rgba(0, 0, 0, .28);
  cursor: pointer;
  pointer-events: all;
  animation: imMsgBannerIn .22s cubic-bezier(.34, 1.56, .64, 1) both;
  transition: opacity .18s ease, transform .18s ease;
}
.im-msg-banner--leaving { opacity: 0; transform: translateY(8px) scale(.97); }
/* 2026-09-01 mega-audit: the entrance uses an overshoot cubic-bezier — exactly
   the springy motion reduced-motion asks us to drop. The banner still appears
   (`both` fill made `animation: none` safe: base state is fully visible). */
@media (prefers-reduced-motion: reduce) {
  .im-msg-banner { animation: none; }
}
.im-msg-banner-avatar {
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--accent, #b08d57);
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}
.im-msg-banner-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.im-msg-banner-title { font-size: 12.5px; font-weight: 700; color: var(--text, #eee); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.im-msg-banner-preview { font-size: 12.5px; color: var(--text-muted, #999); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-height: 1.35; }
.im-msg-banner-close { flex: none; border: 0; background: transparent; color: var(--text-dim, #999); font-size: 16px; line-height: 1; cursor: pointer; padding: 2px 4px; border-radius: 6px; }
.im-msg-banner-close:hover { background: var(--surface-2, #2a2a2a); color: var(--text, #eee); }

/* ═══ WhatsApp-style message bubbles (2026-07-20, Cole) ══════════════════════
   Replaces the flat "newspaper" stack (`.msg-reply` comment above): every
   message is its own rounded box — MY messages accent-tinted + right-aligned,
   everyone else neutral + left-aligned. Selectors are scoped under
   `.msg-replies-container` (2 classes) so they beat the single-class density /
   compact overrides earlier in this file without `!important`; the ≤768px
   overrides live in css/mobile.css. Non-bubble children (activity lines, the
   "No messages yet" empty state) stay full-width separators via align stretch. */
/* PR A (2026-07-28) tightened this: the per-message header row (avatar + full
   name + full date) is gone from the markup — a sender name only, and only on
   the first bubble of an incoming run; time + checkmark now ride the last line
   inside the bubble. Bubbles lost their 1px border for a shadow, which is what
   lets a tail render cleanly. Day separators, the unread rule and the
   jump-to-latest button are further down this file. */
.msg-replies-container {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  padding: 12px 16px 18px;
  /* A single low-contrast wash keeps the surface warm without the visual noise
     of the previous crossed wallpaper pattern. */
  background:
    linear-gradient(var(--msg-weave), var(--msg-weave)),
    var(--msg-paper);
}
:root {
  --msg-weave: rgba(166, 124, 82, 0.055);
  --msg-paper: #efece4;
  --msg-bub-in: #ffffff;
  /* Outgoing bubble = WhatsApp green (Cole 2026-07-30: "I don't like how my
     message is the same color as the background"). The old warm tan sat a few
     percent off the paper wash, so on a busy channel you couldn't tell your own
     messages from everyone else's at a glance. These are WhatsApp's own
     outgoing values, which read correctly against this paper. */
  --msg-bub-out: #d9fdd3;
  --msg-red-strong: #991b1b;
  --msg-blue-strong: #1d4ed8;
  --msg-meta: var(--text-muted);
  --msg-author-1: #1d4ed8;
  --msg-author-2: #b91c1c;
  --msg-author-3: #047857;
  --msg-author-4: #92400e;
  --msg-author-5: #6d28d9;
  --msg-author-6: #9d174d;
  --msg-author-7: #0e7490;
  --msg-author-8: #c2410c;
}
:root[data-theme="dark"] {
  --msg-weave: rgba(192, 154, 106, 0.05);
  --msg-paper: #171715;
  --msg-bub-in: #262624;
  --msg-bub-out: #075e54;
  --msg-red-strong: #fca5a5;
  --msg-blue-strong: #93c5fd;
  --msg-meta: #b8b4ac;
  --msg-author-1: #93c5fd;
  --msg-author-2: #fca5a5;
  --msg-author-3: #6ee7b7;
  --msg-author-4: #fcd34d;
  --msg-author-5: #c4b5fd;
  --msg-author-6: #f9a8d4;
  --msg-author-7: #67e8f9;
  --msg-author-8: #fdba74;
}
:root[data-theme="dark"] .msg-reply-avatar { color: #181816; }

/* Each bubble carries a blurred box-shadow, a clip-path tail pseudo-element and
   (on rows that show one) a shadowed avatar. Without containment a change to any
   one bubble invalidates layout for the whole scroller.
   `layout style`, NOT `content`/`paint`: the tail at :5040 is `left: -6px`, i.e.
   it deliberately overflows the bubble's box, and paint containment would clip
   it away. Layout containment scopes reflow without clipping anything. The
   bubble is already position:relative, so becoming a containing block for its
   own abspos children changes nothing. (2026-08-27) */
.msg-replies-container .msg-reply { contain: layout style; }

/* The scroller is a fixed-height flex column, so every bubble is a flex item
   with the default flex-shrink:1. Normally that is harmless — a flex item's
   automatic min-height is its content height, so it cannot shrink below what
   it holds. Layout containment (above) removes that intrinsic floor: once a
   thread is taller than the box, flexbox shrinks every bubble to the only
   floor left — mobile.css's 44px thumb target — and the text paints over the
   next bubble. Reproduced 2026-09-07 on Cole's iPhone (thread 836: every reply
   44px tall, scrollHeight 88–383px). Bubbles never shrink; the column scrolls. */
.msg-replies-container .msg-reply { flex-shrink: 0; }
/* Same shape for EVERY child of the scroller, not just bubbles: the in-stream
   task-activity lines survived only because their 32px min-height floored the
   shrink; the moment the phone dropped that minimum they collapsed to 4px
   (2026-09-07). Nothing in a scroll column is allowed to shrink — it scrolls. */
.msg-replies-container > * { flex-shrink: 0; }

.msg-replies-container .msg-reply {
  max-width: 76%;
  width: fit-content;
  align-self: flex-start;
  margin: 0;
  padding: 6px 10px 5px;
  border: none;
  border-radius: 3px 12px 12px 12px;
  background: var(--msg-bub-in);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
  position: relative;
}
/* The tail — on the first bubble of a same-author run only, sitting on the
   sharp corner the bubble already has. `background: inherit` keeps it the same
   color as its bubble, including the pinned + own variants. */
.msg-replies-container .msg-reply:not(.msg-reply--cont)::after {
  content: "";
  position: absolute;
  top: 0;
  left: -6px;
  width: 9px;
  height: 9px;
  background: inherit;
  clip-path: polygon(100% 0, 100% 100%, 0 0);
}
/* Same-author continuation: fully rounded, no tail, tucked under its
   predecessor so a run reads as one stack. */
.msg-replies-container .msg-reply.msg-reply--cont {
  border-radius: 12px;
  margin-top: 1px;
  padding-top: 5px;
}
/* 2026-09-01 mega-audit: deleted the `:last-child` / `:has(+ .msg-reply--cont)`
   border-bottom:none block that lived here. Since the later restyle gave every
   bubble a full 1px border it no longer neutralised a newspaper divider — it
   punched an asymmetric hole in a rounded border, and the :has() selector made
   every sibling insertion re-match the run. Bubbles now keep their full border. */

/* MY messages — warm-tinted box, right-aligned, mirrored tail. */
.msg-replies-container .msg-reply-own {
  align-self: flex-end;
  background: var(--msg-bub-out);
  border-radius: 12px 3px 12px 12px;
}
.msg-replies-container .msg-reply-own.msg-reply--cont { border-radius: 12px; }
.msg-replies-container .msg-reply-own:not(.msg-reply--cont)::after {
  left: auto;
  right: -6px;
  background: var(--msg-bub-out);
  clip-path: polygon(0 0, 100% 0, 0 100%);
}
.msg-replies-container .msg-reply-own.msg-reply-pinned:not(.msg-reply--cont)::after {
  /* Byte-identical to the pinned own-bubble background: the tail overlaps the
     bubble, so any difference between the two paints a seam. */
  background: color-mix(in srgb, var(--accent) 13%, var(--msg-bub-out));
}
.msg-replies-container .msg-reply-own .msg-reply-status { justify-content: flex-end; }

/* Pinned bubble keeps its highlight (the JS-injected 1-class rule is
   out-specified by the 2-class bubble background above).

   OPAQUE, mixed onto the bubble's own colour — not `--accent-dim`, which is a
   10%-alpha wash. Two things went wrong with the wash, both measured on a
   375px viewport (2026-08-27): the conversation's paper texture read straight
   through a pinned message, and the tail (`::after`, `right: -6px`, 9px wide)
   overlaps the bubble by 3px, so that strip was painted TWICE with a
   translucent fill and showed as a darker sliver down the bubble's edge — the
   "weird color overlap" on a phone. Compositing the tint into an opaque colour
   fixes both at the source, and follows the rule the Jarvis bubble below
   already states: a pinned X is still an X, so the pin ADJUSTS its colour
   rather than replacing it. */
.msg-replies-container .msg-reply-pinned {
  background: color-mix(in srgb, var(--accent) 13%, var(--msg-bub-in));
}

/* Jarvis's bubble, at the specificity the bubble background actually lives at.
   A ring rather than just a tint: the tint alone has to survive light AND dark
   without competing with the pinned highlight, and the ring reads at a glance
   even when the message is one short line. The tail inherits `background`, so
   it follows automatically. */
.msg-replies-container .msg-reply-jarvis {
  background: color-mix(in srgb, var(--purple) 9%, var(--msg-bub-in));
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.08),
    inset 0 0 0 1px color-mix(in srgb, var(--purple) 30%, transparent);
}
:root[data-theme="dark"] .msg-replies-container .msg-reply-jarvis {
  background: color-mix(in srgb, var(--purple) 17%, var(--msg-bub-in));
}
/* A pinned Jarvis message is still a Jarvis message — keep the identity and let
   the pin show through the ring instead of replacing the colour wholesale. */
.msg-replies-container .msg-reply-jarvis.msg-reply-pinned {
  background: color-mix(in srgb, var(--purple) 9%, var(--msg-bub-in));
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.08),
    inset 0 0 0 2px color-mix(in srgb, var(--accent) 55%, transparent);
}

/* Content sits flush inside the bubble — kill the avatar indent that every
   density / responsive rule adds. */
.msg-replies-container .msg-reply-content,
.msg-replies-container .msg-reply-status { padding-left: 0; }

/* Sender name — incoming, first-of-run only. Color is set inline per author
   (hashed from the user id, same palette as their avatar). */
.msg-replies-container .msg-reply-author {
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.35;
  margin-bottom: 1px;
}

/* Time + checkmark, tucked at the bottom-right inside the bubble.
   Deliberately NOT floated into the last line of text: the content element's
   innerHTML is swapped wholesale by the translate toggle, so the stamp has to
   live outside it, and a float that follows a block sibling can't ride that
   block's last line anyway. A tight negative top margin keeps it hugging the
   text instead of reading as a separate row. */
.msg-replies-container .msg-reply-meta {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 3px;
  margin: -1px -2px -1px 10px;
  font-size: 11px;
  line-height: 1.35;
  color: var(--msg-meta);
}
.msg-replies-container .msg-reply-meta .msg-reply-time {
  font-size: 11px;
  color: inherit;
  margin: 0;
}
.msg-replies-container .msg-reply-meta .msg-reply-status {
  display: inline-flex;
  align-items: center;
  margin: 0;
}
/* Message actions live inside their own bubble, on its BOTTOM row (Cole
   2026-08-06). They hung above the bubble's top-right corner until 2026-07-29,
   which put one control directly over the PREVIOUS message; a contained right
   float fixed that, and this row is the same guarantee taken further — the
   controls are now normal in-flow children of `.msg-reply-meta`, the last box
   inside the bubble, so no positioning trick decides whether they escape it.

   Space is RESERVED and ALWAYS PAINTED (Cole, 2026-08-13: "the three dots
   should always show ... so that we can either do that or hold down on the
   message to work on it"). It used to be opacity-0-until-hover on pointer
   devices, hidden entirely behind a hold gesture; now the row (react · reply ·
   ⋯ · Translate) is visible at rest everywhere, the same as it already was on
   touch. `display: none` is still never used — it resized the bubble under
   the pointer the instant the cluster appeared (measured on the pre-2026-08-06
   build: a short own bubble went 60 → 74px wide and 56.8 → 59.9px tall,
   shoving every message below it down 3px). Opacity is kept only so a future
   state (e.g. a disabled/busy control) can still dim without moving anything. */
.msg-replies-container .msg-reply-meta .msg-reply-actions {
  position: static;
  float: none;
  margin: 0;
  gap: 1px;
  padding: 0;
  border: 0;
  background: transparent;
  box-shadow: none;
  backdrop-filter: none;
  opacity: 1;
  transition: opacity 0.12s ease;
}
.msg-replies-container .msg-reply:hover .msg-reply-actions,
.msg-replies-container .msg-reply:focus-within .msg-reply-actions,
.msg-replies-container .msg-reply.is-menu-open .msg-reply-actions {
  opacity: 1;
}
.msg-replies-container .msg-reply-actions button {
  padding: 2px 4px;
  min-width: 22px;
  min-height: 20px;
  justify-content: center;
}
/* The add-reaction control is a member of the cluster now, so the wrapper owns
   its reveal. Its own chip styling (injected by _msgInjectStyles) still applies
   inside the reactions strip, where it is a permanently visible affordance. */
.msg-replies-container .msg-reply-meta .msg-reply-actions .msg-reaction-add {
  border-color: transparent;
  background: transparent;
  opacity: 1;
  padding: 0 2px;
}
.msg-replies-container .msg-reply-meta .msg-reply-actions .msg-reaction-add:hover {
  background: var(--surface-2);
  border-color: var(--border);
}
.msg-replies-container .msg-reply-meta .msg-reply-actions .msg-reaction-add-icon {
  width: 14px;
  height: 14px;
}
/* Touch has no hover, so the cluster stays visible — long-press remains a
   second WhatsApp-style path to the same menu. */
@media (hover: none) {
  .msg-replies-container .msg-reply-meta .msg-reply-actions { opacity: 1; }
  .msg-replies-container .msg-reply-action-quick { display: none; }
  /* The mark-read toggle is hover-revealed (opacity 0) but its 44px hit halo
     was live on touch — an INVISIBLE control that silently marked a thread
     unread when a tap landed near the row's trailing edge. Visible, or it
     doesn't take taps. */
  .msg-thread-readtoggle { opacity: 0.45; }
  /* The 44px thumb target is declared in css/mobile.css, ≤768px only, and only
     on ⋯. Deliberately NOT here: a 44px pseudo-element on a 21px control
     overhangs ~11px to each side, and on a screen wide enough to still show
     add-reaction next to it those halos overlap — the later sibling wins the
     hit test, so ⋯ would silently swallow taps meant for the emoji button. On a
     phone the emoji and reply controls are hidden (they live in the long-press
     menu), so ⋯ is alone on the row and its halo can't collide with anything. */
  /* Long-press must open OUR menu, not iOS's selection callout. Text is still
     selectable on desktop, and Copy in the menu covers the phone case.
     touch-action: pan-y is the static hint that lets the compositor start
     vertical scrolls immediately instead of waiting on the swipe-to-reply
     gesture's non-passive touchmove listener — without it, every scroll pays
     a main-thread round trip before the first frame moves. */
  .msg-replies-container .msg-reply {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
    touch-action: pan-y;
  }
}

/* ── Bubble context menu (long-press / right-click) ───────────────────────────
   Lives on document.body, positioned in viewport coords, so the replies
   container's overflow can't clip it. */
.msg-bubble-menu-portal {
  position: fixed;
  inset: 0;
  z-index: 1000;
  pointer-events: none;
}
.msg-bubble-menu {
  position: fixed;
  z-index: 1000;
  min-width: 172px;
  max-height: calc(100dvh - 16px);
  overflow-y: auto;
  overscroll-behavior: contain;
  pointer-events: auto;
  padding: 5px;
  display: flex;
  flex-direction: column;
  gap: 1px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  animation: msgBubbleMenuIn 0.1s ease-out;
}

.msg-bubble-menu-meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 7px 10px 8px;
  margin-bottom: 2px;
  border-bottom: 1px solid var(--border);
  color: var(--text-muted);
  font-size: 11.5px;
  line-height: 1.35;
}

.msg-bubble-menu-meta strong {
  color: var(--text);
  font-size: 12.5px;
}
@keyframes msgBubbleMenuIn {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .msg-bubble-menu { animation: none; }
}
.msg-bubble-menu-item {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 8px 10px;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
  text-align: left;
  cursor: pointer;
}
.msg-bubble-menu-item svg { flex: none; opacity: 0.75; }
.msg-bubble-menu-item:hover,
.msg-bubble-menu-item:focus-visible {
  background: var(--surface-2);
  outline: none;
}
.msg-bubble-menu-item:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
.msg-bubble-menu-item.is-danger { color: var(--red); }
.msg-bubble-menu-item.is-danger:hover,
.msg-bubble-menu-item.is-danger:focus-visible { background: var(--red-dim); }
/* The bubble the menu belongs to, so it's obvious which one you long-pressed. */
.msg-replies-container .msg-reply.is-menu-open {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* ── Phone presentation: WhatsApp-style long-press sheet ─────────────────────
   .is-sheet is only ever added on touch+narrow (see _msgBubbleMenuIsSheet), so
   these need no media query. The scrim dims the conversation and makes
   "tap anywhere to dismiss" visible; the emoji quick-row leads the sheet with
   thumb-size targets, replacing the tiny anchored reaction picker there. */
.msg-bubble-menu-scrim {
  position: fixed;
  inset: 0;
  z-index: 999;
  background: rgba(0, 0, 0, 0.42);
  pointer-events: auto;
  animation: msgMenuScrimIn 0.14s ease;
}
@keyframes msgMenuScrimIn { from { opacity: 0; } to { opacity: 1; } }
/* !important where css/mobile.css already claims the property with
   !important (.msg-bubble-menu box-shadow at :9024, .msg-bubble-menu-item
   min-height/padding/font-size at :8683/:9026) — without it the sheet keeps
   the popover's downward shadow and 44px rows in the one viewport where
   .is-sheet is the only presentation. */
.msg-bubble-menu.is-sheet {
  /* Portrait was fine (the 34px home-indicator inset lifts this clear of the
     corner arc). Landscape is not: the inset resolves to 0, h drops to 8px,
     and an 8px side gutter against a ~26px requirement bites the corners off
     the first and last menu ROWS — which are 46px tap targets. The 18px floor
     plus the side insets clears both orientations. */
  left: max(8px, var(--sa-l, 0px));
  right: max(8px, var(--sa-r, 0px));
  top: auto;
  bottom: max(18px, calc(8px + var(--sa-b, 0px)));
  /* The generic menu cap only subtracts 16px, but this sheet also reserves an
     18px bottom gutter (plus the home-indicator inset). On short phones that
     made a full admin menu start above the viewport. Account for both edges;
     overflow-y on the base menu keeps every action reachable inside the sheet. */
  max-height: calc(100dvh - 36px - var(--sa-b, 0px));
  min-width: 0;
  border-radius: 16px;
  padding: 7px;
  box-shadow: 0 -6px 32px rgba(0, 0, 0, 0.4) !important;
  animation: msgMenuSheetUp 0.16s ease;
}
@keyframes msgMenuSheetUp {
  from { transform: translateY(14px); opacity: 0.5; }
  to   { transform: none; opacity: 1; }
}
.msg-bubble-menu.is-sheet .msg-bubble-menu-item {
  min-height: 46px !important;
  padding: 11px 12px !important;
  font-size: 15px !important;
}
.msg-menu-react-row {
  display: flex;
  justify-content: space-around;
  gap: 0;
  padding: 8px 4px 10px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 2px;
}
.msg-menu-react-row button {
  min-width: 40px;
  min-height: 44px;
  padding: 4px;
  border: none;
  border-radius: 10px;
  background: none;
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
}
.msg-menu-react-row button:active {
  background: var(--surface-2);
  transform: scale(1.18);
}
@media (prefers-reduced-motion: reduce) {
  .msg-bubble-menu.is-sheet, .msg-bubble-menu-scrim { animation: none; }
}

/* Attachments never overflow a narrow bubble. */
.msg-replies-container .msg-reply-attachments img,
.msg-replies-container .msg-reply-attachments .msg-attach-img { max-width: 100%; }
.msg-replies-container .msg-reply-attachments .msg-attach-file {
  box-sizing: border-box;
  max-width: 100% !important;
  overflow-wrap: anywhere;
}

/* ── Day separator ────────────────────────────────────────────────────────────
   A quiet timeline rule at each calendar-day boundary. Keep these in normal
   flow: multiple sticky separators shared the same top offset and piled into a
   stack of overlapping pills while scrolling across several days. */
.msg-day-sep {
  align-self: stretch;
  display: flex;
  align-items: center;
  gap: 10px;
  position: static;
  margin: 10px 0 8px; /* 2026-09-01 mega-audit polish: match unread-rule rhythm */
  pointer-events: none;
}
.msg-day-sep::before,
.msg-day-sep::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: color-mix(in srgb, var(--text-dim) 22%, transparent);
}
.msg-day-sep span {
  display: inline-block;
  flex: none;
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0 4px;
  /* 2026-09-01 mega-audit polish: same micro-label metrics as the unread rule;
     colour keeps carrying the difference. */
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  box-shadow: none;
}

/* ── Unread rule ──────────────────────────────────────────────────────────────
   "N unread messages" where the reader left off. Set once at open and held for
   the life of the open thread, so it doesn't crawl as new messages land. */
.msg-unread-rule {
  align-self: stretch;
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 10px 0 8px; /* 2026-09-01 mega-audit polish: match day-sep rhythm */
  color: var(--red);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.msg-unread-rule::before,
.msg-unread-rule::after {
  content: "";
  flex: 1;
  height: 1px;
  background: currentColor;
  opacity: 0.4;
}

/* ── Link previews (mig 853) ──────────────────────────────────────────────────
   One card per message, under the text.
   2026-09-01 mega-audit: the empty placeholder used to be display:none, so a
   card hydrating in pushed the whole stream down. It now reserves the
   hydrated card's footprint — 56px image + 7px×2 padding + 6px margin-top =
   76px — so hydration fills space that already exists.
   TODO(js): on a FAILED hydration remove the .msg-link-preview node (or add a
   class collapsing it) — the old "an un-previewable link leaves no trace"
   contract now depends on JS clearing the reserved box. */
.msg-link-preview:empty { min-height: 76px; }
.msg-link-preview-card {
  display: flex;
  gap: 9px;
  margin-top: 6px;
  padding: 7px 9px;
  border-radius: 10px; /* 2026-09-01 mega-audit polish */
  border-left: 3px solid var(--accent);
  background: color-mix(in srgb, var(--text) 6%, transparent);
  text-decoration: none;
  color: inherit;
  max-width: 100%;
  overflow: hidden;
}
.msg-link-preview-card:hover { background: color-mix(in srgb, var(--text) 10%, transparent); }
.msg-link-preview-img {
  width: 56px;
  height: 56px;
  flex: none;
  object-fit: cover;
  border-radius: 5px;
  background: var(--surface-2);
}
.msg-link-preview-body { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.msg-link-preview-site {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--accent);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.msg-link-preview-title {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text);
  line-height: 1.3;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.msg-link-preview-desc {
  font-size: 11.5px;
  color: var(--text-muted);
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ── Reply-to / quoting (mig 836) ─────────────────────────────────────────────
   The quoted message sits at the top of the bubble, inset and tinted, behind a
   colored bar keyed to the quoted author. Rendered from the snapshot columns, so
   it survives the original being recalled or paged out. */
.msg-quote {
  display: block;
  margin: 0 0 4px;
  padding: 4px 8px;
  border-left: 3px solid var(--msg-quote-accent, var(--accent));
  border-radius: 4px;
  background: color-mix(in srgb, var(--text) 6%, transparent);
  font-size: 12.5px;
  line-height: 1.35;
  max-width: 100%;
  overflow: hidden;
}
.msg-quote.is-jumpable { cursor: pointer; }
/* A quote of a photo shows the photo (Cole 2026-08-06). Fixed square with
   object-fit so the row height cannot change with the source aspect ratio —
   this box is reserved by CSS, not by width/height attributes, which is what
   the 2026-08-06 image audit found an author rule silently overriding. */
.msg-quote.has-thumb {
  display: flex;
  align-items: center;
  gap: 8px;
}
.msg-quote.has-thumb .msg-quote-body {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.msg-quote-thumb {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border-radius: 4px;
  object-fit: cover;
  background: color-mix(in srgb, var(--text) 8%, transparent);
}
/* A quote of a DOCUMENT says which document (Cole 2026-09-04: "make sure that
   the image or the doc always shows so that we can see what is being referred
   to"). Same flex row as .has-thumb, but the chip is the file's own name — a
   34px square would just be a generic icon repeated. */
.msg-quote.has-doc {
  display: flex;
  align-items: center;
  gap: 8px;
}
.msg-quote.has-doc .msg-quote-body {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.msg-quote-doc {
  flex: 0 1 auto;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-width: 45%;
  padding: 3px 7px;
  border-radius: 5px;
  background: color-mix(in srgb, var(--text) 8%, transparent);
  font-size: 11.5px;
  color: var(--text-muted);
}
.msg-quote-doc-glyph { flex: 0 0 auto; line-height: 1; }
.msg-quote-doc-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.msg-quote.is-jumpable:hover { background: color-mix(in srgb, var(--text) 10%, transparent); }
/* 2026-09-01 mega-audit: press feedback on the jumpable quote — independent
   `scale` so it composes with any positioning transform; neutralised under
   reduced motion per the surface-wide convention. */
.msg-quote.is-jumpable:active { scale: 0.985; }
@media (prefers-reduced-motion: reduce) {
  .msg-quote.is-jumpable:active { scale: 1; }
}
.msg-quote.is-jumpable:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.msg-quote-author {
  display: block;
  font-weight: 600;
  color: var(--msg-quote-accent, var(--accent));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Two lines of the quoted text, then ellipsis — enough to recognise it without
   the quote outgrowing the reply itself. */
.msg-quote-text {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  color: var(--text-muted);
  word-break: break-word;
}

/* The strip above the composer: what you're about to answer. */
.msg-reply-target-bar { padding: 0 0 5px; }
.msg-replying-to {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 6px 8px 6px 9px;
  border-left: 3px solid var(--msg-quote-accent, var(--accent));
  border-radius: 4px;
  background: var(--surface-2);
  font-size: 12.5px;
  line-height: 1.35;
}
.msg-replying-to > svg {
  flex: none;
  margin-top: 1px;
  color: var(--text-muted);
}
.msg-replying-to-body { min-width: 0; flex: 1; }
.msg-replying-to-thumb {
  flex: none;
  width: 38px;
  height: 38px;
  object-fit: cover;
  border-radius: 7px;
}
.msg-replying-to-cancel {
  flex: none;
  border: none;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  padding: 2px;
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
}
.msg-replying-to-cancel:hover { color: var(--text); background: var(--surface-3); }

/* Swipe-to-reply (touch): the bubble follows the finger, and a reply glyph is
   revealed underneath it on the left. */
.msg-replies-container .msg-reply.is-swiping { z-index: 1; }
.msg-replies-container .msg-reply.is-swiping::before {
  content: "↩";
  position: absolute;
  left: -30px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 15px;
  color: var(--text-muted);
}
@media (prefers-reduced-motion: reduce) {
  .msg-replies-container .msg-reply.is-swiping { transition: none !important; }
}

/* ── Jump to latest ───────────────────────────────────────────────────────────
   Appears once you've scrolled up out of the live end. Anchored to the convo
   column (which is position:relative) so it floats above the stream, clear of
   the composer. */
.msg-detail-convo { position: relative; }
.msg-jump-latest {
  position: absolute;
  /* Vertically clear at bottom:92px, but this is positioned against
     .msg-detail-convo, which on the phone overlay is left:0/right:0 — flush
     with the physical screen edge. In landscape a bare 16px puts a 44px disc
     under the 44-59px island inset. */
  /* Centred, not in the right corner: the corner is where the newest bubble's
     timestamp sits and where the rulings FAB lives (Cole 2026-09-07: "make
     sure that it doesn't cover other stuff"). A labelled pill reads as an
     action; the bare disc read as decoration. */
  left: 50%;
  /* 2026-09-01 mega-audit: was a hard-coded 92px, which drifted off the
     composer whenever the reply-target bar / voice bar / attach staging grew
     it. JS publishes the live composer height as --msg-composer-h. */
  bottom: calc(var(--msg-composer-h, 80px) + 10px);
  z-index: 4;
  height: 32px;
  padding: 0 12px 0 10px;
  gap: 6px;
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-md);
  color: var(--text-muted);
  /* 2026-09-01 mega-audit: was display:none → inline-flex, a layout toggle
     that popped with no transition. Always laid out; hidden/shown with
     opacity + transform (compositor-only), inert while hidden. */
  display: inline-flex;
  opacity: 0;
  transform: translateX(-50%) scale(0.85);
  pointer-events: none;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: color 0.12s ease, border-color 0.12s ease, opacity 0.16s ease, transform 0.16s ease;
}
.msg-jump-latest.is-visible {
  opacity: 1;
  transform: translateX(-50%) scale(1);
  pointer-events: auto;
}
@media (prefers-reduced-motion: no-preference) {
  .msg-jump-latest {
    transition: color 0.12s ease, border-color 0.12s ease,
      opacity 0.16s ease, transform 0.16s ease;
  }
}
.msg-jump-latest:hover { color: var(--text); border-color: var(--border-hover); }
.msg-jump-count {
  /* 2026-09-01 mega-audit polish: shared count-chip metrics */
  position: absolute;
  top: -4px;
  right: -4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  background: var(--red);
  color: #fff;
  font-size: 10.5px;
  font-weight: 700;
  line-height: 1;
  padding: 0 5px;
  border-radius: 999px;
  font-variant-numeric: tabular-nums;
}

/* 2026-09-01 mega-audit: a byte-identical duplicate of the earlier
   "Attachments never overflow a narrow bubble" max-width rule lived here —
   deleted; the first declaration is the one that holds. (The later
   width/height:auto rule near the retroactive-reserve pass does NOT re-declare
   max-width, so the first rule must stay.) */

/* ── Message footer row — Translate + reactions share one line instead of
   stacking (Cole 2026-07-20: "Translate button could be on the same row as
   the Emoji button"). Both children keep their own display/gap; this wrapper
   just zeroes their individual top margins and lines them up. ────────────── */
.msg-reply-footer {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 5px;
}
.msg-reply-footer .msg-translate-one,
.msg-reply-footer .msg-reactions {
  margin-top: 0;
}

/* ── Per-message Translate button ─────────────────────────────────────────── */
.msg-translate-one {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: 5px;
  padding: 2px 9px;
  font-size: 11px;
  font-weight: 500;
  line-height: 1.5;
  color: var(--msg-meta);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
  transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
.msg-translate-one:hover {
  color: var(--accent-strong);
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}
.msg-translate-one svg { width: 12px; height: 12px; opacity: 0.85; flex-shrink: 0; }
.msg-translate-one.is-busy { opacity: 0.6; cursor: default; }
.msg-translate-one[disabled] { cursor: default; opacity: 0.55; }

/* Fast, consistent press feedback across the Messages surface. `scale` is an
   independent transform property, so it does not overwrite controls whose
   layout already uses `transform` for positioning or animation. */
/* 2026-09-01 mega-audit: + .msg-portal button — body-appended overlays (JS
   stamps .msg-portal on them) escaped #mainContent and got no press feedback. */
/* 2026-09-01 mobile-audit: + non-button tap targets — thread/chat rows are
   div[data-action] (role=button) and bubbles (.msg-reply) take long-press and
   swipe gestures; all of them showed the grey iOS tap-highlight flash and
   missed the double-tap-zoom suppression. Deliberately NOT added to the
   :active scale rule below — pressing a whole row/bubble should not shrink it
   mid-swipe. */
#mainContent .msg-shell button,
#msgMobileDetail button,
.msg-bubble-menu button,
.msg-reaction-picker button,
.msg-portal button,
#mainContent .msg-shell [data-action],
#msgMobileDetail [data-action] {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition-duration: 0.12s;
}
/* Bubbles get ONLY the highlight suppression: their touch-action is a
   deliberate `pan-y` (the compositor hint for swipe-to-reply, declared in the
   coarse-pointer block above at higher effective priority than a blanket
   `manipulation` would deserve), and a blanket transition-duration could
   retime gesture transitions. */
#mainContent .msg-shell .msg-reply,
#msgMobileDetail .msg-reply {
  -webkit-tap-highlight-color: transparent;
}
#mainContent .msg-shell button:not(:disabled):active,
#msgMobileDetail button:not(:disabled):active,
.msg-bubble-menu button:not(:disabled):active,
.msg-reaction-picker button:not(:disabled):active,
.msg-portal button:not(:disabled):active {
  scale: 0.97;
}
#mainContent .msg-shell button[aria-busy="true"],
#msgMobileDetail button[aria-busy="true"],
.msg-bubble-menu button[aria-busy="true"] {
  cursor: progress;
  opacity: 0.62;
}
#mainContent .msg-member-item[aria-busy="true"],
#msgMobileDetail .msg-member-item[aria-busy="true"],
#mainContent .msg-task-select[aria-busy="true"],
#msgMobileDetail .msg-task-select[aria-busy="true"] {
  cursor: progress;
  opacity: 0.62;
  pointer-events: none;
}
.msg-reaction-chip.is-busy,
.msg-reaction-add.is-busy,
.msg-pinned-unpin.is-busy {
  cursor: progress;
  opacity: 0.58;
}
.msg-reactions.is-busy {
  cursor: progress;
  opacity: 0.62;
}
@media (prefers-reduced-motion: reduce) {
  #mainContent .msg-shell button,
  #msgMobileDetail button,
  .msg-bubble-menu button,
  .msg-reaction-picker button,
  .msg-portal button {
    transition: none !important;
  }
  #mainContent .msg-shell button:not(:disabled):active,
  #msgMobileDetail button:not(:disabled):active,
  .msg-bubble-menu button:not(:disabled):active,
  .msg-reaction-picker button:not(:disabled):active,
  .msg-portal button:not(:disabled):active {
    scale: 1;
  }
}

/* ═══ Conversation finish pass (2026-07-29) ════════════════════════════════
   A quieter, more intentional hierarchy for task conversations. These rules
   are visual only: the task lifecycle, customer linkage, messages, and
   composer behavior keep their existing contracts. */
.msg-detail-topbar {
  padding: 10px 20px;
  background: color-mix(in srgb, var(--surface) 88%, var(--bg));
  box-shadow: 0 1px 0 color-mix(in srgb, var(--border) 72%, transparent);
}
.msg-detail-topbar .btn {
  min-height: 36px;
  border-radius: 9px;
  background: var(--surface);
  box-shadow: 0 1px 2px rgba(22, 19, 15, 0.04);
}
.msg-detail-topbar .btn:hover {
  background: var(--surface-2);
  border-color: color-mix(in srgb, var(--accent) 28%, var(--border));
}
.msg-detail-topbar .msg-thread-status-btn--close {
  color: var(--green-strong);
  background: color-mix(in srgb, var(--green-dim) 72%, var(--surface));
  border-color: color-mix(in srgb, var(--green) 24%, var(--border));
}
.msg-detail-topbar .msg-thread-status-btn--reopen {
  color: var(--msg-blue-strong);
  background: color-mix(in srgb, var(--blue-dim) 72%, var(--surface));
  border-color: color-mix(in srgb, var(--blue) 24%, var(--border));
}

.msg-detail-header {
  padding: 15px 24px 13px; /* 2026-09-01 mega-audit polish: 24px detail gutter */
  background:
    linear-gradient(180deg, var(--surface) 0%, color-mix(in srgb, var(--surface) 86%, var(--bg)) 100%);
}
.msg-detail-title-row { margin-bottom: 6px; }
.msg-detail-title {
  font-size: 18px;
  line-height: 1.25;
  letter-spacing: -0.018em;
}
.msg-detail-title-edit-btn {
  width: 32px;
  height: 32px;
  flex: 0 0 32px;
  justify-content: center;
  padding: 0;
  border-radius: 50%;
  background: var(--bg);
  border-color: var(--border);
}
.msg-detail-meta { gap: 7px; }

.msg-task-details {
  border-color: color-mix(in srgb, var(--accent) 24%, var(--border));
  background: color-mix(in srgb, var(--bg) 72%, var(--surface));
  box-shadow: 0 1px 2px rgba(22, 19, 15, 0.035);
}
.msg-task-details-summary { padding-inline: 11px; }
.msg-task-details-summary:hover {
  background: color-mix(in srgb, var(--accent) 5%, var(--surface));
}
.msg-task-details[open] {
  border-color: color-mix(in srgb, var(--accent) 48%, var(--border));
  box-shadow: 0 5px 18px rgba(22, 19, 15, 0.055);
}

.msg-task-supporting:has(.msg-task-context) {
  grid-template-columns: minmax(300px, 0.9fr) minmax(400px, 1.1fr);
  gap: 10px;
}
.msg-task-context-card,
.msg-customer-card {
  box-sizing: border-box;
  min-width: 0;
  height: calc(100% - 8px);
  margin-top: 8px;
  border: 1px solid color-mix(in srgb, var(--accent) 18%, var(--border));
  border-radius: 12px;
  background:
    linear-gradient(145deg, color-mix(in srgb, var(--surface) 96%, var(--accent)) 0%, var(--bg) 100%);
  box-shadow: 0 2px 8px rgba(22, 19, 15, 0.035);
}
.msg-task-context-card { padding: 10px; }
.msg-task-context-title {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--text-muted);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.055em;
  line-height: 1.2;
  text-transform: uppercase;
}
.msg-task-context-title svg { color: var(--accent-strong); }
.msg-task-context {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
  padding-top: 8px;
}
.msg-task-context-chip {
  min-height: 34px;
  padding: 6px 8px;
  border-color: color-mix(in srgb, var(--border) 75%, transparent);
  border-radius: 9px;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
}
.msg-task-context-chip strong {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.msg-customer-card { padding: 10px 11px; }
.msg-customer-card-icon {
  border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 14%, var(--surface));
}
.msg-customer-link-btn--change {
  min-height: 30px;
  padding-inline: 8px;
  border-radius: 8px;
}
.msg-customer-link-btn--change:hover {
  background: var(--surface-2);
  text-decoration: none;
}
.msg-customer-inbox {
  border-top-color: color-mix(in srgb, var(--accent) 15%, var(--border));
}
.msg-customer-inbox-open:hover {
  background: color-mix(in srgb, var(--accent) 9%, transparent);
}

.msg-replies-container {
  padding-inline: max(16px, calc((100% - 1040px) / 2));
  scrollbar-gutter: stable;
  /* The paper weave moved to the NON-SCROLLING parent (.msg-detail-convo). It
     was four background layers — two of them `repeating-linear-gradient` at
     1px/11px — painted on the SCROLLER itself, which is the most expensive
     place to put a texture: the raster is invalidated on every scroll tick,
     for the full height of the content rather than the viewport. Measured 13
     background layers on this element at 1440px.
     css/mobile.css already flattened it to a flat `var(--msg-paper)` at ≤768px,
     but that file is gated to 768px in index.html, so desktop AND the 769-1024
     band (iPhone landscape, iPad) paid it in full.
     The scroller is now transparent and the texture rasterises once on the
     parent. It no longer scrolls with the content, which is the honest reading
     anyway — the paper is the surface the conversation sits ON, not part of
     the conversation. */
  background: transparent;
}
.msg-detail-convo {
  background:
    linear-gradient(180deg, var(--msg-convo-wash, rgba(255, 255, 255, 0.09)), transparent 26%),
    repeating-linear-gradient(135deg, var(--msg-weave) 0 1px, transparent 1px 11px),
    repeating-linear-gradient(45deg, var(--msg-weave) 0 1px, transparent 1px 11px),
    var(--msg-paper);
}
.msg-replies-container .msg-reply {
  max-width: min(72%, 720px);
  padding: 8px 11px 6px;
  border: 1px solid color-mix(in srgb, var(--border) 66%, transparent);
  border-radius: 3px 14px 14px 14px;
  /* 2026-09-01 mega-audit: the second 16px-blur layer was painted per bubble
     per frame across the whole stream for a near-invisible 2.5% halo. One
     cheap 1px/2px layer keeps the lift; the 1px border does the separation. */
  box-shadow: 0 1px 2px rgba(22, 19, 15, 0.08);
}
.msg-replies-container .msg-reply-own {
  border-radius: 14px 3px 14px 14px;
  background: var(--msg-bub-out);
}
.msg-replies-container .msg-reply-own.msg-reply-pinned {
  /* Still visibly YOUR message — the pin tints the green rather than replacing
     it with a wash that reads as somebody else's bubble. Opaque; see the
     double-paint note on .msg-reply-pinned above. */
  background: color-mix(in srgb, var(--accent) 13%, var(--msg-bub-out));
}
:root[data-theme="dark"] .msg-replies-container .msg-reply-own {
  background: var(--msg-bub-out);
}
:root[data-theme="dark"] .msg-replies-container .msg-reply-own.msg-reply-pinned {
  background: color-mix(in srgb, var(--accent) 22%, var(--msg-bub-out));
}
.msg-replies-container .msg-reply.msg-reply--cont { border-radius: 14px; }
.msg-replies-container .msg-reply-author {
  margin-bottom: 2px;
  font-weight: 700;
}
.msg-replies-container .msg-activity-line {
  width: calc(100% - 32px);
  max-width: 1000px;
  align-self: center;
  margin: 8px 16px;
  padding: 8px 10px;
  border-left: 0;
  border-radius: 10px;
  background: color-mix(in srgb, var(--surface) 47%, transparent);
}
.msg-reply-composer {
  padding: 12px max(24px, calc((100% - 1160px) / 2)) 16px;
  background: color-mix(in srgb, var(--bg) 90%, var(--surface));
  box-shadow: 0 -8px 24px rgba(22, 19, 15, 0.025);
}
.msg-reply-composer-inner {
  max-width: 1160px;
  margin-inline: auto;
  padding: 8px 10px;
  border-radius: 14px;
  box-shadow:
    0 1px 2px rgba(22, 19, 15, 0.05),
    0 7px 22px rgba(22, 19, 15, 0.035);
}
.msg-reply-composer-inner:focus-within {
  box-shadow:
    0 0 0 3px var(--accent-dim),
    0 8px 24px rgba(22, 19, 15, 0.05);
}
.msg-reply-composer .send-btn {
  min-height: 38px;
  border-radius: 9px;
  box-shadow: 0 2px 7px color-mix(in srgb, var(--accent) 22%, transparent);
}
.msg-reply-composer .send-btn:disabled {
  opacity: 1;
  color: var(--text-muted);
  background: var(--surface-3);
  border-color: var(--border);
  box-shadow: none;
}
.msg-reply-composer .send-btn:disabled:hover {
  filter: none;
  background: var(--surface-3);
  border-color: var(--border);
}

.msg-bubble-menu {
  min-width: 184px;
  padding: 6px;
  border-color: color-mix(in srgb, var(--accent) 22%, var(--border));
  border-radius: 15px;
  box-shadow:
    0 18px 48px rgba(22, 19, 15, 0.18),
    0 2px 8px rgba(22, 19, 15, 0.08);
}
.msg-bubble-menu-item {
  min-height: 40px;
  border-radius: 10px;
}
.msg-bubble-menu-item.is-danger {
  margin-top: 3px;
  position: relative;
}
.msg-bubble-menu-item.is-danger::before {
  content: "";
  position: absolute;
  top: -3px;
  left: 5px;
  right: 5px;
  height: 1px;
  background: var(--border);
}

/* ═══════════════════════════════════════════════════════════════════════════
   2026-07-30 — @mention picker, scroll-up paging, WhatsApp text formatting
   Appended last so these two-class selectors out-specify the density and
   responsive overrides earlier in the file without reaching for !important.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─── @mention picker ──────────────────────────────────────────────────────
   Was a plain list of names with the role floated right — no avatars, no hover
   or selected state, and nothing on screen until you typed a letter. It now
   reads as a person picker in the conversation's own language: the same hashed
   avatar colours the bubbles use, the typed run bolded in the name, the role as
   a quiet pill, and a highlighted row that arrow keys drive. */
.msg-mention-dropdown {
  padding: 5px;
  min-width: 232px;
  max-width: 320px;
  max-height: 264px;
  border-radius: 14px;
  border-color: color-mix(in srgb, var(--border) 80%, transparent);
  box-shadow:
    0 16px 40px rgba(22, 19, 15, 0.16),
    0 2px 6px rgba(22, 19, 15, 0.07);
  overscroll-behavior: contain;
}
.msg-mention-option {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 9px;
  min-height: 40px;
  border-radius: 10px;
  font-size: 13.5px;
  line-height: 1.3;
  color: var(--text);
  cursor: pointer;
  /* The picker is driven by mousedown (so the composer never loses focus) —
     nothing here should intercept the press. */
  user-select: none;
}
.msg-mention-option + .msg-mention-option { margin-top: 1px; }
.msg-mention-option.is-active {
  background: var(--accent-dim);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
}
.msg-mention-avatar {
  flex: none;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.2px;
  color: #fff;
  text-transform: uppercase;
}
:root[data-theme="dark"] .msg-mention-avatar { color: #181816; }
.msg-mention-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The run the user has typed so far — the only bold in the row. */
.msg-mention-name b { font-weight: 700; color: var(--accent); }
.msg-mention-role {
  flex: none;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--text-muted);
  text-transform: lowercase;
}
.msg-mention-jarvis .msg-mention-name { color: var(--purple); }

/* ─── The shared picker (js/shared/mention-picker.js) ──────────────────────
   The Messages view's dropdown is a child of its composer. This one is a
   BODY-level singleton serving the Team Projects docks and the mini chat dock,
   because those surfaces rebuild their panel DOM underneath it. Two
   consequences it has to carry its own rules for. */

/* 1. The responsive clamp in css/mobile.css is scoped to `#mainContent` and
      therefore cannot reach an element on <body>. Without this a phone gets the
      full 264px desktop panel with the keyboard already up. */
@media (max-width: 768px) {
  .im-mention-dropdown {
    max-height: 220px;
    max-width: calc(100vw - 24px);
    -webkit-overflow-scrolling: touch;
  }
  /* Touch targets, not mouse targets. */
  .im-mention-dropdown .msg-mention-option { min-height: 44px; }
}

/* 2. Jarvis is not a teammate you are notifying — it is the one row that
      ANSWERS. It already reads purple; the tint and the hairline separate it
      from the people below without adding a word of chrome to the row. */
.im-mention-dropdown .msg-mention-jarvis {
  background: color-mix(in srgb, var(--purple) 7%, transparent);
}
.im-mention-dropdown .msg-mention-jarvis + .msg-mention-option {
  margin-top: 5px;
  border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  padding-top: 10px;
}
.im-mention-dropdown .msg-mention-jarvis.is-active {
  background: color-mix(in srgb, var(--purple) 16%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--purple) 45%, transparent);
}
.im-mention-dropdown .msg-mention-jarvis .msg-mention-role {
  color: var(--purple);
  border-color: color-mix(in srgb, var(--purple) 35%, transparent);
  background: color-mix(in srgb, var(--purple) 10%, transparent);
}
/* The typed run is accent-coloured everywhere else; on the Jarvis row that
   fights the purple. */
.im-mention-dropdown .msg-mention-jarvis .msg-mention-name b { color: var(--purple); }

/* Opening should feel like the panel was already there. */
@media (prefers-reduced-motion: no-preference) {
  .im-mention-dropdown { animation: imMentionIn 110ms ease-out; }
  @keyframes imMentionIn {
    from { opacity: 0; transform: translateY(3px); }
    to   { opacity: 1; transform: none; }
  }
}

/* ─── Scroll-up paging ─────────────────────────────────────────────────────
   Opening a chat loads the newest 20 messages; this sits at the top of the
   window and pulls the previous page. It is always rendered while older history
   exists, so a conversation too short to scroll can still reach back. */
.msg-load-older {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 4px 0 10px;
  /* Both states — the button and the "loading earlier messages" line — occupy
     the SAME height. Swapping them happens while the reader is scrolling
     upward toward this header, and a header that changes height at that moment
     would shift every message below it: the exact jump this whole change is
     about, reintroduced by its own progress indicator. */
  min-height: 46px;
  box-sizing: border-box;
}
.msg-load-older-btn {
  border: 1px solid color-mix(in srgb, var(--border) 75%, transparent);
  background: var(--surface);
  color: var(--text-muted);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  padding: 6px 15px;
  min-height: 32px;
  border-radius: 999px;
  cursor: pointer;
  box-shadow: 0 1px 3px rgba(22, 19, 15, 0.07);
  transition: background 0.14s, color 0.14s;
}
.msg-load-older-btn:hover:not(:disabled) {
  background: var(--surface-2);
  color: var(--text);
}
.msg-load-older-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.msg-load-older-btn[aria-busy="true"] { opacity: 0.62; cursor: default; }

.msg-direct-jump {
  margin: 8px 12px 14px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}
.msg-direct-jump > span,
.msg-direct-jump-gap {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--text-muted);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .04em;
}
.msg-direct-jump-gap::before,
.msg-direct-jump-gap::after {
  content: '';
  height: 1px;
  flex: 1;
  background: var(--border);
}
.msg-direct-jump-gap { margin: 14px 0 4px; }

/* ─── WhatsApp text formatting ─────────────────────────────────────────────
   *bold*, _italic_, ~strike~ and ```mono``` render inline; lists and fenced
   blocks come through _msgRenderMd. Lists get a hanging indent so wrapped text
   lines up under the first character rather than under the marker. */
.msg-md .msg-md-list {
  margin: 3px 0;
  padding-left: 1.35em;
}
.msg-md .msg-md-list li { margin: 1px 0; }
.msg-md .msg-md-list li::marker { color: var(--text-muted); }
.msg-reply-content s,
.msg-md s { text-decoration-color: color-mix(in srgb, currentColor 65%, transparent); }
.msg-md .msg-md-pre {
  margin: 5px 0;
  padding: 8px 10px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--surface-2) 80%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  overflow-x: auto;
}
.msg-md .msg-md-pre code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12.5px;
  line-height: 1.5;
  white-space: pre;
  background: none;
  padding: 0;
}

/* Own bubble is green now — keep its secondary text legible on both themes
   rather than inheriting the paper-tuned muted greys.
   
   The `edited` marker and the ⋯ are listed explicitly. They were still taking
   `--msg-meta`, which is tuned against the PAGE background, and measured 3.71:1
   on the dark own-bubble teal (#075e54) — the ⋯ is the only control a message
   has on a phone, so that is a control nobody can see, not a faint decoration.
   The dark alpha went 0.72 → 0.85 for the same reason: the timestamp measured
   4.18:1, under the 4.5:1 that 10px text needs. Measured, both themes. */
.msg-replies-container .msg-reply-own .msg-reply-meta,
.msg-replies-container .msg-reply-own .msg-reply-status,
.msg-replies-container .msg-reply-own .msg-reply-edited,
.msg-replies-container .msg-reply-own .msg-reply-actions button {
  color: color-mix(in srgb, var(--text) 62%, transparent);
}
:root[data-theme="dark"] .msg-replies-container .msg-reply-own .msg-reply-meta,
:root[data-theme="dark"] .msg-replies-container .msg-reply-own .msg-reply-status,
:root[data-theme="dark"] .msg-replies-container .msg-reply-own .msg-reply-edited,
:root[data-theme="dark"] .msg-replies-container .msg-reply-own .msg-reply-actions button {
  color: rgba(233, 237, 232, 0.85);
}
:root[data-theme="dark"] .msg-replies-container .msg-reply-own,
:root[data-theme="dark"] .msg-replies-container .msg-reply-own .msg-reply-content {
  color: #e9ede8;
}

/* ─── Per-message initials disc (2026-07-30) ───────────────────────────────
   Cole: "lets make every message have the icon of the first letter of their
   name … use first and last initials". The sender NAME only renders on the
   first bubble of a same-author run, so the 2nd–Nth message of a run carried
   nothing identifying it. The disc rides in the conversation's gutter, hanging
   outside the bubble rather than joining its flow — so bubble width, the tail,
   and every `.msg-reply` selector elsewhere in this file are unaffected. */
.msg-replies-container {
  /* Room for the discs on both sides. The bubbles themselves are unchanged; the
     gutter is what grew. */
  padding-left: 46px;
  padding-right: 46px;
}
.msg-replies-container .msg-reply > .msg-reply-avatar {
  position: absolute;
  top: 0;
  left: -38px;
  width: 30px;
  height: 30px;
  font-size: 11px;
  /* Never a flex item — it is positioned out of flow in the gutter. */
  flex: none;
  box-shadow: 0 1px 3px rgba(22, 19, 15, 0.16);
}
.msg-replies-container .msg-reply-own > .msg-reply-avatar {
  left: auto;
  right: -38px;
}
/* A run of messages from one person reads as one stack: keep every disc (that
   is the ask) but fade the repeats so the run still groups visually and the
   first message of the run stays the emphasis. */
.msg-replies-container .msg-reply--cont > .msg-reply-avatar { opacity: 0.42; }

/* Narrow columns (split view, a squeezed 3-column layout) can't spare 46px of
   gutter — tuck the disc closer and shrink it rather than clipping it. */
@media (max-width: 1200px) {
  .msg-replies-container { padding-left: 40px; padding-right: 40px; }
  .msg-replies-container .msg-reply > .msg-reply-avatar {
    width: 26px; height: 26px; font-size: 10px; left: -33px;
  }
  .msg-replies-container .msg-reply-own > .msg-reply-avatar { left: auto; right: -33px; }
}

/* Auto-translated note (2026-07-30). A translated message must never pretend to
   be what was typed — this says where it came from and is itself the way back
   to the original. Quiet by default; it is context, not a control to hunt for. */
.msg-auto-translated-note {
  display: block;
  margin-top: 4px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 10.5px;
  line-height: 1.35;
  color: var(--text-muted);
  font-style: italic;
  text-align: left;
  cursor: pointer;
}
.msg-auto-translated-note:hover { color: var(--text-muted); text-decoration: underline; }
.msg-auto-translated-note:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

/* ─── All / Chats / Tasks pill (2026-07-30) ────────────────────────────────
   Cole: "should be a three option rectangle square pill right at the top".
   It sits at the head of the list and scrolls with it, because it describes
   what the list below is showing. Sticky so it stays reachable on a long list. */
.msg-kind-filter {
  position: sticky;
  top: 0;
  z-index: 3;
  display: flex;
  gap: 3px;
  margin: 0 0 6px;
  padding: 3px;
  border-radius: 10px;
  background: var(--surface-2);
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}
.msg-kind-tab {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  min-height: 32px;
  padding: 5px 8px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.14s, color 0.14s;
}
.msg-kind-tab:hover { color: var(--text); }
.msg-kind-tab.is-active {
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 1px 3px rgba(22, 19, 15, 0.1);
}
.msg-kind-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.msg-kind-tab-count {
  min-width: 17px;
  padding: 0 5px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--text-dim) 20%, transparent);
  font-size: 10.5px;
  font-weight: 700;
  line-height: 17px;
}
.msg-kind-tab.is-active .msg-kind-tab-count {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--text);
}

/* ═══════════════════════════════════════════════════════════════════════════
   DENSITY PASS (2026-07-30) — Cole: "make these all way more efficient for
   desktop and mobile."
   Measured before, desktop 1280x, 700px conversation column: 386px of the
   column (55%) was chrome and only 3 messages fit. A one-sentence bubble cost
   92px of which 35px was the sentence.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Bubbles ─────────────────────────────────────────────────────────────── */
/* The translate pill and the add-reaction control now ride the timestamp row
   (see _msgRenderOneReply), so it needs to lay them out inline and stay short. */
.msg-replies-container .msg-reply-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 0;
  line-height: 1.2;
}

/* Reactions now sit on the meta line rather than in a band above it (see
   _msgRenderOneReply, 2026-08-27). `margin-right: auto` pushes the timestamp
   and status to the right edge and keeps the chips hard left, which is the
   WhatsApp arrangement. The row is already `display:flex; align-items:center`,
   so the chips inherit vertical centring for free. */
.msg-replies-container .msg-reply-meta .msg-reply-footer {
  margin-right: auto;
  margin-top: 0;
  min-width: 0;
}
.msg-replies-container .msg-reply-meta .msg-reply-footer .msg-reactions { margin-top: 0; }
/* Push the time + receipt to the right so the controls sit left of them. */
.msg-replies-container .msg-reply-meta .msg-reply-time { margin-left: auto; }
/* These were sized for a row of their own. Inline, they only need to match the
   14px meta text — the bubble menu (long-press / three-dot) is the full-size
   path to both actions, so this is not the only way to reach them. */
.msg-replies-container .msg-reply-meta .msg-translate-one {
  padding: 1px 6px;
  font-size: 10.5px;
  min-height: 0;
  line-height: 1.5;
  border-radius: 999px;
}
.msg-replies-container .msg-reply-meta .msg-reaction-add {
  min-height: 0;
  min-width: 0;
  width: auto;
  padding: 0 3px;
  font-size: 11px;
  line-height: 1.3;
}
/* Only present when there are real reactions now — give it a tight top gap
   rather than the old standalone-row spacing. */
.msg-replies-container .msg-reply-footer { margin-top: 3px; }
.msg-replies-container .msg-reply { padding: 5px 9px 4px; }
.msg-replies-container .msg-reply-author { margin-bottom: 1px; line-height: 1.25; }
.msg-replies-container .msg-reply-content { line-height: 1.45; } /* 2026-09-01 mega-audit polish */
.msg-replies-container { gap: 1px; padding-top: 6px; }

/* ── Conversation header ─────────────────────────────────────────────────── */
/* 204px before a single message. The title block carried page-heading spacing
   inside what is really a toolbar. */
.msg-detail-header { padding-top: 10px; padding-bottom: 8px; }
.msg-detail-header .msg-detail-title { font-size: 17px; line-height: 1.25; margin-bottom: 2px; }
.msg-detail-meta { margin-bottom: 4px; font-size: 11.5px; }
.msg-task-details { margin-top: 4px; }
.msg-task-details-summary { padding: 6px 10px; min-height: 0; }
.msg-task-supporting { margin-top: 4px; gap: 4px; }

/* ── Task rows ───────────────────────────────────────────────────────────── */
/* The row is 37px of content; a 44px read-toggle was setting the height. Take
   the control out of the height calculation and give it a centred hit area
   instead — 59px -> 51px, which is two more tasks per screen. */
.msg-thread-row { align-items: center; }
.msg-thread-row .msg-thread-readtoggle {
  min-height: 0;
  height: 24px;
  width: 24px;
  padding: 0;
  position: relative;
  flex: 0 0 auto;
}
.msg-thread-row .msg-thread-readtoggle::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 40px;
  transform: translate(-50%, -50%);
}

/* Composer: 125px for a one-line reply box — 28px of that was outer padding
   wrapping an inner box that already has its own. */
/* Safe-area + corner-arc insets live on THIS rule, not the base one at the top
   of the file, because this is the declaration that wins. It also covers
   769-1024px, where iPhone LANDSCAPE lives and css/mobile.css — which carries
   the phone composer's own corner gutter — is gated off by index.html. Without
   the bottom inset, `position: sticky; bottom: 0` put the composer's own
   corners at h=0, where a ~55px display radius clips 55px in from each side.
   Verified in-browser: computed padding was `6px 20px 10px` before this. */
.msg-reply-composer {
  padding: 6px max(20px, var(--sa-r, 0px))
           calc(10px + var(--sa-b, 0px))
           max(20px, var(--sa-l, 0px));
}
.msg-reply-composer-inner { gap: 2px; }
.msg-composer-actions { min-height: 32px; }
/* The Link-customer prompt is a suggestion, not a control anyone reads twice —
   it was taking 50px on every task, more than a message. A blanket
   padding-top/bottom here made it WORSE (50 -> 62): `.msg-customer-link` had
   less than 6px to begin with, so the rule added height instead of removing it.
   Target its own box and clamp the height instead of guessing at padding. */
.msg-task-supporting > .msg-customer-link { padding: 0; min-height: 0; }
/* The prompt itself is a two-line button (label + explanation) at 48px. It is
   an offer, not a task control — one line is enough. */
.msg-task-supporting .msg-customer-link-btn {
  padding: 5px 9px;
  min-height: 0;
  line-height: 1.25;
}
.msg-task-supporting .msg-customer-link-btn small,
.msg-task-supporting .msg-customer-link-btn span { line-height: 1.25; }
.msg-detail-meta { line-height: 1.3; }

/* ─── Inline message edit (2026-07-30) ─────────────────────────────────────
   Cole: "editing messges flow is not good UX". It was a fixed rows=2 textarea
   with a drag-to-resize grabber, so a message longer than two lines was clipped
   and you had to drag the corner to read your own words; the caret landed at
   position 0, in front of the text; Cancel re-rendered the whole conversation;
   and there was no Escape. The box now sizes to the message and follows it. */
/* `textarea.msg-edit-input`, not `.msg-edit-input`: dashboard.css carries
   `textarea.form-input { resize: vertical; min-height: 42px }` at (0,1,1), which
   out-specifies a lone class no matter what order the sheets load in. */
textarea.msg-edit-input {
  width: 100%;
  font-size: 16px;          /* 16px prevents iOS zoom-on-focus — do not lower */
  line-height: 1.45;
  resize: none;             /* it grows itself; a grabber means it didn't */
  overflow-y: auto;
  min-height: 0;
  padding: 6px 8px;
  border-radius: 8px;
}
.msg-edit-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 5px;
}
/* The hint sits left and pushes the buttons right, so the pair stays where the
   thumb/cursor expects while the shortcut stays discoverable. */
.msg-edit-hint {
  flex: 1;
  min-width: 0;
  font-size: 10.5px;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.msg-edit-btn {
  font-size: 12px;
  padding: 4px 12px;
  min-height: 30px;
  border-radius: 999px;
}
@media (max-width: 768px) {
  /* Thumb-sized on a phone, and the hint gives way before the buttons do. */
  #mainContent .msg-edit-btn { min-height: 36px !important; padding: 6px 14px !important; }
  #mainContent .msg-edit-hint { display: none !important; }
}

/* ── Attachment staging tray (what you staged, before you send) ─────────────
   Two chip TYPES — a thumbnail and a named file — that must read as ONE row:
   same height, same radius, same remove control. This was previously built
   entirely from inline styles, and inline styles are exactly how the two types
   drifted apart. Measured at a pinned 1280px before the fix: a 56x61 image chip
   sitting beside a 177x55.3 file pill, each with its remove button hanging 6px
   outside its own box (an overhanging control is also the first thing clipped
   once the tray scrolls). Cole, 2026-07-30: "sending a file looks really ugly
   when attached before sending."

   Chip heights are FIXED on purpose. The tray's height feeds the composer's
   height, and the composer's height feeds the conversation's scroll position
   (see _msgPinConversationThroughComposerResize in js/views/messages.js). A chip
   that sizes itself to its content is a chip that moves the conversation. */
.msg-attach-staging {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  overflow-x: auto;
  overflow-y: hidden;
  border-top: 1px solid var(--border);
  scrollbar-width: thin;
}
.msg-attach-staging::-webkit-scrollbar { height: 6px; }
.msg-attach-staging::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.msg-attach-staging-label {
  flex: 0 0 auto;
  color: var(--accent);
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
}
.msg-attach-staging-items {
  display: flex;
  align-items: center;
  gap: 8px;
}

.msg-attach-chip {
  position: relative;
  flex: 0 0 auto;
  box-sizing: border-box;
  height: 52px;
  border-radius: 10px;
}
.msg-attach-chip--img,
.msg-attach-chip--video { width: 52px; }
.msg-attach-chip--img img,
.msg-attach-chip--video video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 10px;
  border: 1px solid var(--border);
}
/* Tap/click a staged photo to mark it up (boxes, circles, lines, text) before
   it sends — WeChat's move. The pencil badge is the visible hint; the whole
   chip is the target, so a thumb never has to hit a 12px glyph. */
.msg-attach-chip--img.is-editable { cursor: pointer; }
.msg-attach-chip--img.is-editable:active img { scale: 0.96; }
.msg-attach-edit {
  position: absolute;
  left: 3px;
  bottom: 3px;
  width: 20px;
  height: 20px;
  border: 0;
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.62);
  color: #fff;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  cursor: pointer;
  pointer-events: none;   /* the chip handles the tap; the badge is a label */
}
.msg-attach-chip--img.is-editable:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 10px; }
@media (prefers-reduced-motion: reduce) { .msg-attach-chip--img.is-editable:active img { scale: 1; } }
.msg-attach-chip--file {
  display: flex;
  align-items: center;
  gap: 9px;
  max-width: 220px;
  /* Right padding reserves the remove button's corner so it can never sit on
     top of the filename. */
  padding: 0 26px 0 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
}
.msg-attach-glyph { font-size: 19px; line-height: 1; flex-shrink: 0; }
.msg-attach-meta { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.msg-attach-name {
  font-size: 12.5px;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 132px;
}
.msg-attach-size { font-size: 11px; color: var(--text-muted); }

/* Painted small, tapped big — the same split used for .icon-btn. The visible
   disc is 18px and sits INSIDE the chip's corner; the transparent ::after
   carries a 40px target. A neutral scrim reads over both a photograph and a
   panel; the old hard-red circle only ever suited the panel. */
.msg-attach-staging .msg-attach-remove {
  position: absolute;
  top: 3px;
  right: 3px;
  z-index: 1;
  width: 18px;
  height: 18px;
  min-width: 18px;
  min-height: 18px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, .62);
  color: #fff;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}
/* The remove control's touch target. Three passes have fought over the SIZE of
   this box (40 → 28 on 2026-09-02, with css/mobile.css independently holding it
   at 44 for the touch floor) because size is the wrong dial.
   
   Measured 2026-09-03 on the real phone surface: chip 52x52, remove button
   22x22 at top:3/right:3, so its centre sits just 12px from the chip's centre
   in BOTH axes. Any halo CENTRED on that button with a half-side over 12px
   reaches the middle of the photo — so the largest centred square that clears
   it is 24px, smaller than the button it is supposed to enlarge. Size alone
   cannot satisfy both "44px touch floor" and "tapping the photo marks it up".
   A hit-test grid over the chip found 81 of 169 sampled points (48%), the whole
   upper-right block INCLUDING the centre, firing delete on a chip whose primary
   gesture is mark-up. Cole: "make it so that it wont delete it if we try to
   mark it up."
   
   SHAPE is the right dial. The box keeps its full declared size — so it still
   extends up and right into the strip's 8px padding, which is where the extra
   reach is actually free — and is clipped so it never descends into the photo
   body. clip-path clips hit-testing, not just paint, which is what makes this a
   fix and not a cosmetic. Percentage-based so the mobile rule's larger box
   inherits the same proportion without a second magic number. The button's own
   border box is unaffected and always hittable, so the × never stops working. */
.msg-attach-staging .msg-attach-remove::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 28px;
  height: 28px;
  transform: translate(-50%, -50%);
}

/* 2026-09-03 round-2 self-audit: the clip was originally on the rule above,
   which is chip-type AGNOSTIC — so it also shrank the delete target on FILE and
   VIDEO chips, which have no competing gesture at all (`is-editable`, and the
   mark-up handler that reads it, are `--img`-only). On those the halo was
   harmless and the trim was pure loss, dropping the target under the touch
   floor on the one chip that never needed protecting. Scoped to image chips,
   which are the only ones where a tap means two things. */
.msg-attach-staging .msg-attach-chip--img .msg-attach-remove::after {
  clip-path: inset(0 0 32% 32%);
}
.msg-attach-staging .msg-attach-remove:hover { background: rgba(0, 0, 0, .82); }

/* ── In-bubble photos reserve their box before they decode ──────────────────
   Without a reserved box an <img> is a ~21px alt-text line until it loads, then
   snaps to its full height — measured ~167px of shift per photo — and
   `loading="lazy"` fires that mid-scroll, once per image. That is the larger
   half of Cole's "the messages tab jumps around a lot" (2026-07-30).

   `width: auto; height: auto` is REQUIRED, not cosmetic: without BOTH, the
   width/height attributes are taken literally and each max-cap clamps its own
   axis independently — a 400x1600 photo rendered as a 280x280 square (the
   stretch Cole reported 2026-08-04). With both auto, the attributes act purely
   as an aspect-ratio hint: the box is still reserved at the right shape before
   decode, and the caps scale the photo proportionally. */
.msg-replies-container .msg-reply-attachments .msg-attach-img {
  width: auto;
  height: auto;
}

/* The retroactive half. Every message sent before dimensions were captured has
   nothing to reserve from. It used to get a fixed 4:3 crop box; Cole reversed
   that trade 2026-08-04 ("an image of any size and ratio … without stretch") —
   at the time read as: legacy images size naturally and accept the one-time
   reflow on decode.
   SUPERSEDED 2026-08-11: that reflow (0px → up to 340px per photo, mid-scroll,
   per repaint under loading="lazy") is exactly the "scrolling a chat with
   photos is glitchy" Cole reported, so the unsized path reserves a 4:3
   object-fit:cover box again (inline, from the JS renderer) — no DISTORTION
   (his 2026-08-04 complaint was a stretched square), but a slight CROP in the
   thumbnail; the lightbox always shows the full photo, and the exact box takes
   over once a decode records real dimensions. If Cole re-asserts no-crop, the
   alternative is adopting the real ratio on load and accepting the shift.
   New messages carry w/h and still reserve their exact box. */
.msg-replies-container .msg-reply-attachments .msg-attach-img--unsized {
  width: auto;
  height: auto;
  max-width: min(280px, 100%);
  max-height: 340px;
  background: var(--surface-2);
}

/* ── "Under the hood" — Claude's reasoning behind a short message ────────────
   Automated messages are hard-capped short (mig 878) so a person can read them.
   The reasoning lives in claude_context_notes and surfaces here, collapsed.
   Deliberately quiet: this must read as a footnote on the message, never as a
   second message competing with it. Admin-only — the fetch never fires for
   anyone else, so they see no affordance at all. (Cole, 2026-07-31) */
.msg-ctxnote {
  margin-top: 6px;
}

.msg-ctxnote-toggle {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 10.5px;
  font-style: italic;
  color: var(--text-muted);
  background: none;
  border: none;
  padding: 1px 0;
  cursor: pointer;
  opacity: 0.65;
  transition: opacity 0.12s ease;
}

.msg-ctxnote-toggle:hover { opacity: 1; color: var(--accent); }
.msg-ctxnote.is-open .msg-ctxnote-toggle { opacity: 1; color: var(--accent); }

.msg-ctxnote-panel {
  margin-top: 6px;
  padding: 8px 10px;
  border-left: 2px solid var(--accent-dim);
  border-radius: 0 6px 6px 0;
  /* Own (outgoing) bubbles are a saturated accent fill, so --text-dim on a
     translucent grey lands at roughly 1.5:1 there — verified unreadable in the
     browser before this fix. Own a solid dark surface and a near-white text
     colour instead of inheriting, so the panel reads identically on incoming
     bubbles, outgoing bubbles, and light/dark themes. */
  background: rgba(18, 18, 20, 0.82);
  font-size: 11.5px;
  line-height: 1.5;
  color: rgba(238, 238, 238, 0.92);
  /* Notes run long by design — that is the point. Scroll rather than let one
     note push the rest of the conversation off the screen. */
  max-height: 320px;
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-word;
}

.msg-ctxnote-lead {
  font-size: 10px;
  font-style: italic;
  opacity: 0.75;
  color: rgba(238, 238, 238, 0.8);
  margin-bottom: 6px;
}

.msg-ctxnote-entry + .msg-ctxnote-entry {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border-subtle, rgba(127, 127, 127, 0.18));
}

.msg-ctxnote-who {
  font-size: 10px;
  font-weight: 600;
  opacity: 0.9;
  color: var(--accent, #d9c6a3);
  margin-bottom: 2px;
}

/* Jarvis paraphrase — content Jarvis is REPEATING BACK, not saying itself.
   Jarvis wraps quoted/echoed message text in «…» and the renderer converts it
   to this span (Cole, 2026-07-31: "if repeating a message, always say it in
   italics and smaller font so we're clear that he's paraphrasing"). The visual
   difference is the whole point — a confirmation that quotes what was sent
   must never read as Jarvis's own wording. */
.msg-jarvis-quote {
  font-style: italic;
  font-size: 0.88em;
  opacity: 0.85;
}

/* @jarvis proposed-action card. Visible to the whole channel so the
   conversation reads coherently; only the person Jarvis proposed TO gets the
   buttons (the payload is token-gated to them server-side regardless). */
.msg-jarvis-proposal {
  margin-top: 8px;
  padding: 10px 12px;
  border: 1px solid var(--accent, #6366f1);
  background: var(--accent-dim, rgba(99, 102, 241, 0.08));
  border-radius: 10px;
  font-size: 13px;
  max-width: 420px;
}
.msg-jarvis-proposal--passive {
  border-color: var(--border, #e5e7eb);
  background: var(--surface-2, #f8f9fb);
  color: var(--text-secondary, #6b7280);
}
.msg-jarvis-proposal__head { font-weight: 600; margin-bottom: 6px; }
.msg-jarvis-proposal__list { margin: 0 0 10px; padding-left: 18px; }
.msg-jarvis-proposal__list li { margin: 2px 0; }
.msg-jarvis-proposal__actions { display: flex; gap: 8px; }
.msg-jarvis-proposal button {
  padding: 6px 14px;
  border-radius: 7px;
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  min-height: 32px;
}
.msg-jarvis-confirm {
  background: var(--accent, #6366f1);
  color: #fff;
  border: 1px solid var(--accent, #6366f1);
}
.msg-jarvis-dismiss {
  background: transparent;
  color: var(--text-secondary, #6b7280);
  border: 1px solid var(--border, #e5e7eb);
}
.msg-jarvis-proposal.is-running { opacity: 0.6; pointer-events: none; }
.msg-jarvis-proposal.is-done { border-color: var(--green, #10b981); }
.msg-jarvis-proposal.is-done .msg-jarvis-proposal__actions { display: none; }

/* The editor owns attachment previews while it is open. Hiding the normal
   gallery prevents an edit click from also opening the underlying lightbox. */
.msg-replies-container .msg-reply.is-editing > .msg-reply-attachments {
  display: none;
}

/* Copy-for-Claude — puts `/ctx task:<id>` or `/ctx chat:<slug>` on the
   clipboard. Quiet by default; a reference is useful often enough to be
   present and rarely enough that it should not compete with the title. */
.msg-ctx-copy-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  min-width: 28px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 7px;
  background: transparent;
  color: var(--text-dim, #9ca3af);
  cursor: pointer;
  flex-shrink: 0;
}
.msg-ctx-copy-btn:hover,
.msg-ctx-copy-btn:focus-visible {
  color: var(--text, #111827);
  background: var(--surface-2, #f3f4f6);
  border-color: var(--border, #e5e7eb);
}
@media (max-width: 768px) {
  /* 44px touch floor on phones, matching the other header controls. */
  .msg-ctx-copy-btn { width: 44px; height: 44px; min-width: 44px; }
}

/* ─── "Jarvis is thinking" ─────────────────────────────────────────────────
   Cole, 2026-08-03: "How do I know if Jarvis is thinking about something and
   loading a response? … if Jarvis is thinking, we should have an
   acknowledgement that Jarvis got the message and is thinking."

   The gap between summoning Jarvis and its answer landing was blank, and blank
   is indistinguishable from broken — which is exactly how it was reported. The
   placeholder is a real reply row (server_jarvis_mentions.post_thinking), so it
   reaches every surface and survives a repaint; this is only its look. The
   same three-dot rhythm as the inbox card's loader, deliberately: one idea of
   "Jarvis is working" across the product, not two. */
.msg-thinking {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 0;
}
.msg-thinking span {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--purple);
  opacity: 0.45;
  animation: msgThinkingPulse 1.25s var(--ease-standard, ease-in-out) infinite;
}
.msg-thinking span:nth-child(2) { animation-delay: 0.16s; }
.msg-thinking span:nth-child(3) { animation-delay: 0.32s; }
@keyframes msgThinkingPulse {
  0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
  30%           { opacity: 1;   transform: translateY(-2px); }
}
/* Reduced motion still needs to say "working" — it just says it without moving. */
@media (prefers-reduced-motion: reduce) {
  .msg-thinking span { animation: none; opacity: 0.55; }
}
/* The word beside the dots. The dots alone are a convention you have to already
   know; the word is the message. Muted + italic so it still reads as a status
   rather than as something Jarvis said.

   Softened by OPACITY on --text, not by a dim token — measured on the purple
   Jarvis bubble, in both themes, rather than eyeballed:

     var(--text-dim)     2.77 dark / 4.06 light   ← the obvious first choice
     var(--text-muted)   3.19 dark / 4.81 light
     var(--text) @ 0.7   5.72 dark / 6.17 light   ← this
     (the dots themselves 4.20 dark / 4.99 light)

   --text-dim is the trap: it goes #a09d97 → #56544f between light and dark,
   i.e. it gets DARKER in dark mode because it is a barely-there token for
   light surfaces. A "thinking" indicator nobody can read is the exact
   complaint this change exists to fix, so the word must clear the dots it sits
   next to, in both themes. */
.msg-thinking-word {
  margin-left: 6px;
  font-size: 13px;
  font-style: italic;
  color: var(--text);
  opacity: 0.7;
  vertical-align: middle;
}
/* The placeholder is a status, not a message: no actions, no timestamp row. */
.msg-reply-thinking .msg-reply-meta,
.msg-reply-thinking .msg-reply-actions { display: none; }

/* ─── WhatsApp-shape composer (2026-08-03) ─────────────────────────────────
   Cole: "the send button should be like WhatsApp. It should be all the way to
   the right and like its own section of the page, not a part of the chat
   bubble. In fact, no buttons should be a part of the chat bubble." The send
   is now a SIBLING of the input pill, not a child — it stays put while the
   pill grows with the text, exactly the WhatsApp geometry. */
.msg-composer-row {
  display: flex;
  align-items: flex-end;
  gap: 8px;
}
.msg-composer-row .msg-reply-composer-inner { flex: 1 1 auto; min-width: 0; }
.msg-send-detached {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  border: none;
  color: #fff;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  /* Bottom-aligned with the pill, and stays there as the textarea grows. */
  margin-bottom: 1px;
  transition: transform 140ms var(--ease-spring), background 140ms var(--ease-standard),
              opacity 140ms var(--ease-standard);
}
.msg-send-detached:hover:not(:disabled) { background: var(--accent-strong); }
.msg-send-detached:active:not(:disabled) { transform: scale(0.92); }
.msg-send-detached:disabled { opacity: 0.35; cursor: default; box-shadow: none; }
.msg-send-detached:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The icon sits optically centered (send glyphs lean left). */
.msg-send-detached svg { margin-left: 2px; }

/* With send outside, the tool row inside the pill is exactly that — tools. It
   gets one compact height instead of reserving a 36px control row. */
.msg-composer-row .msg-composer-actions { min-height: 30px; gap: 2px; }
.msg-composer-row .msg-reply-composer-inner { padding: 4px 8px; }

/* ── One thin row, and it only grows when the text does (Cole 2026-09-04) ────
   Cole, holding WhatsApp beside us: "the typing bar at the bottom is only one
   thin row. It only expands as we type a lot more … we're able to see a full
   page of content, and it doesn't waste a lot of space."

   The pill was a COLUMN — textarea, then a key hint on its own line, then a
   full-width tool row under both. Measured empty at 1512x900: 119px of composer
   under a 523px conversation, i.e. the empty message box was 23% of the height
   of the messages it sits below. Same three controls, one row: tools left, text
   centre, send outside on the right (the 2026-08-03 WhatsApp geometry above is
   untouched — send is still a SIBLING of the pill, not a child of it).

   Gated to >=769px on purpose. css/mobile.css owns the phone composer end to
   end (its own paddings, its 36px/30px keyboard-up compaction, `.msg-composer
   --typing`) and does NOT re-declare flex-direction, so an ungated base change
   would have silently reshaped a layout that was measured against a keyboard. */
@media (min-width: 769px) {
  .msg-composer-row .msg-reply-composer-inner {
    flex-direction: row;
    align-items: flex-end;
    gap: 4px;
    padding: 3px 6px 3px 4px;
  }
  /* The + sits left of the text, where WhatsApp puts it. */
  .msg-composer-row .msg-reply-composer-inner > .msg-composer-actions {
    order: -1;
    flex: 0 0 auto;
    min-height: 0;
  }
  .msg-composer-row .msg-reply-composer-inner > textarea {
    flex: 1 1 auto;
    min-height: 26px;
    /* Was 220px — over a fifth of a 900px viewport handed to a draft nobody has
       finished writing. It still scrolls internally past this. */
    max-height: 160px;
  }
  /* The hint owned a permanent line to say something that stops being true the
     moment you act on it: it is already hidden on focus and while typing, so it
     was only ever visible to someone who is not typing. aria-hidden already, so
     display:none costs a screen reader nothing. */
  .msg-composer-row .msg-composer-hint { display: none; }
  .msg-reply-composer {
    padding: 5px max(20px, var(--sa-r, 0px))
             calc(7px + var(--sa-b, 0px))
             max(20px, var(--sa-l, 0px));
  }
}

/* Auto-translate switch in the Thread Info panel (2026-08-05). A checkbox row
   rather than a bare toggle: the sub-line has to say what it does and which
   language it translates INTO, because the bug this control answers was someone
   not knowing their UI language had changed underneath them. */
.msg-info-toggle-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  cursor: pointer;
  padding: 2px 0;
}
.msg-info-toggle-row input[type="checkbox"] {
  flex: none;
  width: 18px;
  height: 18px;
  margin-top: 1px;
  accent-color: var(--accent);
  cursor: pointer;
}
.msg-info-toggle-label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 2px;
}
.msg-info-toggle-row .msg-info-file-sub { display: block; line-height: 1.45; }

/* Auto-translate switch in the conversation topbar (2026-08-05). Deliberately a
   topbar control and not a panel row: channel chats — the factory conversations
   this setting exists for — have no ⓘ panel to put it in. `is-on` tints it so
   the state is readable without hovering for the tooltip; the whole bug was
   translation happening without the reader knowing it was on. */
.msg-autotranslate-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 8px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: background 0.14s ease, color 0.14s ease, border-color 0.14s ease;
}
.msg-autotranslate-btn:hover { background: var(--surface-2); color: var(--text); }
.msg-autotranslate-btn.is-on {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 38%, transparent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.msg-autotranslate-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Editing reads as the text becoming editable, on BOTH surfaces (2026-08-05).
   The dock's editor used to be a bordered panel dropped over the bubble; it now
   keeps the bubble and drops the field chrome. This does the same for the main
   view, which already edited in place but still rendered a `.form-input` — a
   bordered, differently-coloured box sitting inside a tinted bubble. Scoped
   through `.msg-replies-container .msg-reply` so it only applies to the in-bubble
   editor and never to the composer or any other `.form-input`. */
/* The bubble is `white-space: pre-wrap` so message text keeps its line breaks.
   That also makes the newlines and indentation BETWEEN the editor's elements
   render as real blank lines — measured 40px of dead space above the attachment
   row and another 44px above the buttons, which is what made the editor look
   like it had fallen apart. While editing there is no message text to preserve;
   the textarea keeps its own line breaks regardless of this property. */
.msg-reply.is-editing .msg-reply-content { white-space: normal; }

/* THE BUBBLE MUST STOP SHRINK-TO-FITTING WHILE IT IS BEING EDITED (2026-08-06,
   Cole: "the editing a message UI is still horrible").
   `.msg-reply` is `width: fit-content`, and the editor's textarea is
   `width: 100%` — a percentage resolved against a shrink-to-fit parent is
   CIRCULAR, so the browser falls back to the textarea's intrinsic default
   (~20 cols). Measured at 1280px: opening the editor on a 269-character message
   collapsed its bubble from 738px wide to 234px — 68% narrower — which re-wrapped
   three lines into fourteen, overflowed the 320px height cap, and clipped the
   text. Clipping is the exact complaint the 2026-07-30 pass was meant to end;
   it never did, because that pass fixed the HEIGHT while the WIDTH was quietly
   collapsing. Same circular-percentage class as the in-bubble image sizing bug
   (DR-M8b) — the fix is the same: give the percentage a real width to resolve
   against. `max-width: 76%` still caps it, so this reads "as wide as a bubble is
   allowed to be", not "full bleed". */
.msg-replies-container .msg-reply.is-editing { width: 100%; }
/* An editor is a workspace, not a message: the hover action cluster and the
   Translate pill under an open editor are both clutter and a trap (Recall was
   one hover away from the message you are mid-sentence in). */
.msg-replies-container .msg-reply.is-editing .msg-reply-actions,
.msg-replies-container .msg-reply.is-editing .msg-translate-one { display: none; }

.msg-replies-container .msg-reply textarea.msg-edit-input {
  /* A textarea is inline-block by default, so the line box reserves descender
     space beneath it — that was ~25px of dead green between the text and the
     buttons, which read as the editor being broken rather than styled. */
  display: block;
  background: transparent;
  border: 0;
  border-bottom: 1px solid color-mix(in srgb, currentColor 26%, transparent);
  border-radius: 0;
  color: inherit;
  padding: 0 0 5px;
  box-shadow: none;
}
.msg-replies-container .msg-reply textarea.msg-edit-input:focus {
  outline: none;
  border-bottom-color: color-mix(in srgb, currentColor 50%, transparent);
  box-shadow: none;
}
/* Quiet, inherit-coloured buttons — a filled accent button inside an
   accent-tinted bubble was the loudest thing on the screen. */
.msg-replies-container .msg-reply .msg-edit-actions .btn.msg-edit-btn {
  border: 0;
  background: transparent;
  color: inherit;
  opacity: 0.72;
  font-weight: 600;
  padding: 5px 10px;
  border-radius: 7px;
}
.msg-replies-container .msg-reply .msg-edit-actions .btn.msg-edit-btn:hover {
  opacity: 1;
  background: color-mix(in srgb, currentColor 12%, transparent);
}
.msg-replies-container .msg-reply .msg-edit-actions .btn.btn-primary.msg-edit-btn {
  opacity: 1;
  background: color-mix(in srgb, currentColor 20%, transparent);
}
.msg-replies-container .msg-reply .msg-edit-actions .btn.btn-primary.msg-edit-btn:hover {
  background: color-mix(in srgb, currentColor 30%, transparent);
}
.msg-replies-container .msg-reply .msg-edit-hint { color: inherit; opacity: 0.55; }
/* Attach, inside the editor (2026-08-06). Leads the row rather than sitting with
   Cancel/Save: it acts on the message you are composing, not on the decision to
   keep or drop it, and putting it beside Save is how you get a mis-tap that
   commits an edit you meant to add a photo to. */
.msg-replies-container .msg-reply .msg-edit-actions .btn.msg-edit-attach {
  padding: 5px 7px;
  opacity: 0.62;
  display: inline-flex;
  align-items: center;
}
.msg-replies-container .msg-reply .msg-edit-actions .btn.msg-edit-attach:hover { opacity: 1; }
.msg-replies-container .msg-reply .msg-edit-actions .btn.msg-edit-attach svg { display: block; }
/* The staged-upload tray inherits the composer's chip styling (`.msg-attach-*`),
   so the two read as the same control in both places. It only needs its own
   spacing inside a bubble. */
.msg-replies-container .msg-reply .msg-attach-staging {
  margin-top: 6px;
  gap: 6px;
}
@media (max-width: 768px) {
  /* Thumb-sized, matching the Cancel/Save buttons beside it. */
  #mainContent .msg-edit-attach { min-height: 36px !important; padding: 6px 10px !important; }
}

/* ── Photos and clips hold their box across a repaint (2026-08-06) ───────────
   Cole: "images are still super glitchy, appear and disappear, make us jump up
   and down all over the page … having multiple images in a chat is even more
   glitchy and detrimental to the tab."

   The width/height attributes were supposed to reserve the box and did not.
   `.msg-attach-img { width: auto; height: auto }` above is an AUTHOR rule, and
   the attributes map to a presentational hint, which author rules outrank — so
   both axes computed to `auto` on an element with no intrinsic size yet, and a
   replaced element in that state has no box. Measured: an
   `<img width="410" height="238">` in a bubble laid out at 2x2px (its 1px
   border, nothing more) and snapped to full size on decode. Sixteen photos took
   the conversation from 1059px to 4522px after paint — +3463px, ~217px per
   photo, linear in the number of photos, which is why more of them is worse.

   The renderer now emits an inline `width` + `aspect-ratio` per image, computed
   from the real dimensions and scaled to fit 280x340 as a RATIO (clamping the
   two axes independently is what squashed a 400x1600 photo into a square on
   2026-08-04). Inline beats these rules, so nothing here had to be rewritten —
   what follows only covers what inline styles cannot reach. */

/* 2026-09-14 — download buttons on media bubbles and file chips (Cole: "can be
   downloaded easily"). Top-right on a clip, clear of the native controls bar. */
.msg-attach-video-wrap { position: relative; display: block; max-width: 100%; }
.msg-attach-save,
.msg-attach-file-save {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  cursor: pointer;
  transition: background-color 120ms ease, transform 100ms ease-out;
}
.msg-attach-save {
  position: absolute;
  top: 6px;
  right: 6px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
}
.msg-attach-save:hover { background: rgba(0, 0, 0, 0.72); }
.msg-attach-file-wrap { display: inline-flex; align-items: center; gap: 4px; max-width: 100%; }
.msg-attach-file-save { background: var(--surface-2); color: var(--text); border: 1px solid var(--border); }
.msg-attach-file-save:hover { background: var(--surface-3, var(--surface-2)); }
.msg-attach-save:active,
.msg-attach-file-save:active { transform: scale(0.92); }
.msg-attach-save:focus-visible,
.msg-attach-file-save:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .msg-attach-save:active,
  .msg-attach-file-save:active { transform: none; }
}

/* Video reserved nothing whatsoever — no dimensions, no poster — so every clip
   laid out at the 300x150 default object size and jumped to its real shape when
   metadata arrived. The renderer now sets an inline aspect-ratio (4/3 until the
   clip's real ratio is learned from `loadedmetadata`); `height: auto` is what
   lets that ratio drive the height instead of the default 150px. */
.msg-replies-container .msg-reply-attachments .msg-attach-video {
  display: block;
  height: auto;
  max-width: 100%;
  /* Backstop — the inline width/aspect-ratio set at render time (_msgAttachmentHtml)
     is what actually sizes each clip; this caps it even if that inline style is
     ever missing, so a video can never again lay out at its raw native
     resolution and fill the whole conversation (Cole 2026-08-13, a real clip
     in the Coolbang group chat: "the video is so big and takes everything up"). */
  max-height: 340px;
  background: var(--surface-2);
  opacity: 1;
  filter: none;
}

/* ── Inline task expansion in the group-chat rail (Cole, 2026-08-06) ──────────
   "i just want to be able to click into a task from the sidebar and it drops
   down some details right there … i dont want it to always take me to a whole
   other page", clarified as "the sidebar of a groupchat" — i.e. the Tasks
   section of `.msg-detail-files`, not the conversation list.

   Everything here has to work at 254px. The panel is a SIBLING of
   `.msg-task-rail-item` (a <button> cannot contain controls) and lives inside
   `.msg-task-rail-list`, which is already the section's own scroll region — so
   an expanded task scrolls there and never grows the aside, which must never
   scroll. */
.msg-task-rail-row { display: flex; flex-direction: column; }
.msg-task-rail-row.is-expanded > .msg-task-rail-item {
  border-color: var(--accent-dim);
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  background: var(--surface-2);
}
.msg-task-rail-item { position: relative; }
.msg-task-rail-chevron {
  position: absolute;
  top: 8px;
  right: 7px;
  display: inline-flex;
  color: var(--text-muted);
  /* 2026-09-01 mega-audit: color transition stays (not motion); the transform
     part is gated below so reduced-motion users get an instant flip. */
  transition: color 0.15s ease;
}
@media (prefers-reduced-motion: no-preference) {
  .msg-task-rail-chevron { transition: transform 0.15s ease, color 0.15s ease; }
}
.msg-task-rail-chevron.is-open { transform: rotate(180deg); color: var(--text-secondary); }
.msg-task-rail-item:hover .msg-task-rail-chevron { color: var(--text-secondary); }
/* Leave room for the chevron so a two-line title never runs under it. */
.msg-task-rail-item-title { padding-right: 16px; }

.msg-task-inline {
  display: flex;
  flex-direction: column;
  gap: 9px;
  padding: 10px;
  border: 1px solid var(--accent-dim);
  border-top: 0;
  border-bottom-left-radius: var(--radius-sm);
  border-bottom-right-radius: var(--radius-sm);
  background: var(--surface);
  cursor: default;
}
.msg-task-inline-note {
  margin: 0;
  font-size: 11.5px;
  color: var(--text-muted);
}
.msg-task-inline-retry {
  border: 0;
  background: none;
  padding: 0 0 0 4px;
  color: var(--accent);
  font: inherit;
  font-size: 11.5px;
  cursor: pointer;
  text-decoration: underline;
}
/* The messages under a task are the part people read, so this box gets the
   space and the metadata controls get squeezed (Cole 2026-08-06: "the box to
   see messages sent below a task is too small now"). It is its own scroller so
   a long thread cannot push the controls out of the section. */
.msg-task-inline-detail {
  display: flex;
  flex-direction: column;
  gap: 7px;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--text-secondary);
  min-height: 6em;
  max-height: 15em;
  overflow-y: auto;
  padding: 8px;
  border-radius: 7px;
  background: var(--surface-2);
}
.msg-task-inline-msg { white-space: pre-wrap; word-break: break-word; }
.msg-task-inline-by { font-weight: 700; color: var(--text-muted); margin-right: 4px; }
.msg-task-inline-actions { display: flex; flex-wrap: wrap; gap: 6px; }
.msg-task-inline-actions .btn {
  flex: 1 1 auto;
  justify-content: center;
  font-size: 11.5px;
  padding: 6px 8px;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  white-space: nowrap;
}
/* One column at 254px; two as soon as there is room (Split-View-free wide rails
   and any future widening). */
/* Fixed two-up, not auto-fit: at 222px auto-fit degrades to one field per row,
   which costs five rows of a 254px column and is what starved the message box. */
.msg-task-inline-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
}
.msg-task-inline-field {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.msg-task-inline .msg-task-select {
  min-height: 32px;
  font-size: 11.5px;
  padding: 5px 22px 5px 8px;
  border-radius: 7px;
}
.msg-task-inline .msg-task-context-card { margin: 0; padding: 8px; }
.msg-task-inline-field--wide { grid-column: 1 / -1; }

/* ── Rail section header can carry one action (Add New Task) ─────────────────
   Beside the disclosure toggle, never inside it. */
.msg-rail-section-head { display: flex; align-items: center; }
.msg-rail-section-head .msg-rail-section-toggle { flex: 1; min-width: 0; }
.msg-rail-section-action {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  margin-right: 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text-muted);
  cursor: pointer;
  padding: 0;
}
.msg-rail-section-action:hover { color: var(--text); border-color: var(--accent-dim); background: var(--surface-2); }
.msg-rail-section-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Add New Task form — two VISIBLE fields; a single field with a clever
   placeholder shipped once already and produced title-only rows. */
.msg-rail-compose {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0 2px 10px;
  padding: 9px;
  border: 1px solid var(--accent-dim);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
.msg-rail-compose-input { font-size: 12px; padding: 6px 8px; border-radius: 7px; width: 100%; }
textarea.msg-rail-compose-input { resize: vertical; line-height: 1.4; }
.msg-rail-compose-where { margin: 0; font-size: 10px; color: var(--text-muted); }
.msg-rail-compose-actions { display: flex; gap: 6px; }
.msg-rail-compose-actions .btn { flex: 1; justify-content: center; font-size: 11.5px; padding: 6px 8px; }

/* ── Tier 2: the big task view, mounted INSIDE the Messages shell ─────────────
   `position:absolute` within `.msg-shell` (which is already `position:relative`)
   rather than a page-level modal — the page header, the nav and the Messages tab
   itself stay on screen, which is the point: "it expands within the messages
   page still" (Cole 2026-08-06). Leaving Messages is a separate, explicit button. */
.msg-task-modal {
  position: absolute;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  /* 2026-09-01 mega-audit: dropped `backdrop-filter: blur(2px)` — a full-view
     read-back per frame for a barely visible blur; the color-mix scrim already
     does the dimming. */
  background: color-mix(in srgb, var(--bg) 72%, transparent);
}
.msg-task-modal-card {
  display: flex;
  flex-direction: column;
  width: min(860px, 100%);
  max-height: 100%;
  min-height: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-lg, 0 18px 48px rgba(0,0,0,0.28));
  overflow: hidden;
}
.msg-task-modal-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}
.msg-task-modal-titlewrap { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.msg-task-modal-title { margin: 0; font-size: 16px; font-weight: 700; line-height: 1.3; word-break: break-word; }
.msg-task-modal-close {
  flex-shrink: 0;
  border: 0; background: none; color: var(--text-muted); cursor: pointer;
  padding: 4px; border-radius: 6px; display: inline-flex;
}
.msg-task-modal-close:hover { color: var(--text); background: var(--surface-2); }
.msg-task-modal-body {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: 1fr 260px;
  gap: 0;
}
.msg-task-modal-stream {
  min-height: 0;
  overflow-y: auto;
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.msg-task-modal-msg { font-size: 13px; line-height: 1.55; }
.msg-task-modal-msg-head { display: flex; gap: 8px; align-items: baseline; font-size: 11px; color: var(--text-muted); margin-bottom: 3px; }
.msg-task-modal-msg-head strong { color: var(--text-secondary); font-size: 12px; }
.msg-task-modal-msg-body { white-space: pre-wrap; word-break: break-word; color: var(--text); }
.msg-task-modal-side {
  min-height: 0;
  overflow-y: auto;
  padding: 14px 16px;
  border-left: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 12px;
  background: var(--bg);
}
/* Roomier controls than the rail — there is width here. */
.msg-task-inline-grid--modal { grid-template-columns: 1fr 1fr; gap: 10px; }
.msg-task-inline-grid--modal .msg-task-select { min-height: 36px; font-size: 12.5px; }
.msg-task-modal-foot {
  display: flex;
  gap: 8px;
  padding: 12px 16px;
  border-top: 1px solid var(--border);
}
.msg-task-modal-foot .btn { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; padding: 7px 14px; }
.msg-task-modal-full { margin-left: auto; }
@media (max-width: 900px) {
  .msg-task-modal { padding: 12px; }
  .msg-task-modal-body { grid-template-columns: 1fr; grid-template-rows: 1fr auto; }
  .msg-task-modal-side { border-left: 0; border-top: 1px solid var(--border); }
}

/* A quoted source can live outside the paginated stream. Keep that exact
   message/photo available in a focused view instead of silently ignoring the
   quote click or destroying the reader's current scroll position. */
.msg-quoted-source-modal {
  position: fixed;
  inset: 0;
  z-index: 1450;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  /* 2026-09-01 mega-audit: dropped the blur(3px) backdrop-filter pair — the
     color-mix scrim carries the visual; the filter cost a compositor read-back
     of the whole view every frame. */
  background: color-mix(in srgb, var(--bg) 76%, transparent);
}
.msg-quoted-source-card {
  width: min(560px, 100%);
  max-height: min(720px, calc(100dvh - 40px));
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-lg, 0 18px 48px rgba(0,0,0,.28));
}
.msg-quoted-source-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}
.msg-quoted-source-head > div { flex: 1; min-width: 0; }
.msg-quoted-source-head h2 { margin: 0; font-size: 16px; line-height: 1.3; }
.msg-quoted-source-head p { display: flex; flex-wrap: wrap; gap: 5px 10px; margin: 5px 0 0; font-size: 11px; color: var(--text-muted); }
.msg-quoted-source-head strong { color: var(--text-secondary); }
.msg-quoted-source-close {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 36px;
  min-height: 36px;
  padding: 6px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}
.msg-quoted-source-close:hover { background: var(--surface-2); color: var(--text); }
.msg-quoted-source-body { min-height: 0; overflow: auto; padding: 16px; }
.msg-quoted-source-text { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 14px; line-height: 1.55; color: var(--text); }
.msg-quoted-source-body .msg-reply-attachments { margin-top: 12px !important; }
.msg-quote.is-loading { opacity: .72; cursor: progress; }
@media (max-width: 600px) {
  .msg-quoted-source-modal { align-items: flex-end; padding: 10px; }
  .msg-quoted-source-card { max-height: min(78dvh, 680px); border-radius: 16px; }
}

/* ── Chat details: people + presence, and Message info ──────────────────────
   Cole 2026-08-26: "let us see details on the chat like everyone who is in it
   on the far sidebar as another button around media links and docs" / "show
   when each person was last online" / "let us see everyone who has seen a
   message". Both lists share one row shape so the two panels read as one
   feature rather than two designs. */
.msg-info-people { display: flex; flex-direction: column; gap: 2px; }

.msg-info-person-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 2px;
  min-width: 0;
}

.msg-info-person-main {
  display: flex;
  flex-direction: column;
  min-width: 0;
  gap: 1px;
}

.msg-info-person-name {
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Presence is a STATEMENT about a person, so it never guesses: a member with no
   presence record (warehouse kiosk logins are excluded from /api/presence)
   reads as unknown, not as offline. */
.msg-presence { display: inline-flex; align-items: center; gap: 5px; }
.msg-presence::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--text-dim);
  opacity: .45;
  flex: 0 0 auto;
}
.msg-presence--online::before { background: var(--green, #2e9e5b); opacity: 1; }
.msg-presence--away::before { background: var(--text-dim); opacity: .6; }

/* The receipt mark is a control on every surface that has no hover. */
.msg-receipt.is-tappable { cursor: pointer; }
.msg-receipt.is-tappable:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

.msg-seenby-quote {
  font-size: 12.5px;
  color: var(--text-muted);
  border-left: 2px solid var(--border);
  padding-left: 10px;
  overflow-wrap: anywhere;
}

/* ── Loading a conversation ─────────────────────────────────────────────────
   Reuses the app-wide shimmer, but in the shape of the thing being loaded:
   bubbles on the side they will actually arrive on, at plausible widths. Three
   identical full-width grey slabs read as a broken table, not as a chat that
   is one moment from appearing. The header above this is NOT grey — it carries
   the avatar and name the list row already knew. */
.msg-skel-stream {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 18px 22px;
  min-height: 0;
  flex: 1;
}

/* 2026-09-01 mega-audit: was the shared skeletonShimmer pattern — animating
   background-position over an 800px-wide gradient, which repaints the raster
   every frame. Same visual rebuilt as a ::after highlight band swept with
   transform: translateX(), which stays on the compositor. The shared keyframes
   in modern-theme.css are untouched (other views still use them); the local
   msgSkelSweep keyframes below are the transform equivalent. */
.msg-skel-bubble {
  height: 38px;
  border-radius: 14px;
  background: var(--surface-2);
  opacity: 0.85;
  position: relative;
  overflow: hidden;
}
.msg-skel-bubble::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent 25%,
    var(--bg-hover, var(--surface-3)) 50%,
    transparent 75%
  );
  transform: translateX(-100%);
  animation: msgSkelSweep 1.6s ease-in-out infinite;
}
@keyframes msgSkelSweep {
  from { transform: translateX(-100%); }
  to { transform: translateX(100%); }
}

/* Holds the real header's control cluster's footprint so the title does not
   shift sideways when the loaded header replaces this one. */
.msg-skel-actions {
  display: inline-flex;
  gap: 8px;
  flex: 0 0 auto;
}

.msg-skel-actions > span {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--surface-2);
  opacity: 0.6;
}

.msg-skel-bubble--in { align-self: flex-start; border-bottom-left-radius: 5px; }
.msg-skel-bubble--out { align-self: flex-end; border-bottom-right-radius: 5px; }

/* A shimmer is decorative motion, not information — it is exactly the kind of
   slow looping oscillation reduced-motion asks us to drop. The bubbles stay,
   so the shape of what is coming is still communicated. */
@media (prefers-reduced-motion: reduce) {
  /* 2026-09-01 mega-audit: the sweep now lives on ::after. */
  .msg-skel-bubble::after { animation: none; }
}

/* ── A message that arrives while you are watching should LAND ──────────────
   Transform + opacity only, so it composites on the GPU and cannot force a
   layout pass in the middle of a scroll. Applied only to messages that appear
   after the conversation is already on screen — see _msgFreshReplyIds: a first
   paint animating twenty bubbles at once is noise, one arriving mid-read is
   the point. */
@keyframes msgReplyIn {
  from { opacity: 0; transform: translateY(6px) scale(0.985); }
  to { opacity: 1; transform: none; }
}

.msg-reply.is-arriving {
  animation: msgReplyIn 0.16s cubic-bezier(0.22, 0.61, 0.36, 1);
}

@media (prefers-reduced-motion: reduce) {
  .msg-reply.is-arriving { animation: none; }
}

/* The edited marker. Quiet, and the same size as the timestamp it sits beside —
   it is a footnote about the message, not a second timestamp. */
.msg-reply-edited {
  font-size: 11px;
  color: var(--text-muted);
  font-style: italic;
  flex: 0 0 auto;
  /* The row's own gap closes to 3px on a phone, where a narrow bubble puts this
     directly against the timestamp — "edited7:41 AM". */
  margin-right: 3px;
}

/* ── Swipe back to the list ─────────────────────────────────────────────────
   The conversation follows the finger from the left edge (js/views/messages.js
   _msgBindSwipeBack). While it is moving it must not also be trying to animate
   its opacity, and it must not fight the browser for the gesture. */
.msg-mobile-detail.is-swiping-back {
  transition: none !important;
  will-change: transform;
  /* A drag that has been claimed must not also scroll the page underneath. */
  touch-action: pan-y;
  /* The list is revealed from under the leading edge — a hairline of shadow is
     what makes the conversation read as a sheet sliding off the top of it
     rather than a rectangle that is simply moving. */
  box-shadow: -12px 0 28px rgba(22, 19, 15, 0.18);
}

@media (prefers-reduced-motion: reduce) {
  /* The gesture itself stays — it is direct manipulation, not decoration, and
     removing it would remove a way back. Only the shadow (a depth cue, not a
     control) goes. */
  .msg-mobile-detail.is-swiping-back { box-shadow: none; }
}

/* ── Press feedback on the message menu ─────────────────────────────────────
   The rows had `:hover` only, so on the surface where this menu is used most —
   a long-press sheet on a phone — pressing an item gave nothing back until the
   action completed. A background change is the right signal for a full-width
   row; the global `scale: .97` is not, because a row that shrinks away from
   both its edges reads as the sheet flinching rather than the row responding.
   (Cole 2026-08-27: "make buttons, and the ... more smooth, fast and
   responsive".) */
.msg-bubble-menu-item:active {
  background: var(--surface-2);
}

.msg-bubble-menu-item.is-danger:active {
  background: var(--red-dim);
}

.msg-bubble-menu.is-sheet .msg-bubble-menu-item:not(:disabled):active {
  scale: 1;
  background: var(--surface-2);
}

/* ── The long-press hint ────────────────────────────────────────────────────
   Held-but-not-yet-open. Without it, 380ms of holding a finger on a message
   looks exactly like the press being ignored — the interface should telegraph
   the menu it is about to produce (Apple: hint in the direction of the
   gesture). Transform + filter only, so it composites and cannot reflow the
   stream under the finger. */
.msg-reply.is-pressing {
  transform: scale(0.985);
  transition: transform 0.18s ease-out;
  /* 2026-09-01 mega-audit: promote before the long-press menu work starts so
     the held bubble animates on its own layer. */
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .msg-reply.is-pressing { transform: none; }
}

/* ── Small-text contrast on the Messages surface ────────────────────────────
   Measured, not guessed: each of these was under the 4.5:1 that text below
   ~18px needs, on the surface where Messages is actually read — a phone, one
   handed, in whatever light the room has. `--msg-meta` is the role that
   already carries the bubble timestamp at 5.3:1, so these are being brought
   onto an existing role rather than given a new colour.

   The ⋯ is included deliberately. It is the ONLY control on a message on a
   phone (long-press and the ⋯ are the same menu), so a glyph at 4.28:1 is a
   control nobody can see, not a decoration. */
.msg-reply-edited { color: var(--msg-meta); }
.msg-replies-container .msg-reply-actions button { color: var(--msg-meta); }

/* The in-flight state of the history header. Quiet — it is a progress report on
   something the reader did not ask for (the prefetch runs a screen and a
   quarter ahead of them), so it must not read as an error or an action. */
.msg-load-older-busy {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 12px;
  font-weight: 600;
  color: var(--msg-meta);
  min-height: 32px;
}

.msg-load-older-spinner {
  width: 11px;
  height: 11px;
  border: 1.5px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: msgLoadOlderSpin 0.7s linear infinite;
}

@keyframes msgLoadOlderSpin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .msg-load-older-spinner {
    animation: none;
    border-right-color: currentColor;
    opacity: 0.55;
  }
}

/* ── Offline ────────────────────────────────────────────────────────────────
   A conversation with no network is otherwise indistinguishable from a quiet
   one. Amber, not red: nothing has gone wrong and nothing has been lost — the
   messages are queued and send themselves when the connection returns. */
.msg-offline-strip {
  flex: 0 0 auto;
  padding: 7px 16px;
  font-size: 12px;
  font-weight: 600;
  text-align: center;
  color: var(--yellow-strong, var(--text));
  background: var(--yellow-dim, var(--surface-2));
  border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}

/* A send waiting on the network is not a failure — it needs no alarm colour and
   no urgency, only an honest label and the same tap-to-retry escape hatch. */
.msg-reply-retry.is-waiting {
  color: var(--msg-meta);
  background: transparent;
}

/* 2026-09-01 mega-audit: screen-reader-only box for #msgLiveAnnouncer — the
   aria-live region moved off the replies container (which is replaced
   wholesale per repaint and so announced the whole thread as "additions"). */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* 2026-09-01 mega-audit polish: in dark mode the paper and the bubble surfaces
   sit close in luminance, so bubbles lose their edge. A soft drop + hairline
   inner ring restores separation. This stylesheet's dark convention is the
   explicit `:root[data-theme="dark"]` hook only (no prefers-color-scheme
   twins exist anywhere in the file — the dashboard stamps the attribute). */
:root[data-theme="dark"] .msg-replies-container .msg-reply {
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), inset 0 0 0 1px rgba(255, 255, 255, 0.05);
}

/* ── 2026-09-02 desktop CSS audit ──────────────────────────────────────────
   1. Text painted ON a solid status colour. The dark palette LIGHTENS
      --accent/--red/--purple (#c99079, #f87171, #a78bfa) but the foreground
      stayed #fff — measured 2.7:1 for every unread count and the send button.
      The file already knew the fix for avatars (5113); this applies the same
      inverse to every solid chip. `--on-solid` is the one token to reach for. */
:root { --on-solid: #fff; --msg-convo-wash: rgba(255, 255, 255, 0.09); }
:root[data-theme="dark"] {
  --on-solid: #181816;
  /* 8. the top-of-thread wash was a white gradient in both themes — in dark it
     lifted the top quarter to a grey band with no twin at the bottom. */
  --msg-convo-wash: color-mix(in srgb, var(--text) 5%, transparent);
}
:root[data-theme="dark"] .msg-send-detached,
:root[data-theme="dark"] .msg-jump-count,
:root[data-theme="dark"] .msg-thread-unread-badge,
:root[data-theme="dark"] .msg-thread-section-unread,
:root[data-theme="dark"] .msg-channel-unread-pill,
:root[data-theme="dark"] .msg-split-head-unread,
:root[data-theme="dark"] .msg-split-group-unread,
:root[data-theme="dark"] .msg-filterbar-badge,
:root[data-theme="dark"] .msg-member-avatar,
:root[data-theme="dark"] .msg-chat-row-unread,
:root[data-theme="dark"] .msg-team-tab-badge,
:root[data-theme="dark"] .msg-task-avatar,
:root[data-theme="dark"] .msg-tpill.msg-turgent,
:root[data-theme="dark"] .msg-reply-jarvis .msg-reply-author::after,
:root[data-theme="dark"] .msg-jarvis-confirm,
:root[data-theme="dark"] .im-msg-banner-avatar,
:root[data-theme="dark"] .msg-voice-stop,
:root[data-theme="dark"] .msg-mobile-new { color: var(--on-solid); }

/* 2. Messages' own modals sat an order of magnitude BELOW the ambient chrome
      that floats over them (#imActivityBar 9998, the dock launcher 8900):
      the activity strip painted across the top of the big task view and the
      quoted-source modal. Lift them into the side-panel band; the lightbox
      (9985) still opens above them and the toast (9999) above everything. */
.msg-task-modal,
.msg-camera-overlay,
.msg-quoted-source-modal,
.msg-seko-bol-backdrop { z-index: calc(var(--z-side-panel, 8950) + 6) !important; }
/* 6. lightbox / annotator / new-message banner all tied at 9999 and resolved
      by insertion order — a banner arriving mid-photo landed INSIDE the
      lightbox's dark field. One step under the toast band, above the rest. */
.msg-img-lightbox { z-index: calc(var(--z-toast, 9999) - 14) !important; }   /* 9985 */
.im-annot { z-index: calc(var(--z-toast, 9999) - 13) !important; }           /* 9986 */

/* 10. an unbroken display name (an email, a long handle) overflowed the
       bubble; every other name in the file already wraps or ellipsises. */
.msg-reply-author { overflow-wrap: anywhere; }

/* Long spec values in the SKU / IMC drawer: four lines, then an ellipsis —
   the full text rides in the title and the Price Menu holds the read. */
.msg-sku-cell-v.is-long {
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: 12.5px;
  font-weight: 500;
  line-height: 1.35;
}

/* 2026-09-02 rail audit: the read/unread toggle revealed on hover only —
   keyboard focus landed on a control that was not drawn. */
.msg-thread-readtoggle:focus-visible { opacity: 1; }

/* ── 2026-09-02 motion audit ──────────────────────────────────────────────── */
/* Swipe-to-reply: the ↩ is the arm state, and it is visible — the haptic
   alone told an iPhone nothing. Transform-only, spring-ish, reduced-motion safe. */
.msg-replies-container .msg-reply.is-swiping::before {
  transition: transform 120ms var(--ease-standard, ease), color 120ms var(--ease-standard, ease), opacity 120ms ease;
  opacity: 0.7;
}
.msg-replies-container .msg-reply.is-armed::before {
  transform: translateY(-50%) scale(1.3);
  color: var(--accent);
  opacity: 1;
}
/* A reaction chip that just arrived. */
@keyframes msgReactionIn { from { transform: scale(0.6); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.msg-reaction-chip.is-new { animation: msgReactionIn 160ms var(--ease-spring, cubic-bezier(0.34, 1.3, 0.64, 1)) both; }
/* The composer row answers on pointer-down (composer audit #1). */
.msg-attach-btn:active, .msg-composer-more-toggle:active, .msg-reply-composer .send-btn:active,
.msg-emoji-btn:active, .msg-fmt-btn:active { scale: 0.94; }
@media (prefers-reduced-motion: reduce) {
  .msg-replies-container .msg-reply.is-swiping::before, .msg-replies-container .msg-reply.is-armed::before { transition: none; transform: translateY(-50%); }
  .msg-reaction-chip.is-new { animation: none; }
  .msg-attach-btn:active, .msg-composer-more-toggle:active, .msg-reply-composer .send-btn:active,
  .msg-emoji-btn:active, .msg-fmt-btn:active { scale: 1; }
}

/* ── 2026-09-02 stream + visual audits ─────────────────────────────────────── */
/* The "seen by everyone" check sits on the dark own bubble; --blue on #075e54
   was the one meta glyph never remeasured. */
.msg-reply-own .msg-receipt-seen, :root[data-theme="dark"] .msg-receipt-seen { color: #53bdeb; }
/* Day separators: one object on both surfaces — the dock's pill. */
.msg-day-sep::before, .msg-day-sep::after { display: none; }
.msg-day-sep span {
  display: inline-block; padding: 3px 10px; border-radius: 999px;
  background: var(--surface-2); color: var(--text-muted);
  font-size: 10.5px; font-weight: 600; letter-spacing: 0.02em; text-transform: none;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
/* Pinned strip: one pin, the rest behind a disclosure. */
.msg-pinned-more > summary { cursor: pointer; font-size: 11px; color: var(--text-muted); padding: 2px 0 4px; list-style: none; }
.msg-pinned-more > summary::-webkit-details-marker { display: none; }
.msg-pinned-more > summary::before { content: "＋ "; }
.msg-pinned-more[open] > summary::before { content: "－ "; }

/* Composer key hint (desktop only; fades once you type). */
.msg-composer-hint {
  display: block; font-size: 10.5px; color: var(--text-muted); opacity: 0.7;
  padding: 2px 12px 0; letter-spacing: 0.02em;
}
.msg-reply-composer.msg-composer--typing .msg-composer-hint, .msg-reply-composer:focus-within .msg-composer-hint { opacity: 0; height: 0; padding: 0; overflow: hidden; }
/* Per-file upload progress on a staged photo. */
.msg-attach-chip--img .msg-attach-pct {
  position: absolute; inset: auto 0 0 0; text-align: center; font-size: 10px; font-weight: 700;
  color: var(--on-solid); background: rgba(0, 0, 0, 0.55); padding: 1px 0; border-radius: 0 0 10px 10px;
}
.msg-attach-chip--img.is-uploading img { opacity: 0.6; }

/* ── 2026-09-02 photo-flow audit ───────────────────────────────────────────── */
/* Save a photo out of the app — there was no route to a claims workbook. */
.msg-img-lightbox-save {
  position: absolute;
  top: 16px;
  right: 72px;
  width: 44px;
  height: 44px;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.msg-img-lightbox-save:hover { background: rgba(255, 255, 255, 0.28); }
/* Copy-for-Claude sits beside Save and inherits its whole box, so the two
   controls cannot drift apart; only the offset differs (2026-09-05). */
.msg-img-lightbox-ref {
  position: absolute;
  top: 16px;
  right: 124px;            /* close 20 → save 72 → this 124: one 44px step + 8 */
  width: 44px;
  height: 44px;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.msg-img-lightbox-ref:hover { background: rgba(255, 255, 255, 0.28); }
.msg-img-lightbox-ref:active { scale: 0.96; }
.msg-img-lightbox-ref:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .msg-img-lightbox-ref:active { scale: 1; } }
.msg-img-lightbox-save:active { scale: 0.96; }
.msg-img-lightbox-save:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
/* A staged photo that carries mark-up says so — the flag was written and read
   nowhere, so one edited photo among six was indistinguishable. */
.msg-attach-chip--img.is-marked img { outline: 2px solid var(--accent); outline-offset: -2px; }
.msg-attach-chip--img.is-marked .msg-attach-edit { background: var(--accent); color: var(--on-solid); }
/* A cropped thumbnail admits there is more of the photo. */
.msg-attach-img[data-cropped="1"] { cursor: zoom-in; }
.msg-attach-chip--img.is-marked, .msg-attach-img[data-cropped="1"] { position: relative; }
/* The photo a quote named, when its source opens in the modal. */
.msg-quoted-source-body .is-quoted-target {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 10px;
}
@media (prefers-reduced-motion: reduce) { .msg-img-lightbox-save:active { scale: 1; } }

/* 2026-09-02 audit: the split composers had no drag-and-drop at all, so a
   dropped file hit the browser default and NAVIGATED THE TAB AWAY — both panes
   and both drafts gone. This is the cue for the handler that now catches it. */
.msg-reply-composer.is-drop-target {
  outline: 2px dashed var(--accent);
  outline-offset: -4px;
  background: color-mix(in srgb, var(--accent) 6%, transparent);
}

/* ── Search scope chips + the matched-message preview (Cole, 2026-09-04) ─────
   "i want to be able to search for a specific keywords for certain items
   filtered by tasks or messages too (or in the everything filter, then
   everything applies)" — and, on the preview: "the preview message is a really
   old message that has nothing to do with Chrome Echo."
   The chips narrow the results already on screen; the `match` tag and the mark
   say WHICH message matched, which is what the row could never show before. */
.msg-search-scope-row {
  display: flex;
  gap: 6px;
  padding: 4px 16px 10px;
  flex-wrap: wrap;
}
.msg-search-scope-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* 32px, not 44: this sits directly under the search field and a full-height
     row of chips would push the first result off a phone screen. It is a
     narrowing control, not a primary action. */
  min-height: 32px;
  padding: 5px 11px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text-muted);
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.14s, border-color 0.14s, color 0.14s;
}
.msg-search-scope-chip:hover { background: var(--surface-2); color: var(--text); }
.msg-search-scope-chip.is-on {
  background: var(--accent-dim);
  border-color: var(--accent);
  color: var(--text);
}
.msg-search-scope-count {
  font-variant-numeric: tabular-nums;
  opacity: 0.75;
  font-weight: 500;
}
.msg-search-scope-empty {
  padding: 18px 16px 22px;
  color: var(--text-muted);
  font-size: 12.5px;
}
/* The row's preview is the MATCHED message, not the latest one — labelled so a
   reader is never left guessing why a result appeared. */
.msg-thread-row-hit-tag {
  display: inline-block;
  margin-right: 5px;
  padding: 1px 6px;
  border-radius: 4px;
  background: var(--accent-dim);
  color: var(--text);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  vertical-align: 1px;
}
.msg-search-mark {
  background: var(--accent-dim);
  color: var(--text);
  border-radius: 3px;
  padding: 0 2px;
  font-weight: 600;
}

/* ═══ Vertical density pass (Cole 2026-09-04) ═══════════════════════════════
   Cole, with WhatsApp open beside the Messages tab: "with WhatsApp, we're able
   to see a full page of content, and it doesn't waste a lot of space, so we can
   see way more on a page … make the header at the top of the name of the group
   chat and all just one thin row … make the header that says Messages and New
   Thread, etc., more thin as well."

   Measured at 1512x900 before this pass: page header 45px + 10px margin, chat
   topbar 57px, composer 119px EMPTY, `.main`'s 80px bottom gutter and a shell
   that stopped 69px short of the viewport floor — 523 of 900 pixels reached the
   conversation. Every number below was measured in the browser, not guessed.

   ⚠️ Why this block is at the END of the file and not beside the rules it
   changes. `.msg-page-header`, `.msg-detail-topbar` and the send button are each
   re-declared by a later historical polish pass in this same file — the "Compact
   pass (2026-06-18)" at ~line 3939 and the "Conversation finish pass
   (2026-07-29)" at ~line 6146 — which win purely on source order. Editing the
   original declarations looks right and changes nothing; the first attempt at
   this did exactly that and measured identically. Source order is the mechanism,
   so the override has to live where source order puts it last.

   ⚠️ 901px, not 769px, for anything that shrinks a TAP TARGET. css/mobile.css
   is gated to <=768px in index.html, so it cannot protect a 769-900px touch
   tablet; ux-polish.css draws the house's touch floor at `max-width: 900px`
   (`.refresh-btn, .bulk-btn, .btn-sm { min-height: 44px }`) and these selectors
   out-specify it. Layout changes (one row instead of three) are safe at 769px
   because they do not shrink anything you have to hit. */

/* ── Page header: "Messages" + New Thread / Split View / Chat Dock ─────────── */
.msg-page-header { padding-bottom: 5px; margin-bottom: 7px; }
.msg-page-header .msg-title { font-size: 15px; }
@media (min-width: 901px) {
  /* modern-theme.css puts a 34px floor under every .refresh-btn; that floor —
     not the padding — is what set this row's height. */
  .msg-header-actions .refresh-btn,
  .msg-header-actions .btn { min-height: 28px; }
}

/* ── The chat-scope row (All chats / Team / Factories / Shipping lanes) ────────
   Cole 2026-09-04, with a screenshot of it spilling over the panel's rounded
   corner: "The filter at the top is overlapping."

   MEASURED: the row was 406px wide inside a 360px column — **60px past the
   surface's right edge**, painting over the conversation beside it.

   The cause is `min-width: auto`, the flex default. `.msg-scope-btn` carries
   `flex: 1 1 0` and looks like it should shrink, but `white-space: nowrap` sets
   its min-content width and a flex item will not shrink below that unless it is
   told it may. So the four buttons kept their full width and the row simply grew
   past its parent. Three parts, in order of what gives first:

   1. Reclaim the padding — 10px -> 6px each side (32px), the label/count gap
      5 -> 4 (4px), and the count's own box 18/5px -> 16/4px (12px). ~48px back
      without touching a single word.
   2. `min-width: 0` so the flexbox can actually do its job, with the LABEL (not
      the button) ellipsizing — the count is the information, the word is only
      its label, and truncation then bites the longest label first and only at
      the narrowest widths.
   3. `overflow: hidden` on the row as the hard stop. Parts 1 and 2 fix today's
      four labels; this is what stops a fifth scope, a longer word or a
      translation from ever painting over the conversation again. */
.msg-scope-row { overflow: hidden; }
.msg-scope-btn {
  /* `flex: 1 1 0` gave all four segments an EQUAL width, so the space "Team" did
     not need was taken from the other three — three truncated labels to keep one
     short word centred. Content width, shrinking only as needed, spends the
     shortfall in proportion to how long each label actually is. */
  flex: 0 1 auto;
  min-width: 0;
  gap: 3px;
  padding-left: 5px;
  padding-right: 5px;
  font-size: 11.5px;
}
.msg-scope-label {
  /* `overflow`/`text-overflow` do not apply to a non-replaced INLINE box, so a
     bare <span> clips at the row's edge with no ellipsis and no warning that a
     word was cut. It has to be a block-level box to truncate. */
  display: block;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.msg-scope-count {
  flex: 0 0 auto;
  min-width: 15px;
  padding: 0 3px;
}
.msg-scope-row { gap: 1px; }

/* ── Chat topbar: the group-chat name row ──────────────────────────────────── */
.msg-detail-topbar { padding: 6px 16px; }

/* ⛔ ONE ROW. Cole 2026-09-04, with a screenshot of the people button sitting on
   a second line by itself: "This should only take up one row. We don't need the
   people icon to take up its entire row. The whole point of this is to save
   space."

   The base rule is `flex-wrap: wrap` + `justify-content: space-between`, so the
   moment the row is one pixel over, the LAST child moves to a new line — and
   with space-between it lands at the far left, which is exactly what the
   screenshot shows. Slimming the padding did nothing about that, and my own
   measurement missed it because the harness I measured was 909px wide with the
   buttons that channel happened to carry; a narrower column, or a room that also
   shows the SEKO BoL or Jarvis-filter button, goes over.

   Wrapping is the wrong answer for this row in any case: everything on it is
   chrome, and the one part that can give — the subtitle listing members — is
   already built to ellipsize. So: never wrap, let the title block absorb the
   slack, and let the subtitle truncate before anything moves.

   Scoped by :has() to the CHAT topbar. The task-board topbar shares the class and
   carries real text buttons ("New task", "New urgent task", "Mark all read") that
   genuinely do need to wrap on a narrow column rather than overflow. */
.msg-detail-topbar:has(.msg-chat-detail-title) {
  flex-wrap: nowrap;
  justify-content: flex-start;
}
.msg-detail-topbar > .msg-chat-detail-title {
  flex: 1 1 auto;
  min-width: 0;
}
.msg-detail-topbar:has(.msg-chat-detail-title) > :not(.msg-chat-detail-title) {
  flex: 0 0 auto;
}
/* Inside the title block: the name holds, the subtitle is what gives. Both need
   `min-width: 0` or a flex item refuses to shrink below its content and the
   ellipsis never engages. */
.msg-chat-detail-title > .msg-chat-detail-copy { flex: 0 1 auto; min-width: 0; }
.msg-chat-detail-copy > :first-child { flex: 0 0 auto; }
.msg-chat-detail-copy > .msg-convo-sub { flex: 0 1 auto; min-width: 0; }
/* The member discs are the last thing worth losing — they say who is in the room
   at a glance — so they hold their size and the names truncate first. */
.msg-chat-detail-title > .msg-chat-participants-slot { flex: 0 0 auto; }

/* One row of ragged controls — 26, 27, 28, 30, 31 and 32px — sets the row's
   height by whichever happens to be tallest, so trimming any one of them moves
   nothing. Give them ONE height. 28px + the 6px/6px padding + the hairline is a
   41px row, against 45 before and 87-128 while it was still wrapping.
   901px for the same reason as everything else in this pass: below it these are
   touch targets that css/mobile.css cannot reach. */
@media (min-width: 901px) {
  .msg-detail-topbar:has(.msg-chat-detail-title) > button,
  .msg-detail-topbar:has(.msg-chat-detail-title) > .btn {
    min-height: 28px;
    height: 28px;
    padding-top: 0;
    padding-bottom: 0;
  }
}
@media (min-width: 901px) {
  /* Same shape as the page header: `.msg-detail-topbar .btn { min-height: 36px }`
     from the 2026-07-29 pass was the real floor, so 6px padding alone moved the
     row by nothing. */
  .msg-detail-topbar .btn { min-height: 30px; }
  /* ⛔ NOT shrinking `.msg-detail-back`. It is hidden above 1100px, so the only
     widths it is visible at are 901-1099 — where the full-screen conversation
     overlay is in play and the device may well be a touch tablet css/mobile.css
     cannot reach. It buys nothing anyway: the topbar's height is set by the
     button floor above, measured. */
}

/* ── Composer: the send button, sized against a one-row pill ───────────────── */
@media (min-width: 901px) {
  /* 44px was sized against the three-row pill. Against a one-row pill it becomes
     the tallest thing in the composer and sets the floor by itself. `min-height`
     is here as well as `height` because `.msg-reply-composer .send-btn` carries
     a 38px min-height that would otherwise hold the button open. */
  .msg-composer-row .msg-send-detached {
    width: 36px;
    height: 36px;
    min-height: 36px;
  }
  .msg-composer-row .msg-send-detached svg { width: 15px; height: 15px; }
  /* 7px of vertical padding around a 21.75px line box is what makes an empty
     one-line textarea 36px tall. The autogrow floor in js/views/messages.js
     follows this number, not the other way round. */
  .msg-composer-row .msg-reply-composer-inner > textarea { padding: 4px 8px; }
}

/* ── Activity drawer (2026-09-05, Cole) ──────────────────────────────────────
   "let's have it just pull up the item in a sidebar instead… so that it doesn't
   take us all the way out of the conversation. We could just flip through the
   different hyperlinked activities and see what's going on."

   A side sheet, not the centred `.msg-task-modal` card: this is a companion to
   the conversation still on screen behind it, and the whole point is that the
   reader has not left. Mounted in #msgShell like the task modal, so it inherits
   the same stacking context and cannot escape the view. */
.msg-actdrawer {
  position: absolute;
  inset: 0;
  z-index: 41;              /* above the task modal's 40 — it can open over one */
  display: flex;
  justify-content: flex-end;
  background: color-mix(in srgb, var(--bg) 60%, transparent);
}
/* The mount of last resort (see _msgActDrawerHost): a scoped Messages mount
   replaced the whole view and there is no shell and no phone overlay to sit
   inside. The parent is not a positioned ancestor, so `inset: 0` would resolve
   against the DOCUMENT and stretch the scrim to the full scroll height —
   fixed pins it to the viewport instead. The z-index has to clear the chat
   dock (8900), which is not inside the view and would otherwise sit on top. */
.msg-actdrawer--fixed {
  position: fixed;
  z-index: 8950;
}
.msg-actdrawer-panel {
  display: flex;
  flex-direction: column;
  width: min(460px, 100%);
  height: 100%;
  min-height: 0;
  border-left: 1px solid var(--border);
  background: var(--surface);
  box-shadow: var(--shadow-lg, -18px 0 48px rgba(0,0,0,0.28));
  overflow: hidden;
  animation: msg-actdrawer-in 160ms ease-out;
}
/* Cole's standing rule for this app: motion respects the OS setting. */
@media (prefers-reduced-motion: reduce) {
  .msg-actdrawer-panel { animation: none; }
}
@keyframes msg-actdrawer-in {
  from { transform: translateX(16px); opacity: 0; }
  to   { transform: translateX(0);    opacity: 1; }
}
.msg-actdrawer-head {
  display: flex; align-items: center; gap: 8px;
  padding: 12px 14px; border-bottom: 1px solid var(--border); flex: 0 0 auto;
}
.msg-actdrawer-title { margin: 0; font-size: 14px; font-weight: 700; flex: 1; min-width: 0; }
.msg-actdrawer-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 14px; }
.msg-actdrawer-foot {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 14px; border-top: 1px solid var(--border); flex: 0 0 auto;
}
.msg-actdrawer-img {
  width: 100%; max-height: 220px; object-fit: contain;
  border: 1px solid var(--border); border-radius: var(--radius-md, 8px);
  background: var(--bg); margin-bottom: 12px;
}
.msg-actdrawer-ref { font-family: var(--mono, ui-monospace, monospace); font-size: 12px; opacity: .8; }
.msg-actdrawer-spec { font-size: 12px; opacity: .85; margin-top: 6px; line-height: 1.5; }
.msg-actdrawer-row { display: flex; gap: 8px; margin-top: 8px; }
.msg-actdrawer-row input { flex: 1; min-width: 0; }
.msg-actdrawer-row select,
.msg-actdrawer-row textarea { flex: 1; min-width: 0; }
/* A write block inside a drawer (quote form, project composer). Separated by a
   rule rather than by spacing alone: it is the one part of the panel that
   CHANGES something, and it should not read as more of the summary above it. */
.msg-actdrawer-write {
  margin-top: 14px;
  border-top: 1px solid var(--border);
  padding-top: 12px;
}
.msg-actdrawer-write-title { font-weight: 600; font-size: 13px; margin-bottom: 6px; }
.msg-actdrawer-write textarea { width: 100%; resize: vertical; }
.msg-actdrawer-sub {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-top: 14px;
}
.msg-actdrawer-tasks,
.msg-actdrawer-msgs { list-style: none; margin: 6px 0 0; padding: 0; font-size: 12.5px; }
.msg-actdrawer-tasks li {
  display: flex;
  gap: 7px;
  align-items: baseline;
  padding: 3px 0;
  line-height: 1.45;
}
/* Done is dimmed, not struck through: a finished task is still the record of
   what was done, and a line through it makes the text harder to read for no
   information gained. */
.msg-actdrawer-tasks li.is-done { opacity: 0.55; }
.msg-actdrawer-tick { flex-shrink: 0; opacity: 0.7; }
.msg-actdrawer-msgs li { padding: 5px 0; border-top: 1px solid var(--border); line-height: 1.45; }
.msg-actdrawer-msgs li:first-child { border-top: 0; }
.msg-actdrawer-who { font-weight: 600; margin-right: 6px; }
.msg-actdrawer-when { opacity: 0.55; margin-left: 6px; white-space: nowrap; }
.msg-actdrawer-quotes { font-size: 12px; margin-top: 10px; }
.msg-actdrawer-quotes li { margin: 2px 0; }

/* The drawer's own controls. `msg-mini` did not exist — a class name that
   resolves to nothing renders an unstyled button that still WORKS, so it would
   have shipped looking broken rather than failing loudly. */
.msg-actdrawer .msg-mini {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 6px 12px;
  font-size: 12px; font-weight: 600; line-height: 1.2;
  color: var(--text);
  background: var(--surface-2, var(--bg));
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 8px);
  cursor: pointer;
  text-decoration: none;
}
.msg-actdrawer .msg-mini:hover:not([disabled]),
.msg-actdrawer .msg-mini:focus-visible { border-color: var(--accent, var(--text)); }
.msg-actdrawer .msg-mini[disabled] { opacity: .45; cursor: default; }
.msg-actdrawer input {
  padding: 6px 10px; font-size: 13px;
  color: var(--text); background: var(--bg);
  border: 1px solid var(--border); border-radius: var(--radius-md, 8px);
}

/* Task + project drawers (Cole 2026-09-14): where it lives, a facts grid with
   people as chips, who can see it, and who a reply will notify. Tokens only, so
   light and dark both follow the theme. */
.msg-actdrawer-where {
  display: flex; align-items: center; gap: 8px; width: 100%;
  margin: 0 0 12px; padding: 8px 10px;
  font: inherit; font-size: 12.5px; color: var(--text); text-align: left;
  background: var(--surface-2, var(--bg));
  border: 1px solid var(--border); border-radius: var(--radius-md, 8px);
  cursor: pointer;
  transition: transform 90ms ease-out, border-color 120ms ease-out;
}
.msg-actdrawer-where-label {
  flex: 1; min-width: 0; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.msg-actdrawer-where:hover { border-color: var(--accent, var(--text)); }
.msg-actdrawer-where:focus-visible,
.msg-actdrawer-more:focus-visible,
.msg-actdrawer-person.is-edit:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
.msg-actdrawer-facts {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  column-gap: 14px; row-gap: 8px;
  align-items: center;
  margin: 0; font-size: 12.5px;
}
.msg-actdrawer-facts dt { color: var(--text-dim); font-weight: 500; }
.msg-actdrawer-facts dd { margin: 0; min-width: 0; }
.msg-actdrawer-person {
  position: relative;
  display: inline-flex; align-items: center; gap: 6px;
  max-width: 100%; min-height: 26px;
  padding: 2px 9px 2px 3px;
  background: var(--surface-2, var(--bg));
  border: 1px solid var(--border); border-radius: 999px;
  vertical-align: middle;
}
.msg-actdrawer-avatar {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 50%;
  font-size: 9.5px; font-weight: 700; letter-spacing: .02em; color: #fff;
}
.msg-actdrawer-avatar.is-empty { background: transparent; border: 1px dashed var(--border); }
.msg-actdrawer-person-name {
  min-width: 0; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.msg-actdrawer-person-role { color: var(--text-dim); font-size: 11px; white-space: nowrap; }
.msg-actdrawer-none { color: var(--text-dim); }
.msg-actdrawer-person.is-edit { padding-right: 6px; cursor: pointer; transition: transform 90ms ease-out, border-color 120ms ease-out; }
.msg-actdrawer-person.is-edit:hover { border-color: var(--accent, var(--text)); }
.msg-actdrawer-person.is-busy { opacity: .6; }
.msg-actdrawer-select {
  -webkit-appearance: none; appearance: none;
  min-width: 0; max-width: 220px;
  padding: 0; margin: 0;
  font: inherit; font-weight: 600; color: var(--text);
  background: transparent; border: 0; outline: none;
  cursor: pointer; text-overflow: ellipsis;
}
.msg-actdrawer-select option { color: var(--text); background: var(--surface); }
.msg-actdrawer-select[disabled] { cursor: progress; }
.msg-actdrawer-caret { pointer-events: none; font-size: 10px; color: var(--text-dim); }
.msg-actdrawer-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.msg-actdrawer-chip {
  display: inline-flex; align-items: baseline; gap: 5px;
  padding: 3px 9px; font-size: 12px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 999px;
}
.msg-actdrawer-chip-k { color: var(--text-dim); font-size: 10.5px; letter-spacing: .04em; text-transform: uppercase; }
.msg-actdrawer-section { margin: 0; padding: 0; }
.msg-actdrawer-people { list-style: none; display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 0; padding: 0; font-size: 12px; }
.msg-actdrawer-more {
  margin-top: 6px; padding: 4px 8px;
  font: inherit; font-size: 12px; font-weight: 600;
  color: var(--accent, var(--text)); background: transparent;
  border: 0; border-radius: 6px; cursor: pointer;
  transition: transform 90ms ease-out, background-color 120ms ease-out;
}
.msg-actdrawer-more:hover { background: var(--accent-dim, var(--surface-2)); }
.msg-actdrawer-notify { margin-top: 5px; font-size: 11.5px; color: var(--text-dim); }
/* Press feedback on pointer-down, for every control a finger lands on. */
.msg-actdrawer-where:active,
.msg-actdrawer-more:active,
.msg-actdrawer-person.is-edit:active,
.msg-actdrawer .msg-mini:active:not([disabled]) { transform: scale(0.97); }
@media (prefers-reduced-motion: reduce) {
  .msg-actdrawer-where, .msg-actdrawer-more, .msg-actdrawer-person.is-edit { transition: none; }
  .msg-actdrawer-where:active,
  .msg-actdrawer-more:active,
  .msg-actdrawer-person.is-edit:active,
  .msg-actdrawer .msg-mini:active:not([disabled]) { transform: none; }
}
@media (pointer: coarse) {
  .msg-actdrawer-person.is-edit { min-height: 44px; padding-left: 8px; }
}

/* Conversation composer: one quiet pill, with capture or send at its edge. */
.msg-jump-latest[hidden], #msgReplySendBtn[hidden] { display: none !important; }
.msg-quick-capture { display: flex; flex: 0 0 auto; gap: 2px; align-self: flex-end; }
.msg-quick-capture button {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%;
  background: transparent; color: var(--text-secondary, var(--text)); cursor: pointer;
}
.msg-quick-capture button:hover { background: var(--surface-hover, var(--surface-2)); }
.msg-quick-capture button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.msg-composer--ready .msg-quick-capture { display: none; }
.msg-composer--recording .msg-quick-capture { display: none; }
.msg-composer-row { align-items: flex-end; }
.msg-composer-row .msg-reply-composer-inner { border-radius: 24px; }
.msg-composer-row .msg-send-detached { width: 44px; height: 44px; min-width: 44px; }
@media (prefers-reduced-motion: reduce) {
  .msg-jump-latest, .msg-send-detached { transition: none; }
}

/* Search stays readable at narrow widths, with honest partial-history states. */
.msg-convo-search { flex-wrap: wrap; gap: 4px; }
.msg-convo-search-input { min-width: 60px; border-radius: 16px; padding: 6px 8px; }
.msg-convo-search-input:focus-visible { box-shadow: inset 0 0 0 1px var(--accent); }
.msg-convo-search-status { flex: 1 1 70%; font-size: 12px; line-height: 1.4; }
.msg-convo-search-status[hidden], .msg-search-retry[hidden] { display: none !important; }
.msg-search-retry { border: 1px solid var(--border); background: var(--surface); color: var(--text); border-radius: 18px; padding: 6px 12px; min-height: 36px; cursor: pointer; }
.msg-convo-search-nav { min-width: 32px; min-height: 32px; justify-content: center; }
.msg-convo-search-nav:disabled { opacity: .35; cursor: default; }
.msg-convo-search-nav:focus-visible, .msg-search-retry:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
@media (pointer: coarse) {
  .msg-convo-search-nav { min-width: 44px; min-height: 44px; }
  .msg-gallery-tab, .msg-search-retry { min-height: 44px; }
}

/* Shared media: clear segmented tabs and separate touch actions from preview. */
.msg-gallery-tabs { padding: 4px; border-bottom: 0; border-radius: 12px; background: var(--surface-2); }
.msg-gallery-tab { border-radius: 9px; min-height: 34px; text-transform: none; letter-spacing: 0; font-size: 12.5px; }
@media (pointer: coarse) {
  .msg-gallery-tab { min-height: 44px; }
  .msg-files-grid { grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); gap: 10px; }
  .msg-files-cell { display: grid; grid-template-columns: 1fr 1fr; gap: 2px; align-content: start; }
  .msg-files-thumb { grid-column: 1 / -1; }
  .msg-files-cell .msg-files-jump, .msg-files-cell .msg-files-ref {
    position: static; width: 100%; height: 44px; opacity: 1; border-radius: 8px;
  }
  .msg-files-cell .msg-files-jump svg, .msg-files-cell .msg-files-ref svg { width: 16px; height: 16px; }
  .msg-files-cell .msg-files-cap { position: static; grid-column: 1 / -1; padding: 4px 2px; }
}

/* Keep preview actions usable above the phone keyboard, including narrow screens. */
@media (pointer: coarse) {
  .msg-actdrawer button, .msg-actdrawer .msg-mini { min-height: 44px; min-width: 44px; }
  .msg-actdrawer input, .msg-actdrawer select, .msg-actdrawer textarea { font-size: 16px; }
  .msg-actdrawer-foot { flex-wrap: wrap; }
  .msg-actdrawer-foot > span:empty { display: none; }
  .msg-actdrawer-foot > a { margin-left: auto; }
}

/* Thread actions can outnumber a phone's available columns. Keep Back fixed
   while the action group scrolls, so status/unread remain reachable without
   adding another header row or shrinking the touch targets. */
@media (max-width: 900px) {
  #mainContent .msg-detail-topbar > div:has(> [data-action="msg-open-info"]) {
    flex: 1 1 auto !important;
    min-width: 0;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
  }
  #mainContent .msg-detail-topbar > div:has(> [data-action="msg-open-info"]) > button {
    flex-shrink: 0;
    min-width: 44px;
    min-height: 44px;
  }
}

/* Messages visual rhythm: quiet structure, readable content, clear actions. */
.msg-shell {
  font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
  font-optical-sizing: auto;
}
.msg-shell .msg-chat-row { min-height: 68px; padding-block: 11px; gap: 12px; }
.msg-shell .msg-chat-row:not(.is-active) { border-bottom-color: color-mix(in srgb, var(--border) 60%, transparent); }
.msg-shell .msg-chat-row-name { letter-spacing: -.012em; line-height: 1.35; }
.msg-shell .msg-chat-row-subline { margin-top: 3px; }
.msg-shell .msg-chat-row-time { font-variant-numeric: tabular-nums; letter-spacing: 0; }
.msg-shell .msg-chat-avatar-team { border-color: color-mix(in srgb, var(--border) 65%, transparent); }
.msg-shell .msg-chat-row-empty { font-style: normal; }
.msg-shell .msg-thread-section-head { text-transform: none; letter-spacing: 0; font-weight: 600; }
.msg-shell .msg-mobile-search-input { border-radius: 12px; background: var(--surface-2); border-color: transparent; }
.msg-shell .msg-mobile-search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent); }
.msg-shell .msg-mobile-filters > summary { border-radius: 12px; font-weight: 600; background: var(--surface); border: 1px solid var(--border); }
.msg-shell .msg-mobile-filters[open] > summary,
.msg-shell .msg-mobile-filters.is-active > summary { background: color-mix(in srgb, var(--accent) 10%, var(--surface)); border-color: var(--accent); }
.msg-shell .msg-mobile-filter-row { border-radius: 14px; box-shadow: 0 8px 28px #0002; }
.msg-shell .msg-mobile-new { border-radius: 12px; }
.msg-detail-empty { gap: 16px; padding: 32px 24px; }
.msg-detail-empty-icon { display: grid; place-items: center; width: 64px; height: 64px; border-radius: 20px; background: var(--surface-2); color: var(--text-secondary); opacity: 1; }
.msg-detail-empty-text { max-width: 28ch; font-size: 14px; line-height: 1.6; text-wrap: balance; }
.msg-detail-shell .msg-detail-title { letter-spacing: -.02em; }
.msg-detail-shell .msg-reply-composer-inner { transition: border-color .12s ease, box-shadow .12s ease; }
.msg-detail-shell .msg-reply-composer-inner:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 10%, transparent); }
@media (hover: hover) {
  .msg-shell .msg-chat-row:not(.is-active):not(.msg-board-row--urgent):hover { background: var(--surface-2); }
}
.msg-shell .msg-chat-row:active { background: color-mix(in srgb, var(--accent) 10%, var(--surface)); }
@media (prefers-reduced-motion: reduce) {
  .msg-detail-shell .msg-reply-composer-inner { transition: none; }
}
@media (prefers-contrast: more) {
  .msg-shell .msg-chat-row:not(.is-active) { border-bottom-color: var(--text-secondary); }
}

/* ─── Reply to an activity line (mig 1302, Cole 2026-09-11) ───────────────────
   "make it so that we can also quote and reply to activity feed updates within
   messages." The line is itself a control (it opens the item), so the reply
   button is its SIBLING in a row that takes the line's place — a button inside
   a button is invalid and swallows the click. Revealed on hover/focus with a
   pointer; always shown, and a full 36px target, on touch where there is no
   hover to find it with. */
.msg-activity-row {
  display: flex;
  align-items: center;
  min-width: 0;
}
.msg-activity-row > .msg-task-activity-line {
  flex: 1 1 auto;
  min-width: 0;
}
.msg-activity-reply {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  margin: 0 10px 0 2px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.15s ease, background-color 0.15s ease, transform 0.1s ease;
}
.msg-activity-row:hover .msg-activity-reply,
.msg-activity-line:hover .msg-activity-reply,
.msg-activity-reply:focus-visible { opacity: 1; }
.msg-activity-reply:hover { background: var(--bg); color: var(--text); }
/* Press feedback lands on pointer-DOWN, not on release. */
.msg-activity-reply:active { transform: scale(0.9); background: var(--bg); }
.msg-activity-line .msg-activity-reply { margin: 0 -4px 0 2px; }
@media (hover: none) {
  .msg-activity-reply { opacity: 0.65; width: 36px; height: 36px; }
}
@media (prefers-reduced-motion: reduce) {
  .msg-activity-reply { transition: none; }
  .msg-activity-reply:active { transform: none; }
}

/* The quote a reply carries when it answered an activity line: the same
   left-rail quote block, marked as an EVENT (clock glyph, dashed rail) so it
   never reads as something a person typed. Rendered as the line's own control
   when the line opens something, so the reset below undoes button/link chrome. */
.msg-quote--activity {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  width: 100%;
  box-sizing: border-box;
  border-top: 0;
  border-right: 0;
  border-bottom: 0;
  border-left-style: dashed;
  color: inherit;
  font: inherit;
  font-size: 12.5px;
  text-align: left;
  text-decoration: none;
}
.msg-quote--activity > svg { flex: 0 0 auto; margin-top: 2px; opacity: 0.6; }
.msg-quote--activity .msg-quote-body { display: flex; flex-direction: column; min-width: 0; }
.msg-quote--activity .msg-quote-text { color: var(--text-muted); }

/* ─── Media rail, 2026-09-11 (Cole) ──────────────────────────────────────────
   "let's not have the images crop out the image. If it's not a perfect square,
   then let's just make the image itself even smaller so that you can still see
   everything within the square … make it more smooth."

   Every rule here is an OVERRIDE of the earlier gallery block, left below it on
   purpose: tests/test_messages_media_gallery_density.py reads that block's
   text, and it still describes the base the rail falls back to. */

/* The whole photo, never a crop. The tile stays square; a wide or tall image
   shrinks to fit inside it and sits on a quiet mat, the way a gallery wall
   hangs a print — every tile the same size, nothing cut off. */
.msg-files-thumb {
  object-fit: contain;
  padding: 5px;
  box-sizing: border-box;
  background: var(--surface-2);
  transition: transform 100ms ease-out, border-color 150ms ease, box-shadow 150ms ease;
}
.msg-files-thumb:hover { border-color: var(--border-hover); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12); }
/* Press feedback lands on pointer-DOWN. */
.msg-files-thumb:active { transform: scale(0.97); }

/* A little more air, and tiles wide enough for the three-button group. */
.msg-files-grid { grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: 10px 8px; }

/* The tile's actions are ONE group — Show in chat · Reply · Copy for Claude —
   in a single opaque pill (no backdrop-filter: one per tile in a scroller is
   the most expensive thing a list item can carry; see the note above). */
.msg-files-actions {
  position: absolute;
  top: 6px;
  right: 6px;
  display: flex;
  gap: 1px;
  padding: 2px;
  border-radius: 9px;
  background: color-mix(in srgb, var(--bg) 92%, var(--surface-2));
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
  opacity: 0;
  transition: opacity 140ms ease;
}
.msg-files-cell:hover .msg-files-actions,
.msg-files-cell:focus-within .msg-files-actions,
.msg-files-row:hover .msg-files-actions,
.msg-files-row:focus-within .msg-files-actions { opacity: 1; }
.msg-files-actions .msg-files-jump,
.msg-files-actions .msg-files-ref,
.msg-files-actions .msg-files-save,
.msg-files-reply {
  position: static;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--text);
  cursor: pointer;
  opacity: 1;
  transition: background-color 120ms ease, transform 100ms ease-out;
}
.msg-files-actions button:hover { background: var(--surface-2); }
.msg-files-actions button:active { transform: scale(0.9); }
.msg-files-actions button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.msg-files-actions button.is-loading { opacity: 0.55; cursor: progress; }

/* Caption: who, then when, on two lines — one line cut both to "Kiki · Yester…". */
.msg-files-cap { display: flex; flex-direction: column; gap: 1px; margin-top: 5px; }
.msg-files-cap-by,
.msg-files-cap-when { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-files-cap-by { font-size: 11px; font-weight: 600; color: var(--text-secondary, var(--text)); }
.msg-files-cap-when { font-size: 10px; color: var(--text-muted); }

/* A document row carries the same two moves as a photo, inline at its end. */
.msg-files-row { display: flex; align-items: center; gap: 6px; min-width: 0; }
.msg-files-row > .msg-files-item { flex: 1 1 auto; min-width: 0; }
.msg-files-actions--row { position: static; flex: 0 0 auto; box-shadow: none; background: transparent; }

/* Tabs glide rather than snap. */
.msg-gallery-tab { transition: background-color 150ms ease, color 150ms ease, transform 100ms ease-out; }
.msg-gallery-tab:active { transform: scale(0.98); }

@media (hover: none) {
  .msg-files-actions { opacity: 1; }
}
@media (pointer: coarse) {
  /* Touch: the group leaves the photo and becomes a row of full 44px targets
     under it, extending the existing coarse layout from two buttons to three. */
  .msg-files-grid { grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); }
  .msg-files-cell .msg-files-actions {
    position: static;
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 2px;
    padding: 0;
    background: transparent;
    box-shadow: none;
    opacity: 1;
  }
  .msg-files-cell .msg-files-actions button,
  .msg-files-row .msg-files-actions button {
    width: 100%;
    min-width: 44px;
    height: 44px;
    border-radius: 8px;
    /* A visible surface: over the page ground a near-bg fill left three bare
       icons with no target shape on a phone. */
    background: var(--surface-2);
    color: var(--text);
  }
  /* Who and when belong to the PHOTO, so they sit directly under it; the
     buttons come after. (DOM order is photo · actions · caption, which is right
     on desktop, where the actions float over the photo instead.) */
  .msg-files-cell .msg-files-cap { order: 1; }
  .msg-files-cell .msg-files-actions { order: 2; margin-top: 2px; }
  .msg-files-actions button svg { width: 16px; height: 16px; }
}
@media (prefers-reduced-motion: reduce) {
  .msg-files-thumb,
  .msg-files-actions,
  .msg-files-actions button,
  .msg-gallery-tab { transition: none; }
  .msg-files-thumb:active,
  .msg-files-actions button:active,
  .msg-gallery-tab:active { transform: none; }
}

/* ══ 2026-09-13 — compact activity, grouped phone sheet, a real edit card ══════
   Cole: "Each activity feed thing is spaced out so much, one below the other",
   "on mobile, when we click the three-dot icons, it's so ugly", and editing "is
   completely whack. The color is still green when we're trying to edit". */

/* An activity line on a day with no separator wears the day inline — on a
   phone, where the stream hides the line's timestamp. Desktop already shows
   "Sep 7, 10:00 AM" there, so a tag beside it would say the day twice. */
.msg-activity-daytag { display: none; }
@media (max-width: 768px) {
  #mainContent .msg-replies-container .msg-task-activity-line--stream .msg-activity-daytag { display: inline; }
}
.msg-activity-daytag {
  flex: none;
  margin-left: 6px;
  color: var(--text-muted);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

/* ── Phone action sheet: stacked cards, not one long list ── */
.msg-bubble-menu-scrim { background: rgba(0, 0, 0, 0.32); }
.msg-bubble-menu.is-sheet {
  gap: 8px !important;
  padding: 0 !important;
  background: transparent !important;
  border: 0 !important;
  border-radius: 0 !important;
  box-shadow: none !important;
  animation: msgSheetRise 0.28s cubic-bezier(0.32, 0.72, 0, 1) !important;
}
.msg-bubble-menu.is-sheet:focus { outline: none; }
@keyframes msgSheetRise {
  from { transform: translateY(100%); }
  to   { transform: none; }
}
.msg-sheet-card {
  flex: none;
  overflow: hidden;
  background: var(--surface);
  border-radius: 14px;
  box-shadow: 0 10px 30px rgba(22, 19, 15, 0.16), 0 1px 2px rgba(22, 19, 15, 0.06);
}
.msg-sheet-preview { padding: 11px 16px 12px; }
.msg-sheet-preview-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}
.msg-sheet-preview-head strong { color: var(--text); font-size: 13px; font-weight: 650; }
.msg-sheet-preview-head span { color: var(--text-muted); font-size: 12px; white-space: nowrap; }
.msg-sheet-preview-body {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-top: 3px;
  color: var(--text-secondary);
  font-size: 14px;
  line-height: 1.4;
  overflow-wrap: anywhere;
}
.msg-bubble-menu.is-sheet .msg-menu-react-row.msg-sheet-card {
  margin: 0 !important;
  padding: 6px 6px !important;
  border-bottom: 0 !important;
}
.msg-sheet-group { display: flex; flex-direction: column; }
.msg-bubble-menu.is-sheet .msg-sheet-group .msg-bubble-menu-item {
  position: relative;
  justify-content: space-between;
  gap: 12px;
  min-height: 50px !important;
  margin: 0 !important;
  padding: 0 16px !important;
  border-radius: 0 !important;
  font-size: 16px !important;
  font-weight: 450;
}
.msg-bubble-menu.is-sheet .msg-sheet-group .msg-bubble-menu-item svg { opacity: 0.5; }
.msg-bubble-menu.is-sheet .msg-sheet-group .msg-bubble-menu-item.is-danger svg { opacity: 0.85; }
.msg-bubble-menu.is-sheet .msg-sheet-group .msg-bubble-menu-item::before { display: none; }
.msg-bubble-menu.is-sheet .msg-sheet-group .msg-bubble-menu-item + .msg-bubble-menu-item::before {
  content: "";
  display: block;
  position: absolute;
  top: 0;
  left: 16px;
  right: 0;
  height: 1px;
  background: var(--border);
}
.msg-bubble-menu.is-sheet .msg-sheet-group .msg-bubble-menu-item:disabled { opacity: 0.45; }
.msg-sheet-cancel {
  min-height: 52px;
  border: 0;
  color: var(--text);
  font: inherit;
  font-size: 16px;
  font-weight: 650;
  cursor: pointer;
}
.msg-sheet-cancel:active { background: var(--surface-2); }
.msg-sheet-cancel:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent); }
@media (prefers-reduced-motion: reduce) {
  .msg-bubble-menu.is-sheet { animation: msgMenuScrimIn 0.14s ease !important; }
}

/* ── Editing: a neutral card that says what it is, not a green bubble ── */
.msg-replies-container .msg-reply.is-editing,
#mainContent .msg-replies-container .msg-reply.is-editing {
  padding: 10px 12px !important;
  background: var(--surface) !important;
  color: var(--text) !important;
  border: 1.5px solid var(--accent) !important;
  border-radius: 14px !important;
  box-shadow: 0 8px 24px rgba(22, 19, 15, 0.08), 0 1px 2px rgba(22, 19, 15, 0.06) !important;
}
.msg-replies-container .msg-reply.is-editing::after,
#mainContent .msg-replies-container .msg-reply.is-editing::after { display: none !important; }
.msg-replies-container .msg-reply.is-editing .msg-reply-author,
.msg-replies-container .msg-reply.is-editing .msg-reply-meta,
.msg-replies-container .msg-reply.is-editing .msg-reply-status,
.msg-replies-container .msg-reply.is-editing .msg-reply-more,
.msg-replies-container .msg-reply.is-editing .msg-quote,
.msg-replies-container .msg-reply.is-editing .msg-reactions { display: none !important; }
.msg-edit-head {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 7px;
  color: var(--accent-strong, var(--accent));
  font-size: 12px;
  font-weight: 650;
  letter-spacing: 0.01em;
  white-space: normal;
}
.msg-edit-head svg { flex: none; }
.msg-replies-container .msg-reply textarea.msg-edit-input,
#mainContent .msg-replies-container .msg-reply textarea.msg-edit-input {
  display: block;
  width: 100% !important;
  box-sizing: border-box;
  padding: 9px 11px !important;
  background: var(--surface-2) !important;
  color: var(--text) !important;
  border: 1px solid var(--border) !important;
  border-radius: 10px !important;
  box-shadow: none !important;
  font-family: inherit !important;
  line-height: 1.45 !important;
  resize: none;
}
.msg-replies-container .msg-reply textarea.msg-edit-input:focus,
#mainContent .msg-replies-container .msg-reply textarea.msg-edit-input:focus {
  outline: none;
  border-color: var(--accent) !important;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent) !important;
}
.msg-replies-container .msg-reply .msg-edit-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 9px;
}
.msg-replies-container .msg-reply .msg-edit-hint {
  margin-right: auto;
  color: var(--text-muted);
  font-size: 11.5px;
  opacity: 1;
}
.msg-replies-container .msg-reply .msg-edit-actions .btn.msg-edit-btn {
  min-height: 34px;
  padding: 6px 13px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--text-secondary);
  opacity: 1;
  font-weight: 600;
}
.msg-replies-container .msg-reply .msg-edit-actions .btn.msg-edit-btn:hover { background: var(--surface-2); }
.msg-replies-container .msg-reply .msg-edit-actions .btn.msg-edit-btn:active { background: var(--surface-3, var(--surface-2)); }
.msg-replies-container .msg-reply .msg-edit-actions .btn.btn-primary.msg-edit-btn {
  padding: 6px 16px;
  background: var(--accent);
  color: #fff;
}
.msg-replies-container .msg-reply .msg-edit-actions .btn.btn-primary.msg-edit-btn:hover {
  background: color-mix(in srgb, var(--accent) 88%, #000);
}
.msg-replies-container .msg-reply .msg-edit-actions .btn.btn-primary.msg-edit-btn:disabled {
  background: var(--surface-2);
  color: var(--text-muted);
  cursor: default;
}
.msg-replies-container .msg-reply .msg-edit-actions .btn.msg-edit-attach {
  padding: 6px 8px;
  color: var(--text-muted);
}

/* The task card's thread now uses the conversation bubbles. */
.msg-task-modal-replies {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: none;
  padding: 4px 2px;
  /* The card's stream is the scroller; a second one inside it traps the wheel. */
  overflow: visible;
}

/* Measured (375px, touch): every activity row was 44px tall because the reply
   button inherits the global 44px touch floor — four one-line updates took
   184px. The button keeps a wide HORIZONTAL hit area and gives the row its
   height back; rows now read as one block. */
.msg-replies-container .msg-activity-row .msg-activity-reply,
#mainContent .msg-replies-container .msg-activity-row .msg-activity-reply {
  position: relative;
  width: 28px !important;
  height: 24px !important;
  min-width: 0 !important;
  min-height: 0 !important;
}
.msg-replies-container .msg-activity-row .msg-activity-reply::after {
  content: "";
  position: absolute;
  inset: -2px -10px;
}
.msg-bubble-menu.is-sheet .msg-sheet-group .msg-bubble-menu-item { min-height: 46px !important; }
.msg-bubble-menu.is-sheet { gap: 7px !important; }
.msg-sheet-preview { padding: 10px 16px 11px; }
.msg-sheet-cancel { min-height: 48px; }
@media (max-width: 768px) {
  #mainContent .msg-replies-container .msg-reply .msg-edit-actions .btn.msg-edit-attach { margin-right: auto; }
}

/* The task a reply belongs to, in a folded room's merged stream (2026-09-14). */
.msg-reply-threadctx {
  display: inline-flex; align-items: baseline; gap: 6px; max-width: 100%;
  margin: 2px 0 6px; padding: 3px 9px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--bg-card-hover, transparent);
  color: var(--text-dim); font: inherit; font-size: 12px; line-height: 1.35;
  cursor: pointer; text-align: left;
  transition: transform .08s ease, background-color .15s ease;
}
.msg-reply-threadctx:hover { color: var(--text); }
.msg-reply-threadctx:active { transform: scale(.97); }
.msg-reply-threadctx-kind { font-weight: 600; text-transform: uppercase; letter-spacing: .04em; font-size: 10.5px; }
.msg-reply-threadctx-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (prefers-reduced-motion: reduce) { .msg-reply-threadctx { transition: none; } .msg-reply-threadctx:active { transform: none; } }
