/* ========================================================================== 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); } /* catppuccin (light) lifted for contrast: fg #4c4f69 -> #484b64 (6.57 -> 7.00); muted #6c6f85 -> #64677c (4.06 -> 4.58); faint #8b8e9e -> #656873 (2.67 -> 4.57); link #1e66f5 -> #1c5fe4 (4.04 -> 4.54); danger #d20f39 -> #d00f38 (4.46 -> 4.53); warn #fe640b -> #b24608 (2.45 -> 4.58); success #40a02b -> #307820 (2.75 -> 4.50); star #df8e1d -> #b97618 (2.15 -> 3.05); border-strong #b8bcc8 -> #83858e (1.56 -> 3.02); q1 #1e66f5 -> #1c5fe4 (4.04 -> 4.54); q2 #40a02b -> #307820 (2.75 -> 4.50); q3 #ea76cb -> #9a4e86 (2.17 -> 4.53) */ :root[data-palette="catppuccin"] { --bg: #e6e9ef; --bg-elev: #eff1f5; --bg-sunken: #dce0e8; --bg-hover: rgba(72, 75, 100, 0.06); --bg-active: rgba(72, 75, 100, 0.11); --fg: #484b64; --fg-muted: #64677c; --fg-faint: #656873; --border: #ccd0da; --border-strong: #83858e; --accent: #8839ef; --accent-fg: #ffffff; --accent-soft: #d9d0ef; --accent-soft-fg: #7632d0; --danger: #d00f38; --danger-soft: rgba(208, 15, 56, 0.15); --warn: #b24608; --warn-soft: rgba(178, 70, 8, 0.15); --success: #307820; --success-soft: rgba(48, 120, 32, 0.15); --link: #1c5fe4; --unread-bg: #ffffff; --read-bg: #e1e4eb; --selected-bg: #d9d0ef; --focus-ring: 0 0 0 3px rgba(136, 57, 239, 0.4); --star: #b97618; --q1: #1c5fe4; --q2: #307820; --q3: #9a4e86; --scrollbar: rgba(100, 103, 124, 0.35); color-scheme: light; } /* catppuccin (dark) lifted for contrast: faint #7d829a -> #81869d (4.32 -> 4.55); border-strong #595c71 -> #66697c (2.49 -> 3.03) */ :root[data-theme="dark"][data-palette="catppuccin"] { --bg: #1e1e2e; --bg-elev: #313244; --bg-sunken: #181825; --bg-hover: rgba(205, 214, 244, 0.06); --bg-active: rgba(205, 214, 244, 0.11); --fg: #cdd6f4; --fg-muted: #a6adc8; --fg-faint: #81869d; --border: #45475a; --border-strong: #66697c; --accent: #cba6f7; --accent-fg: #1e1e2e; --accent-soft: rgba(203, 166, 247, 0.16); --accent-soft-fg: #cba6f7; --danger: #f38ba8; --danger-soft: rgba(243, 139, 168, 0.15); --warn: #fab387; --warn-soft: rgba(250, 179, 135, 0.15); --success: #a6e3a1; --success-soft: rgba(166, 227, 161, 0.15); --link: #89b4fa; --unread-bg: #313244; --read-bg: #181825; --selected-bg: rgba(203, 166, 247, 0.18); --focus-ring: 0 0 0 3px rgba(203, 166, 247, 0.4); --star: #f9e2af; --q1: #89b4fa; --q2: #a6e3a1; --q3: #f5c2e7; --scrollbar: rgba(166, 173, 200, 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); } /* solarized (light) lifted for contrast: fg #657b83 -> #47565c (4.13 -> 7.07); muted #93a1a1 -> #687272 (2.48 -> 4.59); faint #959a94 -> #6e726e (2.66 -> 4.53); link #2aa198 -> #217e77 (2.93 -> 4.51); danger #dc322f -> #d5302e (4.29 -> 4.54); warn #cb4b16 -> #c34815 (4.27 -> 4.57); success #859900 -> #687700 (2.97 -> 4.61); star #b58900 -> #b38800 (2.98 -> 3.02); border-strong #cecab8 -> #928f83 (1.53 -> 3.00); q1 #2aa198 -> #217e77 (2.93 -> 4.51); q2 #859900 -> #687700 (2.97 -> 4.61); q3 #6c71c4 -> #666ab8 (4.06 -> 4.51) */ :root[data-palette="solarized"] { --bg: #fdf6e3; --bg-elev: #fffdf6; --bg-sunken: #eee8d5; --bg-hover: rgba(71, 86, 92, 0.06); --bg-active: rgba(71, 86, 92, 0.11); --fg: #47565c; --fg-muted: #687272; --fg-faint: #6e726e; --border: #e6dfc8; --border-strong: #928f83; --accent: #268bd2; --accent-fg: #ffffff; --accent-soft: #dfe7e1; --accent-soft-fg: #1d6ba2; --danger: #d5302e; --danger-soft: rgba(213, 48, 46, 0.15); --warn: #c34815; --warn-soft: rgba(195, 72, 21, 0.15); --success: #687700; --success-soft: rgba(104, 119, 0, 0.15); --link: #217e77; --unread-bg: #ffffff; --read-bg: #f8f1df; --selected-bg: #dfe7e1; --focus-ring: 0 0 0 3px rgba(38, 139, 210, 0.4); --star: #b38800; --q1: #217e77; --q2: #687700; --q3: #666ab8; --scrollbar: rgba(104, 114, 114, 0.35); color-scheme: light; } /* solarized (dark) lifted for contrast: fg #839496 -> #a8b4b6 (4.75 -> 7.06); muted #586e75 -> #809196 (2.79 -> 4.58); faint #5a7279 -> #7d9095 (2.94 -> 4.50); danger #dc322f -> #e56563 (3.25 -> 4.56); warn #cb4b16 -> #d67147 (3.26 -> 4.51); border-strong #245661 -> #4e767f (1.84 -> 3.02); q3 #6c71c4 -> #8488cd (3.43 -> 4.56) */ :root[data-theme="dark"][data-palette="solarized"] { --bg: #002b36; --bg-elev: #073642; --bg-sunken: #001f28; --bg-hover: rgba(168, 180, 182, 0.06); --bg-active: rgba(168, 180, 182, 0.11); --fg: #a8b4b6; --fg-muted: #809196; --fg-faint: #7d9095; --border: #0d4552; --border-strong: #4e767f; --accent: #268bd2; --accent-fg: #002b36; --accent-soft: rgba(38, 139, 210, 0.16); --accent-soft-fg: #56a5dc; --danger: #e56563; --danger-soft: rgba(229, 101, 99, 0.15); --warn: #d67147; --warn-soft: rgba(214, 113, 71, 0.15); --success: #859900; --success-soft: rgba(133, 153, 0, 0.15); --link: #2aa198; --unread-bg: #073642; --read-bg: #001f28; --selected-bg: rgba(38, 139, 210, 0.18); --focus-ring: 0 0 0 3px rgba(38, 139, 210, 0.4); --star: #b58900; --q1: #2aa198; --q2: #859900; --q3: #8488cd; --scrollbar: rgba(128, 145, 150, 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); } /* ayu (light) lifted for contrast: fg #5c6166 -> #52565b (5.93 -> 7.01); muted #828e9f -> #697381 (3.15 -> 4.56); faint #949ba5 -> #6e737a (2.66 -> 4.53); link #22a4e6 -> #1979aa (2.65 -> 4.57); danger #f07171 -> #b45555 (2.73 -> 4.56); warn #fa8532 -> #af5d23 (2.35 -> 4.51); success #86b300 -> #5e7d00 (2.35 -> 4.52); accent #f29718 -> #cb7f14 (2.16 -> 3.03); star #eba400 -> #be8500 (2.02 -> 3.04); border-strong #cacdd0 -> #8d9092 (1.51 -> 3.05); q1 #55b4d4 -> #3a7a90 (2.25 -> 4.56); q2 #86b300 -> #5e7d00 (2.35 -> 4.52); q3 #a37acc -> #8664a7 (3.21 -> 4.53) */ :root[data-palette="ayu"] { --bg: #f8f9fa; --bg-elev: #fcfcfc; --bg-sunken: #ebeef0; --bg-hover: rgba(82, 86, 91, 0.06); --bg-active: rgba(82, 86, 91, 0.11); --fg: #52565b; --fg-muted: #697381; --fg-faint: #6e737a; --border: #dfe2e5; --border-strong: #8d9092; --accent: #cb7f14; --accent-fg: #ffffff; --accent-soft: #f2e8da; --accent-soft-fg: #945d0f; --danger: #b45555; --danger-soft: rgba(180, 85, 85, 0.15); --warn: #af5d23; --warn-soft: rgba(175, 93, 35, 0.15); --success: #5e7d00; --success-soft: rgba(94, 125, 0, 0.15); --link: #1979aa; --unread-bg: #ffffff; --read-bg: #f3f4f5; --selected-bg: #f2e8da; --focus-ring: 0 0 0 3px rgba(203, 127, 20, 0.4); --star: #be8500; --q1: #3a7a90; --q2: #5e7d00; --q3: #8664a7; --scrollbar: rgba(105, 115, 129, 0.35); color-scheme: light; } /* ayu (dark) lifted for contrast: muted #5a6378 -> #747c8e (3.16 -> 4.55); faint #555c6a -> #777d88 (2.83 -> 4.60); border-strong #34373e -> #5f6167 (1.60 -> 3.07) */ :root[data-theme="dark"][data-palette="ayu"] { --bg: #0d1017; --bg-elev: #10141c; --bg-sunken: #070a0f; --bg-hover: rgba(191, 189, 182, 0.06); --bg-active: rgba(191, 189, 182, 0.11); --fg: #bfbdb6; --fg-muted: #747c8e; --fg-faint: #777d88; --border: #1b1f29; --border-strong: #5f6167; --accent: #e6b450; --accent-fg: #0d1017; --accent-soft: rgba(230, 180, 80, 0.16); --accent-soft-fg: #e6b450; --danger: #f07178; --danger-soft: rgba(240, 113, 120, 0.15); --warn: #ff8f40; --warn-soft: rgba(255, 143, 64, 0.15); --success: #aad94c; --success-soft: rgba(170, 217, 76, 0.15); --link: #59c2ff; --unread-bg: #10141c; --read-bg: #070a0f; --selected-bg: rgba(230, 180, 80, 0.18); --focus-ring: 0 0 0 3px rgba(230, 180, 80, 0.4); --star: #ffb454; --q1: #39bae6; --q2: #aad94c; --q3: #d2a6ff; --scrollbar: rgba(116, 124, 142, 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); } /* kanagawa (light) lifted for contrast: fg #545464 -> #434350 (5.41 -> 7.09); muted #716e61 -> #636155 (3.73 -> 4.54); faint #8a8670 -> #636051 (2.67 -> 4.61); link #4d699b -> #47618f (4.02 -> 4.53); danger #c84053 -> #ac3747 (3.55 -> 4.51); warn #cc6d00 -> #934e00 (2.66 -> 4.59); success #6f894e -> #53673b (2.85 -> 4.54); border-strong #bfb997 -> #807c65 (1.44 -> 3.07); q1 #4d699b -> #47618f (4.02 -> 4.53); q2 #6f894e -> #53673b (2.85 -> 4.54); q3 #b35b79 -> #914a62 (3.26 -> 4.55) */ :root[data-palette="kanagawa"] { --bg: #e5ddb0; --bg-elev: #f2ecbc; --bg-sunken: #dcd5ac; --bg-hover: rgba(67, 67, 80, 0.06); --bg-active: rgba(67, 67, 80, 0.11); --fg: #434350; --fg-muted: #636155; --fg-faint: #636051; --border: #d5cea3; --border-strong: #807c65; --accent: #624c83; --accent-fg: #ffffff; --accent-soft: #d3c9aa; --accent-soft-fg: #604a80; --danger: #ac3747; --danger-soft: rgba(172, 55, 71, 0.15); --warn: #934e00; --warn-soft: rgba(147, 78, 0, 0.15); --success: #53673b; --success-soft: rgba(83, 103, 59, 0.15); --link: #47618f; --unread-bg: #ffffff; --read-bg: #e0d8ad; --selected-bg: #d3c9aa; --focus-ring: 0 0 0 3px rgba(98, 76, 131, 0.4); --star: #77713f; --q1: #47618f; --q2: #53673b; --q3: #914a62; --scrollbar: rgba(99, 97, 85, 0.35); color-scheme: light; } /* kanagawa (dark) lifted for contrast: muted #727169 -> #898881 (3.33 -> 4.59); faint #696866 -> #888886 (2.94 -> 4.60); danger #e82424 -> #ed5050 (3.66 -> 4.56); border-strong #4f4e57 -> #696970 (1.99 -> 3.00) */ :root[data-theme="dark"][data-palette="kanagawa"] { --bg: #1f1f28; --bg-elev: #2a2a37; --bg-sunken: #16161d; --bg-hover: rgba(220, 215, 186, 0.06); --bg-active: rgba(220, 215, 186, 0.11); --fg: #dcd7ba; --fg-muted: #898881; --fg-faint: #888886; --border: #363646; --border-strong: #696970; --accent: #7e9cd8; --accent-fg: #1f1f28; --accent-soft: rgba(126, 156, 216, 0.16); --accent-soft-fg: #7e9cd8; --danger: #ed5050; --danger-soft: rgba(237, 80, 80, 0.15); --warn: #ff9e3b; --warn-soft: rgba(255, 158, 59, 0.15); --success: #98bb6c; --success-soft: rgba(152, 187, 108, 0.15); --link: #7fb4ca; --unread-bg: #2a2a37; --read-bg: #16161d; --selected-bg: rgba(126, 156, 216, 0.18); --focus-ring: 0 0 0 3px rgba(126, 156, 216, 0.4); --star: #e6c384; --q1: #7fb4ca; --q2: #98bb6c; --q3: #d27e99; --scrollbar: rgba(137, 136, 129, 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); } /* everforest (light) lifted for contrast: fg #5c6a72 -> #444e54 (4.66 -> 7.11); muted #939f91 -> #646c63 (2.30 -> 4.53); faint #8e9285 -> #686b61 (2.65 -> 4.53); link #3a94c5 -> #2c7096 (2.81 -> 4.53); danger #f85552 -> #b83f3d (2.73 -> 4.59); warn #f57d26 -> #a45419 (2.23 -> 4.53); success #8da101 -> #616f01 (2.42 -> 4.63); accent #8da101 -> #7d8f01 (2.42 -> 3.02); star #dfa000 -> #b07e00 (1.91 -> 3.00); border-strong #abb1a1 -> #84887c (1.84 -> 3.02); q1 #3a94c5 -> #2c7096 (2.81 -> 4.53); q2 #8da101 -> #616f01 (2.42 -> 4.63); q3 #df69ba -> #9e4b84 (2.55 -> 4.61) */ :root[data-palette="everforest"] { --bg: #efebd4; --bg-elev: #fdf6e3; --bg-sunken: #e6e2cc; --bg-hover: rgba(68, 78, 84, 0.06); --bg-active: rgba(68, 78, 84, 0.11); --fg: #444e54; --fg-muted: #646c63; --fg-faint: #686b61; --border: #bdc3af; --border-strong: #84887c; --accent: #7d8f01; --accent-fg: #ffffff; --accent-soft: #dfdeb6; --accent-soft-fg: #5a6701; --danger: #b83f3d; --danger-soft: rgba(184, 63, 61, 0.15); --warn: #a45419; --warn-soft: rgba(164, 84, 25, 0.15); --success: #616f01; --success-soft: rgba(97, 111, 1, 0.15); --link: #2c7096; --unread-bg: #ffffff; --read-bg: #eae6d0; --selected-bg: #dfdeb6; --focus-ring: 0 0 0 3px rgba(125, 143, 1, 0.4); --star: #b07e00; --q1: #2c7096; --q2: #616f01; --q3: #9e4b84; --scrollbar: rgba(100, 108, 99, 0.35); color-scheme: light; } /* everforest (dark) lifted for contrast: muted #859289 -> #949f97 (3.84 -> 4.55); faint #757f7b -> #969e9b (3.02 -> 4.55); border-strong #5c6364 -> #787e7e (2.03 -> 3.02) */ :root[data-theme="dark"][data-palette="everforest"] { --bg: #2d353b; --bg-elev: #343f44; --bg-sunken: #232a2e; --bg-hover: rgba(211, 198, 170, 0.06); --bg-active: rgba(211, 198, 170, 0.11); --fg: #d3c6aa; --fg-muted: #949f97; --fg-faint: #969e9b; --border: #475258; --border-strong: #787e7e; --accent: #a7c080; --accent-fg: #2d353b; --accent-soft: rgba(167, 192, 128, 0.16); --accent-soft-fg: #a7c080; --danger: #e67e80; --danger-soft: rgba(230, 126, 128, 0.15); --warn: #e69875; --warn-soft: rgba(230, 152, 117, 0.15); --success: #a7c080; --success-soft: rgba(167, 192, 128, 0.15); --link: #7fbbb3; --unread-bg: #343f44; --read-bg: #232a2e; --selected-bg: rgba(167, 192, 128, 0.18); --focus-ring: 0 0 0 3px rgba(167, 192, 128, 0.4); --star: #dbbc7f; --q1: #7fbbb3; --q2: #a7c080; --q3: #d699b6; --scrollbar: rgba(148, 159, 151, 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); } /* primer (light) lifted for contrast: faint #889098 -> #6b7278 (3.04 -> 4.58); star #bf8700 -> #bd8600 (2.95 -> 3.00); border-strong #b7bfc5 -> #898f94 (1.75 -> 3.07) */ :root[data-palette="primer"] { --bg: #f6f8fa; --bg-elev: #ffffff; --bg-sunken: #eff2f5; --bg-hover: rgba(37, 41, 46, 0.06); --bg-active: rgba(37, 41, 46, 0.11); --fg: #25292e; --fg-muted: #59636e; --fg-faint: #6b7278; --border: #d1d9e0; --border-strong: #898f94; --accent: #0969da; --accent-fg: #ffffff; --accent-soft: #d5e4f6; --accent-soft-fg: #0861c9; --danger: #cf222e; --danger-soft: rgba(207, 34, 46, 0.15); --warn: #9a6700; --warn-soft: rgba(154, 103, 0, 0.15); --success: #1a7f37; --success-soft: rgba(26, 127, 55, 0.15); --link: #0550ae; --unread-bg: #ffffff; --read-bg: #f0f2f4; --selected-bg: #d5e4f6; --focus-ring: 0 0 0 3px rgba(9, 105, 218, 0.4); --star: #bd8600; --q1: #0550ae; --q2: #116329; --q3: #8250df; --scrollbar: rgba(89, 99, 110, 0.35); color-scheme: light; } /* primer (dark) lifted for contrast: faint #697078 -> #767d84 (3.78 -> 4.54); border-strong #585f67 -> #5a6169 (2.93 -> 3.02) */ :root[data-theme="dark"][data-palette="primer"] { --bg: #0d1117; --bg-elev: #151b23; --bg-sunken: #010409; --bg-hover: rgba(240, 246, 252, 0.06); --bg-active: rgba(240, 246, 252, 0.11); --fg: #f0f6fc; --fg-muted: #9198a1; --fg-faint: #767d84; --border: #3d444d; --border-strong: #5a6169; --accent: #58a6ff; --accent-fg: #0d1117; --accent-soft: rgba(88, 166, 255, 0.16); --accent-soft-fg: #58a6ff; --danger: #ff7b72; --danger-soft: rgba(255, 123, 114, 0.15); --warn: #e3b341; --warn-soft: rgba(227, 179, 65, 0.15); --success: #3fb950; --success-soft: rgba(63, 185, 80, 0.15); --link: #79c0ff; --unread-bg: #151b23; --read-bg: #010409; --selected-bg: rgba(88, 166, 255, 0.18); --focus-ring: 0 0 0 3px rgba(88, 166, 255, 0.4); --star: #d29922; --q1: #79c0ff; --q2: #56d364; --q3: #d2a8ff; --scrollbar: rgba(145, 152, 161, 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); } /* A switch that depends on another one above it. Without this it reads as an ordinary "off", and clicking it does nothing with no explanation. */ .switch:disabled { opacity: .45; cursor: not-allowed; } .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 ---------------------------------------------------------------- */ /* The stacking order everything below is a point on, low to high: 700 mobile tab bar, FAB 940 drawer backdrop 950 mobile navigation drawer 955 composer dock 960 dialog backdrop 1000 popover 2000 tooltip 3000 toast 5000 drag ghost The drawer is where a phone reaches everything, so it is also where a phone starts a folder, an event, a contact and a message -- and each of those answers with a dialog or a full-screen composer that the drawer then covered, because both were stacked below it. A modal has to outrank the navigation that opened it, or the drawer sits on top of the thing it just asked for and the only way out is a press on the dimmed strip beside it. Above the composer, not level with it: a dialog raised from inside a composer -- the discard guard, the attachment picker -- has to clear the composer too. */ .dialog-backdrop { position: fixed; inset: 0; z-index: 960; 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; } /* Destructive, the way .menu-item.danger is: a red label on the ordinary surface. A filled red button in a list of answers is the loudest thing in the dialog, which is wrong when it is the answer that loses your work. */ .dialog-choice.danger { color: var(--danger); } /* 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 ========================================================================== */ /* Above the mobile drawer, below a dialog -- see the stack by `.dialog-backdrop`. */ .composer-dock { position: fixed; right: 16px; bottom: 0; display: flex; align-items: flex-end; gap: 12px; z-index: 955; 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; } /* Full screen is still a child of the dock, so without a layer of its own the positioned parts of any composer after it in the DOM (its recipients row, its editor) would paint straight over it. The others are hidden while one is full screen: they cannot be reached anyway, and the strip left under the 24px inset would otherwise show their footers. */ .composer.maximized { position: fixed; inset: 24px; z-index: 1; width: auto; height: auto; max-width: none; max-height: none; border-radius: var(--radius-lg); border-bottom: 1px solid var(--border); } .composer-dock.has-maximized > .composer:not(.maximized) { display: none; } .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