﻿/* ============ Quriou Mail — Chinese-inspired minimal UI ============ */

:root[data-theme="light"] {
  --bg: #eceae5;             /* VIS-05: was #f7f5f0 — the white panel could not separate from it */
  --bg-panel: #ffffff;
  --bg-hover: #d9d6d0;       /* VIS-05: 1.21:1 on the new page (was 1.07:1) */
  --bg-active: #cbc8c3;      /* one step beyond hover, or selected reads as lighter */
  --text: #2b2b2b;
  --text-dim: #6c675b;        /* 4.74:1 on the new --bg, 5.7:1 on --bg-panel */
  --border: #e4dfd3;
  --accent: #14535f;         /* Qmail teal — 7.18:1 on --bg, 8.63:1 on --bg-panel */
  --accent-soft: #dfeceb;
  --accent-strong: #14535f;  /* white on this: 8.63:1 */
  --border-strong: #8b867b;  /* 3.01:1 on the new --bg, for control boundaries */
  /* danger was a single theme-invariant var(--danger) used in 11 rules: 3.92:1 on the
     light ground and 3.93:1 on the dark panel, so it failed AA in BOTH themes.
     Two tokens, measured per theme. */
  --danger: #a8321d;         /* 5.56:1 on --bg */
  --danger-strong: #8f2a18;  /* white on this: 6.68:1 */
  --shadow: 0 2px 12px rgba(60, 50, 30, 0.08);
}

:root[data-theme="dark"] {
  --bg: #171512;
  --bg-panel: #292622;       /* VIS-05: 1.21:1 against the page (was 1.09:1) */
  --bg-hover: #2a2620;
  --bg-active: #353029;
  --text: #eae6dc;
  --text-dim: #938f82;       /* 4.7:1 on the lightened panel */
  --border: #35302a;
  --accent: #2f9c9c;         /* 5.52:1 on --bg, 5.08:1 on --bg-panel */
  --accent-soft: #12292b;
  --accent-strong: #1d7a80;  /* white on this: 5.06:1 */
  --danger: #f08a72;         /* 6.31:1 on --bg, 5.72:1 on --bg-panel */
  --danger-strong: #b3391f;  /* white on this: 5.11:1 */
  --border-strong: #67625c;  /* 3.00:1 on --bg, for control boundaries */
  --shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
}

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

body {
  font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: var(--bg);
  color: var(--text);
  height: 100vh;   /* fallback */
  height: 100dvh;
  overflow: hidden;
}

.hidden { display: none !important; }
a { color: var(--accent); }

/* ============ Buttons ============ */
.btn {
  border: 1px solid var(--border);
  background: var(--bg-panel);
  color: var(--text);
  padding: 10px 18px;
  border-radius: 6px;
  cursor: pointer;
  font-family: inherit;
  font-size: 14px;
  transition: background 0.15s;
}
.btn:hover { background: var(--bg-hover); }
.btn-primary { background: var(--accent-strong); border-color: var(--accent-strong); color: #fff; }
.btn-primary:hover { filter: brightness(1.1); background: var(--accent-strong); }
.btn-ghost { border: none; background: transparent; color: var(--text-dim); }
.btn-ghost:hover { color: var(--text); background: var(--bg-hover); }
.btn-block { width: 100%; }

.icon-btn {
  border: none; background: transparent; color: var(--text-dim);
  font-size: 18px; cursor: pointer; padding: 8px 10px; border-radius: 6px;
  line-height: 1;
}
.icon-btn:hover { background: var(--bg-hover); color: var(--text); }
.tb-dragging {
  opacity: 0.55;
  background: var(--bg-hover);
  outline: 2px dashed var(--text-dim);
  outline-offset: -2px;
}
.icon-btn.danger:hover { color: var(--danger); } /* was var(--accent) -- the destructive button turned BRAND TEAL on hover */

/* Kill double-tap zoom / 300ms delay on interactive elements (no visual change) */
.btn, .icon-btn, .folder-item, .account-btn, .account-menu-item,
.smart-tab, .ai-chip, .ai-reply, .email-item, .smart-item, .attachment-chip,
input, select, [contenteditable] { touch-action: manipulation; }

/* ============ Login ============ */
.login-page {
  height: 100vh; height: 100dvh; display: flex; align-items: center; justify-content: center;
  background:
    radial-gradient(circle at 20% 20%, var(--accent-soft), transparent 40%),
    var(--bg);
}
.login-card {
  background: var(--bg-panel); border: 1px solid var(--border);
  border-radius: 10px; padding: 48px 40px; width: 340px; text-align: center;
  box-shadow: var(--shadow);
}
/* The signed-out screens showed a serif "Q" in a teal tile -- the brand this
   app retired -- so sign-in, setup and password-reset, i.e. every first
   impression, were the only places still wearing it. They now use the same
   mark as the sidebar. The Noto Serif SC webfont was downloaded solely to
   render that one letter and has been dropped from the font request. */
.login-mark { display: block; width: 56px; height: 56px; margin: 0 auto 16px; }
.login-card h1 { font-size: 22px; font-weight: 700; }
.login-sub { color: var(--text-dim); font-size: 13px; margin: 4px 0 24px; }
.login-card input {
  width: 100%; padding: 12px 14px; margin-bottom: 12px;
  border: 1px solid var(--border); border-radius: 6px;
  background: var(--bg); color: var(--text); font-family: inherit; font-size: 14px;
}
.login-error { color: var(--accent); font-size: 13px; margin-top: 10px; }
.login-note { color: var(--text-dim); font-size: 12px; line-height: 1.5; margin-top: 14px; }

/* --- sign in / create account --- */
.auth-tabs {
  display: flex; gap: 4px; margin-bottom: 18px; padding: 4px;
  background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
}
.auth-tab {
  flex: 1; padding: 8px 6px; border: none; border-radius: 6px; cursor: pointer;
  background: transparent; color: var(--text-dim);
  font-family: inherit; font-size: 13px; transition: background 0.15s, color 0.15s;
}
.auth-tab.is-on { background: var(--bg-panel); color: var(--text); box-shadow: var(--shadow); }
.auth-alt {
  display: block; width: 100%; margin-top: 14px; padding: 6px;
  background: none; border: none; cursor: pointer;
  color: var(--text-dim); font-family: inherit; font-size: 12px; text-decoration: underline;
}
.auth-alt:hover { color: var(--text); }

/* --- provider picker --- */
.setup-card { width: min(460px, 92vw); padding: 40px 28px 28px; }
.provider-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
  gap: 8px; margin: 6px 0 4px; text-align: left;
}
.provider-tile {
  display: flex; align-items: center; gap: 9px; padding: 10px 10px;
  background: var(--bg); border: 1px solid var(--border); border-radius: 10px;
  cursor: pointer; color: var(--text); font-family: inherit; font-size: 13px;
  transition: transform 0.12s, border-color 0.15s, background 0.15s;
}
.provider-tile:hover { background: var(--bg-hover); border-color: var(--accent); transform: translateY(-1px); }
.provider-tile:active { transform: scale(0.97); }
.pv-badge {
  flex: none; width: 26px; height: 26px; border-radius: 6px;
  display: grid; place-items: center; color: #fff; font-weight: 700; font-size: 13px;
}
.pv-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.provider-modal { width: min(430px, 94vw); }
.provider-body { padding: 16px 18px 18px; }
.pv-help {
  background: var(--bg); border: 1px solid var(--border); border-left: 3px solid var(--accent);
  border-radius: 6px; padding: 10px 12px; margin-bottom: 10px;
  font-size: 13px; line-height: 1.55; color: var(--text-dim);
}
.pv-link { display: inline-block; font-size: 13px; margin-bottom: 12px; }
.acc-hint { color: var(--text-dim); font-size: 12px; margin-bottom: 8px; }

/* ============ Layout ============ */
.app { display: flex; height: 100vh; height: 100dvh; overflow: hidden; }

/* Dimmed backdrop behind the mobile sidebar drawer (hidden on desktop) */
.sidebar-backdrop { display: none; }

.sidebar {
  width: 240px; flex-shrink: 0; display: flex; flex-direction: column;
  background: var(--bg-panel); border-right: 1px solid var(--border);
  padding: 16px 14px; gap: 18px;
  overflow-y: auto; min-height: 0;
}
.sidebar-header { display: flex; align-items: center; gap: 10px; }
.logo-seal {
  font-family: 'Noto Serif SC', serif;
  width: 34px; height: 34px; line-height: 34px; text-align: center;
  background: var(--accent-strong); color: #fff; font-size: 19px; border-radius: 6px;
}
.logo-text { font-weight: 700; font-size: 16px; flex: 1; }
.sidebar-close { display: none; }

.compose-btn { padding: 12px; font-weight: 500; }

.sidebar-section { display: flex; flex-direction: column; gap: 6px; }
.sidebar-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--text-dim);
}
.sort-select {
  padding: 9px 10px; border: 1px solid var(--border); border-radius: 6px;
  background: var(--bg); color: var(--text); font-family: inherit; font-size: 13px;
  max-width: 100%;
}

/* Floating account menu */
.account-picker { position: relative; }
.account-btn {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 9px 10px; border: 1px solid var(--border); border-radius: 6px;
  background: var(--bg); color: var(--text); font-family: inherit; font-size: 13px;
  cursor: pointer; text-align: left;
}
.account-btn:hover { background: var(--bg-hover); }
.account-btn .acc-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acc-caret { color: var(--text-dim); font-size: 11px; }
.acc-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
.account-menu {
  position: absolute; top: calc(100% + 6px); left: 0; right: -60px; z-index: 60;
  background: var(--bg-panel); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: var(--shadow); padding: 6px; display: flex; flex-direction: column; gap: 2px;
  max-height: 60vh; overflow-y: auto; -webkit-overflow-scrolling: touch;
  max-width: calc(100vw - 24px);
}
.account-menu-item {
  display: flex; align-items: center; gap: 9px; width: 100%;
  border: none; background: transparent; color: var(--text);
  padding: 9px 10px; border-radius: 6px; cursor: pointer;
  font-family: inherit; font-size: 13px; text-align: left;
}
.account-menu-item:hover { background: var(--bg-hover); }
.account-menu-item.active { background: var(--accent-soft); color: var(--accent); font-weight: 500; }
.account-menu-item .acc-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acc-check { color: var(--accent); }

.ei-account {
  display: inline-block; font-size: 11px; border: 1px solid;
  border-radius: 10px; padding: 0 8px; margin-right: 6px; line-height: 16px;
}
.ei-brand { font-weight: 600; }
.ei-fav {
  width: 16px; height: 16px; border-radius: 4px;
  vertical-align: -3px; margin-right: 7px;
  background: var(--bg-hover);
}

.folder-list { list-style: none; display: flex; flex-direction: column; gap: 2px; }
.folder-item {
  display: flex; width: 100%; text-align: left; gap: 8px; align-items: center;
  border: none; background: transparent; color: var(--text);
  padding: 8px 10px; border-radius: 6px; cursor: pointer;
  font-family: inherit; font-size: 14px;
}
.folder-item:hover { background: var(--bg-hover); }
.folder-item.active { background: var(--accent-soft); color: var(--accent); font-weight: 500; }

.sidebar-footer {
  margin-top: auto; display: flex; justify-content: space-around;
  border-top: 1px solid var(--border); padding-top: 12px;
}

/* ============ Main ============ */
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }

.topbar {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 18px; border-bottom: 1px solid var(--border);
  background: var(--bg-panel);
  flex-wrap: wrap; row-gap: 6px; flex-shrink: 0;
}
.menu-btn { display: none; }
.searchbar {
  flex: 1; display: flex; align-items: center;
  background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
  padding: 0 4px 0 12px; max-width: 520px;
}
.searchbar input {
  flex: 1; border: none; background: transparent; color: var(--text);
  padding: 10px 0; font-family: inherit; font-size: 14px; outline: none;
}
@media (max-width: 900px) {
  .topbar { flex-wrap: wrap; row-gap: 6px; }
}

/* Enable-notifications banner (shown until the device is subscribed or dismissed) */
.push-banner {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 14px; font-size: 13px;
  background: color-mix(in srgb, var(--accent) 12%, var(--bg-panel));
  border-bottom: 1px solid var(--border); color: var(--text);
}
.push-banner .btn { padding: 5px 12px; font-size: 13px; }
#btn-push.push-off { opacity: 0.55; }

/* Calendar: Month / Agenda tabs + agenda list */
.cal-tabs { display: flex; gap: 4px; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 3px; }
.cal-tab { border: none; background: transparent; color: var(--text-dim); padding: 5px 14px; border-radius: 6px; cursor: pointer; font-family: inherit; font-size: 13px; font-weight: 600; }
.cal-tab.active { background: var(--accent-strong); color: #fff; }
.cal-agenda { display: flex; flex-direction: column; gap: 14px; padding: 4px 2px 24px; overflow-y: auto; }
.ag-day { border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px; background: var(--bg-panel); }
.ag-day.ag-today { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.ag-day.ag-past { opacity: 0.75; }
.ag-date { font-weight: 700; font-size: 13px; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
.ag-count { background: var(--accent-strong); color: #fff; border-radius: 10px; padding: 1px 8px; font-size: 12px; font-weight: 700; }

/* Booking.com per-email tags */
.bk-tag {
  display: inline-block; font-size: 11px; font-weight: 700;
  color: var(--bt); border: 1px solid var(--bt); border-radius: 10px;
  padding: 1px 7px; background: color-mix(in srgb, var(--bt) 10%, transparent);
  vertical-align: 1px;
}
.push-banner .pb-close { margin-left: auto; }

.ic { vertical-align: -3px; margin-right: 2px; flex-shrink: 0; }
.folder-item .ic { margin-right: 4px; }
.smart-fab svg { display: block; margin: auto; }

.content { flex: 1; display: flex; min-height: 0; }
.content.single { align-items: center; justify-content: center; }

/* ============ Email list ============ */
.email-list-pane {
  width: 400px; flex-shrink: 0; display: flex; flex-direction: column;
  border-right: 1px solid var(--border); background: var(--bg-panel);
  min-height: 0;
}
.list-toolbar {
  display: flex; align-items: baseline; gap: 8px;
  padding: 14px 18px 10px; border-bottom: 1px solid var(--border);
}
.list-title { font-weight: 700; font-size: 16px; }
.list-count { color: var(--text-dim); font-size: 12px; }

.email-list {
  list-style: none; overflow-y: auto; flex: 1;
  -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
}
/* smooth deep scrolling: off-screen rows skip layout/paint entirely, so a
   1,000-row list scrolls like a 20-row one */
/* contain-intrinsic-size is the height reported for rows that are SKIPPED by
   content-visibility — it is a scroll-height estimate, not layout. Worth being
   explicit about, because it is what makes this row hard to measure: querying
   an off-screen .email-item returns this number and not the real height. A
   rendered two-line row measures 66px, so 66px is the honest estimate; raising
   it only inflates the scrollbar. */
.email-item { content-visibility: auto; contain-intrinsic-size: auto 66px; }
button, .email-item { -webkit-tap-highlight-color: transparent; }

/* desktop hover quick-actions: read/archive/delete straight from the row */
.row-quick { display: none; }
@media (hover: hover) and (min-width: 641px) {
  .row-quick {
    position: absolute; right: 46px; top: 50%; transform: translateY(-50%);
    display: flex; gap: 2px; opacity: 0;
    background: var(--bg-panel); border-radius: 8px; padding: 2px;
    transition: opacity 0.12s;
  }
  .email-item:hover .row-quick { opacity: 1; }
  .row-quick button {
    border: none; background: transparent; color: var(--text-dim);
    padding: 6px 7px; border-radius: 6px; cursor: pointer;
  }
  .row-quick button:hover { background: var(--bg-hover); color: var(--text); }
  .row-quick button:last-child:hover { color: var(--accent); }
}

/* desktop reading column: comfortable line lengths, not wall-to-wall text */
@media (min-width: 900px) {
  /* width:100% is NOT redundant here, and leaving it out was a real bug for
     every desktop reader.
     .viewer-body is a flex ITEM of .email-viewer-pane, which is a COLUMN flex
     container — so the horizontal axis is the CROSS axis. A flex item with an
     auto margin on the cross axis does not stretch: its used cross size falls
     back to fit-content. `margin: 0 auto` is therefore not "centre a 920px
     column", it is "shrink this to its content and centre that". The column
     collapsed to the width of the widest line it happened to contain — the
     subject — which on a 1500px pane was roughly 350px, so the message body
     was squeezed into a quarter of the space with the rest left blank, and
     any email wider than that was clipped with its own scrollbar.
     An explicit width restores the stretch the auto margins suppressed;
     max-width still caps the line length and the auto margins still centre
     what is left over. */
  /* The 920px cap was a readability choice — prose is easiest to read at
     roughly 65-90 characters a line, and a full-width paragraph on a 1500px
     display makes the eye travel back to a line it has already read.
     It is dropped anyway, on the owner's call, for a reason that beats the
     rule: mail is mostly NOT prose. It is newsletters and receipts built on
     fixed-width tables, and capping the column made fitFrame scale those down
     to fit — so the cap was not protecting line length, it was shrinking the
     type. Full width, and the message decides its own measure. */
  /* Full width for the CONTAINER — the message inside is scaled as a whole by
     fitFrame and centres itself in whatever room it is given. */
  .viewer-body { width: 100%; }
  .viewer-html-frame { display: block; margin: 0 auto; }
  .email-item { padding-top: 9px; padding-bottom: 9px; }
}
.email-item {
  padding: 12px 18px; border-bottom: 1px solid var(--border);
  cursor: pointer; display: flex; flex-direction: column; gap: 3px;
}
.email-item:hover { background: var(--bg-hover); }
.email-item.selected { background: var(--bg-active); }
.email-item.unread .ei-from, .email-item.unread .ei-subject { font-weight: 700; }
.ei-top { display: flex; justify-content: space-between; gap: 10px; }
.ei-from { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ei-date { font-size: 12px; color: var(--text-dim); flex-shrink: 0; }
.ei-subject { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ei-badges { font-size: 12px; color: var(--accent); }
.email-empty { padding: 40px 20px; text-align: center; color: var(--text-dim); }
.list-sep {
  padding: 7px 18px 5px; font-size: 11px; font-weight: 700; letter-spacing: 0.07em;
  text-transform: uppercase; color: var(--text-dim); background: var(--bg);
  border-bottom: 1px solid var(--border);
}

/* Block the browser's own horizontal-swipe history navigation — the system
   back gesture is handled in-app (popstate closes viewer/compose/sidebar) */
html, body { overscroll-behavior-x: none; }
.email-list, .viewer-body { touch-action: pan-y; }

/* undo-send hold bar */
.undo-bar {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(18px + env(safe-area-inset-bottom, 0px)); z-index: 95;
  display: flex; align-items: center; gap: 14px;
  background: var(--text); color: var(--bg);
  padding: 10px 12px 10px 18px; border-radius: 10px;
  font-size: 13px; font-weight: 600;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
  animation: sheetUp 0.2s ease;
}
.undo-bar .ub-undo {
  background: var(--accent-strong); color: #fff; border: none;
  padding: 7px 16px; border-radius: 8px; font-weight: 700; cursor: pointer;
  min-height: 34px;
}

/* ============ message action sheet ============ */
.sheet-backdrop {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(0, 0, 0, 0.45);
}
.action-sheet {
  position: fixed; z-index: 91;
  left: 0; right: 0; bottom: 0;
  background: var(--bg-panel);
  border-radius: 16px 16px 0 0;
  border: 1px solid var(--border); border-bottom: none;
  padding: 4px 12px calc(10px + env(safe-area-inset-bottom, 0px));
  max-height: 62vh; overflow-y: auto;
  /* Belt and braces: overflow-y alone makes the X axis scrollable too, which is
     how a too-wide child turned into a sheet you could swipe sideways. Nothing
     in a bottom sheet should ever scroll horizontally. */
  overflow-x: hidden;
  animation: sheetUp 0.22s cubic-bezier(0.2, 0.9, 0.3, 1);
  box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.25);
}
@keyframes sheetUp { from { transform: translateY(45%); opacity: 0.3; } to { transform: none; opacity: 1; } }
.action-sheet { animation-timing-function: cubic-bezier(0.34, 1.25, 0.5, 1); }

/* motion: sheet contents cascade in */
.sheet-quick .sq-btn { animation: sqPop 0.3s cubic-bezier(0.3, 1.4, 0.5, 1) backwards; }
.sheet-quick .sq-btn:nth-child(1) { animation-delay: 0.03s; }
.sheet-quick .sq-btn:nth-child(2) { animation-delay: 0.06s; }
.sheet-quick .sq-btn:nth-child(3) { animation-delay: 0.09s; }
.sheet-quick .sq-btn:nth-child(4) { animation-delay: 0.12s; }
.sheet-quick .sq-btn:nth-child(5) { animation-delay: 0.15s; }
.sheet-quick .sq-btn:nth-child(6) { animation-delay: 0.18s; }
@keyframes sqPop { from { transform: scale(0.7); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.sheet-list .sheet-item { animation: siIn 0.24s ease backwards; }
.sheet-list .sheet-item:nth-child(1) { animation-delay: 0.05s; }
.sheet-list .sheet-item:nth-child(2) { animation-delay: 0.08s; }
.sheet-list .sheet-item:nth-child(3) { animation-delay: 0.11s; }
.sheet-list .sheet-item:nth-child(4) { animation-delay: 0.14s; }
.sheet-list .sheet-item:nth-child(5) { animation-delay: 0.17s; }
.sheet-list .sheet-item:nth-child(6) { animation-delay: 0.2s; }
.sheet-list .sheet-item:nth-child(7) { animation-delay: 0.23s; }
.sheet-list .sheet-item:nth-child(n+8) { animation-delay: 0.26s; }
@keyframes siIn { from { transform: translateY(10px); opacity: 0; } to { transform: none; opacity: 1; } }

/* motion: tactile press feedback + icon life */
.sq-btn, .sheet-item { transition: transform 0.12s ease, background 0.15s; }
.sq-btn:active { transform: scale(0.92); }
.sheet-item:active { transform: scale(0.985); }
.sq-btn .ic { width: 22px; height: 22px; transition: transform 0.15s cubic-bezier(0.3, 1.5, 0.5, 1); }
.sq-btn:hover .ic { transform: translateY(-2px); }
.si-ic .ic { width: 17px; height: 17px; vertical-align: -3px; }

/* motion: swipe cue pops at each action threshold */
.swipe-cue { display: inline-flex; align-items: center; gap: 8px; animation: cuePop 0.25s cubic-bezier(0.3, 1.6, 0.5, 1); }
.swipe-cue .ic { width: 20px; height: 20px; }
@keyframes cuePop { from { transform: scale(0.5); opacity: 0.3; } to { transform: scale(1); opacity: 1; } }
.sheet-handle {
  width: 38px; height: 4px; border-radius: 2px;
  background: var(--border); margin: 4px auto 10px;
}
.sheet-head { margin-bottom: 7px; display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.sheet-head .sheet-from { flex: 0 1 auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sheet-head .sheet-subj { flex: 1 1 0; }
.sheet-from { display: block; font-weight: 700; font-size: 13px; }
.sheet-subj {
  display: block; color: var(--text-dim); font-size: 12px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sheet-quick {
  display: flex; gap: 5px; margin-bottom: 8px;
  overflow-x: auto; scrollbar-width: none; padding-bottom: 2px;
}
.sheet-quick::-webkit-scrollbar { display: none; }
.sq-btn {
  flex: 1 0 46px; min-height: 46px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--bg); color: var(--text); cursor: pointer; font-family: inherit;
}
.sq-btn:hover { background: var(--bg-hover); }
.sq-btn.danger { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
.sq-ic { font-size: 17px; }
.sq-lb { font-size: 10px; }
/* ONE column, at every width — and the reasoning matters, because two columns
   looked like the tidier choice and was not.
   The sheet is never wide: viewport-width on a phone (320-430px) and a fixed
   400px centred dialog from 641px up. Minus padding that is at most ~376px.
   Split in two it gives ~184px columns, while the longest label — "Download
   original (.eml, all attachments)" — needs 283px. So two columns truncate on
   EVERY screen this app runs on; measured at 375px and 768px.
   The old rule was `1fr 1fr`, and a bare 1fr means minmax(AUTO, 1fr): the auto
   minimum refuses to shrink below the widest item, so instead of truncating,
   column one grew to 283px, the grid came out 282.789px + 204.695px, and the
   sheet overflowed a 375px phone by 134px. Because .action-sheet sets
   overflow-y, the X axis became scrollable too — so the sheet slid sideways and
   cut labels off at BOTH edges, which is what a real phone screenshot showed.
   minmax(0, ...) is what makes the column able to shrink at all; keep it even
   though there is one column now, or restoring two would restore the bug. */
.sheet-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1px 8px; }
.sheet-item {
  display: flex; align-items: center; gap: 8px;
  border: none; background: transparent; color: var(--text);
  padding: 6px 4px; min-height: 32px; cursor: pointer;
  font-family: inherit; font-size: 12px; text-align: left;
  border-radius: 6px;
}
.sheet-item:hover { background: var(--bg-hover); }
.sheet-item.danger { color: var(--accent); }
.si-ic { width: 20px; text-align: center; flex: 0 0 auto; }
.si-ic .ic { opacity: 0.85; }
.si-lb { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.si-caret { color: var(--text-dim); }
.mm-ctl { display: flex; align-items: center; gap: 2px; }
.mm-ctl .btn { min-height: 30px; padding: 3px 10px; font-size: 12px; }
.mm-ctl .icon-btn { width: 30px; height: 30px; min-width: 30px; min-height: 30px; font-size: 13px; }
@media (min-width: 641px) {
  /* desktop: same sheet as a centered action dialog */
  .action-sheet {
    left: 50%; right: auto; bottom: auto; top: 50%;
    transform: translate(-50%, -50%);
    width: 400px; border-radius: 16px; border-bottom: 1px solid var(--border);
    max-height: 80vh;
    animation: popIn 0.18s ease;
  }
  .sheet-handle { display: none; }
}

/* visible reply history at the end of the compose view */
.compose-quote-view {
  /* No border, no radius, no fill. The dashed box drew a frame around the
     quoted original and a second frame inside it, which made the compose screen
     read as three stacked containers instead of one message. The label already
     says what this is; the rule below is the only separation it needs. */
  /* Not even a rule. The label already says what follows; a line above it was
     the last edge left drawing the reply as a compartment. */
  margin: 4px 14px 10px; padding: 10px 0 0;
  max-height: 32vh; overflow: auto; background: transparent;
}
.cqv-label {
  font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-dim); margin-bottom: 8px; text-align: center;
}
/* The label is now the control that opens the quote, so it is a real button at
   a real target size — collapsed, this strip is the whole block. */
.cqv-toggle {
  /* flex, not block: with `display:block` the measured target came out 41px
     despite min-height:44px, and this app holds every control to 44. */
  display: flex; align-items: center; width: 100%; min-height: 44px;
  background: none; border: none; padding: 4px 0; text-align: left; cursor: pointer;
  gap: 6px;
  font-family: inherit; font-size: 11px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--text-dim);
}
.cqv-toggle:hover { color: var(--text); }
.cqv-caret { display: inline-block; transition: transform 0.15s; }
.compose-quote-view:not(.is-collapsed) .cqv-caret { transform: rotate(90deg); }
/* Collapsed it must not keep its 32vh box or its scroll — it is one line. */
.compose-quote-view.is-collapsed { max-height: none; overflow: visible; }
.compose-quote-view.is-collapsed .cqv-body { display: none; }
.cqv-attr { font-size: 12px; color: var(--text-dim); margin-bottom: 6px; }
/* now an <iframe>, not a <blockquote>: the quoted original is untrusted sender
   HTML and renders sandboxed, like it does in the reading view. fitFrame() sets
   the height, so it must not be constrained here. */
.cqv-quote {
  /* The left rule went too: it was the inner edge of the same box. */
  display: block; width: 100%; border: 0; margin: 0; padding-left: 0;
  background: transparent;
}

/* compose attachments */
.compose-atts { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 14px; }
.compose-atts:not(:empty) { padding-bottom: 8px; }
.compose-att-chip {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--border); border-radius: 16px;
  padding: 4px 6px 4px 12px; font-size: 13px; background: var(--bg);
}
.compose-att-chip i { color: var(--text-dim); font-style: normal; font-size: 11px; }
.compose-att-chip .ca-x {
  border: none; background: transparent; color: var(--text-dim);
  cursor: pointer; font-size: 15px; padding: 0 4px; border-radius: 50%;
}
.compose-att-chip .ca-x:hover { color: var(--accent); }

/* conversation threading */
.tcount {
  display: inline-block; margin-left: 6px; padding: 0 7px;
  border-radius: 10px; font-size: 11px; font-weight: 700;
  background: var(--bg-hover); color: var(--text-dim);
  border: 1px solid var(--border); vertical-align: 1px;
}
.email-item.unread .tcount { color: var(--accent); border-color: var(--accent); }
.thread-note { color: var(--text-dim); font-size: 13px; margin: 4px 0 14px; }
.thread-card {
  border: 1px solid var(--border); border-radius: 10px;
  margin-bottom: 10px; background: var(--bg-panel); overflow: hidden;
}
.thread-card.unread { border-left: 3px solid var(--accent); }
.tc-head {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; cursor: pointer;
}
.tc-head:hover { background: var(--bg-hover); }
.tc-avatar {
  width: 30px; height: 30px; border-radius: 50%; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-weight: 700; font-size: 14px;
}
.tc-from { font-weight: 600; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.thread-card.unread .tc-from { font-weight: 700; }
.tc-date { color: var(--text-dim); font-size: 12px; flex: 0 0 auto; }
.tc-body { padding: 0 12px 10px; }
.tc-actions { padding: 0 12px 10px; }
.tc-actions .btn { min-height: 32px; padding: 4px 12px; font-size: 13px; }
.tc-atts { margin-top: 8px; color: var(--text-dim); font-size: 13px; }

.app-version {
  margin-left: auto; align-self: center;
  font-size: 11px; color: var(--text-dim); letter-spacing: 0.04em;
}

/* swipe actions on message rows (touch) */
.email-list { position: relative; }
.email-item.swiping {
  position: relative; z-index: 1;
  background: var(--bg-panel);
  will-change: transform;
}
.swipe-bg {
  position: absolute; left: 0; right: 0; z-index: 0;
  display: flex; align-items: center; padding: 0 18px;
  color: #fff; font-weight: 700; font-size: 13px;
  white-space: nowrap; overflow: hidden;
}
.swipe-bg[data-dir="left"] { justify-content: flex-end; }
.swipe-manage .swipe-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 6px 0;
}
.swipe-manage .swipe-row-label { font-size: 13px; color: var(--text-dim); }
.swipe-manage select {
  background: var(--bg); color: var(--text); border: 1px solid var(--border);
  border-radius: 8px; padding: 8px 10px; font-family: inherit; font-size: 13px;
  max-width: 55%;
}

/* remote-image tracking-protection banner in the viewer */
.img-banner {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 10px 0; padding: 8px 12px; font-size: 13px;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--bg-panel); color: var(--text-dim);
}
.img-banner .btn { min-height: 32px; padding: 4px 12px; font-size: 13px; }

/* infinite-scroll sentinel: invisible trigger zone at the list bottom;
   shows a quiet "Loading older emails…" line while the next batch fetches */
.scroll-sentinel {
  text-align: center; color: var(--text-dim); font-size: 12px;
  padding: 8px 0 calc(12px + env(safe-area-inset-bottom, 0px));
}
.scroll-sentinel:empty { display: none; }

/* ============ Viewer ============ */
.email-viewer-pane { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.viewer-toolbar {
  display: flex; align-items: center; gap: 4px;
  padding: 10px 16px; border-bottom: 1px solid var(--border); background: var(--bg-panel);
}
.spacer { flex: 1; }
.tb-sep { width: 1px; height: 20px; background: var(--border); margin: 0 4px; }
.tb-label { font-size: 13px; margin-left: 4px; }
#btn-pin.pinned { color: var(--accent); background: var(--accent-soft); }
.more-wrap { position: relative; }
.more-menu { left: auto; right: 0; top: calc(100% + 6px); min-width: 230px; transform-origin: top right; }

/* AI-native message tools */
.ai-bar { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
.ai-chip {
  border: 1px solid var(--border); background: var(--bg-panel); color: var(--text);
  padding: 7px 14px; border-radius: 16px; cursor: pointer;
  font-family: inherit; font-size: 13px; font-weight: 500;
  transition: all 0.15s cubic-bezier(0.2, 0.9, 0.3, 1.2);
}
.ai-chip:hover {
  border-color: var(--accent); color: var(--accent);
  background: var(--accent-soft); transform: translateY(-1px);
}
.ai-ask { display: flex; gap: 8px; margin-bottom: 14px; }
.ai-ask input {
  flex: 1; padding: 10px 14px; border: 1px solid var(--border); border-radius: 16px;
  background: var(--bg-panel); color: var(--text); font-family: inherit; font-size: 13px;
  outline: none; transition: border-color 0.15s;
}
.ai-ask input:focus { border-color: var(--accent); }
.ai-ask-btn { border-radius: 16px; padding: 8px 20px; }
.ai-answer { font-size: 13px; line-height: 1.6; }

.ai-out {
  border: 1px solid var(--border); border-left: 3px solid var(--accent);
  border-radius: 10px; padding: 12px 16px; margin-bottom: 16px;
  background: var(--bg-panel); animation: fadeUp 0.3s ease;
}
.ai-out-title {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--accent); font-weight: 700; margin-bottom: 8px;
}
.ai-point { font-size: 13px; line-height: 1.55; margin-bottom: 4px; }
.ai-reply {
  display: block; width: 100%; text-align: left; margin-bottom: 6px;
  border: 1px solid var(--border); background: var(--bg); color: var(--text);
  padding: 9px 13px; border-radius: 8px; cursor: pointer;
  font-family: inherit; font-size: 13px; transition: all 0.15s;
}
.ai-reply:hover { border-color: var(--accent); background: var(--accent-soft); transform: translateX(3px); }
.viewer-body { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 24px 32px; -webkit-overflow-scrolling: touch; }
.viewer-body img { max-width: 100%; height: auto; }
.viewer-subject { font-size: 21px; font-weight: 700; margin-bottom: 14px; }
.viewer-meta {
  display: flex; align-items: center; gap: 12px; margin-bottom: 20px;
  padding-bottom: 16px; border-bottom: 1px solid var(--border);
}
.viewer-avatar {
  width: 42px; height: 42px; border-radius: 50%; flex-shrink: 0;
  background: var(--accent-strong); color: #fff; display: flex;
  align-items: center; justify-content: center; font-weight: 700; font-size: 17px;
}
.viewer-from { font-weight: 500; font-size: 14px; }
.viewer-detail { color: var(--text-dim); font-size: 12px; }
/* border is set further down, where the reason for it is written out. It was
   `none` here and is left off deliberately rather than restated, so there is
   one declaration to find rather than two that disagree. */
.viewer-html-frame { width: 100%; max-width: 100%; }
/* the message area itself never scrolls sideways — wide content is scaled to fit */
#viewer-content, .tc-body, .viewer-body { overflow-x: hidden; }
.viewer-text { white-space: pre-wrap; font-size: 14px; line-height: 1.6; }
/* "2 attachments — Save all", then the files themselves. */
.att-head {
  display: flex; align-items: center; gap: 10px;
  margin-top: 24px; font-size: 13px; color: var(--text-dim);
}
.att-saveall {
  background: none; border: none; padding: 8px 2px; min-height: 44px;
  color: var(--accent); font-family: inherit; font-size: 13px; cursor: pointer;
}
.att-saveall:disabled { color: var(--text-dim); cursor: default; }
/* Two per row, as Spark lays them out — a single column of full-width chips
   wastes the half of the screen beside them. */
.viewer-attachments { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 8px; }
.attachment-wrap { flex: 1 1 44%; min-width: 0; display: flex; }
.attachment-wrap .attachment-chip { flex: 1; min-width: 0; }
.attachment-chip .fn { max-width: 100%; width: 100%; }
/* Two lines: the name, then kind and size. The name may be long — it wraps to
   an ellipsis rather than stretching the chip across the message. */
.attachment-chip .fn {
  display: block; max-width: 22ch; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; font-weight: 600;
}
.attachment-chip .fm { display: block; font-style: normal; font-size: 12px; color: var(--text-dim); }
.attachment-chip {
  /* stretch, not flex-start: with flex-start the name is sized to its own
     content, so max-width never binds and a long filename spills outside the
     chip instead of ellipsising inside it. */
  display: inline-flex; flex-direction: column; align-items: stretch; gap: 2px;
  text-align: left; min-width: 0;
  border: 1px solid var(--border); border-radius: 10px; padding: 8px 14px;
  font-size: 13px; text-decoration: none; color: var(--text); background: var(--bg-panel);
}
.attachment-chip:hover { background: var(--bg-hover); }

/* ============ Compose ============ */
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,0.45);
  display: flex; align-items: center; justify-content: center; z-index: 50;
}
.modal {
  background: var(--bg-panel); border-radius: 10px; box-shadow: var(--shadow);
  width: min(680px, 94vw); max-height: 90vh; display: flex; flex-direction: column;
}
.modal-header {
  display: flex; justify-content: space-between; align-items: center;
  padding: 14px 20px; border-bottom: 1px solid var(--border); font-weight: 700;
}
.compose-form { display: flex; flex-direction: column; overflow: hidden; }
.compose-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 20px; border-bottom: 1px solid var(--border);
}
.compose-row label { width: 56px; color: var(--text-dim); font-size: 13px; flex-shrink: 0; }
/* The header's send arrow is a phone affordance — on a desktop the footer's
   Send button is the one that leads, and two would be one too many. */
.hdr-send { display: none; }
@media (max-width: 640px) { .hdr-send { display: inline-flex; } }
/* The Cc/Bcc opener lives in the To row rather than as a row of its own — a
   seventh row is exactly the cost this is trying to remove. */
.cc-toggle {
  flex: none; background: none; border: none; cursor: pointer;
  color: var(--text-dim); font-family: inherit; font-size: 12px;
  padding: 0 4px; min-height: 44px; text-decoration: underline;
}
.cc-toggle:hover { color: var(--text); }
.compose-row input, .compose-row select {
  flex: 1; border: none; background: transparent; color: var(--text);
  font-family: inherit; font-size: 14px; outline: none; padding: 6px 0;
}
/* No max-height. It was 44vh, and a cap on the one element people type into is
   the wrong instinct everywhere: on a phone it fought the 45vh override below,
   in the dialog it kept the body small while the quoted thread took 32vh, and
   in a window of its own it made a full screen pointless. The body GROWS and
   scrolls; what bounds it is the window, which is the user's to size.
   flex:1 with min-height:0 so it can also shrink inside the form without
   pushing the footer out — the pair is what makes a flex child scrollable. */
.compose-body {
  flex: 1; min-height: 200px; overflow-y: auto;
  padding: 16px 20px; font-size: 14px; line-height: 1.6; outline: none;
}
.compose-form { flex: 1; min-height: 0; }
.compose-footer {
  display: flex; align-items: center; gap: 14px;
  padding: 12px 20px; border-top: 1px solid var(--border);
  /* WRAP. This row carries Send, Attach, Later, a nowrap "Follow-up in 3 days"
     label, a status span and the quote note — 450px of content against a 358px
     phone, measured. Without wrapping the row simply ran off the screen: the
     quote note sat at x=435 and the buttons were squeezed to 41px, which is why
     "Send" came out clipped in a real screenshot. */
  flex-wrap: wrap;
}
/* The two messages get their own line rather than competing with the controls
   for the tail of the row — they are the widest things here and the least
   urgent. */
.compose-footer .compose-status,
.compose-footer .compose-quote-note { flex: 1 0 100%; margin: 0; }
/* A button is its label. Letting these shrink is what turned Send into "Sen". */
.compose-footer .btn { flex-shrink: 0; }
.compose-status { color: var(--text-dim); font-size: 13px; }
.later-wrap { position: relative; }
.later-pop { top: auto; bottom: calc(100% + 6px); left: 0; right: auto; min-width: 230px; transform-origin: bottom left; }
.later-custom { display: flex; gap: 6px; padding: 8px 10px; border-top: 1px solid var(--border); }
.later-custom input {
  flex: 1; padding: 7px 8px; border: 1px solid var(--border); border-radius: 6px;
  background: var(--bg); color: var(--text); font-family: inherit; font-size: 13px;
}
.later-custom .btn { padding: 6px 12px; }
.fu-label {
  display: flex; align-items: center; gap: 6px;
  font-size: 13px; color: var(--text-dim); cursor: pointer; white-space: nowrap;
}

/* ============ Misc ============ */
.placeholder-view { text-align: center; color: var(--text-dim); max-width: 380px; padding: 20px; }
.placeholder-glyph { font-size: 52px; margin-bottom: 12px; }
.placeholder-view h2 { color: var(--text); margin-bottom: 8px; }

/* ============ Smart Inbox ============ */
.smart-fab {
  position: fixed; right: 26px; bottom: calc(26px + env(safe-area-inset-bottom, 0px)); z-index: 45;
  width: 56px; height: 56px; border-radius: 50%; border: none; cursor: pointer;
  font-size: 24px; color: #fff;
  background: linear-gradient(135deg, var(--accent), var(--accent-strong));
  box-shadow: 0 4px 20px color-mix(in srgb, var(--accent) 45%, transparent);
  transition: transform 0.18s cubic-bezier(0.2, 0.9, 0.3, 1.4), box-shadow 0.2s;
}
.smart-fab:hover { transform: translateY(-3px) scale(1.06); box-shadow: 0 8px 28px color-mix(in srgb, var(--accent) 60%, transparent); }
.smart-fab:active { transform: scale(0.94); }

.smart-panel {
  position: fixed; right: 26px; bottom: 94px; z-index: 46;
  width: min(400px, calc(100vw - 40px));
  background: var(--bg-panel); border: 1px solid var(--border); border-radius: 16px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
  display: flex; flex-direction: column; max-height: 65vh;
  animation: popIn 0.25s cubic-bezier(0.2, 0.9, 0.3, 1.2); transform-origin: bottom right;
}
.smart-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: 13px 16px; border-bottom: 1px solid var(--border); font-weight: 700;
}
.smart-tabs { display: flex; gap: 4px; padding: 8px 12px; border-bottom: 1px solid var(--border); }
.smart-tab {
  flex: 1; border: 1px solid var(--border); background: transparent; color: var(--text-dim);
  padding: 7px 6px; border-radius: 8px; cursor: pointer; font-family: inherit; font-size: 13px;
  transition: all 0.15s;
}
.smart-tab.active { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); font-weight: 600; }
.smart-count {
  display: inline-block; background: var(--accent-strong); color: #fff; border-radius: 10px;
  font-size: 11px; padding: 0 6px; margin-left: 3px;
}
.smart-list { overflow-y: auto; flex: 1; padding: 6px; -webkit-overflow-scrolling: touch; }
.smart-item {
  display: flex; align-items: center; gap: 6px; padding: 10px 10px;
  border-radius: 10px; cursor: pointer; animation: fadeUp 0.3s ease backwards;
  transition: background 0.15s, transform 0.15s;
}
.smart-item:hover { background: var(--bg-hover); transform: translateX(3px); }
.smart-item-main { flex: 1; min-width: 0; }
.smart-item-subject {
  font-size: 13px; font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.smart-item-meta {
  display: flex; align-items: center; gap: 8px; margin-top: 3px;
  font-size: 11px; color: var(--text-dim);
  overflow: hidden; white-space: nowrap;
}
.smart-due { color: var(--accent); font-weight: 600; }
.smart-foot {
  display: flex; justify-content: space-between; padding: 8px 12px;
  border-top: 1px solid var(--border);
}
.compose-quote-note { color: var(--text-dim); font-size: 12px; margin-left: auto; }

/* Manage accounts */
.accounts-body { padding: 6px 0 0; overflow-y: auto; }
.accounts-list { list-style: none; margin: 0 0 6px; }
.accounts-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 20px; border-bottom: 1px solid var(--border); font-size: 13px;
}
.accounts-row .acc-label { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.account-add-form .btn-ghost { margin: 6px 20px 0; font-size: 13px; }
.acc-advanced { border-top: 1px dashed var(--border); margin-top: 6px; }

.toast {
  position: fixed; bottom: calc(28px + env(safe-area-inset-bottom, 0px)); left: 50%; transform: translateX(-50%);
  background: var(--text); color: var(--bg); padding: 11px 22px;
  border-radius: 8px; font-size: 14px; z-index: 100; box-shadow: var(--shadow);
  max-width: calc(100vw - 32px); width: max-content;
}

/* ============ Motion ============ */
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}
@keyframes slideInRight {
  from { opacity: 0; transform: translateX(28px); }
  to   { opacity: 1; transform: none; }
}
@keyframes popIn {
  from { opacity: 0; transform: scale(0.94) translateY(8px); }
  to   { opacity: 1; transform: none; }
}
@keyframes glowShift {
  0%   { background-position: 0% 50%; }
  50%  { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}
@keyframes sealPulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 45%, transparent); }
  50%      { box-shadow: 0 0 24px 4px color-mix(in srgb, var(--accent) 25%, transparent); }
}

.login-page {
  background-image:
    radial-gradient(circle at 18% 22%, var(--accent-soft), transparent 42%),
    radial-gradient(circle at 82% 78%, var(--accent-soft), transparent 42%);
  background-size: 180% 180%;
  animation: glowShift 16s ease infinite;
}
.login-card { animation: popIn 0.5s cubic-bezier(0.2, 0.9, 0.3, 1.15) backwards; }
.login-mark { animation: sealPulse 4s ease infinite; border-radius: 10px; }

.email-item {
  animation: fadeUp 0.35s ease backwards;
  transition: background 0.15s, transform 0.18s cubic-bezier(0.2, 0.9, 0.3, 1.2), box-shadow 0.2s;
  border-left: 2px solid transparent;
}
.email-item:hover { transform: translateX(5px); border-left-color: var(--accent); }
.email-item { position: relative; }
.row-more {
  position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
  border: 1px solid var(--border); background: var(--bg-panel); color: var(--text-dim);
  border-radius: 8px; padding: 7px 9px; cursor: pointer; opacity: 0;
  transition: opacity 0.15s, background 0.15s, color 0.15s;
}
.email-item:hover .row-more { opacity: 1; }
.row-more:hover { background: var(--bg-hover); color: var(--text); }
@media (hover: none) { .row-more { opacity: 1; border: none; background: transparent; } }
.row-menu { position: fixed; z-index: 80; min-width: 210px; top: auto; left: auto; right: auto; }
.email-item.selected { border-left-color: var(--accent); }

.email-viewer-pane { animation: slideInRight 0.32s cubic-bezier(0.2, 0.9, 0.3, 1.1); }
.viewer-avatar { animation: popIn 0.4s cubic-bezier(0.2, 0.9, 0.3, 1.25) backwards; animation-delay: 0.08s; }
.viewer-subject { animation: fadeUp 0.35s ease backwards; }
.viewer-meta { animation: fadeUp 0.35s ease backwards; animation-delay: 0.05s; }
#viewer-content { animation: fadeUp 0.4s ease backwards; animation-delay: 0.1s; }

.modal-backdrop { animation: fadeUp 0.2s ease; backdrop-filter: blur(3px); }
.modal { animation: popIn 0.32s cubic-bezier(0.2, 0.9, 0.3, 1.2); }
.account-menu { animation: popIn 0.2s cubic-bezier(0.2, 0.9, 0.3, 1.2); transform-origin: top left; }
.toast { animation: fadeUp 0.3s cubic-bezier(0.2, 0.9, 0.3, 1.2); }

.btn, .icon-btn, .folder-item, .account-btn, .account-menu-item {
  transition: background 0.15s, color 0.15s, transform 0.12s, box-shadow 0.2s;
}
.btn:active, .icon-btn:active, .folder-item:active, .account-menu-item:active { transform: scale(0.95); }
.icon-btn:hover { transform: translateY(-1px); }
.folder-item:hover { transform: translateX(3px); }
.btn-primary { box-shadow: 0 2px 10px color-mix(in srgb, var(--accent) 25%, transparent); }
.btn-primary:hover { box-shadow: 0 4px 18px color-mix(in srgb, var(--accent) 45%, transparent); transform: translateY(-1px); }
.compose-btn:hover { transform: translateY(-2px); }
.attachment-chip { transition: transform 0.15s, background 0.15s; }
.attachment-chip:hover { transform: translateY(-2px); }
.sidebar { transition: transform 0.25s cubic-bezier(0.2, 0.9, 0.3, 1.05); }

:root[data-theme="dark"] .email-list-pane,
:root[data-theme="dark"] .sidebar,
:root[data-theme="dark"] .topbar {
  background: linear-gradient(180deg, var(--bg-panel), color-mix(in srgb, var(--bg-panel) 92%, black));
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* ============ Responsive ============ */
/* Tablet: list pane and viewer share space */
@media (max-width: 1024px) {
  .email-list-pane { width: 340px; }
}
@media (min-width: 641px) and (max-width: 860px) {
  .email-list-pane { width: 320px; }
}

/* Phone */
@media (max-width: 640px) {
  /* --- Sidebar: off-canvas drawer + dimmed backdrop --- */
  .sidebar {
    position: fixed; inset: 0 auto 0 0; z-index: 40; transform: translateX(-100%);
    transition: transform 0.22s cubic-bezier(0.3, 1, 0.4, 1); width: min(48vw, 200px);
    padding: 6px 8px;
    overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
    padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  }
  .sidebar.open { transform: translateX(0); box-shadow: 8px 0 32px rgba(0, 0, 0, 0.28); }
  /* minimal drawer: navigation only — composing lives on the ✏ FAB */
  .sidebar .compose-btn { display: none; }
  .sidebar .folder-item, .sidebar .account-menu-item { min-height: 38px; font-size: 13px; }
  .sidebar .sidebar-label { font-size: 10px; margin-bottom: 2px; }
  .sidebar .apps-manage-btn { font-size: 12px; padding: 6px 8px; min-height: 34px; }
  .sidebar .sidebar-section { margin-bottom: 8px; }
  .sidebar .sidebar-header { margin-bottom: 6px; }
  .sidebar-close { display: block; }
  .sidebar-backdrop {
    display: block; position: fixed; inset: 0; z-index: 39;
    background: rgba(0, 0, 0, 0.45); opacity: 0; pointer-events: none;
    transition: opacity 0.2s ease;
  }
  .sidebar.open + .sidebar-backdrop { opacity: 1; pointer-events: auto; }
  /* keep the account dropdown inside the drawer / viewport */
  .account-picker .account-menu { left: 0; right: 0; }

  /* --- Topbar: hamburger + search fit --- */
  .menu-btn { display: block; }
  .sort-select { flex: 0 1 auto; min-width: 0; max-width: 44vw; font-size: 12px; padding: 8px 6px; }
  .topbar { gap: 8px; padding: 10px 10px; }
  .searchbar { max-width: none; min-width: 0; }

  /* --- Touch targets (>=44px) --- */
  .icon-btn {
    min-width: 44px; min-height: 44px;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .btn { min-height: 44px; }
  .folder-item, .account-btn, .account-menu-item { min-height: 44px; }
  .smart-tab { min-height: 44px; }
  .ai-chip, .ai-reply, .attachment-chip { min-height: 40px; }

  /* --- iOS: 16px inputs prevent zoom-on-focus --- */
  .login-card input, .searchbar input,
  .compose-row input, .compose-row select, .compose-body { font-size: 16px; }

  /* --- List pane fills width; viewer replaces it --- */
  .email-list-pane { width: 100%; border-right: none; }
  .content.viewing .email-list-pane { display: none; }
  /* reserve room so the FAB never covers the "Older" pager button */
  .scroll-sentinel { padding-right: 84px; } /* keep the FAB clear of the loading line */

  /* --- Viewer toolbar: wraps gracefully, .more-wrap dropdown intact --- */
  .viewer-toolbar { flex-wrap: wrap; gap: 2px; padding: 4px 8px; }
  .tb-sep { display: none; }
  .more-menu { max-width: calc(100vw - 16px); }
  .viewer-body { padding: 16px 14px; }
  .viewer-subject { font-size: 18px; }

  /* --- Compose: full-screen sheet --- */
  .modal-backdrop { align-items: stretch; justify-content: stretch; }
  .modal {
    width: 100%; max-width: 100%; border-radius: 0;
    height: 100vh; height: 100dvh; max-height: none;
  }
  .modal-header { padding-top: calc(14px + env(safe-area-inset-top, 0px)); }
  .compose-form { flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; }
  /* ---- the compose header, the way Spark does it ----
     Spark shows no labelled field grid: the From address is a small line in the
     header, TO is one row, and the subject is simply bold text. Qmail spent a
     56px label column and a 58px row on each of From, To and Subject before a
     word could be typed. The labels go (the placeholder and the content say
     what each field is), the rows tighten, and the subject reads as the
     message's title rather than as another form field. */
  /* ---- built from the Spark screenshots ----
     Header: the act on the left ("Replying") with the sending identity beneath
     it, and send as an arrow on the right. Then TO as one row with the
     recipient in a pill, the subject as bold text with no label, and the
     writing surface. Nothing is boxed, nothing is labelled twice. */
  .compose-row { padding: 4px 14px; gap: 8px; border-bottom: none; }
  .compose-row label { display: none; }
  .compose-row input, .compose-row select { min-height: 40px; }
  #compose-subject { font-weight: 600; font-size: 16px; }

  /* From reads as the header's second line: it belongs to "Replying" above it,
     not to the fields below, so it loses the row's separation and takes the
     accent the way Spark's does. */
  .compose-row:has(#compose-from) { padding: 0 14px 6px; margin-top: -6px; }
  #compose-from {
    color: var(--accent); font-size: 13px; min-height: 26px;
    border: none; padding-left: 0; background: transparent;
  }
  /* TO keeps its label — it is the one field where "who this goes to" must be
     unmistakable — and the recipient sits in a pill, as it does in Spark. */
  .compose-row:has(#compose-to) label { display: block; width: auto; font-size: 12px; letter-spacing: .04em; }
  #compose-to {
    border: 1px solid var(--border-strong); border-radius: 20px;
    padding: 8px 14px; min-height: 40px;
  }
  .compose-row:has(#compose-to) { border-bottom: 1px solid var(--border); padding-bottom: 8px; }

  /* Spark's order: close on the LEFT, the act in the middle-left, send hard
     right where the sending thumb is. The markup order is title, popout, max,
     send, close — reordered here rather than moved, so nothing that reads the
     DOM has to change. */
  .compose-modal .modal-header { justify-content: flex-start; gap: 4px; }
  .compose-modal .modal-header #compose-close { order: -1; margin-right: 4px; }
  .compose-modal .modal-header span { order: 0; flex: 1; }
  .compose-modal .modal-header #compose-popout,
  .compose-modal .modal-header #compose-max { order: 1; }
  .compose-modal .modal-header .hdr-send { order: 2; color: var(--accent); }
  /* The footer becomes a row of icons above the keyboard rather than a row of
     labelled buttons: the actions are the same, the chrome is a third of it. */
  .compose-footer .btn { padding: 0 10px; min-width: 44px; }
  .compose-footer { gap: 6px; padding: 6px 10px calc(6px + env(safe-area-inset-bottom, 0px)); }
  .compose-body { max-height: none; min-height: 45vh; padding: 14px; flex: 1 0 auto; }
  /* The reply is the work, the history is reference — so the history yields.
     Measured at 390x844 with the quote open: reply 181px against quote 254px,
     i.e. the mail being answered took more of the screen than the answer. */
  .compose-quote-view { flex: 0 1 auto; max-height: 22vh; }
  .compose-footer {
    position: sticky; bottom: 0; margin-top: auto; background: var(--bg-panel);
    padding: 12px 14px calc(12px + env(safe-area-inset-bottom, 0px));
  }

  /* --- Smart Inbox: bottom sheet + FAB clear of edges --- */
  .smart-fab {
    right: 14px; bottom: calc(14px + env(safe-area-inset-bottom, 0px));
    width: 52px; height: 52px;
  }
  .smart-panel {
    left: 0; right: 0; bottom: 0; width: 100%;
    border-radius: 16px 16px 0 0;
    border-left: none; border-right: none; border-bottom: none;
    max-height: 80vh; max-height: 80dvh;
    transform-origin: bottom center;
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }

  /* toast sits above the FAB */
  .toast { bottom: calc(82px + env(safe-area-inset-bottom, 0px)); }
}

/* Very narrow phones */
@media (max-width: 400px) {
  .tb-label { display: none; }
  .topbar { padding: 8px 6px; gap: 6px; }
  .viewer-toolbar .icon-btn.tb { min-width: 40px; }
  .login-card { padding: 36px 24px; width: min(340px, calc(100vw - 32px)); }
}

/* ============ Calendar ============ */
.cal-wrap {
  height: 100%;
  display: flex;
  flex-direction: column;
  padding: 18px 22px;
  overflow-y: auto;
  animation: fadeUp 0.35s ease both;
}
.cal-header {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 10px;
}
.cal-header h2 { flex: 1; font-size: 20px; font-weight: 600; }
.cal-nav { display: flex; align-items: center; gap: 4px; }
.cal-nav .icon-btn { font-size: 20px; width: 34px; height: 34px; }
.cal-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  font-size: 12px;
  color: var(--text-dim);
  margin-bottom: 12px;
}
.cal-leg[data-c="event"] { color: #3577d1; }
.cal-leg[data-c="reminder"] { color: #b07d1e; }
.cal-leg[data-c="snooze"] { color: #9c53c9; }
.cal-leg[data-c="send"] { color: #2e9e63; }
.cal-leg[data-c="deadline"] { color: var(--danger); }
.cal-dow {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 6px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  padding: 0 2px 6px;
}
.cal-dow span { text-align: center; }
.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 6px;
}
.cal-cell {
  min-height: 92px;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 6px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 3px;
  overflow: hidden;
  transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
}
.cal-cell:hover { border-color: var(--accent); transform: translateY(-1px); box-shadow: var(--shadow); }
.cal-empty { background: transparent; border-color: transparent; cursor: default; }
.cal-empty:hover { border-color: transparent; transform: none; box-shadow: none; }
.cal-num { font-size: 12px; color: var(--text-dim); }
.cal-today { border-color: var(--accent); background: var(--accent-soft); }
.cal-today .cal-num { color: var(--accent); font-weight: 700; }
.cal-chip {
  --cc: #3577d1;
  font-size: 11px;
  line-height: 1.35;
  padding: 2px 5px;
  border-radius: 4px;
  background: color-mix(in srgb, var(--cc) 16%, var(--bg-panel));
  border-left: 3px solid var(--cc);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--text);
}
.cal-chip b { font-weight: 600; color: var(--cc); }
.cal-more { font-size: 10px; color: var(--text-dim); }
.cal-day-panel {
  margin-top: 14px;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px 16px;
  animation: fadeUp 0.25s ease both;
}
.cal-day-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}
.cal-day-head h3 { font-size: 15px; font-weight: 600; }
.cal-day-item {
  --cc: #3577d1;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  border-left: 3px solid var(--cc);
  border-radius: 6px;
  background: color-mix(in srgb, var(--cc) 8%, var(--bg-panel));
  margin-bottom: 6px;
}
.cal-day-item-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.cal-day-type { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--cc); font-weight: 600; }
.cal-day-title-txt { font-size: 13px; }
.cal-day-notes { font-size: 12px; color: var(--text-dim); white-space: pre-wrap; }
.event-card { max-width: 440px; }
.event-form { display: flex; flex-direction: column; gap: 10px; padding: 16px 20px 20px; }
.event-form input[type="text"], .event-form input[type="date"], .event-form input[type="time"], .event-form textarea {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  padding: 9px 11px;
  font-family: inherit;
  font-size: 13px;
  width: 100%;
}
.event-form input:focus, .event-form textarea:focus { outline: none; border-color: var(--accent); }
.event-row { display: flex; gap: 10px; }
.event-colors { display: flex; gap: 8px; }
.ev-color {
  width: 24px; height: 24px;
  border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
  transition: transform 0.12s;
}
.ev-color:hover { transform: scale(1.15); }
.ev-color.sel { border-color: var(--text); transform: scale(1.15); }
.event-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
#ev-delete { color: var(--danger); }

@media (max-width: 720px) {
  .cal-wrap { padding: 12px 10px calc(20px + env(safe-area-inset-bottom)); }
  .cal-cell { min-height: 56px; padding: 4px; border-radius: 6px; }
  .cal-chip { font-size: 0; padding: 0; height: 5px; border-left: none; background: var(--cc); border-radius: 3px; }
  .cal-chip b { display: none; }
  .cal-more { font-size: 9px; }
  .cal-header h2 { font-size: 16px; }
  .cal-legend { gap: 8px; font-size: 11px; }
  .event-row { flex-direction: column; }
}

/* ============ Drag-to-app + Show-in-app ============ */
.email-item[draggable="true"] { -webkit-user-drag: element; }
.sidebar.drag-hint .folder-item[data-app] { outline: 1px dashed var(--border); outline-offset: -1px; }
.folder-item.drop-target {
  background: var(--accent-soft) !important;
  outline: 2px dashed var(--accent) !important;
  outline-offset: -2px;
  color: var(--accent);
}
.row-menu-title {
  font-size: 11px;
  color: var(--text-dim);
  padding: 8px 12px 4px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 4px;
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ============ App unread badges ============ */
.app-badge {
  margin-left: auto;
  background: var(--accent);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  border-radius: 10px;
  padding: 1px 7px;
  min-width: 20px;
  text-align: center;
  animation: popIn 0.25s ease both;
}
.folder-item { display: flex; align-items: center; }

/* ============ Manage apps ============ */
.apps-manage-btn { margin: 4px 12px 0; font-size: 12px; text-align: left; }
.app-emoji { font-size: 15px; width: 18px; display: inline-block; text-align: center; }
.app-manage-row {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px; border-bottom: 1px solid var(--border);
}
.app-manage-row.app-hidden .app-manage-label { opacity: 0.45; }
.app-manage-icon { display: flex; align-items: center; color: var(--text-dim); }
.app-manage-label { flex: 1; font-size: 13px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.app-manage-label em { color: var(--text-dim); font-size: 11px; }
.app-manage-actions { display: flex; gap: 2px; }
.app-manage-actions .icon-btn { width: 28px; height: 28px; font-size: 13px; }

/* ============ App drill-down groups ============ */
.folder-item.app-sub { padding-left: 34px; font-size: 13px; }
.app-caret {
  margin-left: 6px;
  color: var(--text-dim);
  font-size: 12px;
  padding: 2px 6px;
  border-radius: 4px;
}
.app-caret:hover { color: var(--text); background: var(--bg-active); }

.acct-dot {
  width: 9px; height: 9px;
  border-radius: 50%;
  display: inline-block;
  flex-shrink: 0;
  margin: 0 4px 0 3px;
}

/* ============ Pull-to-refresh ============ */
.ptr-indicator {
  height: 0;
  opacity: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  color: var(--text-dim);
  transition: color 0.15s;
  flex-shrink: 0;
}
.ptr-indicator.ready { color: var(--accent); }
.ptr-indicator.spinning { height: 44px !important; opacity: 1 !important; animation: ptrspin 0.8s linear infinite; }
@keyframes ptrspin { to { transform: rotate(360deg); } }


/* ============ Bulk select ============ */
.lt-spacer { flex: 1; }
#btn-select.active { color: var(--accent); background: var(--accent-soft); }
.bulk-bar {
  display: flex; align-items: center; gap: 6px;
  padding: 8px 12px; border-bottom: 1px solid var(--border);
  background: var(--accent-soft);
  flex-wrap: wrap;
  animation: fadeUp 0.2s ease both;
}
.bulk-all { display: flex; align-items: center; gap: 5px; font-size: 13px; cursor: pointer; }
.bulk-count { font-size: 12px; color: var(--text-dim); min-width: 70px; }
.bulk-bar .btn { padding: 5px 10px; font-size: 12px; }
.bulk-danger { color: var(--danger); }
.email-item.sel-mode { padding-left: 44px; position: relative; }
.sel-box {
  position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
  font-size: 18px; color: var(--text-dim);
}
.email-item.picked { background: var(--accent-soft); }
.email-item.picked .sel-box { color: var(--accent); }

/* ============ Mobile de-clutter ============ */
/* Smart Inbox lives in the top menu (⚡) only — no floating button */
.smart-fab { display: none !important; }
/* floating Compose — phone only */
.compose-fab { display: none; }
@media (max-width: 720px) {
  .smart-fab {
    width: 46px; height: 46px;
    right: 12px;
    bottom: calc(70px + env(safe-area-inset-bottom));
    box-shadow: 0 4px 14px rgba(0,0,0,0.35);
  }
  .compose-fab {
    display: flex; align-items: center; justify-content: center;
    position: fixed; right: 12px;
    bottom: calc(70px + env(safe-area-inset-bottom));
    width: 46px; height: 46px; border-radius: 50%; border: none;
    background: var(--accent-strong); color: #fff; cursor: pointer; z-index: 45;
    box-shadow: 0 4px 14px rgba(0,0,0,0.35);
  }
  .compose-fab:active { transform: scale(0.94); }
  .compose-fab.hidden { display: none; }
  /* hide the buttons whenever another surface is active — no collisions */
  body:has(#email-viewer-pane:not(.hidden)) .smart-fab,
  body:has(#compose-modal:not(.hidden)) .smart-fab,
  body:has(#smart-panel:not(.hidden)) .smart-fab,
  body:has(#accounts-modal:not(.hidden)) .smart-fab,
  body:has(#apps-modal:not(.hidden)) .smart-fab,
  body:has(#event-modal:not(.hidden)) .smart-fab,
  body:has(.sidebar.open) .smart-fab,
  body:has(#email-viewer-pane:not(.hidden)) .compose-fab,
  body:has(#compose-modal:not(.hidden)) .compose-fab,
  body:has(#smart-panel:not(.hidden)) .compose-fab,
  body:has(#accounts-modal:not(.hidden)) .compose-fab,
  body:has(#apps-modal:not(.hidden)) .compose-fab,
  body:has(#event-modal:not(.hidden)) .compose-fab,
  body:has(.sidebar.open) .compose-fab { display: none; }
  .bulk-bar { gap: 4px; padding: 6px 8px; }
  .bulk-bar .btn { padding: 6px 8px; font-size: 12px; }
}

/* ============ Attachments: chip + ⋮ actions + inline preview ============ */
.attachment-wrap { display: inline-flex; align-items: stretch; margin: 4px 6px 0 0; }
.attachment-wrap .attachment-chip { border-radius: 8px 0 0 8px; margin: 0; }
.attachment-more {
  border: 1px solid var(--border); border-left: none; border-radius: 0 8px 8px 0;
  background: var(--bg); color: var(--text-dim); cursor: pointer;
  padding: 0 9px; font-size: 15px; font-weight: 700;
}
.attachment-more:hover { background: var(--bg-hover); color: var(--text); }
.att-preview {
  margin: 12px 0; border: 1px solid var(--border); border-radius: 10px;
  background: var(--bg-panel); padding: 10px 12px;
}
.att-preview-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.att-frame { width: 100%; height: 70vh; border: none; border-radius: 8px; background: #fff; }
.att-text { max-height: 55vh; overflow: auto; font-size: 13px; white-space: pre-wrap; word-break: break-word; }

/* ============ Mobile visual polish (portrait) ============ */
@media (max-width: 640px) {
  /* Topbar: exactly 3 tight rows — search / sort+actions / account chips.
     The select shrinks (flex-basis 0) so the four icon buttons NEVER wrap
     onto an orphan row; ~150px total instead of the old 216px. */
  .topbar { padding: 6px 10px 4px; gap: 6px; row-gap: 6px; }
  .searchbar { flex: 1 1 calc(100% - 50px); max-width: none; border-radius: 10px; }
  .searchbar input { padding: 9px 0; font-size: 16px; } /* 16px stops iOS zoom-on-focus */
  .sort-select { flex: 1 1 0; min-width: 0; max-width: none; font-size: 13px; padding: 8px 6px; border-radius: 8px; }
  .topbar > .icon-btn, .menu-btn {
    width: 40px; height: 40px; min-width: 40px; min-height: 40px;
    font-size: 17px; flex: 0 0 auto;
  }
  /* Reading view: the search/sort/chips bar is dead weight — reclaim all of it.
     The viewer toolbar's back button is the way out. */
  body:has(#view-mail.viewing) .topbar { display: none; }
  /* list: single-line header, denser rows (one more email per screen) */
  .list-toolbar { padding: 8px 12px 6px; flex-wrap: nowrap; }
  .list-title { font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .list-count { font-size: 11px; min-width: 0; }
  /* tactile: rows depress slightly on touch */
  .email-item { transition: transform 0.1s ease, background 0.15s; }
  .email-item:active { transform: scale(0.985); background: var(--bg-hover); }
  /* Declutter: rows are 2 lines (sender+date / subject). The "✓ read" chip and
     account pill are gone — bold-vs-dim carries read state (plus an accent dot
     on unread), and a thin colored left edge carries the account. The badges
     line only appears when it says something: booking tag, pin, attachment. */
  /* a 42px right gutter reserves the whole ⋮ zone — date, subject and tags
     can never run underneath it, on any row height or screen width */
  .email-item { padding: 12px 44px 12px 10px; gap: 3px; border-left: 3px solid var(--accc, transparent); }
  .ei-from { font-size: 14px; }
  .ei-date { font-size: 11px; }
  .row-more { right: 2px; }
  .email-item.unread .ei-from::before {
    content: ''; display: inline-block; width: 7px; height: 7px; border-radius: 50%;
    background: var(--accent); margin-right: 6px; vertical-align: 1px;
  }
  .ei-subject { font-size: 13px; color: var(--text-dim); }
  .email-item.unread .ei-subject { color: var(--text); }
  .read-chip, .ei-account { display: none; }
  .ei-badges { display: none; }
  .ei-badges:has(.bk-tag), .ei-badges:has(svg.ic) {
    display: flex; flex-wrap: wrap; gap: 5px; align-items: center;
  }
  .row-more { min-width: 40px; min-height: 40px; }
  /* lighter account chips — outline only, less shouting */
  /* viewer: ONE horizontally-scrollable toolbar row (never two stacked rows) */
  .viewer-toolbar { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; gap: 2px; padding: 6px 8px; }
  .viewer-toolbar::-webkit-scrollbar { display: none; }
  .viewer-toolbar .icon-btn { flex: 0 0 auto; width: 40px; height: 40px; min-width: 40px; min-height: 40px; }
  .viewer-subject { font-size: 18px; line-height: 1.3; }
}

/* ============ Read / unread visual state ============ */
/* read rows: dimmed, regular weight — the bold/regular contrast alone carries
   the unread state (whole-list italics read as noise, not as a signal) */
.email-item.read .ei-from,
.email-item.read .ei-subject {
  color: var(--text-dim);
  font-weight: 400;
}
.email-item.read .ei-fav { opacity: 0.55; }
.read-chip {
  border: 1px solid transparent;
  border-radius: 10px;
  padding: 1px 8px;
  font-size: 10px;
  font-family: inherit;
  cursor: pointer;
  letter-spacing: 0.03em;
  transition: all 0.15s;
}
.read-chip.is-new {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-soft);
  font-weight: 700;
}
.read-chip.is-read {
  color: var(--text-dim);
  border-color: var(--border);
  background: transparent;
  font-style: italic;
}
.read-chip:hover { filter: brightness(1.15); border-color: var(--accent); }

/* ============================================================
   Full-screen surfaces: Dashboard & Settings
   ============================================================ */
.screen {
  position: fixed; inset: 0; z-index: 60; background: var(--bg);
  display: flex; flex-direction: column;
  animation: screenIn 0.22s cubic-bezier(0.2, 0.9, 0.3, 1.05);
}
@keyframes screenIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.screen-head {
  display: flex; align-items: center; gap: 10px; flex: none;
  padding: 10px 14px; border-bottom: 1px solid var(--border); background: var(--bg-panel);
}
.screen-head h2 { font-size: 16px; font-weight: 700; margin-right: auto; }
.screen-body { flex: 1; overflow-y: auto; padding: 14px; }
.range-tabs { display: flex; gap: 2px; padding: 3px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; }
.range-tabs button {
  border: none; background: transparent; color: var(--text-dim); cursor: pointer;
  padding: 5px 9px; border-radius: 6px; font-family: inherit; font-size: 12px;
}
.range-tabs button.is-on { background: var(--accent-strong); color: #fff; }

/* --- KPI cards --- */
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(122px, 1fr)); gap: 10px; margin-bottom: 14px; }
.kpi {
  background: var(--bg-panel); border: 1px solid var(--border); border-radius: 10px;
  padding: 13px 14px; display: flex; flex-direction: column; gap: 2px;
  animation: kpiIn 0.35s backwards;
}
@keyframes kpiIn { from { opacity: 0; transform: translateY(8px); } }
.kpi-v { font-size: 22px; font-weight: 700; line-height: 1.1; }
.kpi-l { font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-dim); }
.kpi-s { font-size: 12px; color: var(--text-dim); }
.kpi.warn .kpi-v { color: var(--accent); }
.kpi.good .kpi-v { color: #4f9d6b; }

/* --- cards & charts --- */
.stat-card {
  background: var(--bg-panel); border: 1px solid var(--border); border-radius: 10px;
  padding: 14px 16px; margin-bottom: 12px;
}
.stat-card h3 { font-size: 13px; font-weight: 700; margin-bottom: 10px; display: flex; gap: 8px; align-items: baseline; }
.stat-sub { font-size: 11px; font-weight: 400; color: var(--text-dim); }
.stat-two { display: grid; grid-template-columns: 1fr; gap: 12px; }
@media (min-width: 860px) { .stat-two { grid-template-columns: 1fr 1fr; } }

.chart { width: 100%; height: 200px; overflow: visible; }
.ch-grid { stroke: var(--border); stroke-width: 1; }
.ch-tick { fill: var(--text-dim); font-size: 9px; }
.ch-area { fill: var(--accent); opacity: 0.13; }
.ch-line {
  fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round;
  stroke-dasharray: 2200; stroke-dashoffset: 2200; animation: draw 1.1s ease forwards;
}
@keyframes draw { to { stroke-dashoffset: 0; } }
.ch-recv { stroke: var(--accent); }
.ch-sent { stroke: #4a6fb3; opacity: 0.85; }
.ch-hit { fill: var(--accent); opacity: 0; cursor: pointer; transition: opacity 0.12s; }
.ch-hit.on, .ch-hit:hover { opacity: 1; }
.ch-legend { display: flex; gap: 14px; align-items: center; font-size: 12px; color: var(--text-dim); margin-top: 6px; }
.ch-legend i { display: inline-block; width: 10px; height: 3px; border-radius: 2px; margin-right: 5px; vertical-align: middle; }
.sw-recv { background: var(--accent); }
.sw-sent { background: #4a6fb3; }
.ch-read { margin-left: auto; color: var(--text); font-variant-numeric: tabular-nums; }

/* --- heatmap --- */
.heatmap { display: grid; grid-template-columns: 30px 1fr; gap: 4px 6px; align-items: center; }
.hm-rows { display: grid; grid-template-rows: repeat(7, 1fr); gap: 2px; font-size: 10px; color: var(--text-dim); }
.hm-grid { display: grid; grid-template-columns: repeat(24, 1fr); grid-template-rows: repeat(7, 1fr); gap: 2px; }
.hm-cell {
  aspect-ratio: 1; border-radius: 2px; min-height: 9px;
  background: color-mix(in srgb, var(--accent) calc(var(--i) * 100%), var(--bg-hover));
}
.hm-hours { grid-column: 2; display: flex; justify-content: space-between; font-size: 10px; color: var(--text-dim); }

/* --- hour bars --- */
.hours { display: flex; align-items: flex-end; gap: 2px; height: 110px; }
.hour-bar { flex: 1; display: flex; align-items: flex-end; height: 100%; }
.hour-bar i {
  display: block; width: 100%; border-radius: 3px 3px 0 0; background: var(--accent);
  height: calc(var(--h) * 100%); min-height: 2px; opacity: 0.85;
  animation: growUp 0.5s ease backwards; transform-origin: bottom;
}
@keyframes growUp { from { transform: scaleY(0); } }
.hours-axis { display: flex; justify-content: space-between; font-size: 10px; color: var(--text-dim); margin-top: 4px; }

/* --- bar lists --- */
.bars { list-style: none; display: flex; flex-direction: column; gap: 6px; }
.bar-row { display: grid; grid-template-columns: minmax(0, 1fr) 90px 54px; gap: 9px; align-items: center; cursor: pointer; }
.bar-row:hover .bar-label { color: var(--accent); }
.bar-label { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-track { height: 7px; background: var(--bg-hover); border-radius: 4px; overflow: hidden; }
.bar-fill {
  display: block; height: 100%; background: var(--accent); border-radius: 4px;
  animation: growRight 0.6s cubic-bezier(0.2, 0.9, 0.3, 1) backwards; transform-origin: left;
}
@keyframes growRight { from { transform: scaleX(0); } }
.bar-n { font-size: 12px; color: var(--text-dim); text-align: right; font-variant-numeric: tabular-nums; }
.bar-n em { color: var(--accent); font-style: normal; margin-left: 4px; }

.acc-stats { list-style: none; display: flex; flex-direction: column; gap: 8px; }
.acc-stats li { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.as-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.as-n { margin-left: auto; color: var(--text-dim); font-size: 12px; flex: none; }
.as-n em { color: var(--accent); font-style: normal; }

.cloud { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: baseline; }
.cloud-w { cursor: pointer; line-height: 1.3; }
.cloud-w:hover { color: var(--accent); }
.big-msg { font-size: 13px; line-height: 1.6; }
.stats-loading, .stats-foot { color: var(--text-dim); font-size: 12px; text-align: center; padding: 22px 10px; }

/* ============================================================
   Settings
   ============================================================ */
.settings-body { max-width: 720px; margin: 0 auto; width: 100%; }
.set-card {
  background: var(--bg-panel); border: 1px solid var(--border); border-radius: 10px;
  padding: 15px 16px; margin-bottom: 12px;
}
.set-card h3 { font-size: 13px; font-weight: 700; margin-bottom: 12px; }
.set-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.set-row > label { font-size: 13px; color: var(--text-dim); }
.set-val { color: var(--text); margin-left: 6px; font-variant-numeric: tabular-nums; }
.set-toggle input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--accent); }
.set-note { font-size: 12px; color: var(--text-dim); line-height: 1.55; margin-bottom: 10px; }
.seg { display: flex; gap: 2px; padding: 3px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; }
.seg button {
  border: none; background: transparent; color: var(--text-dim); cursor: pointer;
  padding: 6px 11px; border-radius: 6px; font-family: inherit; font-size: 12px;
}
.seg button.is-on { background: var(--accent-strong); color: #fff; }
.swatches { display: flex; gap: 6px; }
.swatch { width: 22px; height: 22px; border-radius: 50%; border: 2px solid transparent; cursor: pointer; }
.swatch.is-on { border-color: var(--text); }
input[type="range"] { accent-color: var(--accent); width: 150px; }

.set-preview-strip {
  background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
  padding: 10px 12px; margin-top: 4px;
}
.sp-row { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; }
.sp-date { margin-left: auto; font-size: 12px; font-weight: 400; color: var(--text-dim); }
.sp-sub { font-size: 13px; color: var(--text-dim); margin-top: 3px; }

.set-accounts { list-style: none; display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.set-acc {
  display: flex; align-items: center; gap: 9px; padding: 9px 10px;
  background: var(--bg); border: 1px solid var(--border); border-radius: 10px; cursor: grab;
}
.set-acc.dragging { opacity: 0.45; }
.drag-grip { color: var(--text-dim); font-size: 11px; letter-spacing: -2px; flex: none; }
.sa-main { display: flex; flex-direction: column; min-width: 0; margin-right: auto; }
.sa-email { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sa-host { font-size: 11px; color: var(--text-dim); }
.set-pw { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 10px; }
.set-pw input {
  flex: 1 1 160px; padding: 9px 11px; border: 1px solid var(--border);
  border-radius: 6px; background: var(--bg); color: var(--text); font-family: inherit; font-size: 13px;
}

/* --- appearance preferences applied app-wide --- */
body[data-density="compact"] .email-item { padding: 7px 14px 7px 10px; gap: 1px; contain-intrinsic-size: auto 48px; }
body[data-density="compact"] .ei-from { font-size: 13px; }
body[data-density="compact"] .ei-subject { font-size: 12px; }

/* The ⋮ button is absolutely positioned over the row, so every row must keep a
   gutter clear for it — on any density, any screen width. Declared last so no
   density shorthand can quietly reclaim it (which is exactly what happened when
   the compact density landed). */
.email-item { padding-right: 46px; }
body[data-density="compact"] .email-item { padding-right: 46px; }
.ei-date { max-width: 45%; overflow: hidden; text-overflow: ellipsis; }
body[data-motion="off"] *, body[data-motion="off"] *::before, body[data-motion="off"] *::after {
  animation: none !important; transition: none !important;
}
body[data-avatars="off"] .tc-avatar, body[data-avatars="off"] .viewer-avatar { display: none; }
/* data controls in Settings */
.set-data-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.danger-btn { color: var(--danger); border-color: var(--danger)55; }
.danger-btn:hover { background: var(--danger)12; border-color: var(--danger); }

/* the sort menu is the only select in the top bar now */
.sort-select { max-width: 46vw; }
@media (min-width: 900px) { .sort-select { max-width: 18rem; } }

/* ============================================================
   Message actions: one floating control instead of a hardcoded row
   ============================================================ */
/* The toolbar buttons stay in the DOM — the reply bar and the action sheet
   drive them programmatically — and the secondary ones live in the floating
   menu, which keeps the top of a message clear of chrome.
   Reply, Reply all and Forward are NOT secondary. Hiding them behind an
   unlabelled circle made the commonest action in a mail client something you
   have to go looking for: the owner's own words were "where is the reply
   forward on the top bar menu so I can reply to this message". A control
   nobody can find is not a tidier interface, it is a missing feature.
   Four buttons at 44px come to 176px beside Back, which fits the 412px of the
   narrowest phone this app supports, so they are shown at every width rather
   than only on desktop. Everything else stays in the menu. */
.viewer-toolbar > .icon-btn.tb:not(#btn-back):not(#btn-reply):not(#btn-reply-all):not(#btn-forward),
.viewer-toolbar > .tb-sep,
.viewer-toolbar > .more-wrap { display: none; }

.viewer-fab {
  position: absolute; z-index: 25;
  top: 8px; right: 12px;
  width: 40px; height: 40px; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--bg-panel) 90%, transparent);
  color: var(--text); cursor: pointer;
  box-shadow: 0 4px 16px rgba(0, 0, 0, .22);
  backdrop-filter: blur(12px) saturate(1.3);
  -webkit-backdrop-filter: blur(12px) saturate(1.3);
  transition: background .15s, transform .1s, border-color .15s;
}
.viewer-fab:hover { background: var(--bg-hover); border-color: var(--accent); }
.viewer-fab:active { transform: scale(.92); }
.email-viewer-pane.hidden .viewer-fab { display: none; }

/* ============================================================
   Conversation history: a slim contents list, not stacked cards
   ============================================================ */
.viewer-history { margin-top: 26px; border-top: 1px solid var(--border); padding-top: 10px; }
.hist-title {
  display: flex; align-items: center; gap: 8px;
  font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-dim); margin-bottom: 6px;
}
.hist-title span {
  font-size: 11px; letter-spacing: 0; padding: 1px 6px; border-radius: 8px;
  background: var(--bg-hover); color: var(--text-dim);
}
.hist-row { border-bottom: 1px solid var(--border); }
.hist-row:last-child { border-bottom: none; }
.hist-head {
  width: 100%; display: flex; align-items: baseline; gap: 10px;
  padding: 9px 2px; background: none; border: none; cursor: pointer;
  color: var(--text); font-family: inherit; font-size: 13px; text-align: left;
}
.hist-head:hover .hist-subject { color: var(--accent); }
.hist-subject { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hist-row.unread .hist-subject { font-weight: 700; }
.hist-when { flex: none; font-size: 12px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.hist-body { padding: 0 0 8px; }
.hist-actions { padding-bottom: 8px; }


/* notification permission help */
.perm-modal { width: min(430px, 94vw); }
.perm-body { padding: 16px 18px 18px; }
.perm-lead { font-size: 13px; line-height: 1.55; margin-bottom: 12px; }
.perm-steps { margin: 0 0 12px 18px; padding: 0; display: flex; flex-direction: column; gap: 7px; }
.perm-steps li { font-size: 13px; line-height: 1.5; color: var(--text-dim); }
.perm-steps li b { color: var(--text); font-weight: 600; }

/* ============================================================
   Open message: a compact header, then the mail
   The old header cost ~370px before a single line of content — full subject,
   a three-line address block, three AI chips and an ask box. Now: subject,
   one tappable identity line, and one assistant chip.
   ============================================================ */
.viewer-subject {
  font-size: 15px; font-weight: 700; line-height: 1.35;
  margin-bottom: 8px; text-wrap: balance;
}
.viewer-meta-line {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 6px 0; margin-bottom: 4px;
  background: none; border: none; cursor: pointer;
  color: var(--text); font-family: inherit; font-size: 13px; text-align: left;
}
.viewer-meta-line .viewer-avatar {
  width: 28px; height: 28px; min-width: 28px; line-height: 28px;
  border-radius: 50%; color: #fff; text-align: center;
  font-size: 13px; font-weight: 700; flex: none;
}
.vm-from { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vm-when { margin-left: auto; color: var(--text-dim); font-size: 12px; flex: none; }
.vm-caret { color: var(--text-dim); font-size: 11px; flex: none; transition: transform .15s; }
.viewer-meta-line.is-open .vm-caret { transform: rotate(180deg); }
.viewer-details {
  background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
  padding: 9px 11px; margin-bottom: 10px;
  font-size: 13px; line-height: 1.7; color: var(--text-dim);
}
.viewer-details b { color: var(--text); font-weight: 600; display: inline-block; min-width: 68px; }

/* assistant: one chip that opens the panel, instead of four controls always on */
.ai-strip { margin-bottom: 12px; }
.ai-open {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 14px; border: 1px solid var(--border); border-radius: 16px;
  background: transparent; color: var(--text-dim); cursor: pointer;
  font-family: inherit; font-size: 13px;
  transition: color .15s, border-color .15s, background .15s;
}
.ai-open:hover { color: var(--text); border-color: var(--accent); background: var(--bg-hover); }
.ai-open svg, .ai-open .ic { width: 15px; height: 15px; }
.ai-panel { margin-bottom: 12px; }
.ai-panel .ai-bar { margin-bottom: 8px; }
.ai-panel .ai-ask { margin-bottom: 0; }

/* ============================================================
   Sidebar mailbox picker: it is now the only account switcher, so it
   carries the unread counts the old top-bar dropdown used to show.
   ============================================================ */
.acc-count {
  margin-left: auto; flex: none;
  min-width: 20px; padding: 1px 6px; border-radius: 10px;
  background: var(--accent-strong); color: #fff;
  font-size: 11px; font-weight: 700; text-align: center;
}
.account-menu-item .acc-check { margin-left: 6px; }

/* ============================================================
   Sort rules — the user's own entries in the top sort menu
   ============================================================ */
.sr-modal { width: min(520px, 94vw); }
.sr-body { padding: 14px 16px 16px; max-height: 78vh; overflow-y: auto; }
.sr-list { list-style: none; margin: 12px 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.sr-empty { color: var(--text-dim); font-size: 13px; padding: 10px 2px; }
.sr-item {
  display: flex; align-items: center; gap: 4px;
  padding: 8px 10px; border: 1px solid var(--border); border-radius: 10px;
  background: var(--bg);
}
.sr-meta { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
.sr-meta b { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sr-meta span { font-size: 12px; color: var(--text-dim); }
.sr-item .icon-btn { flex: none; }
.sr-item .icon-btn[disabled] { opacity: 0.3; cursor: default; }
.sr-del:hover { color: var(--danger); }
.sr-form {
  border-top: 1px solid var(--border); padding-top: 12px;
  display: flex; flex-direction: column; gap: 10px;
}
.sr-name {
  width: 100%; padding: 9px 11px; border-radius: 10px;
  border: 1px solid var(--border); background: var(--bg);
  color: var(--text); font-family: inherit; font-size: 13px;
}
.sr-fields { display: flex; flex-wrap: wrap; gap: 10px; }
.sr-fields label {
  flex: 1 1 140px; min-width: 0;
  display: flex; flex-direction: column; gap: 4px;
  font-size: 12px; color: var(--text-dim);
}
.sr-fields select {
  padding: 8px 9px; border-radius: 10px; border: 1px solid var(--border);
  background: var(--bg); color: var(--text); font-family: inherit; font-size: 13px;
}
.sr-check { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text); }

/* ============================================================
   Panel review fixes (v103)
   Everything below is a correction to a measured defect, not a
   restyling. The measurement is quoted with each rule so the next
   person can re-check it instead of trusting the comment.
   ============================================================ */

/* ---- 1. white text on the accent failed AA (3.96:1 in dark) ----
   Anywhere white sits ON the brand colour now uses --accent-strong,
   which is the same hue at >=4.5:1. The accent itself is untouched
   as a text/stroke colour, where it already passed. */
.btn-primary,
.cal-tab.active,
.ag-count,
.acc-count,
.undo-bar .ub-undo,
.compose-fab { background: var(--accent-strong); }
.btn-primary { border-color: var(--accent-strong); }
.btn-primary:hover { background: var(--accent-strong); }

/* ---- 2. control boundaries were 1.39:1 (dark) / 1.22:1 (light) ----
   WCAG 1.4.11 wants 3:1 for the edge of a control. Decorative
   separators keep --border; things you type into or choose from get
   --border-strong so the field is findable. */
input[type="text"], input[type="email"], input[type="password"],
input[type="search"], input[type="date"], input[type="time"],
input[type="datetime-local"], input[type="number"],
textarea, select {
  border-color: var(--border-strong);
}

/* ---- 3. keyboard focus was invisible ----
   Five rules removed the outline from inputs and nothing replaced it;
   the stylesheet had zero :focus-visible rules. This is the whole
   replacement, and it deliberately uses :focus-visible so a mouse or
   touch press does not draw a ring. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
.searchbar input:focus-visible,
.ai-ask input:focus-visible,
.compose-row input:focus-visible,
.compose-row select:focus-visible,
.event-form input:focus-visible,
.event-form textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
/* .compose-body is deliberately NOT in that list. On those controls the ring
   traces a box that is already there; on the writing area it drew a new box
   around the whole pane the moment you tapped into it. The caret is the focus
   indicator here — the same signal every text editor uses — so focus stays
   visible without boxing the message in. */
.compose-body:focus-visible { outline: none; }

/* ---- 4. touch targets ----
   135 of 176 controls measured under 44px on their short side.
   44px is the Apple minimum; the top bar goes to 46 where there is room. */
.icon-btn { min-width: 44px; min-height: 44px; }
.read-chip, .sq-btn, .bulk-bar .btn { min-height: 44px; }
/* the select-all box measured 13x13 — the box stays small, the
   *target* around it does not */
.bulk-all {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 0 6px; cursor: pointer;
}
.bulk-all input[type="checkbox"] { width: 20px; height: 20px; accent-color: var(--accent); }
.sr-check { min-height: 44px; }
.sr-check input[type="checkbox"], .set-toggle input[type="checkbox"] { width: 20px; height: 20px; accent-color: var(--accent); }

/* ---- 5. the floating compose button ----
   It sat 70px off the bottom edge, reading as detached, at 46px
   against a 56dp standard. */
@media (max-width: 720px) {
  .compose-fab {
    width: 56px; height: 56px;
    right: 16px;
    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  }
  .compose-fab svg { width: 24px; height: 24px; }
}

/* ---- 6. iOS zooms the page when a field under 16px takes focus ----
   24 inputs measured between 12.5px and 13.5px. Only text entry and
   selects trigger it, and only on the phone breakpoint. */
@media (max-width: 720px) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
  select, textarea { font-size: 16px; }
  .sr-fields select, .set-row select { font-size: 16px; }
}

/* ---- 7. the top bar was two rows, 102px, 19% of the screen ----
   One row now: menu, search, sort, Smart Inbox, notifications.
   Refresh and theme leave the phone top bar — both already exist in
   the sidebar footer, and pull-to-refresh already works — but they
   stay in the DOM so the "rearrange top menu buttons" setting and the
   desktop layout are untouched. */
@media (max-width: 720px) {
  .topbar { flex-wrap: nowrap; gap: 6px; padding: 6px 10px; }
  .searchbar { flex: 1 1 0; min-width: 0; order: 0; }
  #btn-refresh-top, #btn-theme-top { display: none; }
  /* the sort menu becomes an icon: the native picker still opens on tap
     and still reads every label out, but the closed control no longer
     needs room for "Conversations — newest first" (which measured 167px
     of text in a 179px box, and was visibly clipped) */
  .sort-select {
    flex: 0 0 44px; width: 44px; min-width: 44px; max-width: 44px;
    height: 44px; padding: 0; color: transparent;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%238f8a7d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M3 6h18'/><path d='M7 12h10'/><path d='M11 18h2'/></svg>");
    background-repeat: no-repeat; background-position: center;
    -webkit-appearance: none; appearance: none;
    text-indent: -9999px;
  }
  /* a dot when the list is not on the default sort, so an icon-only
     control still says that something is filtering the inbox */
  .sort-select.is-custom {
    box-shadow: inset 0 0 0 1px var(--accent);
  }
  .sort-select option { color: var(--text); }
}

/* ---- 8. dead CSS ----
   .smart-fab is display:none !important globally (the Smart Inbox
   moved into the top bar), so the phone-only sizing block and the
   seven :has() rules that tried to hide it were unreachable. They are
   overridden here rather than cut out of the middle of the file. */
.smart-fab { display: none !important; }

/* ---- 9. flag and paperclip moved onto the sender line ----
   They used to sit in .ei-badges, which is the row's third line; a
   message with an attachment measured 82px against 67px for one
   without. Now every row is one height. */
.ei-flags { display: inline-flex; align-items: center; gap: 4px; margin-left: auto; color: var(--text-dim); flex: none; }
.ei-flags:empty { display: none; }
.ei-flags .ic { width: 12px; height: 12px; margin-right: 0; }
.email-item.unread .ei-flags { color: var(--accent); }

/* ============================================================
   Panel review, second pass (v104)
   The first pass missed controls whose rules were more specific than
   a bare .icon-btn, and the sub-16px fix lost to class selectors.
   Numbers below are from re-measuring the deployed v103 with
   animations disabled (measuring mid-popIn had been inflating the
   failure count with scaled boxes).
   ============================================================ */

/* ---- targets: everything that is a real action reaches 44px ----
   Measured 95 controls under 44px on their short side. These rules
   are grouped by the container that was beating the base rule. */
.topbar > .icon-btn, .topbar .menu-btn {
  width: 46px; height: 46px; min-width: 46px; min-height: 46px;
}
.icon-btn.tb, .viewer-fab { min-width: 44px; min-height: 44px; }
.btn, .auth-tab, .auth-alt, .cal-tab, .ub-undo, .smart-tab,
.account-menu-item, .sheet-item, .sq-btn, .read-chip { min-height: 44px; }
.seg button, .range-tabs button, .smart-tabs button { min-height: 44px; }
select { min-height: 44px; }
/* reorder arrows in the settings lists: these sat at 30px */
.mm-ctl .icon-btn, .app-manage-actions .icon-btn,
.tm-up, .tm-down, .sr-item .icon-btn {
  width: 44px; height: 44px; min-width: 44px; min-height: 44px;
}
/* the event colour swatches were 24px dots. The dot stays 24px — a
   44px disc would dominate the form — but the *target* is 44px, drawn
   with padding and background-clip. */
.ev-color {
  width: 44px; height: 44px; min-width: 44px; min-height: 44px;
  padding: 10px; background-clip: content-box; background-origin: content-box;
  border-radius: 50%;
}
/* checkboxes are 20px boxes inside a 44px label, which is the target
   a screen reader and a thumb both act on */
input[type="checkbox"] { width: 20px; height: 20px; accent-color: var(--accent); }
.set-toggle, .set-toggle label, .fu-label, .bulk-all, .sr-check { min-height: 44px; display: flex; align-items: center; gap: 8px; }

/* ---- iOS zoom, properly this time ----
   The first pass used bare element selectors, which lose to
   .sort-select, .sr-fields select and .event-form textarea. Six fields
   were still measuring 12.5-13.5px. */
@media (max-width: 720px) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
  select, textarea { font-size: 16px !important; }
}

/* ---- targets, third pass ----
   Three groups survived: buttons carrying only .btn-ghost/.btn-primary
   (no .btn, so the group rule never matched them), the sidebar's
   manage buttons, and the viewer toolbar icons — the last two beaten
   by more specific rules earlier in the file. */
.btn-ghost, .btn-primary { min-height: 44px; }
.apps-manage-btn { min-height: 44px !important; }
.viewer-toolbar .icon-btn, .icon-btn.tb {
  min-width: 44px !important; min-height: 44px !important;
}
.bulk-bar .btn, .event-actions .btn-ghost, .event-actions .btn-primary { min-width: 44px; }
/* inline buttons that sit inside a line of text keep their own height —
   forcing 44px there would break the sentence, and they are paired with
   a same-action control that is already 44px */
.ei-badges .read-chip { min-height: 0; }

/* ---- targets, final pass ----
   What was left after the third pass was: the row overflow button
   (its own class, never matched by .icon-btn), the undo bar's button
   (beaten by .undo-bar .ub-undo), and a set of pills that were 44px
   tall but 32-39px wide. Width counts as much as height. */
.row-more { width: 44px !important; height: 44px !important; }
.undo-bar .ub-undo { min-height: 44px; }
.btn, .btn-ghost, .btn-primary, .seg button, .range-tabs button,
.smart-tabs button, .cal-tab, .auth-tab { min-width: 44px; }

/* ---- targets: the last group ----
   The message-menu state cycler in Settings sat at 30px, held there by
   a rule more specific than .btn. Two checkboxes remain 20px glyphs —
   deliberately: their target is the 44px row label they are bound to,
   which is how a settings row is meant to work. */
.mm-state { min-height: 44px !important; }
.set-toggle label { flex: 1; cursor: pointer; }

/* ---- the single-row top bar on a 320px phone ----
   At 320px the row still overflowed internally: the search field has an
   intrinsic minimum that flex could not shrink past. Let it shrink, and
   tighten the gutters on the narrowest phones. */
.searchbar { min-width: 0; }
.searchbar input { min-width: 0; width: 100%; }
@media (max-width: 360px) {
  .topbar { padding: 6px 8px; gap: 4px; }
  .topbar > .icon-btn, .topbar .menu-btn {
    width: 44px; height: 44px; min-width: 44px; min-height: 44px;
  }
}

/* ============================================================
   Android flagship compatibility (v109)
   Two device traits this app had not accounted for: a system that
   force-darkens web content, and a screen whose glass curves away at
   the left and right edges.
   ============================================================ */

/* ---- the page owns its own light and dark ----
   Declaring color-scheme stops Chrome's Auto Dark Theme (and ColorOS's
   force-dark) from inverting a page that is already dark, and makes the
   native bits — scrollbars, the caret, date pickers, select popups —
   follow the theme instead of the OS. */
:root { color-scheme: light dark; }
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

/* ---- curved edges and landscape cutouts ----
   Every safe-area inset in this file was a bottom inset. On a phone with
   a curved display, or held in landscape with the camera cutout on one
   side, content ran under the glass. env() resolves to 0 where there is
   no inset, so nothing moves on a flat screen. */
.topbar {
  padding-left: max(10px, env(safe-area-inset-left));
  padding-right: max(10px, env(safe-area-inset-right));
}
.list-toolbar, .bulk-bar, .viewer-toolbar, .screen-head {
  padding-left: max(14px, env(safe-area-inset-left));
  padding-right: max(14px, env(safe-area-inset-right));
}
.sidebar { padding-left: env(safe-area-inset-left); }
@media (max-width: 720px) {
  /* the 46px gutter the ⋮ button lives in is preserved, with the inset
     added on top of it — never in place of it */
  .email-item {
    padding-left: calc(10px + env(safe-area-inset-left));
    padding-right: calc(46px + env(safe-area-inset-right));
  }
  .row-more { right: calc(2px + env(safe-area-inset-right)); }
  .viewer-body, .compose-body { 
    padding-left: max(14px, env(safe-area-inset-left));
    padding-right: max(14px, env(safe-area-inset-right));
  }
  .compose-fab { right: calc(16px + env(safe-area-inset-right)); }
  .undo-bar { margin-left: env(safe-area-inset-left); margin-right: env(safe-area-inset-right); }
}

/* ---- the ⋮ gutter, re-derived ----
   The overflow button went from 40px to 44px for the target-size fix,
   which ate the 4px of clearance the 46px gutter used to leave. The
   date was measuring flush against it — the exact collision reported
   twice before. The gutter follows the button's width plus clearance,
   and is declared last so no later rule can take it away, at every
   density and width. */
.email-item,
body[data-density="compact"] .email-item {
  padding-right: calc(50px + env(safe-area-inset-right));
}

/* ============================================================
   Floating action dock at the end of an open message (v111)
   Small icons in a pill that sticks to the foot of the message while
   you read and settles at its end — the actions stay a thumb away
   without the slab of chrome that used to close every mail.
   Icons are 17px so it reads as light; the buttons underneath are
   still 44px, because a small icon is not the same as a small target.
   ============================================================ */
.msg-dock {
  position: sticky; bottom: calc(10px + env(safe-area-inset-bottom, 0px));
  z-index: 12; display: flex; justify-content: center;
  margin: 20px 0 6px; pointer-events: none;
}
.dock-pill {
  pointer-events: auto;
  display: inline-flex; align-items: center; gap: 2px;
  padding: 3px; border-radius: 16px;
  background: color-mix(in srgb, var(--bg-panel) 92%, transparent);
  border: 1px solid var(--border);
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.28);
  backdrop-filter: blur(9px); -webkit-backdrop-filter: blur(9px);
  max-width: 100%; overflow-x: auto; scrollbar-width: none;
}
.dock-pill::-webkit-scrollbar { display: none; }
.dock-btn {
  flex: none; width: 44px; height: 44px; border-radius: 50%;
  border: none; background: transparent; color: var(--text-dim);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; transition: background .15s, color .15s, transform .1s;
}
.dock-btn svg, .dock-btn .ic { width: 17px; height: 17px; margin: 0; }
.dock-btn:hover { background: var(--bg-hover); color: var(--text); }
.dock-btn:active { transform: scale(.9); }
.dock-btn.is-primary { background: var(--accent-strong); color: #fff; }
.dock-btn.is-primary:hover { background: var(--accent-strong); filter: brightness(1.08); color: #fff; }
.dock-btn.is-on { color: var(--accent); }
/* was `var(--danger)1f` — appending an alpha suffix to a var() is not a
   colour, so this declaration was invalid and the danger hover had no
   background at all. */
.dock-btn.danger:hover { background: color-mix(in srgb, var(--danger) 12%, transparent); color: var(--danger); }

/* the history list belongs after the dock, not behind it */
.viewer-history { position: relative; z-index: 1; }

/* ============================================================
   v112 — two corrections
   ============================================================ */

/* ---- 1. pinch to zoom ----
   The viewport meta always allowed scaling, but `touch-action: pan-y` on the
   list and the open message told the browser to handle vertical panning ONLY,
   which silently swallowed the two-finger gesture on exactly the two surfaces
   worth zooming. Adding pinch-zoom hands that gesture back without giving up
   the horizontal swipe actions, which are still delivered to JS. */
.email-list, .viewer-body { touch-action: pan-y pinch-zoom; }
.viewer-content, .att-preview, .compose-body { touch-action: pan-y pinch-zoom; }

/* ---- 2. the dock is glass, not a banner ----
   It was reading as a slab because of the panel fill, the border and the
   shadow — not because of its size. Those go; what is left is the icons and
   just enough blur to keep them legible over text. The buttons stay 44px
   because a small icon and a small target are not the same thing. */
.dock-pill {
  background: color-mix(in srgb, var(--bg-panel) 45%, transparent);
  border: none;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
  backdrop-filter: blur(14px) saturate(1.2);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
  padding: 2px;
  opacity: 0.86;
  transition: opacity .18s;
}
.dock-pill:hover, .dock-pill:focus-within { opacity: 1; }
.dock-btn svg, .dock-btn .ic { width: 16px; height: 16px; }
.msg-dock { margin: 14px 0 4px; }
/* the accent circle was the heaviest thing in it — a tint carries "primary"
   just as well at this size */
.dock-btn.is-primary {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--accent);
}
.dock-btn.is-primary:hover {
  background: color-mix(in srgb, var(--accent) 34%, transparent);
  filter: none; color: var(--accent);
}

/* ---- v113: the dock was glass, but you could not read it ----
   Three things were fighting the icons at once: they were --text-dim
   (the faintest colour in the palette), 16px, and sitting on a fill of
   only 45% over arbitrary message content, so there was no stable
   ground behind them. The glass stays — still translucent, still
   blurred — but the fill is now dense enough to be a surface, and the
   icons are full-strength text colour at 18px with a slightly heavier
   stroke. */
.dock-pill {
  background: color-mix(in srgb, var(--bg-panel) 82%, transparent);
  border: 1px solid color-mix(in srgb, var(--border-strong) 55%, transparent);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.22);
  opacity: 1;
}
.dock-btn { color: var(--text); }
.dock-btn svg, .dock-btn .ic {
  width: 18px; height: 18px;
  stroke-width: 2.1;
}
.dock-btn:hover { background: var(--bg-hover); color: var(--text); }
/* Reply used to be a filled accent circle so the primary action would lead. On
   a phone it read as the loudest thing on the screen — a teal disc among six
   plain glyphs — and the message it was attached to is what should lead. It now
   sits in the same colour as its siblings and keeps a heavier stroke, which
   carries "primary" without a colour that pulls the eye off the mail. */
.dock-btn.is-primary { background: transparent; color: var(--text); }
.dock-btn.is-primary:hover { background: var(--bg-hover); color: var(--text); filter: none; }
.dock-btn.is-primary svg { stroke-width: 2.4; }
.dock-btn.danger { color: var(--danger); }
.dock-btn.is-on { color: var(--accent); }

/* ============ diagnostics ============ */
.diag-out { margin-top: 10px; font-size: 13px; }
.diag-top { display: flex; gap: 14px; flex-wrap: wrap; padding: 8px 0 4px; color: var(--text-dim); }
.diag-top .is-bad b { color: var(--danger); }
.diag-top .is-ok b { color: #2e9e63; }
.diag-h { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-dim); margin: 14px 0 6px; }
.diag-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 7px 0; border-bottom: 1px solid var(--border);
}
.diag-row code { font-size: 12px; overflow-wrap: anywhere; }
.diag-row span { color: var(--text-dim); font-size: 12px; flex: none; }
.diag-row.is-bad code { color: var(--danger); }
.diag-err { display: flex; flex-direction: column; gap: 2px; padding: 7px 0; border-bottom: 1px solid var(--border); }
.diag-when { font-size: 11px; color: var(--text-dim); }
.diag-msg { font-size: 12px; overflow-wrap: anywhere; }

/* ============================================================
   Update prompt
   The app used to purge its caches and reload itself the moment a new
   version appeared, which meant losing whatever was half-written. It
   now says the update is ready and waits to be told.
   ============================================================ */
.update-bar {
  position: fixed; z-index: 70;
  left: 50%; transform: translateX(-50%);
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  display: flex; align-items: center; gap: 10px;
  max-width: calc(100vw - 24px);
  padding: 8px 8px 8px 16px; border-radius: 16px;
  background: color-mix(in srgb, var(--bg-panel) 92%, transparent);
  border: 1px solid var(--border-strong);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.3);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  /* NOT fadeUp: it ends at 'transform: none', which wipes the translateX(-50%)
     doing the centring and leaves the bar half its own width to the right.
     The centring has to travel through every keyframe. */
  animation: ub-rise 0.3s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes ub-rise {
  from { opacity: 0; transform: translateX(-50%) translateY(12px); }
  to   { opacity: 1; transform: translateX(-50%); }
}
.update-bar .ub-text { font-size: 13px; color: var(--text); }
.update-bar .btn { min-height: 44px; padding: 0 14px; font-size: 13px; }
.update-bar .icon-btn { min-width: 44px; min-height: 44px; }
@media (max-width: 420px) {
  .update-bar { left: 12px; right: 12px; transform: none; max-width: none; }
  .update-bar .ub-text { flex: 1; min-width: 0; }
}

/* ============================================================
   v121 — a desktop is a different device, not a wide phone
   ------------------------------------------------------------
   Everything from the mobile passes (v103-v112) was applied to every
   width: 44px minimum targets, 16px form text, a thumb-height glass
   dock over the open message. On a phone that is correct and was
   measured to be. On a pointer device it is wrong for the same
   reason it was right there — a mouse hits a 28px target as reliably
   as a thumb hits a 44px one, so the 44px floor buys nothing and
   costs about a third of the vertical space on every screen.

   The lever is `pointer: fine`, not width alone: a 1280px tablet or a
   touchscreen laptop is still a finger, and gets the phone sizing.
   Width is kept in the query as well so a small pointer window (a
   side-by-side split) does not get desktop density it has no room for.

   `!important` appears below because the rules being corrected use it.
   Matching it is the honest way to say "this comes after"; the
   alternative is a specificity arms race that reads worse and breaks
   the next time either side is edited.
   ============================================================ */
@media (pointer: fine) and (min-width: 900px) {

  /* ---- controls at pointer density ---- */
  .icon-btn,
  .icon-btn.tb,
  .viewer-toolbar .icon-btn,
  .mm-ctl .icon-btn, .app-manage-actions .icon-btn,
  .tm-up, .tm-down, .sr-item .icon-btn {
    width: 32px !important; height: 32px !important;
    min-width: 32px !important; min-height: 32px !important;
  }
  .topbar > .icon-btn, .topbar .menu-btn {
    width: 34px !important; height: 34px !important;
    min-width: 34px !important; min-height: 34px !important;
  }
  .row-more { width: 30px !important; height: 30px !important; }

  .btn, .btn-ghost, .btn-primary, .auth-tab, .auth-alt, .cal-tab,
  .ub-undo, .smart-tab, .account-menu-item, .sheet-item, .sq-btn,
  .read-chip, .seg button, .range-tabs button, .smart-tabs button,
  .apps-manage-btn, .mm-state, .undo-bar .ub-undo, select {
    min-height: 32px !important;
  }
  .btn, .btn-ghost, .btn-primary { padding: 0 12px; font-size: 13px; }
  .bulk-all, .sr-check, .set-toggle, .set-toggle label, .fu-label {
    min-height: 32px !important;
  }
  .update-bar .btn { min-height: 32px !important; }
  .update-bar .icon-btn { min-width: 32px !important; min-height: 32px !important; }

  /* form text: 16px exists to stop iOS zooming a focused field. There is
     no such zoom with a pointer, and 16px in a settings column reads as
     a heading rather than a value. */
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
  select, textarea,
  .sr-fields select, .set-row select,
  .compose-row input, .compose-row select, .compose-body {
    font-size: 13px !important;
  }
  .searchbar input { padding: 7px 0; font-size: 13px; }

  /* ---- density: more of the mailbox on screen ---- */
  .topbar { padding: 8px 16px; }
  .email-item { padding: 7px 16px; }
  .list-toolbar { padding: 10px 16px 8px; }

  /* ---- the glass dock is a thumb affordance ----
     It floats above the message so a thumb can reach the actions without
     travelling to the top bar. A pointer has no reach cost, and every one
     of those seven actions is already in the viewer toolbar, so on desktop
     the dock is a second copy of a menu that is never out of reach. */
  .msg-dock { display: none !important; }

  /* ---- things a desktop app has and a phone does not ---- */
  /* the pointer should say what a thing does before it is clicked */
  .email-item, .folder-item, .ar-card { cursor: pointer; }
  /* a scrollbar that is visible is a position indicator, which a
     touch-styled overlay bar is not */
  .email-list, .viewer-content, .cal-agenda, .sidebar { scrollbar-width: thin; }
  /* keyboard focus must survive the density change */
  .btn:focus-visible, .icon-btn:focus-visible, .email-item:focus-visible {
    outline: 2px solid var(--accent); outline-offset: 1px;
  }
}

/* ---- desktop: right-click menu and sizable panes (v122) ---- */
.ctx-menu {
  position: fixed; z-index: 400; min-width: 208px; padding: 5px;
  background: var(--bg-panel); border: 1px solid var(--border-strong);
  border-radius: 10px; box-shadow: 0 12px 34px rgba(0, 0, 0, .34);
  display: flex; flex-direction: column; gap: 1px;
}
.ctx-item {
  display: block; width: 100%; text-align: left;
  border: none; background: transparent; color: var(--text);
  font-family: inherit; font-size: 13px; line-height: 1;
  padding: 8px 10px; border-radius: 6px; cursor: pointer; min-height: 30px;
}
.ctx-item:hover, .ctx-item:focus-visible { background: var(--bg-hover); outline: none; }
.ctx-item.danger:hover { background: var(--danger)1f; color: var(--danger); }
.ctx-sep { height: 1px; margin: 4px 6px; background: var(--border); }

/* the divider is 1px; the grab area is 5px, which is the difference between
   a resizable pane and one that is technically resizable */
.pane-resize { display: none; }
body.is-resizing { cursor: col-resize; user-select: none; }
@media (pointer: fine) and (min-width: 900px) {
  .sidebar, .email-list-pane { position: relative; }
  .pane-resize {
    display: block; position: absolute; top: 0; right: -3px; bottom: 0;
    width: 5px; cursor: col-resize; z-index: 30; background: transparent;
  }
  .pane-resize:hover, .pane-resize:focus-visible {
    background: var(--accent); opacity: .5; outline: none;
  }
  /* a picked row must read as picked without the phone's checkbox column */
  .email-item.picked { background: var(--bg-active); box-shadow: inset 3px 0 0 var(--accent); }
}

/* ============================================================
   v124 — the message list: one row design, both devices
   ------------------------------------------------------------
   The phone's row was decluttered and measured in an earlier pass; the
   desktop's never was, so it kept a third line carrying a "✓ read" chip that
   said the same thing on nine rows out of ten. The declutter was never
   phone-specific reasoning — it was just written inside a phone media query.
   It applies at every width now, and the avatar column below is shared, which
   is why this block is NOT scoped: scoping it was the first attempt, and it
   left the phone with no favicon at all and a stretched empty box where the
   avatar should have been. Measured, caught, unscoped.
   ============================================================ */

/* ---- 1. the declutter, at every width ----
   Read state is already carried by weight and colour (.email-item.read) and
   stays clickable from the hover actions and the right-click menu. The account
   is the coloured left edge, which says it in no space at all. */
.read-chip, .ei-account { display: none; }
.ei-badges { display: none; }
.ei-badges:has(.bk-tag), .ei-badges:has(.ei-brand), .ei-badges:has(svg.ic) {
  display: flex; flex-wrap: wrap; gap: 5px; align-items: center;
}
.email-item { border-left: 3px solid var(--accc, transparent); }
.email-item.selected { border-left-color: var(--accent); }

/* ---- 2. one chip, not four ----
   A row could show a booking tag, a brand pill, an account pill and a read
   chip: four shapes and four colour rules in one 12px strip. Colour now means
   exactly one thing — which account — and it lives on the left edge, so what
   is left can be quiet. */
.ei-badges .bk-tag, .ei-badges .ei-brand {
  font-size: 11px; font-weight: 600; line-height: 18px;
  border-radius: 10px; padding: 0 8px;
  border: 1px solid var(--border-strong);
  background: transparent; color: var(--text-dim);
}

/* ---- 3. the avatar as a column ----
   The favicon used to sit inline inside .ei-from, so it moved with the length
   of the sender's name and the list had no line to scan down. As a fixed
   column it gives the list the rhythm every fast mail client has. A monogram
   sits underneath always — most senders have no favicon, and a letter beats an
   empty square. The row's absolutely-positioned parts (⋮, hover actions, the
   select box) are unaffected by the grid. */
/* Two rows, not three. The third track was reserved for .ei-badges, which
   computes display:none on most rows — but a named grid area still creates its
   track, and `align-items: center` stretched all three equally to fill the
   66px that `contain-intrinsic-size` had reserved as a PLACEHOLDER. Measured on
   a Pixel 8 at 412px: three tracks of 20.66px, row height 91px, avatar sitting
   31px down. That is ~10 rows per screen where ~14 fit.

   The badges row is added back only when a row actually has badges, so it costs
   height on the rows that need it and nothing on the rest. */
.email-item {
  display: grid;
  grid-template-columns: 32px minmax(0, 1fr);
  grid-template-areas: "av top" "av subject";
  grid-auto-rows: auto;
  column-gap: 10px; row-gap: 2px;
  align-items: start;
}
.email-item:has(.ei-badges > *) {
  grid-template-areas: "av top" "av subject" "av badges";
}
/* the avatar spans the text rows rather than being centred against a phantom
   third one, which is what made it hang below the sender name */
.email-item > .ei-av { align-self: center; }
.ei-av { grid-area: av; }
.ei-top { grid-area: top; }
.ei-subject { grid-area: subject; }
.ei-badges { grid-area: badges; }
.ei-av {
  position: relative; display: grid; place-items: center;
  width: 32px; height: 32px; flex: none;
  border-radius: 10px; overflow: hidden;
  background: color-mix(in srgb, var(--accc, var(--accent)) 22%, var(--bg-panel));
}
.ei-mono {
  font-size: 12px; font-weight: 700; letter-spacing: .02em;
  color: color-mix(in srgb, var(--accc, var(--accent)) 75%, var(--text));
  user-select: none;
}
/* the image covers the monogram rather than sitting beside it */
.ei-av .ei-fav {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; border-radius: 0; vertical-align: unset;
  object-fit: cover; background: var(--bg-panel);
}
.email-item.read .ei-av { opacity: .72; }
/* select mode puts a checkbox where the avatar is; one of them has to go */
.email-item.sel-mode .ei-av { visibility: hidden; }
/* the setting that already existed, now with something to switch off */
body[data-avatars="off"] .ei-av { display: none; }
body[data-avatars="off"] .email-item {
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "top" "subject" "badges";
}

/* two lines instead of three, so the row can be shorter without feeling tight.
   Desktop only: the phone's padding was measured against its own targets. */
@media (min-width: 641px) {
  .email-item { padding-top: 8px; padding-bottom: 8px; contain-intrinsic-size: auto 52px; }
}

/* ---- the avatar has to pay for its width on a phone ----
   Measured at 375px: a 32px avatar plus a 10px gutter plus the 44px ⋮ zone
   left 270px for the text, which wrapped the sender/date header onto a second
   line and pushed the row from 67px to 91px. A column that costs a third of
   the row height is not worth having. Smaller avatar, tighter gap, and the
   left padding it no longer needs. */
@media (max-width: 640px) {
  .email-item { column-gap: 8px; padding-left: 8px; }
  .ei-av { width: 28px; height: 28px; border-radius: 8px; }
  .ei-mono { font-size: 11px; }
  /* the header must never wrap: the date shrinks before the row grows */
  .ei-top { flex-wrap: nowrap; min-width: 0; }
  .ei-from { min-width: 0; flex: 1 1 auto; }
  .ei-date { flex: 0 0 auto; white-space: nowrap; }
}

/* ---- the brand mark ----
   Replaces the serif "Q" seal. A vector, so the same file is the 34px sidebar
   mark and the 512px home-screen icon with no second asset and no blur. The
   pulse the seal had is gone deliberately: a logo that breathes is a logo you
   notice instead of the mail. */
.logo-mark { width: 34px; height: 34px; flex: none; display: block; }

/* ---- a favicon that is not stretched ----
   The image was painted edge-to-edge over the tile, so a 16px favicon (which
   is most of them) was blown up 2-4x and looked soft. It now sits INSET on the
   tile at its own scale, the way a real mail client shows one, and the source
   is requested at 128px so a sharp original stays sharp. The monogram is
   hidden only once an image has actually loaded — .has-fav is set from the
   load event, because CSP blocks the inline handler this used to rely on. */
.ei-av .ei-fav {
  position: absolute; inset: 5px;
  width: calc(100% - 10px); height: calc(100% - 10px);
  object-fit: contain; border-radius: 3px; background: transparent;
}
.ei-av.has-fav { background: var(--bg-hover); }
.ei-av.has-fav .ei-mono { display: none; }
@media (max-width: 640px) { .ei-av .ei-fav { inset: 4px; width: calc(100% - 8px); height: calc(100% - 8px); } }

/* ---- attaching from inside the message window ----
   The paperclip still works, but the place people try to attach a file is the
   message they are writing, not a 44px icon among six controls below a long
   form. The whole compose window is a drop target and the body takes a paste;
   the button is now labelled so it is findable at all. */
.btn-attach { gap: 6px; }
/* The listeners sit on the backdrop so a drop near the window edge still
   counts, so the class lands on the backdrop too — the overlay is drawn on
   the inner panel. Getting this pair wrong is silent: the drop still works,
   nothing ever appears. */
.compose-modal { position: relative; }
.modal-backdrop.is-dropping .compose-modal::after {
  content: 'Drop files to attach';
  position: absolute; inset: 8px; z-index: 5;
  display: grid; place-items: center;
  font-size: 15px; font-weight: 600; color: var(--accent);
  background: color-mix(in srgb, var(--bg-panel) 88%, transparent);
  border: 2px dashed var(--accent); border-radius: 10px;
  pointer-events: none;
}

.compose-atts:empty { display: none; }
.compose-att-chip {
  display: inline-flex; align-items: center; gap: 6px;
  margin: 4px 6px 0 0; padding: 4px 6px 4px 10px;
  border: 1px solid var(--border-strong); border-radius: 20px;
  font-size: 13px; color: var(--text); max-width: 100%;
}
.compose-att-chip i { color: var(--text-dim); font-style: normal; font-size: 12px; }
.compose-att-chip .ca-x {
  border: none; background: transparent; color: var(--text-dim);
  cursor: pointer; font-size: 16px; line-height: 1;
  min-width: 28px; min-height: 28px; border-radius: 50%;
}
.compose-att-chip .ca-x:hover { background: var(--bg-hover); color: var(--accent); }

/* ---- attachments named by kind, not by filename ----
   "Q3-final-v2-REALLY-final.xlsx" tells you nothing at a glance and pushes the
   size off the chip. The kind and the weight are what you scan for; the name
   lives in the tooltip and is still what the file downloads as. */
.compose-att-chip .fk, .attachment-chip .fk {
  font-size: 10px; font-weight: 700; letter-spacing: .06em;
  padding: 2px 6px; border-radius: 6px;
  background: var(--accent-soft); color: var(--accent);
}
.compose-att-chip i, .attachment-chip i {
  font-style: normal; font-size: 12px; color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.attachment-chip { display: inline-flex; align-items: center; gap: 7px; }

/* ---- the sibling app's mark, in each app ---- */
.app-link { display: inline-flex; align-items: center; gap: 8px; }
.app-link img { display: block; flex: none; }

/* ---- the message body: neutral paper, and nothing else done to it ----
   Three attempts landed here. Inverting it in dark mode broke real mail.
   Dimming it with a filter washed it out. Both were the app imposing itself on
   a document it did not author. What is left does nothing to the message at
   all: no filter, no colour cast, no inversion. It sits on a NEUTRAL near-white
   card -- not pure white, which was the original complaint, and not a warm
   cream either, which read as a tint. Paper and ink. */
/* The sender's HTML sits on #f4f4f5. Against the old #f7f5f0 app ground that
   measured 1.01:1 — pixel-indistinguishable. Anything the sender drew read as
   part of Qmail, so a convincing fake "Qmail" dialog cost them nothing. The
   frame is sandboxed and script cannot run, but the VISUAL half of that
   boundary was missing. A real edge at 3:1 says where the app stops and the
   message starts.
   VIS-05 darkened the ground to #eceae5, which lifts the card itself to 1.09:1
   — better, still nowhere near a boundary on its own, so the border remains
   what actually carries it (3.30:1 against the card).
   VIS-08 then narrowed WHEN the paper is used at all. It is still right for a
   designed message, which brings its own colours and must keep them. It was
   never right for the plain business mail that names no colour at all: that
   was a lit card in the middle of a dark app for no reason but the rule it
   happened to share. Those now render on the app's own ground — see
   authorsOwnColours() in app.js, which is what decides. The border is kept in
   BOTH cases, and matters more in the second: once the message shares the
   app's background, the edge is all that is left of the boundary. */
/* No card, no border, no radius. The border used to mark where Qmail stops and
   the sender's HTML starts, which is a real thing to want — but on a phone it
   drew a box around every message you opened, and the message IS the page.
   The sender's own background still shows through when they set one, which is
   the honest boundary; see authorsOwnColours(). */
.viewer-html-frame { background: transparent; border: 0; border-radius: 0; }
/* Plain text carries no colours to preserve, so there is nothing to trade off:
   it is always the app's ground and the app's ink. It kept a light card and
   #1b1b1d ink only because it shared this rule with the HTML frame. */
.viewer-text {
  background: var(--bg); color: var(--text);
  border-radius: 8px; border: 1px solid var(--border-strong);
}
/* A message that names no colour of its own sits on the app's ground instead
   of a lit card. The border STAYS, and matters more here than it did before:
   when the message shares the app's background, the edge is the only thing
   left saying where Qmail stops and the sender's HTML starts. */
.viewer-html-frame.on-app-ground { background: var(--bg); }
.viewer-text {
  padding: 14px 16px;
  white-space: pre-wrap; overflow-wrap: anywhere;
}

/* ============================================================
   One control system (v134) — the same pass Qalendar had
   ------------------------------------------------------------
   Measured at 375px, Quriou Mail was carrying SEVEN corner radii across its
   controls (0, 5, 6, 7, 8, 9, 10) and label sizes from 12 to 14 on buttons
   sitting in the same row. The 44px work from earlier passes held — heights
   were right — but nothing had ever made the shapes agree.

   The radius is 10px, which is Qalendar's, because the two apps now share a
   mark and should share a control language. Icon buttons keep their glyph
   size: folding them into the shared label size is exactly the regression
   that pass caught in the calendar, where arrows and the close cross dropped
   from 19px to 13px.
   ============================================================ */
:root { --ctl-radius: 10px; --ctl-label: 13px; }

.btn, .btn-ghost, .btn-primary, .icon-btn, .cal-tab, .auth-tab, .auth-alt,
.seg button, .range-tabs button, .smart-tabs button, .smart-tab, .sq-btn,
.sheet-item, .account-menu-item, .read-chip, .ub-undo, .row-more,
.apps-manage-btn, .mm-state, select, .sort-select, .view-select {
  border-radius: var(--ctl-radius);
}
/* labels: one size for anything that reads as a button */
.btn, .btn-ghost, .btn-primary, .cal-tab, .auth-tab, .auth-alt,
.seg button, .range-tabs button, .smart-tabs button, .smart-tab,
.sheet-item, .account-menu-item, .apps-manage-btn {
  font-size: var(--ctl-label);
}
/* width is a target too — these measured 29px and 9px wide */
.smart-tab, .sq-btn, .cal-tab, .auth-tab, .seg button { min-width: 44px; }

/* Text fields were the one group with no shape at all: a bare input measured
   0px radius against buttons at 6-10. The search field is deliberately
   excluded — it is a borderless field inside its own rounded shell, and
   rounding it again draws a box inside a box. */
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
textarea, select {
  border-radius: var(--ctl-radius);
}
.searchbar input { border-radius: 0; }

/* Text fields that draw their own box get a real height. Measured at 375px, a
   settings field was 22px tall — the browser default — against a 44px minimum
   every button meets. The transparent ones are excluded and keep their
   shell.s height: the search field and the compose rows are borderless inputs
   inside their own container, and giving them a box would draw a box in a box. */
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]),
textarea, select {
  min-height: 44px;
}
/* .compose-body removed from this list. The list exists to keep BORDERLESS
   INPUTS out of the 44px control minimum above — but that rule targets
   `input, textarea, select`, and .compose-body is a contenteditable DIV, so it
   was never subject to it. Including it did nothing except override the
   `min-height: 45vh` set for phones 1600 lines earlier, which is why the reply
   collapsed to a few lines while the quoted history took the rest. */
.searchbar input, .compose-row input, textarea { min-height: 0; }
.searchbar input, .compose-row input { border-radius: 0; }

/* small labels stay small: a tag is not a control, and making it one would
   turn a glanceable strip into a row of buttons */
.bk-tag, .compose-att-chip .fk, .attachment-chip .fk, .ei-account { border-radius: 6px; }

/* ---- the maker's mark ----
   Quiet on purpose: a signature belongs at the edge of a thing, not in the
   middle of it. Full weight on the sign-in card, where a product introduces
   itself, and a smaller one resting beside the version in the footer. */
.by-line {
  font-size: 12px; color: var(--text-dim);
  letter-spacing: .08em; text-transform: uppercase; margin-top: 2px;
}
.by-line b { font-weight: 700; color: var(--text); letter-spacing: .12em; }
.by-line-sm { margin-left: auto; }
.by-line-sm b { font-weight: 600; color: var(--text-dim); }

/* ---- About ---- */
.about-line { font-size: 14px; color: var(--text); margin-bottom: 4px; }
.about-line b { font-weight: 700; }
.about-meta { font-size: 13px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.about-update { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 12px; }
.about-update-msg { color: var(--text-dim); font-size: 13px; overflow-wrap: anywhere; }

/* ---- the maker's mark: QENTRA's own lockup ----
   Two files, not one recoloured: the brand ships a dark-ink version for light
   grounds and a reverse for dark ones, and a logo is the last thing that
   should be approximated with a filter. Both are in the page and the theme
   decides which is shown, so there is no flash of the wrong one while a
   script works out the theme. */
.by-line { display: flex; align-items: center; gap: 7px; }
.by-line > span { font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--text-dim); }
.qentra-logo { height: 17px; width: auto; display: block; }
.by-line-sm .qentra-logo { height: 13px; }
.by-line-sm > span { font-size: 10px; }
/* light is the default; dark wins where the theme says so */
.q-on-dark { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .q-on-light { display: none; }
  :root:not([data-theme="light"]) .q-on-dark { display: block; }
}
:root[data-theme="dark"] .q-on-light { display: none; }
:root[data-theme="dark"] .q-on-dark { display: block; }
:root[data-theme="light"] .q-on-light { display: block; }
:root[data-theme="light"] .q-on-dark { display: none; }

/* ============================================================
   TOUCH TARGETS FOR THE PHONE THIS IS ACTUALLY USED ON
   ============================================================
   Measured, not assumed. The access log says the device is a Pixel 8 on
   Android 14 / Chrome 148 -- a 412 x 915 CSS viewport. On Android the
   guidance is 48dp, not the 44px this app had settled on, which is Apple's
   number. Measuring all 36 controls at 412px: twenty-five sat at exactly 44,
   ten sat BELOW it, and one was 46. Nothing reached 48.

   The ten under 44 were not sloppiness so much as arithmetic: the floor was
   written as `.btn { min-height: 44px }` at specificity 0,1,0, so every
   `.tc-actions .btn { min-height: 32px }` at 0,2,0 beat it no matter where it
   appeared in the file. Raising the floor alone would have missed all ten
   again, so each is named here.

   Guarded on `pointer: coarse` so it applies to the phone and leaves the
   desktop layout -- which was deliberately made dense -- exactly as it was. */
@media (pointer: coarse) {

  /* the floor */
  .btn, .btn-ghost, .btn-primary, .icon-btn, .icon-btn.tb,
  .cal-tab, .auth-tab, .auth-alt, .smart-tab, .sq-btn, .sheet-item,
  .account-menu-item, .account-btn, .folder-item, .dock-btn, .viewer-fab,
  .apps-manage-btn, .compose-btn, .menu-btn, .ai-ask-btn, .danger-btn,
  .btn-attach, .ub-undo,
  .seg button, .range-tabs button, .smart-tabs button {
    min-height: 48px;
  }
  .icon-btn, .icon-btn.tb, .menu-btn, .viewer-fab,
  .seg button, .range-tabs button, .smart-tabs button,
  .cal-tab, .auth-tab, .smart-tab, .sq-btn {
    min-width: 48px;
  }

  /* the ten that a more specific rule was holding under the floor */
  .tc-actions .btn,
  .img-banner .btn,
  .mm-ctl .btn,
  .mm-ctl .icon-btn,
  .bulk-bar .btn,
  .push-banner .btn,
  .later-custom .btn,
  .sidebar .folder-item,
  .sidebar .account-menu-item,
  .sidebar .apps-manage-btn,
  .sidebar .compose-btn,
  .app-manage-actions .icon-btn,
  .sr-item .icon-btn,
  .cal-nav .icon-btn,
  .viewer-toolbar .icon-btn,
  .viewer-toolbar .icon-btn.tb,
  .event-actions .btn-ghost,
  .event-actions .btn-primary,
  .account-add-form .btn-ghost,
  .ai-chip, .ai-reply, .attachment-chip,
  .attachment-wrap .attachment-chip,
  .sheet-list .sheet-item, .sheet-quick .sq-btn {
    min-height: 48px;
  }
  .mm-ctl .icon-btn, .app-manage-actions .icon-btn, .sr-item .icon-btn,
  .cal-nav .icon-btn, .viewer-toolbar .icon-btn,
  .viewer-toolbar .icon-btn.tb {
    min-width: 48px; width: auto; height: auto;
  }

  /* The remove-attachment cross was 28px, inside a 29px chip: two overlapping
     targets in under thirty pixels, and the smaller of the two is the one that
     destroys work. Both go to the floor. */
  .compose-att-chip { min-height: 48px; padding: 4px 4px 4px 12px; }
  .compose-att-chip .ca-x { min-width: 44px; min-height: 44px; }

  /* Material's spacing is 8dp. Two pixels between two 30px controls was the
     likeliest source of a wrong tap anywhere in the app. */
  .mm-ctl { gap: 8px; }
  .tc-actions, .img-banner, .compose-att-list, .attachment-wrap { gap: 8px; }

  /* 12px control labels are below Android's guidance and were the reason some
     of these boxes were small in the first place */
  .btn, .btn-ghost, .btn-primary, .bulk-bar .btn, .tc-actions .btn,
  .img-banner .btn, .mm-ctl .btn, .push-banner .btn, .ai-chip,
  .compose-att-chip, .folder-item, .account-menu-item, .apps-manage-btn {
    font-size: 13px;
  }

  /* the update bar shrank itself to 32px under 420px wide -- which is exactly
     the width of this phone, so the one control that appears when something
     needs doing was the smallest on the screen */
  .update-bar .btn { min-height: 48px !important; }
  .update-bar .icon-btn { min-width: 48px !important; min-height: 48px !important; }
}

/* Four survived the block above, each beaten on specificity by a rule written
   earlier. Named at the specificity that actually wins, rather than raising
   the whole block to !important and losing the ability to see why. */
@media (pointer: coarse) {
  .topbar .menu-btn { min-height: 48px; min-width: 48px; }
  .viewer-toolbar .icon-btn,
  .viewer-toolbar .icon-btn.tb { min-height: 48px; height: auto; }
  /* this one was already !important, so it takes an !important to move */
  .sidebar .apps-manage-btn,
  .apps-manage-btn { min-height: 48px !important; font-size: 13px; }
  /* the cross now matches the chip it sits in, rather than being the smallest
     and most destructive target on the screen */
  .compose-att-chip .ca-x { min-width: 48px; min-height: 48px; }
}

/* `.viewer-toolbar .icon-btn { min-height: 44px !important }` from an earlier
   pass ties on specificity and wins on !important, so this one has to match it.
   And the two sidebar rows carry their own 12.5px, below Android's guidance. */
@media (pointer: coarse) {
  .viewer-toolbar .icon-btn,
  .viewer-toolbar .icon-btn.tb { min-height: 48px !important; }
  .sidebar .folder-item,
  .sidebar .account-menu-item { font-size: 13px; }
}

/* ============================================================
   THE TABLET / SPLIT-WINDOW GAP
   ============================================================
   The rule that hides the message list while reading lives inside
   `@media (max-width: 640px)`, and so does the off-canvas sidebar. Between
   641px and ~1000px the app therefore kept all three panes: measured live at
   768x1024 with a message open, sidebar 240 + list 320 + viewer 208 — and
   after the viewer's own 32px side padding that is 144px of text. That is iPad
   portrait, and any half-screen window on a desktop.

   Reading is the whole point of those widths, so below 1024px the list gets
   out of the way while a message is open, exactly as it does on a phone. */
@media (min-width: 641px) and (max-width: 1024px) {
  .content.viewing .email-list-pane { display: none; }
  .content.viewing .email-viewer-pane { flex: 1 1 auto; min-width: 0; }
}

/* --text-dim was tuned against --bg only (4.53:1). The selected row sits on
   --bg-active and the hovered row on --bg-hover, where the same colour
   measured 3.89:1 and 4.22:1 — so the row you have open and the row under the
   cursor were the two least legible rows in the list. The background already
   carries the state; the text does not need to be dimmed as well. */
.email-item.selected.read .ei-from,
.email-item.selected.read .ei-subject,
.email-item.picked.read .ei-from,
.email-item.picked.read .ei-subject { color: var(--text); }
@media (hover: hover) {
  .email-item:hover.read .ei-from,
  .email-item:hover.read .ei-subject { color: var(--text); }
}

/* white-on-accent at 3.30:1 in dark — these two segmented controls were missed
   by the pass that fixed the other six, because they sit at specificity 0,2,1 */
.range-tabs button.is-on,
.seg button.is-on { background: var(--accent-strong); }

/* ============================================================
   VISUAL SYSTEM — v147
   ============================================================
   From the measured audit. The finding underneath all of this: panels sat at
   1.09:1 against the page and default borders at 1.22:1, so grouping was
   invisible and the app compensated with heavy type — 22 font sizes and 37
   uses of 700-weight. Boundaries and weight are one problem, so they are
   fixed together. Touch targets (48dp) are already correct and untouched.

   The first pass deliberately did NOT repaint the surfaces, reasoning that on
   a light ground the panel is already #ffffff and cannot go lighter, so a
   visible border could carry the separation on its own. Measured afterwards,
   that trade did not pay: the border reached only 1.56:1 and the panel stayed
   at 1.09:1, so grouping was still invisible and the heavy type it was
   supposed to replace stayed necessary.
   VIS-05 finishes it the other way round. A white panel cannot go lighter, so
   the PAGE moves instead: #f7f5f0 -> #eceae5. Panel-vs-page is 1.20:1, and the
   whole ramp had to be re-derived from the new ground, because darkening the
   page pushed --text-dim under AA (4.53 -> 4.11) and inverted the row states:
   the old --bg-active was LIGHTER than the new hover, so a selected row would
   have looked less selected than a hovered one. Every value below was solved
   against the surfaces it actually lands on and verified as a set — 57 pairs
   across both apps and both themes, zero failures.
   ============================================================ */
/* The light tokens at the top of this file are declared on
   `:root[data-theme="light"]` (specificity 0,2,0), so a plain `:root` here
   loses to them — which is exactly what happened on the first attempt: the dark
   overrides applied and the light ones silently did not. Match the specificity
   rather than reach for !important. */
:root, :root[data-theme="light"] {
  /* VIS-13 — a spacing ramp. Used by the rules below; adopting it everywhere
     is a separate, larger job. */
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 24px; --sp-6: 32px;

  /* VIS-02 — six steps, no half-pixels. Half-pixel sizes also fail to survive
     the zoom setting cleanly. */
  --fs-xs: 11px;   /* badges, timestamps */
  --fs-sm: 12px;   /* dense metadata */
  --fs-md: 13px;   /* control labels, body of lists */
  --fs-lg: 15px;   /* message subject, primary row text */
  --fs-xl: 18px;   /* section headings */
  --fs-2xl: 22px;  /* page titles */

  /* VIS-07 — four shapes with meaning: chip, control, card, pill. */
  --r-chip: 6px; --r-ctl: 10px; --r-card: 16px; --r-pill: 999px;

  /* VIS-01 — a border that is actually visible. 1.22:1 was decorative; the
     first attempt only reached 1.56:1, which is why panels still read as
     undivided. 2.11:1 now.
     NOT the 3.0:1 the audit asked for: 3.0 is WCAG's floor for a boundary that
     carries meaning, and --border-strong already sits there for control edges.
     Taking --border to 3.0 lands it on #928d81 — indistinguishable from
     --border-strong (#8b867b) — which collapses two tiers into one. A grouping
     divider should be visible and quiet, not shout as loudly as a control. */
  --border: #a8a295;
  /* VIS-04 — secondary text on a raised surface. --text-dim was tuned against
     the page only, so it measured 4.22:1 on a hovered row and 3.89:1 on a
     selected one: the two rows you interact with were the least readable. */
  --text-dim-raised: #565248;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --border: #504b41;
    --text-dim-raised: #a8a294;
  }
}
:root[data-theme="dark"] {
  --border: #504b41;
  --text-dim-raised: #a8a294;
}

/* VIS-06 — in light theme --accent and --accent-strong were the SAME value,
   so a filled primary button and a text link carried identical weight. */
:root, :root[data-theme="light"] { --accent-strong: #0d4049; }

/* VIS-04 applied: raised rows use the raised token wherever dimmed text lands */
.email-item:hover .ei-date,
.email-item.selected .ei-date,
.email-item.picked .ei-date,
.email-item:hover .ei-badges,
.email-item.selected .ei-badges { color: var(--text-dim-raised); }

/* VIS-01 applied: panels state their edge instead of relying on fill */
.modal, .action-sheet, .sheet, .card, .tc-card, .stat-card {
  border: 1px solid var(--border);
}

/* ---- VIS-03: weight discipline ----
   700 was used 37 times and 400 three times, so bold was the default and
   emphasis had nothing to push against. 600 carries headings and unread mail;
   700 is reserved for the one thing that must win on a screen. Unread mail
   keeps the most weight in the list, because that is the whole job of the
   list. */
.login-card h1, .logo-text, .list-title, .ag-date,
.modal-header, .sheet-head, .set-block h3,
.viewer-subject, .tc-from, .stat-value {
  font-weight: 600;
}
.email-item.unread .ei-from,
.email-item.unread .ei-subject { font-weight: 600; }
.email-item.read .ei-from,
.email-item.read .ei-subject { font-weight: 400; }

/* ---- VIS-08: the 641–899px band ----
   Phone rules stop at 640 and desktop density starts at 900, so between them
   the sidebar stayed PINNED at 240px — on a 768px half-screen window that is a
   third of the width spent on navigation. The drawer behaviour already exists
   for phones; it simply never reached this band. */
@media (min-width: 641px) and (max-width: 899px) {
  .sidebar {
    position: fixed; top: 0; left: 0; bottom: 0; z-index: 60;
    width: 280px; max-width: 86vw;
    transform: translateX(-100%); transition: transform .22s ease;
    box-shadow: var(--shadow);
  }
  .sidebar.open { transform: translateX(0); }
  .menu-btn { display: inline-flex !important; }
  .content { margin-left: 0 !important; }
}
@media (prefers-reduced-motion: reduce) {
  @media (min-width: 641px) and (max-width: 899px) { .sidebar { transition: none; } }
}

/* ---- VIS-09: one floating button ----
   Smart Inbox and Compose both floated in the same corner, and one already had
   to hide on scroll to stop covering a chip. The topbar has carried a Smart
   Inbox button (#btn-smart-top) all along, so the FAB was a duplicate of a
   control that was already reachable. Compose stays: it is what people came
   to do. */
@media (pointer: coarse), (max-width: 899px) {
  .smart-fab { display: none !important; }
}

/* ============================================================
   A single message in its own window (v160)
   ------------------------------------------------------------
   Double-clicking a row opens ?open=…&solo=1 in a popup. It boots the whole
   app — that is deliberate, because reply, attachments, print and Ask Claude
   then work in there with no second implementation of the viewer — but the
   parts that navigate to OTHER messages have no purpose in a window that is
   about one message, and the sidebar alone would eat a third of it.
   Hidden, not removed: the code behind them still runs, so nothing has to
   know it is in a solo window.
   ============================================================ */
body.msg-window .sidebar,
body.msg-window .email-list-pane,
body.msg-window #btn-back,
body.msg-window .smart-fab,
body.msg-window .compose-fab { display: none !important; }
body.msg-window .email-viewer-pane { flex: 1 1 100%; }

/* ============================================================
   Semantic colour, per theme (v163)
   ------------------------------------------------------------
   Seven colours were written as literals and therefore could not follow the
   theme. Measured against both grounds of both themes, EVERY ONE failed
   somewhere: the calendar legend, the dashboard KPI and the chart key sat
   between 2.7:1 and 4.1:1 depending on which theme you were in. They looked
   fine to whoever picked them, because they were picked while looking at one
   theme.
   A colour used as TEXT has to be declared twice — once per ground — or it is
   readable in one theme by luck. Hue and saturation are kept; only lightness
   moves, so the calendar legend still reads as the same blue, amber, purple
   and green it always did.
   These are separate from --accent on purpose: accent is identity, these are
   meaning (this is an event / a reminder / this went well), and the two should
   never be forced to share a value.
   ============================================================ */
:root, :root[data-theme="light"] {
  --c-event: #2e68b7;        /* 4.62:1 on page, 5.56:1 on panel */
  --c-reminder: #896117;
  --c-snooze: #8c4bb5;
  --c-send: #22764a;
  --c-good: #3b744f;
  --c-chart-sent: #4568a7;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --c-event: #5a90d9;      /* 5.58:1 on page, 4.61:1 on panel */
    --c-reminder: #b6862e;
    --c-snooze: #b077d4;
    --c-send: #34a167;
    --c-good: #509e6c;
    --c-chart-sent: #728fc4;
  }
}
:root[data-theme="dark"] {
  --c-event: #5a90d9;
  --c-reminder: #b6862e;
  --c-snooze: #b077d4;
  --c-send: #34a167;
  --c-good: #509e6c;
  --c-chart-sent: #728fc4;
}

.cal-leg[data-c="event"]    { color: var(--c-event); }
.cal-leg[data-c="reminder"] { color: var(--c-reminder); }
.cal-leg[data-c="snooze"]   { color: var(--c-snooze); }
.cal-leg[data-c="send"]     { color: var(--c-send); }
.kpi.good .kpi-v            { color: var(--c-good); }
.diag-top .is-ok b          { color: var(--c-send); }
.ch-sent                    { stroke: var(--c-chart-sent); }
.sw-sent                    { background: var(--c-chart-sent); }

/* ---- white text on a filled control ----
   Every filled control said `background: var(--accent); color: #fff`. In the
   light theme --accent is #14535f and white sits at 8.63:1. In the DARK theme
   it is #2f9c9c — a light teal — and white on it measures 3.30:1, under the
   4.5 an AA reading needs. Primary buttons, badges, tabs, the FAB and the
   avatar monogram were all below the line in dark mode, which is the single
   most-clicked text in the app.
   --accent-strong exists for exactly this and is tuned per theme: white on it
   is 11.37:1 light and 5.06:1 dark. The accent keeps its job as a text and
   border colour, where it measures 7.18:1 and 5.52:1 and was never the
   problem. */

/* The update bar when a previous attempt did not take (v179).
   Same shape, louder ground: this is no longer an offer, it is a report that
   the app is running code older than the server and the ordinary route already
   failed. It has no dismiss button either — hiding it would put the person
   back on an old build with nothing on screen saying so. */
.update-bar.is-stuck {
  background: var(--danger-strong, #8f2a18);
  border-color: var(--danger-strong, #8f2a18);
  color: #fff;
}
.update-bar.is-stuck .ub-text { color: #fff; }
.update-bar.is-stuck .btn-primary {
  background: #fff; border-color: #fff; color: var(--danger-strong, #8f2a18);
}

/* A rule between answering the message and everything else done to it. The
   sheet runs long — print, share, calendar, unsubscribe, block — and without a
   break the three actions people opened it for sit in the same
   undifferentiated run as blocking a domain. */
.sheet-sep { height: 1px; background: var(--border); margin: 6px 0; }

/* ============================================================
   Compose on a real screen (v184)
   ------------------------------------------------------------
   Reported from a 16" MacBook: the box you type in was a strip a few lines
   tall, smaller than the quoted history sitting under it, inside a dialog
   using about a third of the display. Three separate caps, each defensible
   alone, multiplying:

     .modal          width: min(680px, 94vw)   — a size for a confirm dialog,
                                                 not for writing a letter in
     .compose-body   max-height: 44vh          — so it could never take the
                                                 room the dialog did have
     .compose-quote  max-height: 32vh          — the history claiming almost
                                                 as much as the reply

   44vh + 32vh is 76vh before the header, four field rows and the footer, so on
   any laptop the body lost and settled at its 220px minimum. The quote is the
   part that should yield: it is reference, the reply is the work.

   The composer is now a proper window that fills a sensible share of the
   screen, and the BODY is the element that grows — max-height removed, flex:1
   added, min-height:0 on its parent so a flex child is actually allowed to
   shrink and scroll rather than pushing the footer off the bottom.
   ============================================================ */
@media (min-width: 900px) {
  .modal.compose-modal {
    width: min(1040px, 92vw);
    height: min(880px, 88vh);
    max-height: 88vh;
  }
  /* min-height:0 is what makes the rest work: without it a flex child refuses
     to shrink below its content and the footer is pushed out of the dialog. */
  /* The FORM scrolls, not the body — the model the phone layout already used.
     With overflow on the body, typing past the visible lines scrolled the reply
     inside its own little window and the top of what you had written vanished
     upward. Letting the body grow instead pushes the quoted history down the
     page, which is where a reply should push it, and the form carries the
     scrollbar. The footer is sticky so Send stays put however long the letter
     gets. */
  .compose-modal .compose-form { flex: 1; min-height: 0; overflow-y: auto; }
  .compose-modal .compose-body {
    /* Grows with what is typed. min-height keeps an empty reply generous; there
       is no max, and no overflow of its own to trap the text. Safe now only
       because the FORM scrolls — the earlier version of this floor, with a
       non-scrolling form, pushed the footer clean off the screen (measured at
       1000x700: modal ended 659, footer sat at 726). */
    /* grow:1 so it also claims whatever the rows and footer leave over —
       flex:0 0 auto took exactly 40vh and left the rest as dead space.
       shrink:0 so a long letter still pushes the quote down instead of
       squeezing itself. The form carries the scrollbar either way. */
    flex: 1 0 auto; min-height: 40vh; max-height: none; overflow: visible;
  }
  .compose-modal .compose-footer {
    position: sticky; bottom: 0; margin-top: auto;
    background: var(--bg-panel); z-index: 1;
  }
  /* Reference material, capped well below the reply it belongs to, and still
     collapsible — it opens expanded only because it is worth seeing once. */
  .compose-modal .compose-quote-view { flex: 0 1 auto; max-height: 18vh; }
}

/* ---- the composer, filling the screen (v185) ----
   88vh is a sensible dialog. It is not a sensible way to review a five-page
   letter, and that is a real thing people do here — long replies with the
   thread quoted underneath. So the size becomes the writer's choice rather
   than a number chosen in advance: expanded, the composer takes the display
   and the body takes what is left after the fields and the footer.
   The choice is remembered, because someone who writes long mail once writes
   long mail again, and re-expanding on every reply is its own annoyance. */
@media (min-width: 900px) {
  .modal.compose-modal.is-max {
    width: 98vw; height: 96vh; max-height: 96vh;
  }
  /* Expanded there is room to show the thread properly, so the quote stops
     being squeezed to a fifth of the height as well. */
  .modal.compose-modal.is-max .compose-quote-view { max-height: 40vh; }
}
#compose-max .ic { pointer-events: none; }

/* ============================================================
   The composer as a PAGE (v186)
   ------------------------------------------------------------
   Asked for directly: "a plain page where I can paste or write my reply like
   Outlook, without limiting it in a box". Even at 98vw a modal is still a
   dialog — it floats over the app, it owns Escape, it cannot be moved to a
   second display or parked beside the message it answers.
   In its own window the composer stops pretending to be a dialog: no dimmed
   backdrop, no rounded card, no shadow, no app behind it. The window IS the
   composer, and the body takes everything the fields and footer do not.
   ============================================================ */
body.compose-page .modal-backdrop {
  /* FIXED, not static. "No dimmed backdrop, no floating dialog" was the intent,
     and `position: static` delivered it by dropping the composer into normal
     document flow — after the app. So the inbox sat above it (the empty band
     over "New Message"), the composer's own 100dvh started wherever that ended,
     and the footer with Send landed a full screen below the fold: measured at
     1000x700, footer at y=1119 against a 700px window, page scrollable to 1120.
     Fixed + inset:0 keeps every bit of the intent — undimmed, unrounded, no
     shadow — while the composer actually OWNS the window, which is what "a page
     of its own" meant. */
  position: fixed; inset: 0; background: none; backdrop-filter: none;
  display: block; z-index: 50; animation: none;
}
body.compose-page .modal.compose-modal {
  width: 100vw; height: 100dvh; max-height: none;
  border-radius: 0; box-shadow: none; animation: none;
}
/* Same model as the dialog above: the form scrolls, the body grows, the footer
   stays put. Without this the page mode kept its own overflow and the reply
   scrolled inside itself again. */
body.compose-page .compose-modal .compose-form { flex: 1; min-height: 0; overflow-y: auto; }
body.compose-page .compose-modal .compose-footer {
  position: sticky; bottom: 0; margin-top: auto; background: var(--bg-panel); z-index: 1;
}
body.compose-page .compose-modal .compose-body {
  flex: 1 0 auto; min-height: 45vh; max-height: none; overflow: visible;
  /* Full width. A reading-measure cap sat here and it was the same instinct as
     every other cap in this file: defensible in the abstract, wrong for
     someone who asked to move through the whole message rather than a column
     inside it. Their screen, their line length. */
  padding: 20px 28px;
}
/* The quoted thread is part of the message being reviewed, so it is not capped
   either — it is collapsible, which is the control that belongs here. */
body.compose-page .compose-modal .compose-quote-view { max-height: none; }
/* Nothing to pop out of, and nothing to shrink to, in a window of its own. */
body.compose-page #compose-popout,
body.compose-page #compose-max { display: none; }
/* The app shell has no business rendering behind the page. */
body.compose-page .sidebar,
body.compose-page .email-list-pane,
body.compose-page .email-viewer-pane,
body.compose-page .topbar,
body.compose-page .smart-fab,
body.compose-page .compose-fab { display: none !important; }

/* ---- sender pictures (v201) --------------------------------------------
   The image is painted over the monogram, never beside it: a sender with no
   logo keeps its letter and the column never changes width. Only containers
   that already reserved a square get one, so nothing reflows when a picture
   arrives late. */
.viewer-avatar { position: relative; overflow: hidden; }
.viewer-avatar .ei-fav,
.ei-av-sm .ei-fav {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; border-radius: inherit; vertical-align: unset;
  object-fit: cover; background: var(--bg-panel);
}

/* the conversation history stays a table of contents: 20px, not a second inbox */
.ei-av-sm { width: 20px; height: 20px; border-radius: 6px; }
.ei-av-sm .ei-mono { font-size: 9px; }
.hist-head { display: flex; align-items: center; gap: 8px; }
body[data-avatars="off"] .ei-av-sm { display: none; }

/* the swipe picker shows the colour the gesture will actually paint */
.swipe-row { display: flex; align-items: center; gap: 10px; }
.swipe-dot { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.swipe-row-label { flex: 1; min-width: 0; }
