mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 10:04:18 +00:00
**The mobile double scrollbar.** `app.css` sets `html, body { height: 100dvh }` because mobile
browsers measure `100vh` against the viewport with the URL bar HIDDEN, and its comment says the
shells ask for `h-full`. Three did not: the mailbox, settings and admin shells were `h-screen`, so
the shell was taller than the body containing it, the body scrolled to make room, and the page grew
a second scrollbar beside the message list's own. The contract was already written down; these were
the three places not honouring it.
**The demo bar covered the last row.** It is `fixed` on purpose — a banner in the flow costs every
page a strip of reading height — but the cost is that the last row sits behind it, which on a phone
is the row you were reaching for. Scrollers get its height back as clearance.
**The Label-as menu was sliced off inside a thread.** The reading pane is `overflow-hidden` and the
panel was `absolute`, so it was cut at the pane's edge with no way to scroll to the rest — the thing
clipping it is not the thing that scrolls.
`position: fixed` does NOT fix this, which is the part worth recording: a fixed element is
positioned against the nearest ancestor carrying a transform, filter or backdrop-filter, and the
boxed layout gives the panes a backdrop blur. Measured rather than assumed — the panel reported
`top: 125px` inline and rendered at 210, ending eighty pixels below a 420px viewport. So it stays
where it is and is sized to the box that clips it, flipping above the button when there is more room
there.
**More demo data, from using the demo.** There was HTML mail, but List-Unsubscribe correctly files
it under Promotions, so the only formatted mail sat behind a tab nobody clicks and the reading pane
looked as though it could render nothing but plain text. There is now one from a person, in Primary.
And there was no conversation at all — every thread a singleton, which leaves the reading pane's
whole argument with nothing to demonstrate on.
Threading that conversation had to be done one turn at a time. MessageThreader resolves a reply's
parent with a SQL query that joins message.thread, so a parent created earlier in the same UNFLUSHED
batch is invisible to it and all four turns looked like conversation openers. The code already says
as much where Gmail meets the same wall from the other side.
**Tests that survive the fixture growing** — CODESTYLE 9.5, and three specs brought in line. An
assertion that must be edited every time the seeded mailbox gains a thread is asserting that number
rather than the behaviour, and it fails in a spec whose subject has nothing to do with the change.
Baselines where there is a "before", a relation where there is not: every message is its own thread
EXCEPT the conversation, whose turns collapse.
Proved by mutation rather than by reading: seeding a fifth thread and re-running found a third
brittle spec I had not spotted — unread-view's hard-coded "3" — which now takes the badge's own
starting value. Absence checks and own-fixture counts are left alone, and the rule says why.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4033 lines
153 KiB
CSS
4033 lines
153 KiB
CSS
@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) / <alpha>).
|
||
|
||
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 <html>, 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 <html> 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
|
||
<html>; 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 <html> by AppearanceRenderer. */
|
||
--rgb-main: var(--rgb-surface);
|
||
--main-alpha: var(--pane-alpha);
|
||
--pane-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 <html>
|
||
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; --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 <a href> 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;
|
||
}
|
||
|
||
/* ── Mobile viewport ───────────────────────────────────────────────────
|
||
The meta tag pins the scale, but iOS ignores user-scalable and zooms
|
||
in whenever a focused control renders below 16px — which every
|
||
text-sm/text-xs field here does. Setting the *computed* size to 16px
|
||
on small screens is the only thing that actually suppresses it.
|
||
────────────────────────────────────────────────────────────────────── */
|
||
@media (max-width: 767px) {
|
||
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
|
||
select,
|
||
textarea {
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
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.
|
||
*/
|
||
@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));
|
||
}
|
||
|
||
@utility pane-flat {
|
||
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);
|
||
}
|
||
|
||
/*
|
||
A menu that floats over content. Unlike pane, the surface is fully opaque:
|
||
a translucent dropdown lets whatever it covers — toolbar icons, a wall of
|
||
photos — read straight through the labels, which is illegible rather than
|
||
airy. The blur stays for the edges, and the shadow does the lifting.
|
||
*/
|
||
@utility popover {
|
||
background-color: rgb(var(--rgb-surface));
|
||
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 native <select> that looks like the rest of the app's fields.
|
||
*
|
||
* The browser's own control is the thing that gave it away: its chevron, its
|
||
* corner radius and its metrics come from the platform, so a settings pane full
|
||
* of app-styled inputs had one foreign-looking box in the middle of it. There
|
||
* were six different attempts at fixing that scattered through templates/ — two
|
||
* borders, three backgrounds, and three that reached for a hardcoded
|
||
* `ring-indigo-500` and so ignored the accent colour the user picked.
|
||
*
|
||
* It is no longer the whole answer, because it could never reach the popup:
|
||
* ui--select puts a Tom Select over every single-select in the app. This is
|
||
* still what renders — before that script has run, and instead of it when the
|
||
* user has no JavaScript — so the two have to agree, and every value below is
|
||
* mirrored by `.ts-wrapper.single .ts-control` in assets/styles/tom-select.css.
|
||
* Change one and change the other, or the control moves when the page finishes
|
||
* loading. The multiple/size branch at the bottom is the one part with no
|
||
* counterpart: list boxes are left native on purpose.
|
||
*
|
||
* `appearance: none` removes the platform control and the chevron is drawn back
|
||
* on as a background image, which is the one part a <select> 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));
|
||
}
|
||
@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;
|
||
}
|
||
}
|
||
|
||
/* ═══ 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;
|
||
}
|
||
}
|
||
|
||
/* Nav rows, Gmail-style: the pill runs off the left edge and caps with a full
|
||
radius on the right. Every row carries the shape at all times and only the
|
||
colour arrives with hover or .is-active — which is the fix for the highlight
|
||
appearing to "morph into a square bar" on the way out. It was the shape
|
||
snapping back the instant the pointer left while the background was still
|
||
fading through its colour transition, so the tail of every hover was a
|
||
square. Geometry that never changes cannot flash.
|
||
|
||
The row owns its inline padding here rather than through px-3 because a
|
||
label adds its tree depth on top, and the negative margin has to cancel the
|
||
nav's own px-2 without moving the icon: -0.5rem out, +0.5rem back into the
|
||
padding, so text sits exactly where it did.
|
||
|
||
Custom labels are the same rows: the pill hangs off .nav-item, not off the
|
||
anchor, so a nested label gets the identical shape and the sidebar
|
||
controller tints the row rather than the text inside it. */
|
||
#sidebar .nav-item,
|
||
#sidebar-drawer-inner .nav-item {
|
||
margin-inline-start: -0.5rem;
|
||
padding-inline-start: calc(1.25rem + var(--label-depth, 0px));
|
||
padding-inline-end: 0.75rem;
|
||
border-radius: 0 9999px 9999px 0;
|
||
}
|
||
|
||
/* ── Sidebar density ───────────────────────────────────────────────────────
|
||
What the sidebar's density setting actually moves.
|
||
|
||
Every row here declares its TIER rather than its padding. There are two, and
|
||
they were two before this existed: a system row is `py-2` and anything one
|
||
step in — the label tree at every depth, an account's folder list — is
|
||
`py-1.5`. Density scales each tier from its own value, so Comfortable is the
|
||
geometry the file already had, to the pixel, and the two tiers stay visibly
|
||
different at all three steps. See Density::rowPadding().
|
||
|
||
Stated as two attributes rather than one rule plus an override, so nothing
|
||
here depends on source order or on out-specifying a Tailwind utility.
|
||
|
||
Line box is 20px (`text-sm`), so:
|
||
row 36 / 32 / 28px
|
||
tree 32 / 28 / 24px */
|
||
#sidebar [data-nav-tier="row"],
|
||
#sidebar-drawer-inner [data-nav-tier="row"] {
|
||
padding-block: var(--surface-sidebar-row-y);
|
||
}
|
||
|
||
#sidebar [data-nav-tier="tree"],
|
||
#sidebar-drawer-inner [data-nav-tier="tree"] {
|
||
padding-block: var(--surface-sidebar-tree-y);
|
||
}
|
||
|
||
/* The space BETWEEN rows, and above a section heading. `space-y-0.5` and
|
||
`pt-4` are what these were, and Comfortable is both of those exactly. */
|
||
@utility nav-rows {
|
||
& > * + * {
|
||
margin-block-start: var(--surface-sidebar-row-gap);
|
||
}
|
||
}
|
||
|
||
@utility nav-section-y {
|
||
padding-block-start: var(--surface-sidebar-section-y);
|
||
}
|
||
|
||
/* A floor for fingers.
|
||
|
||
Compact's 28px row is a deliberate choice on a desktop and a bad one on a
|
||
phone, where the sidebar is the drawer and every row is a tap target. The
|
||
44px guideline is the one worth naming here, and the honest position is that
|
||
this sidebar has never met it: a Comfortable row is 36px and always has
|
||
been. So this does not invent a new size — it stops the two tighter steps
|
||
going BELOW the size the app already ships at Comfortable, per tier. A phone
|
||
therefore sees no change at Comfortable (the case that must not move) and
|
||
never loses target area by choosing Compact.
|
||
|
||
`pointer: coarse` rather than a width query: a small window on a desktop is
|
||
still a mouse, and it is the input device that decides how big a target has
|
||
to be.
|
||
|
||
The floor is `1lh` plus Comfortable's own padding rather than a pixel
|
||
number, so it IS that row's Comfortable height whatever type size the row is
|
||
set in — a system row at `text-sm` and an account's folder row at `text-xs`
|
||
are 36px and 28px, and a single literal would have grown one of them. */
|
||
@media (pointer: coarse) {
|
||
#sidebar [data-nav-tier="row"],
|
||
#sidebar-drawer-inner [data-nav-tier="row"] {
|
||
min-height: calc(1lh + 1rem);
|
||
}
|
||
|
||
#sidebar [data-nav-tier="tree"],
|
||
#sidebar-drawer-inner [data-nav-tier="tree"] {
|
||
min-height: calc(1lh + 0.75rem);
|
||
}
|
||
|
||
/* The appearance panel's "show preview" control, held to the SAME floor and
|
||
by the same expression rather than to a pixel number of its own. It is
|
||
drawn at `text-xs` in the settings card header, which puts `1lh + 1rem`
|
||
at 32px — below the 36px the utility class already gives it, so on a
|
||
phone the class wins and this is the guarantee underneath rather than the
|
||
size in force. That is the right way round: whatever the type scale does
|
||
to this button (and the panel it sits in can scale the whole app's type),
|
||
it cannot end up smaller than a Comfortable sidebar row. */
|
||
[data-appearance-preview-toggle] {
|
||
min-height: calc(1lh + 1rem);
|
||
}
|
||
}
|
||
|
||
/* The trail to the selected row: a label's ancestors read as "you are in
|
||
here", not as a second selection, so they take the accent on the text and
|
||
leave the filled pill to the row that is actually open. */
|
||
#sidebar .nav-item.is-active-ancestor:not(.is-active),
|
||
#sidebar-drawer-inner .nav-item.is-active-ancestor:not(.is-active) {
|
||
color: rgb(var(--rgb-accent));
|
||
}
|
||
|
||
/* Flat has no card to respect: the sidebar sits straight against the window,
|
||
so the shell's own left gutter goes too and the pill reaches the browser
|
||
edge. Boxed keeps the gutter — a pill bleeding off a floating card would
|
||
just look broken.
|
||
|
||
Only where there IS a sidebar. Below md it is a drawer, so dropping the
|
||
start padding left nothing to bleed and simply glued the mail pane to the
|
||
left edge while the right kept its gutter — lopsided, and the reason the
|
||
list looked cut off on a phone. */
|
||
@media (min-width: 768px) {
|
||
.layout-flat .shell-frame {
|
||
padding-inline-start: 0;
|
||
}
|
||
}
|
||
|
||
/* ═══ Scrollbars — now token-driven ════════════════════════════════════════ */
|
||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||
::-webkit-scrollbar-track { background: transparent; }
|
||
::-webkit-scrollbar-thumb {
|
||
background-color: rgb(var(--rgb-ink) / 0.2);
|
||
border-radius: 9999px;
|
||
}
|
||
::-webkit-scrollbar-thumb:hover { background-color: rgb(var(--rgb-ink) / 0.35); }
|
||
::-webkit-scrollbar-corner { background: transparent; }
|
||
|
||
* {
|
||
scrollbar-width: thin;
|
||
scrollbar-color: rgb(var(--rgb-ink) / 0.2) transparent;
|
||
}
|
||
|
||
|
||
/* ═══ Live-update indicator ════════════════════════════════════════════════
|
||
A dot beside the wordmark saying whether the Mercure stream is carrying
|
||
updates. It earns permanent space because the failure it reports is
|
||
otherwise invisible: the mail list has no polling fallback while connected,
|
||
so a dead stream looks exactly like a quiet mailbox — the inbox stops
|
||
changing and nothing says why.
|
||
|
||
Sized and toned to be findable rather than noticeable. The healthy state is
|
||
the one it is in essentially always, so it stays quiet; the states worth
|
||
reacting to are the ones with contrast to spend.
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
.stream-dot {
|
||
width: 0.4rem;
|
||
height: 0.4rem;
|
||
border-radius: 9999px;
|
||
flex-shrink: 0;
|
||
/* Until the first state arrives. Neutral, not green: claiming a healthy
|
||
connection before one exists is the one thing this must never do. */
|
||
background-color: rgb(var(--rgb-ink-faint) / 0.4);
|
||
transition: background-color 300ms ease;
|
||
}
|
||
|
||
.stream-dot[data-state="connected"] {
|
||
background-color: rgb(var(--rgb-success) / 0.7);
|
||
}
|
||
|
||
.stream-dot[data-state="connecting"] {
|
||
background-color: rgb(var(--rgb-warning) / 0.9);
|
||
}
|
||
|
||
.stream-dot[data-state="offline"] {
|
||
background-color: rgb(var(--rgb-danger));
|
||
}
|
||
|
||
/* ═══ Application tooltip ══════════════════════════════════════════════════
|
||
The bubble ui--tooltip moves around. One element for the whole app, living
|
||
directly on <body> — see that controller for why it cannot sit next to its
|
||
trigger (backdrop-filter on the panes breaks `position: fixed`).
|
||
|
||
Themed from the same `inverse` tokens the old per-template bubble used, so
|
||
it follows every theme without knowing any of them: dark ink on light in the
|
||
dark themes, light ink on dark in the light ones.
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
.app-tooltip {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
z-index: 100;
|
||
max-width: 22rem;
|
||
padding: 0.4rem 0.625rem;
|
||
border-radius: 0.5rem;
|
||
background-color: rgb(var(--rgb-inverse));
|
||
color: rgb(var(--rgb-inverse-ink));
|
||
box-shadow: 0 4px 12px rgb(var(--rgb-shadow) / 0.18);
|
||
font-size: 12.5px;
|
||
font-weight: 500;
|
||
line-height: 1.4;
|
||
/* Wraps rather than running off: a few of these are whole sentences. */
|
||
white-space: normal;
|
||
/* Never a hover target itself — it sits where the pointer is heading, and
|
||
a tooltip that can be hovered flickers as it steals its own trigger's
|
||
mouseout. */
|
||
pointer-events: none;
|
||
/* `visibility`, not just opacity: an opacity-0 element still counts as
|
||
rendered — it stays in the accessibility tree and, before the first
|
||
hover, this one sits empty at the top-left corner. Hidden means hidden. */
|
||
visibility: hidden;
|
||
opacity: 0;
|
||
transition: opacity 90ms ease;
|
||
}
|
||
|
||
/* Laid out for measurement before it is placed: it has to have a real size to
|
||
be positioned against, but must not be seen at the wrong coordinates first.
|
||
`visibility: hidden` still produces a layout box, which is exactly what is
|
||
needed here — `display: none` would measure as zero. */
|
||
.app-tooltip[data-state="measuring"] {
|
||
visibility: hidden;
|
||
opacity: 0;
|
||
}
|
||
|
||
.app-tooltip[data-state="visible"] {
|
||
visibility: visible;
|
||
opacity: 1;
|
||
}
|
||
|
||
.app-tooltip[data-state="hidden"] {
|
||
visibility: hidden;
|
||
opacity: 0;
|
||
}
|
||
|
||
/* The caret. Positioned from --caret-x, which the controller points at the
|
||
trigger rather than at the middle of the bubble — the two differ whenever a
|
||
viewport edge has pushed the bubble off centre, and a caret aimed at nothing
|
||
looks broken. Border-triangle rather than a rotated square so it takes the
|
||
same `inverse` token as the body and needs no separate theming. */
|
||
.app-tooltip::after {
|
||
content: "";
|
||
position: absolute;
|
||
left: var(--caret-x, 50%);
|
||
transform: translateX(-50%);
|
||
border: 6px solid transparent;
|
||
}
|
||
|
||
.app-tooltip[data-placement="below"]::after {
|
||
bottom: 100%;
|
||
border-bottom-color: rgb(var(--rgb-inverse));
|
||
}
|
||
|
||
.app-tooltip[data-placement="above"]::after {
|
||
top: 100%;
|
||
border-top-color: rgb(var(--rgb-inverse));
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.app-tooltip {
|
||
transition: none;
|
||
}
|
||
}
|
||
|
||
/* ═══ Emoji ════════════════════════════════════════════════════════════════
|
||
Noto Color Emoji (SIL Open Font License 1.1 — assets/fonts/OFL.txt),
|
||
vendored rather than linked: this app makes no runtime request to a CDN,
|
||
and a webfont served by a third party is a request on every page load that
|
||
also reports who made it.
|
||
|
||
Why ship glyphs at all. Emoji are the one part of a message whose
|
||
appearance is decided entirely by the reader's operating system: the same
|
||
character is a rounded Noto face on Android, a flat Segoe outline on
|
||
Windows, something else again elsewhere. In a picker that is worse than a
|
||
cosmetic difference — the grid someone is choosing from stops matching the
|
||
phone keyboard they know it by. Pinning one family makes the picker and the
|
||
body agree with each other, and on Android with the keyboard as well.
|
||
|
||
The 2 MB is not paid up front. These are the ten unicode-range slices
|
||
Google Fonts itself serves, so a body with three smileys in it fetches the
|
||
one slice those live in and nothing else; the 700 KB flag subset arrives
|
||
only if a flag is actually shown.
|
||
|
||
COLRv1 throughout — vector, so it scales and tints like a font rather than
|
||
blurring like the CBDT bitmap builds.
|
||
═══════════════════════════════════════════════════════════════════════ */
|
||
|
||
@font-face {
|
||
font-family: 'Noto Color Emoji';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
font-display: swap;
|
||
src: url('../fonts/noto-color-emoji.0.woff2') format('woff2');
|
||
unicode-range: U+1f1e6-1f1ff;
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'Noto Color Emoji';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
font-display: swap;
|
||
src: url('../fonts/noto-color-emoji.1.woff2') format('woff2');
|
||
unicode-range: U+200d, U+2620, U+26a7, U+fe0f, U+1f308, U+1f38c, U+1f3c1, U+1f3f3-1f3f4, U+1f6a9, U+e0062-e0063, U+e0065, U+e0067, U+e006c, U+e006e, U+e0073-e0074, U+e0077, U+e007f;
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'Noto Color Emoji';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
font-display: swap;
|
||
src: url('../fonts/noto-color-emoji.2.woff2') format('woff2');
|
||
unicode-range: U+23, U+2a, U+30-39, U+a9, U+ae, U+200d, U+203c, U+2049, U+20e3, U+2122, U+2139, U+2194-2199, U+21a9-21aa, U+23cf, U+23e9-23ef, U+23f8-23fa, U+24c2, U+25aa-25ab, U+25b6, U+25c0, U+25fb-25fe, U+2611, U+2622-2623, U+2626, U+262a, U+262e-262f, U+2638, U+2640, U+2642, U+2648-2653, U+2660, U+2663, U+2665-2666, U+2668, U+267b, U+267e-267f, U+2695, U+269b-269c, U+26a0, U+26a7, U+26aa-26ab, U+26ce, U+26d4, U+2705, U+2714, U+2716, U+271d, U+2721, U+2733-2734, U+2747, U+274c, U+274e, U+2753-2755, U+2757, U+2764, U+2795-2797, U+27a1, U+27b0, U+27bf, U+2934-2935, U+2b05-2b07, U+2b1b-2b1c, U+2b55, U+3030, U+303d, U+3297, U+3299, U+fe0f, U+1f170-1f171, U+1f17e-1f17f, U+1f18e, U+1f191-1f19a, U+1f201-1f202, U+1f21a, U+1f22f, U+1f232-1f23a, U+1f250-1f251, U+1f310, U+1f3a6, U+1f3b5-1f3b6, U+1f3bc, U+1f3e7, U+1f441, U+1f499-1f49c, U+1f49f-1f4a0, U+1f4ac-1f4ad, U+1f4b1-1f4b2, U+1f4b9, U+1f4db, U+1f4f2-1f4f6, U+1f500-1f507, U+1f515, U+1f518-1f524, U+1f52f-1f53d, U+1f549, U+1f54e, U+1f5a4, U+1f5e8, U+1f5ef, U+1f6ab, U+1f6ad-1f6b1, U+1f6b3, U+1f6b7-1f6bc, U+1f6be, U+1f6c2-1f6c5, U+1f6d0-1f6d1, U+1f6d7, U+1f6dc, U+1f7e0-1f7eb, U+1f7f0, U+1f90d-1f90e, U+1f9d1-1f9d2, U+1f9e1, U+1fa75-1fa77, U+1faaf;
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'Noto Color Emoji';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
font-display: swap;
|
||
src: url('../fonts/noto-color-emoji.3.woff2') format('woff2');
|
||
unicode-range: U+200d, U+231a-231b, U+2328, U+23f0-23f3, U+2602, U+260e, U+2692, U+2694, U+2696-2697, U+2699, U+26b0-26b1, U+26cf, U+26d1, U+26d3, U+2702, U+2709, U+270f, U+2712, U+fe0f, U+1f302, U+1f321, U+1f392-1f393, U+1f3a9, U+1f3bd, U+1f3ee, U+1f3f7, U+1f3fa, U+1f451-1f462, U+1f484, U+1f489-1f48a, U+1f48c-1f48e, U+1f4a1, U+1f4a3, U+1f4a5, U+1f4b0, U+1f4b3-1f4b8, U+1f4bb-1f4da, U+1f4dc-1f4f1, U+1f4ff, U+1f508-1f514, U+1f516-1f517, U+1f526-1f529, U+1f52c-1f52e, U+1f550-1f567, U+1f56f-1f570, U+1f576, U+1f587, U+1f58a-1f58d, U+1f5a5, U+1f5a8, U+1f5b1-1f5b2, U+1f5c2-1f5c4, U+1f5d1-1f5d3, U+1f5dc-1f5de, U+1f5e1, U+1f5f3, U+1f6aa, U+1f6ac, U+1f6bd, U+1f6bf, U+1f6c1, U+1f6cb, U+1f6cd-1f6cf, U+1f6d2, U+1f6e0-1f6e1, U+1f6f0, U+1f97b-1f97f, U+1f9af, U+1f9ba, U+1f9e2-1f9e6, U+1f9ea-1f9ec, U+1f9ee-1f9f4, U+1f9f7-1f9ff, U+1fa71-1fa74, U+1fa79-1fa7b, U+1fa86, U+1fa8e-1fa8f, U+1fa91-1fa93, U+1fa96, U+1fa99-1faa0, U+1faa2-1faa7, U+1faaa-1faae;
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'Noto Color Emoji';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
font-display: swap;
|
||
src: url('../fonts/noto-color-emoji.4.woff2') format('woff2');
|
||
unicode-range: U+265f, U+26bd-26be, U+26f3, U+26f8, U+fe0f, U+1f004, U+1f0cf, U+1f380-1f384, U+1f386-1f38b, U+1f38d-1f391, U+1f396-1f397, U+1f399-1f39b, U+1f39e-1f39f, U+1f3a3-1f3a5, U+1f3a7-1f3a9, U+1f3ab-1f3b4, U+1f3b7-1f3bb, U+1f3bd-1f3c0, U+1f3c5-1f3c6, U+1f3c8-1f3c9, U+1f3cf-1f3d3, U+1f3f8-1f3f9, U+1f47e, U+1f4e2, U+1f4f7-1f4fd, U+1f52b, U+1f579, U+1f58c-1f58d, U+1f5bc, U+1f6f7, U+1f6f9, U+1f6fc, U+1f93f, U+1f941, U+1f945, U+1f947-1f94f, U+1f9e7-1f9e9, U+1f9f5-1f9f6, U+1fa70-1fa71, U+1fa80-1fa81, U+1fa83-1fa85, U+1fa87-1fa8a, U+1fa94-1fa95, U+1fa97-1fa98, U+1faa1, U+1faa9, U+1fadf;
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'Noto Color Emoji';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
font-display: swap;
|
||
src: url('../fonts/noto-color-emoji.5.woff2') format('woff2');
|
||
unicode-range: U+2693, U+26e9-26ea, U+26f1-26f2, U+26f4-26f5, U+26fa, U+26fd, U+2708, U+fe0f, U+1f301, U+1f303, U+1f306-1f307, U+1f309, U+1f310, U+1f3a0-1f3a2, U+1f3aa, U+1f3cd-1f3ce, U+1f3d5, U+1f3d7-1f3db, U+1f3df-1f3e6, U+1f3e8-1f3ed, U+1f3ef-1f3f0, U+1f488, U+1f492, U+1f4ba, U+1f54b-1f54d, U+1f5fa-1f5ff, U+1f680-1f6a2, U+1f6a4-1f6a8, U+1f6b2, U+1f6d1, U+1f6d5-1f6d6, U+1f6dd-1f6df, U+1f6e2-1f6e5, U+1f6e9, U+1f6eb-1f6ec, U+1f6f3-1f6f6, U+1f6f8, U+1f6fa-1f6fb, U+1f9bc-1f9bd, U+1f9ed, U+1f9f3, U+1fa7c;
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'Noto Color Emoji';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
font-display: swap;
|
||
src: url('../fonts/noto-color-emoji.6.woff2') format('woff2');
|
||
unicode-range: U+200d, U+2615, U+fe0f, U+1f32d-1f330, U+1f336, U+1f33d, U+1f344-1f37f, U+1f382, U+1f52a, U+1f7e9, U+1f7eb, U+1f942-1f944, U+1f950-1f96f, U+1f99e, U+1f9aa, U+1f9c0-1f9cb, U+1fad0-1fadc;
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'Noto Color Emoji';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
font-display: swap;
|
||
src: url('../fonts/noto-color-emoji.7.woff2') format('woff2');
|
||
unicode-range: U+200d, U+2600-2601, U+2603-2604, U+2614, U+2618, U+26a1, U+26c4-26c5, U+26c8, U+26f0, U+2728, U+2744, U+2b1b, U+2b50, U+fe0f, U+1f300, U+1f304-1f305, U+1f308, U+1f30a-1f30f, U+1f311-1f321, U+1f324-1f32c, U+1f331-1f335, U+1f337-1f33c, U+1f33e-1f344, U+1f3d4, U+1f3d6, U+1f3dc-1f3de, U+1f3f5, U+1f400-1f43f, U+1f490, U+1f4a7, U+1f4ae, U+1f525, U+1f54a, U+1f573, U+1f577-1f578, U+1f648-1f64a, U+1f6d8, U+1f940, U+1f980-1f9ae, U+1f9ba, U+1fa90, U+1faa8, U+1fab0-1fabf, U+1facd-1facf, U+1fae7;
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'Noto Color Emoji';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
font-display: swap;
|
||
src: url('../fonts/noto-color-emoji.8.woff2') format('woff2');
|
||
unicode-range: U+200d, U+2640, U+2642, U+2695-2696, U+26f7, U+26f9, U+2708, U+2764, U+27a1, U+fe0f, U+1f33e, U+1f373, U+1f37c, U+1f384-1f385, U+1f393, U+1f3a4, U+1f3a8, U+1f3c2-1f3c4, U+1f3c7, U+1f3ca-1f3cc, U+1f3eb, U+1f3ed, U+1f3fb-1f3ff, U+1f430, U+1f466-1f469, U+1f46b-1f478, U+1f47c, U+1f481-1f483, U+1f486-1f487, U+1f48b, U+1f48f, U+1f491, U+1f4bb-1f4bc, U+1f527, U+1f52c, U+1f574-1f575, U+1f57a, U+1f645-1f647, U+1f64b, U+1f64d-1f64e, U+1f680, U+1f692, U+1f6a3, U+1f6b4-1f6b6, U+1f6c0, U+1f6cc, U+1f91d, U+1f926, U+1f930-1f931, U+1f934-1f93a, U+1f93c-1f93e, U+1f977, U+1f9af-1f9b3, U+1f9b8-1f9b9, U+1f9bc-1f9bd, U+1f9cc-1f9cf, U+1f9d1-1f9df, U+1fa70, U+1fa82, U+1fac3-1fac5, U+1fac8, U+1faef;
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'Noto Color Emoji';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
font-display: swap;
|
||
src: url('../fonts/noto-color-emoji.9.woff2') format('woff2');
|
||
unicode-range: U+200d, U+2194-2195, U+2603, U+261d, U+2620, U+2639-263a, U+2665, U+26a1, U+26c4, U+270a-270d, U+2728, U+2763-2764, U+2b50, U+fe0f, U+1f31a-1f31f, U+1f32b, U+1f389-1f38a, U+1f3fb-1f3ff, U+1f440-1f450, U+1f463-1f465, U+1f479-1f47b, U+1f47d-1f480, U+1f485, U+1f48b-1f48c, U+1f493-1f49f, U+1f4a2, U+1f4a4-1f4a6, U+1f4a8-1f4ab, U+1f4af, U+1f525, U+1f573, U+1f590, U+1f595-1f596, U+1f5a4, U+1f5e3, U+1f600-1f644, U+1f648-1f64a, U+1f64c, U+1f64f, U+1f90c-1f925, U+1f927-1f92f, U+1f932-1f933, U+1f970-1f976, U+1f978-1f97a, U+1f9a0, U+1f9b4-1f9b7, U+1f9bb, U+1f9be-1f9bf, U+1f9d0, U+1f9e0-1f9e1, U+1fa75-1fa79, U+1fac0-1fac2, U+1fac6, U+1fae0-1fae6, U+1fae8-1faea, U+1faef-1faf8;
|
||
}
|
||
|
||
/* Scoped, not global. The family covers emoji code points only, so naming it
|
||
first is harmless anywhere — but it is only *wanted* where emoji are the
|
||
point: the picker, and the body being written. Everything else on the page
|
||
keeps whatever the reader's system draws, which is what the rest of the app
|
||
has always looked like.
|
||
|
||
The picker's glyphs live inside its shadow DOM, where a selector cannot
|
||
reach. --emoji-font-family is the component's own hook for exactly this and
|
||
inherits across the boundary; the rest of the custom properties below are
|
||
how it takes a colour scheme, so the panel follows the active theme instead
|
||
of being a white rectangle in a dark window. */
|
||
emoji-picker {
|
||
--emoji-font-family: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
|
||
|
||
/* THE CHANNELS SPENT THROUGH THEIR ALPHAS, which is the only correct way
|
||
to read them, and the "weird black borders" in the report were what
|
||
happens otherwise: --rgb-line is `0 0 0` in every light theme and
|
||
`255 255 255` in every dark one, because it carries a hue and leaves the
|
||
weight to --line-alpha (0.06–0.1). Written as rgb(var(--rgb-line)) with
|
||
no alpha it resolved to PURE BLACK — around the panel, around the search
|
||
field, and between the category tabs — in exactly the themes where a
|
||
hairline was meant. The pairs below are the same ones --color-line,
|
||
--color-hover and --color-sunken are built from, so the picker's edges
|
||
are now drawn with the values every other edge in the app uses. */
|
||
--background: rgb(var(--rgb-surface));
|
||
--border-color: rgb(var(--rgb-line) / var(--line-alpha));
|
||
--input-border-color: rgb(var(--rgb-line) / var(--line-alpha));
|
||
--input-font-color: rgb(var(--rgb-ink));
|
||
--input-placeholder-color: rgb(var(--rgb-ink-faint));
|
||
--category-font-color: rgb(var(--rgb-ink-muted));
|
||
--indicator-color: rgb(var(--rgb-accent));
|
||
--button-active-background: rgb(var(--rgb-raised) / var(--raised-alpha));
|
||
--button-hover-background: rgb(var(--rgb-raised) / var(--hover-alpha));
|
||
--custom-emoji-background: rgb(var(--rgb-sunken) / var(--sunken-alpha));
|
||
--outline-color: rgb(var(--rgb-accent));
|
||
--border-radius: var(--app-radius);
|
||
|
||
/* The second half of the same bug, and the half CSS variables could not
|
||
fix. emoji-picker-element declares `color-scheme` on its own :host from
|
||
`prefers-color-scheme` — the OPERATING SYSTEM's setting, which this app
|
||
does not use for anything: the theme comes from the user's account and
|
||
:root declares color-scheme per theme block to match. On a dark desktop
|
||
running the app in a light theme the picker therefore asked the browser
|
||
for dark UA widgets, and got a black search field and a black scrollbar
|
||
inside a white panel.
|
||
|
||
`inherit` hands it back to the theme. Declarations from the document
|
||
tree beat a shadow tree's :host for the same element, so this wins
|
||
without !important — and because it inherits rather than naming a
|
||
scheme, the runtime theme switch (which rewrites data-theme on <html>
|
||
and re-resolves :root) carries the picker with it. */
|
||
color-scheme: inherit;
|
||
|
||
/* The other half of taming the scrollbar, and the half that was missed.
|
||
`scrollbar-color` was set on an ancestor and reached the grid inside the
|
||
shadow root on its own — both scrollbar properties INHERIT, which is what
|
||
lets a page style a scroller it cannot select. `scrollbar-width` was never
|
||
declared, so the grid kept `auto`: a 15px native bar with stepper arrows
|
||
on it, inside a panel where every other scroller in the app is a thin
|
||
overlay. Themed the right colour and the wrong size is a worse tell than
|
||
leaving it alone. */
|
||
scrollbar-width: thin;
|
||
|
||
display: block;
|
||
width: 100%;
|
||
|
||
/* `--emoji-max-height` is set by compose--emoji-picker from the room the
|
||
compose WINDOW actually has above the toolbar. 50vh alone measured the
|
||
viewport, which knows nothing about where the window's top edge is, and
|
||
put the panel 33px outside the window it belongs to. The fallback keeps
|
||
the old behaviour for any host that never sets it (the settings signature
|
||
editor renders this toolbar too). */
|
||
max-height: min(24rem, 50vh, var(--emoji-max-height, 100vh));
|
||
}
|
||
|
||
/* The panel is centred over the compose WINDOW at every width — it anchors to
|
||
the toolbar block, not to its button, which sits near the right edge of a
|
||
520px dock and would push a 22rem panel off the side of it. The width comes
|
||
from the template (`calc(100% - 1rem)`); this only caps it, so a panel
|
||
centred in a 1000px expanded window is still a panel and not a banner. */
|
||
.emoji-panel {
|
||
max-width: 22rem;
|
||
}
|
||
|
||
@media (max-width: 767px) {
|
||
/* Released below md: the window IS the screen there, so the full
|
||
`calc(100% - 1rem)` is the right width and the cap would leave a
|
||
352px panel floating in a 393px window. */
|
||
.emoji-panel {
|
||
max-width: none;
|
||
}
|
||
|
||
/* Room for the panel AND the toolbar under it on a short phone. `bottom-20`
|
||
puts its foot at 5rem; 50vh of an 851px viewport is 425px, which together
|
||
overflow the top of a 667px iPhone SE. Measured against the viewport
|
||
rather than assumed — compose-mobile.spec.ts asserts the box is inside
|
||
it at 393px and at 320px. */
|
||
emoji-picker {
|
||
max-height: min(20rem, calc(100dvh - 12rem), var(--emoji-max-height, 100vh));
|
||
}
|
||
}
|
||
|
||
/* The body being written. Only emoji code points resolve here; every other
|
||
character falls through to the next family, so the message keeps the
|
||
editor's own typeface. */
|
||
[data-compose--compose-toolbar-target="editor"] {
|
||
font-family: 'Noto Color Emoji', ui-sans-serif, system-ui, sans-serif;
|
||
}
|
||
|
||
/* ── The confirmation dialog ───────────────────────────────────────────────
|
||
The box and the dim behind templates/_partials/_confirm_dialog.html.twig.
|
||
|
||
Here rather than in the class list on the element, because none of it is
|
||
something a utility can say.
|
||
|
||
The geometry first. A <dialog> is centred by the UA stylesheet with
|
||
`margin: auto` against `inset: 0`, and Tailwind's preflight zeroes the margin
|
||
of every element in the document — including this one. What is left is a
|
||
dialog pinned to the top-left corner, sized to fit its content, which is
|
||
exactly what it looked like: a browser artefact sitting on the page rather
|
||
than a panel in the app. `m-auto` in the template would have put it back, but
|
||
it would also have read as decoration rather than as the repair of a reset,
|
||
and the next person to tidy the class list would have removed it.
|
||
|
||
Then the dim. `::backdrop` is painted by the browser and belongs to no parent
|
||
in the document, so the modal's `bg-black/50 backdrop-blur-sm` cannot simply
|
||
be repeated on it: Tailwind's blur utilities are assembled from custom
|
||
properties, and a browser that does not inherit those into ::backdrop (before
|
||
Chrome 122 none did) would paint the dim without the blur. Both values are
|
||
therefore written out, and they are the modal's own two numbers so the two
|
||
surfaces dim the page by the same amount.
|
||
|
||
NOT --scrim-alpha and --pane-blur. Those are the user's wallpaper knobs — the
|
||
scrim is 0 by default, so a dialog dimming the page with it would frequently
|
||
not dim the page at all. What a modal does to the page behind it is the app's
|
||
decision, not a preference.
|
||
───────────────────────────────────────────────────────────────────────── */
|
||
#confirm-dialog {
|
||
position: fixed;
|
||
inset: 0;
|
||
margin: auto;
|
||
|
||
/* Never edge to edge: 2rem of gutter is what keeps it a panel at 390px
|
||
instead of a bar across the phone. */
|
||
width: min(28rem, calc(100vw - 2rem));
|
||
max-height: calc(100dvh - 2rem);
|
||
|
||
/* The form owns the padding, so the surface can round without the corners
|
||
cutting into it. Scrolls rather than overflows, for the two or three
|
||
questions long enough to need it on a short window. */
|
||
padding: 0;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
#confirm-dialog::backdrop {
|
||
background-color: rgb(0 0 0 / 0.5);
|
||
backdrop-filter: blur(4px);
|
||
-webkit-backdrop-filter: blur(4px);
|
||
|
||
/* The dim comes up with the panel rather than snapping on. Deliberately
|
||
without a fallback: on a browser that does not inherit the motion tokens
|
||
into ::backdrop the duration is invalid and resolves to 0s, so the dim
|
||
simply appears — which is the same thing reduced motion asks for, and the
|
||
right way round to fail. */
|
||
animation-name: plmail-fade;
|
||
animation-duration: var(--motion-base);
|
||
animation-timing-function: var(--motion-ease);
|
||
}
|
||
|
||
/* ═══ Appearance: typography, the mail list, and per-surface density ════════
|
||
Everything below is driven by custom properties AppearanceRenderer writes
|
||
inline on <html>. That is not a decoration of the existing scheme, it IS the
|
||
scheme: the appearance already travels as an inline `style` block, the
|
||
logged-out replay in _layout/app.html.twig already restores it verbatim, and
|
||
the settings pane's live preview already works by setting the same
|
||
properties. A feature expressed as a variable inherits all three for free; a
|
||
feature expressed as a class on <html> would have needed a fourth path
|
||
through each of them.
|
||
|
||
These rules are deliberately UNLAYERED, so they win over the Tailwind
|
||
utilities (which live in @layer utilities) they have to override —
|
||
`truncate`'s white-space above all. Layer order, not specificity, decides
|
||
that, which is why none of this needs !important.
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
|
||
/* ── Typography ────────────────────────────────────────────────────────────
|
||
The scale multiplies the root, so every rem-based size in the app — which is
|
||
every Tailwind text, spacing and radius utility — moves together. Scaling
|
||
the utilities individually would mean enumerating them, and missing one
|
||
shows up as a 13px label beside a 17px one.
|
||
|
||
`font-size` on <html> and not on <body>: `rem` is defined against the root
|
||
element specifically, so setting it on body scales precisely nothing. */
|
||
html {
|
||
font-size: calc(16px * var(--app-font-scale));
|
||
font-family: var(--app-font-family);
|
||
}
|
||
|
||
/*
|
||
The compose editor is exempt, and this is the one rule here whose job is to
|
||
STOP a setting reaching something.
|
||
|
||
The editor's font size and family are per-message formatting: the toolbar
|
||
above it writes `style="font-family:…"` and `style="font-size:…"` into spans
|
||
that go out with the mail, and the sender's own eye is the only check on
|
||
whether what they wrote looks right. If the interface scale moved the editor
|
||
too, composing at 1.25 would produce mail that arrives 25% smaller than it
|
||
looked while being written, and nothing on screen would ever have said so.
|
||
So the editor's base is stated in px, outside the rem cascade; the toolbar's
|
||
own inline styles still win over it, because they are inline.
|
||
|
||
14px / 1.625 is `text-sm leading-relaxed`, which is what the element already
|
||
carries in the template. This pins that number rather than changing it.
|
||
*/
|
||
[data-compose--compose-toolbar-target="editor"] {
|
||
font-size: 14px;
|
||
line-height: 1.625;
|
||
}
|
||
|
||
/* ── A visually-hidden input must not escape its own label ─────────────────
|
||
`sr-only` is `position: absolute`, and a <label> is `position: static` by
|
||
default — so the containing block of every `peer sr-only` radio was not its
|
||
label but the nearest positioned ancestor, which on a settings page is
|
||
`.main-pane`. Two consequences, and the second one is severe:
|
||
|
||
1. The input's static position — a full page-height down the panel — became
|
||
a contribution to `.main-pane`'s SCROLLABLE OVERFLOW, because the inner
|
||
`overflow-y-auto` is not the input's containing block and therefore does
|
||
not clip it. `.main-pane` measured 2739px of scroll height inside an
|
||
824px box while appearing to have nothing to scroll.
|
||
|
||
2. `.main-pane` is `overflow: hidden`, which is still a scroll container:
|
||
scrollable programmatically, just without a scrollbar. Clicking any
|
||
segmented control focuses its radio, the browser scrolls the focused
|
||
element into view through every scroll container above it, and
|
||
`.main-pane.scrollTop` jumped to 1314. Its in-flow content — the pane
|
||
header, the section nav, the entire settings body — slid up out of the
|
||
clip, and the pane painted as an empty rounded rectangle. That is the
|
||
"toggling density blanks the whole main pane" report: nothing was
|
||
unrendered, everything was scrolled out of a box with no way to scroll
|
||
it back.
|
||
|
||
Making the label the containing block puts the 1px box back where it
|
||
belongs, so it contributes its overflow to the label and focusing it scrolls
|
||
only the pane's real `overflow-y-auto` scroller.
|
||
|
||
Deliberately not scoped to the appearance panel: every `peer sr-only` tile
|
||
in the app sits in the same trap, and the cure has no cost anywhere else.
|
||
|
||
The appearance panel is also an `@container` now, and `container-type`
|
||
happens to establish a containing block for absolute descendants too — so
|
||
that panel would survive without this rule. That is a side effect of a
|
||
layout decision, though, and it would leave every other `sr-only` tile in
|
||
the app still holding the bug. This rule is the one that says so on
|
||
purpose. */
|
||
label:has(> input.sr-only) {
|
||
position: relative;
|
||
}
|
||
|
||
/* ── Per-surface density ───────────────────────────────────────────────────
|
||
Three surfaces re-point the density tokens at their own resolved pair. The
|
||
pairs are resolved server-side rather than through a CSS fallback chain,
|
||
because `--density-row-y: var(--x, var(--density-row-y))` is a cycle on the
|
||
element declaring it, and every other spelling writes the global value out
|
||
twice.
|
||
|
||
The attributes live on the wrappers in _layout/_mailbox.html.twig rather
|
||
than inside the panes, so a pane picks up its own gutter without every
|
||
include having to know it is inside one.
|
||
|
||
The SIDEBAR's rows do not come through here. They are selected by
|
||
`#sidebar`/`#sidebar-drawer-inner` further down, off the variables directly,
|
||
because that file already owns its rows' inline padding for the same reason
|
||
(the pill has to survive a label's tree depth) and a row whose two axes are
|
||
decided in two different places is a row nobody can predict. */
|
||
[data-surface="sidebar"] {
|
||
--density-row-y: var(--surface-sidebar-row-y);
|
||
--density-gap: var(--surface-sidebar-gap);
|
||
}
|
||
|
||
[data-surface="list"] {
|
||
--density-gap: var(--surface-list-gap);
|
||
}
|
||
|
||
[data-surface="reading"] {
|
||
--density-row-y: var(--surface-reading-row-y);
|
||
--density-gap: var(--surface-reading-gap);
|
||
}
|
||
|
||
/* ── Mail list display ─────────────────────────────────────────────────────
|
||
Each control drives a property the row already had, so the row's markup only
|
||
grew hooks — no branch in Twig, no second markup path, and the miniature
|
||
preview in the settings pane is styled by these same rules rather than by a
|
||
copy of them that could drift. */
|
||
|
||
/* The account corner: the coloured triangle in a row's lower-left. */
|
||
[data-account-corner] {
|
||
display: var(--list-corner-display);
|
||
}
|
||
|
||
/*
|
||
The sender disc, switched off.
|
||
|
||
Not `display: none`: the disc IS the row's checkbox — the real input is
|
||
sr-only behind it, and every bulk action in the toolbar reads it — so hiding
|
||
it would take selection away along with the colour. An overlay in the same
|
||
slot covers the tone and the initial and leaves a plain outlined circle
|
||
behind, so the row keeps its geometry and its click target. The check
|
||
overlay is a SIBLING of this element and painted after it, so a selected or
|
||
hovered row still shows its tick.
|
||
*/
|
||
[data-row-avatar-face] {
|
||
position: relative;
|
||
}
|
||
|
||
[data-row-avatar-face]::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
border-radius: 9999px;
|
||
background-color: rgb(var(--rgb-surface));
|
||
box-shadow: inset 0 0 0 1px rgb(var(--rgb-line) / 0.45);
|
||
opacity: var(--list-avatar-hide);
|
||
transition: opacity 150ms;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/*
|
||
The preview line, and how many of it there are.
|
||
|
||
Stacked, the row has room for a second line and the clamp engages. Wide, it
|
||
does not: the subject and the preview share one line by design, and a second
|
||
preview line there pushes the subject onto a row of its own, which is the
|
||
arrangement the wide layout exists to avoid. So the wide branch takes the
|
||
same on/off switch without the clamp.
|
||
|
||
`@container`, matching the row's own `@xl` (Tailwind's 36rem container
|
||
width, written out by hand because a plain rule cannot use the variant): the
|
||
row switches on the LIST's width, not the viewport's — the list is narrower
|
||
than a phone whenever the calendar pane is docked wide.
|
||
*/
|
||
/*
|
||
`:not(.hidden)` is load-bearing, and only on the stacked branch. A scheduled
|
||
draft's row carries `hidden @xl:inline-block` on this very element — the
|
||
badge saying when it goes has already eaten the line, and what is left of
|
||
the snippet at 393px is two characters and an ellipsis. These rules are
|
||
unlayered and would otherwise beat that `.hidden` and put the fragment back.
|
||
Wide, where the row shows both, there is nothing to exempt.
|
||
*/
|
||
[data-row-snippet]:not(.hidden) {
|
||
display: var(--list-preview-display);
|
||
-webkit-line-clamp: var(--list-preview-lines);
|
||
-webkit-box-orient: vertical;
|
||
white-space: var(--list-preview-wrap);
|
||
overflow: hidden;
|
||
}
|
||
|
||
@container (min-width: 36rem) {
|
||
[data-row-snippet] {
|
||
display: var(--list-preview-display-wide);
|
||
-webkit-line-clamp: 1;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
/*
|
||
The accent bar down an unread row's leading edge — Strong emphasis only,
|
||
where --unread-bar-w is 3px; it is 0px at the other two and paints nothing.
|
||
|
||
A pseudo-element on the row rather than markup, so the row's Twig knows
|
||
nothing about it. It is the row's first child in paint order, which is what
|
||
keeps the account corner — a positioned element later in the DOM — on top of
|
||
it where the two meet in the bottom-left.
|
||
|
||
`[data-unread="true"]` and not `li[…]`: the settings preview's rows are
|
||
divs, and the point of that preview is that it is painted by these rules
|
||
rather than by a copy of them. The attribute is written on mail rows and
|
||
nowhere else, so the loose selector costs nothing.
|
||
*/
|
||
[data-unread="true"]::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset-block: 0;
|
||
inset-inline-start: 0;
|
||
width: var(--unread-bar-w);
|
||
background-color: rgb(var(--rgb-accent));
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* A link in the body has to LOOK like one.
|
||
Tailwind's preflight resets `a` to `color: inherit; text-decoration: inherit`,
|
||
which is right for an app whose every anchor is already a styled button or a
|
||
list row — and wrong for the one surface where anchors are user content. A
|
||
link inserted into the editor was indistinguishable from the words around
|
||
it: nothing said it was there, so nothing invited the click that now opens
|
||
the link popover.
|
||
|
||
Accent rather than a browser blue, so it matches the theme, and the same
|
||
colour the popover writes the address in. The caret stays a text caret: this
|
||
is a document being written, and a pointer cursor over part of a sentence
|
||
would say the click does something other than put the cursor there. */
|
||
[data-compose--compose-toolbar-target="editor"] a[href] {
|
||
color: rgb(var(--rgb-accent));
|
||
text-decoration: underline;
|
||
text-underline-offset: 2px;
|
||
}
|
||
|