@import "tailwindcss"; @import "./tom-select.css"; @import "./motion.css"; @variant dark (&:where(.dark, .dark *)); /* ═══ Palette channels + runtime knobs ══════════════════════════════════════ Unlayered on purpose: these are variable definitions, not utilities, and unlayered declarations outrank Tailwind's cascade layers. Channels are stored as space-separated RGB triplets so alpha stays composable via rgb(var(--x) / ). EVERY PALETTE BLOCK BELOW DECLARES EVERY VARIABLE IN THE INVENTORY. That is a rule, not a style preference, and tests/Service/Appearance/ThemeVariableCompletenessTest fails the build when a block drifts from it. The reason is switching. A theme is chosen by rewriting `data-theme` on , so the old theme's block simply stops matching — which means anything it defined and the new one does not falls back to whatever :root or .dark happens to say. When nord defined four channels and inherited the other thirty-four, "nord" was not a palette, it was a diff against a light-tuned base: its --rgb-info was the light theme's link blue and its --rgb-ink-soft was a neutral grey darker than its own --rgb-ink-muted. Neither was chosen; both were left over. A block that spells out the whole palette cannot leave anything over, and reading one tells you what the theme looks like without tracing three levels of cascade to find out. The knobs at the bottom of :root are deliberately NOT part of the inventory: they belong to the user, not to the theme. AppearanceRenderer writes them inline on from the stored settings, an inline style beats every block here, and a theme that redeclared them would be overruled anyway. --rgb-main and --main-alpha are var() indirections precisely so they keep following the theme's surface without being restated by it. ═══════════════════════════════════════════════════════════════════════════ */ /* ── Light — the base palette, and what `light` and a light `system` paint in. There is no [data-theme="light"] block: :root already is that block. A copy of it under a selector would be a second place to forget. ─────────────── */ :root { /* What the BROWSER should render its own UI in. CSS cannot reach inside a native select's popup, a scrollbar or a date picker — color-scheme is the one channel that can, and without it every popup is OS-light over a dark theme. Declared per theme block, because it has to follow the theme and not the OS. */ color-scheme: light; /* Surfaces */ --rgb-surface: 255 255 255; --rgb-border: 255 255 255; --rgb-line: 0 0 0; --rgb-raised: 0 0 0; --rgb-sunken: 243 244 246; /* gray-100 */ --rgb-shadow: 0 0 0; /* Ink */ --rgb-ink: 39 39 42; --rgb-ink-soft: 55 65 81; /* gray-700 */ --rgb-ink-muted: 95 95 102; --rgb-ink-faint: 116 116 122; /* Accent. The stored setting overrules this from an inline style on ; the value here is what the theme asks for, and Theme::defaults() seeds the setting with the same colour when the theme is picked. */ --rgb-accent: 37 99 235; --rgb-accent-ink: 255 255 255; /* Fields */ --rgb-field: 255 255 255; --rgb-field-border: 212 212 216; /* Status */ --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 37 99 235; /* Inverse — the tooltip bubble and anything else that flips the page. */ --rgb-inverse: 31 41 55; /* gray-800 */ --rgb-inverse-ink: 255 255 255; /* Alphas */ --border-alpha: 0.6; --line-alpha: 0.06; --raised-alpha: 0.04; --hover-alpha: 0.03; --shadow-alpha: 0.05; --sunken-alpha: 1; /* The tint behind an unread row, as it has always been: white light over the surface, heavy on a light palette and barely there on a dark one. It was `bg-white/40 dark:bg-white/[0.03]` inline on the thread row until the emphasis setting needed something it could scale. The user's multiplier is --unread-emphasis and lives in :root. */ --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; /* Page */ --app-bg: linear-gradient(to bottom right, #eff6ff, #f1f5f9, #e0e7ff); /* The sheet a message body is rendered on — see @utility mail-sheet. Every theme carries its own, and every theme's is light: an email's HTML was authored against a light background, senders set dark text and assume white behind it, so rendering it on a near-black surface is both harsh to read and a misrepresentation of what was sent. Light is the constraint; which light is the theme's own business, and a theme whose sheet is picked from its own palette reads as part of the app rather than as a browser window someone pasted in. For Light that means its own white and its own zinc ink ramp: the warm cream is Paper's palette, and wearing it here made the reading pane look like another theme had bled through. */ --rgb-sheet: 255 255 255; --rgb-sheet-ink: 39 39 42; --rgb-sheet-ink-soft: 55 65 81; --rgb-sheet-ink-muted: 95 95 102; --rgb-sheet-ink-faint: 116 116 122; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; /* ── User knobs — NOT part of the theme inventory ──────────────────── Overwritten inline on by AppearanceRenderer. */ --rgb-main: var(--rgb-surface); --main-alpha: var(--pane-alpha); --pane-alpha: 1; /* The floating surfaces' own opacity. Its own knob because a popover is read against whatever it covers, and the two translucencies multiply — see @utility pane-flat and @utility popover below, which is where the two groups are actually divided. */ --popover-alpha: 1; --pane-blur: 0px; --app-radius: 0.75rem; --density-row-y: 0.5rem; --density-gap: 0.75rem; --scrim-alpha: 0; /* Typography. The scale multiplies the 16px root below; the family is the INTERFACE's, never the compose editor's — see the pin further down. */ --app-font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --app-font-scale: 1; /* Mail list display. Every default here is what the list did before the settings existed, so these values are also the answer for a reader with no account at all (the login screen, a shared calendar). */ --list-corner-display: block; --list-avatar-hide: 0; --list-preview-display: block; --list-preview-display-wide: block; --list-preview-lines: 1; --list-preview-wrap: nowrap; /* How loudly an unread row is tinted. --unread-tint-alpha is the THEME's base (it is in the palette inventory below, declared by every block); the multiplier and the bar are the USER's, written inline by AppearanceRenderer from the emphasis setting. */ --unread-emphasis: 1; --unread-bar-w: 0px; /* Per-surface density. AppearanceRenderer resolves each surface to a concrete pair — a surface with no override of its own is emitted carrying the global density's values, so these defaults only ever serve a page rendered for nobody. */ --surface-sidebar-row-y: 0.5rem; --surface-sidebar-tree-y: 0.375rem; --surface-sidebar-row-gap: 0.125rem; --surface-sidebar-section-y: 1rem; --surface-sidebar-gap: 0.75rem; --surface-list-row-y: 0.625rem; --surface-list-gap: 0.75rem; --surface-reading-row-y: 1rem; --surface-reading-gap: 0.75rem; /* One height for every pane's header row. The mail toolbar and the calendar toolbar sit side by side once the pane is docked, so two independently-derived heights are visibly two heights. */ --pane-header-h: 2.75rem; } /* ── The dark base — what a `system` theme paints in when the OS is dark. Also the class Tailwind's `dark:` variant keys off, so it stays on for every dark theme even though each of those now states its own palette in full and inherits nothing from here. ──────────────────────────────── */ .dark { color-scheme: dark; --rgb-surface: 17 24 39; --rgb-border: 255 255 255; --rgb-line: 255 255 255; --rgb-raised: 255 255 255; --rgb-sunken: 255 255 255; --rgb-shadow: 0 0 0; --rgb-ink: 244 244 245; --rgb-ink-soft: 209 213 219; /* gray-300 */ --rgb-ink-muted: 161 161 170; --rgb-ink-faint: 129 129 137; --rgb-accent: 37 99 235; --rgb-accent-ink: 255 255 255; --rgb-field: 39 39 42; --rgb-field-border:113 113 122; --rgb-danger: 248 113 113; --rgb-warning: 251 191 36; --rgb-success: 74 222 128; /* blue-400, to sit with the 400-weight red, amber and green above. This used to fall through to the light palette's 37 99 235 — a link blue tuned for white, on #111827, while its three siblings had all been lightened for exactly this surface. */ --rgb-info: 96 165 250; --rgb-inverse: 243 244 246; /* gray-100 */ --rgb-inverse-ink: 17 24 39; --border-alpha: 0.1; --line-alpha: 0.1; --raised-alpha: 0.06; --hover-alpha: 0.04; --shadow-alpha: 0.3; --sunken-alpha: 0.06; /* The tint behind an unread row, as it has always been: white light over the surface, heavy on a light palette and barely there on a dark one. It was `bg-white/40 dark:bg-white/[0.03]` inline on the thread row until the emphasis setting needed something it could scale. The user's multiplier is --unread-emphasis and lives in :root. */ --unread-tint-alpha: 0.03; --field-alpha: 0.4; --field-border-alpha: 0.3; --danger-soft-alpha: 0.12; --info-soft-alpha: 0.12; --app-bg: linear-gradient(to bottom right, #020617, #0f172a, #1e1b4b); /* The sheet stays light — see :root — but it is THIS theme's light: the slate the gradient above is made of, at its palest, not Paper's cream. */ --rgb-sheet: 248 250 252; /* slate-50 */ --rgb-sheet-ink: 15 23 42; /* slate-900 */ --rgb-sheet-ink-soft: 51 65 85; /* slate-700 */ --rgb-sheet-ink-muted: 81 94 113; /* slate-500 */ --rgb-sheet-ink-faint: 104 114 129; /* slate-400 */ --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* Theme blocks come AFTER .dark so they win at equal specificity. */ /* Paper — warm, low-contrast, no gradient. The point of this one is that the page is a single flat sheet: the app background and the pane surface are the same cream, so the panes stop reading as cards floating over a photo and the list becomes the whole surface. Ink is a soft near-black rather than #000, and the accent is a muted clay — a saturated blue on cream reads as a link, not as furniture. Everything that separates one thing from another here is the ink colour at a low alpha, including the shadow: a neutral-black drop shadow under a warm cream card is the one grey thing on the screen. */ :root[data-theme="paper"] { color-scheme: light; --rgb-surface: 247 245 239; --rgb-border: 35 34 32; --rgb-line: 35 34 32; --rgb-raised: 35 34 32; --rgb-sunken: 35 34 32; --rgb-shadow: 35 34 32; --rgb-ink: 35 34 32; --rgb-ink-soft: 82 78 72; --rgb-ink-muted: 95 91 84; --rgb-ink-faint: 116 111 103; --rgb-accent: 125 107 79; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; /* Same as solar, and for the same reason — see the note there. Paper's surface is 247 245 239, so a white field at 0.6 alpha is within about three percent of the topbar it sits on. The border tone here is warm to match the paper family rather than the cool grey it had. */ --rgb-field-border: 168 162 150; --field-border-alpha: 0.75; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 90 106 130; --rgb-inverse: 45 43 40; --rgb-inverse-ink: 247 245 239; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; /* The tint behind an unread row, as it has always been: white light over the surface, heavy on a light palette and barely there on a dark one. It was `bg-white/40 dark:bg-white/[0.03]` inline on the thread row until the emphasis setting needed something it could scale. The user's multiplier is --unread-emphasis and lives in :root. */ --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; /* A flat colour still has to be written as a gradient: .app-bg paints through background-image, and `background-image: #f2efe7` is not a valid image — it is dropped, the element paints nothing, and the page falls back to the browser's white. Same idiom BackgroundResolver uses for a solid background. */ --app-bg: linear-gradient(#f2efe7, #f2efe7); /* The theme's own surface, so the reading pane is continuous with the app exactly the way the rest of Paper is. */ --rgb-sheet: 247 245 239; --rgb-sheet-ink: 35 34 32; --rgb-sheet-ink-soft: 82 78 72; --rgb-sheet-ink-muted: 95 91 84; --rgb-sheet-ink-faint: 116 111 103; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* Dark — a dim room, not a black one. The first attempt at this was near-black panes on a blacker page with near-white text, and it was harsh in two separate ways. Text at #eae7e2 on #0a0a0a is roughly the contrast of a headlight; and lifting only the panes out of the page made the mail list the one bright rectangle on screen with everything around it falling away. So: the floor comes up off pure black, the step between page and pane is small enough to read as one continuous material rather than a card on a void, and the ink stops well short of white. Panes are separated by their hairlines instead — which is what Paper does too, and it is the same idea in reverse. Warm rather than neutral grey throughout, because a pure grey ramp on black is what makes a dark theme look like a terminal. The accent is a FILL here, not just a tint: Compose, the active nav row and every primary button paint it behind light text, so it has to be deep enough to carry them — a pale sage, which is what the reference uses for 40px avatar circles, becomes a washed-out slab nothing can be read on. */ :root[data-theme="dark"] { color-scheme: dark; --rgb-surface: 28 27 26; --rgb-border: 255 255 255; --rgb-line: 255 255 255; --rgb-raised: 255 255 255; --rgb-sunken: 255 255 255; --rgb-shadow: 0 0 0; --rgb-ink: 214 209 202; --rgb-ink-soft: 186 181 174; --rgb-ink-muted: 159 155 149; --rgb-ink-faint: 134 131 126; --rgb-accent: 58 111 92; --rgb-accent-ink: 240 245 242; /* Fields lighten the surface instead of darkening it. .dark's field is #27272a, which is the same colour as this surface — the search input lost its edges entirely. */ --rgb-field: 255 255 255; --rgb-field-border: 255 255 255; /* Status colours want more light on a dark surface than the light defaults carry, but not so much that they glow — .dark's are tuned for #111827 and read as muddy here. */ --rgb-danger: 226 128 128; --rgb-warning: 219 175 95; --rgb-success: 118 192 150; --rgb-info: 130 166 204; --rgb-inverse: 222 218 211; --rgb-inverse-ink: 28 27 26; /* The hairlines do the separating, so they have to be seen. A border at .dark's 0.1 disappears against a surface this dark and the panes lose their outline entirely. */ --border-alpha: 0.12; --line-alpha: 0.09; --raised-alpha: 0.06; --hover-alpha: 0.05; --shadow-alpha: 0.45; --sunken-alpha: 0.07; /* The tint behind an unread row, as it has always been: white light over the surface, heavy on a light palette and barely there on a dark one. It was `bg-white/40 dark:bg-white/[0.03]` inline on the thread row until the emphasis setting needed something it could scale. The user's multiplier is --unread-emphasis and lives in :root. */ --unread-tint-alpha: 0.03; --field-alpha: 0.04; --field-border-alpha: 0.10; --danger-soft-alpha: 0.12; --info-soft-alpha: 0.12; /* Barely below the surface: enough that a pane has an edge, not so much that the list becomes the only lit thing in the window. */ --app-bg: linear-gradient(#151413, #151413); /* The same warm cream the rest of the theme is a negative of. */ /* This theme's own light — the palest of its slate, not Paper's cream. */ --rgb-sheet: 248 250 252; /* slate-50 */ --rgb-sheet-ink: 15 23 42; /* slate-900 */ --rgb-sheet-ink-soft: 51 65 85; /* slate-700 */ --rgb-sheet-ink-muted: 81 94 113; /* slate-500 */ --rgb-sheet-ink-faint: 104 114 129; /* slate-400 */ --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* Nord — polar night behind snow storm, with the frost blues doing the pointing. The ink ramp is Nord's own #eceff4 / #e5e9f0 / #d8dee9: it used to borrow the neutral grey base for --rgb-ink-soft, which was both colder than everything around it and DARKER than the --rgb-ink-muted below it, so the two steps ran backwards. */ :root[data-theme="nord"] { color-scheme: dark; --rgb-surface: 46 52 64; /* nord0 #2e3440 */ --rgb-border: 255 255 255; --rgb-line: 255 255 255; --rgb-raised: 255 255 255; --rgb-sunken: 255 255 255; --rgb-shadow: 0 0 0; --rgb-ink: 236 239 244; /* nord6 #eceff4 */ --rgb-ink-soft: 229 233 240; /* nord5 #e5e9f0 */ --rgb-ink-muted: 216 222 233; /* nord4 #d8dee9 */ --rgb-ink-faint: 143 158 179; --rgb-accent: 136 192 208; /* frost #88c0d0 */ --rgb-accent-ink: 24 24 27; /* the frost blue is light enough to need dark ink */ --rgb-field: 39 39 42; --rgb-field-border:113 113 122; --rgb-danger: 248 113 113; --rgb-warning: 251 191 36; --rgb-success: 74 222 128; --rgb-info: 129 161 193; /* frost #81a1c1 */ --rgb-inverse: 243 244 246; --rgb-inverse-ink: 17 24 39; --border-alpha: 0.1; --line-alpha: 0.1; --raised-alpha: 0.06; --hover-alpha: 0.04; --shadow-alpha: 0.3; --sunken-alpha: 0.06; /* The tint behind an unread row, as it has always been: white light over the surface, heavy on a light palette and barely there on a dark one. It was `bg-white/40 dark:bg-white/[0.03]` inline on the thread row until the emphasis setting needed something it could scale. The user's multiplier is --unread-emphasis and lives in :root. */ --unread-tint-alpha: 0.03; --field-alpha: 0.4; --field-border-alpha: 0.3; --danger-soft-alpha: 0.12; --info-soft-alpha: 0.12; --app-bg: linear-gradient(to bottom right, #2e3440, #3b4252, #434c5e); /* Snow storm, inverted onto polar night: the sheet is the theme's own lightest colour rather than a warm cream borrowed from elsewhere. */ --rgb-sheet: 236 239 244; --rgb-sheet-ink: 46 52 64; --rgb-sheet-ink-soft: 66 70 78; --rgb-sheet-ink-muted: 83 87 94; --rgb-sheet-ink-faint: 104 107 113; --rgb-sheet-link: 94 129 172; /* #5e81ac */ --rgb-sheet-danger: 191 97 106; /* #bf616a */ } /* Dusk — violet, and violet all the way down. The ink ramp had the same backwards step Nord's did, for the same reason: --rgb-ink-soft was a neutral grey inherited from the dark base while the ink either side of it was tinted. */ :root[data-theme="dusk"] { color-scheme: dark; --rgb-surface: 30 27 46; --rgb-border: 255 255 255; --rgb-line: 255 255 255; --rgb-raised: 255 255 255; --rgb-sunken: 255 255 255; --rgb-shadow: 0 0 0; --rgb-ink: 237 233 254; --rgb-ink-soft: 216 210 250; --rgb-ink-muted: 196 181 253; --rgb-ink-faint: 139 125 191; --rgb-accent: 167 139 250; --rgb-accent-ink: 255 255 255; --rgb-field: 39 39 42; --rgb-field-border:113 113 122; --rgb-danger: 248 113 113; --rgb-warning: 251 191 36; --rgb-success: 74 222 128; --rgb-info: 165 180 252; --rgb-inverse: 243 244 246; --rgb-inverse-ink: 17 24 39; --border-alpha: 0.1; --line-alpha: 0.1; --raised-alpha: 0.06; --hover-alpha: 0.04; --shadow-alpha: 0.3; --sunken-alpha: 0.06; /* The tint behind an unread row, as it has always been: white light over the surface, heavy on a light palette and barely there on a dark one. It was `bg-white/40 dark:bg-white/[0.03]` inline on the thread row until the emphasis setting needed something it could scale. The user's multiplier is --unread-emphasis and lives in :root. */ --unread-tint-alpha: 0.03; --field-alpha: 0.4; --field-border-alpha: 0.3; --danger-soft-alpha: 0.12; --info-soft-alpha: 0.12; --app-bg: linear-gradient(to bottom right, #14101f, #1e1b2e, #2d1b4e); --rgb-sheet: 245 243 255; --rgb-sheet-ink: 30 27 46; --rgb-sheet-ink-soft: 77 75 92; --rgb-sheet-ink-muted: 91 88 103; --rgb-sheet-ink-faint: 111 109 121; --rgb-sheet-link: 109 40 217; --rgb-sheet-danger: 185 28 28; } /* Solarized light. The palette is a fixed sixteen colours chosen against each other, so the ones this used to inherit from the light base stood out as the wrong sixteen: a cool slate --rgb-ink-soft between two warm greys, a #f3f4f6 inset panel on a #fdf6e3 page, and the base link blue where Solarized has a blue of its own. */ :root[data-theme="solar"] { color-scheme: light; --rgb-surface: 253 246 227; /* base3 #fdf6e3 */ --rgb-border: 255 255 255; --rgb-line: 0 0 0; --rgb-raised: 0 0 0; --rgb-sunken: 238 232 213; /* base2 #eee8d5 */ --rgb-shadow: 0 0 0; /* Solarized light's own content tones (base01/base00/base1) sit inside a narrow band — roughly 2.5:1 to 5:1 on base3. That band cannot hold four tiers where even the FAINTEST must clear 4.5:1, so the ramp is extended downward past base02 rather than compressed into illegibility. The hue is solarized's; the range is not. */ --rgb-ink: 46 57 61; --rgb-ink-soft: 61 75 81; --rgb-ink-muted: 77 93 100; /* base00 #657b83, darkened */ --rgb-ink-faint: 104 114 114; --rgb-accent: 181 137 0; /* yellow #b58900 */ --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; /* The search field sits ON the topbar, and in the boxed layout the topbar is translucent (paneAlpha 0.7) over a warm off-white. A pure-white fill at --field-alpha 0.6 lands within a couple of percent of that, so the input had almost no edge: you could see where it was only by the placeholder text inside it. The fill is not the thing to change — a white field on cream is right, and darkening it would make the input look disabled. The border is what delineates a control whose fill legitimately matches its surround, and here it was a cool grey at 0.4 alpha, i.e. barely present. A darker tone from the theme's own family, at an alpha that survives the translucency. Flat is unaffected in practice: an opaque topbar already gives the fill something to sit against, and a firmer border does not hurt it. */ --rgb-field-border: 147 161 161; --field-border-alpha: 0.75; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 38 139 210; /* blue #268bd2 */ --rgb-inverse: 31 41 55; --rgb-inverse-ink: 255 255 255; --border-alpha: 0.6; --line-alpha: 0.06; --raised-alpha: 0.04; --hover-alpha: 0.03; --shadow-alpha: 0.05; --sunken-alpha: 1; /* The tint behind an unread row, as it has always been: white light over the surface, heavy on a light palette and barely there on a dark one. It was `bg-white/40 dark:bg-white/[0.03]` inline on the thread row until the emphasis setting needed something it could scale. The user's multiplier is --unread-emphasis and lives in :root. */ --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(to bottom right, #fdf6e3, #eee8d5, #f5e6c8); --rgb-sheet: 253 246 227; /* base3 */ --rgb-sheet-ink: 7 54 66; /* base02 #073642 */ --rgb-sheet-ink-soft: 61 96 101; --rgb-sheet-ink-muted: 74 96 98; --rgb-sheet-ink-faint: 102 115 112; --rgb-sheet-link: 38 139 210; /* blue #268bd2 */ --rgb-sheet-danger: 220 50 47; /* red #dc322f */ } /* ═══ Logo-colourway themes (generated) ═════════════════════════════════════ One theme per LogoStyle, each with a palette of its own rather than Paper plus a tinted accent. Seven are proper dark themes (ink, midnight-gold, charcoal-amber, espresso-cream, aurora, ember, twilight), built on the dark/dusk/nord model: full dark token sets, a light reading sheet in the theme's own hue. The other twenty-five are light papers whose background, surfaces and hairlines all carry the style's mood — the accent is a colour of the style (its tile or one of its strokes), with --rgb-accent-ink chosen by the same WCAG algorithm AppearanceRenderer and appearance_controller use, so the block agrees with the inline value both of them write. GENERATED — do not hand-edit these blocks. The script lives in the session scratchpad as generate-theme-blocks.mjs; it derives every ink ramp and verifies the contrast floors (body ink >= 7:1 and soft ink >= 7:1 against page and surface, muted >= 4.5:1, faint >= 3.6:1, accent-ink >= 4.5:1) before it will emit anything. Theme::swatch()'s logo arms and isDark() are emitted by the same script and must move with these blocks. ═══════════════════════════════════════════════════════════════════════ */ /* ink — a printed page, in negative: warm charcoal, paper-white ink, the paper tone itself as accent */ :root[data-theme="ink"] { color-scheme: dark; --rgb-surface: 33 30 26; --rgb-border: 255 255 255; --rgb-line: 255 255 255; --rgb-raised: 255 255 255; --rgb-sunken: 255 255 255; --rgb-shadow: 0 0 0; --rgb-ink: 232 226 217; --rgb-ink-soft: 184 179 171; --rgb-ink-muted: 152 148 141; --rgb-ink-faint: 141 136 129; --rgb-accent: 232 226 217; --rgb-accent-ink: 24 24 27; --rgb-field: 255 255 255; --rgb-field-border: 255 255 255; --rgb-danger: 226 128 128; --rgb-warning: 219 175 95; --rgb-success: 118 192 150; --rgb-info: 130 166 204; --rgb-inverse: 236 231 224; --rgb-inverse-ink: 33 30 26; --border-alpha: 0.12; --line-alpha: 0.09; --raised-alpha: 0.06; --hover-alpha: 0.05; --shadow-alpha: 0.45; --sunken-alpha: 0.07; --unread-tint-alpha: 0.03; --field-alpha: 0.04; --field-border-alpha: 0.10; --danger-soft-alpha: 0.12; --info-soft-alpha: 0.12; --app-bg: linear-gradient(#171512, #171512); --rgb-sheet: 242 239 231; --rgb-sheet-ink: 35 34 32; --rgb-sheet-ink-soft: 79 77 74; --rgb-sheet-ink-muted: 108 106 103; --rgb-sheet-ink-faint: 109 107 103; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* midnight-gold — a night library: deep slate-navy shelves, old gold picking things out */ :root[data-theme="midnight-gold"] { color-scheme: dark; --rgb-surface: 26 35 51; --rgb-border: 255 255 255; --rgb-line: 255 255 255; --rgb-raised: 255 255 255; --rgb-sunken: 255 255 255; --rgb-shadow: 0 0 0; --rgb-ink: 229 227 218; --rgb-ink-soft: 180 181 178; --rgb-ink-muted: 148 150 151; --rgb-ink-faint: 136 139 142; --rgb-accent: 217 119 6; --rgb-accent-ink: 24 24 27; --rgb-field: 255 255 255; --rgb-field-border: 255 255 255; --rgb-danger: 226 128 128; --rgb-warning: 219 175 95; --rgb-success: 118 192 150; --rgb-info: 148 163 184; --rgb-inverse: 234 232 225; --rgb-inverse-ink: 26 35 51; --border-alpha: 0.12; --line-alpha: 0.09; --raised-alpha: 0.06; --hover-alpha: 0.05; --shadow-alpha: 0.45; --sunken-alpha: 0.07; --unread-tint-alpha: 0.03; --field-alpha: 0.04; --field-border-alpha: 0.10; --danger-soft-alpha: 0.12; --info-soft-alpha: 0.12; --app-bg: linear-gradient(to bottom right, #0d1526, #111c30); --rgb-sheet: 246 242 230; --rgb-sheet-ink: 36 46 63; --rgb-sheet-ink-soft: 73 81 93; --rgb-sheet-ink-muted: 106 111 118; --rgb-sheet-ink-faint: 104 110 118; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* charcoal-amber — industrial: true charcoal, amber signal lights */ :root[data-theme="charcoal-amber"] { color-scheme: dark; --rgb-surface: 32 30 28; --rgb-border: 255 255 255; --rgb-line: 255 255 255; --rgb-raised: 255 255 255; --rgb-sunken: 255 255 255; --rgb-shadow: 0 0 0; --rgb-ink: 219 215 209; --rgb-ink-soft: 174 171 166; --rgb-ink-muted: 144 141 137; --rgb-ink-faint: 138 135 131; --rgb-accent: 245 158 11; --rgb-accent-ink: 24 24 27; --rgb-field: 255 255 255; --rgb-field-border: 255 255 255; --rgb-danger: 226 128 128; --rgb-warning: 219 175 95; --rgb-success: 118 192 150; --rgb-info: 130 166 204; --rgb-inverse: 225 222 217; --rgb-inverse-ink: 32 30 28; --border-alpha: 0.12; --line-alpha: 0.09; --raised-alpha: 0.06; --hover-alpha: 0.05; --shadow-alpha: 0.45; --sunken-alpha: 0.07; --unread-tint-alpha: 0.03; --field-alpha: 0.04; --field-border-alpha: 0.10; --danger-soft-alpha: 0.12; --info-soft-alpha: 0.12; --app-bg: linear-gradient(#141312, #141312); --rgb-sheet: 244 241 236; --rgb-sheet-ink: 41 37 36; --rgb-sheet-ink-soft: 84 80 78; --rgb-sheet-ink-muted: 112 109 107; --rgb-sheet-ink-faint: 112 108 106; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* espresso-cream — cozy: deep coffee brown, cream poured over it */ :root[data-theme="espresso-cream"] { color-scheme: dark; --rgb-surface: 37 27 19; --rgb-border: 255 255 255; --rgb-line: 255 255 255; --rgb-raised: 255 255 255; --rgb-sunken: 255 255 255; --rgb-shadow: 0 0 0; --rgb-ink: 232 220 196; --rgb-ink-soft: 185 174 154; --rgb-ink-muted: 154 143 125; --rgb-ink-faint: 143 132 114; --rgb-accent: 214 199 174; --rgb-accent-ink: 24 24 27; --rgb-field: 255 255 255; --rgb-field-border: 255 255 255; --rgb-danger: 226 128 128; --rgb-warning: 219 175 95; --rgb-success: 118 192 150; --rgb-info: 211 182 138; --rgb-inverse: 236 226 207; --rgb-inverse-ink: 37 27 19; --border-alpha: 0.12; --line-alpha: 0.09; --raised-alpha: 0.06; --hover-alpha: 0.05; --shadow-alpha: 0.45; --sunken-alpha: 0.07; --unread-tint-alpha: 0.03; --field-alpha: 0.04; --field-border-alpha: 0.10; --danger-soft-alpha: 0.12; --info-soft-alpha: 0.12; --app-bg: linear-gradient(#1a120c, #1a120c); --rgb-sheet: 245 236 220; --rgb-sheet-ink: 47 36 24; --rgb-sheet-ink-soft: 86 75 63; --rgb-sheet-ink-muted: 113 103 89; --rgb-sheet-ink-faint: 113 103 90; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* aurora — northern lights: blue-green night sky, a luminous green-teal ribbon */ :root[data-theme="aurora"] { color-scheme: dark; --rgb-surface: 15 32 39; --rgb-border: 255 255 255; --rgb-line: 255 255 255; --rgb-raised: 255 255 255; --rgb-sunken: 255 255 255; --rgb-shadow: 0 0 0; --rgb-ink: 216 232 230; --rgb-ink-soft: 167 185 186; --rgb-ink-muted: 135 154 157; --rgb-ink-faint: 119 139 144; --rgb-accent: 16 185 129; --rgb-accent-ink: 24 24 27; --rgb-field: 255 255 255; --rgb-field-border: 255 255 255; --rgb-danger: 226 128 128; --rgb-warning: 219 175 95; --rgb-success: 118 192 150; --rgb-info: 108 196 224; --rgb-inverse: 223 236 235; --rgb-inverse-ink: 15 32 39; --border-alpha: 0.12; --line-alpha: 0.09; --raised-alpha: 0.06; --hover-alpha: 0.05; --shadow-alpha: 0.45; --sunken-alpha: 0.07; --unread-tint-alpha: 0.03; --field-alpha: 0.04; --field-border-alpha: 0.10; --danger-soft-alpha: 0.12; --info-soft-alpha: 0.12; --app-bg: linear-gradient(to bottom right, #081418, #0a1b21, #0d2430); --rgb-sheet: 236 246 244; --rgb-sheet-ink: 18 48 54; --rgb-sheet-ink-soft: 61 87 92; --rgb-sheet-ink-muted: 90 114 117; --rgb-sheet-ink-faint: 91 115 119; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* ember — fireside: warm near-black, an ember orange-red glow */ :root[data-theme="ember"] { color-scheme: dark; --rgb-surface: 34 23 19; --rgb-border: 255 255 255; --rgb-line: 255 255 255; --rgb-raised: 255 255 255; --rgb-sunken: 255 255 255; --rgb-shadow: 0 0 0; --rgb-ink: 236 223 215; --rgb-ink-soft: 188 175 168; --rgb-ink-muted: 155 143 137; --rgb-ink-faint: 140 128 122; --rgb-accent: 234 88 12; --rgb-accent-ink: 24 24 27; --rgb-field: 255 255 255; --rgb-field-border: 255 255 255; --rgb-danger: 226 128 128; --rgb-warning: 219 175 95; --rgb-success: 118 192 150; --rgb-info: 130 166 204; --rgb-inverse: 239 229 222; --rgb-inverse-ink: 34 23 19; --border-alpha: 0.12; --line-alpha: 0.09; --raised-alpha: 0.06; --hover-alpha: 0.05; --shadow-alpha: 0.45; --sunken-alpha: 0.07; --unread-tint-alpha: 0.03; --field-alpha: 0.04; --field-border-alpha: 0.10; --danger-soft-alpha: 0.12; --info-soft-alpha: 0.12; --app-bg: linear-gradient(#150f0c, #150f0c); --rgb-sheet: 247 239 231; --rgb-sheet-ink: 51 36 28; --rgb-sheet-ink-soft: 90 76 68; --rgb-sheet-ink-muted: 103 89 81; --rgb-sheet-ink-faint: 119 107 99; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* twilight — dusk going indigo-violet, the last pink of the sky as accent */ :root[data-theme="twilight"] { color-scheme: dark; --rgb-surface: 32 27 56; --rgb-border: 255 255 255; --rgb-line: 255 255 255; --rgb-raised: 255 255 255; --rgb-sunken: 255 255 255; --rgb-shadow: 0 0 0; --rgb-ink: 233 228 247; --rgb-ink-soft: 185 180 201; --rgb-ink-muted: 153 148 171; --rgb-ink-faint: 137 132 156; --rgb-accent: 244 114 182; --rgb-accent-ink: 24 24 27; --rgb-field: 255 255 255; --rgb-field-border: 255 255 255; --rgb-danger: 226 128 128; --rgb-warning: 219 175 95; --rgb-success: 118 192 150; --rgb-info: 165 180 252; --rgb-inverse: 237 233 248; --rgb-inverse-ink: 32 27 56; --border-alpha: 0.12; --line-alpha: 0.09; --raised-alpha: 0.06; --hover-alpha: 0.05; --shadow-alpha: 0.45; --sunken-alpha: 0.07; --unread-tint-alpha: 0.03; --field-alpha: 0.04; --field-border-alpha: 0.10; --danger-soft-alpha: 0.12; --info-soft-alpha: 0.12; --app-bg: linear-gradient(to bottom right, #120e24, #191434, #241740); --rgb-sheet: 245 242 252; --rgb-sheet-ink: 38 32 71; --rgb-sheet-ink-soft: 82 76 109; --rgb-sheet-ink-muted: 111 106 135; --rgb-sheet-ink-faint: 112 107 136; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* product-blue — crisp product blue on a cool blue-white */ :root[data-theme="product-blue"] { color-scheme: light; --rgb-surface: 245 248 252; --rgb-border: 31 42 61; --rgb-line: 31 42 61; --rgb-raised: 31 42 61; --rgb-sunken: 31 42 61; --rgb-shadow: 31 42 61; --rgb-ink: 31 42 61; --rgb-ink-soft: 68 78 95; --rgb-ink-muted: 98 106 120; --rgb-ink-faint: 106 113 128; --rgb-accent: 37 99 235; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 206 208 212; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 74 111 165; --rgb-inverse: 42 53 71; --rgb-inverse-ink: 245 248 252; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#e9eef6, #e9eef6); --rgb-sheet: 245 248 252; --rgb-sheet-ink: 31 42 61; --rgb-sheet-ink-soft: 68 78 95; --rgb-sheet-ink-muted: 98 106 120; --rgb-sheet-ink-faint: 106 113 128; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* slate — a cool grey-blue study, nothing louder than slate */ :root[data-theme="slate"] { color-scheme: light; --rgb-surface: 242 244 246; --rgb-border: 38 46 58; --rgb-line: 38 46 58; --rgb-raised: 38 46 58; --rgb-sunken: 38 46 58; --rgb-shadow: 38 46 58; --rgb-ink: 38 46 58; --rgb-ink-soft: 70 77 87; --rgb-ink-muted: 98 104 113; --rgb-ink-faint: 105 111 120; --rgb-accent: 71 85 105; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 207 209 212; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 90 106 130; --rgb-inverse: 49 56 68; --rgb-inverse-ink: 242 244 246; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#e6e9ed, #e6e9ed); --rgb-sheet: 242 244 246; --rgb-sheet-ink: 38 46 58; --rgb-sheet-ink-soft: 70 77 87; --rgb-sheet-ink-muted: 98 104 113; --rgb-sheet-ink-faint: 105 111 120; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* forest — sage paper, deep forest green accent */ :root[data-theme="forest"] { color-scheme: light; --rgb-surface: 243 247 238; --rgb-border: 36 48 31; --rgb-line: 36 48 31; --rgb-raised: 36 48 31; --rgb-sunken: 36 48 31; --rgb-shadow: 36 48 31; --rgb-ink: 36 48 31; --rgb-ink-soft: 69 79 64; --rgb-ink-muted: 97 107 92; --rgb-ink-faint: 104 113 99; --rgb-accent: 22 101 52; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 207 209 206; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 79 117 102; --rgb-inverse: 47 58 42; --rgb-inverse-ink: 243 247 238; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#e8ede2, #e8ede2); --rgb-sheet: 243 247 238; --rgb-sheet-ink: 36 48 31; --rgb-sheet-ink-soft: 69 79 64; --rgb-sheet-ink-muted: 97 107 92; --rgb-sheet-ink-faint: 104 113 99; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* burgundy — blushed parchment, burgundy wine accent */ :root[data-theme="burgundy"] { color-scheme: light; --rgb-surface: 250 243 241; --rgb-border: 56 36 42; --rgb-line: 56 36 42; --rgb-raised: 56 36 42; --rgb-sunken: 56 36 42; --rgb-shadow: 56 36 42; --rgb-ink: 56 36 42; --rgb-ink-soft: 91 73 77; --rgb-ink-muted: 117 101 105; --rgb-ink-faint: 121 106 109; --rgb-accent: 159 18 57; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 211 207 208; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 140 90 104; --rgb-inverse: 66 47 53; --rgb-inverse-ink: 250 243 241; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#f4eae8, #f4eae8); --rgb-sheet: 250 243 241; --rgb-sheet-ink: 56 36 42; --rgb-sheet-ink-soft: 91 73 77; --rgb-sheet-ink-muted: 117 101 105; --rgb-sheet-ink-faint: 121 106 109; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* deep-violet — pale lavender-grey paper, deep violet accent */ :root[data-theme="deep-violet"] { color-scheme: light; --rgb-surface: 246 244 250; --rgb-border: 43 36 64; --rgb-line: 43 36 64; --rgb-raised: 43 36 64; --rgb-sunken: 43 36 64; --rgb-shadow: 43 36 64; --rgb-ink: 43 36 64; --rgb-ink-soft: 79 73 97; --rgb-ink-muted: 106 101 122; --rgb-ink-faint: 114 108 128; --rgb-accent: 109 40 217; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 208 207 213; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 109 90 150; --rgb-inverse: 54 47 74; --rgb-inverse-ink: 246 244 250; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#ece9f2, #ece9f2); --rgb-sheet: 246 244 250; --rgb-sheet-ink: 43 36 64; --rgb-sheet-ink-soft: 79 73 97; --rgb-sheet-ink-muted: 106 101 122; --rgb-sheet-ink-faint: 114 108 128; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* postal — an envelope: cream paper, navy ink, the postal red kept for danger */ :root[data-theme="postal"] { color-scheme: light; --rgb-surface: 250 245 233; --rgb-border: 36 48 78; --rgb-line: 36 48 78; --rgb-raised: 36 48 78; --rgb-sunken: 36 48 78; --rgb-shadow: 36 48 78; --rgb-ink: 36 48 78; --rgb-ink-soft: 69 79 102; --rgb-ink-muted: 99 106 124; --rgb-ink-faint: 104 110 127; --rgb-accent: 30 58 110; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 207 209 216; --rgb-danger: 200 64 47; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 78 98 133; --rgb-inverse: 47 58 87; --rgb-inverse-ink: 250 245 233; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#f3eddd, #f3eddd); --rgb-sheet: 250 245 233; --rgb-sheet-ink: 36 48 78; --rgb-sheet-ink-soft: 69 79 102; --rgb-sheet-ink-muted: 99 106 124; --rgb-sheet-ink-faint: 104 110 127; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* bronze-ink — deep warm stone, bronze accent — the heaviest of the warm papers */ :root[data-theme="bronze-ink"] { color-scheme: light; --rgb-surface: 245 240 228; --rgb-border: 50 44 34; --rgb-line: 50 44 34; --rgb-raised: 50 44 34; --rgb-sunken: 50 44 34; --rgb-shadow: 50 44 34; --rgb-ink: 50 44 34; --rgb-ink-soft: 79 73 63; --rgb-ink-muted: 107 101 91; --rgb-ink-faint: 113 108 97; --rgb-accent: 125 107 79; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 210 209 206; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 90 106 130; --rgb-inverse: 60 55 45; --rgb-inverse-ink: 245 240 228; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#ece5d6, #ece5d6); --rgb-sheet: 245 240 228; --rgb-sheet-ink: 50 44 34; --rgb-sheet-ink-soft: 79 73 63; --rgb-sheet-ink-muted: 107 101 91; --rgb-sheet-ink-faint: 113 108 97; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* petrol-copper — cool teal-washed paper, petrol accent, copper in the warning */ :root[data-theme="petrol-copper"] { color-scheme: light; --rgb-surface: 241 247 245; --rgb-border: 30 51 48; --rgb-line: 30 51 48; --rgb-raised: 30 51 48; --rgb-sunken: 30 51 48; --rgb-shadow: 30 51 48; --rgb-ink: 30 51 48; --rgb-ink-soft: 61 80 77; --rgb-ink-muted: 92 108 106; --rgb-ink-faint: 100 115 112; --rgb-accent: 15 118 110; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 206 210 209; --rgb-danger: 220 38 38; --rgb-warning: 194 112 61; --rgb-success: 22 163 74; --rgb-info: 71 117 111; --rgb-inverse: 41 61 58; --rgb-inverse-ink: 241 247 245; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#e4edeb, #e4edeb); --rgb-sheet: 241 247 245; --rgb-sheet-ink: 30 51 48; --rgb-sheet-ink-soft: 61 80 77; --rgb-sheet-ink-muted: 92 108 106; --rgb-sheet-ink-faint: 100 115 112; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* navy-sky — airy sky-tinted white, navy accent */ :root[data-theme="navy-sky"] { color-scheme: light; --rgb-surface: 243 248 252; --rgb-border: 27 43 77; --rgb-line: 27 43 77; --rgb-raised: 27 43 77; --rgb-sunken: 27 43 77; --rgb-shadow: 27 43 77; --rgb-ink: 27 43 77; --rgb-ink-soft: 65 79 108; --rgb-ink-muted: 94 107 132; --rgb-ink-faint: 100 112 137; --rgb-accent: 30 58 138; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 205 208 216; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 42 124 176; --rgb-inverse: 38 54 86; --rgb-inverse-ink: 243 248 252; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#e7f0f8, #e7f0f8); --rgb-sheet: 243 248 252; --rgb-sheet-ink: 27 43 77; --rgb-sheet-ink-soft: 65 79 108; --rgb-sheet-ink-muted: 94 107 132; --rgb-sheet-ink-faint: 100 112 137; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* forest-lime — lime-tinted paper, the lime itself as accent over near-ink text */ :root[data-theme="forest-lime"] { color-scheme: light; --rgb-surface: 244 248 234; --rgb-border: 38 49 28; --rgb-line: 38 49 28; --rgb-raised: 38 49 28; --rgb-sunken: 38 49 28; --rgb-shadow: 38 49 28; --rgb-ink: 38 49 28; --rgb-ink-soft: 72 82 62; --rgb-ink-muted: 98 108 88; --rgb-ink-faint: 105 114 95; --rgb-accent: 132 204 22; --rgb-accent-ink: 24 24 27; --rgb-field: 255 255 255; --rgb-field-border: 207 210 205; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 95 124 58; --rgb-inverse: 49 59 39; --rgb-inverse-ink: 244 248 234; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#eaf1dd, #eaf1dd); --rgb-sheet: 244 248 234; --rgb-sheet-ink: 38 49 28; --rgb-sheet-ink-soft: 72 82 62; --rgb-sheet-ink-muted: 98 108 88; --rgb-sheet-ink-faint: 105 114 95; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* plum-rose — rosy paper, plum accent */ :root[data-theme="plum-rose"] { color-scheme: light; --rgb-surface: 250 242 246; --rgb-border: 55 34 46; --rgb-line: 55 34 46; --rgb-raised: 55 34 46; --rgb-sunken: 55 34 46; --rgb-shadow: 55 34 46; --rgb-ink: 55 34 46; --rgb-ink-soft: 90 71 81; --rgb-ink-muted: 116 99 109; --rgb-ink-faint: 123 106 116; --rgb-accent: 134 25 143; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 211 206 209; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 150 83 124; --rgb-inverse: 65 45 56; --rgb-inverse-ink: 250 242 246; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#f4e8ee, #f4e8ee); --rgb-sheet: 250 242 246; --rgb-sheet-ink: 55 34 46; --rgb-sheet-ink-soft: 90 71 81; --rgb-sheet-ink-muted: 116 99 109; --rgb-sheet-ink-faint: 123 106 116; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* cobalt-coral — cool white with cobalt accent — the hairlines and hovers carry the coral warmth */ :root[data-theme="cobalt-coral"] { color-scheme: light; --rgb-surface: 245 247 252; --rgb-border: 181 73 60; --rgb-line: 181 73 60; --rgb-raised: 181 73 60; --rgb-sunken: 181 73 60; --rgb-shadow: 28 37 66; --rgb-ink: 28 37 66; --rgb-ink-soft: 71 78 103; --rgb-ink-muted: 100 107 128; --rgb-ink-faint: 106 113 132; --rgb-accent: 29 78 216; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 205 207 213; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 90 106 130; --rgb-inverse: 39 48 75; --rgb-inverse-ink: 245 247 252; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#eaeef8, #eaeef8); --rgb-sheet: 245 247 252; --rgb-sheet-ink: 28 37 66; --rgb-sheet-ink-soft: 71 78 103; --rgb-sheet-ink-muted: 100 107 128; --rgb-sheet-ink-faint: 106 113 132; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* teal-gold — golden paper, teal accent, the gold already in the warning */ :root[data-theme="teal-gold"] { color-scheme: light; --rgb-surface: 249 244 230; --rgb-border: 35 53 50; --rgb-line: 35 53 50; --rgb-raised: 35 53 50; --rgb-sunken: 35 53 50; --rgb-shadow: 35 53 50; --rgb-ink: 35 53 50; --rgb-ink-soft: 66 81 76; --rgb-ink-muted: 98 108 103; --rgb-ink-faint: 103 113 107; --rgb-accent: 15 118 110; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 207 211 210; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 71 117 111; --rgb-inverse: 46 63 60; --rgb-inverse-ink: 249 244 230; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#f3ecd7, #f3ecd7); --rgb-sheet: 249 244 230; --rgb-sheet-ink: 35 53 50; --rgb-sheet-ink-soft: 66 81 76; --rgb-sheet-ink-muted: 98 108 103; --rgb-sheet-ink-faint: 103 113 107; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* graphite-red — neutral graphite greys, one red accent */ :root[data-theme="graphite-red"] { color-scheme: light; --rgb-surface: 244 244 245; --rgb-border: 35 39 46; --rgb-line: 35 39 46; --rgb-raised: 35 39 46; --rgb-sunken: 35 39 46; --rgb-shadow: 35 39 46; --rgb-ink: 35 39 46; --rgb-ink-soft: 72 75 81; --rgb-ink-muted: 101 103 109; --rgb-ink-faint: 108 110 116; --rgb-accent: 220 38 38; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 207 207 209; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 90 106 130; --rgb-inverse: 46 50 56; --rgb-inverse-ink: 244 244 245; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#e8e8ea, #e8e8ea); --rgb-sheet: 244 244 245; --rgb-sheet-ink: 35 39 46; --rgb-sheet-ink-soft: 72 75 81; --rgb-sheet-ink-muted: 101 103 109; --rgb-sheet-ink-faint: 108 110 116; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* red-flick — quiet near-monochrome warm paper; the flick red is the only colour */ :root[data-theme="red-flick"] { color-scheme: light; --rgb-surface: 249 245 240; --rgb-border: 43 38 32; --rgb-line: 43 38 32; --rgb-raised: 43 38 32; --rgb-sunken: 43 38 32; --rgb-shadow: 43 38 32; --rgb-ink: 43 38 32; --rgb-ink-soft: 81 77 71; --rgb-ink-muted: 111 107 101; --rgb-ink-faint: 114 110 104; --rgb-accent: 200 64 47; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 208 207 206; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 90 106 130; --rgb-inverse: 54 49 43; --rgb-inverse-ink: 249 245 240; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#f2ede7, #f2ede7); --rgb-sheet: 249 245 240; --rgb-sheet-ink: 43 38 32; --rgb-sheet-ink-soft: 81 77 71; --rgb-sheet-ink-muted: 111 107 101; --rgb-sheet-ink-faint: 114 110 104; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* sky-flick — quiet near-monochrome cool paper; the flick sky is the only colour */ :root[data-theme="sky-flick"] { color-scheme: light; --rgb-surface: 245 248 250; --rgb-border: 38 43 46; --rgb-line: 38 43 46; --rgb-raised: 38 43 46; --rgb-sunken: 38 43 46; --rgb-shadow: 38 43 46; --rgb-ink: 38 43 46; --rgb-ink-soft: 77 81 84; --rgb-ink-muted: 103 107 110; --rgb-ink-faint: 108 112 115; --rgb-accent: 2 132 199; --rgb-accent-ink: 0 0 0; --rgb-field: 255 255 255; --rgb-field-border: 207 208 209; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 90 106 130; --rgb-inverse: 49 54 56; --rgb-inverse-ink: 245 248 250; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#ecf1f4, #ecf1f4); --rgb-sheet: 245 248 250; --rgb-sheet-ink: 38 43 46; --rgb-sheet-ink-soft: 77 81 84; --rgb-sheet-ink-muted: 103 107 110; --rgb-sheet-ink-faint: 108 112 115; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* bronze-flick — quiet near-monochrome paper; the flick bronze is the only colour */ :root[data-theme="bronze-flick"] { color-scheme: light; --rgb-surface: 248 244 238; --rgb-border: 43 38 32; --rgb-line: 43 38 32; --rgb-raised: 43 38 32; --rgb-sunken: 43 38 32; --rgb-shadow: 43 38 32; --rgb-ink: 43 38 32; --rgb-ink-soft: 83 78 72; --rgb-ink-muted: 111 106 100; --rgb-ink-faint: 114 109 103; --rgb-accent: 125 107 79; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 208 207 206; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 90 106 130; --rgb-inverse: 54 49 43; --rgb-inverse-ink: 248 244 238; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#f1ece4, #f1ece4); --rgb-sheet: 248 244 238; --rgb-sheet-ink: 43 38 32; --rgb-sheet-ink-soft: 83 78 72; --rgb-sheet-ink-muted: 111 106 100; --rgb-sheet-ink-faint: 114 109 103; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* violet-flick — quiet near-monochrome paper; the flick violet is the only colour */ :root[data-theme="violet-flick"] { color-scheme: light; --rgb-surface: 247 246 251; --rgb-border: 39 36 46; --rgb-line: 39 36 46; --rgb-raised: 39 36 46; --rgb-sunken: 39 36 46; --rgb-shadow: 39 36 46; --rgb-ink: 39 36 46; --rgb-ink-soft: 80 77 86; --rgb-ink-muted: 108 106 114; --rgb-ink-faint: 114 112 120; --rgb-accent: 139 92 246; --rgb-accent-ink: 0 0 0; --rgb-field: 255 255 255; --rgb-field-border: 207 207 209; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 90 106 130; --rgb-inverse: 50 47 56; --rgb-inverse-ink: 247 246 251; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#efedf4, #efedf4); --rgb-sheet: 247 246 251; --rgb-sheet-ink: 39 36 46; --rgb-sheet-ink-soft: 80 77 86; --rgb-sheet-ink-muted: 108 106 114; --rgb-sheet-ink-faint: 114 112 120; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* tricolore — stamp-paper cream, stamp-navy ink, the red franked on; gold in the warning */ :root[data-theme="tricolore"] { color-scheme: light; --rgb-surface: 251 246 232; --rgb-border: 35 44 71; --rgb-line: 35 44 71; --rgb-raised: 35 44 71; --rgb-sunken: 35 44 71; --rgb-shadow: 35 44 71; --rgb-ink: 35 44 71; --rgb-ink-soft: 73 79 100; --rgb-ink-muted: 102 107 121; --rgb-ink-faint: 108 112 125; --rgb-accent: 200 64 47; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 207 209 215; --rgb-danger: 220 38 38; --rgb-warning: 180 137 8; --rgb-success: 22 163 74; --rgb-info: 30 58 110; --rgb-inverse: 46 55 80; --rgb-inverse-ink: 251 246 232; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#f6f0dc, #f6f0dc); --rgb-sheet: 251 246 232; --rgb-sheet-ink: 35 44 71; --rgb-sheet-ink-soft: 73 79 100; --rgb-sheet-ink-muted: 102 107 121; --rgb-sheet-ink-faint: 108 112 125; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* indigo-violet — periwinkle paper, indigo accent */ :root[data-theme="indigo-violet"] { color-scheme: light; --rgb-surface: 244 244 251; --rgb-border: 35 34 68; --rgb-line: 35 34 68; --rgb-raised: 35 34 68; --rgb-sunken: 35 34 68; --rgb-shadow: 35 34 68; --rgb-ink: 35 34 68; --rgb-ink-soft: 74 73 103; --rgb-ink-muted: 105 104 129; --rgb-ink-faint: 108 107 131; --rgb-accent: 79 70 229; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 207 206 214; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 104 102 196; --rgb-inverse: 46 45 77; --rgb-inverse-ink: 244 244 251; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#eaeaf8, #eaeaf8); --rgb-sheet: 244 244 251; --rgb-sheet-ink: 35 34 68; --rgb-sheet-ink-soft: 74 73 103; --rgb-sheet-ink-muted: 105 104 129; --rgb-sheet-ink-faint: 108 107 131; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* sunset — warm peach drifting toward rose, sunset red accent */ :root[data-theme="sunset"] { color-scheme: light; --rgb-surface: 253 246 236; --rgb-border: 59 42 28; --rgb-line: 59 42 28; --rgb-raised: 59 42 28; --rgb-sunken: 59 42 28; --rgb-shadow: 59 42 28; --rgb-ink: 59 42 28; --rgb-ink-soft: 90 74 61; --rgb-ink-muted: 116 102 89; --rgb-ink-faint: 122 109 96; --rgb-accent: 225 29 72; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 212 208 205; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 178 90 65; --rgb-inverse: 69 53 39; --rgb-inverse-ink: 253 246 236; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(to bottom right, #fcefdf, #f9e7de); --rgb-sheet: 253 246 236; --rgb-sheet-ink: 59 42 28; --rgb-sheet-ink-soft: 90 74 61; --rgb-sheet-ink-muted: 116 102 89; --rgb-sheet-ink-faint: 122 109 96; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* ocean — airy aqua-white drifting toward seafoam, deep ocean accent */ :root[data-theme="ocean"] { color-scheme: light; --rgb-surface: 241 248 249; --rgb-border: 20 48 56; --rgb-line: 20 48 56; --rgb-raised: 20 48 56; --rgb-sunken: 20 48 56; --rgb-shadow: 20 48 56; --rgb-ink: 20 48 56; --rgb-ink-soft: 59 83 90; --rgb-ink-muted: 89 111 116; --rgb-ink-faint: 94 116 121; --rgb-accent: 14 116 144; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 203 209 211; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 43 127 118; --rgb-inverse: 32 58 66; --rgb-inverse-ink: 241 248 249; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(to bottom right, #e4f1f3, #e8f3ef); --rgb-sheet: 241 248 249; --rgb-sheet-ink: 20 48 56; --rgb-sheet-ink-soft: 59 83 90; --rgb-sheet-ink-muted: 89 111 116; --rgb-sheet-ink-faint: 94 116 121; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* berry — berry-tinted white, magenta accent */ :root[data-theme="berry"] { color-scheme: light; --rgb-surface: 250 242 248; --rgb-border: 53 32 47; --rgb-line: 53 32 47; --rgb-raised: 53 32 47; --rgb-sunken: 53 32 47; --rgb-shadow: 53 32 47; --rgb-ink: 53 32 47; --rgb-ink-soft: 90 71 84; --rgb-ink-muted: 119 102 114; --rgb-ink-faint: 122 105 117; --rgb-accent: 162 28 175; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 211 206 209; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 144 88 138; --rgb-inverse: 63 43 57; --rgb-inverse-ink: 250 242 248; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#f4e9f2, #f4e9f2); --rgb-sheet: 250 242 248; --rgb-sheet-ink: 53 32 47; --rgb-sheet-ink-soft: 90 71 84; --rgb-sheet-ink-muted: 119 102 114; --rgb-sheet-ink-faint: 122 105 117; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* copper — terracotta paper, copper accent */ :root[data-theme="copper"] { color-scheme: light; --rgb-surface: 250 241 231; --rgb-border: 58 43 29; --rgb-line: 58 43 29; --rgb-raised: 58 43 29; --rgb-sunken: 58 43 29; --rgb-shadow: 58 43 29; --rgb-ink: 58 43 29; --rgb-ink-soft: 88 74 60; --rgb-ink-muted: 114 101 88; --rgb-ink-faint: 120 108 95; --rgb-accent: 180 83 9; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 212 208 205; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 90 106 130; --rgb-inverse: 68 54 40; --rgb-inverse-ink: 250 241 231; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#f4e8da, #f4e8da); --rgb-sheet: 250 241 231; --rgb-sheet-ink: 58 43 29; --rgb-sheet-ink-soft: 88 74 60; --rgb-sheet-ink-muted: 114 101 88; --rgb-sheet-ink-faint: 120 108 95; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* blue-tonal — a blue-grey tonal study: everything is the same blue at a different depth */ :root[data-theme="blue-tonal"] { color-scheme: light; --rgb-surface: 242 245 250; --rgb-border: 29 43 69; --rgb-line: 29 43 69; --rgb-raised: 29 43 69; --rgb-sunken: 29 43 69; --rgb-shadow: 29 43 69; --rgb-ink: 29 43 69; --rgb-ink-soft: 66 78 101; --rgb-ink-muted: 96 106 125; --rgb-ink-faint: 102 111 130; --rgb-accent: 30 64 175; --rgb-accent-ink: 255 255 255; --rgb-field: 255 255 255; --rgb-field-border: 205 208 214; --rgb-danger: 220 38 38; --rgb-warning: 217 119 6; --rgb-success: 22 163 74; --rgb-info: 74 111 165; --rgb-inverse: 40 54 78; --rgb-inverse-ink: 242 245 250; --border-alpha: 0.10; --line-alpha: 0.09; --raised-alpha: 0.04; --hover-alpha: 0.035; --shadow-alpha: 0.05; --sunken-alpha: 0.04; --unread-tint-alpha: 0.4; --field-alpha: 0.6; --field-border-alpha: 0.4; --danger-soft-alpha: 0.08; --info-soft-alpha: 0.08; --app-bg: linear-gradient(#e7ecf4, #e7ecf4); --rgb-sheet: 242 245 250; --rgb-sheet-ink: 29 43 69; --rgb-sheet-ink-soft: 66 78 101; --rgb-sheet-ink-muted: 96 106 125; --rgb-sheet-ink-faint: 102 111 130; --rgb-sheet-link: 29 78 216; --rgb-sheet-danger: 185 28 28; } /* ═══ The calendar switch ═══════════════════════════════════════════════════ Three positions — mail, split, calendar — driven entirely by one attribute on the shell. Rules rather than classes toggled onto .main-pane from two places: the server renders the position and the controller changes it, and those two must not be able to disagree about what a position looks like. See App\Domain\Enum\Calendar\CalendarPaneMode. ═══════════════════════════════════════════════════════════════════════════ */ /* Calendar: the pane has the row and the mail steps aside. The mail is still in the DOM, so coming back is a class change rather than a navigation — which is the whole reason this is a third position of the pane and not a link to /calendar. */ [data-calendar-mode="calendar"] .main-pane:not([data-ui--split-target="pane"]), /* …and the column that carries it. The insight strip is a sibling of the mail card rather than a box inside it, so the two share a wrapper — and a wrapper that survived this rule would leave an insight card floating beside the calendar with the mail it introduces nowhere on screen. */ [data-calendar-mode="calendar"] [data-mail-column] { display: none; } /* …and the pane stops honouring the stored width, at both levels. The wrapper is `lg:w-auto` — sized by its contents — so telling only the pane to fill it is circular and collapses both to the width of the handle. Each has to be told to grow, which is what `flex: 1 1 0%` says and what `width: auto` un-says about the stored pixel count. The handle goes too: there is no boundary left to drag when one side has all of it. */ [data-calendar-mode="calendar"] [data-ui--split-target="wrapper"], [data-calendar-mode="calendar"] [data-ui--split-target="pane"] { flex: 1 1 0%; width: auto; } [data-calendar-mode="calendar"] [data-ui--split-target="handle"] { display: none; } /* Split below lg is Calendar. The row cannot hold a sidebar, a usable message list and a usable calendar at once under 1024px, so rather than squeezing both under their minimums the calendar takes over — the same behaviour the boolean version had, kept because it is right. */ @media (max-width: 1023px) { [data-calendar-mode="split"] .main-pane:not([data-ui--split-target="pane"]), [data-calendar-mode="split"] [data-mail-column] { display: none; } } /* The switch itself needs no rule here, deliberately. Which icon is drawn is the `hidden` ATTRIBUTE on the other two, and the accented state is two utility classes — both written by the server and toggled by ui--split. A first version selected the icon from the shell's mode here, and on a stack whose Tailwind build had not been rerun all three drew on top of each other: a control that says which mode you are in has to be right from the markup alone. The rules below are layout, which genuinely is CSS's job. The rubber band, and the settle back from it. Dragging past the pane's minimum or maximum does not stop dead — the handle keeps moving, with resistance, which is what tells you there IS something past the end without a label saying so. Past a threshold, letting go switches position; short of it, this transition is what puts the pane back. Only ever on for the length of that settle: a transition left on would make every drag lag a frame behind the pointer. */ [data-ui--split-target="pane"].is-settling { transition: width 180ms cubic-bezier(0.22, 1, 0.36, 1); } /* And the grip says the same thing, so the affordance is visible on a handle that is only two pixels wide. */ [data-ui--split-target="handle"].is-straining .split-grip { background-color: rgb(var(--rgb-accent)); transform: scaleY(1.6); } .split-grip { transition: transform 120ms ease-out, background-color 120ms ease-out; } @media (prefers-reduced-motion: reduce) { [data-ui--split-target="pane"].is-settling, .split-grip { transition: none; } [data-ui--split-target="handle"].is-straining .split-grip { transform: none; } } /* Accessibility: never force glass on people who opted out. */ @media (prefers-reduced-transparency: reduce) { :root { --pane-alpha: 1; --popover-alpha: 1; --pane-blur: 0px; --scrim-alpha: 0; --main-alpha: 1;} } /* ═══ A section that FILLS its scroll port ══════════════════════════════════ What .main-pane does for the mail shell, for a section that lives inside somebody else's scroll port: two peer cards side by side, each exactly as tall as the row, each scrolling its own body. The appearance panel is the only user today. Why it is here and not eight `xl:` variants in the template. Three of the pairs this needs are DIRECT conflicts with a utility already on the element — `position: static` against `sticky`, `max-height: none` against `max-h-[30svh]`, `overflow-y: auto` against `@3xl:overflow-visible` — and which of two Tailwind variants wins is source order in the built stylesheet, not specificity. That is the class of bug that reads as broken JavaScript (see the note on `.hidden` and `.flex` in _appearance.html.twig). These rules sit outside every layer, so they beat the utilities they are correcting no matter which order the build emits, and the whole mechanism is one block to read rather than a hunt through two templates. ── Why a MEDIA query when the panel itself is @container-driven ─────────── The panel decides "beside" versus "stacked" from its OWN width (@3xl), which is right: it is narrower than the window and the mail sidebar takes from it. But the HEIGHT has to come from an ancestor — a scroll port three levels up, outside the panel's container — and an element cannot query a container it is not inside. So the ancestors are gated on the viewport, and the same gate is used inside the panel so the two halves of the mechanism cannot disagree: below it, every one of these rules is inert and the layout is exactly the one that shipped, sticky preview card and all. 80rem is chosen so that `viewport >= 80rem` IMPLIES `panel >= @3xl` in both hosts, which is the invariant that makes one gate legal for both. Measured: the settings page crosses @3xl at a 1252px viewport and the setup wizard's modal at about 914px. The band between a host's own crossover and 80rem is the pre-existing layout, which is a working layout — the failure mode this ordering avoids is the other one, a preview card told to fill a row nothing has capped, which is the 2500px empty box this whole section exists to not be. ═══════════════════════════════════════════════════════════════════════════ */ @media (min-width: 80rem) { /* ── Every rule below hands height DOWN a flex chain, and not one of them says `height: 100%` ──────────────────────────────────────────────────── That is not a stylistic preference, it is the difference between working in both hosts and working in one. A percentage height needs a containing block whose height is DEFINITE, and the setup wizard's modal is `max-h-[92vh]` with an auto height — indefinite by construction, since it shrink-wraps a short step. Every flex-1 below it still gets a real used height (that is how the modal's own body scrolls), but a percentage resolves to `auto` against it, so the first version of this capped the settings page and left the wizard with a 2400px row: both cards the same height, and both of them the empty box. The app shell has never used a percentage height for this — `.main-pane` and everything above it is `flex flex-col flex-1 overflow-hidden` — and this now does the same. The scroll port becomes the column. Its one child then takes what it has rather than asking it how much that is. */ .pane-fill-port { display: flex; flex-direction: column; } /* The grid that takes the port's height: one row, and the row is `minmax(0, 1fr)` rather than `auto`. That is the load-bearing half — an auto row is sized by its content first, so a 2500px card would set the row to 2500 and overflow the port however definite the port's own height was. A 1fr row takes the height it is given and hands the overflow to the cards, which is where a scrollbar belongs. */ .pane-fill-host { flex: 1 1 auto; min-height: 0; grid-template-rows: minmax(0, 1fr); } /* A grid item's automatic minimum size is its content, which would undo the line above one level down. */ .pane-fill-host > * { min-height: 0; } /* The settings nav is the other child of the host grid and is not a card — nothing inside it scrolls. On a short window its thirteen links are taller than the row, and left alone they would overflow the port and give the page the second scrollbar this arrangement exists to remove. */ .pane-fill-host > nav { overflow-y: auto; } /* Wrappers between the host and the panel that exist only to nest. They are grid items of .pane-fill-host and are already stretched to its row, so all they need is permission to be shorter than their content. */ .pane-fill { min-height: 0; } /* A card: header, then a body that scrolls. The controls card is a grid item of the row and is stretched to it by grid; the preview card is one flex hop further down (the split's pane target sits between them, carrying the dragged width), and a column flex container stretches its items across, not along — so the card takes the rest of that column explicitly. `flex` is simply ignored on the grid item. `position: static` is what retires the sticky the preview card wears below this width: a card that already fills the row has nowhere to travel to. */ .pane-fill-card { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; position: static; } /* The part of a card that scrolls. */ .pane-fill-scroll { flex: 1 1 auto; min-height: 0; max-height: none; overflow-y: auto; } .pane-fill-stack { display: flex; flex-direction: column; } /* Takes the rest of a column, and may be shrunk to do it. `min-height: 0` is the load-bearing half rather than boilerplate: a flex item's automatic minimum size is its CONTENT, so a wrapper holding a 2400px card is 2400px tall however definite the height above it was, and the cap arrives one level short of the cards it was for. */ .pane-fill-rest { flex: 1 1 auto; min-height: 0; } /* Grows into what is left and is never shrunk below its content — which is what stops a tall card being a mostly-empty one: the miniature fills the preview the way the panes it is a picture of fill the app. `min-height` is deliberately left at `auto` here, the one difference from the class above: on a short window the scroll on .pane-fill-scroll is the right answer, not a squashed sample. */ .pane-fill-grow { flex: 1 1 auto; } .pane-fill-keep { flex: 0 0 auto; } } /* ═══ Base ═════════════════════════════════════════════════════════════════ Tailwind v4's preflight dropped `cursor: pointer` on buttons, so anything clickable that isn't an looks inert unless every single tag remembers `cursor-pointer` — 46 of the 102 buttons in templates/ didn't. Restore it once here; a `cursor-*` utility on the element still wins. ═══════════════════════════════════════════════════════════════════════════ */ @layer base { button, summary, [role="button"] { cursor: pointer; } button:disabled, [role="button"][aria-disabled="true"] { cursor: not-allowed; } /* ── The file picker's button ────────────────────────────────────────── Preflight resets ::file-selector-button along with everything else: transparent background, no border, no padding, no radius. What is left is the word "Choose file" — or "Datei auswählen" — sitting in a line of body text, and people read past it looking for the control. Styled here for the same reason `cursor: pointer` is: it applies to every file input there will ever be, rather than to the ones that remembered a stack of `file:` utilities. The three that did had drifted into three different looks, two of them a 4%-opacity tint with no border, which is why this was reported as unstyled rather than as inconsistent. The secondary control's look — the field border and fill that the app's other bordered controls use — and deliberately not the accent one: picking a file is a step towards the button the form is actually asking for, and two accent buttons in one form is two primary actions. ────────────────────────────────────────────────────────────────────── */ input[type="file"] { color: rgb(var(--rgb-ink-muted)); } input[type="file"]::file-selector-button { margin-inline-end: 0.75rem; height: 2rem; padding-inline: 0.75rem; border: 1px solid rgb(var(--rgb-field-border) / var(--field-border-alpha)); border-radius: 0.5rem; background-color: rgb(var(--rgb-field) / var(--field-alpha)); color: rgb(var(--rgb-ink-soft)); font-size: 0.75rem; font-weight: 500; cursor: pointer; transition: background-color 150ms, border-color 150ms; } /* On the input, not the pseudo-element: hovering anywhere over the control should light the button, which is the only part of it that is clickable in the first place. */ input[type="file"]:hover::file-selector-button { background-color: rgb(var(--rgb-raised) / var(--hover-alpha)); } /* The ring the app's other fields draw on focus. `:focus-visible` on the input rather than the pseudo-element: the button cannot hold focus of its own, the input does, and a keyboard user tabbing here needs to see where they landed. */ input[type="file"]:focus-visible { outline: none; } input[type="file"]:focus-visible::file-selector-button { outline: 2px solid rgb(var(--rgb-accent)); outline-offset: 2px; } input[type="file"]:disabled::file-selector-button { cursor: not-allowed; opacity: 0.5; } html { /* Pinch-zoom and rubber-band scrolling both let the fixed-height shell drift away from the viewport, leaving the app half off screen with no way back. */ touch-action: pan-x pan-y; overscroll-behavior: none; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; } /* `dvh`, not `vh`: mobile Safari and Chrome measure 100vh against the viewport with the URL bar *hidden*, so a 100vh shell hangs its bottom row — the compose dock, the toolbar — under the browser chrome. Height lives here rather than on the shells so the safe-area padding below is subtracted exactly once. The shells ask for `h-full`. */ html, body { height: 100dvh; } /* The demo bar is `fixed` at the bottom, so it sits OVER the panes rather than shortening them — deliberately, because a banner in the flow costs every page a strip of reading height. The cost is that the last row of a scroller ends up behind it, which on a phone is the row you were reaching for. Scrollers get that much padding back, so their content can always be scrolled clear of the bar. On the class rather than a media query: the bar exists only in demo mode, and on a desktop the last row is just as covered — it is simply less often the one you wanted. */ body.has-demo-bar [data-mail--mail-pane-target="list"], body.has-demo-bar #message-list, body.has-demo-bar .demo-bar-clearance { padding-bottom: 3.5rem; } body { /* viewport-fit=cover puts us under the notch and home indicator. */ padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); overscroll-behavior: none; /* Nothing in a mail client should scroll the page sideways; panes scroll their own overflow. */ overflow-x: hidden; } } /* ═══ Semantic tokens → Tailwind utilities ══════════════════════════════════ `inline` is required: it bakes rgb(var(--rgb-*)) into each utility so the utility re-resolves when a theme block changes the channel. ═══════════════════════════════════════════════════════════════════════════ */ @theme inline { --color-surface: rgb(var(--rgb-surface)); --color-line: rgb(var(--rgb-line) / var(--line-alpha)); --color-raised: rgb(var(--rgb-raised) / var(--raised-alpha)); --color-hover: rgb(var(--rgb-raised) / var(--hover-alpha)); --color-ink: rgb(var(--rgb-ink)); --color-ink-muted: rgb(var(--rgb-ink-muted)); --color-ink-faint: rgb(var(--rgb-ink-faint)); --color-accent: rgb(var(--rgb-accent)); --color-accent-strong: color-mix(in oklab, rgb(var(--rgb-accent)) 85%, black); --color-accent-soft: rgb(var(--rgb-accent) / 0.1); --color-accent-ink: rgb(var(--rgb-accent-ink)); --color-ink-soft: rgb(var(--rgb-ink-soft)); --color-sunken: rgb(var(--rgb-sunken) / var(--sunken-alpha)); --color-field: rgb(var(--rgb-field) / var(--field-alpha)); --color-field-border: rgb(var(--rgb-field-border) / var(--field-border-alpha)); --color-danger: rgb(var(--rgb-danger)); --color-warning: rgb(var(--rgb-warning)); --color-success: rgb(var(--rgb-success)); --color-inverse: rgb(var(--rgb-inverse)); --color-inverse-ink: rgb(var(--rgb-inverse-ink)); --color-info: rgb(var(--rgb-info)); --color-danger-soft: rgb(var(--rgb-danger) / var(--danger-soft-alpha)); --color-info-soft: rgb(var(--rgb-info) / var(--info-soft-alpha)); --color-pane-soft: rgb(var(--rgb-surface) / calc(var(--pane-alpha) * 0.6)); --shadow-float: 0 20px 25px -5px rgb(var(--rgb-shadow) / calc(var(--shadow-alpha) * 4)), 0 8px 10px -6px rgb(var(--rgb-shadow) / calc(var(--shadow-alpha) * 4)); --shadow-pop: 0 10px 15px -3px rgb(var(--rgb-shadow) / calc(var(--shadow-alpha) * 3)), 0 4px 6px -4px rgb(var(--rgb-shadow) / calc(var(--shadow-alpha) * 3)); /* Every surface the eye reads as a panel — modals, the compose window, dropdowns, menus, toasts — rounds with this, so the appearance setting actually reaches them. A fixed rounded-xl disagrees at every radius but the default. Controls inside a surface (buttons, inputs, chips, list rows) keep a fixed radius: they should not grow to 2rem corners. */ --radius-pane: var(--app-radius); --spacing-row: var(--density-row-y); --spacing-gutter: var(--density-gap); } /* ═══ Composed utilities ═══════════════════════════════════════════════════ */ /* Why these shadows are shallower than they look like they should be. The shell is `h-screen overflow-hidden` and pads itself by one gutter, so a pane that runs to the bottom of the window — the sidebar, the mail pane, the calendar — ends 12px above the clip. The first version of this reached 10 + 15 - 3 = 22px below the element, so its lower half was cut off by that edge, and at a rounded corner the cut ran across the shadow's own curve and left a hard little arc sitting under each bottom corner. It was invisible for a year because --shadow-alpha is 0.05 in the light themes. The dark themes put it at 0.3–0.45, where a clipped shadow is not subtle at all, and every dark theme showed the artifact on exactly the three panes that touch the bottom — never on the insight strip, whose shadow falls on the mail pane and is never clipped. So the reach is kept inside the gutter: 6 + 12 - 6 = 12px, and 2 + 4 - 2 = 4px for the contact layer. Changing the gutter without revisiting these numbers brings the arc back. */ /* The STRUCTURAL surfaces read --pane-alpha: the sidebar, the top bar and the insight strip here, the mail/calendar/settings ground in main-pane below. They are the layout, so there is nothing stacked behind them and glass costs them nothing. Everything that floats OVER them reads --popover-alpha instead — see pane-flat and popover further down. */ @utility pane { background-color: rgb(var(--rgb-surface) / var(--pane-alpha)); backdrop-filter: blur(var(--pane-blur)); -webkit-backdrop-filter: blur(var(--pane-blur)); border: 1px solid rgb(var(--rgb-border) / var(--border-alpha)); border-radius: var(--app-radius); box-shadow: 0 6px 12px -6px rgb(var(--rgb-shadow) / var(--shadow-alpha)), 0 2px 4px -2px rgb(var(--rgb-shadow) / var(--shadow-alpha)); } /* The compose window, a modal, the confirm dialog, a toast: a surface that FLOATS over the layout rather than being part of it. --popover-alpha, not --pane-alpha, and that split is the whole point. These sit on top of a pane that is itself letting the background through, so the two translucencies multiply — a composer at 0.7 over a sidebar at 0.7 leaves a tenth of the wallpaper landing in the middle of what you are typing. The panes can afford glass because nothing is stacked behind them; these cannot. `--pane-alpha` is re-pointed at the popover's own value for the SUBTREE, the same move mail-sheet makes with the ink channels. Everything painted inside a floating surface is derived from that variable — `bg-pane-soft` compiles to `calc(var(--pane-alpha) * 0.6)` at the point of use — and an inset strip inside the composer belongs to the composer, not to the sidebar behind it. */ @utility pane-flat { --pane-alpha: var(--popover-alpha); background-color: rgb(var(--rgb-surface) / var(--popover-alpha)); backdrop-filter: blur(var(--pane-blur)); -webkit-backdrop-filter: blur(var(--pane-blur)); border: 1px solid rgb(var(--rgb-border) / var(--border-alpha)); border-radius: var(--app-radius); } /* A menu that floats over content: the same surface as pane-flat with a deeper shadow, because a dropdown has no backdrop to separate it from what it covers. This was hardcoded fully opaque, on the reasoning that a translucent dropdown lets whatever it covers — toolbar icons, a wall of photos — read straight through the labels. That reasoning still holds, and it is now stated as a value rather than as a rule: --popover-alpha defaults to 1 and Appearance::RANGE_POPOVER_ALPHA will not go below 0.5, so the menus are exactly as opaque as they have always been unless somebody deliberately asks otherwise, and even then they cannot become as see-through as a pane. */ @utility popover { --pane-alpha: var(--popover-alpha); background-color: rgb(var(--rgb-surface) / var(--popover-alpha)); backdrop-filter: blur(var(--pane-blur)); -webkit-backdrop-filter: blur(var(--pane-blur)); border: 1px solid rgb(var(--rgb-border) / var(--border-alpha)); border-radius: var(--app-radius); box-shadow: 0 20px 25px -5px rgb(var(--rgb-shadow) / calc(var(--shadow-alpha) * 4)), 0 8px 10px -6px rgb(var(--rgb-shadow) / calc(var(--shadow-alpha) * 4)); } @utility main-pane { background-color: rgb(var(--rgb-main) / var(--main-alpha)); backdrop-filter: blur(var(--pane-blur)); -webkit-backdrop-filter: blur(var(--pane-blur)); border: 1px solid rgb(var(--rgb-border) / var(--border-alpha)); border-radius: var(--app-radius); box-shadow: 0 6px 12px -6px rgb(var(--rgb-shadow) / var(--shadow-alpha)), 0 2px 4px -2px rgb(var(--rgb-shadow) / var(--shadow-alpha)); } /* The sheet a message body is rendered on. Mail arrives authored for a light background — senders set dark text and assume white behind it — so the whole reading pane switches to light rather than just the body, in every theme. Re-pointing the CHANNELS rather than restyling each descendant is what makes every token-based class inside (text-ink, border-line, hover:bg-hover, bg-sunken …) resolve to a sheet value: every existing utility keeps working and nothing new has to know the sheet exists. There were two of these for a fortnight — a white one from the first reading-view pass and the warm-paper redesign that replaced it — and because the white one sat further down the file it won on source order, so the redesign was dead code that read as live. This is the redesign, merged with the one thing the white block had that it did not: the stacking context below. The colours are the theme's, not a fixed palette. Light is a constraint the sheet has to meet for the mail's sake; WHICH light is the theme's own business, and every theme states its seven --rgb-sheet-* channels in full. Everything else here is derived from those two ends of the ramp — the hairlines, insets, fields and inverses are all the sheet's ink at a low alpha, which is why they need no channels of their own. */ @utility mail-sheet { --rgb-surface: var(--rgb-sheet); --rgb-main: var(--rgb-sheet); --rgb-ink: var(--rgb-sheet-ink); --rgb-ink-soft: var(--rgb-sheet-ink-soft); --rgb-ink-muted: var(--rgb-sheet-ink-muted); --rgb-ink-faint: var(--rgb-sheet-ink-faint); --rgb-line: var(--rgb-sheet-ink); --rgb-border: var(--rgb-sheet-ink); --rgb-raised: var(--rgb-sheet-ink); --rgb-sunken: var(--rgb-sheet-ink); --rgb-field: var(--rgb-sheet-ink); --rgb-field-border: var(--rgb-sheet-ink); --rgb-inverse: var(--rgb-sheet-ink); --rgb-inverse-ink: var(--rgb-sheet); --rgb-shadow: var(--rgb-sheet-ink); /* A dark theme's --rgb-danger is lightened for a near-black surface and washes out to nothing on a sheet this light, so the sheet carries its own — the same reason every other channel here is re-pointed. */ --rgb-danger: var(--rgb-sheet-danger); --main-alpha: 1; --line-alpha: 0.09; --border-alpha: 0.10; --raised-alpha: 0.04; --sunken-alpha: 0.05; --hover-alpha: 0.045; --field-alpha: 0.03; --field-border-alpha: 0.15; --shadow-alpha: 0.06; --danger-soft-alpha: 0.08; /* Own stacking context: the pane above it is backdrop-filtered, and without isolation Chrome re-samples the app background through the sheet at some viewport sizes, flashing the whole pane dark. */ position: relative; isolation: isolate; background-color: rgb(var(--rgb-surface)); color: rgb(var(--rgb-ink)); /* Plenty of mail leaves its links to the client, and an accent tuned for the surrounding theme is not necessarily readable on the sheet. */ & a:not([class*="text-"]):not([class*="bg-"]) { color: rgb(var(--rgb-sheet-link)); } } @utility app-bg { position: relative; background-image: var(--app-bg); background-size: cover; background-position: center; background-attachment: fixed; &::before { content: ""; position: absolute; inset: 0; pointer-events: none; background-color: rgb(0 0 0 / var(--scrim-alpha)); } & > * { position: relative; } } @utility row-y { padding-block: var(--density-row-y); } /* One row of the mail list. Comfortable resolves to 0.625rem, which is the `py-2.5` this row was hardcoded to before density reached it — so the default install's geometry is unchanged and the other two steps go down from it. See Density::listRowPadding(). */ @utility list-row-y { padding-block: var(--surface-list-row-y); } /* One message block in the reading pane. Comfortable is the old `py-4`. */ @utility reading-y { padding-block: var(--surface-reading-row-y); } /* The tint behind an unread row. Its own utility rather than `bg-white/40 dark:bg-white/[0.03]` inline, because the emphasis setting has to be able to scale it and a pair of hardcoded Tailwind alphas cannot be scaled by anything. The two numbers are the same two numbers; they moved to :root and .dark, where a theme can also disagree with them if it ever needs to. White is written out rather than tokenised: it is white in all seven themes today, and what actually differs between them is how much of it — which is --unread-tint-alpha, and which every palette block declares. A channel that no theme would ever set differently is a variable with one value and a maintenance cost in nine places. The hover state is deliberately NOT expressed here — the row keeps its `hover:bg-white/60`, which is emitted in Tailwind's variant layer and so still wins over this. A row you are pointing at should light up at every emphasis setting, including Subtle, where the tint is otherwise zero. */ @utility row-unread { background-color: rgb(255 255 255 / calc(var(--unread-tint-alpha) * var(--unread-emphasis))); } @utility alert-danger { background-color: rgb(var(--rgb-danger) / var(--danger-soft-alpha)); border: 1px solid rgb(var(--rgb-danger) / 0.25); color: rgb(var(--rgb-danger)); } /* A pill for a step that has actually been answered. Its own utility rather than Tailwind colour classes: --rgb-success is a theme token, so a step marked done stays legible in every theme. */ @utility pill-done { background-color: rgb(var(--rgb-success) / 0.12); border-color: rgb(var(--rgb-success) / 0.35); color: rgb(var(--rgb-success)); } /* A step that was filled in and does not work. Its own utility for the same reason pill-done is: --rgb-danger is a theme token. */ @utility pill-failed { background-color: rgb(var(--rgb-danger) / 0.12); border-color: rgb(var(--rgb-danger) / 0.35); color: rgb(var(--rgb-danger)); } @utility alert-info { background-color: rgb(var(--rgb-info) / var(--info-soft-alpha)); border: 1px solid rgb(var(--rgb-info) / 0.2); color: rgb(var(--rgb-info)); } @utility tooltip-bubble { background-color: rgb(var(--rgb-inverse)); color: rgb(var(--rgb-inverse-ink)); &::before { border-bottom-color: rgb(var(--rgb-inverse)); } } @utility btn-quiet { background-color: rgb(var(--rgb-raised) / calc(var(--raised-alpha) * 2)); color: rgb(var(--rgb-ink-soft)); &:hover { background-color: rgb(var(--rgb-raised) / calc(var(--raised-alpha) * 3.5)); } } /* A clipped chip opens to its own content while the pointer rests on it. The time grid draws a meeting in the box its duration earns, and a quarter of an hour in a 96px column — or half of one, where two meetings overlap — is a box that holds a word. `title=` has always carried the rest, and a native tooltip is a poor answer for something the eye is already on: it arrives somewhere else, after a delay, in the platform's own font, and it cannot be read at the same time as the row above it. So the box opens instead. `max-content` is exactly as wide as the chip wants and no wider; `min-width: 100%` stops a two-word title from SHRINKING the block it is in, which is what happens without it on a wide column; and the cap is the width at which a long title starts wrapping rather than running out across three days. `max(16rem, 100%)` for that cap rather than a bare 16rem, and it is a fix rather than a flourish. A Day-view block is the whole grid — around 970px — and a bare cap SHRANK it to 256 the moment the pointer arrived, so a click aimed at the middle of the block landed on the empty two-thirds the chip had just vacated and opened nothing at all. `min-width` ought to have won that argument by the cascade's own rules and did not; stating the cap as "16rem, or my own width, whichever is the larger" settles it where it cannot be argued with. calendar-timegrid's click test is what caught it. **Width only, and the height is left strictly alone.** This grew both once, and a `height: auto` here broke two things at a stroke. The block's resize preview works by writing a height onto the positioned parent that this element follows with `h-full`, so an element sizing itself stopped following and a resize drag showed nothing happening. Worse and much quieter: the chip INSIDE this element is `h-full` too, and a percentage height against an auto-height parent computes to auto — so in Day view, where a chip is one row of a 46px block, the button shrank to 22px at the top and a click aimed at the middle of the block landed under it, on this element, and opened nothing. calendar-timegrid's click test is what caught that. `:not(:active)` survives the height going away, for the resize half of the same argument: a box that widened out from under a pointer mid-drag is its own kind of wrong. The pressed element keeps `:active` for as long as the button is held, which is exactly the window the growth has to stay out of. Paired with `hover:z-30` on the positioned parent, which is where the rise has to happen: the parent declares `@container` for the chip inside it, and a container is a stacking context, so a z-index here could never lift this past the block next door. */ /* The wash a grid block carries, over a background that is actually there. This was one inline `background-color: 1a` and a `bg-surface` class beside it that had never once applied: an inline style outranks every class, so the only background a block had was a tenth of a calendar colour, and the "surface" under it was whatever the block happened to be over. That is invisible while a block sits in its own column and obvious the moment one opens across its neighbour, which is how it was found. Two layers instead of one. `background-color` is the opaque surface and the tint is a `background-image` composited onto it, which is the one way to stack two paints on a box without a pseudo-element — and a pseudo-element is the wrong tool here because the box is already `overflow-hidden` around a chip and would need a z-index to stay behind it. The custom property is what the caller sets inline, and a property is free to be overridden by a class in a way a `background-color` never was. */ @utility block-tint { background-color: rgb(var(--rgb-surface)); background-image: linear-gradient(var(--block-tint, transparent), var(--block-tint, transparent)); } @utility chip-open { /* Guarded the way Tailwind guards its own `hover:`, and for the reason that variant is guarded: a tap on a touch screen leaves `:hover` stuck on the element until something else is tapped, so without this a block would stay open behind the editor the tap just opened. TWO selectors, because there are two shapes of this. In the time grid the thing that opens is a box INSIDE the positioned block, since the block carries the placement as an inline width and height that a class cannot outrank — so the block is `group/chip` and the hover is read off it. That is not a nicety: the block has a pixel of padding around this box, and reading the hover off the box left a one-pixel ring around every chip where the pointer did nothing, which is exactly as flaky as it sounds when you sweep across a week. In the all-day band the box IS the chip and has no such parent, so it reads its own. */ @media (hover: hover) { .group\/chip:hover:not(:active) > &, &:hover:not(:active) { width: max-content; min-width: 100%; max-width: max(16rem, 100%); } } } /* A native will let CSS place * — there is no pseudo-element to hang it on. Same path as the form theme's * chevron, so the two match. * * The chevron's grey is fixed rather than tokenised because a data: URI cannot * read a custom property. It costs nothing here: --rgb-ink-faint lands between * 108 and 170 across all six themes, so one mid grey is within a few percent of * every one of them, on light and dark alike. * * Height and text size are deliberately not set. Call sites differ — h-9 in * settings, h-8 in the admin tables — and a utility that fixed them would have * to win an argument with a plain `h-8` in the same layer. */ @utility select-field { appearance: none; padding-inline: 0.75rem 2rem; border: 1px solid rgb(var(--rgb-field-border) / var(--field-border-alpha)); border-radius: 0.5rem; background-color: rgb(var(--rgb-field) / var(--field-alpha)); color: rgb(var(--rgb-ink)); cursor: pointer; transition: background-color 100ms, border-color 100ms; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%238b8b93'%3E%3Cpath fill-rule='evenodd' d='M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z' clip-rule='evenodd'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.625rem center; background-size: 0.875rem 0.875rem; &:hover { background-color: rgb(var(--rgb-raised) / var(--hover-alpha)); } &:focus { outline: none; border-color: transparent; box-shadow: 0 0 0 2px rgb(var(--rgb-accent)); } &:disabled { opacity: 0.5; cursor: not-allowed; } /* A list box, not a drop-down: it has no chevron to draw and the padding * reserved for one would just indent every row. */ &:is([multiple], [size]) { background-image: none; padding-inline: 0.75rem; cursor: default; } } @utility badge-danger { background-color: rgb(var(--rgb-danger) / var(--danger-soft-alpha)); color: rgb(var(--rgb-danger)); } @utility badge-warning { background-color: rgb(var(--rgb-warning) / var(--danger-soft-alpha)); color: rgb(var(--rgb-warning)); } /* The fourth badge, added for the push delivery log, where "it arrived" is one of four outcomes shown side by side. It shares --danger-soft-alpha with the two above rather than declaring an alpha of its own: the number is the per-theme strength of a tinted badge, not a statement about danger, and a green mixed at a different opacity than the red beside it reads as a different kind of thing. */ @utility badge-success { background-color: rgb(var(--rgb-success) / var(--danger-soft-alpha)); color: rgb(var(--rgb-success)); } @utility badge-neutral { background-color: rgb(var(--rgb-raised) / calc(var(--raised-alpha) * 2)); color: rgb(var(--rgb-ink-soft)); } /* The fifth, added for Reported mail, where two kinds of report sit in one list and the badge is the only thing separating them at a glance. Info rather than success or warning because neither kind is good or bad news — they are two categories of the same thing, and the pending/handled badge beside them is already carrying the "is this a problem" colour. Same alpha as the rest. */ @utility badge-info { background-color: rgb(var(--rgb-info) / var(--danger-soft-alpha)); color: rgb(var(--rgb-info)); } /* A filter chip: pressed or not, in one class, because the pressed state is set from JavaScript on `data-active` and a Tailwind class list toggled from a controller is a class list nobody can find from the template. */ @utility chip-filter { display: inline-flex; align-items: center; height: 1.5rem; padding-inline: 0.5rem; border-radius: 0.5rem; border: 1px solid var(--color-line); font-size: 0.6875rem; color: rgb(var(--rgb-ink-muted)); cursor: pointer; transition: background-color 150ms, color 150ms, border-color 150ms; &:hover { background-color: rgb(var(--rgb-raised) / var(--raised-alpha)); } &[data-active="true"] { border-color: rgb(var(--rgb-accent)); background-color: rgb(var(--rgb-accent) / var(--danger-soft-alpha)); color: rgb(var(--rgb-accent)); } } @layer utilities { /* Thread reply zone: hide the Reply/Forward buttons while an inline compose card is open. The `composing` class is toggled by the compose controller — a :has() selector here worked, but its invalidation made Chrome repaint the surrounding .mail-sheet without its var()-based background, flashing the whole pane dark. */ [data-reply-zone].composing > [data-reply-actions] { display: none; } .compose-address-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 1rem; flex-shrink: 0; min-height: 36px; } /* `.compose-scroll-x` lived here — the scrollbar-hiding half of a compose toolbar that scrolled sideways on a phone. Both toolbars wrap now (`flex-wrap`, two rows), so there is no scroller to hide a bar on, and with the scroller went the `overflow-y: auto` it implied, which had been clipping every menu that opened upwards out of the strip. */ .compose-field-label { font-size: 0.75rem; font-weight: 500; color: rgb(var(--rgb-ink-faint)); width: 2.5rem; flex-shrink: 0; text-align: right; } } /* ═══ iOS zooms into anything under 16px ═══════════════════════════════════ Tap a control whose COMPUTED font-size is below 16px on an iPhone and Safari zooms the page in to meet it — and does not zoom back out. There is one documented way to stop that and it is to make the computed size 16px; `maximum-scale=1, user-scalable=no` also works and is not available to us, because it takes pinch-zoom away from everyone who needs it (WCAG 1.4.4, and see the viewport meta in app.html.twig where that was decided). THIS RULE ALREADY EXISTED, IN `@layer base`, AND HAD NEVER ONCE FIRED. Tailwind's utilities live in a later layer, so `text-sm` on the element beat it every time — and `text-sm` is on essentially every field in the app. Measured at 390px before moving it: the search box 14px, the login email and password 14px, the compose subject 14px, the compose editor 14px, the formatting bar's selects 12px. Every one of them zoomed. Unlayered, it wins, which is the whole reason the rule is here rather than back where it was. `[contenteditable]` belongs in the list: the compose body is a div, not a textarea, and Safari zooms into it on exactly the same rule. The three input types excluded have no text to render and are sized by the UA. Below md only. On a desktop 16px everywhere would be a redesign, and no pointer-driven browser zooms on focus. ═══════════════════════════════════════════════════════════════════════════ */ @media (max-width: 767px) { input:not([type="checkbox"]):not([type="radio"]):not([type="range"]), select, textarea, [contenteditable="true"] { /* A FLOOR, not a pin. A bare `16px` replaces the utility instead of raising it, which cancels `--app-font-scale` for every field below md — and above about 1.14 it makes fields SMALLER than they were before this rule started firing, and smaller than their own labels. The appearance slider runs to 1.25, so that is five stops of it, and the people standing on those stops are the ones the WCAG argument above is about. 0.875rem is `text-sm`, which is what nearly every field here carries. CSS cannot say "this element's own size, floored at 16px", so the approximation is to floor the common one: the `text-xs` controls in the compose toolbar come out a little larger than drawn at high scale, which is not a defect for someone who asked for larger text. */ font-size: max(16px, 0.875rem); } } /* ═══ The search box on a phone ════════════════════════════════════════════ Above md the search box is what it always was: a field in the topbar row, and none of this applies. Below md that field had about 120px between the logo and the calendar button — too narrow to read a query back in, and the first thing a thumb hit on the way to anything else. It becomes a magnifying-glass button that opens the field as a full-width row hanging under the topbar, which is what a phone expects and what gives the query somewhere to be read. ONE field, moved, rather than a second one for phones. The box is a combobox: it owns `aria-controls`, an id'd listbox, a live region and the recent-search list. A duplicate would have meant duplicate ids — two elements claiming `#search-listbox` — and a screen reader following the wrong one. Absolute rather than a second flex row, so the topbar keeps its height: `--pane-header-h` is a published knob and the shell is laid out against it. The containing block is the
, which is already `relative` for the dropdown's sake. The dropdown stays inside this element and so keeps positioning against it. `(width < 48rem)` — Tailwind's own `max-md:` query, copied verbatim — and NOT a px equivalent. This element's visibility (`md:hidden` on the button), its positioning context (`md:relative`) and its row alignment (`max-md:justify-end`) are all Tailwind variants compiled against 48rem, and rem in a media query resolves against the browser's default-font-size PREFERENCE rather than against `html { font-size }`. So a px boundary here moves independently of those three, and the two disagree for anyone who has changed that preference: • Chrome set to "Small" (12px) puts `md:` at 576px. A px panel rule would still be waiting for 768px, so between the two the button is gone AND the panel is hidden — and there is no other way into mail search. It is simply missing, on a page that otherwise renders at a normal size. • Set to 20px, `md:` lands at 960px: the button and the inline field both render, the button becomes a no-op that still reports `aria-expanded`, and `md:relative` does not match — so the shell computes `static` and the suggestions dropdown anchors to
and paints across the whole topbar. The two `max-width: 767px` font rules elsewhere in this file stay in px on purpose: they exist to clear a 16px threshold that iOS measures in device pixels, and expressing them in rem would push 16px fields onto a desktop whose owner has merely asked for larger text. ═══════════════════════════════════════════════════════════════════════════ */ @media (width < 48rem) { .search-shell { position: absolute; top: 100%; z-index: 30; /* Edge to edge, and the three declarations are one idea. `left: 0` would be the header's PADDING box — and the header is inset from the screen twice over: once by the shell's gutter, once by its own `px-4` — so the strip stopped short of both screen edges and the mail list showed down either side of it. It read as a floating card rather than as part of the topbar. Centring a 100vw box on the header reaches both edges without naming either inset, which matters because both are settings: the gutter moves with the layout (flat drops it) and the padding with the density. Nothing here has to know their values. `100vw` is the viewport including any scrollbar, which is right below md — the shell is `overflow: hidden` and a phone's scrollbar is an overlay with no width. */ left: 50%; width: 100vw; transform: translateX(-50%); /* The horizontal half of this is what keeps the field and the suggestion list off the screen edges while the BACKGROUND runs all the way to them: the dropdown inside is `absolute left-0 right-0`, which resolves against this element's padding box, so it inherits the inset without being told about it. */ padding: 0.25rem 1rem 0.75rem; /* Opaque, always. The list scrolls underneath this and a translucent surface turned the query into something read through moving text. `--rgb-surface` and not the topbar's own `pane`, because the flat layout makes that transparent by design — see `.layout-flat .shell-chrome` — and a see-through search bar is the thing being fixed. */ background-color: rgb(var(--rgb-surface)); border-bottom: 1px solid rgb(var(--rgb-line) / var(--line-alpha)); } /* Closed is the server-rendered state, so a phone that never runs the controller gets the button and no panel rather than a panel stuck open under the topbar. */ .search-shell[data-open="false"] { display: none; } } /* ═══ The virtual keyboard ═════════════════════════════════════════════════ `--keyboard-inset` is how much of the layout viewport the on-screen keyboard has taken. The compose controller publishes it on from `window.visualViewport` and clears it when the window closes; everything anchored to the bottom of the SCREEN has to subtract it. It exists because a fixed element is placed against the LAYOUT viewport and iOS does not shrink that one for the keyboard — so the dock kept sitting `bottom: 1rem` from a screen edge that was now behind a keyboard, and an iPad's compose window had its send row underneath it. Android shrinks the layout viewport instead (`interactive-widget=resizes-content`, see app.html.twig), reports an inset of 0 here, and needs none of this; that is why the same window looked right there and wrong on iOS. Unlayered on purpose, same as the block below: the first rule stands in for the `bottom-4` the dock wrapper used to carry and the second has to beat the `max-h-[90vh]` it still does, and unlayered declarations outrank anything in Tailwind's cascade layers. ═══════════════════════════════════════════════════════════════════════════ */ .compose-dock { /* What the dock actually moves by, which is not always the whole keyboard. There has to be somewhere to move TO: at 20rem the card is down to its own chrome plus a few lines of message, and lifting it any further only trades a covered send row for a header above the top of the screen. A phone held sideways is the case that needs the clamp, and it is not a corner: an iPhone 15 in landscape is 844x390, a Pro Max 926x428, a mini 780x360 — all of them WIDER than md, so all of them get the dock card rather than the fullscreen window, on 390-ish pixels of screen with a 200px keyboard on it. Uncapped, the lift puts the card's header off the top and the cap below squeezes the scroller — From, To, Subject and the message, which share one scroll region — to nothing. 22rem is the 20rem the card is allowed to keep plus the 2rem of dock margin below it (`bottom-4` here, `mb-4` on the card). Declared HERE and not on :root, which belongs to the theme palette and to the appearance knobs and holds nothing else — ThemeVariableCompleteness Test enforces that, and it is right to. The expanded window reads this through inheritance: it is `position: fixed`, but it is still a DOM descendant of the dock. */ --keyboard-lift: min(var(--keyboard-inset, 0px), max(0px, 100dvh - 22rem)); bottom: calc(1rem + var(--keyboard-lift, 0px)); } /* Moving the dock up is only half of it: the card is 600px tall, capped at 90vh, and 90vh is measured against the layout viewport — so the cap says nothing about the room left ABOVE a keyboard, and a card lifted clear of one would run off the top of the screen instead. Capping against what is left makes it shrink rather than climb. `max()` and not a bare `calc()`, for the same reason the lift is clamped: on a short screen 90vh less the keyboard falls under the card's own furniture, and a box shorter than its `shrink-0` children does not shrink them — it spills them out through the bottom edge, past the painted card and back under the keyboard, with the scroller in between collapsed to 0px. The floor is what stops the fix from breaking the case it cannot fix. Above md only. Below it the window is fullscreen and takes its height from `visualViewport` directly, and `max-md:max-h-none` has to keep winning. */ @media (min-width: 768px) { .compose-dock .compose-window { max-height: max(20rem, 90vh - var(--keyboard-inset, 0px)); } } /* ═══ Shell chrome ═════════════════════════════════════════════════════════ Unlayered on purpose (same reason as the channel block at the top): these rules have to beat the `pane` utility, and unlayered declarations outrank anything in Tailwind's cascade layers. `shell-chrome` marks the topbar and the inline sidebar — furniture, as opposed to the main content pane. The `flat` layout drops their card so they sit straight on the app background, Gmail-style, leaving `main-pane` as the only box. The mobile drawer never carries the marker: it floats over a dim backdrop and would be unreadable transparent. ═══════════════════════════════════════════════════════════════════════════ */ .layout-flat .shell-chrome { background-color: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; border-color: transparent; box-shadow: none; } /* ═══ Collapsed sidebar (icon rail) ════════════════════════════════════════ Toggled by sidebar_drawer_controller and pre-applied from localStorage in app.html.twig. Only `#sidebar` is targeted, which is the inline copy — the drawer copy is `#sidebar-drawer-inner`, so the rail can never affect it. Below md the class is inert; the burger opens the drawer instead. ═══════════════════════════════════════════════════════════════════════════ */ @media (min-width: 768px) { .sidebar-rail #sidebar { width: 3.5rem; } .sidebar-rail #sidebar .rail-hide { display: none; } /* Nothing left but the icon — centre it, and drop the padding that was there to make room for the label. */ .sidebar-rail #sidebar nav { padding-inline: 0.5rem; } .sidebar-rail #sidebar nav > a, .sidebar-rail #sidebar nav > a > span { justify-content: center; padding-inline: 0; } /* Compose loses its label, so its height has to come down with it — a tall narrow slab reads as a broken button. Wider than high, never taller. */ .sidebar-rail #sidebar .compose-wrap { padding-inline: 0.5rem; } .sidebar-rail #sidebar .compose-link { padding-block: 0; padding-inline: 0; height: 2.25rem; } /* No label to run into: the pill stays a centred, evenly-rounded square rather than bleeding off the left edge. */ .sidebar-rail #sidebar .nav-item { margin-inline-start: 0; padding-inline-start: 0; border-radius: 0.5rem; } /* The rows that carry an unread badge are a