/* ========================================================================== 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-theme="dark"][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; } /* === generated palettes: begin === */ /* * Written by scripts/build-palettes.py -- edit the sources there, not here. * * Every colour is from the palette's own project (all MIT); the published * values are recorded in .palette-sources/palettes-upstream.md. The tiers * between them are derived, and every text colour is checked against the * surface it sits on: 4.5:1 for prose, 3:1 for borders and marks. Several * of these palettes do not meet that as published -- Dracula's comment grey * is about 3.0:1 on its own background -- so those tiers are lifted, which * is why this is arithmetic rather than a hand-written block. */ /* ihasmail (light) lifted for contrast: faint #7d969c -> #62757a (2.94 -> 4.55); accent #46cac3 -> #379e98 (1.88 -> 3.04); star #f9a34b -> #c5813b (1.91 -> 3.00); border-strong #b2c5c9 -> #849295 (1.69 -> 3.03) */ :root[data-palette="ihasmail"] { --bg: #f4f9f9; --bg-elev: #ffffff; --bg-sunken: #e7f1f2; --bg-hover: rgba(13, 36, 48, 0.06); --bg-active: rgba(13, 36, 48, 0.11); --fg: #0d2430; --fg-muted: #4a6b74; --fg-faint: #62757a; --border: #cfe2e4; --border-strong: #849295; --accent: #379e98; --accent-fg: #ffffff; --accent-soft: #daeceb; --accent-soft-fg: #28736f; --danger: #dc2626; --danger-soft: rgba(220, 38, 38, 0.15); --warn: #b45309; --warn-soft: rgba(180, 83, 9, 0.15); --success: #15803d; --success-soft: rgba(21, 128, 61, 0.15); --link: #0e7490; --unread-bg: #ffffff; --read-bg: #edf3f3; --selected-bg: #daeceb; --focus-ring: 0 0 0 3px rgba(55, 158, 152, 0.4); --star: #c5813b; --q1: #0e7490; --q2: #15803d; --q3: #7c3aed; --scrollbar: rgba(74, 107, 116, 0.35); color-scheme: light; } /* dracula (light) lifted for contrast: faint #98937b -> #787461 (2.98 -> 4.53); border-strong #b5b5c1 -> #91919a (1.96 -> 3.01) */ :root[data-palette="dracula"] { --bg: #fffbeb; --bg-elev: #ffffff; --bg-sunken: #f6f1de; --bg-hover: rgba(31, 31, 31, 0.06); --bg-active: rgba(31, 31, 31, 0.11); --fg: #1f1f1f; --fg-muted: #6c664b; --fg-faint: #787461; --border: #cfcfde; --border-strong: #91919a; --accent: #644ac9; --accent-fg: #ffffff; --accent-soft: #e9e2e6; --accent-soft-fg: #644ac9; --danger: #cb3a2a; --danger-soft: rgba(203, 58, 42, 0.15); --warn: #a34d14; --warn-soft: rgba(163, 77, 20, 0.15); --success: #14710a; --success-soft: rgba(20, 113, 10, 0.15); --link: #036a96; --unread-bg: #ffffff; --read-bg: #f8f4e5; --selected-bg: #e9e2e6; --focus-ring: 0 0 0 3px rgba(100, 74, 201, 0.4); --star: #846e15; --q1: #036a96; --q2: #14710a; --q3: #a3144d; --scrollbar: rgba(108, 102, 75, 0.35); color-scheme: light; } /* dracula (dark) lifted for contrast: muted #6272a4 -> #8390b7 (3.03 -> 4.50); faint #687190 -> #8b92aa (2.95 -> 4.60); border-strong #5f6271 -> #717381 (2.35 -> 3.03) */ :root[data-theme="dark"][data-palette="dracula"] { --bg: #282a36; --bg-elev: #2f3140; --bg-sunken: #21222c; --bg-hover: rgba(248, 248, 242, 0.06); --bg-active: rgba(248, 248, 242, 0.11); --fg: #f8f8f2; --fg-muted: #8390b7; --fg-faint: #8b92aa; --border: #44475a; --border-strong: #717381; --accent: #bd93f9; --accent-fg: #282a36; --accent-soft: rgba(189, 147, 249, 0.16); --accent-soft-fg: #bf96f9; --danger: #ff5555; --danger-soft: rgba(255, 85, 85, 0.15); --warn: #ffb86c; --warn-soft: rgba(255, 184, 108, 0.15); --success: #50fa7b; --success-soft: rgba(80, 250, 123, 0.15); --link: #8be9fd; --unread-bg: #2f3140; --read-bg: #21222c; --selected-bg: rgba(189, 147, 249, 0.18); --focus-ring: 0 0 0 3px rgba(189, 147, 249, 0.4); --star: #f1fa8c; --q1: #8be9fd; --q2: #50fa7b; --q3: #ff79c6; --scrollbar: rgba(131, 144, 183, 0.35); color-scheme: dark; --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); } /* gruvbox (light) lifted for contrast: muted #7c6f64 -> #776b60 (4.29 -> 4.56); faint #9f937f -> #746b5d (2.66 -> 4.62); link #427b58 -> #417956 (4.40 -> 4.52); success #79740e -> #746f0d (4.29 -> 4.60); border-strong #beaf91 -> #948871 (1.90 -> 3.07); q2 #79740e -> #746f0d (4.29 -> 4.60) */ :root[data-palette="gruvbox"] { --bg: #fbf1c7; --bg-elev: #f9f5d7; --bg-sunken: #f2e5bc; --bg-hover: rgba(60, 56, 54, 0.06); --bg-active: rgba(60, 56, 54, 0.11); --fg: #3c3836; --fg-muted: #776b60; --fg-faint: #746b5d; --border: #d5c4a1; --border-strong: #948871; --accent: #076678; --accent-fg: #ffffff; --accent-soft: #d9debc; --accent-soft-fg: #076678; --danger: #9d0006; --danger-soft: rgba(157, 0, 6, 0.15); --warn: #af3a03; --warn-soft: rgba(175, 58, 3, 0.15); --success: #746f0d; --success-soft: rgba(116, 111, 13, 0.15); --link: #417956; --unread-bg: #ffffff; --read-bg: #f5ebc3; --selected-bg: #d9debc; --focus-ring: 0 0 0 3px rgba(7, 102, 120, 0.4); --star: #b57614; --q1: #076678; --q2: #746f0d; --q3: #8f3f71; --scrollbar: rgba(119, 107, 96, 0.35); color-scheme: light; } /* gruvbox (dark) lifted for contrast: faint #827768 -> #968d80 (3.36 -> 4.51); danger #fb4934 -> #fb5440 (4.29 -> 4.52); border-strong #675f55 -> #787168 (2.35 -> 3.06) */ :root[data-theme="dark"][data-palette="gruvbox"] { --bg: #282828; --bg-elev: #32302f; --bg-sunken: #1d2021; --bg-hover: rgba(235, 219, 178, 0.06); --bg-active: rgba(235, 219, 178, 0.11); --fg: #ebdbb2; --fg-muted: #a89984; --fg-faint: #968d80; --border: #504945; --border-strong: #787168; --accent: #83a598; --accent-fg: #282828; --accent-soft: rgba(131, 165, 152, 0.16); --accent-soft-fg: #8cab9f; --danger: #fb5440; --danger-soft: rgba(251, 84, 64, 0.15); --warn: #fe8019; --warn-soft: rgba(254, 128, 25, 0.15); --success: #b8bb26; --success-soft: rgba(184, 187, 38, 0.15); --link: #8ec07c; --unread-bg: #32302f; --read-bg: #1d2021; --selected-bg: rgba(131, 165, 152, 0.18); --focus-ring: 0 0 0 3px rgba(131, 165, 152, 0.4); --star: #fabd2f; --q1: #83a598; --q2: #b8bb26; --q3: #d3869b; --scrollbar: rgba(168, 153, 132, 0.35); color-scheme: dark; --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); } /* rose-pine (light) lifted for contrast: muted #797593 -> #716d89 (4.02 -> 4.52); faint #9a96a7 -> #706e7a (2.64 -> 4.58); danger #b4637a -> #a45a6f (3.84 -> 4.50); warn #ea9d34 -> #986622 (2.05 -> 4.51); success #56949f -> #467881 (3.14 -> 4.51); star #ea9d34 -> #be7f2a (2.05 -> 3.07); border-strong #c8c2c1 -> #908c8b (1.61 -> 3.05); q2 #56949f -> #467881 (3.14 -> 4.51); q3 #907aa9 -> #7a6890 (3.47 -> 4.57) */ :root[data-palette="rose-pine"] { --bg: #faf4ed; --bg-elev: #fffaf3; --bg-sunken: #f2e9e1; --bg-hover: rgba(70, 66, 97, 0.06); --bg-active: rgba(70, 66, 97, 0.11); --fg: #464261; --fg-muted: #716d89; --fg-faint: #706e7a; --border: #dfd9d2; --border-strong: #908c8b; --accent: #907aa9; --accent-fg: #ffffff; --accent-soft: #ebe3e3; --accent-soft-fg: #705f84; --danger: #a45a6f; --danger-soft: rgba(164, 90, 111, 0.15); --warn: #986622; --warn-soft: rgba(152, 102, 34, 0.15); --success: #467881; --success-soft: rgba(70, 120, 129, 0.15); --link: #286983; --unread-bg: #ffffff; --read-bg: #f5efe9; --selected-bg: #ebe3e3; --focus-ring: 0 0 0 3px rgba(144, 122, 169, 0.4); --star: #be7f2a; --q1: #286983; --q2: #467881; --q3: #7a6890; --scrollbar: rgba(113, 109, 137, 0.35); color-scheme: light; } /* rose-pine (dark) lifted for contrast: faint #6c6982 -> #828095 (3.35 -> 4.60); success #31748f -> #5089a0 (3.38 -> 4.56); border-strong #423f56 -> #666376 (1.75 -> 3.04); q2 #31748f -> #5089a0 (3.38 -> 4.56) */ :root[data-theme="dark"][data-palette="rose-pine"] { --bg: #191724; --bg-elev: #1f1d2e; --bg-sunken: #14121f; --bg-hover: rgba(224, 222, 244, 0.06); --bg-active: rgba(224, 222, 244, 0.11); --fg: #e0def4; --fg-muted: #908caa; --fg-faint: #828095; --border: #26233a; --border-strong: #666376; --accent: #c4a7e7; --accent-fg: #191724; --accent-soft: rgba(196, 167, 231, 0.16); --accent-soft-fg: #c4a7e7; --danger: #eb6f92; --danger-soft: rgba(235, 111, 146, 0.15); --warn: #f6c177; --warn-soft: rgba(246, 193, 119, 0.15); --success: #5089a0; --success-soft: rgba(80, 137, 160, 0.15); --link: #9ccfd8; --unread-bg: #1f1d2e; --read-bg: #14121f; --selected-bg: rgba(196, 167, 231, 0.18); --focus-ring: 0 0 0 3px rgba(196, 167, 231, 0.4); --star: #f6c177; --q1: #9ccfd8; --q2: #5089a0; --q3: #c4a7e7; --scrollbar: rgba(144, 140, 170, 0.35); color-scheme: dark; --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); } /* tokyo-night (light) lifted for contrast: faint #777a8b -> #646675 (3.44 -> 4.60); border-strong #acaeb6 -> #83848a (1.79 -> 3.02) */ :root[data-palette="tokyo-night"] { --bg: #e6e7ed; --bg-elev: #f2f3f7; --bg-sunken: #d6d8df; --bg-hover: rgba(52, 59, 89, 0.06); --bg-active: rgba(52, 59, 89, 0.11); --fg: #343b59; --fg-muted: #484c61; --fg-faint: #646675; --border: #c1c2c7; --border-strong: #83848a; --accent: #2959aa; --accent-fg: #ffffff; --accent-soft: #ccd3e4; --accent-soft-fg: #2959aa; --danger: #8c4351; --danger-soft: rgba(140, 67, 81, 0.15); --warn: #8f5e15; --warn-soft: rgba(143, 94, 21, 0.15); --success: #385f0d; --success-soft: rgba(56, 95, 13, 0.15); --link: #006c86; --unread-bg: #ffffff; --read-bg: #e1e2e9; --selected-bg: #ccd3e4; --focus-ring: 0 0 0 3px rgba(41, 89, 170, 0.4); --star: #8f5e15; --q1: #006c86; --q2: #385f0d; --q3: #65359d; --scrollbar: rgba(72, 76, 97, 0.35); color-scheme: light; } /* tokyo-night (dark) lifted for contrast: border-strong #4b506c -> #62677f (2.17 -> 3.06) */ :root[data-theme="dark"][data-palette="tokyo-night"] { --bg: #1a1b26; --bg-elev: #1f2130; --bg-sunken: #16161e; --bg-hover: rgba(192, 202, 245, 0.06); --bg-active: rgba(192, 202, 245, 0.11); --fg: #c0caf5; --fg-muted: #a9b1d6; --fg-faint: #7e84a1; --border: #363b54; --border-strong: #62677f; --accent: #7aa2f7; --accent-fg: #1a1b26; --accent-soft: rgba(122, 162, 247, 0.16); --accent-soft-fg: #7aa2f7; --danger: #f7768e; --danger-soft: rgba(247, 118, 142, 0.15); --warn: #e0af68; --warn-soft: rgba(224, 175, 104, 0.15); --success: #9ece6a; --success-soft: rgba(158, 206, 106, 0.15); --link: #7dcfff; --unread-bg: #1f2130; --read-bg: #16161e; --selected-bg: rgba(122, 162, 247, 0.18); --focus-ring: 0 0 0 3px rgba(122, 162, 247, 0.4); --star: #e0af68; --q1: #7dcfff; --q2: #9ece6a; --q3: #bb9af7; --scrollbar: rgba(169, 177, 214, 0.35); color-scheme: dark; --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); } /* === generated palettes: end === */ /* 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