Rework Dark into a dim room, and fix the flat backgrounds that painted nothing

Both new themes set --app-bg to a bare hex, and .app-bg paints through
`background-image`. `background-image: #151413` is not a valid image, so it
was dropped, the element painted nothing, and the page fell through to the
browser's white — which is why the background stayed light no matter what the
palette said. Written as a one-stop gradient now, the same idiom
BackgroundResolver already uses for a solid background. Every other theme uses
a real gradient, which is why this never came up before.

Dark itself is reworked. The first pass was near-white text on near-black with
only the panes lifted, and it was harsh in two ways at once: #eae7e2 on
#0a0a0a is about the contrast of a headlight, and lifting one rectangle out of
a black page made the mail list the only lit thing in the window. Now the
floor comes up off pure black, the step between page and pane is small enough
to read as one continuous material, and the ink stops well short of white with
the hairlines doing the separating — which is what Paper does, in reverse. The
accent is a fill, not a tint: Compose and the active nav row paint it behind
light text, so the pale sage taken from the reference's avatar circles was a
washed-out slab nothing could be read on.

Two smaller things:

  * The split's mail-side floor was 420px, which stopped the handle before the
    stacked layout it was heading for and barely moved at all on a 1024px
    screen. It is a phone's width now — the list has a layout for exactly that
    size and it should be reachable by dragging.
  * In the compressed row the paperclip and star share a line with the star
    outermost, and the unread dot sits below them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul LütznerandClaude Opus 5 committed 2026-08-01 07:21:12 +02:00
1 parent 57ed537003
commit 24e5e5c93b
5 files changed
+81 -40

No files matched your search

+7 -4
View File
@@ -24,10 +24,13 @@ export default class extends Controller {
token: String,
min: { type: Number, default: 320 },
max: { type: Number, default: 900 },
// What the mail side is never dragged below. The pane's own minimum is
// not enough on its own: without this the handle happily shrinks the
// message list to a column of truncated subjects.
mainMin: { type: Number, default: 420 },
// What the mail side is never dragged below — a phone's width, not a
// comfortable one. The list has a stacked layout for exactly this size
// and it should be reachable by dragging: a floor set at "still looks
// like a desktop list" meant the handle stopped before the layout it
// was heading for, and on a 1024px screen it barely moved at all.
// The pane's own minimum still stops it going further.
mainMin: { type: Number, default: 340 },
open: { type: Boolean, default: false },
step: { type: Number, default: 24 },
default: { type: Number, default: 380 },
+60 -25
View File
@@ -118,40 +118,75 @@
--sunken-alpha: 0.04;
--hover-alpha: 0.035;
--app-bg: #f2efe7;
/* 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);
}
/* Dark, retuned to the neutral near-black it always wanted to be.
It was #111827 — a slate blue that quietly tinted every surface above it,
which is why the greys elsewhere in the UI never looked neutral on it. This
is the closest existing theme to the one being matched, so it is the one
that changes rather than a seventh entry in the picker. */
/* 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. */
[data-theme="dark"] {
--rgb-surface: 18 18 18;
--rgb-ink: 232 230 225;
--rgb-ink-muted: 150 146 139;
--rgb-ink-faint: 110 107 101;
--rgb-ink-soft: 198 195 188;
--rgb-accent: 138 180 160;
--rgb-border: 255 255 255;
--rgb-line: 255 255 255;
--rgb-sunken: 255 255 255;
--rgb-surface: 28 27 26;
--rgb-ink: 214 209 202;
--rgb-ink-soft: 186 181 174;
--rgb-ink-muted: 142 137 130;
--rgb-ink-faint: 108 104 98;
--rgb-accent: 58 111 92;
--rgb-accent-ink: 240 245 242;
--rgb-border: 255 255 255;
--rgb-line: 255 255 255;
--rgb-sunken: 255 255 255;
--rgb-inverse: 222 218 211;
--rgb-inverse-ink: 28 27 26;
/* Status colours want more light on a dark surface than the 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;
/* Fields lighten the surface instead of darkening it. .dark's field is
#27272a, which on its old #111827 surface read as an inset box and on
this near-black is the same colour as the page — the search input lost
its edges entirely. */
#27272a, which is now the same colour as the surface itself — the search
input lost its edges entirely. */
--rgb-field: 255 255 255;
--rgb-field-border: 255 255 255;
--field-alpha: 0.05;
--field-border-alpha: 0.14;
--field-alpha: 0.04;
--field-border-alpha: 0.10;
--border-alpha: 0.09;
--line-alpha: 0.08;
--sunken-alpha: 0.05;
--hover-alpha: 0.045;
/* 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;
--sunken-alpha: 0.07;
--hover-alpha: 0.05;
--shadow-alpha: 0.45;
--app-bg: #0d0d0d;
/* 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);
}
[data-theme="nord"] {
+2 -2
View File
@@ -49,7 +49,7 @@ enum Theme: string
{
return match ($this) {
self::Paper => ['accent' => '#7d6b4f'],
self::Dark => ['accent' => '#8ab4a0'],
self::Dark => ['accent' => '#3a6f5c'],
default => [],
};
}
@@ -61,7 +61,7 @@ enum Theme: string
self::System => ['#ffffff', '#111827', '#2563eb'],
self::Light => ['#ffffff', '#27272a', '#2563eb'],
self::Paper => ['#f7f5ef', '#232220', '#7d6b4f'],
self::Dark => ['#121212', '#e8e6e1', '#8ab4a0'],
self::Dark => ['#1c1b1a', '#d6d1ca', '#3a6f5c'],
self::Nord => ['#2e3440', '#eceff4', '#88c0d0'],
self::Dusk => ['#1e1b2e', '#ede9fe', '#a78bfa'],
self::Solar => ['#fdf6e3', '#586e75', '#b58900'],
+11 -8
View File
@@ -197,7 +197,10 @@ the conversation it answers.
{% endif %}
</span>
<div class="flex flex-col items-end gap-1.5">
{# Attachment and star share a line, star outermost — it is the one
you reach for, so it sits where the thumb already is, and the
paperclip is a fact about the row rather than a control. #}
<div class="flex items-center gap-2">
{% if hasAttachment %}
<span class="text-ink-faint" title="{{ 'thread_row.has_attachment'|trans }}">
<i class="fa-solid fa-paperclip text-xs" aria-hidden="true"></i>
@@ -214,14 +217,14 @@ the conversation it answers.
>
<i class="fa-{{ starred ? 'solid' : 'regular' }} fa-star text-sm"></i>
</button>
{# Unread, as a dot. The bold text says it too, but a dot is
what you count down a column without reading anything. #}
{% if unread %}
<span class="w-2 h-2 rounded-full bg-success" aria-hidden="true"></span>
<span class="sr-only">{{ 'thread_row.unread'|trans|default('Unread') }}</span>
{% endif %}
</div>
{# Unread, as a dot, on its own line. The bold text says it too, but
a dot is what you count down a column without reading anything. #}
{% if unread %}
<span class="w-2 h-2 rounded-full bg-success" aria-hidden="true"></span>
<span class="sr-only">{{ 'thread_row.unread'|trans|default('Unread') }}</span>
{% endif %}
</div>
{# Date + hover actions slot — desktop only #}
+1 -1
View File
@@ -227,7 +227,7 @@ test.describe("calendar pane", () => {
await page.mouse.move(20, box.y + box.height / 2, { steps: 12 });
await page.mouse.up();
expect((await mainPane.boundingBox())!.width).toBeGreaterThanOrEqual(419);
expect((await mainPane.boundingBox())!.width).toBeGreaterThanOrEqual(339);
});
test("centres its grip in the gap between the panes", async ({ page }) => {