Files
ihasmail/web/src/styles/app.css
T
jcoffey-dev 0db795371e Forward a message as an attachment
Forwarding quoted the original into a new message, which is the right
thing for passing on something to be read and the wrong thing for passing
on something to be looked at. Quoting rewrites the body, drops the
headers, and re-parents the attachments, so a bounce, a phishing report or
anything else where the message itself is the evidence arrived altered.

Forward as attachment sends the message whole, as a message/rfc822 part.
It costs no upload at all: a message's own blobId is its RFC822 blob and
already lives in the account, so this goes through the same by-reference
path as attach-from-Files and a 40 MB message attaches as fast as a small
one.

It is in the message's own menu, the list's right-click menu, and the
overflow on the reply strip at the foot of a thread, which is the one a
thumb finds on a phone.

Two things fixed on the way, both exposed rather than introduced by this.

The filename rule was subject.replace(/[^\w.-]+/g, "_"), and \w without
the u flag is ASCII: every character of a Russian, Japanese or Chinese
subject failed the class, so those messages downloaded as a row of
underscores. What is actually unsafe in a filename is much shorter than
"not ASCII" -- path separators, the names Windows reserves, the control
range -- so the rule now keeps letters from any script and drops only
those. It lives in one place and the .eml download uses it too.

And the composer's attachment chip set overflow/text-overflow on a span,
where neither does anything, so the name never truncated and the size ran
on after it on the same line. Only long names showed it, which is every
.eml named from a subject.
2026-09-01 21:57:11 -07:00

1383 lines
100 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ==========================================================================
ihasmail design system
========================================================================== */
:root {
--font-sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
--fs: 14px;
--radius: 10px;
--radius-sm: 6px;
--radius-lg: 16px;
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.08);
--shadow-2: 0 4px 16px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.08);
--shadow-3: 0 12px 40px rgba(0, 0, 0, 0.18), 0 4px 12px rgba(0, 0, 0, 0.1);
--row-h: 44px;
--topbar-h: 56px;
--sidebar-w: 256px;
--sidebar-w-collapsed: 68px;
--list-w: 42%;
--ease: cubic-bezier(0.2, 0, 0, 1);
/* Light palette */
--bg: #f6f8fa;
--bg-elev: #ffffff;
--bg-sunken: #eef1f4;
--bg-hover: rgba(15, 23, 42, 0.05);
--bg-active: rgba(15, 23, 42, 0.09);
--fg: #111827;
--fg-muted: #5b6472;
--fg-faint: #8b94a3;
--border: #e3e7ec;
--border-strong: #cbd2da;
--accent: #0f766e;
--accent-fg: #ffffff;
--accent-soft: #d9f1ee;
--accent-soft-fg: #0b5750;
--danger: #dc2626;
--danger-soft: #fee2e2;
--warn: #b45309;
--warn-soft: #fef3c7;
--success: #15803d;
--success-soft: #dcfce7;
--link: #0e7490;
--unread-bg: #ffffff;
--read-bg: #f3f5f7;
--selected-bg: #d9f1ee;
--focus-ring: 0 0 0 3px rgba(15, 118, 110, 0.35);
--star: #f59e0b;
--q1: #2563eb;
--q2: #16a34a;
--q3: #9333ea;
--scrollbar: rgba(0, 0, 0, 0.25);
color-scheme: light;
}
:root[data-theme="dark"] {
--bg: #0b1220;
--bg-elev: #111a2b;
--bg-sunken: #0d1524;
--bg-hover: rgba(255, 255, 255, 0.06);
--bg-active: rgba(255, 255, 255, 0.1);
--fg: #e5e9f0;
--fg-muted: #9aa5b8;
--fg-faint: #6b7689;
--border: #1f2a3d;
--border-strong: #31405a;
--accent: #2dd4bf;
--accent-fg: #052e2b;
--accent-soft: rgba(45, 212, 191, 0.16);
--accent-soft-fg: #99f6e4;
--danger: #f87171;
--danger-soft: rgba(248, 113, 113, 0.15);
--warn: #fbbf24;
--warn-soft: rgba(251, 191, 36, 0.15);
--success: #4ade80;
--success-soft: rgba(74, 222, 128, 0.15);
--link: #67e8f9;
--unread-bg: #121c2e;
--read-bg: #0e1627;
--selected-bg: rgba(45, 212, 191, 0.18);
--focus-ring: 0 0 0 3px rgba(45, 212, 191, 0.4);
--star: #fbbf24;
--q1: #60a5fa;
--q2: #4ade80;
--q3: #c084fc;
--scrollbar: rgba(255, 255, 255, 0.25);
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4);
--shadow-2: 0 4px 16px rgba(0, 0, 0, 0.5);
--shadow-3: 0 12px 40px rgba(0, 0, 0, 0.6);
color-scheme: dark;
}
/*
* The "ihasmail" theme: the palette from ihasmail.org, which is a teal-navy
* rather than the blue-slate of the plain dark theme, warmed by the orange the
* logo's cat is drawn in.
*
* It rides on data-theme="dark" rather than replacing it, so every dark-only
* rule further down this file -- tooltips, toasts, the message frame -- keeps
* applying without being duplicated. Only the palette is overridden.
*
* Specificity is doing deliberate work here. This block is [data-palette] plus
* :root, so 0,2,0; the accent variants below are :root[data-theme][data-accent],
* so 0,3,0 and they win. That is what makes the accent swatches keep working on
* top of this theme -- and because the default accent ("teal") has no rule of
* its own, ihasmail.org's own accent is what shows until someone picks another.
*/
:root[data-palette="ihasmail"] {
--bg: #0d2430;
--bg-elev: #12303e;
--bg-sunken: #0a1c26;
--bg-hover: rgba(70, 202, 195, 0.10);
--bg-active: rgba(70, 202, 195, 0.16);
--fg: #eaf6f6;
--fg-muted: #a3c3cb;
--fg-faint: #86aab4;
--border: #21505f;
--border-strong: #2e6a7a;
--accent: #46cac3;
--accent-fg: #062028;
--accent-soft: rgba(70, 202, 195, 0.16);
--accent-soft-fg: #9fe6e2;
--danger: #f87171;
--danger-soft: rgba(248, 113, 113, 0.15);
--warn: #f9a34b;
--warn-soft: rgba(249, 163, 75, 0.14);
--success: #4ade80;
--success-soft: rgba(74, 222, 128, 0.15);
--link: #6fdcd6;
--unread-bg: #163a4a;
--read-bg: #12303e;
--selected-bg: rgba(70, 202, 195, 0.18);
--focus-ring: 0 0 0 3px rgba(70, 202, 195, 0.4);
/* The cat is orange; so is the star. */
--star: #f9a34b;
--q1: #6fdcd6;
--q2: #4ade80;
--q3: #c084fc;
--scrollbar: rgba(163, 195, 203, 0.3);
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45);
--shadow-2: 0 8px 24px rgba(0, 0, 0, 0.55);
--shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75);
color-scheme: dark;
}
/* Accent variants */
:root[data-accent="blue"] { --accent: #2563eb; --accent-soft: #dbeafe; --accent-soft-fg: #1e3a8a; --selected-bg: #dbeafe; --focus-ring: 0 0 0 3px rgba(37,99,235,.35); --link:#1d4ed8; }
:root[data-accent="purple"] { --accent: #7c3aed; --accent-soft: #ede9fe; --accent-soft-fg: #4c1d95; --selected-bg: #ede9fe; --focus-ring: 0 0 0 3px rgba(124,58,237,.35); --link:#6d28d9; }
:root[data-accent="rose"] { --accent: #e11d48; --accent-soft: #ffe4e6; --accent-soft-fg: #881337; --selected-bg: #ffe4e6; --focus-ring: 0 0 0 3px rgba(225,29,72,.35); --link:#be123c; }
:root[data-accent="orange"] { --accent: #ea580c; --accent-soft: #ffedd5; --accent-soft-fg: #7c2d12; --selected-bg: #ffedd5; --focus-ring: 0 0 0 3px rgba(234,88,12,.35); --link:#c2410c; }
:root[data-accent="green"] { --accent: #16a34a; --accent-soft: #dcfce7; --accent-soft-fg: #14532d; --selected-bg: #dcfce7; --focus-ring: 0 0 0 3px rgba(22,163,74,.35); --link:#15803d; }
:root[data-theme="dark"][data-accent="blue"] { --accent: #60a5fa; --accent-fg:#0b1a33; --accent-soft: rgba(96,165,250,.18); --accent-soft-fg:#bfdbfe; --selected-bg: rgba(96,165,250,.2); --link:#93c5fd; }
:root[data-theme="dark"][data-accent="purple"] { --accent: #a78bfa; --accent-fg:#1e1040; --accent-soft: rgba(167,139,250,.18); --accent-soft-fg:#ddd6fe; --selected-bg: rgba(167,139,250,.2); --link:#c4b5fd; }
:root[data-theme="dark"][data-accent="rose"] { --accent: #fb7185; --accent-fg:#3b0716; --accent-soft: rgba(251,113,133,.18); --accent-soft-fg:#fecdd3; --selected-bg: rgba(251,113,133,.2); --link:#fda4af; }
:root[data-theme="dark"][data-accent="orange"] { --accent: #fb923c; --accent-fg:#3b1605; --accent-soft: rgba(251,146,60,.18); --accent-soft-fg:#fed7aa; --selected-bg: rgba(251,146,60,.2); --link:#fdba74; }
:root[data-theme="dark"][data-accent="green"] { --accent: #4ade80; --accent-fg:#052e16; --accent-soft: rgba(74,222,128,.18); --accent-soft-fg:#bbf7d0; --selected-bg: rgba(74,222,128,.2); --link:#86efac; }
:root[data-density="comfortable"] { --row-h: 52px; }
:root[data-density="cozy"] { --row-h: 44px; }
:root[data-density="compact"] { --row-h: 36px; }
:root[data-fontsize="small"] { --fs: 13px; }
:root[data-fontsize="large"] { --fs: 15.5px; }
/* Reset ------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html, body, #root { height: 100%; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: var(--font-sans);
font-size: var(--fs);
line-height: 1.45;
color: var(--fg);
background: var(--bg);
-webkit-font-smoothing: antialiased;
overscroll-behavior: none;
}
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; padding: 0; }
a { color: var(--link); }
img { max-width: 100%; }
::selection { background: var(--accent-soft); }
* { scrollbar-width: thin; scrollbar-color: var(--scrollbar) transparent; }
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 8px; }
:focus-visible { outline: none; box-shadow: var(--focus-ring); border-radius: var(--radius-sm); }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.muted { color: var(--fg-muted); }
.faint { color: var(--fg-faint); }
.small { font-size: 0.875em; }
.mono { font-family: var(--font-mono); }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row { display: flex; align-items: center; gap: 8px; }
.col { display: flex; flex-direction: column; }
.grow { flex: 1 1 auto; min-width: 0; }
.spacer { flex: 1; }
.hidden { display: none !important; }
.wrap { flex-wrap: wrap; }
.gap-2 { gap: 2px; } .gap-4 { gap: 4px; } .gap-8 { gap: 8px; } .gap-12 { gap: 12px; } .gap-16 { gap: 16px; }
.mt-8 { margin-top: 8px; } .mt-16 { margin-top: 16px; } .mb-8 { margin-bottom: 8px; } .mb-16 { margin-bottom: 16px; }
.p-16 { padding: 16px; }
.center { display:flex; align-items:center; justify-content:center; }
.nowrap { white-space: nowrap; }
/* Buttons ---------------------------------------------------------------- */
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
height: 36px; padding: 0 14px; border-radius: var(--radius-sm);
border: 1px solid var(--border-strong); background: var(--bg-elev); color: var(--fg);
font-weight: 500; white-space: nowrap; user-select: none; transition: background .12s var(--ease), border-color .12s, box-shadow .12s, transform .05s;
}
.btn:hover { background: var(--bg-hover); }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: .5; cursor: not-allowed; transform: none; }
.btn-primary { background: var(--accent); border-color: transparent; color: var(--accent-fg); }
.btn-primary:hover { filter: brightness(1.06); background: var(--accent); }
.btn-danger { background: var(--danger); border-color: transparent; color: #fff; }
.btn-danger:hover { filter: brightness(1.06); background: var(--danger); }
.btn-ghost { border-color: transparent; background: transparent; }
.btn-ghost:hover { background: var(--bg-hover); }
.btn-soft { background: var(--accent-soft); border-color: transparent; color: var(--accent-soft-fg); }
.btn-sm { height: 30px; padding: 0 10px; font-size: .9em; }
.btn-lg { height: 44px; padding: 0 20px; font-size: 1.05em; border-radius: var(--radius); }
.btn-pill { border-radius: 999px; }
.btn-block { width: 100%; }
.icon-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 36px; height: 36px; border-radius: 50%; color: var(--fg-muted); flex: 0 0 auto;
transition: background .12s var(--ease), color .12s;
}
.icon-btn:hover { background: var(--bg-hover); color: var(--fg); }
.icon-btn:disabled { opacity: .4; cursor: not-allowed; }
.icon-btn.active { color: var(--accent); background: var(--accent-soft); }
.icon-btn.sm { width: 30px; height: 30px; }
.icon-btn.xs { width: 26px; height: 26px; }
.icon-btn.danger:hover { color: var(--danger); background: var(--danger-soft); }
/* Inputs ----------------------------------------------------------------- */
.input, .select, .textarea {
width: 100%; height: 38px; padding: 0 12px; border-radius: var(--radius-sm);
border: 1px solid var(--border-strong); background: var(--bg-elev); color: var(--fg);
transition: border-color .12s, box-shadow .12s;
}
.textarea { height: auto; min-height: 90px; padding: 10px 12px; resize: vertical; line-height: 1.5; }
.input:focus, .select:focus, .textarea:focus { border-color: var(--accent); box-shadow: var(--focus-ring); outline: none; }
.input::placeholder, .textarea::placeholder { color: var(--fg-faint); }
.input.sm { height: 32px; padding: 0 10px; }
.select { appearance: none; padding-right: 30px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238b94a3' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; }
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.field > label, .label { font-size: .85em; font-weight: 600; color: var(--fg-muted); letter-spacing: .01em; }
.field-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
.hint { font-size: .85em; color: var(--fg-faint); }
.check { display: flex; align-items: center; gap: 10px; cursor: pointer; padding: 6px 0; }
.check input { width: 18px; height: 18px; accent-color: var(--accent); margin: 0; }
.switch { position: relative; width: 40px; height: 22px; border-radius: 999px; background: var(--border-strong); transition: background .15s; flex: 0 0 auto; }
.switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: var(--shadow-1); transition: transform .15s var(--ease); }
.switch[aria-checked="true"] { background: var(--accent); }
.switch[aria-checked="true"]::after { transform: translateX(18px); }
.switch-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 0; border-bottom: 1px solid var(--border); }
.switch-row:last-child { border-bottom: 0; }
.switch-row .switch-text { display: flex; flex-direction: column; gap: 2px; }
.switch-row .switch-text .hint { margin: 0; }
.chip { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 10px; border-radius: 999px; background: var(--bg-sunken); border: 1px solid var(--border); font-size: .85em; max-width: 100%; }
.chip .chip-x { display: inline-flex; border-radius: 50%; padding: 2px; color: var(--fg-muted); }
.chip .chip-x:hover { background: var(--bg-active); color: var(--fg); }
.chip.invalid { border-color: var(--danger); color: var(--danger); }
.badge { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px; font-size: 11px; font-weight: 700; background: var(--accent); color: var(--accent-fg); }
.badge.muted { background: var(--bg-active); color: var(--fg-muted); }
.tag { display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 8px; border-radius: 6px; font-size: 11.5px; font-weight: 600; color: #fff; }
.kbd { display: inline-block; min-width: 20px; padding: 1px 6px; border-radius: 4px; border: 1px solid var(--border-strong); border-bottom-width: 2px; background: var(--bg-sunken); font-family: var(--font-mono); font-size: 11.5px; text-align: center; }
/* Avatar ----------------------------------------------------------------- */
.avatar { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 50%; color: #fff; font-weight: 600; font-size: 13px; flex: 0 0 auto; overflow: hidden; user-select: none; }
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar.sm { width: 28px; height: 28px; font-size: 11px; }
.avatar.lg { width: 56px; height: 56px; font-size: 20px; }
.avatar.xl { width: 88px; height: 88px; font-size: 30px; }
/* Menus / popovers ------------------------------------------------------- */
.popover { position: fixed; z-index: 1000; min-width: 180px; max-width: min(420px, calc(100vw - 16px)); max-height: min(70vh, 520px); overflow: auto; background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-2); padding: 6px; animation: pop .12s var(--ease); }
@keyframes pop { from { opacity: 0; transform: translateY(-4px) scale(.98); } to { opacity: 1; transform: none; } }
.menu-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 10px; border-radius: var(--radius-sm); text-align: left; color: var(--fg); white-space: nowrap; }
.menu-item:hover, .menu-item.active { background: var(--bg-hover); }
.menu-item:disabled { opacity: .5; cursor: default; }
/* A menu entry that is a link still looks like a menu entry. The global rule
for `a` would otherwise colour and underline the one item that leaves the
app, which reads as a mistake rather than a distinction. */
a.menu-item { text-decoration: none; color: var(--fg); cursor: pointer; }
a.menu-item:hover { color: var(--fg); }
.menu-item.danger { color: var(--danger); }
.menu-item .menu-kbd { margin-left: auto; color: var(--fg-faint); font-size: .85em; }
.menu-item svg { color: var(--fg-muted); flex: 0 0 auto; }
.menu-item.danger svg { color: var(--danger); }
.menu-sep { height: 1px; background: var(--border); margin: 6px 4px; }
.menu-title { padding: 6px 10px 4px; font-size: .78em; font-weight: 700; color: var(--fg-faint); text-transform: uppercase; letter-spacing: .05em; }
.menu-search { padding: 4px 4px 6px; }
.tooltip { position: fixed; z-index: 2000; background: #1f2937; color: #fff; padding: 5px 8px; border-radius: 6px; font-size: 12px; pointer-events: none; white-space: nowrap; box-shadow: var(--shadow-2); animation: pop .1s var(--ease); }
:root[data-theme="dark"] .tooltip { background: #e5e9f0; color: #0b1220; }
/* Dialogs ---------------------------------------------------------------- */
.dialog-backdrop { position: fixed; inset: 0; z-index: 900; background: rgba(2, 6, 23, 0.45); display: flex; align-items: center; justify-content: center; padding: 16px; animation: fade .15s var(--ease); backdrop-filter: blur(2px); }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.dialog { background: var(--bg-elev); border-radius: var(--radius-lg); box-shadow: var(--shadow-3); width: 100%; max-width: 520px; max-height: calc(100vh - 32px); display: flex; flex-direction: column; animation: rise .18s var(--ease); border: 1px solid var(--border); }
@keyframes rise { from { opacity: 0; transform: translateY(10px) scale(.98); } to { opacity: 1; transform: none; } }
.dialog.lg { max-width: 760px; }
.dialog.xl { max-width: 980px; }
.dialog.sm { max-width: 400px; }
.dialog-head { display: flex; align-items: center; gap: 8px; padding: 16px 20px 8px; }
.dialog-head h2 { margin: 0; font-size: 1.15em; font-weight: 650; flex: 1; }
.dialog-body { padding: 8px 20px 16px; overflow: auto; }
.dialog-foot { display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding: 12px 20px 16px; border-top: 1px solid var(--border); }
.dialog-foot .left { margin-right: auto; }
/* Two mutually exclusive views of the same thing, sized to sit in a dialog
footer beside the ordinary buttons (see ui/filepreview.tsx). */
.segmented { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
.segmented button { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 10px; font-size: .9em; color: var(--fg-muted); background: none; }
.segmented button + button { border-left: 1px solid var(--border); }
.segmented button:hover { background: var(--bg-hover); color: var(--fg); }
.segmented button.active { background: var(--accent-soft); color: var(--accent-soft-fg); font-weight: 600; }
.segmented button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.segmented.left { margin-right: auto; }
.dialog-foot .hint.left { margin-right: auto; }
.dialog-foot textarea.code, .dialog-body > textarea.code { font-family: var(--font-mono); }
/* Rendered Markdown in the file viewer. Deliberately plain: this is somebody's
notes, not a web page, and the point is to read it. */
.md-body { max-height: 65vh; overflow: auto; padding: 4px 2px; overflow-wrap: anywhere; }
.md-body > :first-child { margin-top: 0; }
.md-body > :last-child { margin-bottom: 0; }
.md-body h1, .md-body h2, .md-body h3, .md-body h4 { margin: 1.2em 0 .5em; line-height: 1.25; font-weight: 650; }
.md-body h1 { font-size: 1.5em; }
.md-body h2 { font-size: 1.28em; }
.md-body h3 { font-size: 1.12em; }
.md-body h4 { font-size: 1em; }
.md-body p, .md-body ul, .md-body ol, .md-body blockquote, .md-body table { margin: 0 0 .8em; }
.md-body ul, .md-body ol { padding-left: 1.6em; }
.md-body li { margin: .2em 0; }
.md-body a { color: var(--link); }
.md-body code { font-family: var(--font-mono); font-size: .9em; background: var(--bg-sunken); border-radius: 4px; padding: .1em .35em; }
.md-body pre { background: var(--bg-sunken); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 12px; overflow-x: auto; }
.md-body pre code { background: none; padding: 0; }
.md-body blockquote { margin-left: 0; padding-left: 12px; border-left: 3px solid var(--border-strong); color: var(--fg-muted); }
.md-body hr { border: 0; border-top: 1px solid var(--border); margin: 1.2em 0; }
.md-body table { border-collapse: collapse; }
.md-body th, .md-body td { border: 1px solid var(--border); padding: 5px 9px; text-align: left; }
.md-body th { background: var(--bg-sunken); }
/* An image is shown as its link, never fetched -- see lib/markdown.ts. */
.md-body .md-img::before { content: "🖼 "; }
.md-body .md-img { color: var(--link); text-decoration: underline dotted; text-underline-offset: 2px; }
/* "This occurrence or the whole series" — one button per answer, stacked, so
the destructive one is read rather than landed on by muscle memory. */
.dialog-choices { display: flex; flex-direction: column; gap: 8px; }
.dialog-choice { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; width: 100%; text-align: left; padding: 10px 12px; height: auto; }
.dialog-choice small { font-weight: 400; opacity: 0.75; }
/* Toasts ----------------------------------------------------------------- */
.toast-host { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 3000; display: flex; flex-direction: column; gap: 8px; align-items: center; pointer-events: none; padding: 0 12px; width: 100%; max-width: 520px; }
.toast { pointer-events: auto; display: flex; align-items: center; gap: 12px; background: #1f2937; color: #fff; padding: 10px 10px 10px 16px; border-radius: var(--radius); box-shadow: var(--shadow-2); min-width: 260px; max-width: 100%; position: relative; overflow: hidden; animation: rise .2s var(--ease); }
:root[data-theme="dark"] .toast { background: #e5e9f0; color: #0b1220; }
.toast-error { background: #991b1b; color: #fff; }
:root[data-theme="dark"] .toast-error { background: #fecaca; color: #450a0a; }
.toast-success { background: #14532d; color: #fff; }
:root[data-theme="dark"] .toast-success { background: #bbf7d0; color: #052e16; }
.toast-msg { flex: 1; }
.toast-action { color: #5eead4; font-weight: 700; padding: 6px 8px; border-radius: 6px; }
.toast-action:hover { background: rgba(255,255,255,.12); }
:root[data-theme="dark"] .toast-action { color: #0f766e; }
.toast-close { color: inherit; opacity: .7; display: inline-flex; padding: 4px; border-radius: 50%; }
.toast-close:hover { opacity: 1; background: rgba(255,255,255,.12); }
.toast-progress { position: absolute; left: 0; bottom: 0; height: 3px; background: #5eead4; width: 100%; transform-origin: left; animation: shrink linear forwards; }
@keyframes shrink { from { transform: scaleX(1); } to { transform: scaleX(0); } }
/* Spinner / skeleton ----------------------------------------------------- */
.spinner { width: 20px; height: 20px; border: 2px solid var(--border-strong); border-top-color: var(--accent); border-radius: 50%; animation: spin .7s linear infinite; flex: 0 0 auto; }
.spinner.lg { width: 32px; height: 32px; border-width: 3px; }
@keyframes spin { to { transform: rotate(360deg); } }
.skeleton { background: linear-gradient(90deg, var(--bg-sunken) 25%, var(--bg-hover) 37%, var(--bg-sunken) 63%); background-size: 400% 100%; animation: shimmer 1.4s ease infinite; border-radius: 6px; }
@keyframes shimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
.empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; padding: 48px 24px; color: var(--fg-muted); text-align: center; }
.empty svg { color: var(--fg-faint); }
.empty h3 { margin: 8px 0 0; font-weight: 600; color: var(--fg); }
.empty p { margin: 0; max-width: 360px; }
.error-box { padding: 12px 14px; border-radius: var(--radius-sm); background: var(--danger-soft); color: var(--danger); border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent); }
.info-box { padding: 12px 14px; border-radius: var(--radius-sm); background: var(--accent-soft); color: var(--accent-soft-fg); }
.warn-box { padding: 12px 14px; border-radius: var(--radius-sm); background: var(--warn-soft); color: var(--warn); }
/* ==========================================================================
App shell
========================================================================== */
/*
* The column is stated, and stated as `minmax(0, 1fr)`.
*
* A grid with only rows named gets an implicit `auto` column, which sizes to
* its widest child's min-content -- and the top bar's min-content is the
* search field plus the account buttons, about 470px. On a desktop nobody sees
* it; on a 390px phone the whole app was 84px wider than the screen, so every
* message row ran off the right edge with its date beyond the glass. `1fr`
* alone would not have fixed it: that is `minmax(auto, 1fr)`, and the `auto`
* floor is the same min-content. The zero floor is the part that lets the top
* bar shrink instead of setting the width for everything below it.
*/
.app { height: 100%; display: grid; grid-template-rows: var(--topbar-h) 1fr; grid-template-columns: minmax(0, 1fr); overflow: hidden; }
.topbar { display: flex; align-items: center; gap: 8px; padding: 0 12px; background: var(--bg); position: relative; z-index: 50; }
.topbar .brand { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 1.2em; letter-spacing: -.01em; color: var(--fg); text-decoration: none; padding-right: 8px; min-width: 0; }
.topbar .brand img { width: 34px; height: 34px; object-fit: contain; }
.topbar .brand .brand-name { color: #14b8a6; }
.topbar .brand .brand-name span { color: var(--fg-muted); font-weight: 500; }
/* `min-width: 0`, or the flex default of `auto` holds the search field at its
own min-content and the account buttons beside it are pushed off a phone. */
.searchbar { flex: 1 1 auto; min-width: 0; max-width: 720px; margin: 0 auto; position: relative; }
.searchbar .search-input { display: flex; align-items: center; gap: 8px; height: 44px; padding: 0 8px 0 14px; border-radius: 999px; background: var(--bg-sunken); border: 1px solid transparent; transition: background .12s, box-shadow .12s, border-color .12s; }
.searchbar .search-input:focus-within { background: var(--bg-elev); box-shadow: var(--shadow-1); border-color: var(--border); }
.searchbar input { flex: 1; border: 0; background: transparent; outline: none; min-width: 0; height: 100%; }
.searchbar input::placeholder { color: var(--fg-faint); }
.search-panel { position: absolute; top: calc(100% + 6px); left: 0; right: 0; background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-2); padding: 16px; z-index: 60; animation: pop .12s var(--ease); }
.search-panel .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 14px; }
.search-suggest { position: absolute; top: calc(100% + 6px); left: 0; right: 0; background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-2); padding: 6px; z-index: 60; }
.topbar-actions { display: flex; align-items: center; gap: 4px; }
/* Live-updates indicator. Three states, and a raised bead rather than a flat
speck: at 8px flat it was invisible against either theme. The gloss is a
highlight over a solid colour rather than a colour-mix, so it needs no
per-theme variant -- the same bead reads on light and dark alike. */
.push-status { display: inline-flex; align-items: center; padding: 0 4px; }
.push-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background:
radial-gradient(circle at 34% 30%, rgba(255, 255, 255, .8), rgba(255, 255, 255, 0) 46%),
var(--dot);
box-shadow:
0 0 0 3px var(--dot-halo),
0 1px 2px rgba(0, 0, 0, .45),
inset 0 -1px 2px rgba(0, 0, 0, .3);
}
.push-dot.connected { --dot: #16a34a; --dot-halo: rgba(22, 163, 74, .25); }
.push-dot.connecting { --dot: #eab308; --dot-halo: rgba(234, 179, 8, .25); animation: push-pulse 1.6s ease-in-out infinite; }
.push-dot.disconnected { --dot: #dc2626; --dot-halo: rgba(220, 38, 38, .25); }
@keyframes push-pulse { 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) { .push-dot.connecting { animation: none; } }
.app-body { display: grid; grid-template-columns: var(--sidebar-w) minmax(0, 1fr); min-height: 0; transition: grid-template-columns .2s var(--ease); }
.app-body.collapsed { grid-template-columns: var(--sidebar-w-collapsed) minmax(0, 1fr); }
.sidebar { display: flex; flex-direction: column; min-height: 0; padding: 4px 8px 8px 8px; gap: 2px; overflow: hidden; }
.sidebar-scroll { overflow-y: auto; overflow-x: hidden; flex: 1; min-height: 0; padding-bottom: 8px; }
.compose-btn { display: flex; align-items: center; gap: 12px; height: 52px; padding: 0 22px 0 18px; margin: 6px 4px 12px; border-radius: 16px; background: var(--bg-elev); box-shadow: var(--shadow-1); font-weight: 600; font-size: 1em; color: var(--fg); transition: box-shadow .15s, transform .05s, background .12s; white-space: nowrap; }
.compose-btn:hover { box-shadow: var(--shadow-2); background: var(--bg-elev); }
.compose-btn svg { color: var(--accent); }
.collapsed .compose-btn { width: 52px; padding: 0; justify-content: center; margin-left: auto; margin-right: auto; }
.collapsed .compose-btn span { display: none; }
.nav-section { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px 4px; font-size: .75em; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--fg-faint); }
.nav-section .icon-btn { width: 24px; height: 24px; }
.collapsed .nav-section { display: none; }
.nav-item { position: relative; display: flex; align-items: center; gap: 12px; height: 36px; padding: 0 12px; border-radius: 0 999px 999px 0; margin-right: 8px; color: var(--fg); text-decoration: none; white-space: nowrap; transition: background .12s; user-select: none; font-weight: 450; }
.nav-item:hover { background: var(--bg-hover); }
.nav-item.unread .nav-label, .nav-item.unread .nav-count { font-weight: 700; color: var(--fg); }
.nav-item.active { background: var(--accent-soft); color: var(--accent-soft-fg); font-weight: 650; }
.nav-item.active.unread .nav-label, .nav-item.active.unread .nav-count { color: inherit; }
.nav-item.drop-target { background: var(--accent-soft); outline: 2px dashed var(--accent); outline-offset: -2px; }
.nav-item.folder-row.dragging { opacity: .45; }
/* A folder colour tints its icon; the label keeps the sidebar's contrast. */
.folder-row .folder-icon { display: inline-flex; align-items: center; }
/* .nav-item svg sets colour on the svg itself, so inheriting from the span is
not enough -- the icon has to be targeted directly to win the cascade.
The fill overrides lucide's own fill="none": a CSS rule beats a presentation
attribute, and a solid folder reads at a glance where an outline does not. */
.folder-row .folder-icon[style*="--folder-color"] svg { color: var(--bg); fill: var(--folder-color); }
/* The Folders heading doubles as the way back to the top level while dragging. */
.nav-section.drop-target { background: var(--accent-soft); outline: 2px dashed var(--accent); outline-offset: -2px; border-radius: var(--radius-sm); color: var(--accent-soft-fg); }
.nav-item svg { flex: 0 0 auto; color: var(--fg-muted); }
.nav-item.active svg { color: inherit; }
.nav-item .nav-label { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.nav-item .nav-count { font-size: .8em; font-weight: 700; color: var(--fg-muted); }
.nav-item.active .nav-count { color: inherit; }
.nav-item .nav-more { opacity: 0; width: 24px; height: 24px; margin-right: -6px; }
.nav-item:hover .nav-more, .nav-item:focus-within .nav-more { opacity: 1; }
.nav-item .nav-twisty { width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; color: var(--fg-faint); border-radius: 4px; }
.nav-item .nav-twisty:hover { background: var(--bg-active); }
/* Folder rows keep the expand chevron in a gutter to the LEFT of the icon, so a
folder with subfolders and one without still line up on their icon. */
.nav-item.folder-row { --folder-indent: 0px; position: relative; padding-left: calc(30px + var(--folder-indent)); }
.nav-item.folder-row.depth-1 { --folder-indent: 16px; }
.nav-item.folder-row.depth-2 { --folder-indent: 32px; }
.nav-item.folder-row.depth-3 { --folder-indent: 48px; }
.nav-item.folder-row.depth-4 { --folder-indent: 64px; }
.nav-item.folder-row .nav-twisty { position: absolute; left: calc(8px + var(--folder-indent)); top: 50%; transform: translateY(-50%); margin: 0; }
/* Labels sit in the same column: a 20px slot matching the folder icons. */
/* ---- Drill-down (phones; see mail/MailboxTree.tsx) ---- */
/*
* No indent to pay for, so the name gets the width the tree was spending on
* depth. The twisty's 30px gutter goes with it -- nothing is ever drawn in it
* here, whether or not the folder has children.
*
* Scoped to the list rather than to the rows that offer a drill, which is the
* distinction the first cut got wrong: only folders WITH children carried the
* class, so they sat 18px left of every folder without any and the column of
* icons came apart. Whether a folder has children is not a reason to hang it
* somewhere else.
*/
.folder-drill .nav-item.folder-row { padding-left: 12px; }
.nav-item.folder-row .drill-into { width: 44px; height: 100%; border-radius: 0; margin-right: -12px; color: var(--fg-faint); }
.nav-item.folder-row .drill-into:hover { background: var(--bg-hover); color: var(--fg); }
/* The row it belongs to is a link and paints its own active state; a second
filled rectangle inside it reads as a separate selected thing. */
.nav-item.folder-row.active .drill-into { color: inherit; }
.drill-back { width: 100%; border: 0; background: none; font: inherit; text-align: left; cursor: pointer; color: var(--fg-muted); padding-left: 12px; }
.drill-back:hover { background: var(--bg-hover); color: var(--fg); }
.drill-back svg { flex: 0 0 auto; }
.nav-item.folder-row .nav-label-color { flex: 0 0 20px; width: 20px; height: 20px; border-radius: 0; display: inline-flex; align-items: center; justify-content: center; }
.nav-item.folder-row .nav-label-color::before { content: ""; width: 11px; height: 11px; border-radius: 3px; background: var(--label-color, var(--accent)); }
.collapsed .nav-item { justify-content: center; padding: 0; margin: 0 auto; width: 44px; border-radius: 999px; }
.collapsed .nav-item .nav-label, .collapsed .nav-item .nav-count, .collapsed .nav-item .nav-more, .collapsed .nav-item .nav-twisty { display: none; }
.collapsed .nav-item.depth-1, .collapsed .nav-item.depth-2, .collapsed .nav-item.depth-3 { display: none; }
.collapsed .nav-item .nav-dot { position: absolute; top: 6px; right: 6px; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
.nav-label-color { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto; }
/* Outlook-style module bar at the bottom of the sidebar */
.module-bar { display: flex; align-items: stretch; justify-content: space-around; gap: 2px; padding: 6px 4px 4px; border-top: 1px solid var(--border); flex: 0 0 auto; }
.module-link { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; flex: 1; min-width: 0; padding: 6px 2px; border-radius: var(--radius-sm); color: var(--fg-muted); text-decoration: none; font-size: 11px; font-weight: 500; transition: background .12s, color .12s; }
.module-link:hover { background: var(--bg-hover); color: var(--fg); }
.module-link.active { color: var(--accent-soft-fg); background: var(--accent-soft); }
.module-link svg { flex: 0 0 auto; }
.module-link .module-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.collapsed .module-bar { flex-direction: column; gap: 4px; }
.collapsed .module-link .module-label { display: none; }
.collapsed .module-link { width: 44px; height: 44px; margin: 0 auto; padding: 0; border-radius: 999px; }
.quota { padding: 10px 12px; font-size: .8em; color: var(--fg-muted); }
.quota-bar { height: 4px; background: var(--bg-active); border-radius: 4px; overflow: hidden; margin-top: 6px; }
.quota-bar > span { display: block; height: 100%; background: var(--accent); }
.quota-bar > span.warn { background: var(--warn); }
.quota-bar > span.danger { background: var(--danger); }
.collapsed .quota { display: none; }
.main { min-height: 0; min-width: 0; display: flex; flex-direction: column; background: var(--bg-elev); border-radius: var(--radius-lg) 0 0 0; border: 1px solid var(--border); border-right: 0; border-bottom: 0; overflow: hidden; }
/* Mobile drawer */
.drawer-backdrop { display: none; }
.mobile-tabbar { display: none; }
.fab { display: none; }
/* ==========================================================================
Mail
========================================================================== */
.mail-layout { display: grid; grid-template-columns: minmax(340px, var(--list-w)) 1fr; height: 100%; min-height: 0; flex: 1; }
.mail-layout.pane-off { grid-template-columns: 1fr; }
.mail-layout.pane-off.reading { grid-template-columns: 1fr; }
.mail-layout .mail-list-pane { display: flex; flex-direction: column; min-height: 0; min-width: 0; border-right: 1px solid var(--border); }
.mail-layout.pane-bottom .mail-list-pane { border-right: 0; border-bottom: 1px solid var(--border); }
.mail-layout.pane-off .mail-list-pane { border-right: 0; }
.mail-layout .mail-reading-pane { min-height: 0; min-width: 0; display: flex; flex-direction: column; background: var(--bg-elev); }
.list-toolbar { display: flex; align-items: center; gap: 2px; height: 48px; padding: 0 8px 0 12px; border-bottom: 1px solid var(--border); flex: 0 0 auto; }
.list-toolbar .tb-sep { width: 1px; height: 20px; background: var(--border); margin: 0 6px; }
.list-toolbar .tb-title { font-weight: 650; font-size: 1.02em; margin-right: 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.list-toolbar .tb-count { color: var(--fg-muted); font-size: .85em; white-space: nowrap; }
.select-all { width: 18px; height: 18px; accent-color: var(--accent); margin: 0 6px 0 0; cursor: pointer; }
.mail-list { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; position: relative; outline: none; }
.mail-list-inner { position: relative; width: 100%; }
.msg-row { position: absolute; left: 0; right: 0; display: flex; align-items: center; gap: 8px; height: var(--row-h); padding: 0 12px 0 8px; border-bottom: 1px solid var(--border); background: var(--read-bg); cursor: pointer; user-select: none; -webkit-user-select: none; transition: box-shadow .1s; }
.msg-row.unread { background: var(--unread-bg); }
.msg-row.unread .msg-from, .msg-row.unread .msg-subject { font-weight: 700; color: var(--fg); }
.msg-row:hover { box-shadow: inset 0 0 0 1px var(--border-strong), var(--shadow-1); z-index: 2; }
.msg-row.selected { background: var(--selected-bg); }
.msg-row.focused { box-shadow: inset 3px 0 0 var(--accent); }
.msg-row.open { background: var(--selected-bg); }
.msg-row.dragging { opacity: .5; }
.msg-row .msg-check { width: 18px; height: 18px; accent-color: var(--accent); margin: 0; flex: 0 0 auto; opacity: 0; transition: opacity .1s; cursor: pointer; }
.msg-row:hover .msg-check, .msg-row.selected .msg-check, .mail-list.has-selection .msg-check { opacity: 1; }
.msg-row .msg-star { color: var(--fg-faint); flex: 0 0 auto; display: inline-flex; padding: 4px; border-radius: 50%; }
.msg-row .msg-star:hover { color: var(--star); background: var(--bg-hover); }
.msg-row .msg-star.on { color: var(--star); }
.msg-row .msg-from { width: 180px; flex: 0 0 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--fg); display: flex; align-items: center; gap: 6px; }
.msg-row .msg-from .thread-count { color: var(--fg-muted); font-weight: 400; font-size: .9em; }
.msg-row .msg-main { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 6px; overflow: hidden; }
.msg-row .msg-subject { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 0 1 auto; min-width: 40%; max-width: 100%; color: var(--fg); }
.msg-row .msg-preview { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--fg-muted); flex: 1 1 0; min-width: 0; }
.msg-row .msg-preview::before { content: " "; }
.msg-row .msg-meta { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; color: var(--fg-muted); font-size: .85em; white-space: nowrap; }
.msg-row.unread .msg-meta .msg-date { color: var(--fg); font-weight: 700; }
.msg-row .msg-actions { display: none; align-items: center; gap: 0; margin-left: 4px; }
.msg-row:hover .msg-actions { display: flex; }
.msg-row:hover .msg-meta .msg-date { display: none; }
.msg-row .msg-labels { display: inline-flex; gap: 4px; flex: 0 0 auto; }
.msg-row .msg-labels .tag { height: 18px; font-size: 10.5px; padding: 0 6px; }
.msg-row .avatar { width: 32px; height: 32px; font-size: 12px; }
.mail-list.compact .msg-row .avatar { display: none; }
.msg-row .msg-attach { color: var(--fg-faint); }
.msg-row .msg-answered { color: var(--fg-faint); }
.msg-row .msg-important { color: var(--warn); }
.list-footer { padding: 12px; text-align: center; color: var(--fg-muted); font-size: .9em; }
.list-hint { padding: 8px 12px; font-size: .85em; color: var(--fg-muted); background: var(--bg-sunken); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; }
.list-hint button { color: var(--link); font-weight: 600; white-space: nowrap; }
.drag-ghost { position: fixed; top: -1000px; left: -1000px; padding: 8px 12px; background: var(--accent); color: var(--accent-fg); border-radius: 999px; font-weight: 600; box-shadow: var(--shadow-2); pointer-events: none; z-index: 5000; }
/* Splitter between list and reading pane */
.splitter { position: relative; background: var(--border); flex: 0 0 auto; z-index: 3; touch-action: none; }
.splitter.vertical { width: 6px; cursor: col-resize; }
.splitter.horizontal { height: 6px; cursor: row-resize; }
.splitter:hover, .splitter:focus-visible { background: var(--accent); box-shadow: none; }
.splitter .splitter-grip { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); border-radius: 4px; background: var(--fg-faint); opacity: 0; transition: opacity .15s; }
.splitter.vertical .splitter-grip { width: 3px; height: 36px; }
.splitter.horizontal .splitter-grip { height: 3px; width: 36px; }
.splitter:hover .splitter-grip { opacity: .8; }
.mail-layout.pane-right { grid-template-columns: var(--list-size, 520px) auto 1fr; }
.mail-layout.pane-bottom { grid-template-columns: 1fr; grid-template-rows: var(--list-size, 340px) auto 1fr; }
/* Two-line rows (narrow lists / mobile) */
.mail-list.two-line .msg-row { height: calc(var(--row-h) * 1.5); align-items: flex-start; padding-top: 8px; }
.mail-list.two-line .msg-row .msg-from { width: auto; flex: 1; }
.mail-list.two-line .msg-row .msg-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.mail-list.two-line .msg-row .msg-line1 { display: flex; align-items: center; gap: 8px; }
.mail-list.two-line .msg-row .msg-main { display: flex; gap: 6px; }
.mail-list.two-line .msg-row .msg-subject { flex: 0 1 auto; }
.mail-list.two-line .msg-row .msg-preview { flex: 1; }
.mail-list.two-line .msg-row .msg-meta { margin-left: auto; }
.mail-list.two-line .msg-row:hover .msg-meta .msg-date { display: inline; }
.mail-list.two-line .msg-row:hover .msg-actions { display: none; }
/* Reading pane / thread view */
.thread-view { display: flex; flex-direction: column; min-height: 0; height: 100%; }
.thread-toolbar { display: flex; align-items: center; gap: 2px; height: 48px; padding: 0 8px; border-bottom: 1px solid var(--border); flex: 0 0 auto; }
.thread-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 0 0 80px; }
.thread-subject { padding: 20px 24px 8px; display: flex; align-items: flex-start; gap: 12px; }
.thread-subject h1 { margin: 0; font-size: 1.35em; font-weight: 600; line-height: 1.3; flex: 1; overflow-wrap: anywhere; }
.thread-subject .labels { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
.message { margin: 0 16px 8px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-elev); transition: box-shadow .12s; }
.message.collapsed { cursor: pointer; }
.message.collapsed:hover { box-shadow: var(--shadow-1); }
.message.unread-msg { border-left: 3px solid var(--accent); }
.message-head { display: flex; align-items: flex-start; gap: 12px; padding: 12px 16px; }
.message-head .who { flex: 1; min-width: 0; }
.message-head .who .from { font-weight: 650; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.message-head .who .from .email { color: var(--fg-muted); font-weight: 400; font-size: .9em; }
.message-head .who .to { color: var(--fg-muted); font-size: .88em; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.message-head .who .to button { color: var(--fg-muted); display: inline-flex; align-items: center; }
.message-head .who .snippet { color: var(--fg-muted); font-size: .92em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.message-head .meta { display: flex; align-items: center; gap: 2px; color: var(--fg-muted); font-size: .85em; white-space: nowrap; flex: 0 0 auto; }
.message-head .meta .date { margin-right: 6px; }
.message-details { margin: 0 16px 8px; padding: 10px 12px; background: var(--bg-sunken); border-radius: var(--radius-sm); font-size: .88em; display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; }
.message-details dt { color: var(--fg-muted); }
.message-details dd { margin: 0; overflow-wrap: anywhere; }
/*
* Layout containment here is a security control, not a layout tweak.
*
* Message bodies render in a shadow root, which scopes selectors but not
* layout: a `position:fixed` rule in mail CSS is still positioned against the
* viewport, so a sender could paint over the whole application. Containment
* inside the shadow root cannot stop it — mail CSS lives in the same tree and
* simply overrides it, and `:host` reaches the host element too (an
* `!important` there even beats an `!important` from this file, because
* importance reverses tree order). An ancestor of the host is the one thing
* mail CSS has no selector for. `layout` rather than `paint`: it makes this a
* containing block for fixed descendants without clipping tall messages.
*/
.message-body { padding: 4px 16px 16px; contain: layout; }
.message-body .body-host { display: block; }
.remote-banner { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0 0 12px; padding: 8px 12px; background: var(--warn-soft); color: var(--warn); border-radius: var(--radius-sm); font-size: .9em; }
.remote-banner button { color: inherit; font-weight: 700; text-decoration: underline; }
.scheduled-banner { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0 0 12px; padding: 8px 12px; background: var(--accent-soft); color: var(--accent-soft-fg); border-radius: var(--radius-sm); font-size: .9em; }
.scheduled-banner button { color: inherit; font-weight: 700; text-decoration: underline; }
.receipt-banner { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0 0 12px; padding: 8px 12px; background: var(--warn-soft); color: var(--warn); border-radius: var(--radius-sm); font-size: .9em; }
.receipt-banner button { color: inherit; font-weight: 700; text-decoration: underline; }
.quote-toggle { display: inline-flex; align-items: center; gap: 4px; margin: 8px 0; padding: 2px 10px; border-radius: 999px; background: var(--bg-sunken); color: var(--fg-muted); font-size: 12px; border: 1px solid var(--border); }
.quote-toggle:hover { background: var(--bg-active); }
.attachments { display: flex; flex-wrap: wrap; gap: 10px; padding: 4px 16px 16px; }
.attachment { display: flex; align-items: center; gap: 10px; width: 240px; max-width: 100%; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-elev); text-decoration: none; color: inherit; position: relative; transition: box-shadow .12s; }
.attachment:hover { box-shadow: var(--shadow-1); }
.attachment .att-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; background: var(--accent-soft); color: var(--accent-soft-fg); flex: 0 0 auto; overflow: hidden; }
.attachment .att-icon img { width: 100%; height: 100%; object-fit: cover; }
.attachment .att-text { flex: 1; min-width: 0; }
/* Both are spans, and `overflow`/`text-overflow` do nothing on an inline
element -- so the name never truncated and the size ran on after it on the
same line. Only long names showed it, which is every .eml named from a
subject. */
.attachment .att-name { display: block; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: .92em; }
.attachment .att-size { display: block; color: var(--fg-muted); font-size: .8em; }
.attachment .att-actions { display: none; gap: 0; }
.attachment:hover .att-actions { display: flex; }
.attachment:hover .att-size { display: none; }
.attachment .att-progress { position: absolute; left: 0; bottom: 0; height: 3px; background: var(--accent); border-radius: 0 0 0 6px; }
.attachment.error { border-color: var(--danger); }
.thread-actions { display: flex; gap: 8px; padding: 8px 16px 16px; flex-wrap: wrap; }
.invite-card { margin: 0 16px 12px; padding: 14px 16px; border: 1px solid var(--border); border-left: 4px solid var(--accent); border-radius: var(--radius-sm); background: var(--bg-sunken); }
.invite-card h4 { margin: 0 0 4px; }
.invite-card .rsvp { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.vcard-card { margin: 0 16px 12px; padding: 12px 16px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-sunken); display: flex; align-items: center; gap: 12px; }
.unsubscribe-row { margin: 0 16px 8px; font-size: .88em; color: var(--fg-muted); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.reply-box { margin: 8px 16px 24px; }
/* Wraps, because it does not fit. Three labelled buttons and an overflow need
about 390px of it, which a 430px phone has and a 360px one does not -- and it
was already over the line on the smaller ones before the overflow was added. */
.reply-box .reply-prompt { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 12px; border: 1px solid var(--border); border-radius: var(--radius); color: var(--fg-muted); }
.reply-box .reply-prompt button { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border-radius: 999px; border: 1px solid var(--border-strong); color: var(--fg); font-weight: 500; }
.reply-box .reply-prompt button:hover { background: var(--bg-hover); }
.no-thread { height: 100%; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 8px; color: var(--fg-muted); }
.no-thread img { width: 140px; opacity: .6; filter: grayscale(.3); }
.thread-nav { display: flex; align-items: center; gap: 2px; margin-left: auto; color: var(--fg-muted); font-size: .85em; }
.print-only { display: none; }
/* Labels (keywords) */
.label-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
/* ==========================================================================
Composer
========================================================================== */
.composer-dock { position: fixed; right: 16px; bottom: 0; display: flex; align-items: flex-end; gap: 12px; z-index: 800; pointer-events: none; }
.composer { pointer-events: auto; width: 580px; max-width: calc(100vw - 32px); height: 600px; max-height: calc(100vh - 24px); display: flex; flex-direction: column; background: var(--bg-elev); border-radius: var(--radius-lg) var(--radius-lg) 0 0; box-shadow: var(--shadow-3); border: 1px solid var(--border); border-bottom: 0; overflow: hidden; animation: rise .2s var(--ease); }
.composer.minimized { height: 44px; width: 280px; }
.composer.maximized { position: fixed; inset: 24px; width: auto; height: auto; max-width: none; max-height: none; border-radius: var(--radius-lg); border-bottom: 1px solid var(--border); }
.composer-head { display: flex; align-items: center; gap: 4px; height: 44px; padding: 0 6px 0 14px; background: var(--bg-sunken); border-bottom: 1px solid var(--border); flex: 0 0 auto; cursor: default; }
.composer-head .title { flex: 1; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.composer-head .status { color: var(--fg-faint); font-size: .8em; margin-right: 6px; white-space: nowrap; }
.composer-body { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.composer-fields { flex: 0 0 auto; padding: 0 12px; }
.composer-field { display: flex; align-items: center; gap: 8px; min-height: 40px; border-bottom: 1px solid var(--border); padding: 4px 0; }
.composer-field > label { color: var(--fg-muted); width: 42px; flex: 0 0 auto; font-size: .92em; }
.composer-field .field-extra { display: flex; gap: 4px; color: var(--fg-muted); font-size: .85em; }
.composer-field .field-extra button { color: var(--fg-muted); padding: 2px 6px; border-radius: 4px; }
.composer-field .field-extra button:hover { background: var(--bg-hover); color: var(--fg); }
.composer-field input.plain { flex: 1; border: 0; background: transparent; outline: none; min-width: 80px; height: 30px; }
.composer-field .from-select { flex: 1; border: 0; background: transparent; color: var(--fg); padding: 0; height: 30px; cursor: pointer; }
/*
* A native <select>'s dropdown is painted by the browser from the element's own
* colours, not the page's. `.from-select` is deliberately transparent so it
* sits flush in the composer's From line -- which left its popup with no
* background of its own, so the browser drew a light one while the text kept
* the app's light foreground: light on light, unreadable in any dark theme.
*
* Styling `option` fixes the popup without giving the closed control a box.
* Scoped to every select rather than this one, because nothing else in the app
* styled options either -- the next transparent select would have arrived with
* the same bug.
*/
select option,
select optgroup { background-color: var(--bg-elev); color: var(--fg); }
.recipients { flex: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 4px; min-width: 0; position: relative; }
.recipients .chip { height: 24px; }
.recipients input { flex: 1; min-width: 120px; border: 0; background: transparent; outline: none; height: 28px; }
.composer-editor { flex: 1; min-height: 0; display: flex; flex-direction: column; position: relative; }
.editor-area { flex: 1; min-height: 120px; overflow-y: auto; padding: 12px 16px; outline: none; line-height: 1.5; font-size: 14px; font-family: var(--font-sans); }
.editor-area:empty::before, .editor-area[data-empty="true"]::before { content: attr(data-placeholder); color: var(--fg-faint); pointer-events: none; position: absolute; }
.editor-area blockquote { margin: 0 0 0 .8ex; border-left: 2px solid var(--border-strong); padding-left: 1ex; color: var(--fg-muted); }
.editor-area img { max-width: 100%; height: auto; }
.editor-area a { color: var(--link); }
.editor-area .ihm-signature { color: var(--fg-muted); }
.editor-area pre { font-family: var(--font-mono); background: var(--bg-sunken); padding: 8px; border-radius: 6px; overflow: auto; }
.editor-textarea { flex: 1; border: 0; resize: none; outline: none; padding: 12px 16px; font-family: var(--font-mono); font-size: 13.5px; line-height: 1.5; background: transparent; }
.editor-toolbar { display: flex; align-items: center; gap: 1px; flex-wrap: wrap; padding: 4px 8px; border-top: 1px solid var(--border); flex: 0 0 auto; }
.editor-toolbar .icon-btn { width: 30px; height: 30px; border-radius: 6px; }
.editor-toolbar .tb-sep { width: 1px; height: 18px; background: var(--border); margin: 0 4px; }
.editor-toolbar select { height: 28px; border: 0; background: transparent; color: var(--fg-muted); font-size: .85em; max-width: 110px; }
.composer-attachments { display: flex; flex-wrap: wrap; gap: 8px; padding: 8px 12px 0; max-height: 140px; overflow-y: auto; }
.composer-attachments .attachment { width: 200px; padding: 6px 8px; }
.composer-foot { display: flex; align-items: center; gap: 4px; padding: 8px 12px; border-top: 1px solid var(--border); flex: 0 0 auto; }
.composer-foot .send-group { display: flex; }
.composer-foot .send-group .btn:first-child { border-radius: 999px 0 0 999px; padding-left: 20px; }
.composer-foot .send-group .btn:last-child { border-radius: 0 999px 999px 0; padding: 0 8px; border-left: 1px solid rgba(255,255,255,.3); }
.composer-foot .more-actions { display: flex; align-items: center; gap: 2px; margin-left: 8px; }
.composer.dropping .composer-body { outline: 3px dashed var(--accent); outline-offset: -8px; }
.suggest-list { position: absolute; top: 100%; left: 0; min-width: 300px; max-width: 100%; background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius-sm); box-shadow: var(--shadow-2); z-index: 20; padding: 4px; max-height: 260px; overflow: auto; }
.suggest-item { display: flex; align-items: center; gap: 10px; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
.suggest-item.active, .suggest-item:hover { background: var(--bg-hover); }
.suggest-item .s-name { font-weight: 550; }
.suggest-item .s-email { color: var(--fg-muted); font-size: .85em; }
.suggest-item .s-src { margin-left: auto; font-size: .72em; color: var(--fg-faint); text-transform: uppercase; letter-spacing: .05em; }
.emoji-grid { display: grid; grid-template-columns: repeat(8, 32px); gap: 2px; padding: 4px; }
.emoji-grid button { font-size: 20px; height: 32px; border-radius: 6px; }
.emoji-grid button:hover { background: var(--bg-hover); }
.link-popup { display: flex; gap: 6px; padding: 8px; }
.color-grid { display: grid; grid-template-columns: repeat(8, 22px); gap: 4px; padding: 6px; }
.color-grid button { width: 22px; height: 22px; border-radius: 4px; border: 1px solid rgba(0,0,0,.1); }
/* ==========================================================================
Settings
========================================================================== */
.settings-layout { display: grid; grid-template-columns: 240px 1fr; height: 100%; min-height: 0; flex: 1; }
.settings-nav { border-right: 1px solid var(--border); padding: 12px 8px; overflow-y: auto; }
.settings-nav .nav-item { margin-right: 0; border-radius: var(--radius-sm); }
.settings-content { --pad-b: 64px; overflow-y: auto; padding: 24px 32px var(--pad-b); max-width: 860px; }
.settings-content h1 { margin: 0 0 4px; font-size: 1.5em; font-weight: 650; }
.settings-content h2 { margin: 28px 0 12px; font-size: 1.05em; font-weight: 650; padding-bottom: 6px; border-bottom: 1px solid var(--border); }
.settings-content .lead { color: var(--fg-muted); margin: 0 0 20px; }
.card { border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-elev); padding: 14px 16px; margin-bottom: 12px; }
.card.clickable { cursor: pointer; transition: box-shadow .12s; }
.card.clickable:hover { box-shadow: var(--shadow-1); }
.card-head { display: flex; align-items: center; gap: 10px; }
.card-head h3 { margin: 0; font-size: 1em; font-weight: 650; flex: 1; }
.theme-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; }
.theme-card { border: 2px solid var(--border); border-radius: var(--radius); padding: 8px; cursor: pointer; text-align: center; font-size: .9em; }
.theme-card.active { border-color: var(--accent); }
.theme-card .preview { height: 56px; border-radius: 6px; margin-bottom: 6px; border: 1px solid var(--border); }
.swatches { display: flex; gap: 10px; flex-wrap: wrap; }
.swatch { width: 32px; height: 32px; border-radius: 50%; border: 3px solid transparent; cursor: pointer; }
.swatch.active { border-color: var(--fg); }
.rule-card { border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; margin-bottom: 10px; background: var(--bg-elev); }
.rule-card.disabled { opacity: .6; }
.rule-card.dragging { opacity: .5; }
/* The line shows which side of this card the dragged rule would land on. */
.rule-card.drop-above { box-shadow: inset 0 3px 0 0 var(--accent); }
.rule-card.drop-below { box-shadow: inset 0 -3px 0 0 var(--accent); }
.rule-card .drag-handle { display: flex; align-items: center; padding: 2px; margin-left: -4px; color: var(--fg-faint); cursor: grab; touch-action: none; }
.rule-card .drag-handle:active { cursor: grabbing; }
/* The bar that says whether there is unsaved work, kept where it can be read.
Unpinned it sat after the rules, which on a full list means off the bottom of
the window: the one indicator that anything was pending was the one thing you
had to scroll to find (issue #175).
The offset is what pins it flush. A sticky `bottom: 0` stops at the scroll
container's padding box, which leaves the pane's bottom padding as a strip
below the bar for rules to scroll through; sticking to minus that padding
closes it, on the narrow layout's larger padding as well. */
.save-bar { position: sticky; bottom: calc(var(--pad-b, 0px) * -1); z-index: 2; margin-top: 12px; padding: 12px 0; background: var(--bg); border-top: 1px solid var(--border); }
.save-bar .unsaved { color: var(--warn); font-weight: 600; font-size: .9em; }
.rule-row { display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 8px; align-items: center; margin-bottom: 8px; }
.rule-row.actions { grid-template-columns: 1fr 2fr auto; }
/* A header typed by hand needs a box of its own, alongside the comparator. */
.rule-row.named-header { grid-template-columns: 1fr 1fr 1fr 1fr auto; }
.code { font-family: var(--font-mono); font-size: 12.5px; line-height: 1.5; white-space: pre; overflow: auto; background: var(--bg-sunken); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px; min-height: 240px; width: 100%; resize: vertical; tab-size: 2; }
.shortcut-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px 32px; }
.shortcut-grid h3 { margin: 0 0 6px; font-size: .9em; text-transform: uppercase; letter-spacing: .05em; color: var(--fg-faint); }
.shortcut-row { display: flex; justify-content: space-between; align-items: center; padding: 4px 0; gap: 12px; }
.shortcut-row .keys { display: flex; gap: 4px; }
.sessions-table { width: 100%; border-collapse: collapse; font-size: .92em; }
.sessions-table th, .sessions-table td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); }
.sessions-table th { color: var(--fg-muted); font-weight: 600; font-size: .85em; }
/* ==========================================================================
Contacts
========================================================================== */
.contacts-layout { display: grid; grid-template-columns: 220px minmax(280px, 360px) 1fr; height: 100%; min-height: 0; flex: 1; }
.contacts-books { border-right: 1px solid var(--border); padding: 12px 8px; overflow-y: auto; overflow-x: hidden; }
.contacts-list { border-right: 1px solid var(--border); display: flex; flex-direction: column; min-height: 0; }
.contacts-list .list-search { padding: 10px 12px; border-bottom: 1px solid var(--border); }
.contacts-scroll { flex: 1; overflow-y: auto; }
.contact-row { display: flex; align-items: center; gap: 12px; padding: 8px 14px; cursor: pointer; border-bottom: 1px solid var(--border); }
.contact-row:hover { background: var(--bg-hover); }
.contact-row.active { background: var(--selected-bg); }
.contact-row .c-name { font-weight: 550; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.contact-row .c-email { color: var(--fg-muted); font-size: .85em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.contact-letter { position: sticky; top: 0; background: var(--bg-sunken); padding: 4px 14px; font-size: .78em; font-weight: 700; color: var(--fg-muted); letter-spacing: .05em; z-index: 1; }
.contact-detail { overflow-y: auto; padding: 28px 32px 64px; }
.contact-hero { display: flex; align-items: center; gap: 20px; margin-bottom: 20px; }
.contact-hero h1 { margin: 0; font-size: 1.6em; font-weight: 650; }
.contact-hero .sub { color: var(--fg-muted); }
.contact-section { margin-bottom: 20px; }
.contact-section h3 { margin: 0 0 8px; font-size: .8em; text-transform: uppercase; letter-spacing: .06em; color: var(--fg-faint); }
.contact-kv { display: flex; align-items: flex-start; gap: 12px; padding: 6px 0; }
.contact-kv .k { width: 90px; color: var(--fg-muted); font-size: .9em; flex: 0 0 auto; padding-top: 2px; }
.contact-kv .v { flex: 1; overflow-wrap: anywhere; }
.contact-form .multi { display: flex; flex-direction: column; gap: 8px; }
.contact-form .multi-row { display: grid; grid-template-columns: 1fr 130px auto; gap: 8px; align-items: center; }
.contact-form .multi-row.address { grid-template-columns: 1fr auto; }
.contact-form .addr-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
/* ==========================================================================
Calendar
========================================================================== */
.cal-layout { display: grid; grid-template-columns: 240px 1fr; height: 100%; min-height: 0; }
.cal-side { border-right: 1px solid var(--border); padding: 12px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; }
.cal-main { display: flex; flex-direction: column; min-height: 0; min-width: 0; flex: 1; height: 100%; }
.cal-toolbar { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--border); flex: 0 0 auto; }
.cal-toolbar h2 { margin: 0 8px; font-size: 1.2em; font-weight: 650; min-width: 0; white-space: nowrap; }
.view-switch { display: inline-flex; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; }
.view-switch button { padding: 0 12px; height: 34px; font-weight: 500; color: var(--fg-muted); }
.view-switch button.active { background: var(--accent-soft); color: var(--accent-soft-fg); }
.view-switch button:not(:last-child) { border-right: 1px solid var(--border-strong); }
.mini-cal { font-size: .85em; }
.mini-cal .mc-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; font-weight: 650; }
.mini-cal .mc-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; text-align: center; }
.mini-cal .mc-dow { color: var(--fg-faint); font-size: .85em; padding: 2px 0; }
.mini-cal .mc-day { height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.mini-cal .mc-day:hover { background: var(--bg-hover); }
.mini-cal .mc-day.other { color: var(--fg-faint); }
.mini-cal .mc-day.today { font-weight: 700; color: var(--accent); }
.mini-cal .mc-day.selected { background: var(--accent); color: var(--accent-fg); }
.mini-cal .mc-day.has-events::after { content: ""; display: block; width: 4px; height: 4px; border-radius: 50%; background: var(--accent); position: relative; top: 9px; left: -1px; }
.cal-list-item { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: var(--radius-sm); cursor: pointer; }
.cal-list-item:hover { background: var(--bg-hover); }
.cal-list-item .cal-color { width: 14px; height: 14px; border-radius: 4px; border: 2px solid transparent; flex: 0 0 auto; }
.cal-list-item.hidden-cal .cal-color { background: transparent !important; }
.cal-list-item .cal-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cal-list-item .nav-more { opacity: 0; }
.cal-list-item:hover .nav-more { opacity: 1; }
.month-grid { flex: 1; display: grid; grid-template-rows: auto repeat(6, 1fr); min-height: 0; }
.month-grid .dow-row { display: grid; grid-template-columns: repeat(7, 1fr); border-bottom: 1px solid var(--border); }
.month-grid .dow-row div { padding: 6px; text-align: center; font-size: .8em; font-weight: 600; color: var(--fg-muted); text-transform: uppercase; letter-spacing: .04em; }
.month-grid .week-row { display: grid; grid-template-columns: repeat(7, 1fr); min-height: 0; border-bottom: 1px solid var(--border); }
.month-cell { border-right: 1px solid var(--border); padding: 4px; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 2px; overflow: hidden; cursor: pointer; position: relative; }
.month-cell:last-child { border-right: 0; }
.month-cell.other { background: var(--bg-sunken); color: var(--fg-faint); }
.month-cell:hover { background: var(--bg-hover); }
.month-cell .day-num { min-width: 26px; height: 26px; padding: 0 4px; display: flex; align-items: center; justify-content: center; border-radius: 999px; font-size: .9em; font-weight: 500; align-self: flex-start; white-space: nowrap; }
.month-cell.today .day-num { background: var(--accent); color: var(--accent-fg); font-weight: 700; }
.month-cell .more { font-size: .78em; color: var(--fg-muted); padding-left: 4px; font-weight: 600; }
.ev-chip { display: flex; align-items: center; gap: 4px; padding: 1px 6px; border-radius: 4px; font-size: .8em; line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; color: #fff; font-weight: 500; flex: 0 0 auto; border: 1px solid transparent; }
.ev-chip.timed { background: transparent !important; color: var(--fg); border-color: transparent !important; }
.ev-chip.timed .ev-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
.ev-chip.timed .ev-time { color: var(--fg-muted); }
.ev-chip:hover { filter: brightness(.95); }
.ev-chip.timed:hover { background: var(--bg-hover) !important; }
.ev-chip.declined { text-decoration: line-through; opacity: .6; }
.ev-chip.tentative { border-style: dashed; opacity: .85; }
.ev-chip.cancelled { text-decoration: line-through; opacity: .5; }
.week-view { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.week-head { display: grid; grid-template-columns: 56px repeat(var(--cols, 7), 1fr); border-bottom: 1px solid var(--border); flex: 0 0 auto; }
.week-head .wh-day { padding: 8px 4px 4px; text-align: center; border-left: 1px solid var(--border); cursor: pointer; }
.week-head .wh-day .dow { font-size: .75em; text-transform: uppercase; color: var(--fg-muted); font-weight: 600; }
.week-head .wh-day .dnum { font-size: 1.4em; font-weight: 500; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; }
.week-head .wh-day.today .dnum { background: var(--accent); color: var(--accent-fg); }
.week-allday { display: grid; grid-template-columns: 56px repeat(var(--cols, 7), 1fr); border-bottom: 1px solid var(--border); min-height: 28px; flex: 0 0 auto; }
.week-allday .ad-cell { border-left: 1px solid var(--border); padding: 2px; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.week-allday .ad-label { font-size: .7em; color: var(--fg-faint); text-align: right; padding: 4px 6px; }
.week-scroll { flex: 1; overflow-y: auto; position: relative; }
.week-body { display: grid; grid-template-columns: 56px repeat(var(--cols, 7), 1fr); position: relative; height: calc(24 * var(--hour-h, 48px)); }
.week-body .time-col { position: relative; }
.week-body .time-col .hour-label { position: absolute; right: 6px; transform: translateY(-50%); font-size: .72em; color: var(--fg-faint); }
.week-body .day-col { position: relative; border-left: 1px solid var(--border); cursor: pointer; }
.week-body .day-col.today { background: color-mix(in srgb, var(--accent-soft) 35%, transparent); }
.week-body .hour-line { position: absolute; left: 0; right: 0; border-top: 1px solid var(--border); pointer-events: none; }
.week-body .half-line { position: absolute; left: 0; right: 0; border-top: 1px dashed color-mix(in srgb, var(--border) 60%, transparent); pointer-events: none; }
.week-body .now-line { position: absolute; left: 0; right: 0; border-top: 2px solid var(--danger); z-index: 5; pointer-events: none; }
.week-body .now-line::before { content: ""; position: absolute; left: -5px; top: -6px; width: 10px; height: 10px; border-radius: 50%; background: var(--danger); }
.week-body .work-hours { position: absolute; left: 0; right: 0; background: color-mix(in srgb, var(--bg-elev) 60%, transparent); pointer-events: none; }
.ev-block { position: absolute; border-radius: 6px; padding: 3px 6px; font-size: .8em; color: #fff; overflow: hidden; cursor: pointer; box-shadow: var(--shadow-1); border: 1px solid rgba(255,255,255,.4); line-height: 1.3; z-index: 2; transition: box-shadow .1s; }
.ev-block:hover { box-shadow: var(--shadow-2); z-index: 3; }
.ev-block .ev-title { font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ev-block .ev-time { opacity: .9; font-size: .92em; }
.ev-block.declined { opacity: .5; text-decoration: line-through; }
.ev-block.tentative { background-image: repeating-linear-gradient(45deg, rgba(255,255,255,.12) 0 6px, transparent 6px 12px); }
.ev-block.draft-new { opacity: .7; border: 2px dashed #fff; }
.agenda { overflow-y: auto; padding: 12px 16px 64px; }
.agenda-day { display: grid; grid-template-columns: 120px 1fr; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border); }
.agenda-day .ad-date { font-weight: 600; }
.agenda-day .ad-date.today { color: var(--accent); }
.agenda-day .ad-date small { display: block; color: var(--fg-muted); font-weight: 400; }
.agenda-ev { display: flex; align-items: center; gap: 10px; padding: 6px 8px; border-radius: var(--radius-sm); cursor: pointer; }
.agenda-ev:hover { background: var(--bg-hover); }
.agenda-ev .ev-dot { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; }
.agenda-ev .ev-when { width: 140px; color: var(--fg-muted); font-size: .9em; flex: 0 0 auto; }
.event-popover { width: 360px; max-width: calc(100vw - 24px); padding: 14px 16px; }
.event-popover h3 { margin: 0 0 6px; font-size: 1.15em; font-weight: 650; padding-left: 16px; position: relative; }
.event-popover h3::before { content: ""; position: absolute; left: 0; top: 6px; width: 10px; height: 10px; border-radius: 3px; background: var(--ev-color, var(--accent)); }
.event-popover .ev-line { display: flex; gap: 10px; align-items: flex-start; color: var(--fg-muted); font-size: .92em; margin: 6px 0; }
.event-popover .ev-line svg { flex: 0 0 auto; margin-top: 2px; }
.participant-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: .92em; }
.participant-row .p-status { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; }
.p-status.accepted { background: var(--success); } .p-status.declined { background: var(--danger); } .p-status.tentative { background: var(--warn); } .p-status.needs-action { background: var(--fg-faint); }
.event-form .time-row { display: grid; grid-template-columns: 1fr auto 1fr; gap: 8px; align-items: center; }
.event-form .alerts-list { display: flex; flex-direction: column; gap: 6px; }
.event-form .alerts-list .row { gap: 6px; }
.freebusy { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; }
.freebusy .fb-row { display: flex; align-items: center; gap: 8px; font-size: .85em; }
.freebusy .fb-bar { flex: 1; height: 14px; background: var(--bg-sunken); border-radius: 4px; position: relative; overflow: hidden; }
.freebusy .fb-busy { position: absolute; top: 0; bottom: 0; background: var(--danger); opacity: .65; }
.freebusy .fb-window { position: absolute; top: 0; bottom: 0; border: 2px solid var(--accent); border-radius: 3px; }
/* The hour and day marks, and the labels above them. Without these the bar says
only "somewhere in here", and which hours it covered was a guess (issue #172). */
.freebusy .fb-axis-row { height: 14px; }
.freebusy .fb-axis { flex: 1; position: relative; height: 100%; }
.freebusy .fb-axis-label { position: absolute; top: 0; font-size: .78em; color: var(--fg-muted); transform: translateX(-50%); white-space: nowrap; }
/* Midnight and the far edge sit on the ends, where half of each would be cut off. */
.freebusy .fb-axis-label[style*="left: 0%"] { transform: none; }
.freebusy .fb-axis-label.end { left: auto; right: 0; transform: none; }
.freebusy .fb-tick { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--border); }
.freebusy .fb-tick.major { background: var(--fg-faint); }
.freebusy .fb-head { display: flex; align-items: center; gap: 6px; margin-bottom: 2px; font-size: .85em; }
.freebusy .fb-range { font-weight: 600; }
.freebusy .fb-self { font-weight: 600; }
/* A bar is somewhere to put the event, not only something to read. */
.freebusy .fb-bar { cursor: crosshair; }
.freebusy .fb-guide { position: absolute; top: -2px; bottom: -2px; width: 1px; background: var(--fg); opacity: .55; pointer-events: none; }
/*
* "We cannot say", which is not the same as "free" and must not look like it.
* Somebody outside this server has no free/busy to read, and an empty bar in a
* grid reads as an empty diary -- the one reading that is certainly wrong.
*/
.freebusy .fb-bar.no-data {
background-image: repeating-linear-gradient(135deg, var(--bg-sunken), var(--bg-sunken) 4px, var(--border) 4px, var(--border) 8px);
cursor: crosshair;
}
/* ==========================================================================
Files
========================================================================== */
.files-layout { display: flex; flex-direction: column; height: 100%; min-height: 0; flex: 1; }
.files-toolbar { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.breadcrumb { display: flex; align-items: center; gap: 4px; flex: 1; min-width: 0; overflow: hidden; }
.breadcrumb button { padding: 4px 8px; border-radius: 6px; color: var(--fg-muted); font-weight: 500; white-space: nowrap; }
.breadcrumb button:hover { background: var(--bg-hover); color: var(--fg); }
.breadcrumb button.current { color: var(--fg); font-weight: 650; }
.files-table { width: 100%; border-collapse: collapse; }
.files-table th { text-align: left; padding: 8px 12px; font-size: .8em; color: var(--fg-muted); font-weight: 600; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg-elev); z-index: 1; }
.files-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); }
.files-table tr { cursor: pointer; }
.files-table tbody tr:hover { background: var(--bg-hover); }
.files-table tr.selected { background: var(--selected-bg); }
.files-table .f-name { display: flex; align-items: center; gap: 10px; font-weight: 500; }
.files-table .f-name svg { color: var(--fg-muted); }
.files-scroll { flex: 1; overflow: auto; }
.files-layout.dropping .files-scroll { outline: 3px dashed var(--accent); outline-offset: -10px; }
/* ==========================================================================
Login
========================================================================== */
.login-page { min-height: 100%; display: flex; align-items: center; justify-content: center; padding: 24px; background: radial-gradient(1200px 600px at 10% -10%, var(--accent-soft), transparent 60%), radial-gradient(900px 500px at 110% 110%, var(--accent-soft), transparent 60%), var(--bg); }
.login-card { width: 100%; max-width: 400px; background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-2); padding: 32px 28px 24px; animation: rise .25s var(--ease); }
.login-card .logo { display: flex; flex-direction: column; align-items: center; gap: 6px; margin-bottom: 20px; }
.login-card .logo img { width: 120px; height: auto; }
/* The logo is the mark alone, so the name is set here rather than baked in. */
.login-card h1 { margin: 0; font-size: 1.6em; font-weight: 800; letter-spacing: -.02em; color: #14b8a6; }
.login-card .sub { color: var(--fg-muted); font-size: .92em; margin: 0; }
.login-card .tagline { color: var(--fg-muted); font-size: 1em; margin: 4px 0 0; text-align: center; }
.login-card .foot a { color: var(--fg-muted); text-decoration: underline; }
.login-card .foot { margin-top: 20px; font-size: .8em; color: var(--fg-faint); text-align: center; }
.login-card .pw-wrap { position: relative; }
.login-card .pw-wrap .icon-btn { position: absolute; right: 2px; top: 1px; }
/* The translate boundary is a wrapper with no appearance of its own; it must
not interrupt the flex/grid chain the panes inside it rely on. */
.translate-boundary { display: contents; }
/* ==========================================================================
Touch gestures (see lib/touch.ts)
========================================================================== */
/*
* `pan-y` is what makes a swipe possible without costing a scroll: the browser
* keeps the vertical drag for itself, at its own frame rate, and only sends us
* the horizontal movement it now knows it will not use. Without it every
* pointermove would arrive after the scroll had already started, and the list
* would stutter on the way past.
*/
/*
* `pinch-zoom` is listed alongside `pan-y` rather than left implied: naming any
* value at all drops every gesture not named, so a bare `pan-y` also told the
* browser not to zoom here. Pinch worked on an open message and died on the
* list, which read as the zoom being broken at random. The swipe is unaffected
* -- horizontal is still not in the list, so it still arrives here.
*/
.msg-row { touch-action: pan-y pinch-zoom; -webkit-touch-callout: none; }
.msg-row.swiping { z-index: 4; box-shadow: var(--shadow-2); }
/* The row being dragged is the one thing on screen that must not lag. */
.msg-row.swiping, .mail-list-pull { will-change: transform; }
/* Stop a pull at the top of the list from dragging the page behind it. */
.mail-list { overscroll-behavior-y: contain; }
/* The strip a swiped row slides off to reveal, painted into the row's slot. */
.msg-swipe { position: absolute; left: 0; right: 0; z-index: 1; display: flex; align-items: center; padding: 0 22px; color: #fff; border-bottom: 1px solid var(--border); }
.msg-swipe.from-left { justify-content: flex-start; }
.msg-swipe.from-right { justify-content: flex-end; }
.msg-swipe.danger { background: var(--danger); }
.msg-swipe.warn { background: var(--warn); }
.msg-swipe.accent { background: var(--accent); color: var(--accent-fg); }
.msg-swipe.neutral { background: var(--fg-muted); }
/*
* Dim until the swipe is far enough to fire, then full strength and a little
* larger. It is the same promise the haptic tap makes, for anyone whose phone
* has no motor or has it switched off.
*/
.msg-swipe-act { display: inline-flex; align-items: center; gap: 10px; font-weight: 650; opacity: .72; transform: scale(.94); transition: opacity .12s var(--ease), transform .12s var(--ease); }
.msg-swipe.armed .msg-swipe-act { opacity: 1; transform: none; }
/* Pull to refresh: a zero-height sticky perch for the dial, so an unpulled
list gives up no space to it. */
.ptr { position: sticky; top: 0; left: 0; height: 0; z-index: 5; pointer-events: none; }
.ptr-dial { position: absolute; left: 50%; top: 0; display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px; border-radius: 50%; background: var(--bg-elev); border: 1px solid var(--border); box-shadow: var(--shadow-2); color: var(--fg-muted); }
.ptr-dial.armed { color: var(--accent); border-color: var(--accent); }
/*
* A tap that leaves a grey rectangle behind reads as a rendering glitch rather
* than as feedback, and every surface here has a :active or a ripple of its own.
*/
@media (hover: none) {
* { -webkit-tap-highlight-color: transparent; }
.msg-row:active { background: var(--bg-hover); }
.nav-item, .module-link, .mobile-tabbar a { -webkit-touch-callout: none; }
}
/* ==========================================================================
Responsive
========================================================================== */
@media (max-width: 1100px) {
:root { --list-w: 44%; }
.msg-row .msg-from { width: 140px; }
.contacts-layout { grid-template-columns: 200px 300px 1fr; }
}
@media (max-width: 900px) {
.mail-layout:not(.pane-off):not(.pane-bottom) { grid-template-columns: 1fr !important; }
.mail-layout.pane-bottom { grid-template-rows: 1fr !important; }
.mail-layout.pane-bottom .mail-reading-pane { display: none; }
.mail-layout.pane-bottom.reading .mail-list-pane { display: none; }
.mail-layout.pane-bottom.reading .mail-reading-pane { display: flex; }
.mail-layout .splitter { display: none; }
.mail-layout:not(.pane-bottom) .mail-reading-pane { display: none; }
.mail-layout.reading .mail-list-pane { display: none; }
.mail-layout.reading .mail-reading-pane { display: flex; }
.settings-layout { grid-template-columns: 1fr; }
.settings-nav { display: none; }
.settings-layout.section .settings-nav { display: none; }
.settings-layout.root .settings-nav { display: block; border-right: 0; }
.settings-layout.root .settings-content { display: none; }
.settings-content { --pad-b: 80px; padding: 16px 16px var(--pad-b); }
.contacts-layout { grid-template-columns: 1fr; }
.contacts-books { display: none; }
.contacts-layout.detail .contacts-list { display: none; }
.contacts-layout:not(.detail) .contact-detail { display: none; }
.cal-layout { grid-template-columns: 1fr; }
.cal-side { display: none; }
}
@media (max-width: 768px) {
:root { --topbar-h: 52px; }
.app-body, .app-body.collapsed { grid-template-columns: minmax(0, 1fr); }
.sidebar { position: fixed; top: 0; bottom: 0; left: 0; width: min(300px, 85vw); background: var(--bg-elev); z-index: 950; transform: translateX(-105%); transition: transform .22s var(--ease); box-shadow: var(--shadow-3); padding-top: 12px; }
.sidebar.open { transform: none; }
.collapsed .sidebar .nav-item, .collapsed .sidebar .compose-btn { all: revert; }
.collapsed .module-bar { flex-direction: row; }
.collapsed .module-link { width: auto; height: auto; padding: 6px 2px; border-radius: var(--radius-sm); }
.collapsed .module-link .module-label { display: block; }
.drawer-backdrop { display: block; position: fixed; inset: 0; z-index: 940; background: rgba(2,6,23,.45); opacity: 0; pointer-events: none; transition: opacity .2s; }
.drawer-backdrop.open { opacity: 1; pointer-events: auto; }
.main { border-radius: 0; border: 0; }
/*
index.html asks for `viewport-fit=cover`, which is what lets the app paint
under a notch and behind the home indicator — and what makes it this
stylesheet's job to keep anything readable out from under them. The insets
are zero everywhere else, so these need no media query of their own.
*/
.topbar { padding: 0 max(6px, env(safe-area-inset-right)) 0 max(6px, env(safe-area-inset-left)); gap: 4px; }
.topbar .brand .brand-name { display: none; }
.searchbar .search-input { height: 40px; }
.topbar-actions .hide-mobile { display: none; }
.msg-row { padding-right: 8px; }
.mail-list { padding-bottom: 72px; }
/*
`dvh`, not `vh`. On a phone browser `100vh` is the tall layout viewport that
ignores the address bar, so a full-screen composer put its Send button
behind the toolbar until the page was scrolled — which a fixed element
cannot be. In an installed PWA there is no bar and the two agree.
*/
.composer { width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; border-radius: 0; position: fixed; inset: 0; }
.composer.minimized { height: calc(44px + env(safe-area-inset-bottom)); width: 100vw; top: auto; bottom: 0; }
.composer-dock { right: 0; }
.dialog { max-height: calc(100dvh - 16px); }
.fab { display: flex; position: fixed; right: calc(18px + env(safe-area-inset-right)); bottom: calc(78px + env(safe-area-inset-bottom)); width: 56px; height: 56px; border-radius: 18px; background: var(--accent); color: var(--accent-fg); align-items: center; justify-content: center; box-shadow: var(--shadow-2); z-index: 700; }
/* The bar grows by the home indicator rather than hiding behind it, and
`.main` below reserves exactly that much. */
.mobile-tabbar { display: grid; grid-template-columns: repeat(4, 1fr); position: fixed; left: 0; right: 0; bottom: 0; height: calc(60px + env(safe-area-inset-bottom)); background: var(--bg-elev); border-top: 1px solid var(--border); z-index: 700; padding-bottom: env(safe-area-inset-bottom); }
.mobile-tabbar a { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; color: var(--fg-muted); text-decoration: none; font-size: 11px; }
.mobile-tabbar a.active { color: var(--accent); }
.main { padding-bottom: calc(60px + env(safe-area-inset-bottom)); }
.sidebar { padding-bottom: env(safe-area-inset-bottom); padding-left: env(safe-area-inset-left); }
/* Lines the close up with the hamburger underneath it: same height as the
top bar, same 12px in from the edge, so the second press lands where the
first one did. The negative margin cancels the drawer's own padding-top,
which would otherwise push it a row lower than the thing it replaces. */
.drawer-head { display: flex; align-items: center; height: var(--topbar-h); margin-top: -12px; padding-left: max(6px, env(safe-area-inset-left)); }
.thread-subject { padding: 14px 16px 8px; }
.message { margin: 0 8px 8px; }
.message-head { padding: 10px 12px; }
.message-body { padding: 4px 12px 12px; }
.thread-toolbar .hide-mobile { display: none; }
.list-toolbar .hide-mobile { display: none; }
.week-head .wh-day .dnum { width: 32px; height: 32px; font-size: 1.1em; }
.cal-toolbar h2 { font-size: 1em; }
.contact-detail { padding: 16px 16px 80px; }
.files-table .hide-mobile { display: none; }
.event-popover { width: calc(100vw - 24px); }
}
/*
* Touch targets.
*
* `pointer: coarse` rather than a width, for the reason ui/misc.tsx gives about
* useIsTouch: width decides the layout, the pointer decides what has to be
* aimed at. A tablet in landscape is a wide screen with fat targets.
*
* Everything here was under the 44px a fingertip actually covers -- the top bar
* icons at 36px (the search filter at 30), the folder twisty at 18, the row ⋮
* at 24. Growing the boxes would reflow a top bar that has no room to give, so
* each control keeps the size it draws at and gains a transparent hit area
* centred on it. Rows grow for real, because a 44px hit area inside a 36px row
* would reach into the rows above and below and steal their taps.
*/
@media (pointer: coarse) {
.icon-btn, .nav-twisty { position: relative; }
/* `.drill-into` already draws at 44px and is excluded: expanding a control
that is big enough only lets it reach into its neighbour. */
.icon-btn:not(.drill-into)::after, .nav-twisty[role="button"]::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: max(100%, 44px);
height: max(100%, 44px);
transform: translate(-50%, -50%);
}
/*
* The ⋮ is the exception, and narrower on purpose. It sits immediately left
* of the drill chevron, and at a full 44px wide the two hit areas overlapped
* by 4px -- so the right edge of "folder options" silently drilled instead.
* The row is 44px tall here, so it still gets the full height, which is the
* axis a thumb actually misses on.
*/
.nav-item .nav-more::after { width: max(100%, 36px); }
.sidebar .nav-item, .cal-list-item { height: 44px; }
/* The hamburger is the one control in the corner a thumb is worst at, so it
gets real distance from the bezel rather than just a wider hit area. */
/* Both, together: the close in the drawer stands exactly on the hamburger it
covers, and it only does that if the two are moved by the same rule. */
.topbar, .drawer-head { padding-left: max(12px, env(safe-area-inset-left)); }
}
@media (hover: none) {
.msg-row .msg-check { opacity: 1; }
.msg-row .msg-actions { display: none !important; }
.msg-row:hover .msg-meta .msg-date { display: inline; }
.nav-item .nav-more, .cal-list-item .nav-more { opacity: 1; }
}
/*
* Print.
*
* Paper has no theme. Whatever palette is on screen, the tokens are pinned to
* a light, unpainted set here -- backgrounds white rather than the light
* theme's greys, since a printer would otherwise lay ink over the whole sheet.
* Doing it on the tokens rather than per rule is what reaches the message
* body: it renders in a shadow root this stylesheet cannot select into, and
* follows the app theme through inherited custom properties (see
* EMAIL_BASE_CSS in lib/html.ts). Mail that brings colours of its own is left
* alone -- it is not themed in the first place.
*
* `!important` rather than a longer selector: the dark palettes sit on
* :root[data-theme] and :root[data-palette][data-accent], and out-specifying
* every one of them is a chain that the next palette breaks.
*/
@media print {
:root {
--bg: #fff !important;
--bg-elev: #fff !important;
--bg-sunken: #fff !important;
--bg-hover: transparent !important;
--bg-active: transparent !important;
--unread-bg: #fff !important;
--read-bg: #fff !important;
--selected-bg: transparent !important;
--fg: #000 !important;
--fg-muted: #3a3a3a !important;
--fg-faint: #5a5a5a !important;
--border: #ccc !important;
--border-strong: #999 !important;
--accent: #0f766e !important;
--accent-fg: #fff !important;
--accent-soft: transparent !important;
--accent-soft-fg: #0b5750 !important;
--danger: #b91c1c !important;
--danger-soft: transparent !important;
--warn: #92400e !important;
--warn-soft: transparent !important;
--success: #15803d !important;
--success-soft: transparent !important;
--link: #0e7490 !important;
--star: #444 !important;
--q1: #1d4ed8 !important;
--q2: #15803d !important;
--q3: #7e22ce !important;
--shadow-1: none !important;
--shadow-2: none !important;
--shadow-3: none !important;
color-scheme: light !important;
}
.topbar, .sidebar, .mail-list-pane, .splitter, .thread-toolbar, .composer-dock, .toast-host, .mobile-tabbar, .fab, .reply-box, .thread-actions, .quote-toggle { display: none !important; }
.message-head .meta .icon-btn, .message-head .who .to button { display: none !important; }
.app, .app-body, .main, .mail-layout, .mail-reading-pane, .thread-view, .thread-scroll { display: block !important; height: auto !important; overflow: visible !important; grid-template-columns: 1fr !important; border: 0 !important; }
/*
* `break-inside: avoid` on the whole card is what put the first message on
* page two: a message longer than a sheet cannot honour it, and Chrome
* answers by moving the card to a fresh page and breaking it there anyway --
* leaving page one holding nothing but the subject. Only the header is
* indivisible now, and it is kept with the body that follows it.
*/
.message { break-inside: auto; border: 1px solid var(--border); }
.message-head { break-inside: avoid; break-after: avoid; }
/*
* "Print" in a message's own menu marks that card and prints it alone;
* "Print conversation" in the thread toolbar sets no mark and prints them
* all. The subject heading stays either way -- a printed
* message with no subject on it is a page nobody can file -- but the
* "3 messages" count beside it goes, since only one of them is on the page.
*/
.printing-one .message:not(.print-target) { display: none !important; }
.printing-one .thread-subject > .muted { display: none !important; }
/*
* Printing from the file viewer. The dialog is the document: the mail or the
* file listing behind it is not what was asked for, and the chrome of the
* dialog itself -- title bar, Print and Download buttons -- is not part of
* the file. A PDF never comes through here; it prints itself from its own
* iframe (see ui/filepreview.tsx).
*/
/* The dialog is portalled to <body>, so the whole app goes -- hiding `.app`
alone left `#root` holding its `height: 100%` and printing a blank first
page, the same way `break-inside` did on a message. */
.printing-preview #root { display: none !important; }
.printing-preview body { height: auto !important; }
.printing-preview .dialog-backdrop { position: static !important; display: block !important; padding: 0 !important; background: none !important; backdrop-filter: none !important; animation: none !important; }
.printing-preview .dialog { max-width: none !important; max-height: none !important; border: 0 !important; box-shadow: none !important; animation: none !important; background: none !important; }
.printing-preview .dialog-head, .printing-preview .dialog-foot { display: none !important; }
.printing-preview .dialog-body { padding: 0 !important; overflow: visible !important; }
.printing-preview .dialog-body .code { max-height: none !important; overflow: visible !important; border: 0 !important; padding: 0 !important; }
.printing-preview .dialog-body .md-body { max-height: none !important; overflow: visible !important; }
.printing-preview .md-body pre { overflow: visible !important; white-space: pre-wrap !important; }
.printing-preview .dialog-body img { max-height: none !important; }
.print-only { display: block; }
body { background: #fff; color: #000; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}
/* Addresses in a message carry a right-click menu (see mail/AddressMenu.tsx). */
.addr { cursor: context-menu; }
.message-details .addr:hover, .message-head .from .addr:hover { text-decoration: underline dotted; text-underline-offset: 2px; }
/* ---- Date & time fields (custom pickers; see ui/datefield.tsx) ---- */
.dp-field { position: relative; display: inline-flex; align-items: center; width: 100%; }
.dp-field .input { width: 100%; padding-right: 30px; }
.dp-open { position: absolute; right: 4px; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: 0; background: none; color: var(--fg-muted); cursor: pointer; border-radius: var(--radius-sm); }
button.dp-open:hover { background: var(--bg-hover); color: var(--fg); }
button.dp-open:disabled { cursor: default; opacity: .5; }
.dp-field.w-auto { width: auto; }
.dp-field.w-auto .input { width: 10em; }
.dp-datetime { display: flex; gap: 6px; align-items: center; }
.dp-datetime .dp-field { flex: 1 1 auto; }
.dp-datetime .dp-time-field { flex: 0 0 8.5em; }
.dp-pop { padding: 10px; }
.dp-pop-wide { display: flex; }
.dp-split { display: flex; gap: 10px; align-items: stretch; }
.dp-cal { width: 15.5em; }
.dp-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; font-weight: 650; }
.dp-dow, .dp-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; text-align: center; }
.dp-dow { color: var(--fg-faint); font-size: .8em; padding-bottom: 2px; }
.dp-day { height: 28px; border: 0; background: none; color: inherit; font: inherit; font-size: .9em; border-radius: 50%; cursor: pointer; }
.dp-day:hover { background: var(--bg-hover); }
.dp-day:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.dp-day.other { color: var(--fg-faint); }
.dp-day.today { font-weight: 700; color: var(--accent); }
.dp-day.selected { background: var(--accent); color: var(--accent-fg); font-weight: 600; }
.dp-foot { display: flex; justify-content: space-between; margin-top: 6px; }
.dp-times { display: flex; flex-direction: column; gap: 1px; overflow-y: auto; max-height: 17em; min-width: 7.5em; border-left: 1px solid var(--border); padding-left: 8px; }
.dp-time { border: 0; background: none; color: inherit; font: inherit; font-size: .9em; text-align: left; padding: 4px 8px; border-radius: var(--radius-sm); cursor: pointer; white-space: nowrap; }
.dp-time:hover { background: var(--bg-hover); }
.dp-time.selected { background: var(--accent); color: var(--accent-fg); }
@media (max-width: 480px) {
/* The spacer would take the whole of the first line and push the overflow
onto a line of its own; packed together they wrap as a group instead. */
.reply-box .reply-prompt .spacer { display: none; }
.dp-datetime { flex-wrap: wrap; }
.dp-datetime .dp-time-field { flex: 1 1 100%; }
.dp-split { flex-direction: column; }
.dp-times { flex-direction: row; overflow-x: auto; max-height: none; border-left: 0; border-top: 1px solid var(--border); padding: 6px 0 0; }
}
/* Files: the sidebar tree reuses .nav-item, so only the parts the mail tree has
no equivalent for are here. A row in the list is a drop target the same way a
folder in the tree is, and says so the same way. */
/* Shown only when more than one row is selected: with a single row the row
menu already says everything this would. */
.selection-bar { display: flex; align-items: center; gap: 8px; padding: 8px 12px; margin: 0 12px 4px; border-radius: var(--radius); background: var(--accent-soft); color: var(--accent-soft-fg); }
.selection-bar .grow { font-weight: 600; }
.files-table tbody tr.drop-target > td { background: var(--accent-soft); }
.files-table tbody tr.drop-target > td:first-child { box-shadow: inset 2px 0 0 var(--accent); }
.files-table tbody tr[draggable="true"] { cursor: grab; }
.files-table tbody tr[draggable="true"]:active { cursor: grabbing; }
.sidebar .nav-item[draggable="true"] { cursor: pointer; }
.f-name .faint { flex: none; }
/* The "Shared with me" header carries a refresh control, so it is a row rather
than the plain label the other sections use. */
.sidebar .nav-section { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.spin { animation: spin 1s linear infinite; }
@media (prefers-reduced-motion: reduce) { .spin { animation: none; } }
/* The composer's To label doubles as the way into the address books. */
.composer-field label .link-btn { background: none; border: 0; padding: 0; font: inherit; color: inherit; cursor: pointer; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 3px; }
.composer-field label .link-btn:hover { color: var(--accent); }
.composer-field label .link-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }