Six more palettes, taking the picker to twelve

Catppuccin, Solarized, Ayu, Kanagawa, Everforest and Primer, each with the
light and dark variant its own project publishes: Latte and Mocha, Lotus
and Wave, and so on. Values were fetched from each project's own repository
and recorded in .palette-sources/palettes-upstream.md, with the two tiers
no project publishes marked derived rather than passed off as upstream.

Four candidates were rejected rather than adapted. Nord and Synthwave '84
publish no light variant, and inventing one is not porting a theme.
Monokai is proprietary and its licence forbids redistribution. Material
Theme has become a commercial product whose repository no longer publishes
a palette at all.

Body text is now lifted for contrast like every other text tone rather
than exempted and merely checked. Most of these palettes target their own
~4.5:1 for body text where ihasmail asks 7:1, so the old rule would have
rejected five of the six on a bar their designers never aimed at. Nudging
the published colour along its own hue is what the script already does for
muted text, links and accents, and every shift is printed in the generated
CSS: Solarized light moves 4.13 to 7.07, Primer needed nothing at all.

Primer is named for the design system, not for GitHub. The colour values
are MIT; the name and the logo are trademarks, and NOTICE says plainly
that nothing here is endorsed.

The picker grid already wrapped on its own, so twelve cards needed no
layout change.
This commit is contained in:
2026-09-06 16:31:38 -07:00
parent a35f360952
commit befe1dbf53
7 changed files with 728 additions and 9 deletions
+101
View File
@@ -47,3 +47,104 @@ bg #e6e7ed · bg_dark #d6d8df · fg #343b59 · line numbers #9da0ab · border #c
link #2959aa link #2959aa
accents: purple #65359d · red #8c4351 · cyan #006c86 · blue #2959aa accents: purple #65359d · red #8c4351 · cyan #006c86 · blue #2959aa
yellow #8f5e15 · teal #33635c · green #385f0d yellow #8f5e15 · teal #33635c · green #385f0d
---
Fetched 2026-09-06 from the projects' own repositories, same rule as above.
Where a project publishes fewer background tiers than ihasmail needs, the
missing one is derived and marked **derived** here rather than passed off as
upstream. Body text is lifted to 7:1 by the build script for most of these —
they target their own ~4.5:1 — and every shift is printed in the generated CSS.
## Catppuccin — catppuccin/palette, MIT (palette.json)
Cited from the palette repo rather than the hub README; it is the normative
machine-readable source.
### Mocha (dark)
base #1e1e2e · mantle #181825 · crust #11111b · surface0 #313244 · surface1 #45475a
text #cdd6f4 · subtext0 #a6adc8 · overlay1 #7f849c
mauve #cba6f7 · blue #89b4fa · red #f38ba8 · peach #fab387 · green #a6e3a1
yellow #f9e2af · pink #f5c2e7
### Latte (light)
base #eff1f5 · mantle #e6e9ef · crust #dce0e8 · surface0 #ccd0da · surface1 #bcc0cc
text #4c4f69 · subtext0 #6c6f85
mauve #8839ef · blue #1e66f5 · red #d20f39 · peach #fe640b · green #40a02b
yellow #df8e1d · pink #ea76cb
Latte publishes no tier lighter than `base`, so `base` is used as the elevated
surface and `mantle` as the page behind it.
## Solarized — altercation/solarized, MIT (README "The Values")
base03 #002b36 · base02 #073642 · base01 #586e75 · base00 #657b83
base0 #839496 · base1 #93a1a1 · base2 #eee8d5 · base3 #fdf6e3
yellow #b58900 · orange #cb4b16 · red #dc322f · magenta #d33682
violet #6c71c4 · blue #268bd2 · cyan #2aa198 · green #859900
The accents are shared by both modes by design. Two tiers are **derived**: the
sunken dark surface #001f28 (below base03) and the raised light surface
#fffdf6 (above base3), neither of which Solarized publishes, plus the two
rule colours #0d4552 and #e6dfc8.
## Everforest — sainnhe/everforest, MIT (palette.md), medium contrast
### Dark
bg_dim #232a2e · bg0 #2d353b · bg1 #343f44 · bg3 #475258
fg #d3c6aa · grey1 #859289
red #e67e80 · orange #e69875 · yellow #dbbc7f · green #a7c080 · aqua #83c092
blue #7fbbb3 · purple #d699b6
### Light
bg_dim #efebd4 · bg0 #fdf6e3 · bg3 #e6e2cc · bg5 #bdc3af
fg #5c6a72 · grey1 #939f91
red #f85552 · orange #f57d26 · yellow #dfa000 · green #8da101 · aqua #35a77c
blue #3a94c5 · purple #df69ba
Light uses bg_dim as the page and bg0 as the raised surface, so the card the
reader looks at is the colour Everforest calls its background.
## Kanagawa — rebelot/kanagawa.nvim, MIT (lua/kanagawa/colors.lua)
### Wave (dark)
sumiInk0 #16161D · sumiInk3 #1F1F28 · sumiInk4 #2A2A37 · sumiInk5 #363646
fujiWhite #DCD7BA · fujiGray #727169
crystalBlue #7E9CD8 · springBlue #7FB4CA · samuraiRed #E82424 · roninYellow #FF9E3B
springGreen #98BB6C · carpYellow #E6C384 · sakuraPink #D27E99
### Lotus (light)
lotusWhite0 #d5cea3 · lotusWhite1 #dcd5ac · lotusWhite2 #e5ddb0 · lotusWhite3 #f2ecbc
lotusInk1 #545464 · lotusGray2 #716e61
lotusViolet4 #624c83 · lotusBlue4 #4d699b · lotusRed #c84053 · lotusOrange #cc6d00
lotusGreen #6f894e · lotusYellow #77713f · lotusPink #b35b79
## Ayu — ayu-theme/ayu-colors, MIT (themes/dark.yaml, themes/light.yaml)
The YAMLs give the base palette and the surfaces as literals but express syntax
roles as references (`$palette.indigo.l2`), and the resolved files are not
committed. The two signature accents are taken from the same organisation's
MIT-licensed ayu-theme/vscode-ayu build.
### Dark
surface base #0D1017 · lift #10141C (sunk is `base -L0.1`, **derived** here as #070a0f)
ui line #1B1F29 · ui fg #5A6378 · editor fg #BFBDB6
red #F07178 · orange #FF8F40 · yellow #FFB454 · green #AAD94C · teal #95E6CB
indigo #39BAE6 · blue #59C2FF · purple #D2A6FF · accent #E6B450 (vscode-ayu)
### Light
surface sunk #EBEEF0 · base #F8F9FA · lift #FCFCFC
ui fg #828E9F · editor fg #5C6166 · rule #dfe2e5 (**derived**)
red #F07171 · orange #FA8532 · yellow #EBA400 · green #86B300 · teal #4CBF99
indigo #55B4D4 · blue #22A4E6 · purple #A37ACC · accent #F29718 (vscode-ayu)
## Primer — primer/primitives, MIT (src/tokens/base/color/{dark,light})
Named "Primer" after the design system. The colour values are MIT; "GitHub"
and the Invertocat are trademarks, and nothing here is endorsed by them.
### Dark
neutral #0D1117 #151B23 #212830 #262C36 #2A313C #2F3742 #3D444D #656C76
#9198A1 #B7BDC8 #D1D7E0 #F0F6FC · black #010409
blue #79c0ff #58a6ff · green #56d364 #3fb950 · yellow #e3b341 #d29922
red #ff7b72 · purple #d2a8ff
### Light
neutral #F6F8FA #EFF2F5 #E6EAEF #E0E6EB #DAE0E7 #D1D9E0 #C8D1DA #818B98
#59636E #454C54 #393F46 #25292E
blue #0969da #0550ae · green #1a7f37 #116329 · yellow #bf8700 #9a6700
red #cf222e · purple #8250df
+40 -2
View File
@@ -5,7 +5,7 @@ other people that ships inside it and the terms it comes under.
## Colour palettes ## Colour palettes
Four of the palettes offered in Settings Appearance are the work of their own Ten of the palettes offered in Settings Appearance are the work of their own
projects and are used under the MIT licence. Only the published colour values projects and are used under the MIT licence. Only the published colour values
are used — no code, and nothing from anyone else's reimplementation of them. are used — no code, and nothing from anyone else's reimplementation of them.
The values as fetched from each project are recorded in The values as fetched from each project are recorded in
@@ -35,9 +35,47 @@ Licensed under the MIT licence. The light variant is "Dawn".
Copyright (c) 2019 enkia — https://github.com/enkia/tokyo-night-vscode-theme Copyright (c) 2019 enkia — https://github.com/enkia/tokyo-night-vscode-theme
Licensed under the MIT licence. The light variant is "Day". Licensed under the MIT licence. The light variant is "Day".
### Catppuccin
Copyright (c) 2021 Catppuccin — https://github.com/catppuccin/palette
Licensed under the MIT licence. "Mocha" is the dark variant and "Latte" the
light one; both are published in that repository's palette.json.
### Solarized
Copyright (c) 2011 Ethan Schoonover — https://github.com/altercation/solarized
Licensed under the MIT licence. Light and dark are both original to it, and
share one set of accent values by design.
### Ayu
Copyright (c) Konstantin Pschera — https://github.com/ayu-theme/ayu-colors
Licensed under the MIT licence. The two signature accent colours come from the
same author's ayu-theme/vscode-ayu, also MIT.
### Kanagawa
Copyright (c) 2021 Tommaso Laurenzi — https://github.com/rebelot/kanagawa.nvim
Licensed under the MIT licence. "Wave" is the dark variant and "Lotus" the
light one. The theme takes its name from Hokusai's print.
### Everforest
Copyright (c) 2019 Sainnhe Park — https://github.com/sainnhe/everforest
Licensed under the MIT licence. The medium-contrast variant of each mode is
the one used here.
### Primer
Copyright (c) GitHub, Inc. — https://github.com/primer/primitives
Licensed under the MIT licence, which covers the colour values. "GitHub" and
the Invertocat logo are trademarks of GitHub, Inc.; this palette is named
"Primer" after the design system and is neither affiliated with nor endorsed
by GitHub.
--- ---
The MIT licence, under which all four are used: The MIT licence, under which all ten are used:
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
+93
View File
@@ -163,6 +163,90 @@ SOURCES = {
q1="#006c86", q2="#385f0d", q3="#65359d", q1="#006c86", q2="#385f0d", q3="#65359d",
), ),
}, },
"catppuccin": {
"dark": dict( # Mocha
bg="#1e1e2e", elev="#313244", sunken="#181825", line="#45475a",
fg="#cdd6f4", muted="#a6adc8", accent="#cba6f7", link="#89b4fa",
danger="#f38ba8", warn="#fab387", success="#a6e3a1", star="#f9e2af",
q1="#89b4fa", q2="#a6e3a1", q3="#f5c2e7",
),
"light": dict( # Latte
bg="#e6e9ef", elev="#eff1f5", sunken="#dce0e8", line="#ccd0da",
fg="#4c4f69", muted="#6c6f85", accent="#8839ef", link="#1e66f5",
danger="#d20f39", warn="#fe640b", success="#40a02b", star="#df8e1d",
q1="#1e66f5", q2="#40a02b", q3="#ea76cb",
),
},
"solarized": {
"dark": dict(
bg="#002b36", elev="#073642", sunken="#001f28", line="#0d4552",
fg="#839496", muted="#586e75", accent="#268bd2", link="#2aa198",
danger="#dc322f", warn="#cb4b16", success="#859900", star="#b58900",
q1="#2aa198", q2="#859900", q3="#6c71c4",
),
"light": dict(
bg="#fdf6e3", elev="#fffdf6", sunken="#eee8d5", line="#e6dfc8",
fg="#657b83", muted="#93a1a1", accent="#268bd2", link="#2aa198",
danger="#dc322f", warn="#cb4b16", success="#859900", star="#b58900",
q1="#2aa198", q2="#859900", q3="#6c71c4",
),
},
"ayu": {
"dark": dict(
bg="#0d1017", elev="#10141c", sunken="#070a0f", line="#1b1f29",
fg="#bfbdb6", muted="#5a6378", accent="#e6b450", link="#59c2ff",
danger="#f07178", warn="#ff8f40", success="#aad94c", star="#ffb454",
q1="#39bae6", q2="#aad94c", q3="#d2a6ff",
),
"light": dict(
bg="#f8f9fa", elev="#fcfcfc", sunken="#ebeef0", line="#dfe2e5",
fg="#5c6166", muted="#828e9f", accent="#f29718", link="#22a4e6",
danger="#f07171", warn="#fa8532", success="#86b300", star="#eba400",
q1="#55b4d4", q2="#86b300", q3="#a37acc",
),
},
"kanagawa": {
"dark": dict( # Wave
bg="#1f1f28", elev="#2a2a37", sunken="#16161d", line="#363646",
fg="#dcd7ba", muted="#727169", accent="#7e9cd8", link="#7fb4ca",
danger="#e82424", warn="#ff9e3b", success="#98bb6c", star="#e6c384",
q1="#7fb4ca", q2="#98bb6c", q3="#d27e99",
),
"light": dict( # Lotus
bg="#e5ddb0", elev="#f2ecbc", sunken="#dcd5ac", line="#d5cea3",
fg="#545464", muted="#716e61", accent="#624c83", link="#4d699b",
danger="#c84053", warn="#cc6d00", success="#6f894e", star="#77713f",
q1="#4d699b", q2="#6f894e", q3="#b35b79",
),
},
"everforest": {
"dark": dict( # medium
bg="#2d353b", elev="#343f44", sunken="#232a2e", line="#475258",
fg="#d3c6aa", muted="#859289", accent="#a7c080", link="#7fbbb3",
danger="#e67e80", warn="#e69875", success="#a7c080", star="#dbbc7f",
q1="#7fbbb3", q2="#a7c080", q3="#d699b6",
),
"light": dict( # medium
bg="#efebd4", elev="#fdf6e3", sunken="#e6e2cc", line="#bdc3af",
fg="#5c6a72", muted="#939f91", accent="#8da101", link="#3a94c5",
danger="#f85552", warn="#f57d26", success="#8da101", star="#dfa000",
q1="#3a94c5", q2="#8da101", q3="#df69ba",
),
},
"primer": {
"dark": dict(
bg="#0d1117", elev="#151b23", sunken="#010409", line="#3d444d",
fg="#f0f6fc", muted="#9198a1", accent="#58a6ff", link="#79c0ff",
danger="#ff7b72", warn="#e3b341", success="#3fb950", star="#d29922",
q1="#79c0ff", q2="#56d364", q3="#d2a8ff",
),
"light": dict(
bg="#f6f8fa", elev="#ffffff", sunken="#eff2f5", line="#d1d9e0",
fg="#25292e", muted="#59636e", accent="#0969da", link="#0550ae",
danger="#cf222e", warn="#9a6700", success="#1a7f37", star="#bf8700",
q1="#0550ae", q2="#116329", q3="#8250df",
),
},
} }
# What each token has to clear, and against which surface. Normal text is 4.5; # What each token has to clear, and against which surface. Normal text is 4.5;
@@ -183,6 +267,15 @@ def build(pid: str, mode: str, src: dict[str, str]) -> tuple[dict[str, str], lis
notes.append(f"{name} {colour} -> {out} ({contrast(colour, bg):.2f} -> {contrast(out, bg):.2f})") notes.append(f"{name} {colour} -> {out} ({contrast(colour, bg):.2f} -> {contrast(out, bg):.2f})")
return out return out
# Body text is lifted like every other text tone rather than exempted.
# Most of these palettes publish a body colour around 4.5:1 -- their own
# target -- and ihasmail asks 7:1 of the text a reader looks at all day.
# Rejecting a palette over that would have cost five of the six added in
# 2026-09; nudging the published colour along its own hue costs nothing a
# reader can name, and the shift is recorded in the header of the
# generated block like every other one.
fg = lift("fg", fg, TEXT_ON_BG["fg"])
muted = lift("muted", src["muted"], TEXT_ON_BG["muted"]) muted = lift("muted", src["muted"], TEXT_ON_BG["muted"])
# Between muted and the background, but still readable: this is timestamps # Between muted and the background, but still readable: this is timestamps
# and counts, which are small and still prose. # and counts, which are small and still prose.
+4 -1
View File
@@ -6,7 +6,10 @@ describe("the palettes themselves", () => {
// The reason there is no "this palette is dark only" machinery: there is // The reason there is no "this palette is dark only" machinery: there is
// no such palette. ihasmail's own gained a light half, and the override, // no such palette. ihasmail's own gained a light half, and the override,
// the toggle's memory and a greyed-out control all went with it. // the toggle's memory and a greyed-out control all went with it.
expect(PALETTES.map((p) => p.id)).toEqual(["default", "ihasmail", "dracula", "gruvbox", "rose-pine", "tokyo-night"]); expect(PALETTES.map((p) => p.id)).toEqual([
"default", "ihasmail", "dracula", "gruvbox", "rose-pine", "tokyo-night",
"catppuccin", "solarized", "ayu", "kanagawa", "everforest", "primer",
]);
}); });
it("credits every borrowed palette and neither of ihasmail's own", () => { it("credits every borrowed palette and neither of ihasmail's own", () => {
+16 -6
View File
@@ -13,7 +13,9 @@
* the derivation can be checked rather than taken on trust. * the derivation can be checked rather than taken on trust.
*/ */
export type PaletteId = "default" | "ihasmail" | "dracula" | "gruvbox" | "rose-pine" | "tokyo-night"; export type PaletteId =
| "default" | "ihasmail" | "dracula" | "gruvbox" | "rose-pine" | "tokyo-night"
| "catppuccin" | "solarized" | "ayu" | "kanagawa" | "everforest" | "primer";
export type Mode = "system" | "light" | "dark"; export type Mode = "system" | "light" | "dark";
/** What a mode resolves to once the system has been asked. */ /** What a mode resolves to once the system has been asked. */
export type ResolvedMode = "light" | "dark"; export type ResolvedMode = "light" | "dark";
@@ -26,11 +28,11 @@ export interface PaletteMeta {
/** /**
* Whether the name is a word rather than a name. * Whether the name is a word rather than a name.
* *
* Five of these six are proper names -- ihasmail, Dracula, Gruvbox, Rosé * All but one of these are proper names -- ihasmail, Dracula, Gruvbox and
* Pine, Tokyo Night -- and are rendered translate="no" so a page translator * the rest -- and are rendered translate="no" so a page translator leaves
* leaves them alone. "Classic" is not a name, it is an adjective describing * them alone. "Classic" is not a name, it is an adjective describing the
* the theme, and a German reader should see "Klassisch". Reported by a * theme, and a German reader should see "Klassisch". Reported by a native
* native speaker reviewing the German catalogue (#247). * speaker reviewing the German catalogue (#247).
*/ */
translatable?: boolean; translatable?: boolean;
} }
@@ -42,6 +44,14 @@ export const PALETTES: PaletteMeta[] = [
{ id: "gruvbox", name: "Gruvbox", credit: "gruvbox by morhetz (MIT)" }, { id: "gruvbox", name: "Gruvbox", credit: "gruvbox by morhetz (MIT)" },
{ id: "rose-pine", name: "Rosé Pine", credit: "Rosé Pine (MIT) — light variant is Dawn" }, { id: "rose-pine", name: "Rosé Pine", credit: "Rosé Pine (MIT) — light variant is Dawn" },
{ id: "tokyo-night", name: "Tokyo Night", credit: "Tokyo Night by enkia (MIT) — light variant is Day" }, { id: "tokyo-night", name: "Tokyo Night", credit: "Tokyo Night by enkia (MIT) — light variant is Day" },
{ id: "catppuccin", name: "Catppuccin", credit: "Catppuccin (MIT) — dark is Mocha, light is Latte" },
{ id: "solarized", name: "Solarized", credit: "Solarized by Ethan Schoonover (MIT) — light and dark are both original" },
{ id: "ayu", name: "Ayu", credit: "Ayu by Konstantin Pschera (MIT)" },
{ id: "kanagawa", name: "Kanagawa", credit: "Kanagawa by rebelot (MIT) — dark is Wave, light is Lotus" },
{ id: "everforest", name: "Everforest", credit: "Everforest by sainnhe (MIT)" },
// Named for the design system rather than for GitHub: the colours are MIT,
// the name and the logo are trademarks, and nothing here is endorsed.
{ id: "primer", name: "Primer", credit: "GitHub's Primer primitives (MIT); not affiliated with or endorsed by GitHub" },
]; ];
const byId = new Map(PALETTES.map((p) => [p.id, p])); const byId = new Map(PALETTES.map((p) => [p.id, p]));
+450
View File
@@ -503,6 +503,456 @@
--shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75); --shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75);
} }
/* catppuccin (light) lifted for contrast: fg #4c4f69 -> #484b64 (6.57 -> 7.00); muted #6c6f85 -> #64677c (4.06 -> 4.58); faint #8b8e9e -> #656873 (2.67 -> 4.57); link #1e66f5 -> #1c5fe4 (4.04 -> 4.54); danger #d20f39 -> #d00f38 (4.46 -> 4.53); warn #fe640b -> #b24608 (2.45 -> 4.58); success #40a02b -> #307820 (2.75 -> 4.50); star #df8e1d -> #b97618 (2.15 -> 3.05); border-strong #b8bcc8 -> #83858e (1.56 -> 3.02); q1 #1e66f5 -> #1c5fe4 (4.04 -> 4.54); q2 #40a02b -> #307820 (2.75 -> 4.50); q3 #ea76cb -> #9a4e86 (2.17 -> 4.53) */
:root[data-palette="catppuccin"] {
--bg: #e6e9ef;
--bg-elev: #eff1f5;
--bg-sunken: #dce0e8;
--bg-hover: rgba(72, 75, 100, 0.06);
--bg-active: rgba(72, 75, 100, 0.11);
--fg: #484b64;
--fg-muted: #64677c;
--fg-faint: #656873;
--border: #ccd0da;
--border-strong: #83858e;
--accent: #8839ef;
--accent-fg: #ffffff;
--accent-soft: #d9d0ef;
--accent-soft-fg: #7632d0;
--danger: #d00f38;
--danger-soft: rgba(208, 15, 56, 0.15);
--warn: #b24608;
--warn-soft: rgba(178, 70, 8, 0.15);
--success: #307820;
--success-soft: rgba(48, 120, 32, 0.15);
--link: #1c5fe4;
--unread-bg: #ffffff;
--read-bg: #e1e4eb;
--selected-bg: #d9d0ef;
--focus-ring: 0 0 0 3px rgba(136, 57, 239, 0.4);
--star: #b97618;
--q1: #1c5fe4;
--q2: #307820;
--q3: #9a4e86;
--scrollbar: rgba(100, 103, 124, 0.35);
color-scheme: light;
}
/* catppuccin (dark) lifted for contrast: faint #7d829a -> #81869d (4.32 -> 4.55); border-strong #595c71 -> #66697c (2.49 -> 3.03) */
:root[data-theme="dark"][data-palette="catppuccin"] {
--bg: #1e1e2e;
--bg-elev: #313244;
--bg-sunken: #181825;
--bg-hover: rgba(205, 214, 244, 0.06);
--bg-active: rgba(205, 214, 244, 0.11);
--fg: #cdd6f4;
--fg-muted: #a6adc8;
--fg-faint: #81869d;
--border: #45475a;
--border-strong: #66697c;
--accent: #cba6f7;
--accent-fg: #1e1e2e;
--accent-soft: rgba(203, 166, 247, 0.16);
--accent-soft-fg: #cba6f7;
--danger: #f38ba8;
--danger-soft: rgba(243, 139, 168, 0.15);
--warn: #fab387;
--warn-soft: rgba(250, 179, 135, 0.15);
--success: #a6e3a1;
--success-soft: rgba(166, 227, 161, 0.15);
--link: #89b4fa;
--unread-bg: #313244;
--read-bg: #181825;
--selected-bg: rgba(203, 166, 247, 0.18);
--focus-ring: 0 0 0 3px rgba(203, 166, 247, 0.4);
--star: #f9e2af;
--q1: #89b4fa;
--q2: #a6e3a1;
--q3: #f5c2e7;
--scrollbar: rgba(166, 173, 200, 0.35);
color-scheme: dark;
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45);
--shadow-2: 0 8px 24px rgba(0, 0, 0, 0.55);
--shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75);
}
/* solarized (light) lifted for contrast: fg #657b83 -> #47565c (4.13 -> 7.07); muted #93a1a1 -> #687272 (2.48 -> 4.59); faint #959a94 -> #6e726e (2.66 -> 4.53); link #2aa198 -> #217e77 (2.93 -> 4.51); danger #dc322f -> #d5302e (4.29 -> 4.54); warn #cb4b16 -> #c34815 (4.27 -> 4.57); success #859900 -> #687700 (2.97 -> 4.61); star #b58900 -> #b38800 (2.98 -> 3.02); border-strong #cecab8 -> #928f83 (1.53 -> 3.00); q1 #2aa198 -> #217e77 (2.93 -> 4.51); q2 #859900 -> #687700 (2.97 -> 4.61); q3 #6c71c4 -> #666ab8 (4.06 -> 4.51) */
:root[data-palette="solarized"] {
--bg: #fdf6e3;
--bg-elev: #fffdf6;
--bg-sunken: #eee8d5;
--bg-hover: rgba(71, 86, 92, 0.06);
--bg-active: rgba(71, 86, 92, 0.11);
--fg: #47565c;
--fg-muted: #687272;
--fg-faint: #6e726e;
--border: #e6dfc8;
--border-strong: #928f83;
--accent: #268bd2;
--accent-fg: #ffffff;
--accent-soft: #dfe7e1;
--accent-soft-fg: #1d6ba2;
--danger: #d5302e;
--danger-soft: rgba(213, 48, 46, 0.15);
--warn: #c34815;
--warn-soft: rgba(195, 72, 21, 0.15);
--success: #687700;
--success-soft: rgba(104, 119, 0, 0.15);
--link: #217e77;
--unread-bg: #ffffff;
--read-bg: #f8f1df;
--selected-bg: #dfe7e1;
--focus-ring: 0 0 0 3px rgba(38, 139, 210, 0.4);
--star: #b38800;
--q1: #217e77;
--q2: #687700;
--q3: #666ab8;
--scrollbar: rgba(104, 114, 114, 0.35);
color-scheme: light;
}
/* solarized (dark) lifted for contrast: fg #839496 -> #a8b4b6 (4.75 -> 7.06); muted #586e75 -> #809196 (2.79 -> 4.58); faint #5a7279 -> #7d9095 (2.94 -> 4.50); danger #dc322f -> #e56563 (3.25 -> 4.56); warn #cb4b16 -> #d67147 (3.26 -> 4.51); border-strong #245661 -> #4e767f (1.84 -> 3.02); q3 #6c71c4 -> #8488cd (3.43 -> 4.56) */
:root[data-theme="dark"][data-palette="solarized"] {
--bg: #002b36;
--bg-elev: #073642;
--bg-sunken: #001f28;
--bg-hover: rgba(168, 180, 182, 0.06);
--bg-active: rgba(168, 180, 182, 0.11);
--fg: #a8b4b6;
--fg-muted: #809196;
--fg-faint: #7d9095;
--border: #0d4552;
--border-strong: #4e767f;
--accent: #268bd2;
--accent-fg: #002b36;
--accent-soft: rgba(38, 139, 210, 0.16);
--accent-soft-fg: #56a5dc;
--danger: #e56563;
--danger-soft: rgba(229, 101, 99, 0.15);
--warn: #d67147;
--warn-soft: rgba(214, 113, 71, 0.15);
--success: #859900;
--success-soft: rgba(133, 153, 0, 0.15);
--link: #2aa198;
--unread-bg: #073642;
--read-bg: #001f28;
--selected-bg: rgba(38, 139, 210, 0.18);
--focus-ring: 0 0 0 3px rgba(38, 139, 210, 0.4);
--star: #b58900;
--q1: #2aa198;
--q2: #859900;
--q3: #8488cd;
--scrollbar: rgba(128, 145, 150, 0.35);
color-scheme: dark;
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45);
--shadow-2: 0 8px 24px rgba(0, 0, 0, 0.55);
--shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75);
}
/* ayu (light) lifted for contrast: fg #5c6166 -> #52565b (5.93 -> 7.01); muted #828e9f -> #697381 (3.15 -> 4.56); faint #949ba5 -> #6e737a (2.66 -> 4.53); link #22a4e6 -> #1979aa (2.65 -> 4.57); danger #f07171 -> #b45555 (2.73 -> 4.56); warn #fa8532 -> #af5d23 (2.35 -> 4.51); success #86b300 -> #5e7d00 (2.35 -> 4.52); accent #f29718 -> #cb7f14 (2.16 -> 3.03); star #eba400 -> #be8500 (2.02 -> 3.04); border-strong #cacdd0 -> #8d9092 (1.51 -> 3.05); q1 #55b4d4 -> #3a7a90 (2.25 -> 4.56); q2 #86b300 -> #5e7d00 (2.35 -> 4.52); q3 #a37acc -> #8664a7 (3.21 -> 4.53) */
:root[data-palette="ayu"] {
--bg: #f8f9fa;
--bg-elev: #fcfcfc;
--bg-sunken: #ebeef0;
--bg-hover: rgba(82, 86, 91, 0.06);
--bg-active: rgba(82, 86, 91, 0.11);
--fg: #52565b;
--fg-muted: #697381;
--fg-faint: #6e737a;
--border: #dfe2e5;
--border-strong: #8d9092;
--accent: #cb7f14;
--accent-fg: #ffffff;
--accent-soft: #f2e8da;
--accent-soft-fg: #945d0f;
--danger: #b45555;
--danger-soft: rgba(180, 85, 85, 0.15);
--warn: #af5d23;
--warn-soft: rgba(175, 93, 35, 0.15);
--success: #5e7d00;
--success-soft: rgba(94, 125, 0, 0.15);
--link: #1979aa;
--unread-bg: #ffffff;
--read-bg: #f3f4f5;
--selected-bg: #f2e8da;
--focus-ring: 0 0 0 3px rgba(203, 127, 20, 0.4);
--star: #be8500;
--q1: #3a7a90;
--q2: #5e7d00;
--q3: #8664a7;
--scrollbar: rgba(105, 115, 129, 0.35);
color-scheme: light;
}
/* ayu (dark) lifted for contrast: muted #5a6378 -> #747c8e (3.16 -> 4.55); faint #555c6a -> #777d88 (2.83 -> 4.60); border-strong #34373e -> #5f6167 (1.60 -> 3.07) */
:root[data-theme="dark"][data-palette="ayu"] {
--bg: #0d1017;
--bg-elev: #10141c;
--bg-sunken: #070a0f;
--bg-hover: rgba(191, 189, 182, 0.06);
--bg-active: rgba(191, 189, 182, 0.11);
--fg: #bfbdb6;
--fg-muted: #747c8e;
--fg-faint: #777d88;
--border: #1b1f29;
--border-strong: #5f6167;
--accent: #e6b450;
--accent-fg: #0d1017;
--accent-soft: rgba(230, 180, 80, 0.16);
--accent-soft-fg: #e6b450;
--danger: #f07178;
--danger-soft: rgba(240, 113, 120, 0.15);
--warn: #ff8f40;
--warn-soft: rgba(255, 143, 64, 0.15);
--success: #aad94c;
--success-soft: rgba(170, 217, 76, 0.15);
--link: #59c2ff;
--unread-bg: #10141c;
--read-bg: #070a0f;
--selected-bg: rgba(230, 180, 80, 0.18);
--focus-ring: 0 0 0 3px rgba(230, 180, 80, 0.4);
--star: #ffb454;
--q1: #39bae6;
--q2: #aad94c;
--q3: #d2a6ff;
--scrollbar: rgba(116, 124, 142, 0.35);
color-scheme: dark;
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45);
--shadow-2: 0 8px 24px rgba(0, 0, 0, 0.55);
--shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75);
}
/* kanagawa (light) lifted for contrast: fg #545464 -> #434350 (5.41 -> 7.09); muted #716e61 -> #636155 (3.73 -> 4.54); faint #8a8670 -> #636051 (2.67 -> 4.61); link #4d699b -> #47618f (4.02 -> 4.53); danger #c84053 -> #ac3747 (3.55 -> 4.51); warn #cc6d00 -> #934e00 (2.66 -> 4.59); success #6f894e -> #53673b (2.85 -> 4.54); border-strong #bfb997 -> #807c65 (1.44 -> 3.07); q1 #4d699b -> #47618f (4.02 -> 4.53); q2 #6f894e -> #53673b (2.85 -> 4.54); q3 #b35b79 -> #914a62 (3.26 -> 4.55) */
:root[data-palette="kanagawa"] {
--bg: #e5ddb0;
--bg-elev: #f2ecbc;
--bg-sunken: #dcd5ac;
--bg-hover: rgba(67, 67, 80, 0.06);
--bg-active: rgba(67, 67, 80, 0.11);
--fg: #434350;
--fg-muted: #636155;
--fg-faint: #636051;
--border: #d5cea3;
--border-strong: #807c65;
--accent: #624c83;
--accent-fg: #ffffff;
--accent-soft: #d3c9aa;
--accent-soft-fg: #604a80;
--danger: #ac3747;
--danger-soft: rgba(172, 55, 71, 0.15);
--warn: #934e00;
--warn-soft: rgba(147, 78, 0, 0.15);
--success: #53673b;
--success-soft: rgba(83, 103, 59, 0.15);
--link: #47618f;
--unread-bg: #ffffff;
--read-bg: #e0d8ad;
--selected-bg: #d3c9aa;
--focus-ring: 0 0 0 3px rgba(98, 76, 131, 0.4);
--star: #77713f;
--q1: #47618f;
--q2: #53673b;
--q3: #914a62;
--scrollbar: rgba(99, 97, 85, 0.35);
color-scheme: light;
}
/* kanagawa (dark) lifted for contrast: muted #727169 -> #898881 (3.33 -> 4.59); faint #696866 -> #888886 (2.94 -> 4.60); danger #e82424 -> #ed5050 (3.66 -> 4.56); border-strong #4f4e57 -> #696970 (1.99 -> 3.00) */
:root[data-theme="dark"][data-palette="kanagawa"] {
--bg: #1f1f28;
--bg-elev: #2a2a37;
--bg-sunken: #16161d;
--bg-hover: rgba(220, 215, 186, 0.06);
--bg-active: rgba(220, 215, 186, 0.11);
--fg: #dcd7ba;
--fg-muted: #898881;
--fg-faint: #888886;
--border: #363646;
--border-strong: #696970;
--accent: #7e9cd8;
--accent-fg: #1f1f28;
--accent-soft: rgba(126, 156, 216, 0.16);
--accent-soft-fg: #7e9cd8;
--danger: #ed5050;
--danger-soft: rgba(237, 80, 80, 0.15);
--warn: #ff9e3b;
--warn-soft: rgba(255, 158, 59, 0.15);
--success: #98bb6c;
--success-soft: rgba(152, 187, 108, 0.15);
--link: #7fb4ca;
--unread-bg: #2a2a37;
--read-bg: #16161d;
--selected-bg: rgba(126, 156, 216, 0.18);
--focus-ring: 0 0 0 3px rgba(126, 156, 216, 0.4);
--star: #e6c384;
--q1: #7fb4ca;
--q2: #98bb6c;
--q3: #d27e99;
--scrollbar: rgba(137, 136, 129, 0.35);
color-scheme: dark;
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45);
--shadow-2: 0 8px 24px rgba(0, 0, 0, 0.55);
--shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75);
}
/* everforest (light) lifted for contrast: fg #5c6a72 -> #444e54 (4.66 -> 7.11); muted #939f91 -> #646c63 (2.30 -> 4.53); faint #8e9285 -> #686b61 (2.65 -> 4.53); link #3a94c5 -> #2c7096 (2.81 -> 4.53); danger #f85552 -> #b83f3d (2.73 -> 4.59); warn #f57d26 -> #a45419 (2.23 -> 4.53); success #8da101 -> #616f01 (2.42 -> 4.63); accent #8da101 -> #7d8f01 (2.42 -> 3.02); star #dfa000 -> #b07e00 (1.91 -> 3.00); border-strong #abb1a1 -> #84887c (1.84 -> 3.02); q1 #3a94c5 -> #2c7096 (2.81 -> 4.53); q2 #8da101 -> #616f01 (2.42 -> 4.63); q3 #df69ba -> #9e4b84 (2.55 -> 4.61) */
:root[data-palette="everforest"] {
--bg: #efebd4;
--bg-elev: #fdf6e3;
--bg-sunken: #e6e2cc;
--bg-hover: rgba(68, 78, 84, 0.06);
--bg-active: rgba(68, 78, 84, 0.11);
--fg: #444e54;
--fg-muted: #646c63;
--fg-faint: #686b61;
--border: #bdc3af;
--border-strong: #84887c;
--accent: #7d8f01;
--accent-fg: #ffffff;
--accent-soft: #dfdeb6;
--accent-soft-fg: #5a6701;
--danger: #b83f3d;
--danger-soft: rgba(184, 63, 61, 0.15);
--warn: #a45419;
--warn-soft: rgba(164, 84, 25, 0.15);
--success: #616f01;
--success-soft: rgba(97, 111, 1, 0.15);
--link: #2c7096;
--unread-bg: #ffffff;
--read-bg: #eae6d0;
--selected-bg: #dfdeb6;
--focus-ring: 0 0 0 3px rgba(125, 143, 1, 0.4);
--star: #b07e00;
--q1: #2c7096;
--q2: #616f01;
--q3: #9e4b84;
--scrollbar: rgba(100, 108, 99, 0.35);
color-scheme: light;
}
/* everforest (dark) lifted for contrast: muted #859289 -> #949f97 (3.84 -> 4.55); faint #757f7b -> #969e9b (3.02 -> 4.55); border-strong #5c6364 -> #787e7e (2.03 -> 3.02) */
:root[data-theme="dark"][data-palette="everforest"] {
--bg: #2d353b;
--bg-elev: #343f44;
--bg-sunken: #232a2e;
--bg-hover: rgba(211, 198, 170, 0.06);
--bg-active: rgba(211, 198, 170, 0.11);
--fg: #d3c6aa;
--fg-muted: #949f97;
--fg-faint: #969e9b;
--border: #475258;
--border-strong: #787e7e;
--accent: #a7c080;
--accent-fg: #2d353b;
--accent-soft: rgba(167, 192, 128, 0.16);
--accent-soft-fg: #a7c080;
--danger: #e67e80;
--danger-soft: rgba(230, 126, 128, 0.15);
--warn: #e69875;
--warn-soft: rgba(230, 152, 117, 0.15);
--success: #a7c080;
--success-soft: rgba(167, 192, 128, 0.15);
--link: #7fbbb3;
--unread-bg: #343f44;
--read-bg: #232a2e;
--selected-bg: rgba(167, 192, 128, 0.18);
--focus-ring: 0 0 0 3px rgba(167, 192, 128, 0.4);
--star: #dbbc7f;
--q1: #7fbbb3;
--q2: #a7c080;
--q3: #d699b6;
--scrollbar: rgba(148, 159, 151, 0.35);
color-scheme: dark;
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45);
--shadow-2: 0 8px 24px rgba(0, 0, 0, 0.55);
--shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75);
}
/* primer (light) lifted for contrast: faint #889098 -> #6b7278 (3.04 -> 4.58); star #bf8700 -> #bd8600 (2.95 -> 3.00); border-strong #b7bfc5 -> #898f94 (1.75 -> 3.07) */
:root[data-palette="primer"] {
--bg: #f6f8fa;
--bg-elev: #ffffff;
--bg-sunken: #eff2f5;
--bg-hover: rgba(37, 41, 46, 0.06);
--bg-active: rgba(37, 41, 46, 0.11);
--fg: #25292e;
--fg-muted: #59636e;
--fg-faint: #6b7278;
--border: #d1d9e0;
--border-strong: #898f94;
--accent: #0969da;
--accent-fg: #ffffff;
--accent-soft: #d5e4f6;
--accent-soft-fg: #0861c9;
--danger: #cf222e;
--danger-soft: rgba(207, 34, 46, 0.15);
--warn: #9a6700;
--warn-soft: rgba(154, 103, 0, 0.15);
--success: #1a7f37;
--success-soft: rgba(26, 127, 55, 0.15);
--link: #0550ae;
--unread-bg: #ffffff;
--read-bg: #f0f2f4;
--selected-bg: #d5e4f6;
--focus-ring: 0 0 0 3px rgba(9, 105, 218, 0.4);
--star: #bd8600;
--q1: #0550ae;
--q2: #116329;
--q3: #8250df;
--scrollbar: rgba(89, 99, 110, 0.35);
color-scheme: light;
}
/* primer (dark) lifted for contrast: faint #697078 -> #767d84 (3.78 -> 4.54); border-strong #585f67 -> #5a6169 (2.93 -> 3.02) */
:root[data-theme="dark"][data-palette="primer"] {
--bg: #0d1117;
--bg-elev: #151b23;
--bg-sunken: #010409;
--bg-hover: rgba(240, 246, 252, 0.06);
--bg-active: rgba(240, 246, 252, 0.11);
--fg: #f0f6fc;
--fg-muted: #9198a1;
--fg-faint: #767d84;
--border: #3d444d;
--border-strong: #5a6169;
--accent: #58a6ff;
--accent-fg: #0d1117;
--accent-soft: rgba(88, 166, 255, 0.16);
--accent-soft-fg: #58a6ff;
--danger: #ff7b72;
--danger-soft: rgba(255, 123, 114, 0.15);
--warn: #e3b341;
--warn-soft: rgba(227, 179, 65, 0.15);
--success: #3fb950;
--success-soft: rgba(63, 185, 80, 0.15);
--link: #79c0ff;
--unread-bg: #151b23;
--read-bg: #010409;
--selected-bg: rgba(88, 166, 255, 0.18);
--focus-ring: 0 0 0 3px rgba(88, 166, 255, 0.4);
--star: #d29922;
--q1: #79c0ff;
--q2: #56d364;
--q3: #d2a8ff;
--scrollbar: rgba(145, 152, 161, 0.35);
color-scheme: dark;
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45);
--shadow-2: 0 8px 24px rgba(0, 0, 0, 0.55);
--shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75);
}
/* === generated palettes: end === */ /* === generated palettes: end === */
/* Accent variants */ /* Accent variants */
@@ -33,6 +33,30 @@ const PALETTE_PREVIEW: Record<PaletteId, { light: string; dark: string }> = {
light: "linear-gradient(135deg,#e6e7ed 0%,#d6d8df 55%,#2959aa 55%,#2959aa 78%,#8c4351 78%)", light: "linear-gradient(135deg,#e6e7ed 0%,#d6d8df 55%,#2959aa 55%,#2959aa 78%,#8c4351 78%)",
dark: "linear-gradient(135deg,#1a1b26 0%,#1f2130 55%,#7aa2f7 55%,#7aa2f7 78%,#bb9af7 78%)", dark: "linear-gradient(135deg,#1a1b26 0%,#1f2130 55%,#7aa2f7 55%,#7aa2f7 78%,#bb9af7 78%)",
}, },
catppuccin: {
light: "linear-gradient(135deg,#e6e9ef 0%,#eff1f5 55%,#8839ef 55%,#8839ef 78%,#ea76cb 78%)",
dark: "linear-gradient(135deg,#1e1e2e 0%,#313244 55%,#cba6f7 55%,#cba6f7 78%,#f5c2e7 78%)",
},
solarized: {
light: "linear-gradient(135deg,#fdf6e3 0%,#eee8d5 55%,#268bd2 55%,#268bd2 78%,#cb4b16 78%)",
dark: "linear-gradient(135deg,#002b36 0%,#073642 55%,#268bd2 55%,#268bd2 78%,#cb4b16 78%)",
},
ayu: {
light: "linear-gradient(135deg,#f8f9fa 0%,#ebeef0 55%,#f29718 55%,#f29718 78%,#55b4d4 78%)",
dark: "linear-gradient(135deg,#0d1017 0%,#10141c 55%,#e6b450 55%,#e6b450 78%,#39bae6 78%)",
},
kanagawa: {
light: "linear-gradient(135deg,#e5ddb0 0%,#f2ecbc 55%,#624c83 55%,#624c83 78%,#b35b79 78%)",
dark: "linear-gradient(135deg,#1f1f28 0%,#2a2a37 55%,#7e9cd8 55%,#7e9cd8 78%,#d27e99 78%)",
},
everforest: {
light: "linear-gradient(135deg,#efebd4 0%,#fdf6e3 55%,#8da101 55%,#8da101 78%,#df69ba 78%)",
dark: "linear-gradient(135deg,#2d353b 0%,#343f44 55%,#a7c080 55%,#a7c080 78%,#d699b6 78%)",
},
primer: {
light: "linear-gradient(135deg,#f6f8fa 0%,#ffffff 55%,#0969da 55%,#0969da 78%,#8250df 78%)",
dark: "linear-gradient(135deg,#0d1117 0%,#151b23 55%,#58a6ff 55%,#58a6ff 78%,#d2a8ff 78%)",
},
}; };
const MODES: Array<{ id: Mode; label: string }> = [ const MODES: Array<{ id: Mode; label: string }> = [