/* consolidated-comms — Cool Slate B theme
 * Mirrors the Figma Theme variable collection (file tvXD2vnD0aMCLfIMg3fOGi).
 * Dark mode is the default; Light mode applies via [data-theme="light"].
 */

:root,
[data-theme="dark"] {
  /* Surfaces */
  --bg: #0b1120;
  --surface: #162032;
  --surface-alt: #1e293b;
  --border: #324057;
  --border-soft: #232e41;

  /* Text */
  --text: #f2f5f8;
  --text-muted: #9cabc4;
  --text-subtle: #60718e;

  /* Brand */
  --accent: #0ea5e9;
  --accent-hover: #0284c7;
  --accent-soft: #7dd3fc;

  /* States */
  --success: #16a34a;
  --warning: #ca8a04;
  --danger: #dc2626;
  --high: #f7634f;

  /* AI */
  --ai: #8b5cf6;
  --ai-soft: #aa85f8;

  /* Chat bubbles */
  --bubble-in: #2d3342;
  --bubble-out: #0ea5e9;

  /* Highlights / search */
  --highlight: #f2ca4c;

  /* Source colors */
  --src-m365: #4a5bf2;
  --src-icloud: #007aff;
  --src-imessage: #34c759;
  --src-chat: #0ea5e9;
  --src-doc: #8b5cf6;

  /* Categories */
  --cat-prospect: #588df2;
  --cat-personal: #f7634f;

  /* Layout tokens */
  --rail-width: 80px;
  --sidebar-width: 280px;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-xl: 20px;

  /* Type */
  --font-sans: 'Inter', -apple-system, system-ui, sans-serif;
  --font-mono: 'SFMono-Regular', Menlo, Consolas, monospace;
}

[data-theme="light"] {
  --bg: #ffffff;
  --surface: #f8fafc;
  --surface-alt: #eef2f7;
  --border: #d6dde7;
  --border-soft: #e5ebf2;
  --text: #0f172a;
  --text-muted: #475569;
  --text-subtle: #8395ad;
  --bubble-in: #f1f5f9;
  --bubble-out: #0ea5e9;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html, body, #root {
  height: 100%;
  /* Kill the browser's native pull-to-refresh on mobile.
     Why: Jay reported touching the kanban column background and
     dragging down was triggering pull-to-refresh instead of scrolling.
     The kanban column's inner scroller already handles vertical
     scroll inside itself; we just need overscroll at the document
     edge to stop bubbling into the browser chrome. */
  overscroll-behavior-y: none;
}

body {
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.5;
  color: var(--text);
  background: var(--bg);
  font-feature-settings: 'cv11', 'ss01', 'ss03';
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a {
  color: var(--accent);
  text-decoration: none;
}
a:hover { text-decoration: underline; }

button {
  font-family: inherit;
  font-size: inherit;
  cursor: pointer;
  border: 0;
  background: transparent;
  color: inherit;
}

input, textarea {
  font-family: inherit;
  font-size: inherit;
  color: inherit;
  background: transparent;
  border: 0;
  outline: 0;
}

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-soft); border-radius: 6px; }
::-webkit-scrollbar-thumb:hover { background: var(--border); }

/* ===========================================================================
 * Sign-in screen
 * =======================================================================*/

.signin {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
}

.signin-card {
  width: 100%;
  max-width: 440px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  padding: 40px 32px;
  text-align: center;
}

.signin-logo {
  width: 56px;
  height: 56px;
  margin: 0 auto 24px;
  display: grid;
  place-items: center;
  background: var(--accent);
  color: var(--bg);
  font-size: 28px;
  font-weight: 700;
  border-radius: 14px;
}

.signin-title {
  font-size: 22px;
  font-weight: 700;
  margin-bottom: 8px;
}

.signin-sub {
  color: var(--text-muted);
  margin-bottom: 28px;
  font-size: 13px;
}

.signin-actions {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 0 16px;
  height: 44px;
  border-radius: var(--radius-md);
  font-weight: 600;
  font-size: 14px;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}

.btn-primary {
  background: var(--accent);
  color: var(--bg);
}
.btn-primary:hover { background: var(--accent-hover); }

.btn-secondary {
  background: var(--surface-alt);
  border: 1px solid var(--border);
  color: var(--text);
}
.btn-secondary:hover { background: var(--border-soft); }

.btn-ghost {
  background: transparent;
  color: var(--text-muted);
}
.btn-ghost:hover { color: var(--text); }

.signin-foot {
  margin-top: 24px;
  font-size: 11px;
  color: var(--text-subtle);
  letter-spacing: 0.03em;
}

/* ===========================================================================
 * App shell — left rail + sidebar + main pane
 * =======================================================================*/

.app {
  display: grid;
  grid-template-columns: var(--rail-width) var(--sidebar-width) 1fr;
  height: 100vh;
  height: 100dvh; /* dynamic viewport — survives mobile keyboard + URL bar */
}

/* Mobile @media block lives at the END of this file (search "Mobile shell —")
 * — putting it here would be overridden by every desktop rule below since
 * mobile rules + desktop rules have the same specificity and the cascade
 * resolves by source order. Keep this section structural-only.
 */

/* Bottom tab bar (mobile only) — base styles
   ────────────────────────────────────────────────────────────────────
   Position-fixed bottom:0 because three rounds of fighting iOS PWA
   `100dvh` quirks failed (user kept catching empty bands below the
   tab bar). Pinning the tab bar to the literal viewport bottom edge
   bypasses every vh/dvh/lvh weirdness. The tab bar's background +
   padding-bottom: env(safe-area-inset-bottom) extends through the
   home-indicator zone so there's nothing visible below it. Content
   panes get matching padding-bottom so cards aren't hidden behind. */
.mobile-tabbar {
  display: none;
  background: var(--surface);
  border-top: 1px solid var(--border);
  padding-bottom: env(safe-area-inset-bottom, 0);
}

.mobile-tab {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 8px 0 6px;
  color: var(--text-muted);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-decoration: none;
  min-height: 56px;
  position: relative;
  transition: color 0.12s;
}
.mobile-tab:hover { color: var(--text); text-decoration: none; }
.mobile-tab.active { color: var(--accent); }
.mobile-tab.active::before {
  content: '';
  position: absolute;
  top: 0;
  left: 24px;
  right: 24px;
  height: 2px;
  background: var(--accent);
  border-radius: 0 0 2px 2px;
}
.mobile-tab .tab-badge {
  position: absolute;
  top: 4px;
  right: calc(50% - 22px);
  background: var(--high);
  color: var(--bg);
  font-size: 9px;
  font-weight: 700;
  padding: 1px 5px;
  border-radius: 8px;
  min-width: 14px;
  text-align: center;
  line-height: 1.3;
}

/* Unread badge — iMessage-style red pill anchored to the top-right
   of a tab icon. Shared between the mobile tab bar and the desktop
   nav rail. The wrapper .mobile-tab-icon (mobile) and .rail-nav-item
   (desktop) both set position: relative so the badge anchors to the
   icon, not to the whole tab cell (which would push it past the
   label on mobile). */
.mobile-tab-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.unread-badge {
  position: absolute;
  top: -4px;
  right: -8px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: #ff3b30; /* iMessage notification red */
  color: white;
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  box-sizing: border-box;
  border: 2px solid var(--bg);
  pointer-events: none;
  font-variant-numeric: tabular-nums;
}
/* Mobile tab icons sit inside .mobile-tab-icon — push the badge a hair
   farther right so it doesn't kiss the icon. */
.mobile-tab .unread-badge {
  top: -2px;
  right: -10px;
}

/* Back button — visible only on mobile inside main-head when there's somewhere to go */
.mobile-back-btn {
  display: none;
  width: 40px;
  height: 40px;
  border-radius: 8px;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  flex-shrink: 0;
}
.mobile-back-btn:hover { color: var(--text); background: var(--surface-alt); }

/* "More" sheet — opens from More tab, dismissible via backdrop tap */
.mobile-more-sheet {
  position: fixed;
  inset: auto 0 0 0;
  background: var(--surface);
  border-top: 1px solid var(--border);
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  padding: 16px 16px calc(16px + env(safe-area-inset-bottom, 0px));
  z-index: 200;
  box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.5);
  animation: slide-up 0.2s ease-out;
}
@keyframes slide-up {
  from { transform: translateY(100%); }
  to { transform: translateY(0); }
}
.mobile-more-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
}
.mobile-more-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 14px 8px;
  border-radius: 12px;
  background: var(--surface-alt);
  color: var(--text);
  font-size: 11px;
  font-weight: 600;
  text-decoration: none;
  min-height: 80px;
}
.mobile-more-item:hover { background: var(--border-soft); text-decoration: none; }

/* Left rail */
.rail {
  background: var(--surface);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 24px 0;
  gap: 8px;
}

.rail-logo {
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  background: var(--accent);
  color: var(--bg);
  font-weight: 800;
  border-radius: 8px;
  margin-bottom: 24px;
}

.rail-nav {
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 1;
}

.rail-nav-item {
  position: relative;
  width: 32px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: 8px;
  color: var(--text-muted);
  font-size: 16px;
  font-weight: 500;
}
.rail-nav-item:hover { color: var(--text); background: var(--surface-alt); }
.rail-nav-item.active {
  color: var(--text);
  background: var(--surface-alt);
  font-weight: 700;
}
.rail-nav-item.active::before {
  content: '';
  position: absolute;
  left: -16px;
  top: 8px;
  bottom: 8px;
  width: 3px;
  background: var(--accent);
  border-radius: 2px;
}

.rail-user {
  width: 32px;
  height: 32px;
  border-radius: 16px;
  display: grid;
  place-items: center;
  background: var(--cat-prospect);
  color: var(--bg);
  font-weight: 700;
  font-size: 11px;
}

/* Sidebar (channel list / contact list / etc.) */
.sidebar {
  background: var(--bg);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.sidebar-head {
  padding: 16px 16px 8px;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}

.sidebar-title {
  font-size: 16px;
  font-weight: 700;
}

.sidebar-add {
  width: 24px;
  height: 24px;
  border-radius: 6px;
  border: 1px solid var(--border);
  display: grid;
  place-items: center;
  color: var(--text-muted);
  background: var(--surface);
  font-weight: 600;
}
.sidebar-add:hover { color: var(--text); }

.sidebar-section-title {
  padding: 16px 16px 6px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--text-subtle);
  text-transform: uppercase;
}

.sidebar-list {
  flex: 1;
  overflow-y: auto;
  padding: 0 8px 8px;
}

.channel-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-radius: var(--radius-md);
  cursor: pointer;
  font-size: 14px;
  color: var(--text-muted);
  margin-bottom: 1px;
}
.channel-row:hover { background: var(--surface); color: var(--text); }
.channel-row.active { background: var(--surface-alt); color: var(--text); font-weight: 600; }
.channel-row .hash { color: var(--text-subtle); font-weight: 600; }
.channel-row .unread {
  margin-left: auto;
  background: var(--high);
  color: var(--bg);
  font-size: 10px;
  font-weight: 700;
  padding: 2px 6px;
  border-radius: 8px;
}

.dm-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: var(--radius-md);
  cursor: pointer;
}
.dm-row:hover { background: var(--surface); }
.dm-row.active { background: var(--surface-alt); }
.dm-avatar {
  width: 24px;
  height: 24px;
  border-radius: 12px;
  background: var(--cat-prospect);
  color: var(--bg);
  display: grid;
  place-items: center;
  font-size: 10px;
  font-weight: 700;
}
.dm-name { font-size: 13px; }

/* Main pane */
.main {
  display: flex;
  flex-direction: column;
  background: var(--bg);
  overflow: hidden;
}

.main-head {
  padding: 14px 24px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 12px;
}
.main-title { font-size: 16px; font-weight: 700; }
.main-sub { color: var(--text-muted); font-size: 12px; }
.main-actions { margin-left: auto; display: flex; gap: 6px; }

/* Chat pane */
.chat-feed {
  flex: 1;
  overflow-y: auto;
  padding: 16px 24px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* WebSocket connection-state dot for the chat header.
   green=live, amber pulse=reconnecting, red=offline, grey=connecting.
   Without this the user has no signal that real-time messaging is up
   versus silently degraded to 5s polling. */
.ws-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-subtle);
  transition: background 0.2s;
  flex-shrink: 0;
}
.ws-dot[data-state='live']         { background: #22c55e; }
.ws-dot[data-state='reconnecting'] { background: #eab308; animation: ws-pulse 1.2s ease-in-out infinite; }
.ws-dot[data-state='offline']      { background: #ef4444; }
.ws-dot[data-state='connecting']   { background: var(--text-subtle); animation: ws-pulse 1.2s ease-in-out infinite; }
@keyframes ws-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}

/* Floating X close button on large detail modals (message detail, doc
   detail). The Close button at the bottom is preserved for clarity, but
   desktop users expect the corner X — without it modals feel locked. */
.modal-close-x {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  color: var(--text-muted);
  border: 1px solid transparent;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
  z-index: 1;
}
.modal-close-x:hover, .modal-close-x:focus {
  color: var(--text);
  background: var(--bubble-in);
  border-color: var(--border);
  outline: none;
}

/* Pull-to-refresh indicator — anchored at the top, slides down with the
   finger, snaps back on release (or to the threshold if release fired the
   refresh). Hidden by default; only takes the gesture when the user is at
   scrollTop=0 on a list/filter page. Pattern from monitor's PWA shell. */
.ptr-indicator {
  position: fixed;
  top: -40px;
  left: 50%;
  transform: translate(-50%, 0);
  z-index: 300;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 12px;
  color: var(--text-muted);
  opacity: 0;
  pointer-events: none;
  font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, sans-serif);
  white-space: nowrap;
}
.ptr-indicator[data-active='true'] { pointer-events: auto; }
.ptr-spinner {
  width: 12px;
  height: 12px;
  border: 2px solid var(--text-subtle);
  border-top-color: var(--accent, #38BDF8);
  border-radius: 50%;
  animation: ptr-spin 800ms linear infinite;
  display: inline-block;
}
@keyframes ptr-spin { to { transform: rotate(360deg); } }

/* Version tap — fixed-position footer chip showing the loaded app version.
   Tap to force-reload (clears caches + unregisters SW). Goes red-tinged
   when the live worker version diverges from the loaded one (the silent
   reload will fire on the next visibility=hidden, but the indicator
   tells the user there's a new version available and they can pull it
   down sooner). Lesson: lessons/version-display-hardcoded.md. */
.version-tap {
  position: fixed;
  bottom: 8px;
  right: 12px;
  /* Sits ABOVE page content but BELOW any open modal/sheet. Prior z-index
     was 200 — the chip floated over modal action buttons (caught live by
     the user 2026-05-01 with the chip covering "Close" on the message
     detail). Modals are z-index: 50, sheets z-index: 60ish; chip at 5
     stays out of their way. */
  z-index: 5;
  font-size: 10px;
  color: var(--text-subtle);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px 9px;
  cursor: pointer;
  font-family: var(--font-mono, 'SF Mono', Menlo, monospace);
  letter-spacing: 0.3px;
  user-select: none;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.version-tap:hover, .version-tap:active { color: var(--text); border-color: var(--text-muted); }
.version-tap[data-stale='true'] {
  color: var(--accent, #38BDF8);
  border-color: var(--accent, #38BDF8);
  background: rgba(56, 189, 248, 0.08);
}
.version-tap[data-stale='true']::before {
  content: '↻ ';
  display: inline-block;
  margin-right: 2px;
}
@media (max-width: 720px) {
  /* Move the version chip out of the way of the bottom tab bar on mobile. */
  .version-tap { bottom: calc(60px + env(safe-area-inset-bottom, 0px) + 4px); right: 8px; }
}

/* Kanban card action buttons (Mark done / Snooze / File / Flag /
   Re-open). Were inline-styled with no feedback — taps felt dead.
   - Idle: muted text + subtle border, transparent bg
   - Hover (desktop): bg lifts to bubble-in, text + border darken
   - Active (tap on mobile fires :active): scale-down briefly +
     accent-tinted bg so the press is felt visually
   - .is-active (flag is set): persistent accent-tinted state until
     toggled off — same shape used by other "on/off" toggle buttons */
.card-action-btn {
  background: transparent;
  color: var(--text-subtle);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 3px 6px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease, transform 80ms ease;
  -webkit-tap-highlight-color: transparent;
}
.card-action-btn:hover {
  background: var(--bubble-in);
  color: var(--text);
  border-color: var(--text-muted);
}
.card-action-btn:active {
  transform: scale(0.92);
  background: rgba(56, 189, 248, 0.18);
  color: var(--accent, #38BDF8);
  border-color: var(--accent, #38BDF8);
}
.card-action-btn.is-active {
  background: rgba(56, 189, 248, 0.14);
  color: var(--accent, #38BDF8);
  border-color: rgba(56, 189, 248, 0.4);
}
.card-action-btn.is-active:hover {
  background: rgba(56, 189, 248, 0.22);
}

/* Tab-to-tab transitions — directional fade-slide ported from
   personal-jay-rsvp (the user wanted parity with that project).
   - DOWN: sub-page drill-in (default). Old slides up + fades; new
     enters from below.
   - LEFT: navigating to a tab further RIGHT in TAB_ORDER. Old slides
     left + fades; new enters from the right.
   - RIGHT: navigating to a tab further LEFT in TAB_ORDER. Old slides
     right + fades; new enters from the left.
   Exit is 120ms ease-in (snappier outgoing); enter is 180ms ease-out
   (lingering incoming). pointer-events:none during exit so a tap
   during the animation can't double-fire a navigation. */

/* Outgoing fades in place (no slide). Incoming fades + slides from
   the directional offset. Both animations start at the same frame
   and run for the same duration so the user reads a single
   simultaneous crossfade-with-slide instead of "old leaves, new
   arrives" sequenced motion (caught live 2026-05-01). */
@keyframes page-exit       { to { opacity: 0; } }
@keyframes page-enter-down  { from { opacity: 0; transform: translateY(8px);  } to { opacity: 1; transform: none; } }
@keyframes page-enter-left  { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: none; } }
@keyframes page-enter-right { from { opacity: 0; transform: translateX(-16px); } to { opacity: 1; transform: none; } }

.sidebar, .main, .page-stub { will-change: opacity, transform; }

.sidebar.page-exit, .main.page-exit, .page-stub.page-exit { pointer-events: none; }
/* Direction-agnostic exit (just fades in place) + direction-specific
   enter (slides + fades from the offset). Both 180ms ease-out so they
   peak together. */
.sidebar.page-exit, .main.page-exit, .page-stub.page-exit { animation: page-exit 0.18s ease-out forwards; }
.sidebar.page-enter-down,  .main.page-enter-down,  .page-stub.page-enter-down  { animation: page-enter-down  0.18s ease-out forwards; }
.sidebar.page-enter-left,  .main.page-enter-left,  .page-stub.page-enter-left  { animation: page-enter-left  0.18s ease-out forwards; }
.sidebar.page-enter-right, .main.page-enter-right, .page-stub.page-enter-right { animation: page-enter-right 0.18s ease-out forwards; }

/* Loading skeleton — shown synchronously while a route's data fetch awaits.
   Without this the middle of the screen is blank between rail+tabbar paint
   and the page's sidebar/main mount. */
.shimmer-bar {
  background: linear-gradient(90deg,
    var(--surface) 0%,
    var(--surface-alt, rgba(255,255,255,0.04)) 50%,
    var(--surface) 100%);
  background-size: 200% 100%;
  border-radius: 4px;
  animation: shimmer 1.6s linear infinite;
}
@keyframes shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
.sidebar.skeleton, .main.skeleton { pointer-events: none; }

/* iMessage transcript feed — same scroll mechanics as chat, different padding/gap.
   Without flex:1 + overflow:auto + min-height:0 the feed pushes the composer
   off-screen on mobile and scrolling silently no-ops. */
.feed {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 12px 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}

.msg {
  display: grid;
  grid-template-columns: 36px 1fr;
  gap: 12px;
  padding: 6px 0;
}
.msg.continuation { padding-top: 2px; padding-bottom: 2px; }
.msg.continuation .msg-avatar { visibility: hidden; }
.msg.continuation .msg-meta { display: none; }

.msg-avatar {
  width: 32px;
  height: 32px;
  border-radius: 16px;
  background: var(--cat-prospect);
  color: var(--bg);
  display: grid;
  place-items: center;
  font-size: 11px;
  font-weight: 700;
  margin-top: 2px;
}

.msg-meta {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 2px;
}
.msg-author { font-weight: 600; font-size: 14px; }
.msg-time { color: var(--text-subtle); font-size: 11px; }

.msg-body {
  font-size: 14px;
  color: var(--text);
  line-height: 1.5;
  word-wrap: break-word;
  overflow-wrap: anywhere;
  min-width: 0;
}
/* Back-link pill above a chat message that was posted via "Discuss in
   chat" — clicking it deep-links to the original email/iMessage. */
.msg-source-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 2px 0 6px;
  padding: 3px 9px;
  border: 1px solid var(--border);
  background: var(--surface-alt);
  color: var(--text-muted);
  font-size: 11px;
  font-weight: 500;
  border-radius: 999px;
  cursor: pointer;
  transition: background 150ms ease, color 150ms ease;
}
.msg-source-pill:hover {
  background: var(--bubble-in);
  color: var(--text);
}
/* Long unbroken URLs and tokens in digest markdown were pushing the
   .main-sub line off-screen + breaking the Today card width. Force
   any inline content to wrap. */
.msg-body a, .msg-body code { word-break: break-word; }
.msg-body code {
  background: var(--surface-alt);
  padding: 1px 5px;
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: 0.92em;
}
.msg-body pre {
  background: var(--surface-alt);
  padding: 10px 12px;
  border-radius: 8px;
  overflow-x: auto;
  margin: 6px 0;
  font-family: var(--font-mono);
  font-size: 0.92em;
}
.msg-body .cc-mention {
  color: var(--accent);
  background: rgba(14, 165, 233, 0.12);
  padding: 0 4px;
  border-radius: 4px;
  font-weight: 600;
}

.msg.deleted .msg-body {
  color: var(--text-subtle);
  font-style: italic;
}

.chat-typing {
  padding: 2px 24px 0;
  font-size: 12px;
  color: var(--text-muted);
  font-style: italic;
  min-height: 0;
  transition: opacity 0.2s;
}
.composer-wrap {
  padding: 12px 24px 20px;
  border-top: 1px solid var(--border);
}
.composer {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 10px 12px;
  display: flex;
  gap: 8px;
  align-items: flex-end;
  transition: border-color 0.15s;
}
.composer:focus-within { border-color: var(--accent); }
.composer-input {
  flex: 1;
  resize: none;
  font-family: inherit;
  line-height: 1.5;
  font-size: 14px;
  min-height: 22px;
  max-height: 160px;
}
.composer-send {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  background: var(--accent);
  color: var(--bg);
  display: grid;
  place-items: center;
  font-weight: 700;
}
.composer-send:disabled {
  background: var(--border);
  color: var(--text-subtle);
  cursor: not-allowed;
}

/* iMessage composer — pill input ------------------------------------
   iOS Messages.app shape: a round `+` button on the left, a flat
   pill input that grows with the message, and a circular send /
   mic button anchored to the right edge of the pill. No outline ring
   (the previous border + focus glow read as a generic chat input,
   not iMessage). On focus the pill subtly bumps its background
   instead of drawing a colored border. */
.imsg-composer-wrap { padding: 8px 12px max(8px, env(safe-area-inset-bottom, 0)); border-top: 1px solid var(--border); background: var(--surface); }
.imsg-composer-row { display: flex; gap: 8px; align-items: flex-end; }

.imsg-plus-btn {
  flex: 0 0 auto;
  /* 40px hit area — was 34px which fell below iOS 44pt thumb-tap
     guidance. Padding + flex-end alignment keeps the visual still
     compact next to the input pill. */
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--surface-alt);
  color: var(--text-muted);
  border: 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.1s, color 0.1s;
}
.imsg-plus-btn:hover { background: var(--bubble-in); color: var(--text); }
.imsg-plus-btn:active { transform: scale(0.94); }

.imsg-input-pill {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: flex-end;
  background: var(--surface-alt);
  border-radius: 18px;
  padding: 4px 4px 4px 14px;
  position: relative;
  transition: background 0.15s;
}
.imsg-input-pill:focus-within { background: var(--bubble-in); }
.imsg-input {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: 0;
  outline: 0;
  color: var(--text);
  font-family: inherit;
  /* 16px minimum so iOS Safari doesn't auto-zoom on focus. Anything
     below 16px in the focused field triggers the zoom, which on a
     PWA never zooms back out cleanly. */
  font-size: 16px;
  line-height: 1.35;
  padding: 6px 0;
  resize: none;
  max-height: 140px;
}

.imsg-send {
  flex: 0 0 auto;
  /* 36px — was 28px (below 44pt). The pill input that wraps it has
     its own padding so the actual hit area extends to ~44pt. */
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  margin-bottom: 1px;
  transition: background 0.15s, color 0.15s, transform 0.1s;
}
/* Empty state: show a mic glyph in muted color (no recording wired
   yet — the mic is a visual cue that matches iMessage). */
.imsg-send[data-empty="true"] {
  background: transparent;
  color: var(--text-muted);
}
.imsg-send[data-empty="true"] .imsg-send-arrow { display: none; }
.imsg-send[data-empty="true"] .imsg-send-mic { display: flex; align-items: center; }
/* Active state: blue circle with ↑ arrow. */
.imsg-send[data-empty="false"] {
  background: #007aff;
  color: white;
}
.imsg-send[data-empty="false"]:hover { background: #0066cc; }
.imsg-send[data-empty="false"]:active { transform: scale(0.92); }
.imsg-send[data-empty="false"] .imsg-send-arrow { display: flex; align-items: center; }
.imsg-send[data-empty="false"] .imsg-send-mic { display: none; }
.imsg-send:disabled { cursor: not-allowed; }

/* Bubble tail removed 2026-05-01. The pseudo-element approach was
   extending 16px past the bubble's right edge (the masking ::after
   sat outside the row's padding), which made the feed overflow
   horizontally on mobile and surfaced a stray scroll indicator
   across bubbles. Bubbles already have iMessage-shaped corner
   radii from the continuation logic in renderImessageBubble; the
   little notch is a polish flourish that we'll re-add later via
   SVG once the geometry is right. */
.imsg-bubble { position: relative; }

/* iMessage transcript ----------------------------------------------------
   Tighter spacing on the feed; pill composer with a blue circular send
   button on the right. The bubbles themselves are styled inline in
   renderImessageBubble so the radius logic for continuation grouping
   stays close to where it's calculated.

   Long-press behavior: on iOS Safari + Chrome, holding a touch on
   text triggers the native selection magnifier + handles + share
   sheet (callout). That fights our custom long-press menu — the
   user sees text get highlighted before our tapback picker shows.
   Disable the native callout + selection on bubbles so the
   gesture is fully ours. user-select stays enabled on the
   composer + reply preview so the user can still copy & paste
   normally outside the bubble. */
.imsg-row,
.imsg-row * {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  /* manipulation also kills the iOS double-tap-to-zoom 300ms delay,
     which makes our tap responses feel instant. */
  touch-action: manipulation;
}
/* The Info popover lets the user copy the GUID — keep selection there. */
.modal, .modal * { user-select: text; -webkit-user-select: text; -webkit-touch-callout: default; }
.imsg-feed {
  /* Top + bottom breathing room so the first bubble doesn't kiss
     the chat header and the last bubble doesn't kiss the composer.
     Live-reported 2026-05-04 — top/bottom margins were collapsed.
     8px is enough to read as "there's a gap" without wasting
     vertical space on phones. */
  padding-top: 8px;
  padding-bottom: 16px;
  /* Backstop — anything that accidentally extends past the bubble's
     logical width (tapback badges sticking out the corner, an
     overzealous attachment thumb) gets clipped instead of scrolling
     the whole page horizontally. */
  overflow-x: hidden;
  overflow-wrap: anywhere;
}
.imsg-main {
  overflow-x: hidden;
}
.imsg-row {
  /* Ensure individual rows can't push the feed wider than itself. */
  max-width: 100%;
  box-sizing: border-box;
  position: relative;
  /* Translated by the swipe-to-reveal-time gesture (drag right-to-left
     on the feed). CSS var --feed-drag is set on the parent
     .imsg-feed during the drag; rows read it so all rows translate
     as one. Spring-back uses cubic-bezier(0.32, 0.72, 0, 1) — Apple's
     ease-out-quart-ish curve that matches the iOS Messages snap.
     --feed-drag-duration is set on release and scales with pull
     distance so a tiny peek bounces back faster than a full pull. */
  transform: translateX(var(--feed-drag, 0px));
  transition: transform var(--feed-drag-duration, 220ms) cubic-bezier(0.32, 0.72, 0, 1);
  will-change: transform;
}
.imsg-feed.imsg-feed-dragging .imsg-row {
  /* Disable the snap-back transition while the drag is live so the
     feed tracks the finger 1:1. CSS class is toggled on/off in JS. */
  transition: none;
}
.imsg-time-rail {
  /* Per-row absolute lane positioned to the right of the row's
     content. The negative `right` offset puts the lane in the
     viewport gap that opens when the feed swipes left by
     REVEAL_MAX (72px); when the feed is at rest the rail sits
     just past the right edge and is invisible.
     `right: -64px` + width 56px = lane spans the 72px gap minus
     8px of viewport-edge padding so the time isn't kissing the
     screen edge. */
  position: absolute;
  right: -64px;
  top: 50%;
  width: 56px;
  transform: translateY(-50%);
  text-align: right;
  font-size: 11px;
  color: var(--text-subtle);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  pointer-events: none;
  user-select: none;
  opacity: 0;
  transition: opacity 180ms ease;
}
.imsg-feed-dragging .imsg-time-rail,
.imsg-row.imsg-row-revealed .imsg-time-rail {
  opacity: 1;
}

/* Status row — a thin caption rendered below the LAST outbound
   bubble in a stack. Apple's convention: only the most recent
   sent message in a thread shows "Delivered" / "Read"; older
   ones drop it once a newer message takes the slot. */
.imsg-status {
  font-size: 10px;
  font-weight: 600;
  padding: 2px 14px 4px;
  letter-spacing: 0.2px;
  color: var(--text-subtle);
  text-align: right;
}
.imsg-status.imsg-status-failed { color: #ef4444; }
.imsg-status.imsg-status-sending { color: var(--text-subtle); }
.imsg-status.imsg-status-read { color: var(--text-muted); }
.imsg-status .imsg-status-glyph {
  display: inline-block;
  width: 10px;
  margin-right: 3px;
  text-align: center;
}

/* Three-dot pulse for the live typing indicator bubble. Apple's
   timing is ~1.2s with each dot offset by a couple frames so they
   read as a wave. */
@keyframes imsg-typing-pulse {
  0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
  30%           { opacity: 1;   transform: translateY(-2px); }
}

/* Inline address card — Apple-Maps-style tile with a faux mini-map
   icon. Tapping opens Apple Maps via a Universal Link. */
.imsg-address-card {
  display: flex;
  gap: 10px;
  align-items: center;
  padding: 8px 10px 8px 8px;
  border-radius: 14px;
  background: rgba(0, 0, 0, 0.18);
  text-decoration: none;
  color: inherit;
  min-width: 240px;
  max-width: 320px;
  cursor: pointer;
  border: 1px solid rgba(255, 255, 255, 0.08);
}
.imsg-address-card:hover { background: rgba(0, 0, 0, 0.24); }
.imsg-address-map {
  flex-shrink: 0;
  width: 56px;
  height: 56px;
  border-radius: 10px;
  display: block;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.imsg-address-meta {
  flex: 1;
  min-width: 0;
}
.imsg-address-street {
  font-size: 14px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.imsg-address-sub {
  font-size: 12px;
  opacity: 0.85;
  line-height: 1.25;
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Drive-time line under the address. Two slots: a label
   ("12 min · 7.2 mi" / "Calculating…" / "Drive time") and an
   optional action button ("Recalculate" / "Use my location") that
   re-runs the geolocation + routing flow. The action stops bubble-
   menu propagation in JS so it doesn't trigger a long-press. */
.imsg-address-drive {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 6px;
  font-size: 11px;
  line-height: 1.3;
}
.imsg-address-drive-label {
  color: var(--text-muted);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}
.imsg-address-drive-error {
  color: var(--text-subtle);
  font-style: italic;
}
.imsg-address-drive-action {
  background: transparent;
  border: 0;
  padding: 0;
  color: inherit;
  font-family: inherit;
  font-size: 11px;
  font-weight: 500;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
  opacity: 0.7;
}
.imsg-address-drive-action:hover {
  opacity: 1;
}

/* Receipt footer morph — fade + tiny vertical slide between
   states (Sending… → Delivered → Read 2:25 PM). The polling loop
   adds .imsg-receipt-morph-out, swaps the inner spans at the
   midpoint, then adds .imsg-receipt-morph-in for the entrance.
   Reads as a smooth text-changing morph instead of a hard flash. */
.imsg-receipt {
  transition: opacity 160ms ease-out, transform 160ms ease-out;
}
.imsg-receipt.imsg-receipt-morph-out {
  opacity: 0;
  transform: translateY(-2px);
}
.imsg-receipt.imsg-receipt-morph-in {
  animation: imsg-receipt-fade-in 220ms ease-out;
}
@keyframes imsg-receipt-fade-in {
  from { opacity: 0; transform: translateY(2px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Brief flash applied to a message when the user taps a reply
   preview pointing at it — draws the eye to the parent without a
   permanent highlight. */
@keyframes imsg-parent-flash {
  0%   { box-shadow: 0 0 0 0 rgba(0, 122, 255, 0.0); }
  20%  { box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.55); }
  100% { box-shadow: 0 0 0 0 rgba(0, 122, 255, 0.0); }
}
.imsg-bubble.imsg-flash {
  animation: imsg-parent-flash 1100ms ease-out;
}
/* Older `.imsg-send` rules (the !important blue background + the
   bordered input) are intentionally removed. The pill-input styles
   above replace them in full. */
/* iOS Safari auto-zooms when a focused input has font-size < 16px.
   Set a 16px floor on EVERY input/textarea/select on mobile so no
   field ever triggers the zoom, including the search bars and
   filter inputs scattered through the app. The viewport meta also
   sets maximum-scale=1 as belt + suspenders. */
@media (max-width: 720px) {
  input,
  textarea,
  select {
    font-size: 16px !important;
  }
}

/* ─────────────────────────────────────────────────────────────────
   "Composing" mode — the award-winning focus animation.

   When the iMessage composer textarea is focused on mobile we add
   `body.imsg-composing`. The mobile tab bar slides down off-screen,
   the composer subtly lifts (small shadow + 1px elevation), and
   the feed gains the freed bottom padding. The keyboard handles
   pushing the composer above itself via the visualViewport API
   (interactive-widget=resizes-content) so the composer always
   sits flush with the keyboard's top edge.

   Easing: Apple's "spring" curve cubic-bezier(0.32, 0.72, 0, 1).
   That's the same curve iOS uses for system sheets — tab bar slides
   down with momentum and the composer arrives slightly ahead of it
   so the eye locks on the input as the chrome retreats.

   Timings are intentionally close to the iOS keyboard show duration
   (~280ms) so the whole orchestra moves as one. */
.mobile-tabbar,
.imsg-composer-wrap,
.imsg-feed,
.imsg-plus-btn,
.imsg-input-pill {
  transition:
    transform 320ms cubic-bezier(0.32, 0.72, 0, 1),
    opacity 200ms ease-out,
    box-shadow 240ms cubic-bezier(0.32, 0.72, 0, 1),
    padding 240ms cubic-bezier(0.32, 0.72, 0, 1),
    background 200ms ease-out;
}

body.imsg-composing .mobile-tabbar,
body.imsg-thread-open .mobile-tabbar {
  /* Hide the bottom tabbar when:
       - composer focused (existing behavior — keyboard owns the bottom)
       - viewing a single iMessage thread (iOS Messages convention —
         transcript + composer get the full screen, swipe-back gesture
         on the thread header takes you to the list).
     Slide down + fade so the transition matches the composer focus
     animation. */
  transform: translateY(120%);
  opacity: 0;
  pointer-events: none;
}
body.imsg-composing .imsg-composer-wrap {
  /* Subtle elevation that says "this is the active surface". */
  box-shadow: 0 -8px 28px rgba(0, 0, 0, 0.35);
  /* Tighten padding so the keyboard doesn't push the pill away
     from its natural resting line. */
  padding-bottom: max(8px, env(safe-area-inset-bottom, 0));
}
body.imsg-composing .imsg-input-pill {
  /* Bump the pill bg a hair more for visual focus. */
  background: var(--bubble-in);
}
body.imsg-composing .imsg-feed {
  /* The tab bar's vacated space goes back to the transcript. */
  padding-bottom: 8px;
}

/* The plus button retracts on focus — iMessage hides the camera/sticker
   tray when typing. We don't have one yet, but the affordance shrinks
   the icon to a chevron-style hint that lets the user expand it back
   by tapping. Animation is the same spring. */
body.imsg-composing .imsg-plus-btn {
  transform: scale(0.78);
  opacity: 0.5;
}

/* Inbox keyboard cursor — highlight the row currently selected via
   J/K nav. Subtle blue ring + slight bg lift so it doesn't fight
   the source-stripe-on-the-left visual. */
.inbox-card.cursor-active {
  outline: 2px solid #007aff;
  outline-offset: -2px;
  background: rgba(0, 122, 255, 0.06) !important;
}

/* Shared dismiss button — every X / dismiss / cancel chip in the
   app should hit at least 36×36px so it works under a thumb. The
   actual visible glyph is 16-18px; the rest is invisible padding
   that catches the tap. Apply via class instead of inline-styling
   each occurrence. */
.btn-dismiss {
  background: transparent;
  border: 0;
  padding: 0;
  width: 36px;
  height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--text-subtle);
  font-size: 16px;
  line-height: 1;
  border-radius: 8px;
  flex-shrink: 0;
  transition: background 0.12s, color 0.12s;
}
.btn-dismiss:hover { background: var(--surface-alt); color: var(--text); }
.btn-dismiss:active { transform: scale(0.94); }

/* Drag-drop overlay on the iMessage feed. The dashed outline + tinted
   scrim makes it obvious where the file will land. Shown only while
   a file is being dragged over the feed. */
.imsg-feed.imsg-drop-active::after {
  content: 'Drop to send';
  position: absolute;
  inset: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px dashed #007aff;
  border-radius: 14px;
  background: rgba(0, 122, 255, 0.08);
  color: #007aff;
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.3px;
  pointer-events: none;
  z-index: 10;
}

/* iMessage-style pinned grid — circular avatars at the top of the
   Messages sidebar. Mirrors iOS Messages' pin layout: 3-across grid,
   big circle avatars, name beneath, unread count floats on the
   top-right of the avatar, latest inbound message bubbles next to it. */
.imsg-pinned-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px 8px;
  padding: 14px 12px 18px;
  border-bottom: 1px solid var(--border);
}
.imsg-pin-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.imsg-pin-tile:hover { text-decoration: none; }
.imsg-pin-tile:active .imsg-pin-avatar { transform: scale(0.94); }
.imsg-pin-avatar-wrap {
  position: relative;
  width: 72px;
  height: 72px;
}
.imsg-pin-avatar {
  width: 72px;
  height: 72px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 26px;
  color: white;
  letter-spacing: 0.5px;
  user-select: none;
  transition: transform 120ms ease;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
}
.imsg-pin-tile.active .imsg-pin-avatar {
  outline: 2px solid #007aff;
  outline-offset: 2px;
}
.imsg-pin-tile.unread .imsg-pin-avatar {
  /* Subtle blue ring on unread pins — matches iMessage's color
     accent without overpowering the avatar gradient. */
  box-shadow: 0 0 0 2px #007aff, 0 2px 8px rgba(0, 122, 255, 0.35);
}
.imsg-pin-unread {
  position: absolute;
  top: -3px;
  right: -3px;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: 999px;
  background: #ff3b30;
  color: white;
  font-size: 11px;
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--surface);
  box-sizing: border-box;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}
.imsg-pin-muted {
  position: absolute;
  bottom: -2px;
  right: -2px;
  font-size: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* Floating "they just messaged" bubble next to the avatar. iMessage
   shows this for ~30s after a new inbound; we keep it as long as the
   row is unread. Tail points back at the avatar. */
.imsg-pin-preview {
  position: absolute;
  left: 76px;
  top: 4px;
  max-width: 180px;
  padding: 6px 10px 7px;
  background: var(--bubble-in, rgba(127,127,127,0.18));
  border-radius: 14px 14px 14px 4px;
  font-size: 11px;
  line-height: 1.3;
  color: var(--text);
  pointer-events: none;
  white-space: normal;
  word-break: break-word;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
.imsg-pin-name {
  font-size: 11px;
  font-weight: 500;
  color: var(--text);
  text-align: center;
  max-width: 84px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1.2;
}
/* Two-across when the sidebar is narrow — avoids cramped avatars on
   small phones in landscape that share the rail with the rest of
   the app chrome. */
@media (max-width: 380px) {
  .imsg-pinned-grid { grid-template-columns: repeat(2, 1fr); }
  .imsg-pin-preview { display: none; }
}

/* Sidebar thread row — iMessage-style hover + active states */
.imsg-thread-row {
  display: block;
  padding: 10px 14px;
  border-radius: 0;
  text-decoration: none;
  color: inherit;
  border-bottom: 1px solid var(--border);
}
.imsg-thread-row:hover { background: var(--surface-alt); text-decoration: none; }
.imsg-thread-row.active { background: var(--surface-alt); }
.imsg-thread-row.active .imsg-thread-title { color: var(--text); }

/* Empty / loading / error states */
.empty {
  flex: 1;
  display: grid;
  place-items: center;
  text-align: center;
  padding: 48px;
}
.empty-title {
  font-size: 18px;
  font-weight: 700;
  margin-bottom: 6px;
}
.empty-sub {
  color: var(--text-muted);
  font-size: 13px;
  max-width: 360px;
  margin: 0 auto 16px;
}

/* Toast stack — single bottom-anchored container that all toasts
   render into. Stack flows column-reverse so newest sits at the
   BOTTOM of the visible group (closest to the user's reading focus).
   Pointer-events: none on the host so the stack itself doesn't block
   clicks; individual toasts re-enable. */
#toast-stack {
  position: fixed;
  bottom: max(24px, env(safe-area-inset-bottom, 0));
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column-reverse;
  gap: 8px;
  align-items: center;
  z-index: 100;
  pointer-events: none;
  max-width: 92vw;
}
.toast {
  position: relative;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 10px 16px;
  font-size: 13px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
  pointer-events: auto;
  /* Slide up from below + fade in. Older toasts already in the stack
     simply translate to make room because the stack is a flex column-
     reverse; their `transform` baseline stays 0 so we don't fight a
     shared property. */
  opacity: 0;
  transform: translateY(8px) scale(0.96);
  transition: opacity 220ms ease, transform 240ms cubic-bezier(0.32, 0.72, 0, 1);
}
.toast.toast-in {
  opacity: 1;
  transform: translateY(0) scale(1);
}

/* Action toast (Gmail-style undo growl). Replaces native confirm()
   for destructive ops. Layout: message · action button · countdown
   bar at the bottom. */
.toast-with-action {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 12px 10px 16px;
  min-width: 240px;
  max-width: calc(100vw - 32px);
  overflow: hidden;
}
.toast-with-action .toast-message {
  color: var(--text);
}
.toast-with-action .toast-action {
  background: transparent;
  color: var(--accent, #38BDF8);
  border: none;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 6px;
  cursor: pointer;
}
.toast-with-action .toast-action:hover, .toast-with-action .toast-action:active {
  background: rgba(56, 189, 248, 0.14);
}
.toast-with-action .toast-progress {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background: var(--accent, #38BDF8);
  transform: scaleX(1);
  transform-origin: left center;
  transition: transform linear;
}
@media (max-width: 720px) {
  /* Don't sit behind the fixed tab bar on mobile */
  .toast {
    bottom: calc(60px + env(safe-area-inset-bottom, 0px) + 12px);
  }
}
.toast.error { border-color: var(--danger); color: var(--danger); }
.toast.success { border-color: var(--success); color: var(--success); }

/* Modal */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(7, 12, 24, 0.6);
  backdrop-filter: blur(4px);
  z-index: 50;
  display: grid;
  place-items: center;
  /* Top + bottom padding pushes the modal below the iOS Dynamic Island /
     status bar (caught live 2026-05-01: the iOS clock pill was painting
     over the modal title). max() guarantees at least 24px of breathing
     room even when env() resolves to 0 (desktop / older iOS). */
  padding:
    max(24px, calc(env(safe-area-inset-top, 0px) + 8px))
    24px
    max(24px, calc(env(safe-area-inset-bottom, 0px) + 8px));
}
.modal {
  width: 100%;
  max-width: 420px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  padding: 24px;
}
.modal-title { font-size: 18px; font-weight: 700; margin-bottom: 12px; }
.modal-body { color: var(--text-muted); margin-bottom: 18px; }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; }
.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 12px;
}
.field-label { font-size: 11px; font-weight: 600; color: var(--text-muted); letter-spacing: 0.04em; text-transform: uppercase; }
.field-input {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 10px 12px;
  font-size: 14px;
  transition: border-color 0.15s;
}
.field-input:focus { border-color: var(--accent); }

/* Stub pages */
.page-stub {
  flex: 1;
  display: grid;
  place-items: center;
  padding: 48px;
  text-align: center;
}
.page-stub h2 { font-size: 22px; margin-bottom: 8px; }
.page-stub p { color: var(--text-muted); max-width: 480px; }

/* ===========================================================================
 * Mobile shell — MUST live at the end of this file
 * ---------------------------------------------------------------------------
 * Why end-of-file: every selector below has the same specificity as its
 * desktop counterpart (single class). CSS resolves ties by SOURCE ORDER,
 * so mobile rules must come AFTER desktop rules to win when the @media
 * query matches. Putting the mobile block earlier (where it conceptually
 * "belongs" near .app) silently produces a no-op — all overrides get
 * clobbered by the later desktop rules. Found via the "rail visible on
 * mobile + tab bar at top" bug in the first rebuild attempt.
 *
 * Pattern:
 *   list   routes (chat, messages, contacts, person) → toggle sidebar↔main
 *   filter routes (today, inbox, search, docs, settings) → main only
 * ======================================================================== */
/* Person view — design ------------------------------------------------
   Single-column flow on mobile: sticky header → horizontal stat-chip
   row → AI dialogue card → timeline → collapsible Threads / Documents
   / Identities. Side panel keeps a fixed-width column on desktop,
   collapses into the same scroll stack below the timeline on mobile.

   Caught live 2026-05-01: a fixed 280px side panel + uppercase
   stat-label stack squeezed the AI dialogue text into one-character-
   per-line wrapping on iPhone. The fix is structural — chips for
   stats, accordions for the side, and a clean single scroll. */

/* Accordion chevron rotates on open. */
.person-accordion[open] .person-accordion-chev {
  transform: rotate(90deg);
}

/* In-bubble @-mention. The token "@Bob" or "@\"Bob Smith\"" gets
   wrapped in this span so the eye lands on the addressee at a
   glance. Color picks up the bubble color (inherit) so it reads on
   both blue outbound and gray inbound bubbles. */
.imsg-mention {
  background: rgba(0, 122, 255, 0.18);
  color: inherit;
  border-radius: 4px;
  padding: 0 4px;
  font-weight: 600;
}

/* In-bubble URL. Tappable, opens in a new tab, doesn't trigger the
   bubble's long-press menu (onclick.stopPropagation in JS). Underline
   on the link so it's discoverable as tappable; hover/active states
   match the rest of the SPA. */
.imsg-link {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
  word-break: break-word;
  overflow-wrap: anywhere;
}
.imsg-link:hover,
.imsg-link:active {
  text-decoration-thickness: 2px;
}

/* Tapback row + pills. Tap a pill → attribution popover lists who
   reacted (essential in groups). Two visual modes: slim pill that
   pokes out the top of a text bubble, chunky circle on the corner
   of an image attachment. */
.imsg-tapback-row {
  position: absolute;
  display: flex;
  pointer-events: auto;
}
.imsg-tapback-row-text {
  top: -10px;
  gap: 2px;
}
.imsg-tapback-row-text.imsg-tapback-row-left { left: -6px; }
.imsg-tapback-row-text.imsg-tapback-row-right { right: -6px; }
.imsg-tapback-row-image {
  bottom: -10px;
  gap: 4px;
}
.imsg-tapback-row-image.imsg-tapback-row-left { left: -8px; }
.imsg-tapback-row-image.imsg-tapback-row-right { right: -8px; }

.imsg-tapback {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  cursor: pointer;
  font-family: inherit;
  line-height: 1;
  transition: transform 120ms ease, box-shadow 120ms ease;
}
.imsg-tapback:hover {
  transform: scale(1.08);
}
.imsg-tapback:active {
  transform: scale(0.96);
}
.imsg-tapback.imsg-tapback-mine {
  background: #007aff;
  color: white;
}
.imsg-tapback-text {
  padding: 2px 6px;
  border-radius: 999px;
  font-size: 12px;
}
.imsg-tapback-text .imsg-tapback-count {
  font-size: 10px;
  font-weight: 600;
}
.imsg-tapback-image {
  min-width: 30px;
  height: 30px;
  padding: 0 6px;
  border: 2px solid var(--bg, white);
  border-radius: 999px;
  font-size: 16px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
}
.imsg-tapback-image .imsg-tapback-count {
  font-size: 11px;
  font-weight: 700;
}

/* Tapback attribution popover. Anchored at click coords by JS,
   above modals so it always sits in front of the bubble. */
.imsg-tapback-attribution {
  position: fixed;
  z-index: 80;
  width: 240px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  padding: 12px;
  font-size: 13px;
  animation: imsg-pop-in 140ms cubic-bezier(0.32, 0.72, 0, 1);
}
@keyframes imsg-pop-in {
  from { opacity: 0; transform: translateY(4px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.imsg-tapback-attribution-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-bottom: 8px;
  margin-bottom: 6px;
  border-bottom: 1px solid var(--border);
}
.imsg-tapback-attribution-glyph {
  font-size: 22px;
}
.imsg-tapback-attribution-verb {
  font-size: 11px;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--text-muted);
  font-weight: 700;
}
.imsg-tapback-attribution-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.imsg-tapback-attribution-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 2px;
}
.imsg-tapback-attribution-avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 700;
  color: white;
  background: linear-gradient(
    135deg,
    hsl(var(--imsg-stack-hue, 210), 65%, 58%),
    hsl(calc(var(--imsg-stack-hue, 210) + 40), 65%, 48%)
  );
  flex-shrink: 0;
}
.imsg-tapback-attribution-name {
  font-size: 13px;
  color: var(--text);
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* iMessage thread header avatar — single 36px circle for 1:1 chats,
   stacked mini-avatars for groups (Apple convention). */
.imsg-header-avatar {
  width: 36px;
  height: 36px;
  flex-shrink: 0;
  border-radius: 50%;
}
.imsg-header-avatar-photo {
  object-fit: cover;
}
.imsg-header-sub {
  font-size: 11px;
  color: var(--text-subtle);
}

/* Group avatar stack — overlapping mini circles, up to 3. The third
   tile becomes "+N" when the group has more members. Tiles use a
   data-driven hue (`--imsg-stack-hue`) so each participant has a
   stable color. */
.imsg-group-stack {
  position: relative;
  width: 36px;
  height: 36px;
  flex-shrink: 0;
}
.imsg-group-stack-tile {
  position: absolute;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 9px;
  font-weight: 700;
  color: white;
  letter-spacing: 0.2px;
  background: linear-gradient(
    135deg,
    hsl(var(--imsg-stack-hue, 210), 65%, 58%),
    hsl(calc(var(--imsg-stack-hue, 210) + 40), 65%, 48%)
  );
  border: 2px solid var(--surface);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
}
.imsg-group-stack-tile-0 { top: 0; left: 0; z-index: 3; }
.imsg-group-stack-tile-1 { bottom: 0; right: 0; z-index: 2; }
.imsg-group-stack-tile-2 { top: 4px; right: -2px; z-index: 1; }
.imsg-group-stack-extra {
  font-size: 9px;
  font-weight: 700;
}

/* ─────────────────────────────────────────────────────────────────
   Person AI search card — the surface where you open a contact,
   type a free-form question, and get an AI-synthesized answer
   pulled from every source where this person appears (email, chat,
   iMessage, linked documents). Hybrid retrieval: Vectorize semantic
   + FTS5 keyword on the server, citation chips deep-link back to
   the source on tap.
   ───────────────────────────────────────────────────────────────── */
.person-ai-card {
  margin: 16px 24px;
  padding: 18px;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: 12px;
  min-height: 180px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.person-ai-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.person-ai-eyebrow {
  font-size: 11px;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--text-subtle);
  font-weight: 700;
}
.person-ai-refresh {
  padding: 4px 10px;
  font-size: 12px;
}

/* Search bar — bigger and more prominent than a follow-up input.
   This is the main affordance: the user opens a contact and starts
   typing here. */
.person-ai-ask {
  display: flex;
  gap: 8px;
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 6px 8px 6px 12px;
  transition: border-color 150ms ease, box-shadow 150ms ease;
}
.person-ai-ask:focus-within {
  border-color: var(--accent, #38bdf8);
  box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.15);
}
.person-ai-ask-icon {
  display: inline-flex;
  align-items: center;
  color: var(--text-subtle);
  flex-shrink: 0;
}
.person-ai-input {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: 0;
  outline: 0;
  padding: 8px 0;
  font-size: 14px;
  color: var(--text);
}
.person-ai-input::placeholder {
  color: var(--text-subtle);
}
.person-ai-ask-btn {
  padding: 6px 14px;
  font-size: 13px;
  flex-shrink: 0;
}

.person-ai-body {
  color: var(--text);
  line-height: 1.6;
  font-size: 13px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.person-ai-snapshot { /* the cached recent-activity snapshot */ }
.person-ai-empty {
  color: var(--text-subtle);
  font-style: italic;
}
.person-ai-error {
  color: var(--danger, #ef4444);
}

/* Snapshot intro paragraph — prose context before the bullet cards. */
.person-ai-para {
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-muted);
  padding: 2px 2px 6px;
}

/* Snapshot bullet rows — each insight as a distinct card row so they
   scan quickly instead of blending into a wall of list text. */
.person-ai-bullet {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.person-ai-bullet-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent, #38bdf8);
  margin-top: 5px;
  flex-shrink: 0;
  opacity: 0.7;
}
.person-ai-bullet-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.person-ai-bullet-text {
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
}
.person-ai-bullet-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

/* Scope footer — tells the user exactly what window was searched for
   the snapshot, and hints that search goes further. */
.person-ai-scope {
  font-size: 11px;
  color: var(--text-subtle);
  padding: 4px 2px;
  border-top: 1px solid var(--border);
  margin-top: 2px;
  padding-top: 8px;
}

/* Q&A turn: user bubble + AI answer block */
.person-ai-turn {
  padding: 10px 0;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.person-ai-user-bubble {
  align-self: flex-end;
  max-width: 88%;
  background: rgba(56, 189, 248, 0.12);
  border: 1px solid rgba(56, 189, 248, 0.25);
  border-radius: 14px 14px 4px 14px;
  padding: 8px 14px;
  font-size: 13px;
  color: var(--text);
  line-height: 1.45;
}
.person-ai-answer-block {
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px 14px 14px 14px;
  padding: 10px 14px;
}
.person-ai-answer-label {
  font-size: 10px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--text-subtle);
  font-weight: 700;
}
.person-ai-turn-answer {
  font-size: 13px;
  line-height: 1.55;
}
.person-ai-pending {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--text-subtle);
  font-size: 12px;
  font-style: italic;
  padding: 4px 2px;
}
.person-ai-pending::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--text-subtle);
  animation: imsg-typing-pulse 1.4s infinite ease-in-out;
}
.person-ai-retrieval-note {
  font-size: 10px;
  color: var(--text-subtle);
  font-style: italic;
  margin-top: 2px;
}

/* Citation chips — tappable, kind-coded by left-edge accent. */
.person-ai-cites {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px;
  margin-top: 4px;
}
.person-ai-cites-label {
  font-size: 10px;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--text-subtle);
  font-weight: 700;
  margin-right: 2px;
}
.person-ai-cite {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--bubble-in);
  color: var(--text-muted);
  border: 1px solid transparent;
  font-size: 11px;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
  font-family: inherit;
}
.person-ai-cite:hover {
  background: var(--surface-alt);
  color: var(--text);
}
.person-ai-cite-tag {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.person-ai-cite-email {
  border-color: rgba(56, 189, 248, 0.35);
}
.person-ai-cite-chat {
  border-color: rgba(34, 197, 94, 0.35);
}
.person-ai-cite-document {
  border-color: rgba(168, 85, 247, 0.35);
}

@media (max-width: 720px) {
  .person-body {
    flex-direction: column;
  }
  .person-side-panel {
    width: 100% !important;
    border-left: 0 !important;
    padding: 10px 12px !important;
    overflow-y: visible !important;
    /* Sits flat against the timeline column on mobile — no separator
       line; the accordion borders give each section its own frame. */
    border-top: 1px solid var(--border);
  }
  .person-primary {
    flex: 1 1 auto;
    min-height: 220px;
  }
  .person-info-strip {
    /* Stat chips scroll horizontally on phones. The container
       is already overflow-x: auto inline; this removes the visible
       scrollbar to keep the affordance subtle. */
    scrollbar-width: none;
  }
  .person-info-strip::-webkit-scrollbar {
    display: none;
  }
  /* Tighten the AI dialogue card on mobile so it doesn't dominate. */
  .person-ai-card {
    margin: 10px 12px;
    padding: 14px;
  }
}

@media (max-width: 720px) {
  /* Round 5: previous rounds 1-4 all assumed `height: 100%` would
     propagate from html → body → #root → .app correctly on iOS PWA
     standalone. User reported the gap still present after R4
     (position-fixed tab bar at bottom:0) — which means the issue
     ISN'T tab bar position; it's that `height: 100%` is resolving to
     something LESS than the actual viewport, so there's just a chunk
     of body underneath the .app that's showing the body's background.
     Two-pronged fix:
       1. Force html/body/#root/.app to literal viewport dimensions
          using a cascade of vh/lvh/dvh + position:fixed inset:0 on
          .app as the absolute backstop.
       2. Defensive body background matches the tab bar so even if
          some phantom band is showing through, it's the same color
          as the tab bar — invisibly contiguous. */
  html, body {
    height: 100vh;
    height: 100lvh;
    width: 100vw;
    overflow: hidden;
    margin: 0;
    padding: 0;
    /* Match tab bar background — any gap below the tab bar that shows
       body bg through becomes invisible because the colors line up. */
    background: var(--surface);
  }
  #root {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
  }
  .app {
    position: absolute;
    /* Bottom edge follows the visualViewport keyboard offset that
       app.js writes to --kb-h. When the keyboard opens this lifts
       the entire .app (including the composer at its bottom) above
       the keyboard. Without this iOS PWA standalone leaves a fat
       black gap between the composer and the keyboard's top edge.
       Spring transition matches the iOS keyboard show curve. */
    inset: 0 0 var(--kb-h, 0px) 0;
    width: 100%;
    height: auto;
    transition: bottom 280ms cubic-bezier(0.32, 0.72, 0, 1);
    background: var(--bg);
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
    grid-template-areas: 'content';
    padding-top: 0;
  }
  .rail { display: none; }
  .sidebar, .main {
    grid-area: content;
    min-height: 0;
    min-width: 0;
    padding-top: env(safe-area-inset-top, 0px);
    /* Reserves 56px so content isn't hidden behind the fixed
       bottom tabbar. When the tabbar is hidden (composing OR
       viewing a single iMessage thread) the rules below collapse
       this padding to just the home-indicator safe area —
       otherwise the vacated tabbar slot becomes dead space below
       the last bubble. Caught live 2026-05-03 in the Michelle
       transcript: ~70px of empty band between "Right back at ya"
       and the composer. */
    padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px));
  }
  body.imsg-thread-open .main {
    /* No padding here. .imsg-composer-wrap's own
       `padding-bottom: max(8px, env(safe-area-inset-bottom))` already
       provides the home-indicator buffer. Stacking another safe-area
       inset on .main was the dead band the user circled
       — composer pill ended up 68px (34 + 34) above the viewport
       bottom on iPhones with a notch. */
    padding-bottom: 0;
  }
  /* When the composer is focused, iOS owns the bottom of the screen
     (keyboard + form-assistant bar). The home-indicator safe area
     gets covered by chrome, so reserving safe-area-inset-bottom on
     .main here would just produce a dead band between our composer
     and the form-assistant bar — caught live in Michelle's
     transcript with two big empty stripes between the iMessage
     pill and the keyboard. Drive it to 0 so the composer sits
     flush with whatever iOS draws underneath. */
  body.imsg-composing .main {
    padding-bottom: 0;
  }
  body.imsg-composing .imsg-composer-wrap {
    padding-bottom: 0;
  }
  .mobile-tabbar {
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 30;
    /* Honor the iOS home-indicator safe area so the tab bar's bottom
       row of icons doesn't kiss the indicator. The 8px floor handles
       devices without a bottom inset (iPhone SE, iPad). */
    padding-bottom: max(8px, env(safe-area-inset-bottom, 0px));
  }
  .mobile-back-btn { display: inline-flex; }

  /* List routes — sidebar (picker) ↔ main (detail) toggle */
  .app[data-mobile-route-kind="list"][data-mobile-pane="sidebar"] .main { display: none; }
  .app[data-mobile-route-kind="list"][data-mobile-pane="main"] .sidebar { display: none; }
  /* When viewing a detail pane (DM/channel/contact/iMessage thread), the tab
     bar belongs to the picker layer — hide it in the detail view so it doesn't
     float over the composer or waste space. The composer-wrap already handles
     safe-area-inset-bottom itself, so padding-bottom on .main goes to 0. */
  .app[data-mobile-route-kind="list"][data-mobile-pane="main"] .mobile-tabbar { display: none; }
  .app[data-mobile-route-kind="list"][data-mobile-pane="main"] .main { padding-bottom: 0; }
  /* Filter routes — main only on mobile */
  .app[data-mobile-route-kind="filter"] .sidebar { display: none; }

  /* Sidebar takes the whole content area on mobile */
  .sidebar { border-right: 0; }
  .sidebar-head { padding: 16px 12px 8px; }
  .sidebar-section-title { padding: 16px 12px 6px; }
  .sidebar-list { padding: 0 8px 16px; }

  /* Main pane — tighter padding, safe-area on bottom */
  .main-head { padding: 12px 12px; gap: 8px; }
  .main-title { font-size: 17px; }
  .chat-feed { padding: 12px 12px; }
  .chat-typing { padding: 2px 12px 0; }
  .composer-wrap { padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px)); }

  /* Tap targets ≥44pt — Apple HIG + WCAG 2.5.5 */
  .channel-row, .dm-row, .sidebar-row { min-height: 44px; padding: 10px 12px; }
  .composer-send { width: 40px; height: 40px; }
  .btn { min-height: 44px; padding: 0 14px; }

  /* Long contact / DM / channel names must not break the layout — clip them
     with an ellipsis on the picker rows. */
  .sidebar-row, .channel-row, .dm-row { overflow: hidden; }
  .sidebar-row-title, .channel-row > span:last-child, .dm-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 100%;
  }

  /* Header subtitle lines (e.g. 'X unread · Y total · All sources') must
     truncate, not wrap, on narrow viewports. */
  .main-sub {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Hide cosmetic elements that don't make sense on mobile */
  .rail-user { display: none; }

  /* Kanban on mobile: keep horizontal swipe between columns. User's
     ask: "the kanban should be horizontal" — the earlier vertical
     stack was the wrong pivot. Bug being fixed THIS time: horizontal
     scroll INSIDE individual columns (long unbreakable text in cards
     was widening the column past its 88vw track). Set min-width: 0 +
     overflow-x: hidden on the column wrapper so cards are forced to
     break-word instead of expanding the column.
     Each column = 88vw wide; one column dominates the viewport with a
     thin sliver of the next as a swipe affordance. Snap locks releases
     to the nearest column edge. */
  .kanban-grid {
    grid-auto-columns: 88vw !important;
    grid-template-columns: none !important;
    grid-auto-flow: column;
    scroll-snap-type: x mandatory;
    scroll-padding-left: 12px;
    -webkit-overflow-scrolling: touch;
    /* Contain on BOTH axes — y for the rare case where the grid itself
       gets a vertical scroll, x for the inter-column swipe. Without y
       containment, dragging the empty area between cards used to
       bubble up into the browser pull-to-refresh. */
    overscroll-behavior: contain;
  }
  /* Each column's inner card list is its own y-scroller. Contain so
     reaching the top of a column doesn't trigger pull-to-refresh. */
  .kanban-grid > * > div:last-child {
    overscroll-behavior-y: contain;
    -webkit-overflow-scrolling: touch;
  }
  .kanban-grid > * {
    scroll-snap-align: start;
    min-width: 0;
    overflow-x: hidden;
  }
  /* Force any text inside a column to wrap rather than expand the
     column horizontally. Long URLs, hex tokens, run-on subject lines
     all force break-word now. */
  .kanban-grid > * * {
    overflow-wrap: anywhere;
    word-break: break-word;
    min-width: 0;
  }

  /* Inbox header on mobile: don't let the search input + title fight
     for the same row. Stack them vertically; search becomes a thin
     full-width row UNDER the title. Saves real estate above the kanban. */
  .main-head {
    flex-wrap: wrap;
  }
  .main-head .main-actions {
    flex-basis: 100%;
    margin-left: 0 !important;
    margin-top: 6px;
  }
  .main-head .main-actions input.composer-input {
    max-width: 100% !important;
    flex: 1;
  }

  /* Modal sizing on mobile — the .modal class is fine (420px max-width),
     but several modals inline `width: 80vw; max-width: 920px; height: 80vh`
     for desktop sprawl (message detail, doc detail). Force them into the
     viewport on small screens. The `> *` selector covers any direct child
     of the backdrop regardless of class. */
  .modal-backdrop {
    padding:
      max(12px, calc(env(safe-area-inset-top, 0px) + 8px))
      12px
      max(12px, calc(env(safe-area-inset-bottom, 0px) + 8px));
  }
  .modal-backdrop > * {
    width: 100% !important;
    max-width: 100% !important;
    max-height: calc(100vh - 24px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)) !important;
  }
}

/* ── Inbox inline view picker (mobile) ── */
.inbox-view-picker {
  display: flex;
  gap: 6px;
  padding: 8px 16px 0;
}
.inbox-view-btn {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 7px 6px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-muted);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: all 150ms;
}
.inbox-view-btn.active {
  border-color: var(--accent);
  background: var(--accent);
  color: #fff;
  font-weight: 700;
}
/* Hide on desktop — sidebar handles view switching there. */
@media (min-width: 721px) {
  .inbox-view-picker { display: none; }
}

/* ── Billing card ── */
.billing-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px 14px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid #F97316;
  border-radius: 10px;
  cursor: pointer;
  transition:
    transform 240ms cubic-bezier(0.32, 0.72, 0, 1),
    opacity 200ms ease-out,
    max-height 280ms cubic-bezier(0.32, 0.72, 0, 1),
    margin 240ms cubic-bezier(0.32, 0.72, 0, 1),
    padding 240ms cubic-bezier(0.32, 0.72, 0, 1),
    border-width 240ms;
}
.billing-card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
}
.billing-card-client {
  font-weight: 700;
  color: var(--text);
  font-size: 15px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
  line-height: 1.25;
}
.billing-card-header-right {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}
.billing-card-project-badge {
  background: #F9731622;
  color: #F97316;
  border-radius: 6px;
  padding: 3px 10px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.3px;
}
.billing-card-description {
  font-size: 13px;
  color: var(--text-muted);
  line-height: 1.45;
  -webkit-line-clamp: 2;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.billing-card-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
}
.billing-card-meta {
  display: flex;
  align-items: center;
  gap: 8px;
}
.billing-card-time {
  font-size: 12px;
  color: var(--text-subtle);
  font-weight: 500;
}
.billing-card-value {
  background: #F97316;
  color: #fff;
  border-radius: 6px;
  padding: 2px 10px;
  font-size: 12px;
  font-weight: 700;
}
.billing-card-msg-count {
  background: #F9731444;
  color: #F97316;
  border-radius: 999px;
  padding: 1px 7px;
  font-size: 11px;
  font-weight: 700;
}
.card-action-btn.billing-cta {
  background: #F97316;
  color: #fff;
  border: none;
  border-radius: 8px;
  padding: 6px 14px;
  font-size: 12px;
  font-weight: 700;
  white-space: nowrap;
  flex-shrink: 0;
}
.card-action-btn.billing-cta:hover {
  background: #ea6b0e;
}
