/* ════════════════════════════════════════════════════════════════════
 * Huppmail Webmail — light theme, Gmail-grade.
 * Plain CSS, custom properties for theming. Mobile-first with min-width
 * breakpoints. No frameworks.
 * ════════════════════════════════════════════════════════════════════ */

:root {
  --bg: #f6f8fc;
  --surface: #ffffff;
  --surface-2: #f1f3f9;
  --row-hover: #f7f8fb; /* subtle row hover, lighter than --surface-2 */
  --border: #e2e6ee;
  --conv-pad-x: 24px;   /* conversation horizontal padding (drives full-bleed dividers); Gmail-tight, equal L/R */
  --row-divider: rgba(154, 163, 178, 0.20); /* colour of the line between inbox rows (user-picked 2026-07-05) */
  --text: #1f2329;
  --text-soft: #5f6571;
  --text-mute: #8b909c;
  --accent: #2563eb;
  --accent-soft: #e8f0fe;
  --danger: #dc2626;
  --warn: #d97706;
  --ok: #16a34a;
  --shadow: 0 1px 3px rgba(20, 30, 60, .08), 0 4px 18px rgba(20, 30, 60, .06);
  --radius: 12px;
  --topbar-h: 60px;
  --sidebar-w: 240px;
  /* Plus Jakarta Sans: user-picked in the font-demo (2026-07-02), loaded from
     the Google Fonts API in app.ejs. Roboto stays as the Gmail-style fallback. */
  --font: "Plus Jakarta Sans", "Roboto", "RobotoDraft", Helvetica, Arial, sans-serif;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { margin: 0; padding: 0; }

/* THE ROOT MUST PAINT, or iOS shows WHITE around the app.
 *
 * The operator photographed a white band under the mailbox three times, and his
 * on-device readings are what finally explained it. They showed EVERY layout
 * number healthy at the same instant: window 375x640, body fixed at top 0 and
 * bottom 640, no overflow, no gap between the layout and visual viewports. Yet
 * the screen is 812 tall, so 172px of it belongs to Safari's own chrome — and
 * iOS paints THAT area from the root canvas, not from the page layout.
 *
 * Nothing was painting it. `body` carries `background: var(--bg)`, but a
 * background only reaches the canvas from an element IN FLOW, and the shell is
 * `position: fixed`. The glass theme's backdrop cannot help either: it is fixed
 * as well, so it is anchored to the same layout viewport and stops at the same
 * edge. Measured on production before this rule: both `html` and `body`
 * computed to `rgba(0, 0, 0, 0)` and there was no `theme-color` meta, so the
 * exposed strip fell back to the browser default, which is white.
 *
 * `html` is always in flow and always propagates to the canvas, so painting it
 * covers anything iOS ever exposes: the strip under the app, the area behind a
 * collapsing address bar, and the rubber-band overscroll. It changes nothing
 * inside the app, which paints over it completely. */
html { background: var(--bg); }

/* Under the glass theme the shell and its panels are deliberately transparent
 * so the animated mesh shows through, which means the ROOT is what the eye
 * actually sees in the strip iOS keeps for its own toolbars.
 *
 * THE ROOT PAINTS THE SAME MESH, not merely the base tint. A flat tint DID
 * remove the white (the operator's band went from white to peach, which is how
 * we know the root fix reached his phone), but it replaced a white rectangle
 * with a PEACH one: the app above it shows a soft gradient, the strip below
 * shows one flat colour, and the boundary between them is a visible seam. He
 * photographed exactly that on 7 Sep 2026 and replied "LOOOOOOOL".
 *
 * His own on-device readings say why nothing the app draws can ever cover it:
 * the layout viewport is 640 tall while the screen is 812, so 172px belongs to
 * Chrome's chrome, and `100vh` resolves to 748 — 108px MORE than the viewport
 * the page is given. Every `position: fixed` box, the shell and the themed
 * backdrop alike, is clipped to those 640, so only the root canvas reaches the
 * strip.
 *
 * `background-attachment: fixed` anchors the gradient to the viewport rather
 * than to the element box, so the mesh lines up with the backdrop above it
 * instead of restarting at the seam. The gradients are copies of the ones on
 * `.bg-layer` below (kept in step by hand; there are eight and they change
 * rarely). The animation is deliberately NOT copied: the strip is a sliver
 * beside a moving mesh, and an animated root would cost a repaint of the whole
 * canvas for something nobody looks at directly.
 *
 * `:has()` is needed because the theme sits on BODY while the element that must
 * paint is its PARENT. If an engine lacks it, `html { background: var(--bg) }`
 * above still applies, so the strip is a plain tint rather than white. */
:root:has(body[data-bg]) { background-attachment: fixed; background-size: cover; }
:root:has(body[data-bg="aurora"]) {
  background-color: #e9f0ff;
  background-image: radial-gradient(at 16% 18%, #9ec5fd 0, transparent 42%), radial-gradient(at 82% 14%, #c9b6fd 0, transparent 42%), radial-gradient(at 68% 82%, #a7b4fc 0, transparent 48%), radial-gradient(at 22% 86%, #bfe0ff 0, transparent 44%);
}
:root:has(body[data-bg="sunrise"]) {
  background-color: #eef1ff;
  background-image: radial-gradient(at 12% 16%, #a9c6ff 0, transparent 44%), radial-gradient(at 86% 20%, #ffc6a3 0, transparent 42%), radial-gradient(at 78% 84%, #f7a6c8 0, transparent 46%), radial-gradient(at 20% 86%, #c9b6fd 0, transparent 46%);
}
:root:has(body[data-bg="sunset"]) {
  background-color: #ffeede;
  background-image: radial-gradient(at 18% 20%, #ffb38a 0, transparent 44%), radial-gradient(at 82% 16%, #ff7eb3 0, transparent 42%), radial-gradient(at 74% 84%, #b98bff 0, transparent 48%), radial-gradient(at 18% 86%, #ffd6a3 0, transparent 44%);
}
:root:has(body[data-bg="candy"]) {
  background-color: #fbf0fb;
  background-image: radial-gradient(at 18% 22%, #ffc6e6 0, transparent 44%), radial-gradient(at 84% 16%, #b9d4ff 0, transparent 44%), radial-gradient(at 72% 82%, #d7c0ff 0, transparent 48%), radial-gradient(at 16% 84%, #ffd9c2 0, transparent 44%);
}
:root:has(body[data-bg="spectrum"]) {
  background-color: #cdbcff;
  background-image: conic-gradient(from 210deg at 50% 40%, #bfe0ff, #cdbcff, #ffc6e6, #ffe2b8, #c6f3da, #bfe0ff);
}
:root:has(body[data-bg="ocean"]) {
  background-color: #e6f8fb;
  background-image: radial-gradient(at 16% 18%, #a7f0e4 0, transparent 44%), radial-gradient(at 84% 16%, #a3d3ff 0, transparent 44%), radial-gradient(at 76% 84%, #7fd1f7 0, transparent 48%), radial-gradient(at 18% 86%, #c2f5ea 0, transparent 44%);
}
:root:has(body[data-bg="meadow"]) {
  background-color: #eefaf0;
  background-image: radial-gradient(at 14% 18%, #bdf0c8 0, transparent 44%), radial-gradient(at 86% 20%, #a8e0ff 0, transparent 42%), radial-gradient(at 78% 84%, #c8eeb0 0, transparent 48%), radial-gradient(at 20% 84%, #d5f5e0 0, transparent 44%);
}
:root:has(body[data-bg="dusk"]) {
  background-color: #dde6ff;
  background-image: radial-gradient(at 14% 16%, #7fa8ff 0, transparent 44%), radial-gradient(at 86% 18%, #b08bff 0, transparent 42%), radial-gradient(at 80% 86%, #ff9bbf 0, transparent 46%), radial-gradient(at 18% 88%, #9ec0ff 0, transparent 46%);
}

/* Kill the browser's default blue focus outline on clicked elements, forever.
   We never want the blue box that appears when a button/input is focused via
   mouse. Keyboard users still get a subtle, intentional ring (:focus-visible)
   for accessibility, never the default chrome rectangle. */
*:focus { outline: none; }
*:focus:not(:focus-visible) { outline: none; box-shadow: none; }
*:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: 1px; }
body {
  font-family: var(--font);
  color: var(--text);
  background: var(--bg);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

button { font-family: inherit; cursor: pointer; }
a { color: var(--accent); text-decoration: none; }

/* The [hidden] attribute must always win over component display rules
   (e.g. .compose-window{display:flex}), otherwise JS toggling el.hidden
   has no visible effect. */
[hidden] { display: none !important; }

/* ── Buttons ── */
.btn-primary {
  background: var(--accent); color: #fff; border: none;
  padding: 9px 18px; border-radius: 8px; font-weight: 600; font-size: 14px;
}
.btn-primary:hover { background: #1d4ed8; }
.btn-primary:disabled { opacity: .6; cursor: default; }
.btn-block { width: 100%; }
.btn-ghost {
  background: transparent; border: 1px solid var(--border); color: var(--text-soft);
  padding: 7px 14px; border-radius: 8px; font-weight: 500;
}
.btn-ghost:hover { background: var(--surface-2); color: var(--text); }
.icon-btn {
  background: transparent; border: none; color: var(--text-soft);
  width: 38px; height: 38px; border-radius: 50%; display: inline-flex;
  align-items: center; justify-content: center;
}
.icon-btn:hover { background: var(--surface-2); color: var(--text); }
/* A toolbar button that carries a WORD next to its icon. Used for Spam and Not
   spam: those are the two actions a user actively looks for, and an icon alone
   makes them guess. Pill-shaped rather than round, since it holds text. */
.icon-btn.btn-worded {
  width: auto; height: 34px; border-radius: 17px; gap: 6px;
  padding: 0 12px 0 10px; font-size: 13px; font-weight: 500;
}
.icon-btn.btn-worded svg { flex: 0 0 auto; }
#conv-spam:hover, #bulk-spam:hover { background: #fdeaea; color: #a32020; }
#conv-notspam:hover, #bulk-notspam:hover { background: #e8f5ec; color: #1c6b36; }
/* Read-history button carrying a trace: a small amber dot, never a loud badge.
   It says "there is something on record here", the drawer says what. Positioned
   on the button itself so it survives the agent strip clearing on open. */
#conv-history { position: relative; }
#conv-history.alert { color: #97590f; }
#conv-history.alert::after {
  content: ""; position: absolute; top: 7px; right: 7px;
  width: 7px; height: 7px; border-radius: 50%;
  background: #d08706; box-shadow: 0 0 0 2px var(--surface, #fff);
}

/* ════════ LOGIN ════════ */
/* The login page is the FIRST screen on a phone: signed out, mailbox.devv.be
   redirects here. It is a single centred card, so it never overflowed — but it
   failed on the two things a measurement of width cannot see (fixed 2026-09-04,
   after auditing it at 320 and 390 in WebKit). */
.login-page {
  display: flex; align-items: center; justify-content: center;
  /* dvh, not vh: iOS resolves `vh` against the toolbar-HIDDEN viewport, so a
     100vh page is taller than the screen and the card centres against a height
     that is partly under the browser chrome. */
  min-height: 100vh; min-height: 100dvh;
  padding: 20px;
  /* The register form is 4 fields + a button; on a short phone it is taller
     than the screen, so the page must be allowed to scroll to reach Create. */
  padding-top: max(20px, env(safe-area-inset-top));
  padding-bottom: max(20px, env(safe-area-inset-bottom));
  background: radial-gradient(120% 120% at 50% 0%, #eef3ff 0%, var(--bg) 60%);
}
.login-card {
  background: var(--surface); width: 100%; max-width: 400px;
  padding: 38px 34px; border-radius: 16px; box-shadow: var(--shadow);
}
.login-brand { display: flex; align-items: center; gap: 9px; margin-bottom: 26px; }
.brand-mark { color: var(--accent); display: inline-flex; }
.brand-text { font-size: 20px; font-weight: 800; letter-spacing: -.02em; }
.brand-accent { color: var(--accent); }
.login-card h1 { font-size: 22px; margin: 0 0 4px; }
.login-card h2 { font-size: 18px; margin: 6px 0 16px; }
.login-sub { color: var(--text-soft); margin: 0 0 22px; }
.field { display: block; margin-bottom: 16px; }
.field > span { display: block; font-size: 13px; font-weight: 600; color: var(--text-soft); margin-bottom: 6px; }
.field input {
  width: 100%; padding: 12px 13px; border: 1px solid var(--border); border-radius: 9px;
  /* 16px, NOT 14. Under 16px iOS Safari zooms the whole page the moment the
     field is focused, and it never zooms back out — so the very first tap a new
     user makes on this app left them on a page they had to pinch to escape.
     16px is also the right size on desktop, so this is not gated on a media
     query. */
  font-size: 16px; background: var(--surface); color: var(--text);
}
.field input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.login-error { background: #fdecec; color: var(--danger); padding: 9px 12px; border-radius: 8px; font-size: 13px; margin-bottom: 14px; }
.login-foot { margin-top: 18px; font-size: 13px; color: var(--text-soft); text-align: center; }
/* The two links that switch between the sign-in and register forms
   ("Create a mailbox" / "Back to sign in") were 16px-tall inline text, which is
   under half a fingertip. They are the ONLY way between the two forms. */
.login-foot a {
  display: inline-block; padding: 10px 8px; margin: -6px 0;
  font-weight: 600;
}
/* The submit button on this page is the primary action of the whole app's
   entry point, and .btn-primary's 9px padding made it 36px tall. On a phone
   that is the difference between hitting it and hitting the field above. */
.login-card .btn-block { padding: 13px 18px; font-size: 15px; }

/* ════════ APP SHELL ════════ */
/* Gmail model: the whole app sits on a grey background; the topbar and sidebar
   ARE the background, and the main panel floats on top as a rounded white card. */
/* STATIC, deliberately. `position: fixed` was tried here on 5 Sep 2026 and
   REMOVED on 7 Sep after it caused a worse fault than the one it cured. Do not
   reintroduce it without reading this.

   It was introduced to stop the shell being panned off-screen: the shell is
   <body>, so its height is the document height, and during the frame between
   the viewport shrinking and the shell being remeasured the body is briefly
   taller than the window, which lets the engine scroll it and leaves it parked.
   That reasoning was sound and a fixed box does prevent it.

   What it also does, on Chrome for iOS, is anchor the shell to the LAYOUT
   viewport — whose origin is the PHYSICAL TOP of the screen, underneath the
   browser chrome. So `top: 0` means "behind the address bar", not "the top of
   what you can see". His debug ruler proved it: marks 100 to 600 were visible
   and correctly placed while **0 and 50 were missing**, i.e. the shell was the
   right size, drawn ~95px too high, with a bare band of the same height left at
   the foot. Crucially every self-measurement read healthy (top 0, bottom 640,
   overflow 0, no viewport gap), so four rounds of fixes and a purpose-built
   guard all reported success against a visibly broken screen.

   A static body cannot have a wrong origin: normal flow begins below the
   chrome. The panning risk that justified `fixed` is handled by the height
   instead — `100dvh` is the CURRENT visible height, so the shell is never
   taller than the window and the document has no overflow to pan. Verified
   after the change: documentOverflow 0, scrollHeight === clientHeight. */
.mail-app {
  height: 100vh; overflow: hidden; display: flex; flex-direction: column; background: var(--bg);
}

/* THE DOCUMENT MUST NEVER BE SCROLLABLE, and this is the rule that stops it.
 *
 * Captured automatically from his iPhone on 8 Sep 2026, during the fault:
 *   innerH 546, appH 640, scrollY 94, vvTop 94, appTop -94, barTop -94
 * The app was 640 tall inside a 546 window, so it overflowed by 94px, the
 * document therefore had 94px it could scroll, and iOS scrolled exactly that,
 * parking the search bar off the top of the screen. 94 = 640 - 546: the scroll
 * distance IS the overflow, which is what identifies the mechanism rather than
 * merely describing the symptom.
 *
 * Why it survived every previous fix: the app measures itself with
 * getBoundingClientRect, which is relative to the scrolled frame, so its own
 * numbers stayed perfect (height 640, top 0) while the page underneath had
 * moved. Nothing the page could measure about itself could see it.
 *
 * `position: fixed` on the root removes the document from the scrolling model
 * entirely: with nothing scrollable, a stale height can no longer be converted
 * into a displacement, whatever the browser does with its toolbars. The app
 * shell is its own flex container and every list inside it scrolls on its own,
 * so nothing legitimate is lost. */
html, body {
  overflow: hidden;
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  overscroll-behavior: none;
}

.topbar {
  height: var(--topbar-h); flex-shrink: 0;
  display: flex; align-items: center; gap: 14px; padding: 0 16px;
  background: var(--bg); z-index: 20;
}
.menu-toggle { display: none; }
/* Brand occupies the sidebar's width so the search bar's left edge lines up
   exactly with the main panel below it (just like Gmail). */
.topbar-brand { font-size: 19px; font-weight: 800; letter-spacing: -.02em; width: calc(var(--sidebar-w) - 30px); flex-shrink: 0; }
/* Search bar + its advanced-options dropdown */
.search-wrap { flex: 1; max-width: 720px; position: relative; }
.topbar-search {
  display: flex; align-items: center; gap: 6px; position: relative;
  background: var(--surface-2); border-radius: 10px; padding: 4px 6px 4px 8px; color: var(--text-mute);
}
.topbar-search:focus-within { background: var(--surface); box-shadow: 0 1px 3px rgba(20,30,60,.18); }
.topbar-search input { border: none; background: transparent; flex: 1; font-size: 14px; color: var(--text); outline: none; padding: 6px 0; }

/* Ghost completion — the grey preview of the query that Enter/Tab would produce,
   drawn only while a suggestion is highlighted by an arrow key. It is laid
   exactly over the real input, which turns its own text transparent for the
   duration (.ghosting) so the two can never double-render. The caret goes too:
   the preview is longer than what was typed, so a caret parked inside the grey
   tail would point at a character the user has not written. */
.search-ghost {
  position: absolute; pointer-events: none; z-index: 1;
  white-space: pre; overflow: hidden; display: flex; align-items: center;
}
/* No shrinking: a preview longer than the bar must overflow and be clipped on
   the right, exactly as the real input does. Flex's default shrink would
   squeeze the three parts instead and misalign every character. */
.search-ghost .sg-keep, .search-ghost .sg-new { flex: 0 0 auto; }
.search-ghost .sg-keep { color: var(--text); }
.search-ghost .sg-new { color: var(--text-mute); }
.topbar-search input.ghosting { color: transparent; caret-color: transparent; }
.search-ico-btn, .search-clear, .search-adv-btn {
  border: none; background: transparent; color: var(--text-mute);
  width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
}
.search-ico-btn:hover, .search-clear:hover, .search-adv-btn:hover { background: rgba(20,30,60,.08); color: var(--text); }

/* When the options are open, the bar + options read as ONE continuous body:
   the bar keeps its rounded TOP corners and squares its bottom; the options
   panel sits flush beneath it (no gap, same white surface, shared shadow) and
   rounds only its bottom corners. */
.search-wrap.open .topbar-search {
  background: var(--surface); border-radius: 10px 10px 0 0; box-shadow: none;
  border: 1px solid var(--border); border-bottom: none;
}
.search-wrap.open .topbar-search:focus-within { box-shadow: none; }
.search-wrap.open { border-radius: 10px; }

.search-adv {
  position: absolute; top: 100%; left: 0; right: 0;
  background: var(--surface); padding: 14px 18px 16px;
  border-radius: 0 0 10px 10px; z-index: 60;
  border: 1px solid var(--border); border-top: none;
  box-shadow: 0 12px 28px rgba(20,30,60,.22);
}
/* Header of the advanced-search panel. It exists ONLY for the mobile sheet
   (which is full-screen and therefore has no outside to click away on); on
   desktop the panel is a dropdown and this row must not appear at all. */
.adv-sheet-head { display: none; }
.adv-row { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.adv-row > span { width: 130px; flex-shrink: 0; font-size: 13px; color: var(--text-soft); }
.adv-row input[type="text"], .adv-row input[type="date"], .adv-row select {
  flex: 1; border: none; border-bottom: 1px solid var(--border); background: transparent;
  font-size: 14px; color: var(--text); padding: 6px 2px; font-family: var(--font);
}
.adv-row input:focus, .adv-row select:focus { border-bottom-color: var(--accent); }
.adv-checks { gap: 28px; }
.adv-check { width: auto; display: flex; align-items: center; gap: 7px; font-size: 13px; color: var(--text-soft); }
.adv-foot { display: flex; justify-content: flex-end; gap: 10px; margin-top: 6px; align-items: center; }
.adv-foot-spacer { flex: 1; }

/* Split button ("New filter" + caret) in the advanced-search footer. */
.split-btn { position: relative; display: inline-flex; }
.split-btn .split-main { border-radius: 8px 0 0 8px; }
.split-btn .split-caret {
  border-radius: 0 8px 8px 0; border-left: none; padding: 7px 8px;
  display: inline-flex; align-items: center;
}
.split-btn .split-main:hover, .split-btn .split-caret:hover { background: var(--surface-2); color: var(--text); }
.split-menu {
  position: absolute; top: calc(100% + 4px); left: 0; min-width: 200px; padding: 6px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: var(--shadow); z-index: 70;
}
.split-menu .ctx-item:hover { background: var(--surface-2); }
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 12px; min-width: 0; }

/* Address caption — sits just left of the account avatar so there is never any
   doubt about which inbox is on screen. Truncates rather than pushing the
   topbar wide. */
.topbar-account-addr {
  max-width: 240px; min-width: 0; flex: 0 1 auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 12.5px; font-weight: 600; color: var(--text-soft);
  padding: 6px 11px; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--border);
  line-height: 1.2;
}

/* Account switcher */
.account-switcher { position: relative; }
.account-avatar {
  width: 38px; height: 38px; border-radius: 50%; border: none;
  background: var(--accent); color: #fff; font-weight: 700; font-size: 15px;
}
.account-menu {
  position: absolute; right: 0; top: 46px; width: 300px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
  box-shadow: var(--shadow); padding: 8px; z-index: 40;
}
.account-menu-close {
  position: absolute; top: 10px; right: 10px; width: 30px; height: 30px;
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text-soft);
  border-radius: 8px; display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; z-index: 1; transition: background .12s, color .12s, border-color .12s;
}
.account-menu-close:hover { background: var(--surface); color: var(--text); border-color: var(--text-mute); }

/* Active account: big, centered hero at the top of the popup (the VIP slot) */
.account-hero {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 6px; padding: 18px 14px 16px; margin-bottom: 4px;
}
.account-hero:empty { display: none; }
.account-hero-avatar {
  position: relative; width: 60px; height: 60px; border-radius: 50%; background: var(--accent); color: #fff;
  display: inline-flex; align-items: center; justify-content: center; border: none; cursor: pointer;
  font-weight: 700; font-size: 24px; margin-bottom: 4px; background-size: cover; background-position: center;
}
/* Little pencil badge over the hero avatar — the ONLY route into the avatar
   editor now. It sits half on the portrait so it reads as belonging to it.
   The "Change avatar" text button that used to sit under the address is gone
   (REMOVED — do not restore without explicit user request): it said the same
   thing twice and cost a line right where Settings now lives. Slightly larger
   than before, and it takes the accent colour on hover of the whole avatar, so
   a badge that is now carrying the feature alone cannot be missed. */
.account-hero-avatar-edit {
  position: absolute; right: -2px; bottom: -2px; width: 24px; height: 24px; border-radius: 50%;
  background: var(--surface); color: var(--text-soft); border: 1px solid var(--border);
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: 0 1px 4px rgba(20,30,60,.14); transition: color .12s, background .12s, border-color .12s;
}
.account-hero-avatar:hover .account-hero-avatar-edit {
  color: var(--accent); border-color: var(--accent); background: var(--accent-soft);
}

/* Avatar editor panel (swatches + photo controls) inside the account popup */
.avatar-editor { padding: 14px 12px 8px; display: flex; flex-direction: column; align-items: center; gap: 10px; }
.avatar-editor-preview {
  width: 72px; height: 72px; border-radius: 50%; background: var(--accent); color: #fff;
  display: inline-flex; align-items: center; justify-content: center; font-weight: 700; font-size: 28px;
  background-size: cover; background-position: center;
}
.avatar-editor-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--text-mute); align-self: flex-start; padding-left: 2px; }
/* Smaller swatches, more per row (8 across), so many colours fit compactly. */
.avatar-swatches { display: grid; grid-template-columns: repeat(8, 1fr); gap: 6px; width: 100%; }
.avatar-swatch {
  width: 100%; aspect-ratio: 1; border-radius: 50%; border: 2px solid transparent; cursor: pointer;
  position: relative; transition: transform .1s;
}
.avatar-swatch:hover { transform: scale(1.15); }
.avatar-swatch.selected { border-color: var(--text); box-shadow: 0 0 0 2px var(--surface) inset; }
.avatar-editor-actions { display: flex; gap: 8px; width: 100%; }
.avatar-act {
  flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  border: 1px solid var(--border); background: var(--surface); color: var(--text-soft);
  border-radius: 8px; padding: 8px 10px; font-size: 13px; cursor: pointer;
}
.avatar-act:hover { background: var(--surface-2); color: var(--text); }
.avatar-act-remove { color: var(--danger); }
.avatar-editor-done {
  width: 100%; border: none; background: var(--accent); color: #fff; font-weight: 600;
  border-radius: 8px; padding: 9px; cursor: pointer; margin-top: 2px;
}
.avatar-editor-done:hover { filter: brightness(1.05); }
.account-hero-name { font-size: 16px; font-weight: 700; line-height: 1.2; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-hero-addr { font-size: 13px; color: var(--text-soft); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-others-label {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-mute); padding: 8px 10px 4px; border-top: 1px solid var(--border);
}
.account-item {
  width: 100%; display: flex; align-items: center; gap: 11px; padding: 9px 10px;
  border: none; background: transparent; border-radius: 9px; text-align: left;
}
.account-item:hover { background: var(--surface-2); }
.account-item.active { background: var(--accent-soft); }
.account-item-avatar {
  width: 32px; height: 32px; border-radius: 50%; background: var(--accent); color: #fff;
  display: inline-flex; align-items: center; justify-content: center; font-weight: 700; font-size: 13px; flex-shrink: 0;
}
.account-item-info { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.account-item-name { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-item-addr { font-size: 12px; color: var(--text-mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-item-tag { font-size: 11px; color: var(--warn); }
.account-item-check { color: var(--accent); display: inline-flex; }
/* "Add another account" presented as the last row of the account list */
.account-add { cursor: pointer; }
.account-add .account-item-name { color: var(--text-soft); font-weight: 500; }
.account-add:hover .account-item-name { color: var(--text); }
.account-add-avatar {
  background: var(--surface-2) !important; color: var(--text-soft) !important;
  border: 1px dashed var(--border);
}
.account-add:hover .account-add-avatar { color: var(--accent) !important; border-color: var(--accent); }
.account-add-form {
  display: flex; flex-direction: column; gap: 8px; padding: 10px 6px 6px;
  margin-top: 4px; border-top: 1px solid var(--border);
}
.account-add-form input {
  width: 100%; padding: 9px 11px; border: 1px solid var(--border); border-radius: 9px;
  font-size: 13px; background: var(--surface); color: var(--text);
}
.account-add-form input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.account-add-error { background: #fdecec; color: var(--danger); padding: 7px 10px; border-radius: 7px; font-size: 12px; }
.account-add-actions { display: flex; justify-content: flex-end; gap: 8px; }
.account-add-actions .btn-ghost, .account-add-actions .btn-primary { padding: 7px 14px; font-size: 13px; }
/* Secondary, lower-priority "create a new account" path under the sign-in form */
.account-add-alt {
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border);
  font-size: 12px; color: var(--text-mute); text-align: center;
}
.account-add-link {
  border: none; background: transparent; color: var(--accent); cursor: pointer;
  font-size: 12px; font-weight: 600; padding: 0; text-decoration: underline;
}
.account-add-link:hover { color: #1d4ed8; }
/* Settings + Sign out, as two tiles under the active account. They replace the
   full-width "Settings" row and the "Sign out of this account" line that used
   to close the popup (REMOVED — do not restore without explicit user request):
   both sat below the entire account list, so the control the user reaches for
   most was the one furthest from the top. */
.hero-tiles {
  display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 0 4px 12px;
}
.hero-tile {
  display: flex; flex-direction: column; align-items: center; gap: 7px; padding: 14px 8px;
  border: 1px solid var(--border); border-radius: 12px; background: var(--surface);
  color: var(--text-soft); font-size: 12.5px; font-weight: 600; cursor: pointer;
  transition: background .12s, color .12s, box-shadow .12s;
}
.hero-tile svg { color: var(--text-mute); flex-shrink: 0; transition: color .12s; }
.hero-tile:hover { background: var(--surface-2); color: var(--text); box-shadow: 0 2px 10px rgba(20,30,60,.08); }
.hero-tile:hover svg { color: var(--accent); }
.hero-tile-danger:hover { color: var(--danger); background: #fdf2f2; }
.hero-tile-danger:hover svg { color: var(--danger); }

/* Layout — Gmail-style: sidebar + ONE main pane (list OR conversation, never split) */
.mail-layout { flex: 1; display: grid; grid-template-columns: var(--sidebar-w) 1fr; min-height: 0; background: var(--bg); }
/* The main panel floats: rounded white card on the grey background, with a gap
   around it so the background shows through (foreground vs background). */
.main-pane {
  min-width: 0; min-height: 0; display: flex; flex-direction: column;
  background: var(--surface); border-radius: 16px; overflow: hidden;
  margin: 0 16px 16px 0; box-shadow: 0 1px 2px rgba(20,30,60,.06);
}
.main-pane > section { flex: 1; min-height: 0; }

/* Sidebar — part of the background, no border or own surface (Gmail-style) */
.mail-sidebar {
  background: transparent; padding: 8px 8px 16px; display: flex; flex-direction: column;
  overflow-y: auto;
}
.compose-btn {
  display: flex; align-items: center; gap: 10px; padding: 13px 22px; margin-bottom: 14px;
  border: none; background: var(--surface); color: var(--text); font-weight: 600; font-size: 14px;
  border-radius: 16px; box-shadow: var(--shadow); align-self: flex-start;
}
.compose-btn:hover { box-shadow: 0 1px 3px rgba(20,30,60,.12), 0 6px 22px rgba(20,30,60,.12); }
.compose-btn svg { color: var(--accent); }
.folder-nav { display: flex; flex-direction: column; gap: 1px; }
/* Deploy version stamp (vM.D.H.xx) pinned to the bottom of the sidebar. */
.sidebar-version {
  margin-top: auto; flex-shrink: 0; padding: 10px 14px 0;
  font-size: 13px; font-weight: 600; line-height: 1.3;
  color: var(--text-mute); user-select: text;
}
.folder {
  display: flex; align-items: center; gap: 13px; padding: 9px 16px; border-radius: 0 20px 20px 0;
  color: var(--text-soft); font-weight: 500;
}
.folder:hover { background: var(--surface-2); }
.folder.active { background: var(--accent-soft); color: var(--accent); font-weight: 700; }
.folder-ico { display: inline-flex; }
.folder-name { flex: 1; }
.folder-count { font-size: 12px; font-weight: 700; }

/* User labels below the folder nav. Same row geometry as .folder. */
.sidebar-labels { margin-top: 14px; }
.sidebar-labels-head {
  padding: 0 16px 4px; font-size: 12px; font-weight: 700; color: var(--text-mute);
  text-transform: uppercase; letter-spacing: .4px;
}
.label-row {
  display: flex; align-items: center; gap: 13px; padding: 8px 16px;
  border-radius: 0 20px 20px 0; color: var(--text-soft); font-weight: 500; font-size: 14px;
}
.label-row:hover { background: var(--surface-2); }
.label-row.active { background: var(--accent-soft); color: var(--accent); font-weight: 700; }
.label-dot { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; background: var(--text-mute); margin: 0 4px; }
.label-row-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.label-row-count { font-size: 12px; font-weight: 700; color: var(--text-mute); }

/* Label chips on a thread row (Gmail-style, left of the subject). */
.thread-label-chip {
  display: inline-block; padding: 1px 8px; margin-right: 6px; border-radius: 6px;
  font-size: 11px; font-weight: 600; line-height: 16px; vertical-align: 1px;
  background: var(--surface-2); color: var(--text-soft); white-space: nowrap;
}

/* ── Tags: the picker panel, and the removable chips on an open conversation ── */

/* Anchored under the tag button, positioned in JS like .ctx-menu. */
.tag-pop {
  position: fixed; z-index: 95; width: 268px; padding: 8px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  box-shadow: var(--shadow);
}
.tag-pop-head {
  padding: 4px 8px 8px; font-size: 12px; font-weight: 700; letter-spacing: .4px;
  text-transform: uppercase; color: var(--text-mute);
}
.tag-pop-search {
  width: 100%; padding: 7px 10px; border: 1px solid var(--border); border-radius: 8px;
  font: inherit; font-size: 13px; color: var(--text); background: var(--surface);
}
.tag-pop-search:focus { outline: none; border-color: var(--accent); }
/* Caps at about six rows, then scrolls: a mailbox with 30 tags must not grow a
   panel taller than the window. */
.tag-pop-list { max-height: 232px; overflow-y: auto; margin: 6px 0 0; }
.tag-pop-item {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  border: none; background: transparent; padding: 8px 8px; border-radius: 8px;
  font-size: 14px; color: var(--text); cursor: pointer;
}
.tag-pop-item:hover { background: var(--surface-2); }
.tag-pop-box {
  flex-shrink: 0; width: 16px; height: 16px; border-radius: 4px;
  border: 2px solid var(--text-mute); display: inline-flex;
  align-items: center; justify-content: center; color: #fff;
}
.tag-pop-box svg { width: 12px; height: 12px; opacity: 0; }
/* Checked: the box fills with the tag's own colour, so the panel itself teaches
   which colour means what. */
.tag-pop-item.on .tag-pop-box { border-color: transparent; background: var(--accent); }
.tag-pop-item.on .tag-pop-box svg { opacity: 1; }
/* Some of a selection tagged, not all — Gmail's dash state. */
.tag-pop-item.some .tag-pop-box { border-color: transparent; background: var(--text-mute); }
.tag-pop-item.some .tag-pop-box svg { opacity: 1; }
.tag-pop-dot { flex-shrink: 0; width: 10px; height: 10px; border-radius: 3px; background: var(--text-mute); }
.tag-pop-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tag-pop-empty { padding: 10px 8px; font-size: 13px; color: var(--text-mute); }
.tag-pop-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border); }
.tag-pop-create {
  display: flex; align-items: center; gap: 8px; flex: 1; border: none; background: transparent;
  padding: 8px; border-radius: 8px; font-size: 14px; font-weight: 600; color: var(--accent);
  cursor: pointer; text-align: left; overflow: hidden;
}
.tag-pop-create:hover { background: var(--accent-soft); }
.tag-pop-create span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tag-pop-manage {
  border: none; background: transparent; padding: 8px; border-radius: 8px;
  font-size: 13px; color: var(--text-soft); cursor: pointer; white-space: nowrap;
}
.tag-pop-manage:hover { background: var(--surface-2); }

/* Chips on the OPEN conversation, each with its own remove cross. They live
   INSIDE the subject row, to the right of the title. The title is 22px with a
   1.3 line-height, so the chips are nudged down to sit on its optical centre
   rather than hanging off the top of the line. flex-shrink:0 keeps them intact
   and lets the long subject wrap instead. */
.conv-tags {
  display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
  margin-top: 4px;
  /* min-width:0 lets the group wrap to its own line when the tags no longer fit
     beside the subject, instead of forcing the row wider than the screen. */
  min-width: 0;
}
.conv-tags[hidden] { display: none; }
.conv-tag {
  display: inline-flex; align-items: center; gap: 6px; padding: 2px 4px 2px 9px;
  border-radius: 7px; font-size: 12px; font-weight: 600; line-height: 18px;
  background: var(--surface-2); color: var(--text-soft);
}
.conv-tag-x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border: none; border-radius: 4px; padding: 0;
  background: transparent; color: inherit; cursor: pointer; opacity: .6;
}
.conv-tag-x:hover { opacity: 1; background: rgba(0, 0, 0, .12); }
.conv-tag-x svg { width: 11px; height: 11px; }

/* ── Settings → Tags ── */
.tag-manage-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border); }
.tag-manage-row:last-child { border-bottom: none; }
.tag-manage-name { flex: 1; min-width: 0; }
.tag-manage-count { font-size: 12px; font-weight: 700; color: var(--text-mute); white-space: nowrap; }
/* The palette: one swatch per colour, the active one ringed. */
.tag-swatches { display: flex; flex-wrap: wrap; gap: 6px; }
.tag-swatch {
  width: 22px; height: 22px; border-radius: 6px; border: 2px solid transparent;
  cursor: pointer; padding: 0;
}
.tag-swatch.on { border-color: var(--text); }
.tag-swatch-none {
  background: var(--surface-2); color: var(--text-mute);
  display: inline-flex; align-items: center; justify-content: center; font-size: 13px;
}
/* Account-health badge — lives in the topbar, just left of the account avatar.
   Only shown when the account is not healthy; clicking it opens Settings. */
.health-badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 12px; border-radius: 999px; border: 1px solid transparent;
  font-size: 12.5px; font-weight: 600; cursor: pointer; line-height: 1;
  transition: filter .12s, box-shadow .12s;
}
.health-badge svg { flex-shrink: 0; }
.health-badge:hover { filter: brightness(0.97); box-shadow: 0 1px 4px rgba(0,0,0,.08); }
.health-badge.pending { background: #fdecec; color: var(--danger); border-color: #f6cccc; }
.health-badge.issues { background: #fff4e5; color: var(--warn); border-color: #f5dfba; }

/* Thread list (now fills the whole main pane, Gmail-style) */
/* Floating Compose button. Hidden by default — it exists ONLY for the phone
   layout, where the real Compose button is inside the off-canvas drawer. The
   mobile block below reveals it. */
.compose-fab { display: none; }
.thread-list-pane { background: var(--surface); display: flex; flex-direction: column; min-height: 0; }
.list-toolbar { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; }
/* While the bulk action bar is up, hide the plain title toolbar so the list
   header doesn't show two stacked rows. */
.thread-list-pane.has-bulk .list-toolbar { display: none; }
.list-title { font-weight: 700; font-size: 15px; }
/* "Search Gmail" escalation button in the search-results toolbar. Pushed to the
   right (just before the refresh button) and only visible in search mode. */
.search-escalate-top { margin-left: auto; margin-right: 8px; padding: 5px 12px; font-size: 13px; cursor: pointer; }
.search-escalate-top:disabled { opacity: .6; cursor: default; }
/* Refresh button spins while a reload is in flight. */
@keyframes refresh-spin { to { transform: rotate(360deg); } }
#refresh-btn.spinning svg { animation: refresh-spin .6s linear infinite; transform-origin: 50% 50%; }
/* New-mail nudge: after 30s of held arrivals with no safe window, the refresh
   button expands horizontally into a glowing accent pill with a "N new
   messages" label — the click that answers it is unambiguous intent (and puts
   the pointer on the toolbar, out of the list). max-width tweens the reveal;
   the glow breathes so it reads from the corner of the eye. */
#refresh-btn { transition: background .25s ease, box-shadow .25s ease, color .25s ease; }
#refresh-btn .refresh-new-label {
  max-width: 0; overflow: hidden; white-space: nowrap; opacity: 0;
  font-size: 13px; font-weight: 600;
  transition: max-width .45s cubic-bezier(.4, 0, .2, 1), opacity .3s ease .1s,
    margin-left .45s cubic-bezier(.4, 0, .2, 1);
}
#refresh-btn.has-new {
  width: auto; border-radius: 19px; padding: 0 14px 0 10px;
  background: var(--accent); color: #fff;
  animation: refresh-new-glow 2s ease-in-out infinite;
}
#refresh-btn.has-new .refresh-new-label { max-width: 160px; opacity: 1; margin-left: 7px; }
@keyframes refresh-new-glow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(37, 99, 235, .45); }
  50% { box-shadow: 0 0 0 7px rgba(37, 99, 235, 0); }
}
@media (prefers-reduced-motion: reduce) {
  #refresh-btn.has-new { animation: none; box-shadow: 0 0 0 3px var(--accent-soft); }
}
/* Last-reload caption in the list toolbar ("Refreshed 2 minutes ago · tab
   focus"). A quiet text button; clicking opens the reload-history panel. */
.reload-caption { margin-left: auto; margin-right: 10px; border: 0; background: none;
  font: inherit; font-size: 12px; color: var(--text-mute); cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 46%; }
.reload-caption:hover { color: var(--text); }
.reload-caption:empty { display: none; }
/* When the caption is present it owns the auto margin; keep the Search-Gmail
   escalation button's own auto margin from double-pushing. */
.list-toolbar .reload-caption + .reload-debug + .search-escalate-top { margin-left: 0; }
/* Reload-history debug panel (anchored under the toolbar, above the list). */
.reload-debug { position: absolute; top: 44px; right: 14px; z-index: 60;
  width: 380px; max-height: 320px; overflow-y: auto; background: var(--surface);
  border: 1px solid var(--border); border-radius: 10px;
  box-shadow: 0 8px 28px rgba(31,35,41,.14); padding: 6px 0; }
.thread-list-pane { position: relative; }
.reload-debug-head { padding: 8px 14px 6px; font-size: 12px; font-weight: 700;
  color: var(--text-mute); text-transform: uppercase; letter-spacing: .04em; }
.reload-debug-row { display: flex; gap: 10px; align-items: baseline;
  padding: 5px 14px; font-size: 12.5px; }
.reload-debug-row.skip { opacity: .55; }
.reload-debug-row .rd-time { font-variant-numeric: tabular-nums; color: var(--text); flex: none; }
.reload-debug-row .rd-ago { color: var(--text-mute); flex: none; min-width: 88px; }
.reload-debug-row .rd-reason { color: var(--text); }
.reload-debug-empty { padding: 14px; text-align: center; color: var(--text-mute); font-size: 12.5px; }
.thread-list { flex: 1; overflow-y: auto; }
.list-empty { padding: 40px 20px; text-align: center; color: var(--text-mute); }
/* Gmail search preview (Option A): matches shown as importable rows below the
   local results. Click a row to import that one conversation and open it. */
.gmail-preview { border-top: 1px solid var(--border); margin-top: 4px; }
.gmail-preview-head { padding: 10px 16px; font-size: 12px; font-weight: 600;
  color: var(--text-mute); text-transform: uppercase; letter-spacing: .04em; }
.gmail-preview-empty { padding: 18px 16px; text-align: center; color: var(--text-mute); }
/* "Load more from Gmail" pagination footer under the preview rows. */
.gmail-preview-more { padding: 14px 16px; text-align: center; }
.gmail-preview-more .btn-ghost { cursor: pointer; }
.gmail-preview-more .btn-ghost:disabled { opacity: .6; cursor: default; }
.gmail-row { cursor: pointer; opacity: .92; }
.gmail-row:hover { opacity: 1; background: var(--surface-2); }
/* Snoozed folder: "auto-imported with placeholder wake date" reminder notice.
   A standalone centered pill in the folder body, detached from the rows. */
.snooze-auto-banner { display: flex; align-items: center; justify-content: center; gap: 10px;
  width: fit-content; max-width: 620px; margin: 28px auto 18px;
  padding: 12px 24px; border-radius: 12px;
  background: #ede9fe; border: 1px solid #ddd6fe;
  font-size: 13px; color: #5b21b6; line-height: 1.5; text-align: left; }
.snooze-auto-banner svg { flex: none; }

/* "Kept unread" bar — sits under the conversation toolbar while the open thread
   is being deliberately held unread. Calm blue (an informative state, not a
   warning) so it reassures rather than alarms; it is the signal that the user
   can keep reading, walk newer/older, or refresh with nothing being consumed.
   A pure STATUS strip: it carries NO button of its own and NO toast accompanies
   it (user-decided 2026-08-02, both read as too much) — the toolbar envelope is
   the single control for the state. */
.conv-unread-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 20px; background: var(--accent-soft);
  border-bottom: 1px solid rgba(37, 99, 235, .18);
  animation: kept-unread-in .18s ease-out;
}
@keyframes kept-unread-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.conv-unread-ico { display: inline-flex; color: var(--accent); flex: none; }
/* min-width:0 lets the text shrink instead of forcing the bar (and with it the
   whole page) wider than a narrow phone viewport. */
.conv-unread-text { font-size: 13px; font-weight: 600; color: #1d4ed8; flex: 1 1 auto; min-width: 0; }
/* Narrow phones: tighter gutters so the bar never widens the page. */
@media (max-width: 420px) {
  .conv-unread-bar { padding: 9px 12px; gap: 8px; }
  .conv-unread-text { font-size: 12.5px; }
}
/* The toolbar envelope lights up while the thread is held unread, so the state
   is legible from the toolbar alone once the bar has been read and ignored. */
#conv-unread.on { color: var(--accent); }
@media (prefers-reduced-motion: reduce) { .conv-unread-bar { animation: none; } }

/* "Snoozed until <date>" bar — the exact twin of the kept-unread bar above, in
   the snooze violet already used for the Snoozed folder's dates. It answers the
   two questions a snooze raises on the open conversation ("did that work?" and
   "why is this still unread?") without adding any control of its own: the
   toolbar clock re-opens the picker, the Snoozed folder does the unsnoozing. */
.conv-snooze-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 20px; background: #f3efff;
  border-bottom: 1px solid rgba(124, 58, 237, .18);
  animation: kept-unread-in .18s ease-out;
}
.conv-snooze-ico { display: inline-flex; color: #7c3aed; flex: none; }
/* min-width:0 lets the text shrink instead of widening the page on a phone. */
.conv-snooze-text { font-size: 13px; font-weight: 600; color: #6d28d9; flex: 1 1 auto; min-width: 0; }
/* The toolbar clock lights up while the thread is snoozed, matching the
   envelope's behaviour, so the state survives after the bar is scrolled past. */
#conv-snooze.on { color: #7c3aed; }
@media (max-width: 420px) {
  .conv-snooze-bar { padding: 9px 12px; gap: 8px; }
  .conv-snooze-text { font-size: 12.5px; }
}
@media (prefers-reduced-motion: reduce) { .conv-snooze-bar { animation: none; } }

/* ── Agent status strip on the open conversation ──────────────────────────────
   Sibling of the snooze bar, same anatomy, two moods:
     .working  (blue)  an agent holds this thread right now
     .returned (amber) the safety net put it back to unread and says why
   Both carry the "Read history" button, which is the escape hatch from
   reassurance into evidence. */
.conv-agent-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 20px;
  font-size: 13px; line-height: 1.45;
}
.conv-agent-bar.working {
  background: #eaf1fd;
  border-bottom: 1px solid rgba(26, 86, 184, .18);
  color: #16418c;
}
.conv-agent-bar.returned {
  background: #fdf5e8;
  border-bottom: 1px solid rgba(151, 89, 15, .2);
  color: #7d4a0c;
}
/* .reserved — an agent is holding this conversation RIGHT NOW. Same purple as
   the list row's "Reserved" chip so the two read as one feature: he sees the
   chip in the list, opens it, and the same colour tells him the hold is still
   on. Calm on purpose, exactly like the chip: a held thread is the system
   working, not a problem, and reading it changes nothing. */
.conv-agent-bar.reserved {
  background: #f3edfc;
  border-bottom: 1px solid #e0d2f5;
  color: #6b3fa0;
}
.conv-agent-ico { display: inline-flex; flex-shrink: 0; }
.conv-agent-bar.working .conv-agent-ico svg {
  animation: agentspin 3.2s linear infinite; transform-origin: 50% 50%;
}
.conv-agent-text { flex: 1; min-width: 0; }
.conv-agent-link {
  flex-shrink: 0; font: inherit; font-weight: 700; font-size: 12.5px;
  color: inherit; background: rgba(255, 255, 255, .7);
  border: 1px solid currentColor; border-radius: 6px;
  padding: 4px 11px; cursor: pointer; opacity: .9;
}
.conv-agent-link:hover { opacity: 1; background: #fff; }
@media (max-width: 420px) {
  .conv-agent-bar { padding: 9px 12px; gap: 8px; flex-wrap: wrap; }
  .conv-agent-text { font-size: 12.5px; flex: 1 1 100%; order: 2; }
  .conv-agent-link { order: 3; }
}
@media (prefers-reduced-motion: reduce) {
  .conv-agent-bar.working .conv-agent-ico svg { animation: none; }
}

/* ── Unfollowed-thread warning ────────────────────────────────────────────────
   An agent SENT into this conversation, the other side replied, and nobody has
   treated that reply. Third sibling of the bars above, same anatomy.

   A DEEPER amber than .conv-agent-bar.returned on purpose. That one explains a
   thing the system already fixed (your mail came back, nothing is lost); this
   one reports something still broken right now. Same family so it reads as part
   of the set, more saturated so it outranks its sibling when both are on
   screen. */
.conv-followup-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 20px;
  font-size: 13px; line-height: 1.45;
  background: #fdece8;
  border-bottom: 1px solid rgba(180, 60, 30, .22);
  color: #9a3412;
  font-weight: 600;
  animation: kept-unread-in .18s ease-out;
}
.conv-followup-ico { display: inline-flex; flex-shrink: 0; }
.conv-followup-text { flex: 1; min-width: 0; }
/* Same shape as .conv-agent-link so the two bars read as one family. */
.conv-followup-link {
  flex-shrink: 0; font: inherit; font-weight: 700; font-size: 12.5px;
  color: inherit; background: rgba(255, 255, 255, .7);
  border: 1px solid currentColor; border-radius: 6px;
  padding: 4px 11px; cursor: pointer; opacity: .9;
}
.conv-followup-link:hover { opacity: 1; background: #fff; }
.conv-followup-link[disabled] { opacity: .55; cursor: default; }
@media (max-width: 420px) {
  .conv-followup-bar { padding: 9px 12px; gap: 8px; flex-wrap: wrap; }
  .conv-followup-text { font-size: 12.5px; flex: 1 1 100%; order: 2; }
  .conv-followup-link { order: 3; }
}
@media (prefers-reduced-motion: reduce) { .conv-followup-bar { animation: none; } }

/* ── "Sent by agent" chip on a message header ─────────────────────────────────
   Marks an outbound message a machine sent in the operator's name. Sits right
   after the sender, because the sender line is what it corrects.

   Deliberately NEUTRAL slate, not a warning colour: an agent sending mail is
   the system working as intended, and the chip states a fact rather than
   raising an alarm. The amber above is reserved for the thread that is actually
   being neglected. */
.msg-agent-sent {
  display: inline-flex; align-items: center; gap: 4px;
  flex-shrink: 0; margin-left: 8px;
  padding: 1px 7px 1px 5px;
  border: 1px solid #cbd5e1; border-radius: 999px;
  background: #f1f5f9; color: #475569;
  font-size: 11px; font-weight: 700; line-height: 1.6;
  letter-spacing: .01em; white-space: nowrap;
  cursor: default; vertical-align: middle;
}
.msg-agent-sent svg { flex-shrink: 0; opacity: .85; }
.msg-agent-sent:hover { background: #e2e8f0; border-color: #94a3b8; color: #334155; }
/* "Treated by agent" — the read-state twin. Same neutral slate as its sibling
   (it states a fact, never an alarm), but it is often an <a> to the handling
   session, so give the link form a pointer and drop the default underline. */
a.msg-agent-treated { text-decoration: none; }
a.msg-agent-treated:hover { cursor: pointer; }
/* On a narrow phone the header has no room for the word: keep the robot glyph,
   which is unambiguous next to a sender name, and let the tooltip carry the
   rest. Better a legible icon than a chip that pushes the date off screen. */
@media (max-width: 520px) {
  .msg-agent-sent { margin-left: 6px; padding: 1px 5px; }
  .msg-agent-sent-txt { display: none; }
}

/* ── Read-history drawer ──────────────────────────────────────────────────────
   The full read-state ledger for one conversation. Slides in from the right so
   the conversation stays visible behind it: the point is to explain THIS
   thread, not to replace it. */
/* z-index 2000 clears every popover in the app (the translate popover sits at
   1200, the resize box at 3000 but only inside an editor). Equally important,
   this element is a direct child of <body>: it used to live inside
   .mail-layout, which creates a stacking context, so its z-index was measured
   against its SIBLINGS rather than against the page and the search bar's
   popovers painted straight over it however high the number went. */
.rh-overlay {
  position: fixed; inset: 0; z-index: 2000;
  background: rgba(16, 22, 32, .38);
  display: flex; justify-content: flex-end;
  animation: rhFade .16s ease;
}
@keyframes rhFade { from { opacity: 0; } }
/* Twice the old 460px. The timeline carries long session titles next to a
   sentence, so a narrow drawer wrapped nearly every row onto two lines and
   undid the compacting. Capped at 92vw so it still behaves on a small screen. */
.rh-panel {
  width: min(920px, 92vw); height: 100%;
  background: #fff; display: flex; flex-direction: column;
  box-shadow: -8px 0 30px rgba(0, 0, 0, .16);
  animation: rhSlide .2s cubic-bezier(.2, .8, .3, 1);
}
@keyframes rhSlide { from { transform: translateX(24px); opacity: .6; } }
.rh-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  padding: 18px 22px 15px; border-bottom: 1px solid var(--border);
  background: var(--surface);
}
.rh-head-main { min-width: 0; }
/* NB: the drawer's heading is .rh-title-MAIN, never .rh-title. `.rh-title` is
   the SESSION TITLE inside a timeline row, and for a while both used the same
   class with two conflicting rules — the heading's 16px/margin:0 and the row's
   plain bold fought each other down the file. Two names now. */
.rh-title-main { margin: 0; font-size: 16.5px; font-weight: 700; letter-spacing: -.01em; }
.rh-sub { margin: 5px 0 0; font-size: 12.5px; color: var(--text-soft); line-height: 1.5; }
.rh-body { flex: 1; overflow-y: auto; padding: 18px 22px 30px; }
.rh-loading, .rh-empty { color: var(--text-soft); font-size: 13px; }

/* ── The lead card: the CURRENT state, before any history ────────────────────
   This used to be one run-on paragraph of five sentences, which is unreadable
   at a glance — the operator opens this drawer with ONE question ("what is
   going on with this mail?") and had to read a wall to answer it. It is now a
   small card: a heading, then labelled facts in a grid, then one line of
   consequence. Same information, scannable. */
.rh-note {
  font-size: 12.5px; line-height: 1.5; border-radius: 10px;
  padding: 13px 15px; margin-bottom: 18px;
}
.rh-note-head {
  display: flex; align-items: center; gap: 8px;
  font-size: 13.5px; font-weight: 700; margin-bottom: 9px;
}
.rh-note-head svg { flex-shrink: 0; }
/* The facts, as a two-column grid: label on the left, value on the right, each
   on its own line. A colon-separated run of "Workspace: x Session: y Held: z"
   is exactly the concatenated shape this replaces. */
.rh-note-facts {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: 3px 12px; margin: 0 0 9px;
}
.rh-note-facts dt {
  font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; opacity: .72; white-space: nowrap; align-self: baseline;
}
.rh-note-facts dd {
  margin: 0; min-width: 0; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The closing line: what this means for him. Separated by a hairline so it
   reads as the conclusion rather than as a fourth fact. */
.rh-note-foot {
  border-top: 1px solid currentColor; padding-top: 8px; margin-top: 2px;
  opacity: .9;
}
.rh-note.working { background: #eaf1fd; color: #16418c; border: 1px solid #cfe0fa; }
.rh-note.returned { background: #fdf5e8; color: #7d4a0c; border: 1px solid #f3e2c4; }
/* A live reservation. Same violet as the row chip, so "this is held by an
   agent" reads as one idea across the list and the drawer. */
.rh-note.reserved { background: #f5effd; color: #58317f; border: 1px solid #e0d2f5; }
.rh-note.reserved a { color: inherit; font-weight: 700; }
.rh-note-foot { border-top-color: rgba(0, 0, 0, .12); }

.rh-list { list-style: none; margin: 0; padding: 0 0 0 4px; }
/* Day separator: the date is printed ONCE per day instead of on every row,
   which is where most of the old vertical bulk came from. */
.rh-day {
  padding: 10px 0 4px; font-size: 11px; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; color: var(--text-soft);
}
.rh-day:first-child { padding-top: 0; }
/* A new message is the chapter break of this timeline: the operator's question
   is nearly always "what happened SINCE it arrived?". A full-width rule with
   the label sitting on it, so it reads as a divider rather than as one more
   event row. */
.rh-arrival {
  display: flex; align-items: center; gap: 10px;
  margin: 13px 0 11px; padding: 0;
  font-size: 11.5px; font-weight: 700; color: #2f6bd4;
  letter-spacing: .02em;
}
.rh-arrival::before, .rh-arrival::after {
  content: ""; height: 1px; background: #cfdcf3; flex: 0 0 auto;
}
.rh-arrival::before { width: 14px; }
.rh-arrival::after { flex: 1 1 auto; }
.rh-arrival-time { font-variant-numeric: tabular-nums; color: #2f6bd4; }
.rh-arrival-label { text-transform: uppercase; }
.rh-arrival-ago { font-weight: 600; color: #7d90b3; }
/* THE ROW, rebuilt 2026-09-11. The old six-column grid put time, age, actor,
   sentence and pill on ONE baseline, so a row read as five unrelated fragments
   run together and the columns drifted apart on a wide drawer — the "blocks of
   concatenated text with no formatting" the operator reported.
   Now each event is a small CARD on a rail: WHO on the first line, WHAT on the
   second, the clock in a fixed left gutter, the outcome pill on the right. The
   eye lands on one row at a time instead of scanning five columns. */
.rh-item {
  position: relative; display: grid;
  grid-template-columns: 62px 16px minmax(0, 1fr) auto;
  grid-template-areas:
    "when rail who  mark"
    "when rail what mark";
  gap: 0 10px; align-items: start;
  padding: 9px 11px 9px 0; border-radius: 9px;
  font-size: 13px; line-height: 1.5;
}
.rh-item:hover { background: var(--surface-2); }
.rh-item .rh-when { grid-area: when; justify-self: end; padding-top: 1px; }
.rh-item .rh-who  { grid-area: who; overflow: hidden; text-overflow: ellipsis; }
.rh-item .rh-what { grid-area: what; }
.rh-item .rh-mark-cell { grid-area: mark; align-self: center; }
/* The rail: a vertical line through the whole list with each event's dot on
   it, so the rows read as one timeline rather than as separate lines. */
.rh-item .rh-dot { grid-area: rail; }
/* The rail must run the FULL height of the row and bridge the gap to the next
   one, or it reads as a stack of little ticks rather than one line. `height:
   100%` on a grid item resolves against the grid AREA (40px of a 58px row), not
   the row box — measured 11 Sep 2026 — so it is positioned against the row
   itself instead. It is drawn BEHIND the dot, which carries a ring of page
   colour to punch through it. */
/* x = the "when" column + the column gap + half the 16px rail column, which is
   where the dot centres. Measured on the live page: dot centre sits 80px from
   the row's left edge at the default size (62 + 10 + 8), so the line is placed
   there and shifted back by half its own width. */
.rh-item::before {
  content: ""; position: absolute; top: 0; bottom: -1px;
  left: 80px; margin-left: -1px;
  /* #e6eaf2 was too close to white to register at all (measured: it painted
     correctly and was still invisible in a screenshot). This is the same tone
     the arrival divider already uses, so the two read as one timeline. */
  width: 2px; background: #cfdcf3;
}
@media (max-width: 700px) { .rh-item::before { left: 69px; } }
@media (max-width: 420px) { .rh-item::before { left: 63px; } }
/* The first and last rows of a day taper the line so the timeline does not
   collide with the day heading above it. */
.rh-day + .rh-item::before { top: 6px; }
/* The age moves UNDER the clock, so the left gutter carries the whole "when"
   and the sentence line is never interrupted by it. */
.rh-item .rh-ago {
  grid-area: when; justify-self: end; align-self: end;
  font-size: 11px; padding-bottom: 1px;
}
@media (max-width: 700px) {
  .rh-item { grid-template-columns: 52px 14px minmax(0, 1fr) auto; font-size: 12.5px; }
}
/* The dot sits ON the rail, so it needs a ring of page colour to punch a hole
   in the line rather than sitting on top of it. */
.rh-dot {
  position: relative; z-index: 1;
  width: 9px; height: 9px; border-radius: 50%;
  align-self: start; justify-self: center; margin-top: 5px;
  box-shadow: 0 0 0 3px var(--surface);
}
.rh-item:hover .rh-dot { box-shadow: 0 0 0 3px var(--surface-2); }
.rh-read .rh-dot { background: #c9820f; }
.rh-unread .rh-dot { background: #0f8a5f; }
.rh-arrived .rh-dot { background: #9aa3b2; }
/* `reserved` and `noturn` had NO colour at all, so those rows showed an empty
   hole on the rail (invisible before the rail existed, obvious after it).
   Violet for a reservation, matching the chip and the strip; hollow grey for a
   watcher that merely matched and never held anything. */
.rh-reserved .rh-dot { background: #6b3fa0; }
.rh-noturn .rh-dot { background: #fff; box-shadow: 0 0 0 3px var(--surface), inset 0 0 0 2px #b9c2d0; }
.rh-item.rh-noturn:hover .rh-dot { box-shadow: 0 0 0 3px var(--surface-2), inset 0 0 0 2px #b9c2d0; }
/* Agent-lifecycle beats, deliberately louder than a routine flip: the nudge and
   the sweep are the rows that explain a surprise. */
.rh-nudge .rh-dot { background: #1a56b8; }
.rh-resolved .rh-dot { background: #0f8a5f; }
.rh-swept .rh-dot { background: #d04406; }
.rh-swept .rh-what, .rh-nudge .rh-what { color: var(--text); }
.rh-swept .rh-what { font-weight: 600; }
.rh-when {
  color: var(--text); font-weight: 600; white-space: nowrap;
  font-variant-numeric: tabular-nums; font-size: 12px;
}
/* Relative age, UNDER the clock in the same gutter. When a thread turns bold
   unexpectedly the first question is "how long ago", not "at what time", so it
   keeps the strong violet even at the smaller size. */
.rh-ago { color: #5b21b6; font-weight: 700; white-space: nowrap; }
.rh-who { font-weight: 700; white-space: nowrap; }
/* The sentence is its own line now, so the old "— " prefix is gone: it existed
   only to separate the sentence from the actor when both shared one line, and
   it is exactly what made a row read as concatenated fragments. */
.rh-what { display: block; margin-top: 1px; }
/* Agent actor: always "<workspace> → <session title>". The workspace is the
   quieter half (it repeats down the list and is context, not identity), the
   title is the loud half, and the arrow is faint punctuation between them. */
.rh-ws { font-weight: 600; color: var(--text-soft); }
.rh-arrow { color: #c3cad6; margin: 0 5px; font-weight: 400; }
.rh-title { font-weight: 700; }
.rh-what { color: var(--text-soft); }
/* The marker the act produced, as a small pill after its own arrow. Two
   independent markers live here: read/unread, and treated/not treated. Keeping
   them visually distinct from the sentence is what lets the operator scan the
   right-hand edge and see how the state actually moved. */
/* The marker sits in its own right-hand cell so a long sentence can never
   break the pill onto a second line. */
.rh-mark-cell { white-space: nowrap; justify-self: end; }
.rh-mark-arrow { color: #c3cad6; margin: 0 6px; }
.rh-mark {
  font-weight: 700; padding: 1px 7px; border-radius: 20px; font-size: 11.5px;
  white-space: nowrap;
}
.rh-mark-read { background: #fdf3e3; color: #97590f; }
.rh-mark-unread { background: #e6f5ee; color: #0b6b49; }
.rh-mark-treated { background: #e6f5ee; color: #0b6b49; }
.rh-mark-untreated { background: #fdeae2; color: #a5350a; }
.rh-mark-pending { background: #eef1f7; color: #4a5468; }
/* "never held it" / "was held" — statements of fact with no verdict in them, so
   they must not borrow the green of treated or the red of untreated. */
.rh-mark-neutral { background: #f1f3f7; color: #5b6577; }

/* The session title becomes a link on any row whose holder is a real chat
   session, which is what lets the operator jump from "who held this mail" into
   that conversation. Underlined only on hover: every reservation row carries one,
   and a wall of permanent underlines would make the log look like a link list. */
.rh-title-link { color: inherit; text-decoration: none; cursor: pointer; }
.rh-title-link:hover { text-decoration: underline; }
/* The watchers that matched and never got a turn are CONTEXT, not events. Muted
   so the eye lands on the reservation that actually happened. */
.rh-item.rh-noturn { opacity: .72; }
.rh-item.rh-noturn .rh-title { font-weight: 600; }
@media (max-width: 420px) {
  /* On a phone the outcome pill cannot keep its own right-hand column without
     squeezing the sentence to a few characters, so it drops onto a third line
     under the text. The clock keeps its gutter: it is what the operator scans.
     (The old override pinned every cell into one column because the row was a
     single line; the row is now two lines by design, so only the pill moves.) */
  .rh-item {
    grid-template-columns: 46px 14px minmax(0, 1fr);
    grid-template-areas:
      "when rail who"
      "when rail what"
      "when rail mark";
  }
  .rh-item .rh-mark-cell { justify-self: start; white-space: normal; margin-top: 3px; }
  .rh-who { white-space: normal; }
}

/* External (Gmail) read-only preview: banner + hidden local-only chrome. */
.ext-preview-bar { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 10px 20px; background: #fef3c7; border-bottom: 1px solid #fde68a; }
.ext-preview-text { font-size: 13px; font-weight: 600; color: #92400e; }
.ext-preview-import { padding: 7px 16px; font-size: 13px; }
/* In external read mode, hide the local-mail action buttons (archive/delete/
   unread/snooze/nav) and the reply bar — none apply to un-imported Gmail mail. */
.mail-layout.ext-reading #conv-inbox,
.mail-layout.ext-reading #conv-archive,
.mail-layout.ext-reading #conv-delete,
.mail-layout.ext-reading #conv-delete-forever,
.mail-layout.ext-reading #conv-unread,
.mail-layout.ext-reading #conv-unread-bar,
.mail-layout.ext-reading #conv-snooze-bar,
.mail-layout.ext-reading #conv-snooze,
.mail-layout.ext-reading #conv-star,
.mail-layout.ext-reading #conv-nav,
.mail-layout.ext-reading .conv-reply-bar,
.mail-layout.ext-reading .msg-actions,
.mail-layout.ext-reading .conv-toolbar-sep { display: none !important; }
.gmail-row.importing { opacity: .5; pointer-events: none; }
.gmail-row-time { margin-left: 10px; }
.gmail-import-btn {
  flex: none; margin-left: 12px; padding: 4px 14px; font-size: 13px; font-weight: 600;
  color: var(--accent); background: transparent; border: 1px solid var(--border);
  border-radius: 8px; cursor: pointer; transition: background .12s ease, border-color .12s ease;
}
.gmail-import-btn:hover { background: var(--accent-soft); border-color: var(--accent); }
.gmail-row-badge { flex: none; font-size: 11px; font-weight: 700; color: #b45309;
  background: #fef3c7; border-radius: 4px; padding: 2px 6px; margin-left: 4px;
  width: 40px; text-align: center; box-sizing: border-box; }
.search-escalate { display: flex; flex-direction: column; align-items: center; gap: 14px; }
.search-escalate-text { color: var(--text-mute); }
.search-escalate .btn-ghost { cursor: pointer; }
.search-escalate .btn-ghost:disabled { opacity: .6; cursor: default; }
/* Footer variant: shown BELOW search results (not the empty state) so the user
   can reach into Gmail even when some local mail matched. */
.search-escalate-footer { flex-direction: row; justify-content: center; gap: 12px;
  padding: 18px 20px; border-top: 1px solid var(--border); margin-top: 4px; }
.search-escalate-footer .search-escalate-text { font-size: 13px; }

/* Dead external-account banner — a connected Gmail account whose credentials
   were refused stops importing mail permanently and SILENTLY (the worker only
   polls 'active' accounts, so it is never retried and logs nothing further).
   The old signal was one line inside Settings, which is why an account once
   sat dead for three weeks unnoticed. This sits above the thread list in every
   folder until it is dealt with, so it cannot be missed. Deliberately uses the
   danger palette, not the softer warn one: mail is being lost. */
.ext-dead-banner { border-bottom: 1px solid #f6cccc; background: #fdecec; }
.ext-dead-row { display: flex; align-items: center; gap: 11px; padding: 11px 16px; }
.ext-dead-row + .ext-dead-row { border-top: 1px solid #f6cccc; }
.ext-dead-icon { flex-shrink: 0; color: var(--danger); }
.ext-dead-text { flex: 1; min-width: 0; font-size: 13px; line-height: 1.45; color: #7f1d1d; }
.ext-dead-text strong { font-weight: 600; color: var(--danger); }
.ext-dead-fix { flex-shrink: 0; border: 1px solid #e9b4b4; color: var(--danger); font-weight: 600; }
.ext-dead-fix:hover { background: #f9dcdc; color: var(--danger); }
/* Dismiss is the secondary action: same shape as "Fix it" but muted, so the
   eye still lands on the fix. It only snoozes the row for 7 days. */
.ext-dead-dismiss { flex-shrink: 0; border: 1px solid #ecd2d2; color: #9a6a6a; font-weight: 500; }
.ext-dead-dismiss:hover { background: #f9e6e6; color: #7f1d1d; }

/* Bulk action bar — shown when one or more rows are checked. Sits where the
   list-toolbar is and shares its row height so the list doesn't jump. */
.bulk-bar { display: flex; align-items: center; gap: 2px; padding: 4px 16px; border-bottom: 1px solid var(--border); min-height: 48px; }
.bulk-selectall { width: 34px; height: 34px; flex-shrink: 0; opacity: 1; color: var(--accent); }
.bulk-selectall .thread-check-tick { opacity: 0; }
.bulk-selectall.checked rect, .bulk-selectall.partial rect { fill: var(--accent); stroke: var(--accent); }
.bulk-selectall.checked .thread-check-tick { opacity: 1; stroke: #fff; }
.bulk-partial-line { opacity: 0; stroke: #fff; }
.bulk-selectall.partial .thread-check-tick { opacity: 0; }
.bulk-selectall.partial .bulk-partial-line { opacity: 1; }
.bulk-count { font-size: 13px; font-weight: 600; color: var(--text); margin: 0 8px 0 4px; white-space: nowrap; }
.bulk-sep { width: 1px; height: 22px; background: var(--border); margin: 0 6px; flex-shrink: 0; }

/* Right-click context menu */
.ctx-menu {
  position: fixed; z-index: 90; min-width: 200px; padding: 6px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: var(--shadow);
}
.ctx-item {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left; border: none; background: transparent;
  padding: 9px 14px 9px 12px; border-radius: 7px; font-size: 14px; color: var(--text); cursor: pointer;
}
.ctx-item:hover { background: var(--surface-2); }
.ctx-item-ico { flex-shrink: 0; width: 18px; height: 18px; color: var(--text-soft); display: inline-flex; align-items: center; justify-content: center; }
.ctx-item-ico svg { width: 18px; height: 18px; }
.ctx-item-label { flex: 1; }
.ctx-sep { height: 1px; background: var(--border); margin: 5px 6px; }

/* Priority Inbox section headers + per-section pager. These scroll WITH the
   list (no sticky): a stuck caption overlaps rows under the glass theme and
   mislabels the section while scrolling. */
.inbox-section-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px 8px; font-size: 15px; font-weight: 700; color: var(--text-soft);
  background: var(--surface);
}
/* Bottom snackbar / toast (Gmail-style undo-send + undo-action). */
.toast {
  position: fixed; left: 24px; bottom: 24px; z-index: 80;
  display: flex; align-items: center; gap: 18px;
  background: #2c2c33; color: #fff; border-radius: 8px;
  padding: 12px 14px 12px 18px; box-shadow: 0 6px 24px rgba(0,0,0,.28);
  font-size: 14px; max-width: 460px; animation: toast-in .16s ease-out;
}
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.toast-msg { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.toast-actions { display: flex; align-items: center; gap: 6px; margin-left: auto; flex-shrink: 0; }
.toast-action {
  border: none; background: transparent; color: #8ab4f8; cursor: pointer;
  font-size: 14px; font-weight: 600; padding: 6px 10px; border-radius: 6px;
}
.toast-action:hover { background: rgba(255,255,255,.10); }
.toast-action.primary { color: #fff; }
/* An accent action that reads as the clear call-to-action inside the dark toast
   (e.g. the "Click here to refresh" new-message nudge). Filled blue pill,
   visibly distinct from the toast body and the plain text actions. */
.toast-action.accent { color: #fff; background: #1a73e8; }
.toast-action.accent:hover { background: #1b66c9; }
/* Transient status pill (e.g. "Draft saved"). Sits in the bottom-left slot,
   fades in/out on its own so it never disturbs a sticky undo-send /
   new-message toast. */
.mini-toast {
  position: fixed; left: 24px; bottom: 24px; z-index: 79;
  background: #2c2c33; color: #e8eaed; border-radius: 8px;
  padding: 9px 14px; box-shadow: 0 4px 16px rgba(0,0,0,.24);
  font-size: 13px; opacity: 0; transform: translateY(6px);
  transition: opacity .18s ease, transform .18s ease; pointer-events: none;
}
.mini-toast.show { opacity: 1; transform: none; }
/* When the action snackbar is visible it owns the bottom-left slot, so lift the
   mini pill above it to avoid overlap. */
.toast:not([hidden]) ~ .mini-toast { bottom: 72px; }

/* ── Top-center connectivity banner (Gmail-style offline / reconnecting) ── */
.conn-banner {
  position: fixed; top: 10px; left: 50%; transform: translateX(-50%); z-index: 95;
  display: flex; align-items: center; gap: 10px;
  background: #2c2c33; color: #fff; border-radius: 8px;
  padding: 9px 14px; box-shadow: 0 6px 24px rgba(0,0,0,.28);
  font-size: 13.5px; white-space: nowrap; animation: conn-in .16s ease-out;
}
@keyframes conn-in { from { opacity: 0; transform: translate(-50%, -8px); } to { opacity: 1; transform: translate(-50%, 0); } }
.conn-banner.online { background: #1e8e3e; }
.conn-dot {
  width: 8px; height: 8px; border-radius: 50%; background: #f28b82; flex-shrink: 0;
}
.conn-dot.on { background: #ceead6; }
.conn-try {
  border: none; background: rgba(255,255,255,.14); color: #fff; cursor: pointer;
  font-size: 13px; font-weight: 600; padding: 5px 11px; border-radius: 6px; margin-left: 4px;
}
.conn-try:hover { background: rgba(255,255,255,.24); }

/* ── Empty "Unread" celebration state ── */
/* Kept in place of the Unread rows when the section is empty. Colourful +
   confetti on the just-hit-zero moment, then settles to grey after 5s; a plain
   load/refresh shows the grey rest state directly. */
.empty-unread {
  position: relative; display: flex; flex-direction: column; align-items: center;
  justify-content: center; text-align: center; gap: 6px; padding: 18px 16px 26px;
  min-height: 180px; overflow: hidden;
}
.empty-unread-confetti { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 2; }
.empty-unread-illo {
  position: relative; z-index: 1; width: 104px; height: 104px; object-fit: contain;
  transition: filter .8s ease, opacity .8s ease;
}
.empty-unread-title { position: relative; z-index: 1; font-size: 14px; font-weight: 600; color: var(--text); }
.empty-unread-sub { position: relative; z-index: 1; font-size: 13px; color: var(--text-mute); margin-top: -2px; }
/* Rest state = grey (for load/refresh, and after the 5s settle). */
.empty-unread.grey .empty-unread-illo,
.empty-unread.to-grey .empty-unread-illo { filter: grayscale(1) contrast(.98) brightness(1.02) opacity(.85); }
/* Celebration entrance: the popper kicks once. */
.empty-unread.pop-anim .empty-unread-illo { animation: eu-pop .6s cubic-bezier(.2,.9,.3,1.25); }
@keyframes eu-pop {
  0% { transform: rotate(-12deg) translateY(10px) scale(.8); }
  50% { transform: rotate(6deg) translateY(-4px) scale(1.08); }
  100% { transform: rotate(0) translateY(0) scale(1); }
}
.empty-unread.pop-anim .empty-unread-title { animation: eu-rise .5s .14s both ease-out; }
.empty-unread.pop-anim .empty-unread-sub { animation: eu-rise .5s .22s both ease-out; }
@keyframes eu-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* Extra breathing room before the second ("Everything else") section. */
.inbox-section-head + .thread-row,
.thread-row + .inbox-section-head { margin-top: 0; }
.inbox-section-head.secondary { margin-top: 22px; }
.section-head-label { display: inline-flex; align-items: center; gap: 4px; }
.section-collapse {
  border: none; background: transparent; color: var(--text-soft); cursor: pointer;
  width: 24px; height: 24px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; padding: 0;
}
.section-collapse:hover { background: rgba(20,30,60,.08); color: var(--text); }
.section-collapse svg { transition: transform .15s; }
.section-collapse svg.chev-collapsed { transform: rotate(-90deg); }
.inbox-pager { display: flex; align-items: center; gap: 4px; font-weight: 400; }
.pager-range { font-size: 12px; color: var(--text-mute); margin-right: 4px; }
.pager-btn {
  border: none; background: transparent; color: var(--text-soft);
  width: 30px; height: 30px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
}
.pager-btn:hover:not([disabled]) { background: rgba(20,30,60,.08); color: var(--text); }
.pager-btn[disabled] { opacity: .35; cursor: default; }

/* Gmail-style single-line row:
   [check][star]  sender(fixed)   subject — snippet(flex)   attach  time/hover-actions */
/* Reading-density preference (General settings): compact shrinks row height */
body.density-compact .thread-row { height: 26px; }
/* Snippet preference: hide the message preview text in the list */
body.hide-snippets .thread-snippet, body.hide-snippets .thread-dash { display: none; }
.thread-row {
  /* right padding 27px (was 12px): the date/time needs clear air from the
     pane edge (user request 2026-07-03). */
  display: flex; align-items: center; gap: 0; padding: 0 27px 0 8px; height: 40px;
  border-bottom: 1px solid var(--row-divider); cursor: pointer; position: relative;
  font-size: 14px;
}
.thread-row:hover { background: var(--row-hover); box-shadow: inset 1px 0 0 var(--border), 0 1px 2px rgba(20,30,60,.08); z-index: 1; }
.thread-row:hover .thread-actions { display: flex; }
/* Hide the time on hover WITHOUT removing its space, so the attachment icon to
   its left never shifts. The actions overlay the same right-edge slot. */
.thread-row:hover .thread-time { visibility: hidden; }
.thread-row.selected { background: var(--accent-soft); }
.thread-row.checked { background: #fdf6e3; }

/* Loading skeleton — shimmer rows shown the instant a folder is clicked, while
   the real threads are being fetched. Non-interactive; matches .thread-row
   geometry so the swap to real rows doesn't reflow. */
.thread-row.skeleton { cursor: default; pointer-events: none; }
.thread-row.skeleton:hover { background: transparent; box-shadow: none; }
.sk-block {
  display: inline-block; height: 12px; border-radius: 6px;
  background: linear-gradient(90deg, var(--row-divider) 25%, var(--row-hover) 37%, var(--row-divider) 63%);
  background-size: 400% 100%; animation: sk-shimmer 1.4s ease infinite;
}
.sk-check { width: 18px; height: 18px; border-radius: 4px; margin: 0 9px 0 9px; flex-shrink: 0; }
.sk-star { width: 18px; height: 18px; border-radius: 4px; margin-right: 8px; flex-shrink: 0; }
.sk-sender { width: 150px; margin-left: 8px; margin-right: 18px; flex-shrink: 0; }
.sk-subject { width: 40%; max-width: 360px; }
.sk-time { width: 42px; margin-left: auto; flex-shrink: 0; }
@keyframes sk-shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
@media (prefers-reduced-motion: reduce) { .sk-block { animation: none; } }

/* Full-panel search loader — the whole thread list is emptied and replaced by
   one turning spinner while a search runs, so no stale rows can read as the
   result of the search being typed. Skeleton rows are deliberately NOT reused
   here: they imply "the same kind of list is coming back", which is wrong when
   the list is about to change completely. */
.list-loading {
  display: flex; align-items: center; justify-content: center;
  padding: 72px 20px; color: var(--text-mute);
}
.list-loading-spinner {
  width: 30px; height: 30px; border-radius: 50%;
  border: 3px solid var(--row-divider); border-top-color: var(--accent);
  animation: list-loading-spin .7s linear infinite;
}
/* Screen-reader only: the list title already reads "Searching…" directly above
   the spinner, so a second visible copy of the same word is a stutter. */
.list-loading-label {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
@keyframes list-loading-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .list-loading-spinner { animation-duration: 2.4s; } }

/* ── Conversation loading (cold open: newer/older, "," / ".", a row click) ──
   A cached thread paints in the same frame, but a cold one empties the pane and
   then awaits the network, so the reading area simply went WHITE and nothing
   said the key had registered. Two quiet signals, no third:
     · a 2px indeterminate bar under the toolbar (the app-level "working" sign);
     · a skeleton shaped like the message that is coming, so the pane is never
       blank and the swap to the real message barely moves anything.
   Reuses the thread-list sk-shimmer keyframes on purpose: one loading idiom
   across the app rather than a second visual language for the same idea.
   Both are held back ~140ms in JS — a fetch that lands sooner must not flash a
   spinner, which reads worse than the short wait it was meant to explain.

   The bar always occupies its 2px and is merely TRANSPARENT when idle, rather
   than being `hidden`: a bar that appears in the flow would push the whole
   conversation down 2px at the exact moment the user is watching it, which is
   its own small glitch. It cannot be absolutely positioned inside the toolbar
   either, because on a phone that toolbar scrolls sideways and would carry the
   bar off with it. */
.conv-progress {
  height: 2px; flex-shrink: 0; position: relative; overflow: hidden;
  background: transparent;
}
.conv-progress.on { background: var(--row-divider); }
.conv-progress.on::after {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 36%;
  background: var(--accent); border-radius: 2px;
  animation: conv-progress-slide 1.05s cubic-bezier(.45, .05, .55, .95) infinite;
}
@keyframes conv-progress-slide {
  0% { transform: translateX(-110%); }
  100% { transform: translateX(390%); }
}
@media (prefers-reduced-motion: reduce) { .conv-progress.on::after { animation-duration: 2.6s; } }

/* Skeleton message. Geometry mirrors .conv-subject + .msg-head + .msg-body
   (40px avatar, 14px gap, 54px body indent) so the real message lands on the
   same lines the placeholder occupied. */
.conv-skeleton { padding-bottom: 24px; }
.conv-sk-subject, .conv-sk-avatar, .conv-sk-line {
  background: linear-gradient(90deg, var(--row-divider) 25%, var(--row-hover) 37%, var(--row-divider) 63%);
  background-size: 400% 100%; animation: sk-shimmer 1.4s ease infinite;
}
@media (prefers-reduced-motion: reduce) {
  .conv-sk-subject, .conv-sk-avatar, .conv-sk-line { animation: none; }
}
.conv-sk-subject { display: block; height: 22px; width: 54%; max-width: 420px; border-radius: 7px; margin-bottom: 26px; }
.conv-sk-head { display: flex; align-items: flex-start; gap: 14px; }
.conv-sk-avatar { width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0; }
.conv-sk-meta { flex: 1; min-width: 0; padding-top: 3px; }
.conv-sk-line { display: block; height: 12px; border-radius: 6px; }
.conv-sk-meta .conv-sk-line { margin-bottom: 9px; }
.conv-sk-meta .conv-sk-line:nth-child(1) { width: 34%; max-width: 220px; }
.conv-sk-meta .conv-sk-line:nth-child(2) { width: 20%; max-width: 130px; }
.conv-sk-body { padding: 16px 0 0 54px; }
.conv-sk-body .conv-sk-line { margin-bottom: 12px; }
.conv-sk-body .conv-sk-line:nth-child(1) { width: 93%; }
.conv-sk-body .conv-sk-line:nth-child(2) { width: 87%; }
.conv-sk-body .conv-sk-line:nth-child(3) { width: 90%; }
.conv-sk-body .conv-sk-line:nth-child(4) { width: 58%; }

/* Unread = bold + darker, like Gmail */
.thread-row.unread { background: var(--surface); }
/* Unread rows must darken on hover too — without this, the .unread background
   rule (same specificity, declared later) cancels the generic :hover. */
.thread-row.unread:hover { background: var(--row-hover); }
.thread-row.unread .thread-sender,
.thread-row.unread .thread-subject { font-weight: 700; color: var(--text); }
/* The body preview (snippet) stays NORMAL weight even on unread rows — only the
   sender and subject carry the unread bold (Gmail-style, per user decision). */
.thread-row.unread .thread-snippet,
.thread-row.unread .thread-dash { font-weight: 400; }
.thread-row:not(.unread) .thread-sender { color: var(--text-soft); }

/* Checkbox + star — rigid fixed-width columns so the sender ALWAYS starts at
   the same x, no matter the icon. This is what keeps the sender aligned. */
.thread-check, .thread-star {
  border: none; background: transparent; color: var(--text-mute); padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; flex-shrink: 0;
  opacity: .5; transition: opacity .12s;
}
/* Always visible at 50%, full opacity on row hover (Gmail behaviour). */
.thread-row:hover .thread-check, .thread-row:hover .thread-star,
.thread-row.checked .thread-check { opacity: 1; }
/* A set superstar stays fully visible even when the row isn't hovered. */
.thread-star.on { opacity: 1; }
.thread-check:hover, .thread-star:hover { color: var(--text); }
/* The checkmark is hidden until the row is checked, then the box fills with the
   accent colour and a white tick shows (Gmail-style selection). */
.thread-check-tick { opacity: 0; }
.thread-row.checked .thread-check { opacity: 1; color: var(--accent); }
.thread-row.checked .thread-check rect { fill: var(--accent); stroke: var(--accent); }
.thread-row.checked .thread-check-tick { opacity: 1; stroke: #fff; }

/* Sender column — fixed width like Gmail, with a little left gap after the star */
/* Fixed-width sender column with a real gap before the subject so it never
   crowds the next cell. Overflowing text FADES into the background (mask)
   instead of showing an ellipsis. */
.thread-sender-wrap { width: 234px; flex-shrink: 0; overflow: hidden; padding-left: 8px; margin-right: 18px; display: flex; align-items: center; gap: 5px; }
.thread-sender {
  /* line-height must leave room for descenders (g, j, y); with overflow:hidden
     a line-height of 1 clips the bottom of those glyphs. */
  font-size: 14px; line-height: 1.35; color: var(--text); white-space: nowrap;
  overflow: hidden; min-width: 0;
}
/* Only when the name is too long: fade its tail into the background instead of
   an ellipsis. Added by JS when scrollWidth exceeds the visible width. */
.thread-sender.faded {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 26px), transparent 100%);
          mask-image: linear-gradient(to right, #000 calc(100% - 26px), transparent 100%);
}

/* Subject + snippet take the remaining width, single line */
.thread-subjectcol { flex: 1; min-width: 0; display: flex; align-items: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.thread-subject { color: var(--text); flex-shrink: 0; max-width: 60%; overflow: hidden; text-overflow: ellipsis; }
.thread-dash { color: var(--text-mute); flex-shrink: 0; margin: 0 7px; }
.thread-snippet { color: var(--text-mute); overflow: hidden; text-overflow: ellipsis; }
/* Search-term highlight: yellow background on the matched words, in subject or
   the body context excerpt. Inherits the row's text colour so it stays legible. */
mark.search-hl { background: #fde047; color: inherit; border-radius: 2px; padding: 0 1px; }
/* When the snippet is a body-match excerpt (not the generic preview), let it use
   more of the row and read slightly darker so the surrounding context is clear. */
.thread-snippet.thread-snippet-match { color: var(--text-soft); }
.thread-count { font-size: 11px; font-weight: 700; color: var(--text-soft); background: var(--surface-2); border-radius: 10px; padding: 1px 7px; flex-shrink: 0; }
/* The red "Draft" marker on a Drafts-folder row (Gmail parity: recipients
   first, then a red Draft label). */
.draft-label { color: #d93025; }

/* Wrapper around the paperclip + the agent chip. On desktop it is
   `display: contents`, so it generates NO box at all and its two children act
   as direct flex items of .thread-row — the desktop row is unchanged. It exists
   only so the phone grid can place both markers as ONE item; without it they
   land in the same grid cell as the subject and paint over it. */
.thread-marks { display: contents; }
.thread-attach { color: var(--text-mute); display: inline-flex; flex-shrink: 0; margin-left: 30px; }
.thread-time { font-size: 13px; color: var(--text-soft); flex-shrink: 0; width: 70px; text-align: right; margin-left: 10px; }
.thread-row.unread .thread-time { font-weight: 700; color: var(--text); }
/* Snoozed folder: the snooze-until date, bold strong violet (Gmail-style). */
.thread-snooze-time, .thread-row.unread .thread-snooze-time { color: #7c3aed; font-weight: 700; }
/* Inbox: a thread that woke from a timed snooze — the WAKE time in orange behind
   a clock icon, in place of the received date. No wording: the icon and the
   colour carry the meaning, so the column keeps reading as a date column. The
   time sits inline rather than in a tooltip (the row's time slot is hidden on
   hover and the action buttons overlay it, so a tooltip here would never be
   visible). Auto width + the icon need a little more room than the 70px date
   slot; the gap is deliberately wide (two spaces' worth) so the icon does not
   crowd the digits. */
.thread-time.thread-unsnoozed,
.thread-row.unread .thread-time.thread-unsnoozed {
  color: #e8710a;
  font-weight: 700;
  width: auto;
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  white-space: nowrap;
}
.thread-time.thread-unsnoozed svg { flex-shrink: 0; }

/* Agent markers on an inbox row. Two states, deliberately different in tone:

   .working  — an agent holds this thread right now. Calm blue, and the icon
               turns slowly, because this is GOOD news: work is happening
               without the operator. It disappears when the agent answers.
   .returned — the safety net put the thread back to unread. Amber, static,
               because it asks for a look, but it must NOT read as an error:
               nothing is broken, the thread simply came back.

   Both sit left of the date and collapse away under the hover actions, like
   every other right-side element on the row. */
.thread-agent {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex-shrink: 0;
  margin-left: 10px;
  padding: 2px 8px 2px 6px;
  border-radius: 20px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .01em;
  white-space: nowrap;
  cursor: help;
}
.thread-agent svg { flex-shrink: 0; }
.thread-agent.working {
  color: #1a56b8;
  background: #e8f0fd;
  border: 1px solid #cfe0fa;
}
.thread-agent.working svg { animation: agentspin 3.2s linear infinite; transform-origin: 50% 50%; }
.thread-agent.returned {
  color: #97590f;
  background: #fdf3e3;
  border: 1px solid #f3e2c4;
}
/* Deeper amber than .returned, matching .conv-followup-bar: that one explains a
   thing the system already put right, this one flags an answer still being
   ignored. It outranks its siblings when several would apply. */
.thread-agent.unfollowed {
  color: #9a3412;
  background: #fdece8;
  border: 1px solid #f6d2c8;
}

/* RESERVED — an agent is holding this conversation right now.
   Violet rather than the blue of .working, because it says something different:
   .working meant "read behind your back, and it may bounce back to unread",
   while this means "parked with a named owner, and nothing will move on its
   own". A calm colour is correct for a state that is safe by construction.
   It is a real link (to the holding chat session), so it takes a pointer
   cursor instead of the help cursor its siblings use. */
.thread-agent.reserved {
  color: #6b3fa0;
  background: #f3edfc;
  border: 1px solid #e0d2f5;
  cursor: pointer;
  text-decoration: none;
}
a.thread-agent.reserved:hover { background: #eae0fa; border-color: #d1bcf0; }
/* 2 hours: held longer than usual. Amber = "go and look", never "something
   broke" — nothing is wrong yet, and nothing will be taken back. */
.thread-agent.reserved.overdue {
  color: #97590f;
  background: #fdf3e3;
  border-color: #f3e2c4;
}
a.thread-agent.reserved.overdue:hover { background: #fbebd2; }
/* 24 hours: impossible to miss, as the PRD asks. Usually the agent asked the
   operator something in that session and never got an answer, so the loud chip
   is aimed at HIM, not at the agent. Still no automatic action. */
.thread-agent.reserved.loud {
  color: #fff;
  background: #b42318;
  border-color: #941d13;
  font-weight: 600;
}
a.thread-agent.reserved.loud:hover { background: #9f2018; }

/* The Reserved section header sits between Unread and Everything else. Tinted
   just enough to read as its own bucket at a glance, without competing with
   Unread for attention. */
.inbox-section-head.reserved-head .section-head-label { color: #6b3fa0; }
/* Empty Reserved pool: a quiet muted line, not a celebration. An empty pool is
   the normal state, so this is a plain grey rest block under the header. */
.empty-reserved {
  padding: 8px 16px 14px;
  font-size: 13px;
  color: var(--text-mute);
}
@keyframes agentspin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .thread-agent.working svg { animation: none; }
}
/* The hover actions slide over the RIGHT edge of the row. The date cell hides
   under them (see .thread-time), but the agent chip must NOT: its entire value
   is the tooltip, and a chip the cursor cannot reach can never show one.
   So on hover the chip is lifted ABOVE the action strip and stays hoverable.
   Measured 2026-08-08: while it sat in the hover-hidden zone, every pointer
   event was intercepted by .thread-actions — exactly what a real cursor hits. */
.thread-row:hover .thread-agent { position: relative; z-index: 3; }

/* Actions overlay the right edge on hover (absolute) so revealing them never
   reflows the row, the attachment icon to the left stays put. */
/* The hover actions sit where the date/time was, but their fill is a wide (~150px)
   gradient band that fades from fully transparent on the left to the row-hover
   tint on the right, over a 3-4cm distance, so there's no hard seam. The overlay
   is anchored to the row's right edge (right:0) and holds the icons in place with
   right padding. */
.thread-actions { display: none; align-items: center; gap: 1px; position: absolute; right: 0; top: 50%; transform: translateY(-50%); background: linear-gradient(to right, transparent 0%, transparent 12%, var(--row-hover) 78%, var(--row-hover) 100%); padding-left: 150px; padding-right: 25px; }
.thread-row:hover .thread-actions { display: flex; }
.thread-row.unread:hover .thread-actions { background: linear-gradient(to right, transparent 0%, transparent 12%, var(--row-hover) 78%, var(--row-hover) 100%); }
.row-act { border: none; background: transparent; color: var(--text-soft); width: 30px; height: 30px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; }
.row-act:hover { background: rgba(20,30,60,.10); color: var(--text); }

/* Read-aloud re-enable badge. Shown at the clicked word while click-to-read is
   OFF, so the way back is always one click away wherever the user is, instead
   of a trip to Settings. Deliberately quiet: muted grey, no pulse, no sound,
   and it fades on its own after a few seconds. It must never become the new
   annoyance the off switch was built to solve. */
.tts-offer {
  position: fixed; z-index: 300; display: flex; align-items: center; gap: 6px;
  background: rgba(60,64,67,.92); color: #fff; border: none;
  border-radius: 999px; padding: 5px 11px 5px 9px; cursor: pointer;
  font: inherit; font-size: 11.5px; line-height: 1.2; white-space: nowrap;
  box-shadow: 0 2px 10px rgba(0,0,0,.24); user-select: none;
  animation: tts-offer-in .14s ease-out;
}
@keyframes tts-offer-in { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }
.tts-offer:hover { background: #1a73e8; }
.tts-offer:focus { outline: none; }
.tts-offer:focus-visible { outline: 2px solid #1a73e8; outline-offset: 2px; }

/* Sender hover card */
.sender-card {
  position: fixed; z-index: 80; display: flex; align-items: center; gap: 11px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  box-shadow: var(--shadow); padding: 12px 14px; max-width: 320px;
}
.sender-card-avatar { width: 36px; height: 36px; border-radius: 50%; background: var(--accent); color: #fff; display: inline-flex; align-items: center; justify-content: center; font-weight: 700; flex-shrink: 0; }

/* Recipient autocomplete dropdown (To/Cc fields). */
.ac-dropdown {
  position: fixed; z-index: 90; background: var(--surface);
  border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 8px 28px rgba(20,30,60,.18);
  padding: 5px; max-height: 320px; overflow-y: auto;
}
.ac-item { display: flex; align-items: center; gap: 10px; padding: 7px 9px; border-radius: 7px; cursor: pointer; }
.ac-item.active, .ac-item:hover { background: var(--surface-2); }
.ac-avatar { width: 30px; height: 30px; border-radius: 50%; background: var(--accent); color: #fff; display: inline-flex; align-items: center; justify-content: center; font-weight: 700; font-size: 13px; flex-shrink: 0; }
.ac-info { display: flex; flex-direction: column; min-width: 0; }
.ac-name { font-size: 14px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ac-addr { font-size: 12px; color: var(--text-mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── Recipient chips (Gmail-style pills in To/Cc) ── */
/* The original <input> stays as the canonical value store but is removed from
   layout; the chips-field flex row is what the user sees and edits. */
.chips-hidden-input { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; border: 0; overflow: hidden; clip: rect(0 0 0 0); }
.chips-field { flex: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 5px; min-width: 0; }
.chip {
  display: inline-flex; align-items: center; gap: 5px; max-width: 100%;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 999px;
  padding: 2px 4px 2px 10px; font-size: 13px; color: var(--text); line-height: 1.6;
}
.chip-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip-x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border: none; border-radius: 50%; padding: 0;
  background: transparent; color: var(--text-soft); cursor: pointer; flex-shrink: 0;
}
.chip-x:hover { background: var(--border); color: var(--text); }
.chips-typer {
  flex: 1; min-width: 80px; border: none; outline: none; background: transparent;
  font-size: 14px; color: var(--text); font-family: var(--font); padding: 2px 0;
}
.sender-card-info { display: flex; flex-direction: column; min-width: 0; }
.sender-card-name { font-weight: 600; font-size: 14px; }
.sender-card-addr { font-size: 12px; color: var(--text-mute); overflow: hidden; text-overflow: ellipsis; -webkit-user-select: all; user-select: all; }
.sender-card-copy {
  border: none; background: transparent; color: var(--text-soft); cursor: pointer;
  width: 30px; height: 30px; border-radius: 7px; flex-shrink: 0; margin-left: 2px;
  display: inline-flex; align-items: center; justify-content: center;
}
.sender-card-copy:hover { background: var(--surface-2); color: var(--text); }
.sender-card-copy.copied { color: #1e8e3e; }

/* Conversation: full-page (replaces the list), with a fixed top toolbar */
.reading-pane { background: var(--surface); display: flex; flex-direction: column; min-height: 0; }
.conv-toolbar {
  display: flex; align-items: center; gap: 2px; padding: 6px 12px;
  background: var(--surface); border-bottom: 1px solid var(--border); flex-shrink: 0;
}
.conv-toolbar-sep { width: 1px; height: 22px; background: var(--border); margin: 0 8px; }
/* Superstar in the conversation toolbar. Unset it reads as a soft outline like
   an unstarred list row; once set, the marker itself carries the colour. */
.conv-star { color: var(--text-mute); }
.conv-star:hover { color: var(--text); }
.conv-star.on { color: var(--text); }
.conv-star svg { pointer-events: none; }

/* The marker picker, opened by right-clicking the toolbar star. Rendered on
   document.body (never clipped) per the global tooltip/popup rule. */
.star-picker {
  position: fixed; z-index: 95; display: flex; align-items: center; gap: 2px;
  padding: 6px; background: var(--surface); border: 1px solid var(--border);
  border-radius: 10px; box-shadow: var(--shadow);
}
.star-pick {
  border: none; background: transparent; color: var(--text-mute); cursor: pointer;
  width: 32px; height: 32px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
}
.star-pick:hover { background: var(--surface-2); color: var(--text); }
.star-pick.on { background: var(--surface-2); }
.star-picker-sep { width: 1px; height: 20px; background: var(--border); margin: 0 4px; }

/* "X of N" + newer/older arrows, pushed to the right of the toolbar (Gmail). */
.conv-nav { margin-left: auto; display: flex; align-items: center; gap: 2px; }
.conv-pos { font-size: 13px; color: var(--text-soft); margin-right: 6px; white-space: nowrap; }
.conv-nav .icon-btn[disabled] { opacity: .35; cursor: default; }
.conv-nav .icon-btn[disabled]:hover { background: transparent; }
/* Keypress echo on the arrow the "," / "." shortcut stands for. A click gets
   :hover and :active for free; a keystroke gets nothing, and on a slow thread
   nothing at all moved for the first instant. One short pulse in the accent
   colour, gone in 340ms — it acknowledges the key, it is not a loading sign
   (the bar under the toolbar is). */
.conv-nav .icon-btn.nav-flash { animation: conv-nav-flash .34s ease-out; }
@keyframes conv-nav-flash {
  0% { background: var(--accent); color: #fff; }
  100% { background: transparent; color: var(--text-soft); }
}
@media (prefers-reduced-motion: reduce) { .conv-nav .icon-btn.nav-flash { animation: none; } }
/* Full-bleed white conversation: messages span the panel, no card-on-grey.
   NOTE: no bottom padding — a position:sticky child pins to the padding-box
   edge, so any padding-bottom here would float the sticky reply bar up off the
   pane bottom. The breathing room below the bar lives on .conv-foot-sentinel. */
.conversation { flex: 1; overflow-y: auto; padding: 24px var(--conv-pad-x) 0; background: var(--surface); }

/* ══════════════════════════════════════════════════════════════════════════
 * SWIPE BETWEEN MESSAGES — see wireConversationSwipe() in mailbox.js
 * ══════════════════════════════════════════════════════════════════════════
 * These live at top level, NOT inside a phone media query, because a landscape
 * phone is wider than 640px and would otherwise miss them entirely (measured:
 * the message body stayed unswipeable in landscape while portrait was fixed).
 * They are inert on a mouse: the gesture only ever arms on a touch device.
 */

/* `pan-y` tells the browser up front that this element scrolls vertically and
   that horizontal movement belongs to the script. Without it iOS may start its
   own horizontal handling on the first pixels and the swipe ends up fighting
   the browser for the gesture, which is the hesitation this design avoids. */
.conversation { touch-action: pan-y; }

/* The slide must not reveal the page behind it.
   Scoped to TOUCH devices, because the swipe only exists there.
   NOTE the preview is positioned against `.conversation`, NOT against this
   pane — see the block below for why that distinction is the whole bug. */
@media (hover: none) and (pointer: coarse) {
  .reading-pane { overflow-x: hidden; }
  .conversation { position: relative; }
}

/* ── The incoming message, parked one screen off to the side ──────────────
   Dragging used to expose a blank strip, which reads as "loading" rather than
   "the next message is arriving". This panel carries the neighbour's REAL
   content and travels with the same finger movement.

   IT IS ANCHORED TO `.conversation`, NEVER TO `.reading-pane`. That one word
   was the whole of a real bug: the pane INCLUDES the action toolbar (back,
   archive, delete, the newer/older pill), so a panel filling the pane slid its
   own copy of the message up OVER the live toolbar. On the operator's screen
   recording the incoming message started at the very top of the screen,
   covering the toolbar during the whole drag, then dropped down into place on
   release — "it's all ugly and full of glitches". Anchoring to the scroller
   confines the preview to exactly the area the real message occupies, so the
   toolbar stays put and nothing jumps at the end.

   `top: 0` is deliberate rather than `inset: 0`: .conversation is a scrolling
   box, so its padding-box top is the top of the VISIBLE message area. */
.conv-peek {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  z-index: 1;
  overflow: hidden;
  background: var(--surface);
  padding: 24px var(--conv-pad-x) 0;
  pointer-events: none;            /* a picture, never a target */
  will-change: transform;
}
/* THE MOVING EDGE. Without a seam the two messages read as one long page
   sliding, and on a mostly-empty message nothing visibly moves at all. A
   hairline plus a short shadow travels with the panel and makes the motion
   legible even against a blank body. It sits on the side the panel comes
   FROM, so it is always the leading edge. */
.conv-peek.from-right { box-shadow: -1px 0 0 var(--border), -16px 0 22px -14px rgba(20, 30, 60, .28); }
.conv-peek.from-left  { box-shadow:  1px 0 0 var(--border),  16px 0 22px -14px rgba(20, 30, 60, .28); }
/* MEMORY: `will-change: transform` promotes EVERY message to its own
   compositor layer, and each layer is an off-screen texture kept in video
   memory at device-pixel resolution (a Retina pane is ~4x the CSS area). On a
   188-message conversation that is 188 permanent textures, held for a swipe
   gesture that does not exist on a desktop.
   It only earns its cost while a finger is actually dragging, so it is now
   scoped to touch devices — same pointer query as `.reading-pane` above, which
   already gates the swipe's containing block. Desktop keeps the identical
   appearance: `will-change` is a performance hint and paints nothing. */
@media (hover: none) and (pointer: coarse) {
  .conversation > *:not(.conv-reply-bar) { will-change: transform; }
}
/* The preview is a still picture: nothing in it should animate or react. */
.conv-peek * { animation: none !important; transition: none !important; }
.conv-peek .conv-reply-bar, .conv-peek .msg-actions,
.conv-peek .conv-foot-sentinel { display: none; }
/* Subject row: the title, plus any right-aligned conversation-level action
   (currently the unsubscribe button). Baseline alignment keeps the button
   optically level with the first line of a wrapping subject. */
.conv-subject {
  margin: 0 0 22px;
  display: flex;
  align-items: flex-start;
  gap: 16px;
  /* Several tags on one conversation must WRAP onto their own line rather than
     squeeze the title: without this, three chips crushed a long subject down to
     a single letter on a phone (measured in Safari at 390px and 320px). */
  flex-wrap: wrap;
}
/* The title takes only the width it needs, so the tag chips sit immediately
   AFTER the text (Gmail-style) instead of being pushed to the far right edge.
   The unsubscribe button keeps its right-hand corner via its own auto margin. */
.conv-subject h2 { font-size: 22px; margin: 0; line-height: 1.3; font-weight: 500; flex: 0 1 auto; min-width: 0; }
.msg { background: var(--surface); border: none; border-radius: 0; margin: 0; overflow: visible; padding-bottom: 22px; }
/* MEMORY: `loading=lazy` on the images only DEFERS the first decode — once a
   picture has been scrolled past, the browser keeps the decoded bitmap because
   the element is still in the layout. Scrolling a 188-message thread with 1948
   images therefore still climbed past 8GB, just more slowly (measured
   2026-09-05, after the lazy-loading change).
   `content-visibility: auto` is the part that actually gives memory back: the
   browser skips rendering a message that is off screen entirely and DROPS its
   painted output, including the decoded images inside it, re-rendering when it
   comes back. That also removes the layout/paint work for ~187 off-screen
   messages, which is what made the scroll stutter.
   `contain-intrinsic-size` supplies a placeholder height so the scrollbar does
   not jump around while messages are skipped; the browser remembers each real
   height once measured. Applied only to EXPANDED messages: a collapsed row is
   one cheap line and skipping it would gain nothing while risking a jump. */
.msg.expanded {
  content-visibility: auto;
  contain-intrinsic-size: auto 600px;
}
/* Full-bleed divider between messages: the negative margins + matching padding
   stretch the 1px line edge-to-edge across the whole panel (Gmail-style). */
.msg + .msg {
  border-top: 1px solid var(--border); padding-top: 22px;
  margin-left: calc(-1 * var(--conv-pad-x)); margin-right: calc(-1 * var(--conv-pad-x));
  padding-left: var(--conv-pad-x); padding-right: var(--conv-pad-x);
}
.msg-head { display: flex; align-items: flex-start; gap: 14px; padding: 0; }
.msg-avatar { width: 40px; height: 40px; border-radius: 50%; background: var(--accent); color: #fff; display: inline-flex; align-items: center; justify-content: center; font-weight: 700; flex-shrink: 0; }
.msg-meta { flex: 1; min-width: 0; padding-top: 1px; }
.msg-from { display: block; font-weight: 600; font-size: 14px; color: var(--text); }
/* Name + email on one line: name in bold, email in grey angle-brackets (Gmail). */
.msg-from-line { display: flex; align-items: baseline; gap: 6px; min-width: 0; flex-wrap: wrap; }
.msg-from-line .msg-from { display: inline; }
.msg-email { font-size: 13px; color: var(--text-mute); font-weight: 400; word-break: break-all; }
.msg-to { display: block; font-size: 12px; color: var(--text-soft); margin-top: 1px; }
/* Recipient overview line: one compact "to name, name" list + the details caret
   (Gmail parity — short names only; full To/Cc/addresses live in the popup). */
.msg-recip-line { display: flex; align-items: baseline; gap: 2px; margin-top: 1px; min-width: 0; }
.msg-recip-line .msg-to { margin-top: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-details-btn {
  display: inline-flex; align-items: center; justify-content: center;
  border: none; background: transparent; color: var(--text-mute); cursor: pointer;
  width: 18px; height: 18px; padding: 0; border-radius: 4px; flex-shrink: 0;
  position: relative; top: 3px;
}
.msg-details-btn:hover { background: var(--surface-2); color: var(--text); }
/* Floating "show details" popup (rendered on document.body, never clipped).
   Uses the neutral reading font (Roboto/Arial), not the branded UI font, and a
   roomier size + line spacing so it reads comfortably like Gmail's. */
.msg-details-card {
  position: fixed; z-index: 85; background: var(--surface);
  border: 1px solid var(--border); border-radius: 10px; box-shadow: var(--shadow);
  padding: 14px 18px; max-width: 460px; font-size: 13.5px; color: var(--text);
  font-family: Roboto, Arial, Helvetica, sans-serif;
}
.msg-det-row { display: flex; gap: 10px; line-height: 1.6; padding: 3px 0; }
.msg-det-label { color: var(--text-mute); text-align: right; min-width: 82px; flex-shrink: 0; }
.msg-det-value { color: var(--text); word-break: break-word; min-width: 0; }
.msg-date { font-size: 12px; color: var(--text-mute); white-space: nowrap; padding-top: 2px; }
/* Per-message actions: hidden until the message is hovered (Gmail). */
.msg-actions { display: flex; align-items: center; gap: 2px; margin-left: 8px; opacity: 0; transition: opacity .12s; }
.msg:hover .msg-actions, .msg.expanded .msg-actions { opacity: 1; }
.msg-actions .icon-btn { width: 30px; height: 30px; }
/* Message bodies render in a NEUTRAL reading font (Gmail-style), NOT the app's
   branded UI font. An email that declares its own font in its HTML overrides
   this and is untouched; one that doesn't gets a classic sans-serif instead of
   inheriting Plus Jakarta Sans from body. */
/* Colour + smoothing matched to Gmail's reading pane EXACTLY. Gmail renders
   message text in #202124 and does NOT force -webkit-font-smoothing:antialiased
   on the body content, so on macOS the glyphs stay at full (subpixel) weight.
   Our global body rule forces antialiased, which made the same Arial text look
   slightly lighter/thinner than Gmail; reset it here so it renders identically. */
/* `overflow-x: auto` is here for the rare newsletter laid out wider than the
   column. On a touch screen that alone makes the browser treat the body as a
   horizontal scroller EVEN WHEN the content fits, which swallowed every
   swipe-between-messages gesture that started inside a rich HTML email — the
   only responsive strip left was the date line above it.
   `touch-action: pan-y` hands sideways movement back to the page. It is safe
   for the genuinely wide case too: wireConversationSwipe measures scrollWidth
   before claiming a gesture, so a body that really does overflow keeps its own
   scrolling. Declared HERE, not inside a phone media query, because a
   landscape phone is wider than 640px and would otherwise keep the dead body. */
.msg-body { padding: 14px 0 0 54px; font-size: 14px; line-height: 1.6; overflow-x: auto; touch-action: pan-y; word-break: break-word; font-family: Arial, Helvetica, sans-serif; color: #202124; -webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto; }
/* The box that carries a fixed-width email's own markup so it can be scaled
   down to fit the phone (see fitOneBody in mailbox.js). It is a plain wrapper
   at every other width — `zoom` is set inline, only when the content really is
   wider than the column, and only on the phone layout. */
.msg-fit { width: 100%; }
/* Once a message has been scaled, its content fits by definition, so the body
   no longer needs to advertise itself as a horizontal scroller. Saying so
   explicitly also keeps the swipe-between-messages gesture: wireConversationSwipe
   asks whether an element can REALLY scroll sideways before yielding to it, and
   a fitted body answers no. */
.msg-body.is-fitted { overflow-x: hidden; }
/* A message too wide to fit even at the 0.4 floor keeps its own sideways
   panning, so the remainder is still reachable. `pan-y` alone would hand every
   horizontal movement straight to the swipe and leave that content unreachable
   — which was the original complaint. The swipe is not lost: it takes over
   again once this body has been panned to its edge (see scrollableAncestor). */
.msg-body[data-fit-scale]:not(.is-fitted) { touch-action: pan-x pan-y; }
/* Placeholder shown while a lazily-loaded (pruned) message body is fetched. */
.msg-body-loading { padding: 14px 0 0 54px; font-size: 13px; color: var(--muted, #80868b); font-style: italic; }

/* Body-image loading placeholder. Body images always stream from the network
   (cid: attachment bytes, externalized /inline/ URLs, proxied remote images), so
   without this a heavy photo leaves a blank gap and the reader cannot tell a
   loading image from an empty message. The box is sized to the image's own
   dimensions (see imgReservedStyle) so nothing jumps when the picture lands.
   Reuses the thread-list sk-shimmer keyframes for one consistent loading idiom. */
.mb-img-loading {
  position: relative;
  display: inline-block; vertical-align: middle; border-radius: 6px;
  max-width: 100%; box-sizing: border-box;
  /* Explicit greys, NOT the --row-divider/--row-hover pair used by the thin
     thread-list skeleton bars: that pair is a translucent grey against
     near-white, which reads as a flat dead box at photo size and looks like a
     broken image rather than a loading one (caught in QA). These two steps keep
     the sweep clearly visible on a large surface. */
  background: linear-gradient(90deg, #e4e7ec 25%, #f4f6f9 37%, #e4e7ec 63%);
  background-size: 400% 100%; animation: sk-shimmer 1.4s ease infinite;
}
/* A centred picture glyph so the box says "an image is coming" even at the
   instant the shimmer sweep is mid-stroke (a moving gradient alone can read as
   a flat dead rectangle in a frozen frame — and to a user glancing at a slow
   connection). Pure CSS mask, no extra request. */
.mb-img-loading::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: 34px; height: 34px; margin: -17px 0 0 -17px;
  background-color: #b6bcc7;
  -webkit-mask: var(--mb-img-glyph) center / contain no-repeat;
  mask: var(--mb-img-glyph) center / contain no-repeat;
  opacity: .9;
}
/* Hide the glyph when the box is too small to hold it (signature logos) — the
   .is-tiny class is set in markLoadingImages, where the reserved size is known. */
.mb-img-loading.is-tiny::after { display: none; }
.mb-img-loading.mb-img-failed::after { display: none; }
:root {
  --mb-img-glyph: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='3' width='18' height='18' rx='2'/><circle cx='8.5' cy='8.5' r='1.5'/><path d='M21 15l-5-5L5 21'/></svg>");
}
/* The real <img> stays in the flow but invisible until it paints, so the
   shimmer (not a half-drawn image) is what the reader sees. */
img.mb-img-pending { opacity: 0; width: 0 !important; height: 0 !important; }
/* Failed for good: keep it collapsed. Our placeholder already says the image
   is missing, so letting the <img> back into the flow would add the browser's
   own broken-image glyph and the sender's raw alt text next to it — two error
   markers for one missing picture. Cleared when a retry is fired.
   display:none, NOT the width:0 used above for the pending state: a broken
   image with alt text is laid out as an INLINE TEXT box, and width/height do
   not apply to an inline box, so it kept ~36x62px of the sender's alt text on
   screen (measured). */
img.mb-img-broken { display: none !important; }
/* Failed load: keep the reserved box, explain it, offer one retry. A bare
   broken-image glyph with no explanation is the confusion this replaces.
   The reserved size is re-applied as min-width/min-height (see
   imgReservedStyle), so the box grows to fit its own words: pinned to the
   image's exact width, a small icon wrapped the message into a one-letter-wide
   vertical column. width:auto overrides any width the sender left inline. */
.mb-img-loading.mb-img-failed {
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 7px; padding: 10px; animation: none; background: var(--row-divider, #f1f3f4);
  border: 1px solid var(--border, #dadce0); color: var(--muted, #80868b); font-size: 12.5px;
  width: auto; height: auto; white-space: nowrap;
}
/* A failed decoration in a narrow cell (a 20px rounded corner, a 35px icon)
   gets a silent marker, not a worded card: the words are in its tooltip. A
   newsletter is a table of small cells, so growing each failure to fit "Image
   could not load" + a Retry button tore the sender's columns apart and buried
   the letter under a grid of grey boxes. Set in markLoadingImages, where the
   slot width is known. */
.mb-img-loading.mb-img-failed.is-compact {
  display: inline-block; padding: 0; gap: 0;
  min-width: 0; width: auto; max-width: 100%;
  background: var(--row-divider, #f1f3f4);
  border: 1px dashed var(--border, #dadce0); border-radius: 4px;
  opacity: .65;
}
.mb-img-failed-txt { text-align: center; white-space: nowrap; }
.mb-img-retry {
  border: 1px solid var(--border, #dadce0); background: #fff; color: var(--accent, #2563eb);
  border-radius: 6px; padding: 3px 11px; font-size: 12.5px; font-weight: 500; cursor: pointer;
}
.mb-img-retry:hover { background: var(--row-hover, #f1f3f4); }
@media (prefers-reduced-motion: reduce) { .mb-img-loading { animation: none; } }
/* Links inside a message body (incl. linkified plain-text URLs) render like
   Gmail: blue, underlined on hover. An HTML email that colours its own links
   inline overrides this. */
.msg-body a { color: #1a73e8; text-decoration: none; }
.msg-body a:hover { text-decoration: underline; }
.msg-body img { max-width: 100%; height: auto; }
.msg-body .plain { white-space: pre-wrap; font-family: Arial, Helvetica, sans-serif; margin: 0; }
/* Reply/Forward foot bar. It STICKS to the bottom of the reading pane while the
   conversation is taller than the viewport (Gmail behaviour), then settles inline
   once you scroll all the way down. Full-bleed so the surface hides content
   scrolling under it; the shadow shows only while it's pinned (.stuck, toggled by
   an IntersectionObserver watching the sentinel that follows it). */
.conv-reply-bar {
  position: sticky; bottom: 0; z-index: 2;
  display: flex; gap: 10px;
  /* full-bleed horizontally; the container now has no bottom padding, so bottom:0
     pins the bar flush to the true pane bottom. */
  margin: 18px calc(-1 * var(--conv-pad-x)) 0;
  padding: 14px var(--conv-pad-x) 16px calc(54px + var(--conv-pad-x));
  background: var(--surface);
  transition: box-shadow .18s ease;
}
.conv-reply-bar.stuck { box-shadow: 0 -1px 0 var(--border), 0 -8px 18px -8px rgba(20, 30, 60, .14); }
/* Zero-height marker after the bar; when it scrolls into view we've hit the end.
   Carries the conversation's former bottom breathing room so the settled bar
   isn't jammed against the last message. */
.conv-foot-sentinel { height: 1px; margin-bottom: 24px; }
.conv-reply-bar .btn-ghost {
  display: inline-flex; align-items: center; gap: 8px; padding: 9px 20px; border-radius: 20px;
  /* Gmail's Reply/Forward chips have a clearly-visible border + darker text, not
     the faint --border/--text-soft used for low-priority ghost buttons. */
  border-color: #c4c9d4; color: var(--text);
}
.conv-reply-bar .btn-ghost svg { stroke: var(--text-soft); }
.conv-reply-bar .btn-ghost:hover { background: var(--surface-2); border-color: #adb3c0; color: var(--text); }

/* REMOVED — collapsed .conv-draft-chip styles deleted 2026-07-20 by user
   request: thread drafts always restore as the full inline editor now. */

/* Inline reply/forward editor at the foot of the conversation (Gmail-style):
   the sender avatar sits to the LEFT of the reply card, like a message. */
/* The avatar column must line up EXACTLY with the message avatars above (same
   40px size, same x). gap:14px matches .msg-head so the card's left edge also
   aligns with the message bodies; no negative margin (that shifted it 8px left
   of the message avatars). */
.inline-reply-wrap {
  display: flex; align-items: flex-start; gap: 14px; margin: 18px 0 8px 0;
  /* Full-bleed horizontal rule UNDER the draft, separating it from the message
     below (Gmail behaviour). Drawn edge-to-edge like the message dividers by
     pulling the border out over the conversation's horizontal padding. */
  position: relative; padding-bottom: 20px;
  border-bottom: 1px solid var(--border);
  margin-left: calc(-1 * var(--conv-pad-x)); margin-right: calc(-1 * var(--conv-pad-x));
  padding-left: var(--conv-pad-x); padding-right: var(--conv-pad-x);
}
.ir-avatar {
  width: 40px; height: 40px; border-radius: 50%; background: var(--accent); color: #fff;
  display: inline-flex; align-items: center; justify-content: center; font-weight: 700;
  flex-shrink: 0; margin-top: 4px;
}
.inline-reply {
  flex: 1; min-width: 0; background: var(--surface);
  border: 1px solid var(--border); border-radius: 12px;
  box-shadow: 0 4px 16px rgba(20,30,60,.16), 0 1px 3px rgba(20,30,60,.10); padding: 12px 16px 14px;
}
/* "Loading draft…" placeholder shown at the conversation foot while a saved
   draft is still downloading (instant open from cache). Same card geometry as
   the real inline editor so the swap doesn't reflow. */
.draft-loading {
  display: flex; align-items: center; gap: 10px;
  color: var(--text-soft); font-size: 14px; min-height: 44px;
}
.draft-loading-spinner {
  width: 16px; height: 16px; flex-shrink: 0; border-radius: 50%;
  border: 2px solid var(--border); border-top-color: var(--accent);
  animation: draft-spin .7s linear infinite;
}
@keyframes draft-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .draft-loading-spinner { animation-duration: 2s; } }
.ir-head { display: flex; align-items: center; gap: 8px; color: var(--text-soft); font-size: 13px; padding-bottom: 8px; }
.ir-title { font-weight: 600; color: var(--text); }
.ir-head .ir-close { margin-left: auto; width: 28px; height: 28px; }
.ir-row { display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--border); padding: 6px 0; }
.ir-row label { width: 56px; flex-shrink: 0; font-size: 13px; color: var(--text-mute); cursor: text; }
.ir-row input, .ir-row select {
  flex: 1; border: none; outline: none; background: transparent;
  font-size: 14px; color: var(--text); font-family: var(--font); padding: 2px 0;
}
/* Cc / Bcc reveal toggles, shared by the compose popup and the inline reply.
   They sit at the right end of the To row and disappear once their row is up. */
.ir-cc-toggle, .cc-toggle { border: none; background: transparent; color: var(--text-soft); font-size: 13px; cursor: pointer; padding: 2px 8px; flex-shrink: 0; }
.ir-cc-toggle:hover, .cc-toggle:hover { color: var(--accent); }
.ir-text {
  width: 100%; border: none; outline: none; resize: vertical; min-height: 140px;
  margin-top: 12px; font-family: var(--font); font-size: 14px; line-height: 1.6; color: var(--text);
}
.ir-error { color: #e8483c; font-size: 13px; margin-top: 6px; }
.ir-foot { display: flex; align-items: center; gap: 4px; margin-top: 10px; flex-wrap: wrap; }
.ir-foot .ir-discard { margin-left: auto; }
.ir-toolbar { display: flex; align-items: center; gap: 1px; flex-wrap: wrap; }

/* Collapsed message (Gmail-style): the avatar column, then a two-line meta block
   — sender + date on top, and the trimmed snippet in DARK text on its own line
   below. Reset the inherited expanded paddings so spacing comes from the head. */
.msg.collapsed { padding-top: 0; padding-bottom: 0; cursor: pointer; }
.msg + .msg.collapsed { padding-top: 0; }
.msg.collapsed .msg-head { align-items: flex-start; gap: 14px; padding: 14px 0; border-radius: 8px; }
.msg.collapsed:hover .msg-head { background: var(--surface-2); padding-left: 8px; padding-right: 8px; margin: 0 -8px; }
.msg.collapsed .msg-from { font-weight: 600; flex-shrink: 0; }
/* The two-line meta column to the right of the avatar. */
.msg-collapsed-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.msg-collapsed-top { display: flex; align-items: baseline; gap: 10px; }
.msg-collapsed-top .msg-attach-ind { margin-left: auto; flex-shrink: 0; }
/* When a paperclip is present it already carries the margin-left:auto that
   pushes the trailing group right; the date then just sits next to it. */
.msg-collapsed-top .msg-attach-ind + .msg-date { margin-left: 0; }
.msg-collapsed-top .msg-date { margin-left: auto; flex-shrink: 0; }
/* Collapsed-row attachment paperclip: muted, aligned with the date baseline. */
.msg-attach-ind { display: inline-flex; align-items: center; color: var(--text-mute); flex-shrink: 0; position: relative; top: 2px; }
/* A sender name is hoverable (shows the copy-email card) without looking like a link. */
/* The sender name is only as wide as its text, so the hover card only triggers
   on the name itself (not the rest of the line). */
.msg-sender { cursor: default; display: inline-block; }
.msg-sender:hover { text-decoration: underline; text-underline-offset: 2px; }
.msg-avatar[data-sender-addr] { cursor: default; }
/* Trimmed content on its OWN line, in dark text (Gmail), single-line ellipsis. */
/* Match the expanded .msg-body font exactly so the one-liner preview reads in the
   same family/size as the message it collapses (default email body font). */
.msg-collapsed-snippet { min-width: 0; color: var(--text); font-family: Arial, Helvetica, sans-serif; font-size: 14px; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg.expanded .msg-head { cursor: pointer; }

/* Middle-group collapse marker (Gmail-style): a ROUND count badge in the avatar
   column, sitting ON TWO thin full-width rules. Clicking the row expands the
   group. Full-bleed (negative margins) so the rules reach the true panel edges,
   exactly like the .msg dividers. */
.msg-group {
  position: relative;
  display: flex; align-items: center;
  /* Tighter than a full message gap — a collapser should SAVE vertical space. */
  padding: 12px var(--conv-pad-x);
  margin-left: calc(-1 * var(--conv-pad-x));
  margin-right: calc(-1 * var(--conv-pad-x));
  cursor: pointer;
}
/* Two separate thin 1px rules with a clear 2px gap between them (Gmail-style),
   spanning the FULL width behind the badge, edge to edge. */
.msg-group::before {
  content: ""; position: absolute; left: 0; right: 0; top: 50%;
  transform: translateY(-50%); height: 4px;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  pointer-events: none;
}
/* Force the messages immediately around the group to give up their edge padding
   so the gap above and below the badge is EQUAL regardless of whether the
   neighbour is expanded (pb 22) or collapsed (pb 0). */
.msg:has(+ .msg-group) { padding-bottom: 0; }
.msg-group + .msg { padding-top: 0; border-top: none; }
/* A COLLAPSED message keeps 11px of internal head padding, which would add extra
   space on the side facing the group and break the above/below symmetry. Trim
   the head padding on the side adjacent to the group so the badge is centred. */
.msg.collapsed:has(+ .msg-group) .msg-head { padding-bottom: 0; }
.msg-group + .msg.collapsed .msg-head { padding-top: 0; }
/* Round badge, identical 40px footprint to .msg-avatar so it lines up in the
   avatar column, sitting on the rules as a chip: white fill, thin grey border,
   a very small drop shadow. */
.msg-group-badge {
  position: relative; z-index: 1;
  width: 40px; height: 40px; flex-shrink: 0; border-radius: 50%;
  border: 1px solid var(--border); background: var(--surface); color: var(--text-soft);
  font-size: 13px; font-weight: 600; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: 0 1px 2px rgba(20,30,60,.12);
}
.msg-group:hover .msg-group-badge { background: var(--surface-2); color: var(--text); }

/* Quote-hiding "•••" toggle inside a message body (trimmed/duplicated content). */
.quote-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  margin: 8px 0; padding: 1px 8px; height: 20px;
  background: var(--surface-2); border: none; border-radius: 11px; color: var(--text-soft); cursor: pointer;
}
.quote-toggle:hover, .quote-toggle.active { background: var(--border); }
.quote-hidden { margin-top: 6px; }

/* Same "•••" affordance INSIDE an editor (a restored draft's quoted history —
   Gmail parity: the draft opens clean, the pill reveals the quote chain). */
.edit-quote { margin-top: 10px; }
.edit-quote-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  margin: 2px 0 6px; padding: 1px 8px; height: 20px;
  background: var(--surface-2); border: none; border-radius: 11px; color: var(--text-soft); cursor: pointer;
}
.edit-quote-toggle:hover, .edit-quote-toggle.active { background: var(--border); }

/* Expand-all toolbar button active state. */
#conv-expand.active { color: var(--accent); }

/* Snooze overlay only (compose no longer dims the app behind it). */
.snooze-overlay { position: fixed; inset: 0; background: rgba(20,30,60,.2); z-index: 50; }

/* Filter-creation dialog (advanced search → New filter). */
.filter-pop {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 460px; max-width: calc(100vw - 32px); background: var(--surface);
  border-radius: 14px; box-shadow: var(--shadow); z-index: 60; padding: 22px;
}
.filter-pop h3 { margin: 0 0 6px; font-size: 17px; }
.filter-hint { font-size: 12.5px; color: var(--text-soft); line-height: 1.5; margin-bottom: 12px; }
.filter-query { width: 100%; box-sizing: border-box; margin-bottom: 10px; font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12.5px; }
.filter-foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }

/* Compose: a floating window that sits ON TOP of the app without darkening it. */
.compose-window {
  position: fixed; right: 24px; bottom: 0; width: 540px; max-width: calc(100vw - 32px);
  background: var(--surface); border-radius: 14px 14px 0 0; box-shadow: 0 -2px 30px rgba(20,30,60,.25); z-index: 60;
  display: flex; flex-direction: column; max-height: 86vh;
}
.compose-window.minimized { max-height: none; }
.compose-window.minimized .compose-body, .compose-window.minimized .compose-foot { display: none; }
/* Maximized = Gmail-style centered modal: a wide, tall card floating in the
   middle of the viewport over a dimmed backdrop, with generous side margins
   (NOT the docked bottom-right corner). */
.compose-window.maximized {
  right: auto; left: 50%; top: 50%; bottom: auto;
  transform: translate(-50%, -50%);
  width: min(1050px, calc(100vw - 96px));
  height: calc(100vh - 96px); max-height: calc(100vh - 96px);
  border-radius: 12px; box-shadow: 0 8px 40px rgba(20,30,60,.32);
}
/* Dimmed backdrop behind the maximized compose modal. */
.compose-overlay { position: fixed; inset: 0; background: rgba(20,30,60,.32); z-index: 55; }
.compose-overlay[hidden] { display: none; }
.compose-head { display: flex; align-items: center; justify-content: space-between; padding: 6px 8px 6px 16px; background: var(--surface-2); border-radius: 14px 14px 0 0; font-size: 13px; font-weight: 600; cursor: pointer; }
.compose-window.maximized .compose-head { border-radius: 14px 14px 0 0; }
.compose-win-actions { display: flex; align-items: center; gap: 2px; }
.win-btn { border: none; background: transparent; color: var(--text-soft); width: 28px; height: 28px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
.win-btn:hover { background: rgba(20,30,60,.10); color: var(--text); }
.compose-body { padding: 4px 16px 14px; overflow-y: auto; flex: 1; }
.compose-row { display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--border); padding: 7px 0; }
.compose-row label { width: 56px; flex-shrink: 0; font-size: 13px; color: var(--text-mute); cursor: text; }
.compose-row input, .compose-row select {
  flex: 1; border: none; outline: none; font-size: 14px; background: transparent; color: var(--text);
  padding: 4px 0; margin: 0; font-family: var(--font);
  -webkit-appearance: none; -moz-appearance: none; appearance: none; text-indent: 0;
}
/* Keep the dropdown chevron on the From select even with appearance reset. */
.compose-row select { padding-right: 22px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%235f6571' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 2px center; }
.compose-error { background: #fdecec; color: var(--danger); padding: 8px 12px; border-radius: 8px; font-size: 13px; margin-top: 10px; }
.compose-foot { padding: 10px 14px; border-top: 1px solid var(--border); display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.compose-foot .btn-primary { order: -1; }
/* REMOVED — do not restore without explicit user request. The read-draft-aloud
   button used to accent itself and relabel to "Stop reading" while a reading
   ran. It is a plain start button now: stopping belongs to the floating control
   pill next to the text. */

/* Rich-text contentEditable editor (compose + inline reply share .editor-html).
   The composing font is Gmail's classic Arial (not the app's Plus Jakarta Sans)
   so drafts read exactly like a Gmail message. */
.editor-html {
  width: 100%; min-height: 200px; outline: none; margin-top: 12px;
  font-family: Arial, Helvetica, sans-serif; font-size: 14px; line-height: 1.6; color: var(--text);
  overflow-y: auto; word-break: break-word;
}
.editor-html.ir-text { min-height: 130px; max-height: 320px; margin-top: 10px; }
.editor-html:empty::before { content: attr(data-placeholder); color: var(--text-mute); pointer-events: none; }
.editor-html img { max-width: 100%; height: auto; }
/* A selectable inline image gets a subtle pointer affordance in the editor. */
.editor-html img { cursor: pointer; }
/* Gmail-style inline-image resize overlay. One body-level box (position:fixed,
   the compose popup is itself fixed) tracks the selected editor image; the
   frame ignores the pointer so clicking the image still works, only the four
   corner handles are interactive. */
.img-resize-box {
  position: fixed; z-index: 3000; display: none;
  border: 1.5px solid var(--accent, #1a73e8);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .6);
  pointer-events: none; box-sizing: border-box;
}
.img-resize-box .irh {
  position: absolute; width: 12px; height: 12px; box-sizing: border-box;
  background: #fff; border: 1.5px solid var(--accent, #1a73e8); border-radius: 2px;
  pointer-events: auto;
}
.img-resize-box .irh[data-corner="nw"] { top: -6px; left: -6px; cursor: nwse-resize; }
.img-resize-box .irh[data-corner="ne"] { top: -6px; right: -6px; cursor: nesw-resize; }
.img-resize-box .irh[data-corner="sw"] { bottom: -6px; left: -6px; cursor: nesw-resize; }
.img-resize-box .irh[data-corner="se"] { bottom: -6px; right: -6px; cursor: nwse-resize; }
.img-resize-box .img-resize-dim {
  position: absolute; left: 50%; bottom: -24px; transform: translateX(-50%);
  background: rgba(32, 33, 36, .85); color: #fff; font-size: 11px; line-height: 1;
  padding: 3px 6px; border-radius: 4px; white-space: nowrap; pointer-events: none;
}
.img-resize-box .img-resize-dim:empty { display: none; }
/* While dragging a handle, suppress text selection/caret across the page. */
body.img-resizing, body.img-resizing * { user-select: none !important; }
/* Drag-and-drop file overlay (compose body + inline reply). The zone gets
   position:relative so the overlay covers exactly it; it only shows while a
   file drag is over the editor (.dnd-active). */
.compose-body, .inline-reply { position: relative; }
.dnd-overlay {
  position: absolute; inset: 0; z-index: 30; display: none;
  align-items: center; justify-content: center;
  background: rgba(255, 255, 255, .82);
  border: 2px dashed var(--border); border-radius: 12px;
  pointer-events: none; backdrop-filter: blur(1px);
}
.dnd-active > .dnd-overlay { display: flex; }
.dnd-overlay-inner {
  font-size: 14px; font-weight: 500; color: var(--text-mute);
  letter-spacing: .2px;
}
.forward-quote { color: var(--text-soft); border-left: 2px solid var(--border); padding-left: 12px; margin-top: 8px; }
.forward-sep { border: none; border-top: 1px solid var(--border); margin: 14px 0; }

/* Formatting toolbar (font, size, bold/italic/underline, colors, lists). */
.fmt-toolbar { display: flex; align-items: center; gap: 1px; flex-wrap: wrap; }
.fmt-btn { border: none; background: transparent; color: var(--text-soft); width: 30px; height: 30px; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
.fmt-btn:hover { background: rgba(20,30,60,.10); color: var(--text); }
/* Master formatting toggle (Gmail-style "A"): reveals the whole group. */
.fmt-toggle { border: none; background: transparent; color: var(--text-soft); width: 32px; height: 32px; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
.fmt-toggle:hover { background: rgba(20,30,60,.10); color: var(--text); }
.fmt-toggle.on { background: rgba(20,30,60,.10); color: var(--accent); }
/* The formatting group is a flex inline that only shows when un-hidden. */
.fmt-group { display: flex; align-items: center; gap: 1px; flex-wrap: wrap; }
.fmt-group[hidden] { display: none; }
/* Font/size popover behind the "Type" glyph so the two dropdowns don't clash
   with the Send button height when the bar is open. */
.fmt-fonts-wrap { position: relative; display: inline-flex; }
.fmt-fonts-pop { position: absolute; bottom: calc(100% + 6px); left: 0; z-index: 5; display: flex; gap: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 6px; box-shadow: 0 6px 20px rgba(20,30,60,.18); }
.fmt-fonts-pop[hidden] { display: none; }
.fmt-fonts-toggle.on { background: rgba(20,30,60,.10); color: var(--accent); }
.fmt-select { border: 1px solid var(--border); background: var(--surface); color: var(--text); font-size: 12px; border-radius: 6px; padding: 4px 4px; cursor: pointer; max-width: 110px; }
.fmt-sep { width: 1px; height: 20px; background: var(--border); margin: 0 4px; }
.fmt-color { position: relative; width: 30px; height: 30px; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; border: none; background: transparent; }
.fmt-color:hover { background: rgba(20,30,60,.10); }
.fmt-color-ico { font-weight: 700; font-size: 14px; line-height: 1; border-bottom: 3px solid #000; padding-bottom: 1px; color: var(--text); }
.fmt-color-ico.fmt-hilite { background: #ffec3d; padding: 0 2px; border-bottom: none; }
.fmt-color:hover { background: rgba(20,30,60,.10); }

/* 36-colour preset palette popup (text colour / highlight colour). */
.color-palette {
  position: fixed; z-index: 95; background: var(--surface);
  border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 8px 28px rgba(20,30,60,.22);
  padding: 10px;
}
.cp-title { font-size: 12px; font-weight: 600; color: var(--text-soft); margin: 0 2px 8px; }
.cp-grid { display: grid; grid-template-columns: repeat(6, 22px); gap: 6px; }
.cp-swatch { width: 22px; height: 22px; border-radius: 5px; border: 1px solid rgba(0,0,0,.12); cursor: pointer; padding: 0; }
.cp-swatch:hover { transform: scale(1.12); box-shadow: 0 0 0 2px var(--accent-light, rgba(99,102,241,.25)); }

/* Attachment chips */
.attach-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.attach-chip { display: inline-flex; align-items: center; gap: 6px; background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 5px 8px; font-size: 13px; color: var(--text); cursor: pointer; }
.attach-chip:hover { background: var(--surface-3, #eef1f6); border-color: var(--text-mute); }
.attach-size { color: var(--text-mute); font-size: 12px; }
.attach-x { border: none; background: transparent; color: var(--text-soft); cursor: pointer; font-size: 16px; line-height: 1; padding: 0 2px; }
.attach-x:hover { color: var(--danger, #e8483c); }

/* ── Calendar invitation card ────────────────────────────────────────────────
   The message carries an .ics, so instead of an opaque "calendar.ics" chip we
   render the event itself. Deliberately LOUD compared with an attachment card:
   the user asked to be proposed the calendar add "loudly and visibly", and an
   invitation is the point of the message rather than a file hanging off it.
   Aligned to the same 54px body indent as .msg-body / .msg-attachments so it
   sits in the message's text column, not floating in the avatar gutter. */
.cal-ev {
  box-sizing: border-box;
  /* Sits between the message header and the body, so it needs breathing room on
     both sides rather than the tighter spacing an attachment strip uses. */
  margin: 14px 0 4px 54px; max-width: 560px;
  border: 1px solid var(--border); border-left: 4px solid var(--accent);
  border-radius: 12px; padding: 14px 16px 12px;
  background: linear-gradient(180deg, var(--accent-soft) 0%, var(--surface) 62%);
  color: var(--text);
}
.cal-ev-head { display: flex; align-items: flex-start; gap: 12px; }
.cal-ev-badge {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 40px; height: 40px; border-radius: 10px;
  background: var(--surface); color: var(--accent);
  border: 1px solid var(--border);
}
.cal-ev-headtxt { min-width: 0; }
.cal-ev-kicker {
  font-size: 11px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  color: var(--accent);
}
.cal-ev-title {
  font-size: 17px; font-weight: 700; line-height: 1.3; margin-top: 2px;
  word-break: break-word;
}
.cal-ev-when { font-size: 15px; font-weight: 600; margin: 10px 0 0 52px; }
.cal-ev-rel { font-weight: 400; color: var(--text-mute); }
.cal-ev-rows { margin: 8px 0 0 52px; display: flex; flex-direction: column; gap: 5px; }
.cal-ev-row { display: flex; align-items: flex-start; gap: 8px; font-size: 13px; color: var(--text-mute); }
.cal-ev-ico { display: inline-flex; flex: none; margin-top: 1px; color: var(--text-soft); }
.cal-ev-rowtxt { min-width: 0; word-break: break-word; }
.cal-ev-guests { color: var(--text-soft); }
.cal-ev-note { color: var(--text-soft); font-style: italic; }
.cal-ev-link { color: var(--accent); }
.cal-ev-link:hover { text-decoration: underline; }

.cal-ev-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 14px 0 0 52px; }
.cal-ev-btn {
  display: inline-flex; align-items: center; gap: 8px;
  border-radius: 999px; padding: 9px 16px; cursor: pointer;
  font-size: 13.5px; font-weight: 600; font-family: inherit;
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
  text-decoration: none; transition: box-shadow .12s ease, border-color .12s ease, filter .12s ease;
}
.cal-ev-btn svg { flex: none; }
.cal-ev-btn-primary {
  border-color: transparent; background: var(--accent); color: #fff;
  box-shadow: 0 1px 3px rgba(60, 64, 67, .22);
}
.cal-ev-btn-primary:hover { filter: brightness(.95); box-shadow: 0 2px 6px rgba(60, 64, 67, .28); }
/* The Google "G" is multi-colour, so it needs a white chip to stay legible on
   the blue button rather than disappearing into it. */
.cal-ev-btn-primary svg { background: #fff; border-radius: 50%; padding: 2px; box-sizing: content-box; }
.cal-ev-btn-ghost:hover { border-color: var(--accent); color: var(--accent); }
.cal-ev-btn-done {
  border-color: transparent; background: #e6f4ea; color: #137333; cursor: default;
}
.cal-ev-btn-done:hover { filter: brightness(.97); }
.cal-ev-btn:disabled { opacity: .7; cursor: default; }
.cal-ev-btn.is-loading { opacity: .75; cursor: progress; }

/* A cancelled invitation still renders (the user must see WHY it is in the
   inbox) but reads as struck-through and grey, and offers no add action. */
.cal-ev-cancelled { border-left-color: #d93025; background: var(--surface); }
.cal-ev-cancelled .cal-ev-kicker { color: #d93025; }
.cal-ev-cancelled .cal-ev-badge { color: #d93025; }
.cal-ev-cancelled .cal-ev-title { text-decoration: line-through; color: var(--text-mute); }
/* An event already in the past is muted: still addable, just not urgent. */
.cal-ev-past { background: var(--surface); border-left-color: var(--border-strong, #c4c7cc); }
.cal-ev-past .cal-ev-kicker { color: var(--text-mute); }
.cal-ev-past .cal-ev-badge { color: var(--text-mute); }

/* Narrow screens: drop the 52px text indent so the card keeps its content
   width instead of squeezing the title into two words per line. */
@media (max-width: 700px) {
  .cal-ev { margin-left: 0; padding: 12px 13px 11px; }
  .cal-ev-when, .cal-ev-rows, .cal-ev-actions { margin-left: 0; }
  .cal-ev-title { font-size: 16px; }
  .cal-ev-btn { padding: 9px 14px; }
}

/* Received-message attachment cards (Gmail-style large preview chips) */
.msg-attachments { padding: 4px 0 6px 54px; }
.msg-att-label { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-mute); margin: 10px 0 10px; }
.msg-att-label svg { color: var(--text-soft); }
.msg-att-cards { display: flex; flex-wrap: wrap; gap: 12px; }

/* A large card: preview area on top, filename+size footer below, hover actions. */
.msg-att-card {
  position: relative; box-sizing: border-box;
  width: 260px; max-width: 100%;
  border: 1px solid var(--border); border-radius: 12px; overflow: hidden;
  background: var(--surface); color: var(--text);
  transition: border-color .12s ease, box-shadow .12s ease;
}
.msg-att-card:hover { border-color: var(--border-strong, #c4c7cc); box-shadow: 0 1px 4px rgba(60,64,67,.18); }

/* Top preview area (thumbnail for images, big glyph otherwise). */
.msg-att-preview { display: block; height: 128px; background: var(--surface-2); text-decoration: none; }
.msg-att-thumb { display: block; width: 100%; height: 100%; background-size: cover; background-position: center top; background-repeat: no-repeat; }
.msg-att-glyph { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; color: var(--text-soft); }
.msg-att-preview .msg-att-glyph svg { width: 44px; height: 44px; }

/* Cards with NO real preview (the area only holds a type icon) don't earn a
   260×128 canvas — compact them to a Gmail-chip footprint. Image AND video
   cards are excluded: both show real picture content, which earns the size. */
.msg-att-card:not(.msg-att-card-image):not(.msg-att-card-video) { width: 188px; }
.msg-att-card:not(.msg-att-card-image):not(.msg-att-card-video) .msg-att-preview { height: 52px; }
.msg-att-card:not(.msg-att-card-image):not(.msg-att-card-video) .msg-att-preview .msg-att-glyph svg { width: 26px; height: 26px; }
.msg-att-card:not(.msg-att-card-image):not(.msg-att-card-video) .msg-att-actbar { top: 10px; right: 6px; }
.msg-att-card:not(.msg-att-card-image):not(.msg-att-card-video) .msg-att-act { width: 28px; height: 28px; }
.msg-att-card:not(.msg-att-card-image):not(.msg-att-card-video) .msg-att-act svg { width: 15px; height: 15px; }
.msg-att-card:not(.msg-att-card-image):not(.msg-att-card-video) .msg-att-foot { padding: 7px 10px; }

/* Video card poster: the first frame painted by a muted, controls-less <video>,
   with a play badge over it so it reads as "startable" rather than as a still.
   pointer-events:none keeps every click on the card's own preview button, so
   the overlay viewer opens instead of the element's native controls. */
.msg-att-thumb-video { position: relative; background: #16181c; }
.msg-att-vthumb {
  display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: center top;
  pointer-events: none; background: #16181c;
}
.msg-att-playbadge {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  display: flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; border-radius: 50%;
  background: rgba(0, 0, 0, .55); color: #fff; pointer-events: none;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .35);
}
.msg-att-playbadge svg { margin-left: 2px; } /* optical centring of the triangle */
.msg-att-card-video:hover .msg-att-playbadge { background: rgba(0, 0, 0, .72); }

/* Type-tinted glyphs (icon colour only, matching common file-type colours). */
.msg-att-glyph-pdf { color: #d93025; }
.msg-att-glyph-sheet { color: #188038; }
.msg-att-glyph-doc { color: #1a73e8; }
.msg-att-glyph-slide { color: #e37400; }
.msg-att-glyph-image { color: #7b53c1; }
.msg-att-glyph-video { color: #d93025; }
.msg-att-glyph-audio { color: #e37400; }
.msg-att-glyph-zip { color: #5f6368; }

/* Hover action bar (top-right of the preview). */
.msg-att-actbar {
  position: absolute; top: 8px; right: 8px; display: flex; gap: 6px;
  opacity: 0; transition: opacity .12s ease;
}
.msg-att-card:hover .msg-att-actbar { opacity: 1; }
.msg-att-act {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: 50%;
  background: var(--surface); color: var(--text); text-decoration: none;
  border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(60,64,67,.2);
}
.msg-att-act:hover { background: var(--surface-2); color: var(--accent); }
/* "Open in Google Sheets" keeps its green glyph on hover (don't tint to accent). */
.msg-att-act-gsheet:hover svg { stroke: #0f9d58; }
/* Dark high-contrast "Sheet is ready → Open" bubble, anchored on the attachment
   card when a popup blocker stopped the auto-open. Rendered on document.body. */
.sheet-ready-bubble {
  position: fixed; z-index: 100000;
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px 10px 14px; border-radius: 12px;
  background: #202124; color: #fff; cursor: pointer;
  box-shadow: 0 8px 28px rgba(0,0,0,.38), 0 0 0 1px rgba(255,255,255,.06);
  font-size: 13.5px; font-weight: 500; white-space: nowrap;
  animation: srb-pop .16s ease-out;
}
@keyframes srb-pop { from { opacity: 0; transform: translateY(4px) scale(.97); } to { opacity: 1; transform: none; } }
.sheet-ready-bubble .srb-icon { flex: 0 0 auto; }
.sheet-ready-bubble .srb-text { flex: 1 1 auto; }
.sheet-ready-bubble .srb-open {
  flex: 0 0 auto; cursor: pointer; font: inherit; font-weight: 600;
  color: #202124; background: #fff; border: none;
  padding: 6px 16px; border-radius: 8px;
}
.sheet-ready-bubble .srb-open:hover { background: #e8eaed; }
/* On-card variant: covers the attachment card, content stacked vertically. */
.sheet-ready-bubble.srb-oncard {
  flex-direction: column; gap: 8px; padding: 12px;
  white-space: normal; text-align: center; border-radius: 14px;
}
.sheet-ready-bubble.srb-oncard .srb-text { flex: 0 0 auto; font-size: 12.5px; line-height: 1.3; }
.sheet-ready-bubble.srb-oncard .srb-open { padding: 5px 18px; }
/* Spinner while the xlsx is uploaded/converted to a Google Sheet. */
.msg-att-act.is-loading { pointer-events: none; position: relative; }
.msg-att-act.is-loading svg { opacity: 0; }
.msg-att-act.is-loading::after {
  content: ""; position: absolute; width: 15px; height: 15px; border-radius: 50%;
  border: 2px solid var(--border); border-top-color: #0f9d58;
  animation: msg-att-spin .7s linear infinite;
}
@keyframes msg-att-spin { to { transform: rotate(360deg); } }

/* Footer: small type icon + filename + size. */
.msg-att-foot { display: flex; align-items: center; gap: 8px; padding: 9px 11px; border-top: 1px solid var(--border); }
.msg-att-ficon { flex: 0 0 auto; display: inline-flex; }
.msg-att-info { display: flex; flex-direction: column; min-width: 0; }
.msg-att-name { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.msg-att-size { font-size: 12px; color: var(--text-mute); margin-top: 1px; }

/* Preview/Open triggers are <button>s (intercepted for the in-app viewer); strip
   their native chrome so they read as the card surface, not a form control. */
button.msg-att-preview { display: block; width: 100%; padding: 0; border: none; cursor: pointer; font: inherit; }
button.msg-att-act { cursor: pointer; font: inherit; }

/* Optimistic "Sending…" card: the file has no server id yet, so the card is a
   non-interactive preview (glyph + name + size). Read as in-flight, not broken:
   default cursor, no hover lift, no action bar. */
.msg-att-card-pending { cursor: default; }
/* A PENDING video has no server URL yet, so its card shows a glyph, not a
   poster. Without this it would keep the full-size video footprint and render
   as a large empty rectangle — compact it like every other preview-less card. */
.msg-att-card-pending.msg-att-card-video { width: 188px; }
.msg-att-card-pending.msg-att-card-video .msg-att-preview { height: 52px; }
.msg-att-card-pending.msg-att-card-video .msg-att-preview .msg-att-glyph svg { width: 26px; height: 26px; }
.msg-att-card-pending.msg-att-card-video .msg-att-foot { padding: 7px 10px; }
.msg-att-card-pending:hover { border-color: var(--border, #dadce0); box-shadow: none; }
.msg-att-card-pending .msg-att-preview { cursor: default; }

/* In-app attachment viewer overlay (Gmail-style lightbox). */
.att-viewer-overlay {
  position: fixed; inset: 0; z-index: 200;
  display: flex; flex-direction: column;
  background: rgba(20, 22, 26, .85);
}
.att-viewer-head {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  height: 56px; padding: 0 18px; color: #fff; flex: 0 0 auto;
}
.att-viewer-name { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.att-viewer-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.att-viewer-zoom { display: flex; align-items: center; gap: 4px; margin-right: 4px; padding-right: 8px; border-right: 1px solid rgba(255, 255, 255, .18); }
.att-viewer-zoom-pct { min-width: 44px; text-align: center; font-size: 13px; color: rgba(255, 255, 255, .9); font-variant-numeric: tabular-nums; }
.att-viewer-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border-radius: 50%;
  background: transparent; color: #fff; border: none; cursor: pointer; text-decoration: none;
  transition: background .12s ease;
}
.att-viewer-btn:hover { background: rgba(255, 255, 255, .16); }
.att-viewer-body { flex: 1 1 auto; min-height: 0; display: flex; align-items: center; justify-content: center; padding: 0 24px 24px; }
.att-viewer-frame { width: 100%; height: 100%; border: none; border-radius: 8px; background: #fff; }
.att-viewer-img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 8px; box-shadow: 0 8px 40px rgba(0, 0, 0, .5); }
/* In-app media player. The video is contained (never cropped) and never grows
   past the overlay; audio is a normal-width control bar rather than a player
   stretched across a 2000px screen. */
.att-viewer-video {
  max-width: 100%; max-height: 100%; border-radius: 8px;
  background: #000; box-shadow: 0 8px 40px rgba(0, 0, 0, .5);
}
.att-viewer-audio { width: min(560px, 100%); }
.att-viewer-medianote {
  margin: auto; max-width: 420px; text-align: center;
  color: rgba(255, 255, 255, .85); font-size: 14px; line-height: 1.5;
}
/* pdf.js-rendered PDF: a scrollable stack of page canvases (Gmail-style), so
   the browser's native PDF viewer chrome is never involved. */
.att-viewer-pdf {
  align-self: stretch; flex: 1 1 auto; min-height: 0;
  overflow: auto; display: block; padding: 16px 0; text-align: center;
}
/* margin auto (not flex centering) so a zoomed page wider than the viewport
   can still be scrolled to on BOTH sides instead of being clipped on the left. */
.att-viewer-pdf-page {
  display: block; margin: 0 auto 16px; border-radius: 4px;
  box-shadow: 0 4px 24px rgba(0, 0, 0, .45); background: #fff;
}
.att-viewer-pdf-page:last-child { margin-bottom: 0; }
.att-viewer-pdf-loading { color: rgba(255, 255, 255, .85); font-size: 14px; margin: auto; }

/* In-app editable spreadsheet overlay (x-spreadsheet + SheetJS) */
.sheet-editor-overlay {
  position: fixed; inset: 0; z-index: 200;
  display: flex; flex-direction: column; background: #f6f7f9;
}
.sheet-editor-head {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  height: 52px; padding: 0 14px; flex: 0 0 auto;
  background: var(--surface); border-bottom: 1px solid var(--border);
}
.sheet-editor-name { font-size: 14px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sheet-editor-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.sheet-editor-btn {
  display: inline-flex; align-items: center; gap: 7px;
  height: 34px; padding: 0 14px; border-radius: 8px; cursor: pointer;
  background: var(--surface); color: var(--text); border: 1px solid var(--border);
  font-size: 13px; font-weight: 500; text-decoration: none;
  transition: background .12s ease, border-color .12s ease, opacity .12s ease;
}
.sheet-editor-btn:hover { background: var(--surface-2); }
.sheet-editor-btn[disabled] { opacity: .5; cursor: default; pointer-events: none; }
.sheet-editor-btn.sheet-editor-primary { background: var(--accent, #1a73e8); color: #fff; border-color: transparent; }
.sheet-editor-btn.sheet-editor-primary:hover { filter: brightness(.95); background: var(--accent, #1a73e8); }
/* the close button is icon-only */
.sheet-editor-btn[data-sheet-close] { padding: 0; width: 34px; justify-content: center; }
.sheet-editor-body { flex: 1 1 auto; min-height: 0; position: relative; }
.sheet-editor-loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--text-mute); font-size: 14px; }
.sheet-editor-grid { position: absolute; inset: 0; }
/* x-spreadsheet paints into a canvas that reads its size from the container. */
.sheet-editor-grid .x-spreadsheet { height: 100%; }

/* Snooze */
.snooze-pop {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 340px; max-width: calc(100vw - 32px); background: var(--surface);
  border-radius: 14px; box-shadow: var(--shadow); z-index: 60; padding: 20px;
}
.snooze-pop h3 { margin: 0 0 14px; font-size: 16px; }
/* Post-link onboarding checklist modal (reuses the snooze overlay for the dim). */
.onboard-pop {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 460px; max-width: calc(100vw - 32px); max-height: calc(100vh - 48px);
  overflow-y: auto; background: var(--surface); border-radius: 14px;
  box-shadow: var(--shadow); z-index: 60; padding: 22px;
}
.onboard-pop h3 { margin: 0 0 4px; font-size: 16px; }
.onboard-list { margin: 8px 0 0; padding-left: 18px; }
.onboard-list li { font-size: 13px; line-height: 1.5; color: var(--text); margin-bottom: 9px; }
.onboard-list li strong { color: var(--text); }
/* Compact 2-column grid of choices; label on top, small day/time under it. */
.snooze-presets { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 12px; }
.snooze-preset {
  display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
  padding: 8px 11px; border: 1px solid var(--border); border-radius: 9px;
  background: var(--surface); text-align: left; cursor: pointer;
}
.snooze-preset:hover { background: var(--surface-2); border-color: var(--accent); }
.snooze-label { font-weight: 600; font-size: 13px; color: var(--text); }
.snooze-when { font-size: 11px; color: var(--text-mute); }
.snooze-custom { display: flex; }
.snooze-custom-toggle {
  flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 10px 12px; border: 1px solid var(--border); border-radius: 9px;
  background: var(--surface); color: var(--text-soft); font-size: 13px; font-weight: 600; cursor: pointer;
}
.snooze-custom-toggle:hover { background: var(--surface-2); color: var(--text); border-color: var(--accent); }
.snooze-custom input { flex: 1; padding: 10px 11px; border: 1px solid var(--border); border-radius: 9px; font-size: 13px; }

/* ── Settings pane: the main panel IS the surface. Gmail-style category tabs,
   compact label/control rows, full-bleed divider lines edge to edge. ── */
.settings-pane { background: var(--surface); display: flex; flex-direction: column; min-height: 0; }
.settings-toolbar {
  display: flex; align-items: center; gap: 8px; padding: 6px 12px;
  background: var(--surface); border-bottom: 1px solid var(--border); flex-shrink: 0;
}
.settings-title { font-weight: 700; font-size: 15px; }
.settings-tabs {
  display: flex; gap: 2px; padding: 0 12px; background: var(--surface);
  border-bottom: 1px solid var(--border); flex-shrink: 0;
}
.settings-tab-btn {
  border: none; background: transparent; color: var(--text-soft); cursor: pointer;
  font-size: 13px; font-weight: 600; padding: 11px 14px; border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.settings-tab-btn:hover { color: var(--text); }
.settings-tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
.settings-body { flex: 1; overflow-y: auto; padding: 0; }
.settings-row {
  display: flex; gap: 24px; align-items: flex-start;
  padding: 16px 32px; border-bottom: 1px solid var(--border);
}
.settings-row-label { width: 180px; flex-shrink: 0; font-size: 13px; font-weight: 600; padding-top: 2px; }
.settings-row-control { flex: 1; min-width: 0; }
.settings-hint { font-size: 12px; color: var(--text-soft); line-height: 1.5; margin-top: 6px; }
.settings-inline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.settings-input {
  padding: 8px 11px; border: 1px solid var(--border); border-radius: 8px;
  font-size: 13px; background: var(--surface); color: var(--text); min-width: 220px;
}
.settings-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.settings-btn-sm { padding: 7px 13px; font-size: 13px; }
.settings-error { background: #fdecec; color: var(--danger); padding: 8px 11px; border-radius: 8px; font-size: 12px; margin-top: 10px; }
.settings-saved { font-size: 13px; color: var(--accent); font-weight: 600; }
.settings-radio, .settings-check {
  display: flex; align-items: center; gap: 9px; padding: 4px 0; cursor: pointer; font-size: 13px;
}
.settings-radio input, .settings-check input { flex-shrink: 0; accent-color: var(--accent); margin: 0; }
/* Signature editor (Settings → General). Reuses .editor-html so the signature
   is composed in the very same surface (and Arial font) as a real message,
   wrapped in a bordered box with its formatting toolbar underneath. */
.signature-editor-wrap {
  border: 1px solid var(--border); border-radius: 8px; background: var(--surface);
  padding: 8px 11px; max-width: 620px;
}
.signature-editor-wrap:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.editor-html.signature-editor {
  min-height: 92px; max-height: 260px; margin-top: 0;
}
.signature-toolbar { border-top: 1px solid var(--surface-2); margin-top: 8px; padding-top: 6px; }

.rule-actions-pick { display: grid; grid-template-columns: 1fr 1fr; gap: 2px 16px; margin: 8px 0; }

/* A filter is a CARD, not a table row. The old single-line form read
   "from:x@y.com -> Skip inbox . Label", which never named the label and packed
   the condition and its consequences into one dense line (operator, 30 Aug
   2026). The condition and the actions are now separate labelled blocks. */
.rules-list { display: flex; flex-direction: column; gap: 12px; }
.rule-card {
  border: 1px solid var(--border); border-radius: 12px;
  padding: 14px 16px; background: var(--surface);
}
.rule-card-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding-bottom: 12px; margin-bottom: 12px;
  border-bottom: 1px solid var(--surface-2);
}
.rule-card-name { font-size: 13.5px; font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rule-part { display: flex; align-items: flex-start; gap: 14px; }
.rule-part + .rule-part { margin-top: 12px; }
/* Fixed gutter so "When mail matches" and "Then" line up under each other and
   the eye can jump straight down the left edge. */
.rule-part-label {
  width: 150px; flex-shrink: 0; font-size: 11.5px; font-weight: 600;
  letter-spacing: .04em; text-transform: uppercase; color: var(--text-soft);
  padding-top: 3px; white-space: nowrap;
}
.rule-query {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px; background: var(--surface-2); border-radius: 7px;
  padding: 3px 8px; word-break: break-all; line-height: 1.6;
}
.rule-acts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; min-width: 0; }
.rule-act { display: flex; align-items: center; gap: 9px; font-size: 13px; line-height: 1.45; }
.rule-act strong { font-weight: 600; }
.rule-act-icon { width: 15px; height: 15px; flex-shrink: 0; color: var(--text-soft); }
.rule-act .label-dot { flex-shrink: 0; }

.alias-list { display: flex; flex-direction: column; }
.alias-list { display: flex; flex-direction: column; }
.alias-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 9px 0; border-bottom: 1px solid var(--surface-2);
}
.alias-row:first-child { border-top: 1px solid var(--surface-2); }
.alias-identity { display: flex; align-items: center; gap: 11px; min-width: 0; }
.alias-addr { font-size: 13px; display: flex; align-items: center; gap: 8px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Per-address avatar (its own picture/colour) with a small edit badge. */
.alias-avatar { position: relative; width: 34px; height: 34px; flex-shrink: 0; border: none; background: none; padding: 0; cursor: pointer; }
.alias-avatar-face {
  width: 34px; height: 34px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 13px; background-size: cover; background-position: center; color: #fff;
}
.alias-avatar-edit {
  position: absolute; right: -3px; bottom: -3px; width: 16px; height: 16px; border-radius: 50%;
  background: var(--surface); color: var(--text-soft); border: 1px solid var(--border);
  display: inline-flex; align-items: center; justify-content: center;
}
.alias-avatar:hover .alias-avatar-edit { color: var(--accent); }
/* Inline per-address avatar editor. */
.alias-avatar-editor { padding: 10px 0 14px 45px; display: flex; flex-direction: column; gap: 9px; }
.alias-avatar-editor .avatar-swatches { grid-template-columns: repeat(12, 1fr); gap: 5px; max-width: 420px; }
.alias-avatar-editor .avatar-editor-actions { max-width: 420px; }
.alias-badge { font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 5px; flex-shrink: 0; }
.alias-badge.default { background: var(--accent-soft); color: var(--accent); }
.alias-badge.primary { background: var(--surface-2); color: var(--text-soft); }
.alias-badge.pending { background: #fff4e5; color: var(--warn); }
.account-pending-note { padding: 14px 4px 6px; text-align: center; }
.account-pending-note strong { display: block; font-size: 14px; margin-bottom: 6px; }
.account-pending-note p { font-size: 12.5px; color: var(--text-soft); line-height: 1.5; margin: 0 0 12px; }
.alias-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
/* Collapsed-by-default "Refresh Everywhere" activity log under an account. */
.ext-ev-toggle {
  display: inline-flex; align-items: center; gap: 5px; margin-top: 6px;
  padding: 0; border: none; background: none; cursor: pointer;
  font-size: 12px; font-weight: 600; color: var(--text-soft);
}
.ext-ev-toggle:hover { color: var(--accent); }
.ext-ev-caret { transition: transform .15s; flex-shrink: 0; }
.ext-ev-toggle[aria-expanded="true"] .ext-ev-caret { transform: rotate(90deg); }
.ext-ev-log { margin-top: 6px; display: flex; flex-direction: column; gap: 2px; }
.ext-ev-row {
  display: flex; align-items: baseline; gap: 8px;
  font-size: 11.5px; line-height: 1.5; color: var(--text-soft);
}
.ext-ev-when { flex-shrink: 0; min-width: 74px; color: var(--text-mute); }
.ext-ev-src { flex-shrink: 0; min-width: 74px; font-weight: 600; color: var(--text-soft); }
.ext-ev-src.ext-ev-err { color: var(--danger); }
.ext-ev-what { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ext-ev-empty { margin-top: 0; }
.alias-health {
  font-size: 11px; font-weight: 700; padding: 3px 8px; border-radius: 11px; white-space: nowrap;
  cursor: default; display: inline-flex; align-items: center; gap: 5px;
}
.alias-health::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.alias-health.ok { background: #e7f6ec; color: var(--ok); }
.alias-health.issues { background: #fff4e5; color: var(--warn); }
.alias-health.pending { background: #fdecec; color: var(--danger); }
.alias-health.unknown, .alias-health.checking { background: var(--surface-2); color: var(--text-mute); }
.alias-health.clickable { border: none; cursor: pointer; }
.alias-health.clickable:hover { filter: brightness(.96); }
.alias-health-caret { transition: transform .15s; }
.alias-health.open .alias-health-caret { transform: rotate(180deg); }

/* Inline "how to fix" panel under an unhealthy address */
.alias-row-wrap { border-bottom: 1px solid var(--surface-2); }
.alias-row-wrap:first-child { border-top: 1px solid var(--surface-2); }
.alias-row-wrap .alias-row { border-bottom: none; }
.alias-fix {
  background: var(--surface-2); border-radius: 10px; padding: 14px 16px; margin: 2px 0 12px;
  font-size: 13px;
}
.fix-prob { padding: 10px 0; border-bottom: 1px solid var(--border); }
.fix-prob:first-child { padding-top: 0; }
.fix-prob:last-child { border-bottom: none; }
.fix-prob-head { font-weight: 700; margin-bottom: 3px; }
.fix-how { color: var(--text-soft); line-height: 1.5; margin-bottom: 9px; }
.fix-record { display: flex; flex-direction: column; gap: 6px; }
.fix-field { display: flex; align-items: center; gap: 8px; }
.fix-field.fix-value { align-items: flex-start; }
.fix-k { width: 90px; flex-shrink: 0; font-size: 11px; font-weight: 700; color: var(--text-mute); text-transform: uppercase; letter-spacing: .03em; padding-top: 5px; }
.fix-field code {
  flex: 1; background: var(--surface); border: 1px solid var(--border); border-radius: 7px;
  padding: 6px 9px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
  color: var(--text); word-break: break-all; line-height: 1.45;
}
.fix-copy {
  flex-shrink: 0; border: 1px solid var(--border); background: var(--surface); color: var(--text-soft);
  width: 30px; height: 30px; border-radius: 7px; display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
}
.fix-copy:hover { background: var(--surface-2); color: var(--text); }
.fix-copy.copied { color: var(--ok); border-color: var(--ok); }
.fix-note { margin-top: 10px; font-size: 12px; color: var(--text-soft); line-height: 1.5; }
.fix-prob-head { display: flex; align-items: center; gap: 8px; }
.fix-action { font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 5px; text-transform: uppercase; letter-spacing: .03em; }
.fix-action.add { background: #e7f6ec; color: var(--ok); }
.fix-action.replace { background: #fff4e5; color: var(--warn); }
.fix-warning {
  display: flex; gap: 9px; align-items: flex-start; margin: 9px 0;
  background: #fff8ed; border: 1px solid #f4d9a8; border-radius: 9px;
  padding: 10px 12px; font-size: 12px; line-height: 1.5; color: #8a5a00;
}
.fix-warning svg { flex-shrink: 0; margin-top: 1px; color: var(--warn); }
.fix-current {
  margin: 8px 0; font-size: 12px; display: flex; flex-direction: column; gap: 5px;
}
.fix-current-label { color: var(--text-mute); font-weight: 600; }
.fix-current code {
  background: var(--surface); border: 1px dashed var(--border); border-radius: 6px;
  padding: 4px 8px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px;
  color: var(--text-soft); word-break: break-all;
}
.fix-record-label { font-size: 11px; font-weight: 700; color: var(--text-mute); text-transform: uppercase; letter-spacing: .03em; margin: 8px 0 5px; }
.alias-act {
  border: 1px solid var(--border); background: var(--surface); color: var(--text-soft);
  font-size: 12px; font-weight: 600; padding: 4px 9px; border-radius: 6px;
  display: inline-flex; align-items: center; justify-content: center;
}
.alias-act:hover { background: var(--surface-2); color: var(--text); }
.alias-act.danger:hover { color: var(--danger); border-color: var(--danger); }
.settings-add-trigger { display: inline-flex; align-items: center; gap: 7px; margin-top: 12px; font-size: 13px; }
.alias-add-form { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; align-items: center; }
.alias-add-form .settings-input { flex: 1; min-width: 180px; }
.alias-add-actions { display: flex; gap: 8px; }

/* ── Custom tooltip (never native title=). Rendered on document.body by JS so
   it ALWAYS sits above everything and can never be clipped by a container's
   overflow:hidden. Very high z-index per the global rule.

   Being on <body> only prevents clipping by an ancestor's overflow — it does
   nothing about the SCREEN edge, so the JS also CLAMPS it inside the viewport.

   Position comes ENTIRELY from the JS transform: the element stays at
   top/left 0 and is moved with translate(). Setting `left` instead would cap
   the box's available width at (viewport - left), making it shrink-wrap
   against the right edge before any transform runs — a right-anchored tooltip
   collapsed from 340px to 178px that way. Anchored at 0, max-width alone
   decides the width.

   max-width + wrapping is the other half of the guarantee: with a hard nowrap
   a long sentence can exceed a phone screen, and no clamp can fit it. ── */
.app-tooltip {
  position: fixed; top: 0; left: 0; z-index: 99999; pointer-events: none;
  background: #2b2f36; color: #fff; font-size: 12px; font-weight: 500; line-height: 1.35;
  padding: 6px 9px; border-radius: 6px;
  max-width: min(340px, calc(100vw - 16px));
  box-shadow: 0 2px 8px rgba(20,30,60,.25);
  opacity: 0; transition: opacity .1s;
  /* Honour newlines in the tooltip text. A tooltip that answers "which agent
     sent this?" carries labelled lines (Workspace: …, Chat session: …), and
     collapsing them into one paragraph turns the answer back into a wall the
     reader has to parse. Wrapping still applies, so long sentences behave. */
  white-space: pre-wrap;
}
.app-tooltip.show { opacity: 1; }

/* ── Responsive ── */
@media (max-width: 1000px) {
  .mail-layout { grid-template-columns: 64px 1fr; }
  .mail-sidebar .folder-name, .mail-sidebar .compose-btn span { display: none; }
}
@media (max-width: 640px) {
  .menu-toggle { display: inline-flex; }
  .mail-layout { grid-template-columns: 1fr; }
  .mail-sidebar {
    position: fixed; left: 0; top: var(--topbar-h); bottom: 0; width: 260px; z-index: 30;
    transform: translateX(-100%); transition: transform .2s; border-right: 1px solid var(--border);
  }
  .mail-sidebar.open { transform: translateX(0); }
  .mail-sidebar .folder-name, .mail-sidebar .compose-btn span { display: inline; }
  /* Keep the topbar uncluttered on phones: health badge shows just its icon. */
  .health-badge span { display: none; }
  .health-badge { padding: 8px; }
  /* On a phone the address is information and the wordmark is decoration, so
     the brand gives up its room to the caption rather than the reverse. */
  .topbar-brand { display: none; }
  .topbar-account-addr { max-width: none; font-size: 11.5px; padding: 5px 9px; }

  /* Message header on a phone: put the DATE on its own line under the sender.
     On one row the header is avatar 40 + date ~160 + actions 62 + two 14px gaps
     = ~304px of a 326px column, leaving the sender ~14px, so the address wrapped
     one letter per line (word-break:break-all on .msg-email) into a 400px-tall
     vertical column. The date is white-space:nowrap and cannot shrink, so the
     sender is always the one that loses. Wrapping lets .msg-meta take the full
     width and the date sit beneath it, which is what Gmail does at this size. */
  .msg-head { flex-wrap: wrap; }
  /* Line 1 is AVATAR + SENDER (they belong together — the avatar identifies
     the name beside it). Line 2 is the date and the actions. The earlier
     version gave .msg-meta `flex: 1 1 100%`, which forced it onto its own row
     and left the avatar alone on line 1 with 350px of empty space next to it:
     a whole wasted line at the top of every message. `1 1 0` lets it share. */
  /* THREE bands, and the order of the flex properties matters more than it
     looks. Band 1 is avatar + sender/address; band 2 is the date and the
     per-message actions, side by side.
       .msg-meta   `1 1 0`   — shares band 1 with the avatar. With `1 1 100%`
                              it took a whole row of its own and left the
                              avatar alone next to 350px of nothing.
       .msg-date   `1 1 100%` — FORCES the wrap to band 2. With `1 1 0` it fits
                              beside the sender instead, which squeezes the
                              address into a 4-character column that wraps
                              "<qa-webmail@devv.be>" over three lines.
       .msg-actions          — follows the date on band 2, pushed right.
     Both failures above were seen on real 390px screenshots before landing
     here; do not "simplify" these three values. */
  .msg-avatar { order: 1; flex: 0 0 auto; }
  .msg-meta { flex: 1 1 0; order: 2; min-width: 0; }
  .msg-date { order: 4; flex: 0 1 auto; min-width: 0; }
  .msg-actions { order: 5; flex: 0 0 auto; margin-left: auto; }
  /* The wrap is forced by a zero-height, full-width spacer at order 3 — between
     the meta (2) and the date (4) — rather than by the date's own flex-basis.
     That way the date keeps its natural width on band 2 and the actions sit
     next to it instead of being pushed onto a third row. Only the EXPANDED
     header wraps; the collapsed one is nowrap and must not grow a spacer. */
  .msg.expanded .msg-head::before {
    content: ""; order: 3; flex: 1 0 100%; height: 0; margin: 0;
  }
  /* The address can still be longer than the phone is wide; break it at any
     character ONLY as a last resort, after normal wrapping has been tried. */
  .msg-email { overflow-wrap: anywhere; word-break: normal; }
  /* The collapsed row stays single-line (its date already sits right via
     margin-left:auto); only the EXPANDED header re-flows.
     The `order` values above are set on .msg-avatar, which is present in the
     collapsed head too — so without an explicit order on the collapsed meta
     block (default 0) the AVATAR sorted after it and jumped to the right of
     the sender. Seen on a real 390px screenshot. */
  .msg.collapsed .msg-head { flex-wrap: nowrap; }
  .msg.collapsed .msg-collapsed-meta { order: 2; }
  .msg.collapsed .msg-date { padding-left: 0; flex: 0 0 auto; }
}

/* ════════════════════════════════════════════════════════════════════
 * APPEARANCE — frosted-glass theme (opt-in, per account) — "v02" look
 * Restored from the theme-lab v02 iteration the user chose. Everything here is
 * GATED under body.theme-glass so the plain default UI is 100% untouched when
 * the theme is off. The coloured radial-mesh backdrop lives on #bg-layer and
 * MOVES via transform (drift/blobs/zoom) or background-position (wave) or a hue
 * filter (hue). JS writes the frosted-glass values as --glass-* custom
 * properties + sets data-bg / data-anim on <body>.
 * ════════════════════════════════════════════════════════════════════ */

/* The backdrop layer is inert + hidden until the glass theme is switched on.
   inset:-35% gives the transform-animated mesh room so it always covers. It is
   position:FIXED (not absolute): a body-level absolute child at negative inset
   inflates the body scrollWidth, and body overflow propagates to the viewport,
   so it leaks a horizontal scrollbar. A fixed layer is out of every scroll box
   yet looks identical here (the app is a non-scrolling 100vh shell). */
.bg-layer { display: none; }
body.theme-glass .bg-layer { display: block; position: fixed; inset: -35%; z-index: 0; }

/* Lift the real UI above the backdrop. The topbar must stay ABOVE the layout
   (like the default z-index:20) so its account-menu popup isn't painted over by
   the later-in-DOM .mail-layout, which also creates a stacking context here. */
body.theme-glass .mail-layout { position: relative; z-index: 1; }
body.theme-glass .topbar { position: relative; z-index: 20; }

/* Make the shell + its opaque-white inner panels transparent so the backdrop
   shows through and only the frosted panels read as glass. */
body.theme-glass.mail-app,
body.theme-glass .topbar,
body.theme-glass .mail-layout,
body.theme-glass .mail-sidebar { background: transparent; }
body.theme-glass .thread-list-pane,
body.theme-glass .reading-pane,
body.theme-glass .settings-pane,
body.theme-glass .conversation,
body.theme-glass .conv-toolbar,
body.theme-glass .msg,
body.theme-glass .settings-toolbar,
body.theme-glass .settings-tabs,
body.theme-glass .inbox-section-head { background: transparent; }
/* Under the glass theme, an unread row must look EXACTLY like a read one at rest
   (fully transparent — no brighter fill); the bold text alone signals unread.
   The brightening happens only on hover (below), same as a read row. */
body.theme-glass .thread-row.unread { background: transparent; }
/* Hover states that use a solid --surface-2 fill read as an opaque white bar on
   the glass; use a soft translucent tint instead so the frost shows through. */
body.theme-glass .msg.collapsed:hover .msg-head,
body.theme-glass .thread-row:hover,
body.theme-glass .thread-row.unread:hover { background: rgba(255, 255, 255, .28); }
/* Drop the inset/3D hover shadow under the theme (read AND unread rows) so the
   highlight is a flat translucent tint, identical for both. */
body.theme-glass .thread-row:hover,
body.theme-glass .thread-row.unread:hover { box-shadow: none; }
/* Under the glass theme the fade uses a white tint (instead of --row-hover) so it
   blends with the frost. Position/padding inherited from the base rule. */
body.theme-glass .thread-row:hover .thread-actions,
body.theme-glass .thread-row.unread:hover .thread-actions {
  background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0) 12%, rgba(255,255,255,.55) 78%, rgba(255,255,255,.6) 100%);
}

/* Transparency is an INBOX-only choice. When a conversation is open (the reading
   pane is visible) the main panel goes back to a solid white surface so email
   content is always read on white, regardless of the chosen glass settings. The
   coloured background still shows around the panel; only the panel is opaque. */
body.theme-glass .main-pane:has(#reading-pane:not([hidden])) {
  background: var(--surface);
}
body.theme-glass .main-pane:has(#reading-pane:not([hidden])) .conversation,
body.theme-glass .main-pane:has(#reading-pane:not([hidden])) .conv-toolbar,
body.theme-glass .main-pane:has(#reading-pane:not([hidden])) .msg { background: var(--surface); }

/* The main panel is the primary uniform sheet of frosted glass.
   NO backdrop-filter here (or anywhere): live-blurring panels over an animated
   backdrop made Chromium drop the filtered surfaces for one frame on every
   layer-tree change — the whole UI blinked ~16ms, ~10x/30s (2026-07-18 bug).
   REMOVED — do not restore backdrop-filter without explicit user request.
   The "frost" now comes from the translucent hsla fill + a single cheap blur
   applied to #bg-layer itself (see below). */
body.theme-glass .main-pane {
  background: var(--glass-bg);
  box-shadow: var(--glass-shadow);
  border: 1px solid var(--glass-border);
}
/* Compose button + the active folder pill are glass-matched chrome. */
body.theme-glass .compose-btn,
body.theme-glass .folder.active {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
}
/* The search bar is a lighter frost (0.7x opacity). */
body.theme-glass .topbar-search {
  background: var(--glass-bg-soft);
}

/* The Blur knob now softens the backdrop mesh itself: one filter on one layer,
   rasterized once per animation cycle instead of re-filtered behind every panel
   every frame. --bg-blur is written by applyTheme() from the Blur slider. */
body.theme-glass .bg-layer { filter: blur(var(--bg-blur, 0px)); }

/* MEMORY / CPU: the mesh is a blurred layer stretched to 170% of the viewport
   (inset: -35%) and animated forever. A blurred, animated layer is held as
   compositor textures and re-rasterized as it moves, and none of that is worth
   anything while the tab is in the background — where a mail client spends most
   of its life.
   `animation-play-state: paused` freezes it without unsetting the animation, so
   the mesh resumes at the same point when the tab comes back and the user sees
   no reset. Set via a class the app toggles on visibilitychange, because the
   animation shorthand itself is written inline by applyTheme() and a CSS rule
   must not fight it. */
body.theme-glass.bg-idle .bg-layer { animation-play-state: paused !important; }

/* ── v02 backgrounds: soft diffuse radial meshes ── */
body[data-bg="aurora"] .bg-layer{background:radial-gradient(at 16% 18%,#9ec5fd 0,transparent 42%),radial-gradient(at 82% 14%,#c9b6fd 0,transparent 42%),radial-gradient(at 68% 82%,#a7b4fc 0,transparent 48%),radial-gradient(at 22% 86%,#bfe0ff 0,transparent 44%),#e9f0ff;}
body[data-bg="sunrise"] .bg-layer{background:radial-gradient(at 12% 16%,#a9c6ff 0,transparent 44%),radial-gradient(at 86% 20%,#ffc6a3 0,transparent 42%),radial-gradient(at 78% 84%,#f7a6c8 0,transparent 46%),radial-gradient(at 20% 86%,#c9b6fd 0,transparent 46%),#eef1ff;}
body[data-bg="sunset"] .bg-layer{background:radial-gradient(at 18% 20%,#ffb38a 0,transparent 44%),radial-gradient(at 82% 16%,#ff7eb3 0,transparent 42%),radial-gradient(at 74% 84%,#b98bff 0,transparent 48%),radial-gradient(at 18% 86%,#ffd6a3 0,transparent 44%),#ffeede;}
body[data-bg="candy"] .bg-layer{background:radial-gradient(at 18% 22%,#ffc6e6 0,transparent 44%),radial-gradient(at 84% 16%,#b9d4ff 0,transparent 44%),radial-gradient(at 72% 82%,#d7c0ff 0,transparent 48%),radial-gradient(at 16% 84%,#ffd9c2 0,transparent 44%),#fbf0fb;}
body[data-bg="spectrum"] .bg-layer{background:conic-gradient(from 210deg at 50% 40%,#bfe0ff,#cdbcff,#ffc6e6,#ffe2b8,#c6f3da,#bfe0ff);}
body[data-bg="ocean"] .bg-layer{background:radial-gradient(at 16% 18%,#a7f0e4 0,transparent 44%),radial-gradient(at 84% 16%,#a3d3ff 0,transparent 44%),radial-gradient(at 76% 84%,#7fd1f7 0,transparent 48%),radial-gradient(at 18% 86%,#c2f5ea 0,transparent 44%),#e6f8fb;}
body[data-bg="meadow"] .bg-layer{background:radial-gradient(at 14% 18%,#bdf0c8 0,transparent 44%),radial-gradient(at 86% 20%,#a8e0ff 0,transparent 42%),radial-gradient(at 78% 84%,#c8eeb0 0,transparent 48%),radial-gradient(at 20% 84%,#d5f5e0 0,transparent 44%),#eefaf0;}
body[data-bg="dusk"] .bg-layer{background:radial-gradient(at 14% 16%,#7fa8ff 0,transparent 44%),radial-gradient(at 86% 18%,#b08bff 0,transparent 42%),radial-gradient(at 80% 86%,#ff9bbf 0,transparent 46%),radial-gradient(at 18% 88%,#9ec0ff 0,transparent 46%),#dde6ff;}
/* Wave needs a sized linear gradient instead of the radial mesh. */
body[data-anim="wave"] .bg-layer{background:linear-gradient(120deg,#a9c6ff,#cdbcff,#ffc6e6,#bfe0ff,#c6f3da,#a9c6ff)!important;background-size:340% 340%!important;}

/* ── v02 keyframes (transform / position / filter based) ── */
@keyframes drift{0%{transform:translate(0,0) scale(1);}50%{transform:translate(6%,-5%) scale(1.16);}100%{transform:translate(-6%,4%) scale(1.08);}}
@keyframes blobs{0%{transform:translate(0,0) rotate(0deg);}50%{transform:translate(-8%,6%) rotate(8deg);}100%{transform:translate(7%,-5%) rotate(-7deg);}}
@keyframes wave{0%{background-position:0% 50%;}50%{background-position:100% 50%;}100%{background-position:0% 50%;}}
@keyframes hue{from{filter:blur(var(--bg-blur,0px)) hue-rotate(0deg);}to{filter:blur(var(--bg-blur,0px)) hue-rotate(360deg);}}
@keyframes zoom{0%{transform:scale(1) translate(0,0);}100%{transform:scale(1.22) translate(-3%,2%);}}

/* ── Appearance tab controls (swatches, sliders, presets) ── */
.theme-swatch-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: thin; scroll-snap-type: x proximity; }
.theme-swatch-row::-webkit-scrollbar { height: 6px; }
.theme-swatch-row::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.theme-swatch {
  position: relative; flex: 0 0 auto; width: 46px; height: 46px; border-radius: 10px;
  border: 2px solid transparent; cursor: pointer; overflow: hidden; padding: 0;
  background-clip: padding-box; scroll-snap-align: start;
}
.theme-swatch:hover { border-color: var(--border); }
.theme-swatch.sel { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.theme-swatch .lbl {
  position: absolute; left: 0; right: 0; bottom: 0; font-size: 8px; font-weight: 700; color: #fff;
  background: rgba(0,0,0,.42); padding: 1px 2px; text-align: center; line-height: 1.3;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.theme-swatch-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.theme-anim-sw {
  display: inline-flex; align-items: center; gap: 7px; padding: 8px 12px; border-radius: 9px;
  border: 1px solid var(--border); background: var(--surface); color: var(--text-soft);
  font-size: 12px; font-weight: 600; cursor: pointer;
}
.theme-anim-sw:hover { background: var(--surface-2); color: var(--text); }
.theme-anim-sw.sel { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.theme-anim-sw svg { width: 16px; height: 16px; }
.theme-slider-row { display: flex; flex-direction: column; gap: 3px; max-width: 360px; margin-top: 10px; }
.theme-slider-label { display: flex; justify-content: space-between; align-items: center; font-size: 12px; font-weight: 600; color: var(--text-soft); }
.theme-slider-val { font-variant-numeric: tabular-nums; color: var(--text-mute); font-weight: 500; }
.theme-slider-row input[type=range] { width: 100%; accent-color: var(--accent); }
.theme-preset-row { display: flex; flex-wrap: wrap; gap: 8px; }
.theme-preset-sw {
  padding: 7px 14px; border-radius: 8px; border: 1px solid var(--border);
  background: var(--surface); color: var(--text); font-size: 12px; font-weight: 600; cursor: pointer;
}
.theme-preset-sw:hover { background: var(--surface-2); border-color: var(--accent); }
.theme-preset-actions { margin-top: 12px; }

/* ── Tab-icon editor (Appearance tab) ──
   Two columns: a live preview on the left (large, plus the icon at its TRUE
   16px inside a mock tab, because 16px is the size that actually decides it),
   and the colour controls on the right. Wraps to one column when narrow. */
.favicon-editor { display: flex; gap: 26px; flex-wrap: wrap; align-items: flex-start; }
/* Caps at 210px on desktop but is free to shrink on a phone, where a fixed
   width pushed the mock tab past the edge of the pane. */
.favicon-preview { display: flex; flex-direction: column; gap: 10px; flex: 1 1 210px; min-width: 0; max-width: 210px; }
#favicon-prev-big { width: 84px; height: 84px; border-radius: 18px; }
.favicon-prev-strip { background: var(--surface-2); border-radius: 9px 9px 0 0; padding: 7px 7px 0; }
.favicon-prev-tab {
  display: flex; align-items: center; gap: 7px; background: var(--surface);
  border-radius: 7px 7px 0 0; padding: 7px 10px;
}
.favicon-prev-tab canvas { width: 16px; height: 16px; flex: none; }
.favicon-prev-tab b {
  font-weight: 400; font-size: 11.5px; color: var(--text-soft);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.favicon-prev-note { font-variant-numeric: tabular-nums; }
.favicon-prev-note.weak { color: var(--warn); }

/* min-width:260px would keep the column WIDER than its parent on a phone, so
   the presets and the envelope segment spilled outside the settings row.
   min-width:0 lets it shrink; the rows below wrap instead of overflowing. */
.favicon-controls { flex: 1 1 260px; min-width: 0; display: flex; flex-direction: column; gap: 14px; }
.favicon-presets { display: flex; flex-wrap: wrap; gap: 8px; }
.favicon-preset {
  width: 34px; height: 34px; border-radius: 9px; padding: 0;
  border: 2px solid transparent; box-shadow: 0 0 0 1px var(--border) inset; cursor: pointer;
}
.favicon-preset:hover { transform: translateY(-1px); }
.favicon-preset.sel { border-color: var(--accent); box-shadow: none; }

/* The plate's own heading line, carrying the follow/snap action. Solid rather
   than ghost: as a ghost button on a busy panel it was missed entirely. */
.favicon-bg-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.favicon-followbtn { font-weight: 600; }

.favicon-slider-row { display: flex; flex-direction: column; gap: 4px; }
/* margin:0 matters: a range input carries a default horizontal margin, so
   width:100% overflowed its column by 2px on a phone. */
.favicon-slider-row input[type=range] { width: 100%; margin: 0; accent-color: var(--accent); }

.favicon-custom-row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.favicon-colour { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 600; color: var(--text-soft); }
.favicon-colour input[type=color] {
  width: 42px; height: 28px; padding: 0; cursor: pointer;
  border: 1px solid var(--border); border-radius: 7px; background: none;
}
.favicon-solid { font-size: 12px; }

.favicon-fg-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.favicon-fg-label { font-size: 12px; font-weight: 600; color: var(--text-soft); }
.favicon-seg { display: inline-flex; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.favicon-seg button {
  font: inherit; font-size: 12px; font-weight: 500; padding: 6px 13px;
  border: 0; background: var(--surface); color: var(--text-soft); cursor: pointer;
}
.favicon-seg { flex-wrap: wrap; }
.favicon-seg button + button { border-left: 1px solid var(--border); }

/* On a phone the settings row's fixed 180px label leaves ~120px for the
   control, which is not enough for a swatch grid plus a colour picker. Stack
   the label above the editor for THIS row only, so the controls get the full
   width without disturbing every other settings row. */
@media (max-width: 640px) {
  .settings-row:has(.favicon-editor) { flex-direction: column; gap: 12px; }
  .settings-row:has(.favicon-editor) .settings-row-label { width: auto; }
  .favicon-preview { max-width: none; }
}
.favicon-seg button:hover { background: var(--surface-2); }
.favicon-seg button.on { background: var(--accent); color: #fff; }

/* Appearance Save bar (draft/save model). Sticks to the bottom of the tab so
   Save is always reachable while scrolling the controls. */
.appearance-savebar {
  position: sticky; bottom: 0; z-index: 3;
  display: flex; align-items: center; justify-content: flex-end; gap: 12px;
  padding: 14px 32px; margin-top: 4px;
  background: var(--surface); border-top: 1px solid var(--border);
}
.appearance-dirty-hint { margin-right: auto; color: var(--warn); }

/* Send-failure alert dialog: blocking, centered, impossible to miss. */
.sendfail-overlay {
  position: fixed; inset: 0; z-index: 140; background: rgba(20,30,60,.38);
}
.sendfail-dialog {
  position: fixed; z-index: 141; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: calc(100% - 40px); max-width: 460px;
  background: var(--surface); border-radius: 14px; box-shadow: 0 18px 50px rgba(20,30,60,.32);
  padding: 22px 24px; border-top: 4px solid var(--danger);
}
.sendfail-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; color: var(--danger); }
.sendfail-head h3 { margin: 0; font-size: 17px; color: var(--text); }
.sendfail-reason {
  margin: 0 0 12px; font-size: 13.5px; line-height: 1.5; color: var(--danger);
  background: rgba(217,48,37,.07); border-radius: 8px; padding: 10px 12px;
  word-break: break-word;
}
.sendfail-note { margin: 0 0 20px; font-size: 13.5px; color: var(--text-soft); line-height: 1.5; }
.sendfail-actions { display: flex; justify-content: flex-end; }

/* Unsaved-changes dialog (leaving the Appearance tab with a dirty draft). */
.theme-unsaved-overlay {
  position: fixed; inset: 0; z-index: 120; background: rgba(20,30,60,.32);
}
.theme-unsaved-dialog {
  position: fixed; z-index: 121; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: calc(100% - 40px); max-width: 420px;
  background: var(--surface); border-radius: 14px; box-shadow: 0 18px 50px rgba(20,30,60,.28);
  padding: 22px 24px;
}
.theme-unsaved-dialog h3 { margin: 0 0 8px; font-size: 17px; }
.theme-unsaved-dialog p { margin: 0 0 20px; font-size: 13.5px; color: var(--text-soft); line-height: 1.5; }
.theme-unsaved-actions { display: flex; justify-content: flex-end; gap: 8px; }
.theme-unsaved-discard:hover { color: var(--danger); border-color: var(--danger); }

/* ── Phishing warning banner + defanged links (anti-phishing) ── */
.phish-banner {
  border-radius: 10px;
  padding: 12px 14px;
  margin: 0 0 14px;
  font-size: 13.5px;
  line-height: 1.5;
  border: 1px solid;
}
.phish-banner.phish-phishing {
  background: #fef2f2;
  border-color: #fecaca;
  color: #7f1d1d;
}
.phish-banner.phish-suspicious {
  background: #fffbeb;
  border-color: #fde68a;
  color: #78350f;
}
.phish-head {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 700;
  font-size: 14px;
}
.phish-head svg { flex-shrink: 0; }
.phish-body { margin-top: 4px; }
.phish-reasons {
  margin: 8px 0 0;
  padding-left: 18px;
}
.phish-reasons li { margin: 2px 0; }

/* Shape-only notice: one quiet line that opens on demand. Nothing here is an
   accusation against the sender, so it must not outweigh the message it sits
   above (it was 239px tall over a newsletter before this). */
.phish-banner.phish-compact {
  padding: 7px 12px;
  margin: 0 0 10px;
  font-size: 12.5px;
  background: #fcfbf7;
  border-color: #ece4cf;
  color: #6b5b3e;
}
.phish-compact > .phish-head {
  font-weight: 600;
  font-size: 12.5px;
  cursor: pointer;
  list-style: none;
  gap: 7px;
}
/* Kill the native disclosure triangle in both engines. */
.phish-compact > .phish-head::-webkit-details-marker { display: none; }
.phish-compact > .phish-head::marker { content: ""; }
.phish-compact .phish-head svg { width: 15px; height: 15px; opacity: 0.75; }
/* Our own caret, so the row still reads as openable. */
.phish-compact > .phish-head::after {
  content: "";
  margin-left: auto;
  width: 7px;
  height: 7px;
  border-right: 1.6px solid currentColor;
  border-bottom: 1.6px solid currentColor;
  opacity: 0.55;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 0.15s ease;
}
.phish-compact[open] > .phish-head::after {
  transform: rotate(-135deg) translate(-2px, -2px);
}
.phish-compact .phish-body { margin-top: 7px; }
.phish-compact:hover { border-color: #ddd0b0; }

/* Inert, non-clickable replacement for a risky link. Looks like a warning chip;
   clicking it only reveals the real URL for manual copy (never navigates). */
.phish-link {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  max-width: 100%;
  padding: 1px 7px;
  border-radius: 6px;
  background: #fef2f2;
  border: 1px dashed #f0a5a5;
  color: #b91c1c;
  font-weight: 600;
  cursor: pointer;
  text-decoration: line-through;
  vertical-align: baseline;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.phish-link:hover { background: #fee2e2; }

/* ── One-click unsubscribe button (RFC 2369 / RFC 8058) ──
   Lives on the conversation SUBJECT LINE, right-aligned — client chrome, not
   part of the sender's email. It was originally a wide banner at the top of the
   message body, but there it read as the sender's own content and was easy to
   skim past, which defeated the nudge (user-decided 2026-08-02).
   One button, always unsubscribe-AND-archive. */
/* Pastel orange with a soft peach-to-apricot fade (user-picked 2026-08-02).
   The deep brown-orange text on a pale ground keeps AA contrast, which a white
   label on a pastel fill would not — a pastel button has to carry its contrast
   in the TEXT, not the background. */
.conv-unsub {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
  margin-left: auto; /* stays pinned to the right now that the title no longer grows */
  border: 1px solid #f6c99a;
  border-radius: 999px;
  padding: 6px 14px;
  font-size: 13px;
  font-weight: 700;
  line-height: 1.2;
  background: linear-gradient(135deg, #fef1e2 0%, #fde0c0 55%, #fbd0a6 100%);
  color: #9a4a09;
  white-space: nowrap;
  box-shadow: 0 1px 2px rgba(180, 100, 30, .10);
  transition: filter .15s ease, box-shadow .15s ease, opacity .15s ease;
}
.conv-unsub:hover {
  filter: brightness(.97) saturate(1.08);
  box-shadow: 0 2px 6px rgba(180, 100, 30, .16);
}
.conv-unsub:active { filter: brightness(.94) saturate(1.1); box-shadow: none; }
.conv-unsub:disabled { opacity: .65; cursor: default; filter: none; box-shadow: none; }
.conv-unsub svg { flex-shrink: 0; }
/* Manual tier: the sender's header is not DKIM-covered (or is mailto-only), so
   the user completes it on the sender's own page. Same pastel family, but flat
   and outlined rather than filled — a quieter weight for the tier where WE are
   not the ones completing the action. */
.conv-unsub.conv-unsub-manual {
  background: #fffaf3;
  border-color: #f0cfae;
  color: #a8630f;
  box-shadow: none;
}
.conv-unsub.conv-unsub-manual:hover { background: #fef3e6; filter: none; box-shadow: 0 1px 3px rgba(180, 100, 30, .12); }
/* Post-unsubscribe confirmation (only seen when the thread was NOT archived). */
.conv-unsub.conv-unsub-done {
  background: var(--surface-2);
  border-color: var(--border);
  color: var(--text-soft);
  cursor: default;
  box-shadow: none;
}

/* Narrow screens: keep the button but drop its label to the icon only, so a
   long subject keeps the width it needs on a phone. */
@media (max-width: 560px) {
  .conv-unsub { padding: 7px 10px; gap: 0; }
  .conv-unsub span { display: none; }
}


/* ════════════════════════════════════════════════════════════════════
   Read/unread section-move transitions — TWO-PHASE (flair + burst).
   Final style ("fast ghost, read only", chosen 2026-07-19; the 14-variant
   picker widget was REMOVED — do not restore without explicit request).
   Phase 1 (here): marking READ fades the leaving row to 25% in 2s and
   HOLDS it (fill-mode both); marking UNREAD has no fade. Resumed
   mid-frame after any re-render or full page refresh via an inline
   negative animation-delay.
   Phase 2 (JS motion engine): when the pointer steps outside the list,
   every waiting row collapses simultaneously (clip-path + translateY,
   sub-pixel smooth), then after a quiet beat the arriving twins grow.
   .rm-burst kills the CSS animation so the engine's inline opacity
   drives the final fade.
   ════════════════════════════════════════════════════════════════════ */
.thread-row.rm-leave, .thread-row.rm-arrive {
  animation-fill-mode: both;
  position: relative;
  will-change: transform, opacity, clip-path;
}
/* The arriving twin is an invisible height placeholder until its burst; the
   clickable original still lives in the source section. */
.thread-row.rm-arrive { pointer-events: none; opacity: 0; }
/* During the burst the JS engine owns opacity — silence the flair. */
.thread-row.rm-burst { animation: none !important; }
@media (prefers-reduced-motion: reduce) {
  .thread-row.rm-leave, .thread-row.rm-arrive { animation: none !important; }
}

/* Ghost flair: 2s fade to 25%, ONLY when marking read (data-rmstamp="Read");
   marking unread keeps full opacity and just waits for the burst. */
@keyframes rm-ghost { 0% { opacity: 1; } 100% { opacity: .25; } }
.thread-row.rm-leave[data-rmstamp="Read"] { animation: rm-ghost 2s linear both; }

/* Fresh-arrival flash: a NEW message grows in over a peachy pink-orange wash
   that holds warm, then melts to the normal row background — unmistakably
   "new mail", not a quiet read-state move. Painted on a ::before layer (the
   row's own background is owned by hover/checked/selected rules) and resumed
   mid-frame after any re-render via the inline --na-delay. */
.thread-row.na-fresh { position: relative; }
.thread-row.na-fresh::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(90deg, #ffe3d3, #ffd9cf 45%, #ffe8d9);
  animation: na-fresh-fade 4s ease-out both;
  animation-delay: var(--na-delay, 0ms);
}
/* Row content sits above the wash. */
.thread-row.na-fresh > * { position: relative; z-index: 1; }
@keyframes na-fresh-fade {
  0% { opacity: 1; }
  55% { opacity: 1; }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .thread-row.na-fresh::before { animation: none; opacity: 0; }
}

/* ── Translation widget (user-picked design, 2026-08-07):
   left-aligned segmented pill [Original | Français | ⋮] above the body, and
   an animated full-height stripe marking the TRANSLATED body so it can never
   be mistaken for the original. ── */
.tr-seg {
  display: inline-flex;
  align-items: center;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 3px;
  margin: 0 0 12px;
}
.tr-seg-btn {
  font: inherit;
  border: none;
  background: none;
  border-radius: 15px;
  padding: 5px 14px;
  font-size: 12.5px;
  color: var(--text-soft);
  font-weight: 500;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.tr-seg-btn.on {
  background: #1a56db;
  color: #fff;
  font-weight: 700;
  box-shadow: 0 1px 4px rgba(26, 86, 219, 0.4);
}
.tr-seg-btn .tr-globe { display: inline-flex; }
.tr-seg-btn .tr-globe svg, .tr-seg-btn svg { width: 13px; height: 13px; }
.tr-seg-kebab {
  font: inherit;
  border: none;
  background: none;
  padding: 5px 9px;
  color: var(--text-soft);
  border-left: 1px solid var(--border);
  border-radius: 0 15px 15px 0;
  margin-left: 2px;
  cursor: pointer;
  display: inline-flex;
}
.tr-seg-kebab:hover { color: #1a56db; }
/* Translated body marker: the stripe draws itself in when the translation
   lands, then stays for as long as the translated view is shown. */
.tr-translated {
  position: relative;
  padding-left: 18px;
}
.tr-translated::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 3px;
  height: 0;
  background: #1a56db;
  border-radius: 2px;
  animation: tr-draw 0.6s ease forwards;
}
@keyframes tr-draw { to { height: 100%; } }
@media (prefers-reduced-motion: reduce) {
  .tr-translated::before { animation: none; height: 100%; }
}
.tr-spinner {
  width: 13px;
  height: 13px;
  border: 2px solid #c4d2e8;
  border-top-color: #1a56db;
  border-radius: 50%;
  animation: tr-spin 0.8s linear infinite;
  flex-shrink: 0;
  display: inline-block;
}
@keyframes tr-spin { to { transform: rotate(360deg); } }
/* Dedicated translation-options popover (matches the round-3 demo panel:
   rounded card, section heads, real toggle switch, settings footer). */
.tr-pop {
  position: fixed;
  z-index: 1200;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 10px 38px rgba(20, 30, 60, 0.18);
  min-width: 270px;
  max-width: 330px;
  padding: 8px;
  animation: tr-pop-in 0.18s ease;
}
@keyframes tr-pop-in { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: none; } }
.tr-pop-head {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text-mute);
  padding: 8px 12px 4px;
}
.tr-pop-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  border: none;
  background: none;
  text-align: left;
  padding: 9px 12px;
  border-radius: 9px;
  font-size: 13.5px;
  color: var(--text);
  cursor: pointer;
  font-family: inherit;
}
.tr-pop-item:hover { background: var(--surface-2); }
.tr-pop-item .tr-pop-check { margin-left: auto; color: #1a56db; font-weight: 700; }
.tr-pop-sep { height: 1px; background: var(--border); margin: 6px 4px; }
.tr-pop-toggle {
  margin-left: auto;
  width: 34px;
  height: 20px;
  border-radius: 10px;
  background: #cfd6e1;
  position: relative;
  transition: background 0.2s;
  flex-shrink: 0;
}
.tr-pop-toggle::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #fff;
  transition: left 0.2s;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}
.tr-pop-toggle.on { background: #1a56db; }
.tr-pop-toggle.on::after { left: 16px; }
.tr-pop-foot {
  font-size: 11.5px;
  color: var(--text-mute);
  padding: 6px 12px;
}
.tr-pop-foot a { color: #1a56db; text-decoration: none; font-weight: 600; cursor: pointer; }

/* Settings → Translation */
.tr-lang-checks { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 2px 14px; }
.settings-select {
  font: inherit;
  padding: 7px 10px;
  border: 1px solid #d4dae3;
  border-radius: 8px;
  background: #fff;
  color: inherit;
  min-width: 200px;
}
.tr-auto-list { display: flex; flex-wrap: wrap; gap: 8px; }
.tr-auto-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: #eef2f8;
  border: 1px solid #d9e1ec;
  border-radius: 16px;
  padding: 4px 6px 4px 12px;
  font-size: 13px;
}
.tr-auto-remove {
  border: none;
  background: none;
  cursor: pointer;
  font-size: 15px;
  line-height: 1;
  color: #6a7280;
  padding: 2px 6px;
  border-radius: 50%;
}
.tr-auto-remove:hover { background: #dde4ee; color: #1c2430; }

/* ── Draft history panel ──────────────────────────────────────────────────
   Reads the append-only revision trail for one draft. Every version ever saved
   is listed with its author, so an overwrite can always be undone. */
.draft-history-pop {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 620px; max-width: calc(100vw - 32px); max-height: calc(100vh - 64px);
  display: flex; flex-direction: column;
  background: var(--surface); border-radius: 14px; box-shadow: var(--shadow);
  z-index: 60; padding: 22px;
}
.draft-history-pop h3 { margin: 0 0 4px; font-size: 16px; }
.dh-list { overflow-y: auto; flex: 1 1 auto; min-height: 0; }
.dh-empty { padding: 18px 4px; color: var(--text-soft); font-size: 13px; }
.dh-row { border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px; margin-bottom: 8px; }
.dh-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12px; }
.dh-who { font-weight: 700; }
.dh-agent { color: #8250df; }
.dh-web { color: #0b6bcb; }
.dh-when, .dh-size { color: var(--text-soft); }
.dh-ev { color: #b54708; background: #fff4e5; border-radius: 4px; padding: 1px 6px; }
.dh-snippet { font-size: 12.5px; color: var(--text); margin: 6px 0 8px; line-height: 1.45; }
.dh-acts { display: flex; gap: 8px; }
.dh-btn {
  font-size: 12px; padding: 4px 10px; border-radius: 6px;
  border: 1px solid var(--border); background: var(--surface); cursor: pointer;
}
.dh-btn:hover { background: var(--hover); }
.dh-restore { border-color: #0b6bcb; color: #0b6bcb; font-weight: 600; }
.dh-preview {
  border-top: 1px solid var(--border); margin-top: 10px; padding-top: 10px;
  max-height: 220px; overflow-y: auto; font-size: 13px; line-height: 1.5;
}
.dh-foot { display: flex; justify-content: flex-end; margin-top: 12px; }

/* ════════════════════════════════════════════════════════════════════════════
 * MOBILE  (≤ 640px)  —  the phone layout, in one place
 * ════════════════════════════════════════════════════════════════════════════
 * Everything below is inside `@media (max-width: 640px)` (or narrower), so a
 * desktop browser never evaluates a single declaration here. That containment
 * is the whole point: the phone layout is a different layout, not a squeezed
 * copy of the desktop one, and it must be impossible for it to leak upward.
 *
 * The five structural decisions, and why:
 *
 * 1. THE ROW BECOMES TWO LINES. A 390px screen cannot hold Gmail's desktop row
 *    (234px fixed sender + flexible subject + 70px date = 300px of chrome
 *    before a single word of subject). Measured before this change: the subject
 *    and the date were BOTH off-screen on every row, so the list showed a
 *    column of sender names and nothing else. Line 1 is sender + date, line 2
 *    is subject + snippet, which is what every mail app on a phone does.
 *
 * 2. HOVER DOES NOT EXIST, SO HOVER-ONLY CONTROLS MUST NOT. The row actions,
 *    the message actions and the star all appeared on :hover only. On a
 *    touch screen that is a feature that cannot be reached at all, so they are
 *    made permanently visible here (and the hover-reveal gradient is dropped).
 *
 * 3. SEARCH COMES BACK. `.topbar-search{display:none}` hid the search bar
 *    entirely below 640px, so searching a mailbox was impossible on a phone.
 *    The bar is restored, and the advanced panel becomes a full-screen sheet
 *    instead of a 460px dropdown anchored to a 320px viewport.
 *
 * 4. EVERY PANEL THAT WAS A FLOATING CARD BECOMES A SHEET. Compose, the
 *    read-history drawer, the snooze / filter / tag / onboarding popups and the
 *    account menu were positioned as desktop popovers; on a phone they either
 *    overflowed the viewport or floated in a way that wasted most of it. They
 *    become bottom sheets or full-screen panels.
 *
 * 5. HEIGHTS USE dvh, NEVER vh. iOS Safari resolves `vh` against the
 *    toolbar-HIDDEN viewport, so a `max-height:86vh` panel renders TALLER than
 *    what is actually on screen and its bottom (the Send button, the last
 *    option) is clipped under the browser chrome. `dvh` tracks the real
 *    visible area. The `vh` value is kept first as the fallback for engines
 *    without dvh.
 * ════════════════════════════════════════════════════════════════════════════ */

@media (max-width: 640px) {

  /* ── Login page ───────────────────────────────────────────────────────── */

  /* The entry point. It never overflowed (one centred card), but on a 320px
     screen its 34px side padding left only 232px for a field, and the register
     form is tall enough to need scrolling on a short phone. */
  /* `safe center` keeps the card CENTRED on a tall phone, but when the card is
     taller than the screen (the 4-field register form on a short one) it falls
     back to start-aligned instead of overflowing equally in both directions —
     a plain `center` puts the top of an over-tall card ABOVE the viewport,
     where it cannot be scrolled to. */
  .login-page { padding: 16px 12px; align-items: safe center; }
  .login-card { padding: 26px 20px; border-radius: 14px; }
  .login-card h1 { font-size: 21px; }
  .login-sub { margin-bottom: 18px; }
  .field { margin-bottom: 14px; }
  /* A phone keyboard covers roughly half the screen; a 16px gap between the
     last field and the button is enough to mis-tap. */
  .login-card .btn-block { margin-top: 4px; }

  /* ── Shell ────────────────────────────────────────────────────────────── */

  /* NO HEIGHT AT ALL. `top:0` + `bottom:0` size the shell, and that is the
     whole fix.

     History, because both previous attempts look right and are not:

     `100dvh` tracks the CURRENT viewport, so while Safari's toolbars animate
     the shell is briefly TALLER than the screen. A fixed box that overhangs
     gets pinned with its top above the visible area, which is the sliced top
     bar.

     `100svh` was then tried as the cure. svh is the toolbar-SHOWN height, the
     SMALLEST the viewport ever takes, so the shell can never overhang — but it
     also never GROWS. The moment Safari hides its toolbars the visible area
     gets taller while the shell keeps the small value, and the difference is
     exposed as a band of empty page under the app. Measured on his iPhone at
     375px wide: shell 640, screen 724, so an 84px gap at the foot, which is
     exactly the screenshot. The commit that introduced svh predicted this
     ("the cost is a strip of background under the shell") and judged it
     invisible because the shell and the page share a background. They do not:
     the shell paints the app, and what shows under it is bare page.

     `bottom:0` was added alongside svh as belt and braces, and it was INERT.
     For a fixed box, `top` + `bottom` + an explicit `height` are
     over-constrained, so the height wins and the bottom pin is ignored. It was
     never doing anything, which is why the fault survived it.

     THE WHOLE `position: fixed` APPROACH WAS THE BUG. It is reverted here.

     His debug ruler settled it. The ruler paints the shell's own coordinates
     down the left edge; on 7 Sep 2026 the marks 100 through 600 were all
     visible and correct, while **0 and 50 were missing** — hidden behind
     Chrome's address bar. So the shell was the RIGHT SIZE in the WRONG PLACE:
     640 tall in a 640 window, drawn about 95px too high, with its top slice
     under the chrome and a bare band of the same size exposed at the foot.
     Every measurement the app could take of itself read perfectly healthy
     (top 0, bottom 640, no overflow, no viewport gap), which is exactly why
     four rounds of fixes and a purpose-built guard all reported success while
     he was looking at a broken screen.

     The cause: `position: fixed` anchors to the LAYOUT viewport, and on
     Chrome for iOS that box starts at the PHYSICAL TOP of the screen,
     underneath the browser chrome. `top: 0` therefore does not mean "the top
     of what you can see", it means "behind the address bar". No amount of
     re-sizing (dvh, then svh, then top+bottom, then a negative bottom) can
     correct a box whose ORIGIN is wrong, which is the pattern behind every
     previous attempt.

     A STATIC body has no such problem: it is laid out in normal flow, whose
     origin is the top of the content area, below the chrome. `100dvh` is the
     currently-visible height, so the shell is exactly as tall as the screen
     shows and creates NO document overflow — which is what the original
     `position: fixed` was introduced to prevent, so nothing is lost by
     dropping it. Measured after the change: document overflow 0, so the app
     still cannot be panned off-screen. */
  /* PHONE VIEWPORT GAP — the real cure, found 9 Sep 2026 by reproducing the
   * fault on the operator's own iPhone over a USB cable (Chrome launched, killed
   * and reopened from a URL, screen captured each cycle) and bisecting the live
   * page. Findings that survive:
   *   - Stripping ALL app JavaScript did NOT fix it, so no script is the cause.
   *   - Stripping the stylesheet DID fix it, so the cause is purely CSS.
   *   - A minimal page with the SAME fixed-root + 100dvh shell never broke,
   *     however hard it was stressed, so the trigger needs the real app's box.
   *   - Forcing the shell into NORMAL DOCUMENT FLOW cured it 15 opens out of 15.
   *
   * Why fixed failed and flow works: on Chrome for iOS a `position: fixed` root
   * is anchored to the LAYOUT viewport, whose top sits UNDER the browser chrome.
   * When a tab is restored or opened from a URL, the shell paints against the
   * pre-settle window height and is left parked ~94px too high, with its top
   * behind the address bar and a bare band at the foot — and every self-measure
   * reads healthy (top 0, no overflow) because the frame moved with its own
   * ruler. In normal flow the origin is the top of the CONTENT area, below the
   * chrome, so the shell can no longer be given a wrong origin.
   *
   * The topbar is made `sticky` so it still pins to the top as the list scrolls,
   * which keeps the pre-existing single-screen feel. The document itself scrolls
   * (min-height 100svh, no overflow trap), which is exactly what removes the
   * displacement. Verified: 15/15 clean URL opens on the physical phone. */
  html, body { position: static; overflow: visible; height: auto; }
  body.mail-app {
    height: auto; max-height: none; min-height: 100svh;
    display: block; overflow: visible;
  }
  body.mail-app .topbar { position: sticky; top: 0; z-index: 30; }

  /* The floating white card becomes the whole screen: on a phone the grey
     margin around it is 32px of nothing, and the rounded corners cost a row. */
  .main-pane { margin: 0; border-radius: 0; box-shadow: none; }

  .topbar {
    padding: 0 8px 0 4px; gap: 6px;
    /* A notched phone in landscape puts the topbar under the sensor housing. */
    padding-left: max(4px, env(safe-area-inset-left));
    padding-right: max(8px, env(safe-area-inset-right));
  }

  /* The account address caption is the first thing to go: the avatar already
     answers "which account", and on a 320px screen the caption ate the room
     the search bar needs. It comes back at 480px+ (see that block below). */
  .topbar-account-addr { display: none; }
  .topbar-right { gap: 4px; }

  /* ── Sidebar drawer ───────────────────────────────────────────────────── */

  /* The drawer had a transparent background (it inherits `background:
     transparent` from the desktop rule, where it sits on the grey shell). Over
     the white thread list it was unreadable: the folder names and the rows
     behind them overlapped, letter on letter. It needs its own surface, a
     shadow, and a scrim to dismiss it. */
  .mail-sidebar {
    background: var(--surface);
    box-shadow: 4px 0 24px rgba(20, 30, 60, .18);
    padding-top: 12px;
    padding-bottom: max(16px, env(safe-area-inset-bottom));
    /* The drawer opens over the list; nothing inside it may scroll the page. */
    overscroll-behavior: contain;
  }

  /* THE GLASS THEME MAKES THE DRAWER SEE-THROUGH, and it wins on order.
     `body.theme-glass .mail-sidebar { background: transparent }` is declared
     several hundred lines LATER in this file, so it beats the rule above and
     the inbox rows read straight through the folder names (the operator
     screenshotted exactly this on 2026-09-04).
     On DESKTOP that transparency is correct and deliberate: the sidebar is part
     of the background there, sitting on the coloured mesh with nothing behind
     it. On a PHONE the same element is a drawer laid OVER the message list, so
     it has the opposite job and needs a real surface.
     It is given the theme's own frosted fill rather than flat white, so it
     still looks like the rest of the theme — but at full opacity, because a
     translucent panel over text is the defect being fixed. */
  /* The drawer keeps the theme's LOOK without its transparency.
     Measured on his account: `--glass-bg` resolves to
     `hsla(225,48%,97%,0.100)` — a 10% tint. On desktop that reads as frosted
     glass only because the coloured backdrop shows THROUGH it; an opaque
     drawer blocks that backdrop, so reusing the variable gave a dull grey
     panel. So the drawer paints its own near-white frost at full opacity, in
     the same hue family the theme uses, plus the theme's own light border. */
  body.theme-glass .mail-sidebar {
    background: hsl(225, 48%, 99%);
    border-right: 1px solid var(--glass-border, var(--border));
    box-shadow: 4px 0 26px rgba(20, 30, 60, .26);
  }
  /* The Compose pill and the active folder are translucent by design, which is
     invisible against a near-white panel. Give them the theme's accent tint so
     the drawer still reads as themed rather than as the plain light UI. */
  body.theme-glass .mail-sidebar .compose-btn {
    background: #fff;
    border: 1px solid var(--glass-border, var(--border));
    box-shadow: 0 1px 3px rgba(20, 30, 60, .10), 0 6px 18px rgba(20, 30, 60, .10);
  }
  body.theme-glass .mail-sidebar .folder.active { background: var(--accent-soft); }
  /* Tap-anywhere-to-close, and a dim so the drawer reads as being on top.
     Rendered by JS as a sibling of the sidebar (see openSidebar). */
  /* THE SCRIM MUST LIVE IN THE SAME STACKING CONTEXT AS THE DRAWER.
     The drawer is a child of `.mail-layout`, and under the glass theme that
     element becomes a stacking context (`position: relative; z-index: 1`).
     A body-level scrim therefore cannot be ordered against the drawer at all:
     whatever z-index it carries, it is compared against the LAYOUT's level 1,
     not against the drawer's 30 — so a scrim at 29 painted over the open
     drawer and greyed the folder names, which is what the operator saw even
     after the drawer itself was made opaque.
     So the JS appends the scrim INSIDE `.mail-layout` (see ensureSidebarScrim),
     where 29 vs 30 means what it says. It is `position: fixed`, so it still
     covers the viewport regardless of where it sits in the DOM. */
  .sidebar-scrim {
    position: fixed; inset: var(--topbar-h) 0 0 0; z-index: 29;
    background: rgba(16, 22, 32, .38);
    opacity: 0; pointer-events: none; transition: opacity .2s;
  }
  .sidebar-scrim.on { opacity: 1; pointer-events: auto; }

  /* Folder rows are the primary navigation on a phone and were 39px tall with
     a 9px hit area above and below. 48px is the accessible minimum. */
  .folder { padding: 12px 16px; border-radius: 0 24px 24px 0; }
  .compose-btn { padding: 14px 24px; }

  /* ── Search ───────────────────────────────────────────────────────────── */

  /* THE BAR IS BACK. It was `display:none` below 640px, which removed the
     ability to search mail at all on a phone — the single worst UX defect
     found in the audit, because there is no other route to the search API. */
  .topbar-search { display: flex; min-width: 0; padding-left: 4px; padding-right: 2px; }
  .search-wrap { flex: 1; min-width: 0; }
  /* A text input's default `min-width: auto` is its intrinsic size (~170px
     here), so `flex: 1` alone cannot shrink it and the bar pushed the topbar
     25px past a 320px screen. Both the input AND its flex parent need the
     explicit 0 floor, or the innermost one still refuses to give ground. */
  .topbar-search input { font-size: 16px; min-width: 0; }
  /* The advanced panel is a 460px form with 130px labels; anchored under a
     320px bar it overflowed the viewport by 181px and its inputs ran off the
     right edge. On a phone it becomes a full-screen sheet with the label above
     its field, which is the only shape that fits. */
  .search-adv {
    /* inset TOP 0, not --topbar-h: the panel covers the topbar rather than
       starting under it. Leaving the bar showing kept the hamburger and the
       account badge live above the panel, so a menu could open ON TOP of it.
       Reported with Settings: "no other menu should be able to come and
       overlay on top of that... let it take the full space of the screen,
       just like when you compose a new message." */
    position: fixed; inset: 0;
    z-index: 70;
    border: none; border-radius: 0;
    padding: env(safe-area-inset-top) 16px max(20px, env(safe-area-inset-bottom));
    overflow-y: auto; overscroll-behavior: contain;
    box-shadow: none;
  }

  /* SETTINGS OWNS THE WHOLE SCREEN on a phone, for the same reason and from
     the same report. On desktop the topbar staying above the settings pane is
     fine: there is room, and nothing slides over anything. On a phone the
     hamburger opens a sidebar that covers the pane the user is reading, which
     is the "dangerous" part he named. Full screen removes the possibility
     rather than defending against it.
     The pane already carries its own back arrow, so nothing is lost.
     There is no "settings is open" class on the body; the pane is toggled with
     the `hidden` attribute (showSettingsView / backToList), so `:not([hidden])`
     is the state to select on, and `:has()` lets the topbar react to it. */
  .settings-pane:not([hidden]) {
    position: fixed; inset: 0; z-index: 65;
    padding-top: env(safe-area-inset-top);
    overflow-y: auto; overscroll-behavior: contain;
  }
  body:has(.settings-pane:not([hidden])) .topbar { display: none; }
  /* Sticky title + close, because a full-screen sheet has no outside to tap. */
  .adv-sheet-head {
    display: flex; align-items: center; justify-content: space-between;
    position: sticky; top: 0; z-index: 1;
    margin: 0 -16px 12px; padding: 6px 8px 6px 16px;
    background: var(--surface); border-bottom: 1px solid var(--border);
  }
  .adv-sheet-title { font-size: 15px; font-weight: 700; }
  .search-wrap.open .topbar-search { border-radius: 10px; border: 1px solid var(--border); }
  .adv-row { flex-direction: column; align-items: stretch; gap: 5px; margin-bottom: 14px; }
  .adv-row > span { width: auto; font-weight: 600; }
  .adv-row input[type="text"], .adv-row input[type="date"], .adv-row select {
    width: 100%; min-width: 0; font-size: 16px; padding: 9px 2px;
  }
  /* "Date within" and "Size" each hold a select AND an input; side by side at
     320px they were 93px and 4px wide. Give them a row of their own. */
  .adv-row select + input { margin-top: 8px; }
  .adv-checks { flex-direction: row; flex-wrap: wrap; gap: 16px; }
  .adv-check { font-size: 14px; }
  .adv-check input { width: 20px; height: 20px; }
  .adv-foot { flex-wrap: wrap; gap: 8px; }
  .adv-foot-spacer { display: none; }
  .adv-foot .btn-primary, .adv-foot .btn-ghost { padding: 10px 16px; min-height: 38px; }
  .adv-foot .btn-primary { flex: 1; }
  .split-btn { flex: 1 1 100%; }
  .split-btn .split-main { flex: 1; }

  /* ── Thread list toolbar ──────────────────────────────────────────────── */

  .list-toolbar { padding: 10px 12px; gap: 4px; }
  /* "Refreshed 2 minutes ago · folder opened" needs 243px and had 125px, so it
     was cut mid-word on every screen. The relative time is the useful half. */
  .reload-caption { max-width: 40%; margin-right: 4px; font-size: 11.5px; }
  .reload-debug { right: 8px; left: 8px; width: auto; }

  /* ── Floating Compose ─────────────────────────────────────────────────── */

  /* Writing a message cost THREE interactions on a phone: tap the hamburger,
     wait for the drawer, tap Compose. It is the primary action of a mail app,
     so it gets its own always-visible button, bottom-right, above the safe
     area. Shown only in the LIST (a conversation has its own Reply bar and a
     floating pencil there would compete with it). */
  .compose-fab {
    display: inline-flex; align-items: center; justify-content: center;
    position: fixed; z-index: 25;
    right: max(16px, env(safe-area-inset-right));
    bottom: max(20px, env(safe-area-inset-bottom));
    width: 56px; height: 56px; border-radius: 18px;
    border: none; background: var(--accent); color: #fff;
    box-shadow: 0 3px 10px rgba(20, 30, 60, .22), 0 8px 24px rgba(37, 99, 235, .28);
    transition: transform .12s ease, box-shadow .12s ease;
  }
  .compose-fab:active { transform: scale(.94); }
  /* Hidden wherever it would be wrong or in the way. `.mail-layout.viewing`
     covers BOTH the open conversation and the settings page (showSettingsView
     sets the same class), and .has-bulk covers an active selection, whose own
     action bar owns the screen at that moment.
     Both are matched with `body:has(...)` because the button lives at BODY
     level (it has to, so the toast can lift it — see the toast rule below), so
     it is not a descendant of .mail-layout nor a sibling of the list pane. */
  body:has(.mail-layout.viewing) .compose-fab,
  body:has(.thread-list-pane.has-bulk) .compose-fab { display: none; }
  /* The toast rises above it rather than under it. */
  .toast { bottom: max(88px, calc(env(safe-area-inset-bottom) + 88px)); }
  /* …and the list gets room to scroll PAST the button, so the last
     conversation is never permanently covered by it. */
  .thread-list { padding-bottom: 84px; }

  /* ── THE THREAD ROW: two lines ────────────────────────────────────────── */

  /* Desktop is one 40px line with a fixed 234px sender column. On a phone that
     column alone is 60% of the screen, so the subject and date were pushed off
     the right edge entirely (measured: row scrollWidth 412 in a 304px pane).
     Here the row becomes a 2-row grid:
         [check] [star] | sender ............ date
                        | subject - snippet
     The check and star stay in their own rigid columns spanning both lines, so
     the text still starts at one x and the list reads as a column. */
  /* Four columns: [check] [star] | text (elastic) | trailing (auto).
     Line 1 puts the sender in col 3 and the DATE in col 4; line 2 puts the
     subject in col 3 and the paperclip/agent markers in col 4. So the two
     right-hand items never collide with the text, and the text column is the
     only one that shrinks. */
  .thread-row {
    display: grid;
    grid-template-columns: 36px 36px minmax(0, 1fr) auto;
    grid-template-rows: auto auto;
    align-items: center;
    height: auto; min-height: 64px;
    padding: 9px 12px 9px 2px;
    column-gap: 0; row-gap: 2px;
  }
  .thread-check, .thread-star {
    grid-row: 1 / span 2; align-self: center;
    /* 36×44: the width matches the grid column so the sender still starts at a
       fixed x, and 36 is the smallest square that is reliably hit with a thumb
       (below that the miss rate is real, and a mis-tap here opens a
       conversation instead of selecting it). */
    width: 36px; height: 44px;
    /* Both were hover-revealed at opacity .5; a phone has no hover, and the
       checkbox is the ONLY way to start a bulk selection. */
    opacity: 1;
  }
  .thread-star { color: var(--text-mute); }

  /* Line 1: sender (elastic, col 3) + date (auto, col 4). */
  .thread-sender-wrap {
    grid-column: 3; grid-row: 1;
    width: auto; min-width: 0; margin-right: 0; padding-left: 6px;
    overflow: hidden;
  }
  .thread-sender { font-size: 14.5px; }
  .thread-time {
    grid-column: 4; grid-row: 1; justify-self: end;
    width: auto; margin-left: 8px; padding-left: 6px;
    font-size: 12px; white-space: nowrap;
  }
  /* Line 2: subject + snippet on the left, markers on the right.
     The row is a GRID, and two children placed in the same cell stack ON TOP
     of each other — that is how the "Not followed up" chip came to paint over
     the subject text on a real 390px screenshot. So the subject column and the
     markers get their own columns on line 2 (`1fr auto`), which lets the
     subject shrink and the chip keep its full width. */
  .thread-subjectcol {
    grid-column: 3; grid-row: 2;
    padding-left: 6px; padding-right: 4px; font-size: 13.5px;
  }
  /* SUBJECT FIRST, and the shrink ORDER is the whole point. On desktop the
     subject is capped at 60% so the snippet always gets a share; on a 390px
     line that split cuts the subject to about four words while the snippet
     shows "This is a…", which identifies neither.
     Plain `flex: 0 1 auto` on both does NOT fix it: flex shrinks items in
     PROPORTION to their content width, so a long snippet still eats the
     subject. The snippet must be the one that gives way, so it carries the
     shrink factor (1) and the subject refuses to shrink until the snippet has
     been reduced to nothing (flex-shrink: 0 on a max-width-bounded box). */
  .thread-subject { flex: 0 0 auto; max-width: 100%; }
  .thread-snippet { color: var(--text-mute); flex: 1 1 0; min-width: 0; }
  .thread-dash { flex-shrink: 0; }
  /* The wrapper becomes a REAL box here (it is display:contents on desktop) so
     paperclip + chip occupy one grid cell together, at the end of line 2. */
  .thread-marks {
    display: inline-flex; align-items: center; gap: 4px;
    grid-column: 4; grid-row: 2; justify-self: end;
    padding-left: 6px;
  }
  /* With no paperclip and no chip the wrapper is an empty 6px box that still
     steals a column; most rows are that case, so collapse it. */
  .thread-marks:empty { display: none; }
  .thread-attach { margin-left: 0; flex-shrink: 0; }
  .thread-agent {
    margin-left: 0; flex-shrink: 0;
    font-size: 10px; padding: 1px 7px 1px 5px;
    max-width: 44vw; overflow: hidden; text-overflow: ellipsis;
  }

  /* Row hover actions are a DESKTOP affordance: they are revealed by :hover and
     overlay the date. On touch there is no hover, so they would either never
     appear or (worse, on iOS) appear on the first tap and swallow it. The
     long-press context menu replaces them — see the JS. */
  .thread-actions { display: none !important; }
  .thread-row:hover .thread-actions { display: none !important; }
  /* …and with the actions gone, the date must stop hiding itself for them. */
  .thread-row:hover .thread-time { visibility: visible; }
  /* No hover state at all on touch: it only ever fires as a sticky
     after-effect of a tap, which reads as "this row is selected" when it is
     not. Keep the real selection states (.checked / .selected). */
  .thread-row:hover { background: transparent; box-shadow: none; }
  .thread-row.unread:hover { background: var(--surface); }
  .thread-row.checked, .thread-row.checked:hover { background: #fdf6e3; }
  .thread-row.selected, .thread-row.selected:hover { background: var(--accent-soft); }
  /* A tap should still say something happened. */
  .thread-row:active { background: var(--row-hover); }

  /* Skeleton rows must match the new 2-line geometry or the swap reflows. */
  .thread-row.skeleton { min-height: 64px; }
  .sk-check { grid-row: 1 / span 2; margin: 0 8px; }
  .sk-star { grid-row: 1 / span 2; margin: 0 8px 0 0; }
  .sk-sender { grid-column: 3; grid-row: 1; width: 120px; margin-left: 6px; }
  .sk-subject { grid-column: 3; grid-row: 2; width: 70%; max-width: none; margin-left: 6px; }
  .sk-time { grid-column: 4; grid-row: 1; justify-self: end; margin-left: 0; }

  body.density-compact .thread-row { height: auto; min-height: 54px; padding-top: 6px; padding-bottom: 6px; }

  .inbox-section-head { padding-left: 12px; padding-right: 12px; }
  .empty-unread { min-height: 140px; padding: 12px 14px 18px; }
  .empty-unread-illo { width: 76px; height: 76px; }

  /* ── Bulk action bar ──────────────────────────────────────────────────── */

  /* Ten icon buttons at 38px plus a count and two separators need 394px; at
     320 and 390 the last three (tag, snooze, and part of unread) were cut off
     the right edge with no way to reach them. Let it scroll horizontally —
     the alternative, hiding actions, removes them from a phone entirely. */
  .bulk-bar {
    padding: 4px 8px; gap: 0;
    overflow-x: auto; overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    flex-wrap: nowrap;
  }
  .bulk-bar::-webkit-scrollbar { display: none; }
  .bulk-bar .icon-btn { flex-shrink: 0; width: 42px; height: 42px; }
  /* Same exemption as the conversation toolbar: keep the Spam / Not spam label
     readable on a phone instead of squashing it into a 42px square. */
  .bulk-bar .icon-btn.btn-worded { width: auto; height: 38px; padding: 0 12px 0 10px; }
  .bulk-count { flex-shrink: 0; font-size: 13px; white-space: nowrap; }
  .bulk-selectall { flex-shrink: 0; }

  /* ── Conversation ─────────────────────────────────────────────────────── */

  /* Ten toolbar buttons + the "1 of 12" nav needed 464px in a 304px pane, so
     Back was visible and everything after Snooze was not. Same treatment as
     the bulk bar: keep every action, scroll to it. */

  .conv-toolbar {
    padding: 4px 6px; gap: 0;
    overflow-x: auto; overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    flex-wrap: nowrap;
  }
  .conv-toolbar::-webkit-scrollbar { display: none; }
  .conv-toolbar .icon-btn { flex-shrink: 0; width: 42px; height: 42px; }
  /* Worded buttons (Spam / Not spam) must keep their label on mobile too. The
     blanket 42px square above clipped "Not spam" to an unreadable sliver, which
     defeats the whole point of wording them. The toolbar scrolls horizontally,
     so the extra width costs nothing. */
  .conv-toolbar .icon-btn.btn-worded { width: auto; height: 38px; padding: 0 12px 0 10px; }
  .conv-toolbar-sep { flex-shrink: 0; margin: 0 4px; }
  /* Back is the one control that must never scroll away: it is the only way
     out of the conversation on a phone (no sidebar visible, no Esc key). */
  #conv-back {
    position: sticky; left: 0; z-index: 2;
    background: var(--surface);
    box-shadow: 6px 0 8px -6px rgba(20, 30, 60, .18);
  }

  /* ── Newer / Older PINNED to the right, in a tinted pill ───────────────
     These two are the most important buttons in the toolbar and they used to
     be the least reachable: measured at 390px the nav block ran x=359 to
     x=541, so 151px of it sat off-screen. The toolbar scrolls, so they were
     technically reachable and practically invisible.
     Now they are sticky to the RIGHT edge, exactly as Back is sticky to the
     left, and the other actions scroll UNDERNEATH them. The pill is what
     makes that readable: without a solid, differently-coloured background the
     icons scrolling behind would show through and the two sets would blur
     into each other. */
  .conv-nav {
    position: sticky; right: 0; z-index: 3;
    flex-shrink: 0; margin-left: auto;
    display: flex; align-items: center; gap: 0;
    padding: 3px 4px 3px 6px;
    border-radius: 999px;
    /* Pastel blue by default; overridden per appearance background below. */
    background: var(--nav-pill-bg, #dbe8fb);
    box-shadow:
      /* A soft fade on the LEFT so a button sliding under the pill dims out
         rather than being chopped off by a hard edge. */
      -14px 0 12px -10px rgba(20, 30, 60, .20),
      0 1px 2px rgba(20, 30, 60, .10);
  }
  /* The pill needs a little clear air before the first scrolling action, or
     the two touch each other at rest. */
  .conv-nav::before {
    content: ""; position: absolute; right: 100%; top: 0; bottom: 0;
    width: 10px; pointer-events: none;
  }
  .conv-nav .icon-btn { width: 38px; height: 38px; color: var(--nav-pill-fg, #14315e); }
  .conv-nav .icon-btn:hover { background: rgba(255, 255, 255, .55); }
  .conv-nav .icon-btn[disabled] { opacity: .32; }
  /* Expand-all and the separator are NOT navigation; on a phone they only
     make the pill wider and dilute what it is for. The counter goes too —
     the position is already obvious from the arrows greying out at the ends. */
  .conv-nav #conv-expand,
  .conv-nav .conv-toolbar-sep,
  .conv-nav .conv-pos { display: none; }
  .conv-pos { font-size: 12px; }

  /* The pill follows the Appearance background choice. Each pair is the two
     signature hues of that mesh (the same reduction the favicon uses in
     THEME_FAVICON_STOPS), lightened so dark icons stay legible on it. */
  body[data-bg="aurora"]  .conv-nav { --nav-pill-bg: #d8e6fe; --nav-pill-fg: #23386b; }
  body[data-bg="sunrise"] .conv-nav { --nav-pill-bg: #fadfe7; --nav-pill-fg: #6d2f45; }
  body[data-bg="sunset"]  .conv-nav { --nav-pill-bg: #fbdcc9; --nav-pill-fg: #6d3a22; }
  body[data-bg="candy"]   .conv-nav { --nav-pill-bg: #fbdcef; --nav-pill-fg: #6a2e56; }
  body[data-bg="spectrum"] .conv-nav { --nav-pill-bg: #e2ddfb; --nav-pill-fg: #383069; }
  body[data-bg="ocean"]   .conv-nav { --nav-pill-bg: #cdeef7; --nav-pill-fg: #14495c; }
  body[data-bg="meadow"]  .conv-nav { --nav-pill-bg: #d8f2dd; --nav-pill-fg: #214a2c; }
  body[data-bg="dusk"]    .conv-nav { --nav-pill-bg: #dbe3fd; --nav-pill-fg: #2b3a6b; }

  /* Tighter side padding: 24px each side of a 320px screen is 15% of the width
     given to margin. This variable also drives the full-bleed dividers, the
     sticky reply bar and the message-group rules, so they all follow. */
  .conversation { --conv-pad-x: 14px; padding-top: 16px; }
  .conv-subject { font-size: 19px; margin-bottom: 16px; gap: 10px; }
  .conv-subject h1, .conv-subject .conv-subject-text { font-size: 19px; line-height: 1.3; }

  /* The message body was indented 54px to clear the avatar column. On a phone
     that is a sixth of the screen lost on every line, and the avatar is
     already above the text (the header wraps). Full width instead. */
  /* The skeleton mirrors the real message, so it must lose the same indent —
     otherwise the placeholder sits 54px right of the text that replaces it. */
  .msg-body, .msg-body-loading, .conv-sk-body { padding-left: 0; }
  .msg-attachments { padding-left: 0; }
  /* The date's 54px indent existed to line it up under that avatar-cleared
     body. With the body now full-width the indent pointed at nothing, so the
     date sat alone in the middle of its line. */
  .msg-date { padding-left: 0; }
  .msg-att-cards { gap: 8px; }
  .msg-att-card { width: 100%; }
  .msg-att-card:not(.msg-att-card-image) { width: 100%; }
  /* Per-message actions are hover-revealed on desktop; on touch they must be
     permanently visible or the kebab (Reply / Forward / Download / Print) is
     unreachable. */
  .msg-actions { opacity: 1; }
  .msg-actions .icon-btn { width: 40px; height: 40px; }
  /* The "show details" caret next to the recipient line was an 18px square —
     the smallest target in the app, and the only route to the full To/Cc/date
     of a message. `top:0` removes the 3px nudge that only made sense at 18px. */
  .msg-details-btn { width: 38px; height: 38px; top: 0; }
  .msg-collapsed-snippet { font-size: 13px; }
  .msg.collapsed .msg-head { padding: 10px 0; }
  /* Wide content inside an email (a fixed-width HTML newsletter, a big table)
     must scroll INSIDE the message rather than widening the page. */
  .msg-body table { max-width: 100%; }
  .msg-body pre { overflow-x: auto; }

  /* Reply / Forward chips: full width, thumb-sized, side by side. */
  /* Reply / Forward chips: full width, thumb-sized, side by side. */
  .conv-reply-bar {
    padding: 12px var(--conv-pad-x) max(14px, env(safe-area-inset-bottom));
    gap: 8px;
  }
  .conv-reply-bar .btn-ghost { flex: 1; justify-content: center; padding: 12px 10px; }

  /* The inline reply editor sat in a 40px-indented card, leaving ~250px for the
     recipient chips and the subject. Drop the avatar column and the card
     inset so the editor gets the full width. */
  .inline-reply-wrap { gap: 0; padding-left: var(--conv-pad-x); padding-right: var(--conv-pad-x); }
  .ir-avatar { display: none; }
  .inline-reply { padding: 10px 12px 12px; border-radius: 10px; }
  .ir-row label { width: 44px; font-size: 12px; }
  /* 16px on every text field: iOS Safari ZOOMS the whole page when a focused
     input has a font-size under 16px, and the page never zooms back. That zoom
     is what makes a mobile web app feel broken more than any layout bug. */
  .ir-row input, .ir-row select, .ir-text, .editor-html { font-size: 16px; }
  /* The font and size pickers in the formatting popup are <select>s at 12px.
     A select under 16px zooms iOS Safari exactly like a text input does, and
     the page never zooms back out. */
  .fmt-select { font-size: 16px; }
  .ir-foot { gap: 2px; }
  .ir-foot .btn-primary { padding: 10px 20px; }
  .fmt-btn { width: 36px; height: 36px; }
  .fmt-toggle { width: 38px; height: 38px; }

  /* ── Compose: a full-screen sheet ─────────────────────────────────────── */

  /* Docked bottom-right at 540px wide, it covered the screen anyway but kept
     desktop proportions (a 56px label column, a 86vh height that iOS resolves
     against the wrong viewport, rounded corners top and bottom). On a phone
     compose IS the screen. */
  .compose-window {
    right: 0; left: 0; bottom: 0; top: 0;
    width: 100%; max-width: 100%;
    max-height: none; height: 100vh; height: 100dvh;
    border-radius: 0;
    padding-bottom: env(safe-area-inset-bottom);
  }
  /* Maximise is meaningless when it is already full-screen: it would only shrink
     the sheet back to a floating card. The button is hidden in the JS. */
  .compose-window.maximized {
    right: 0; left: 0; top: 0; bottom: 0; transform: none;
    width: 100%; height: 100vh; height: 100dvh; max-height: none;
    border-radius: 0;
  }
  /* Minimised must stay a bar at the bottom, not a full-height sheet. */
  .compose-window.minimized {
    top: auto; height: auto; max-height: none;
    border-radius: 14px 14px 0 0;
  }
  .compose-head { padding: 10px 8px 10px 14px; }
  .win-btn { width: 38px; height: 38px; }
  .compose-body { padding: 4px 14px 14px; }
  .compose-row label { width: 44px; font-size: 12px; }
  /* A <select> sizes itself to its LONGEST option, and the From list holds
     "Display Name <a-long-address@domain.be>", so it rendered 306px wide in a
     292px row and ran off the sheet. min-width:0 lets flex shrink it; the
     ellipsis keeps the chosen value readable rather than cut mid-character. */
  .compose-row input, .compose-row select, .ir-row input, .ir-row select {
    font-size: 16px; min-width: 0; max-width: 100%;
  }
  .compose-row select, .ir-row select { text-overflow: ellipsis; }
  /* The To row is label + input + a Cc AND a Bcc button on one line. At 320px
     those four need 338px, so the Bcc button sat outside the sheet. Both
     toggles are the same control conceptually ("reveal another recipient
     field"), so they collapse into a single narrow chip pair that cannot push
     the input out. */
  .compose-row, .ir-row { min-width: 0; }
  .cc-toggle, .ir-cc-toggle { padding: 2px 6px; font-size: 12.5px; flex-shrink: 0; min-height: 36px; }
  .compose-foot {
    padding: 10px 12px max(10px, env(safe-area-inset-bottom));
    gap: 2px;
  }
  .compose-foot .btn-primary { padding: 11px 24px; }
  /* The recipient autocomplete is positioned in JS from the input's rect; at
     390px a 260px-minimum box hanging off a field that starts at x=170 ran
     100px past the right edge. The JS now clamps it, and this caps the height
     so it cannot cover the whole sheet. */
  .ac-dropdown { max-height: 44vh; max-height: 44dvh; }
  .ac-item { padding: 10px 9px; }

  /* ── Settings ─────────────────────────────────────────────────────────── */

  /* Seven tabs need 602px and had 304px, so "Reading", "Translation" and
     "Appearance" were simply unreachable — no scroll, no overflow menu, the
     tab strip just ended. Horizontal scroll keeps all seven. */
  .settings-tabs {
    padding: 0 8px; gap: 0;
    overflow-x: auto; overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    flex-wrap: nowrap;
  }
  .settings-tabs::-webkit-scrollbar { display: none; }
  .settings-tab-btn { flex-shrink: 0; padding: 13px 12px; white-space: nowrap; }

  /* The 180px label + control side-by-side row left ~100px for the control, so
     every hint wrapped to one or two words per line and inputs were 36px wide.
     Stack them: label on top, control full width. */
  .settings-row { flex-direction: column; gap: 8px; padding: 16px; }
  .settings-row-label { width: auto; font-size: 13.5px; padding-top: 0; }
  .settings-row-control { width: 100%; }
  .settings-input { min-width: 0; width: 100%; font-size: 16px; }
  .settings-inline { gap: 8px; }
  .settings-inline .settings-input { flex: 1 1 100%; }
  .settings-select { width: 100%; font-size: 16px; }
  .settings-hint { font-size: 12.5px; }
  /* Checkboxes and radios are the main controls in Reading / Translation and
     were 12-13px squares. */
  .settings-radio input, .settings-check input { width: 20px; height: 20px; }
  .settings-radio, .settings-check { padding: 7px 0; font-size: 14px; }
  /* Two-column action grids collapse to one. */
  .rule-actions-pick { grid-template-columns: 1fr; }
  .signature-editor-wrap { max-width: none; }
  .settings-body { padding-bottom: max(24px, env(safe-area-inset-bottom)); }
  .settings-btn-sm { padding: 9px 16px; }

  /* An address row is avatar + address + up to two badges + a menu button on
     ONE line; at 320px the badges were pushed off the right edge. Let the row
     wrap so the badges drop under the address they describe. */
  .alias-row { flex-wrap: wrap; row-gap: 6px; }
  .alias-identity { flex: 1 1 100%; }
  .alias-addr { flex-wrap: wrap; row-gap: 4px; }
  .alias-avatar, .alias-avatar-face { width: 36px; height: 36px; }
  .alias-avatar-editor { padding-left: 0; }
  .settings-add-trigger { padding: 10px 14px; }

  /* ── Popups → sheets ──────────────────────────────────────────────────── */

  /* All of these were centred desktop cards. On a phone a bottom sheet is both
     easier to reach with a thumb and impossible to mis-size. */
  .snooze-pop, .filter-pop, .onboard-pop, .draft-history-pop {
    top: auto; bottom: 0; left: 0; right: 0;
    transform: none;
    width: 100%; max-width: 100%;
    max-height: 88vh; max-height: 88dvh;
    border-radius: 16px 16px 0 0;
    padding: 18px 16px max(18px, env(safe-area-inset-bottom));
    overflow-y: auto; overscroll-behavior: contain;
  }
  /* The 2-up snooze grid at 320px gave each option ~140px, enough for "Tomorrow
     morning" only if it wrapped. One column, full width, thumb-sized. */
  .snooze-presets { grid-template-columns: 1fr; gap: 6px; }
  .snooze-preset { flex-direction: row; align-items: baseline; gap: 8px; padding: 12px 14px; }
  .snooze-when { margin-left: auto; }
  .snooze-custom-toggle { padding: 13px 12px; }
  .snooze-custom input { font-size: 16px; }
  .filter-foot { flex-wrap: wrap; gap: 8px; }
  .filter-foot .btn-primary, .filter-foot .btn-ghost { flex: 1 1 auto; padding: 11px 14px; }

  /* The tag picker is positioned by JS at a click point; on a phone that point
     can be anywhere, so it becomes a sheet like the rest. */
  .tag-pop {
    position: fixed; top: auto; bottom: 0; left: 0; right: 0;
    width: 100%; max-width: 100%;
    border-radius: 16px 16px 0 0;
    padding: 14px 14px max(14px, env(safe-area-inset-bottom));
    max-height: 80vh; max-height: 80dvh; overflow-y: auto;
    /* Like the context menu and the account menu, this picker has no overlay
       element of its own, so it paints its own dim with a spread shadow. */
    box-shadow: 0 -2px 20px rgba(20, 30, 60, .22), 0 0 0 100vmax rgba(16, 22, 32, .38);
  }
  .tag-pop-list { max-height: 46vh; max-height: 46dvh; }
  .tag-pop-item { padding: 11px 8px; }
  .tag-pop-search { font-size: 16px; }

  /* Read-history: a 92vw drawer left a useless 8% sliver of the page behind
     it. Full screen, and its 6-column grid already collapses at 420px. */
  .rh-panel { width: 100%; }
  .rh-head { padding: 14px 14px 12px; }
  .rh-body { padding: 14px 14px max(24px, env(safe-area-inset-bottom)); }

  /* The account menu is a 300px popover anchored to the avatar at the far
     right; at 320px it hung 20px off the edge. Sheet — and like the context
     menu it owns no overlay element, so it paints its own dim with a spread
     shadow rather than floating over a fully-lit page. */
  .account-menu {
    position: fixed; top: auto; bottom: 0; left: 0; right: 0;
    width: 100%; border-radius: 16px 16px 0 0;
    max-height: 86vh; max-height: 86dvh; overflow-y: auto;
    padding: 8px 8px max(12px, env(safe-area-inset-bottom));
    box-shadow: 0 -2px 20px rgba(20, 30, 60, .22), 0 0 0 100vmax rgba(16, 22, 32, .38);
  }
  .account-item { padding: 12px 10px; }
  .account-add-form input { font-size: 16px; }
  .account-menu-close { width: 38px; height: 38px; }
  /* The tiles are already 45px tall, but the pencil badge is the only way into
     the avatar editor now, so on a phone it gets a full finger target. */
  .account-hero-avatar-edit { width: 30px; height: 30px; right: -4px; bottom: -4px; }
  .favicon-preset { min-width: 38px; min-height: 38px; }
  /* Appearance: the Automatic/White/Black segmented control and the tag
     picker's "Manage tags" link were both 30-32px tall. */
  .favicon-seg button { min-height: 38px; }
  .tag-pop-manage, .tag-pop-create { min-height: 38px; }
  /* The Cc / Bcc reveal toggles sit at the end of the To row; keep them a
     comfortable 38px without letting them push the input out. */
  .cc-toggle, .ir-cc-toggle { min-width: 38px; min-height: 38px; }

  /* The context menu (long-press) is a sheet too: positioned at a touch point
     it would open half off-screen near an edge. Every other sheet here has a
     dimmed backdrop; this one has no overlay element of its own, so it draws
     its own with a huge spread shadow. Without it the sheet floats over a
     fully-lit list and reads as part of the page rather than on top of it. */
  .ctx-menu {
    position: fixed; top: auto !important; bottom: 0; left: 0 !important; right: 0;
    width: 100%; max-width: 100%;
    border-radius: 16px 16px 0 0;
    padding: 8px 8px max(12px, env(safe-area-inset-bottom));
    max-height: 80vh; max-height: 80dvh; overflow-y: auto;
    box-shadow: 0 -2px 20px rgba(20, 30, 60, .22), 0 0 0 100vmax rgba(16, 22, 32, .38);
  }
  .ctx-item { padding: 13px 12px; font-size: 15px; }

  .sendfail-dialog, .theme-unsaved-dialog {
    width: calc(100% - 24px); max-width: none;
    max-height: 86vh; max-height: 86dvh; overflow-y: auto;
  }

  /* ── Toast ────────────────────────────────────────────────────────────── */

  /* Full-bleed above the safe area, and its label may wrap: a 320px screen
     cannot hold "Conversation moved to Trash" plus an Undo button on one
     nowrap line. */
  .toast {
    left: 8px; right: 8px; bottom: max(12px, env(safe-area-inset-bottom));
    width: auto; max-width: none;
    /* Above the floating Compose button (z-index 25), never behind it. */
    z-index: 80;
  }
  .toast-msg { white-space: normal; }
  .toast-action { padding: 8px 12px; }
  .toast:not([hidden]) ~ .mini-toast { bottom: calc(72px + env(safe-area-inset-bottom)); }
  /* A full-bleed toast covers the Compose button where it sits (measured
     overlapping at 390px). Undo is the only thing that matters for those few
     seconds, so the button steps ASIDE — up, above the toast — instead of being
     covered, and drops back on its own when the toast retires.
     `body:has(...)` rather than a sibling combinator: the toast comes AFTER the
     button in the DOM and `~` cannot look backwards. Supported in every engine
     that ships dvh, and if it ever fails the button simply stays where it is,
     which is the pre-existing behaviour rather than a broken layout. */
  body:has(.toast:not([hidden])) .compose-fab {
    bottom: max(84px, calc(env(safe-area-inset-bottom) + 84px));
  }

  /* ── Tap-target floor ─────────────────────────────────────────────────── */

  /* 38px icon buttons everywhere; 44px is Apple's documented minimum and the
     difference is felt on every mis-tap. Applied to the shell controls only,
     so it cannot inflate a dense list row. */
  .topbar .icon-btn, .menu-toggle, .account-avatar { width: 42px; height: 42px; }
  .search-ico-btn, .search-clear, .search-adv-btn { width: 38px; height: 38px; }
  .pager-btn { width: 38px; height: 38px; }
  .section-collapse { width: 36px; height: 36px; }
  #settings-back, #conv-back, #refresh-btn { width: 42px; height: 42px; }
  .settings-btn-sm, .settings-add-trigger { min-height: 38px; }
  /* The reload caption is a real button (it opens the reload-history panel) but
     was only 22px tall, so it was easy to miss and easy to hit by accident when
     aiming at the refresh icon next to it. */
  .reload-caption { min-height: 36px; }

  /* ── The small JS-positioned popovers ─────────────────────────────────── */

  /* These are placed by script from an anchor's rect and clamped only on the
     LEFT (Math.max(8, …)), so a card wider than the screen still runs off the
     right edge. They are too small and too transient to be worth turning into
     sheets, but they must FIT: cap each at the viewport minus a gutter. The
     details card (460px) and the translate menu (330px) are both wider than a
     320px phone without this. */
  .msg-details-card, .tr-pop, .sender-card, .star-picker, .color-palette,
  .reload-debug, .ac-dropdown {
    max-width: calc(100vw - 16px);
  }
  /* The details card is a label/value table with an 82px right-aligned label
     column; at 320px that leaves ~200px for a long address, so let it wrap. */
  .msg-det-row { flex-wrap: wrap; row-gap: 2px; }
  .msg-det-label { min-width: 0; text-align: left; }
  .star-pick { width: 38px; height: 38px; }
  .cp-swatch { width: 26px; height: 26px; }
}

/* ── 480px and up: the phone has room for the account caption again ──────── */
@media (min-width: 481px) and (max-width: 640px) {
  .topbar-account-addr { display: inline-block; max-width: 170px; }
}

/* ── Very narrow (≤ 360px): the last few pixels ─────────────────────────── */
@media (max-width: 360px) {
  /* The star costs 34px of a 320px row and duplicates a context-menu action;
     the checkbox does not (it is the only route into bulk selection). */
  .thread-row { grid-template-columns: 34px 0px minmax(0, 1fr) auto; padding-right: 8px; }
  .thread-star { display: none; }
  .sk-star { display: none; }
  /* The floating Compose button is 56px + a 16px margin. At 320px it covered
     the right-hand markers of whichever row it happened to sit over (seen on a
     real screenshot: it hid the "Not followed up" chip). Shrink it and tuck it
     into the corner so it overlaps as little as possible; the list already
     scrolls past it, so nothing is permanently hidden. */
  .compose-fab {
    width: 50px; height: 50px; border-radius: 16px;
    right: max(12px, env(safe-area-inset-right));
    bottom: max(14px, env(safe-area-inset-bottom));
  }
  .thread-list { padding-bottom: 74px; }
  .conversation { --conv-pad-x: 12px; }
  .conv-subject, .conv-subject h1 { font-size: 18px; }
  .thread-sender { font-size: 14px; }
  .thread-subjectcol { font-size: 13px; }
}

/* ── Landscape phone: vertical space is the scarce axis ─────────────────── */
/* ════════════════════════════════════════════════════════════════════════════
 * LANDSCAPE PHONE — height is the scarce axis, so BUY BACK VERTICAL SPACE
 * ════════════════════════════════════════════════════════════════════════════
 * Portrait and landscape are not the same problem turned sideways. In portrait
 * the screen is 390 wide and 760 tall, so WIDTH is scarce and the fixes are
 * about fitting sideways. In landscape it is 844 wide and about 300 tall once
 * Safari's own chrome is subtracted — wider than a small laptop window, and
 * shorter than a single email header.
 *
 * Measured on the shipped portrait build at 844x300 (an iPhone 14 held
 * sideways), before this block existed:
 *
 *     topbar          60px
 *     list toolbar    62px
 *     section head    45px
 *     panel margins   16px
 *     ───────────────────
 *     CHROME         183px  of 300  →  61% of the screen
 *     rows visible     0
 *
 * Zero. Not "cramped": a 40px row could not fit in what was left, so the app
 * showed a title bar, a caption, a heading, and no mail at all. That is the
 * screenshot the operator sent.
 *
 * Every rule below buys height back. The width is not touched, because there
 * is plenty of it — that is exactly why the SIDEBAR can stay open here as a
 * permanent rail rather than collapsing to a drawer.
 * ════════════════════════════════════════════════════════════════════════════ */
/* The width ceiling is 1000px, not 900: a 16 Pro Max is 956x440 in landscape
   and was missing this whole block at 900 (measured — it still showed 183px of
   chrome and no rows). 1000 covers every phone and the small tablets held
   sideways, while the `max-height: 480px` term keeps every real laptop out:
   no laptop viewport is under 480px tall. */
@media (max-width: 1000px) and (max-height: 480px) and (orientation: landscape) {
  /* A 44px-tall sheet header plus a 60px topbar leaves almost nothing for the
     content; sheets take the full height and scroll instead. */
  .snooze-pop, .filter-pop, .onboard-pop, .tag-pop, .ctx-menu, .account-menu,
  .draft-history-pop { max-height: 92vh; max-height: 92dvh; }
  .compose-window, .compose-window.maximized { height: 100vh; height: 100dvh; }
  /* The "no unread messages" celebration cost 125px — 42% of a 300px screen —
     to deliver one piece of news, and pushed the FIRST message row to y=292 on
     a 300px viewport. That is why the operator's landscape screenshot showed a
     title bar, a heading, and no mail whatsoever.
     It becomes a single centred LINE: illustration inline at 24px, title
     beside it, subtitle and confetti dropped. Still a celebration, a fifth of
     the cost. */
  .empty-unread {
    min-height: 0; padding: 4px 12px;
    flex-direction: row; gap: 8px; justify-content: center;
  }
  .empty-unread-illo { width: 24px; height: 24px; }
  .empty-unread-title { font-size: 12.5px; }
  .empty-unread-sub { display: none; }
  .empty-unread-confetti { display: none; }
  /* When Unread is EMPTY the whole band above the real mail is a heading, a
     congratulation and a second heading — measured at 120px of a 300px screen,
     leaving the first actual message at y=208. The "Unread" heading over an
     empty section says nothing the line beneath it does not, so it goes, and
     the "Everything else" heading below it loses its top margin. The headings
     both stay whenever there IS unread mail, which is when they mean
     something. */
  .inbox-section-head:has(+ .empty-unread) { display: none; }
  .empty-unread + .inbox-section-head.secondary { margin-top: 0; }

  /* ── Buy back the 183px ────────────────────────────────────────────────── */

  /* 1. The topbar: 60 → 44px. It holds a search field and an avatar, both of
     which are comfortable at 44. That is 16px, or half a message row. */
  :root { --topbar-h: 44px; }
  .topbar { height: 44px; padding: 0 8px; gap: 6px; }
  .topbar .icon-btn, .menu-toggle, .account-avatar { width: 36px; height: 36px; }
  .topbar-search { padding: 2px 4px 2px 6px; }
  .topbar-search input { padding: 4px 0; }
  .search-ico-btn, .search-clear, .search-adv-btn { width: 32px; height: 32px; }
  /* The brand wordmark is pure decoration and costs width that the search bar
     can use; the address caption already says which account this is. */
  .topbar-brand { display: none; }

  /* 2. The list toolbar: 62 → 38px. Its "Refreshed just now · folder opened
     (inbox)" caption is the single least valuable line on a 300px screen, so
     it goes; the refresh button beside it does the same job on demand. */
  .list-toolbar { padding: 5px 12px; }
  .list-title { font-size: 14px; }
  .reload-caption { display: none; }
  #refresh-btn { width: 34px; height: 34px; }

  /* 3. The section heading ("Unread" / "Everything else"): 45 → 26px. */
  .inbox-section-head { padding: 5px 12px 3px; font-size: 13px; }
  .inbox-section-head.secondary { margin-top: 8px; }
  .section-collapse { width: 26px; height: 26px; }
  .pager-btn { width: 26px; height: 26px; }
  .pager-range { font-size: 11px; }

  /* 4. The panel's floating margin: the rounded card on a grey background is a
     desktop nicety that costs 16px of height and 16px of width here. */
  .main-pane { margin: 0; border-radius: 0; }

  /* ── The sidebar STAYS OPEN as an icon rail ───────────────────────────── */

  /* This is the one place where landscape is genuinely better than portrait:
     844px of width is plenty for a 64px rail AND a full-width list, so the
     folders can be permanently visible instead of hidden behind a drawer that
     costs a tap. It is the desktop's own ≤1000px collapsed rail, which already
     exists — it just needs to survive the portrait rules that turn the sidebar
     into an off-canvas drawer.
     `position: static` is what puts it back in the grid. */
  .mail-layout { grid-template-columns: 64px 1fr; }
  .mail-sidebar {
    position: static; transform: none;
    width: auto; z-index: auto;
    background: transparent; box-shadow: none; border-right: none;
    padding: 6px 6px 8px;
    /* Seven folder icons at 36px do not fit in 300px next to a compose button,
       so the rail scrolls rather than clipping its last entries (measured:
       its own content overflowed with no way to reach the bottom). */
    overflow-y: auto; overscroll-behavior: contain;
  }
  .mail-sidebar .folder-name,
  .mail-sidebar .compose-btn span,
  .sidebar-labels, .sidebar-version { display: none; }
  /* Seven folders + a compose button must fit 300px of height without
     scrolling, or the last folder (Trash) sits below the fold in a rail that
     is meant to be glanceable. Measured before tightening: 302px of content in
     256px of rail. 30px rows + a 34px button + gaps lands at ~250px. */
  .folder { padding: 5px 8px; justify-content: center; border-radius: 8px; }
  .folder-ico svg { width: 17px; height: 17px; }
  .folder-nav { gap: 0; }
  .folder-count { display: none; }
  .compose-btn {
    padding: 7px; margin-bottom: 6px; border-radius: 10px;
    align-self: center; justify-content: center;
    box-shadow: 0 1px 3px rgba(20, 30, 60, .12);
  }
  /* The hamburger has nothing to open now, and the drawer scrim must never
     appear: both would fight the permanent rail. */
  .menu-toggle { display: none; }
  .sidebar-scrim { display: none; }
  /* …and with the rail always there, the floating Compose button is a second
     button for the same job, in the corner where it covers a row. */
  .compose-fab { display: none !important; }

  /* ── The rows themselves ──────────────────────────────────────────────── */

  /* Back to ONE line. The two-line row exists because a 390px-wide screen
     cannot hold sender + subject + date side by side; at 844px it comfortably
     can, and one 34px line instead of two 64px ones roughly TRIPLES the number
     of messages on screen. This is the single biggest win in the block. */
  .thread-row {
    grid-template-columns: 34px 34px 190px minmax(0, 1fr) auto;
    grid-template-rows: auto;
    min-height: 34px; height: 34px;
    padding: 0 12px 0 2px; row-gap: 0;
  }
  .thread-check, .thread-star { grid-row: 1; width: 34px; height: 32px; }
  .thread-sender-wrap { grid-column: 3; grid-row: 1; }
  .thread-subjectcol { grid-column: 4; grid-row: 1; padding-left: 10px; }
  .thread-marks { grid-column: 5; grid-row: 1; }
  .thread-time { grid-column: 5; grid-row: 1; justify-self: end; }
  /* With sender and subject on one line the subject may take what it needs. */
  .thread-subject { max-width: 60%; }
  .thread-list { padding-bottom: 8px; }
  body.density-compact .thread-row { height: 30px; min-height: 30px; }
  /* Skeletons must match, or the swap to real rows jumps the list. */
  .thread-row.skeleton { min-height: 34px; height: 34px; }
  .sk-check, .sk-star { grid-row: 1; }
  .sk-sender { grid-column: 3; grid-row: 1; }
  .sk-subject { grid-column: 4; grid-row: 1; }
  .sk-time { grid-column: 5; grid-row: 1; }

  /* ── Reading a message ────────────────────────────────────────────────── */

  /* The conversation had 173px of reading area under a 51px toolbar and a
     28px subject. Trim the chrome so the message itself gets the screen. */
  .conv-toolbar { padding: 2px 6px; }
  .conv-toolbar .icon-btn { width: 36px; height: 36px; }
  .conv-toolbar .icon-btn.btn-worded { width: auto; height: 36px; padding: 0 11px 0 9px; font-size: 12px; }
  #conv-back { width: 36px; height: 36px; }
  .conversation { padding-top: 10px; }
  .conv-subject { font-size: 17px; margin-bottom: 10px; }
  .conv-subject h1, .conv-subject .conv-subject-text { font-size: 17px; }
  .msg-head { padding-bottom: 2px; }
  .msg-avatar { width: 32px; height: 32px; }
  .msg-body { padding-top: 8px; }
  /* The reply/forward bar is sticky at the foot; at this height its 14/16px
     padding is a visible slice of the message it sits under. */
  .conv-reply-bar { padding-top: 8px; padding-bottom: 8px; }
  .conv-reply-bar .btn-ghost { padding: 8px 10px; }
  /* The status strips (kept-unread, snoozed, agent, follow-up) are one line
     each and can stack three deep on a 300px screen. */
  .conv-unread-bar, .conv-snooze-bar, .conv-agent-bar, .conv-followup-bar {
    padding: 5px 12px; font-size: 12px;
  }

  /* ── Top-anchored popovers ────────────────────────────────────────────── */

  /* A LANDSCAPE PHONE IS WIDER THAN 640px (an iPhone 14 is 844x390), so the
     ≤640 sheet rules do NOT apply to it: the account menu is still the desktop
     popover, anchored 46px under the avatar. Measured at 844x390 it ran from
     y=50 to y=409 in a 390px-tall viewport, putting "Sign out of this account"
     off the bottom with no way to reach it.
     A popover anchored near the top can only ever be as tall as the room under
     that anchor, so it is capped against the VIEWPORT and made scrollable.
     These MUST live in the 480px block, not the 320px one below: at 844x390 a
     320px-tall query does not match, and the menu silently loses its scroll. */
  .account-menu {
    max-height: calc(100dvh - var(--topbar-h) - 16px);
    overflow-y: auto; overscroll-behavior: contain;
  }
  /* The hero avatar block is 60px of portrait + 18px padding for decoration
     that repeats the avatar already in the topbar; on a 390px-tall screen that
     is a sixth of the sheet spent on a picture. */
  .account-hero { padding: 10px 14px 8px; }
  .account-hero-avatar { width: 44px; height: 44px; font-size: 18px; }
  /* The same reasoning for every other top-anchored popover here. */
  .search-adv, .reload-debug, .tr-pop, .msg-details-card {
    max-height: calc(100dvh - var(--topbar-h) - 16px);
    overflow-y: auto;
  }
}

/* The smallest phone held sideways (an SE is 667x285 once Safari's chrome is
   gone) needs one more turn of the screw in the CONVERSATION only. Measured
   there: toolbar 41 + subject 29 + a 71px message header left the actual
   message text starting at y=314 on a 285px screen — the whole point of
   opening the mail was off the bottom. */
@media (max-width: 1000px) and (max-height: 320px) and (orientation: landscape) {
  .conv-subject { font-size: 15px; margin-bottom: 6px; }
  .conv-subject h1, .conv-subject .conv-subject-text { font-size: 15px; }
  .conversation { padding-top: 6px; }
  /* The header carries an avatar, a name, the address and a date over three
     lines; at this height the name and date on one line is enough, and the
     address is a tap away in the details card. */
  .msg-head { padding-bottom: 0; gap: 8px; }
  .msg-avatar { width: 26px; height: 26px; font-size: 12px; }
  .msg-from-addr { display: none; }
  .msg-body { padding-top: 4px; }
  .msg-collapsed { padding-top: 4px; padding-bottom: 4px; }
  /* The reply bar was 50px of a 285px screen, a sixth of it, permanently
     pinned over the message. */
  .conv-reply-bar { padding-top: 5px; padding-bottom: 5px; }
  .conv-reply-bar .btn-ghost { padding: 6px 10px; font-size: 13px; }

  /* ── Settings ─────────────────────────────────────────────────────────── */

  /* Stacked rows are a portrait answer to a narrow screen. Here the screen is
     844px wide, so the label goes back beside its control and each row costs
     one line instead of two. */
  .settings-toolbar { padding: 2px 8px; }
  .settings-tab-btn { padding: 8px 12px; }
  .settings-row { flex-direction: row; gap: 20px; padding: 10px 16px; }
  .settings-row-label { width: 150px; }
  .settings-input { width: auto; min-width: 240px; }
  .settings-inline .settings-input { flex: 0 1 auto; }
}

/* ── Touch (any width): hover-only affordances must not be the only route ── */
/* Keyed on the POINTER, not the width, so a touchscreen laptop or an iPad in
   landscape (both wider than 640px) also gets reachable controls. Deliberately
   narrow in scope: it reveals things, it never re-lays-out anything. */
@media (hover: none) and (pointer: coarse) {
  .msg-actions { opacity: 1; }
  .thread-check, .thread-star { opacity: 1; }
  .msg-att-actbar { opacity: 1; }
  /* A sticky :hover after a tap makes a row look permanently selected. */
  .thread-row:hover { background: transparent; box-shadow: none; }
  .thread-row.unread:hover { background: var(--surface); }
  .thread-row:hover .thread-time { visibility: visible; }
  .thread-row:hover .thread-actions { display: none; }

  /* A TABLET (768-1024px) runs the DESKTOP layout — wide enough for it — but
     still cannot hover, so the small desktop targets are hit with a finger.
     Raise only the ones measured under 36px; this changes no geometry, since
     each already sits in a row with slack around it. */
  .search-ico-btn, .search-clear, .search-adv-btn { width: 38px; height: 38px; }
  .pager-btn { width: 36px; height: 36px; }
  .section-collapse { width: 36px; height: 36px; }
  .reload-caption { min-height: 36px; }
  .account-menu-close { width: 36px; height: 36px; }
  .account-hero-avatar-edit { width: 28px; height: 28px; }
  .msg-details-btn { width: 36px; height: 36px; top: 0; }
  .favicon-preset, .favicon-seg button { min-width: 36px; min-height: 36px; }
  .tag-pop-manage, .tag-pop-create { min-height: 36px; }
  .settings-btn-sm, .settings-add-trigger { min-height: 36px; }
  .adv-foot .btn-ghost, .adv-foot .btn-primary { min-height: 36px; }
  .cc-toggle, .ir-cc-toggle { min-width: 36px; min-height: 36px; }
  .dh-btn, .conn-try { min-height: 36px; }
  .win-btn { width: 36px; height: 36px; }
  .fmt-toggle, .fmt-btn { width: 36px; height: 36px; }
  .msg-actions .icon-btn { width: 36px; height: 36px; }
  .alias-avatar, .alias-avatar-face { width: 36px; height: 36px; }
  .split-caret { min-width: 36px; }
  /* A native date input renders its own control; when the flex row squeezes it
     the browser clips the calendar glyph to a few pixels. Give it a floor so
     the picker stays reachable on a tablet's advanced-search panel. */
  #adv-date { min-width: 140px; }
}

/* A file too big to attach is uploaded and sent as a link. This is the
   placeholder shown in the editor while its bytes are going up: a big video
   takes a while, and with no marker the editor looks like it ignored the drop. */
.file-link-uploading {
  margin: 10px 0;
  padding: 10px 12px;
  border: 1px dashed var(--border);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text-soft);
  font-size: 13px;
}

/* ── Mail-server button (topbar) ────────────────────────────────
   The crossing between the inbox and the mail-server dashboard. It sits at the
   top right, always visible and one click away, because a control people reach
   for rarely must not have to be hunted for (asked 2026-09-13).

   The SAME slot carries the upsell for a customer with no server yet, since
   "get your own domain" and "here is your dashboard" are the same destination
   one decision apart. */
.mailserver-btn {
  display: inline-flex; align-items: center; gap: 7px;
  height: 30px; padding: 0 14px; margin-right: 10px;
  border-radius: 100px; text-decoration: none; cursor: pointer;
  background: linear-gradient(135deg, #6366f1, #8b5cf6);
  color: #fff; font-size: 12.5px; font-weight: 700; letter-spacing: .01em;
  border: none;
  box-shadow: 0 2px 8px rgba(99,102,241,.32);
  transition: transform .12s ease, box-shadow .12s ease, filter .12s ease;
  white-space: nowrap;
}
.mailserver-btn:hover { filter: brightness(1.07); box-shadow: 0 4px 14px rgba(99,102,241,.42); transform: translateY(-1px); }
.mailserver-btn:active { transform: translateY(0); }
.mailserver-btn svg { flex: 0 0 auto; opacity: .95; }
.mailserver-btn-text { line-height: 1; }
/* The domain COUNT, never a domain name. */
.mailserver-btn-sub { font-weight: 500; opacity: .82; }

/* Two icons, one per state: the rack when a server exists, the spark when it
   is still an invitation. Only one is ever displayed. */
.mailserver-btn .mailserver-btn-ico-up { display: none; }
.mailserver-btn.is-upsell .mailserver-btn-ico-has { display: none; }
.mailserver-btn.is-upsell .mailserver-btn-ico-up { display: inline-flex; }

/* The upsell is an invitation, not an alarm: it whispers where the real button
   speaks, so an existing server always outranks it visually. */
.mailserver-btn.is-upsell {
  background: #fff; color: #4f46e5;
  border: 1px solid #c7d2fe;
  box-shadow: none; font-weight: 600;
}
.mailserver-btn.is-upsell:hover { background: #eef2ff; filter: none; box-shadow: 0 2px 8px rgba(99,102,241,.18); }

/* On a narrow screen the count folds away rather than pushing the avatar off. */
@media (max-width: 720px) { .mailserver-btn .mailserver-btn-sub { display: none; } }
