v3: melange joins the styles — savq/melange-nvim's palette taken verbatim (the a.* neutrals, b.yellow as the accent since it's the scheme's Function colour, the dusty c.* row as furniture), but built as a warm 70s print instead of a second editor skin: one cut of Fraunces with opsz pinned low, so a headline and the caption under it stay the same face; pillowy borderless cards lifted on warm sunlight shadow rather than held inside a line; the mélange itself — every desert hue blended once — laid along the bottom edge of the header; and grain over the whole page, which has to differ per scheme because soft-light grey noise is invisible on a pale ground: fine film grain on the coffee dark, a coarser multiply tooth on the plaster light, both switched through custom properties since light-dark() only carries colours. Light keeps the warmth in the page and the bleach in the cards — a.float underneath, tints above — since melange's own light bg reads grey next to its floats; the header takes a lighter shade of its own so it sits over the page as a board.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-09-19 22:32:26 +00:00
parent d70761ecae
commit 27c55ea96f
5 changed files with 352 additions and 2 deletions

View File

@@ -10,7 +10,7 @@
<link rel="icon" type="image/png" sizes="192x192" href="/favicon-192x192.png?v=__ICON_VERSION__" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Saira:ital,wght@0,100..900;1,100..900&family=Space+Grotesk:wght@400;700;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght,SOFT,WONK@0,9..144,300..900,0..100,0..1;1,9..144,300..900,0..100,0..1&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Saira:ital,wght@0,100..900;1,100..900&family=Space+Grotesk:wght@400;700;900&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#01152a" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#faf5e9" />

View File

@@ -1,7 +1,12 @@
import { createContext } from "react";
export type StyleName =
"smooth" | "brutalist" | "geocities" | "nyt" | "kanagawa";
| "smooth"
| "brutalist"
| "geocities"
| "nyt"
| "kanagawa"
| "melange";
export type ColorScheme = "auto" | "light" | "dark";
export interface ThemeContextValue {

View File

@@ -12,6 +12,7 @@ import "./themes/brutalist.css";
import "./themes/geocities.css";
import "./themes/nyt.css";
import "./themes/kanagawa.css";
import "./themes/melange.css";
await loadCatalog();

View File

@@ -1238,6 +1238,13 @@ export function UserPublicProfile() {
>
Kanagawa
</button>
<button
type="button"
className={style === "melange" ? "active" : ""}
onClick={() => setStyle("melange")}
>
Melange
</button>
</div>
</div>
<div className="profile-appearance-row">

337
src/themes/melange.css Normal file
View File

@@ -0,0 +1,337 @@
/* ══════════════════════════════════════════════════════════════════════
Melange theme — after savq/melange-nvim, whose palette is pure 1970s
desert: toasted amber, terracotta, olive, dusty rose, laid on roasted
coffee (dark) or warm plaster (light).
So this is not an editor skin — it's a warm print. The scheme's own
colours are kept exactly; what's built on them is analog: a soft
Windsor-ish serif, film grain over the whole page, pillowy borderless
cards that sit in warm shadow rather than inside a line, and a spice
gradient — the mélange itself, every hue blended — along the header.
Dark scheme → melange dark (#292522 grounds, cream ink).
Light scheme → melange light (plaster, and every ink is a brown).
══════════════════════════════════════════════════════════════════════ */
/* ── Palette (light-dark: see tokens.css) ─────────────────────────── */
:root[data-style="melange"] {
/* One family, soft-serif: Fraunces sits at the Souvenir/Cooper end of
the 70s, which is exactly where this palette lives.
Its optical-size axis is the catch — left on `auto`, a headline is
cut as a high-contrast Didone while the caption under it is a sturdy
slab, and one family starts reading as three. So opsz is pinned low
and everywhere: sturdy, low-contrast, warm at every size. Any rule
that touches SOFT or WONK repeats opsz, since font-variation-settings
replaces the whole list rather than merging into it. */
--font-body: "Fraunces", Georgia, serif;
--font-display: "Fraunces", Georgia, serif;
--mel-opsz: 14;
/* Nothing here has a sharp corner. */
--radius-xs: 5px;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 22px;
/* Text — a.fg / a.com / a.ui */
--color-text: light-dark(#54433a, #ece1d7);
--color-text-secondary: light-dark(#7d6658, #c1a78e);
--color-text-muted: light-dark(#a98a78, #867462);
--color-on-accent: light-dark(#f7f4f1, #292522);
/* Surfaces — sunken < bg < surface < raised.
Dark: below a.bg · a.bg · a.float · a.sel.
Light: a.sel is the well, a.float is the *page* — melange's light
side keeps its warmth in the floats, so on a page made of
cards the plaster belongs underneath and the cards are the
bleached tints above it. Reversing those two tiers is what
keeps the light scheme from reading as grey. */
--color-bg: light-dark(#e9e1db, #292522);
--color-surface: light-dark(#f6f2ef, #34302c);
--color-surface-raised: light-dark(#fdfbf9, #403a36);
--color-surface-sunken: light-dark(#d9d3ce, #201d1b);
/* Borders — a.ui, but mostly unused: this theme separates by warmth
and shadow, not by line. */
--color-border: light-dark(rgb(169 138 120 / 0.38), rgb(134 116 98 / 0.45));
--color-border-subtle: light-dark(
rgb(169 138 120 / 0.18),
rgb(134 116 98 / 0.22)
);
/* Accent — b.yellow, the spice. Hover walks it toward c.yellow's burn. */
--color-accent: light-dark(#a06d00, #ebc06d);
--color-accent-hover: light-dark(#bc5c00, #e49b5d);
--color-accent-soft: light-dark(
rgb(160 109 0 / 0.13),
rgb(235 192 109 / 0.15)
);
--color-focus-ring: light-dark(
rgb(160 109 0 / 0.35),
rgb(235 192 109 / 0.4)
);
/* Links */
--color-link: light-dark(#a06d00, #ebc06d);
--color-link-hover: light-dark(#bc5c00, #e49b5d);
/* Danger — b.red on d.red */
--color-danger: light-dark(#bf0021, #d47766);
--color-danger-bg: light-dark(#f1dedf, #7d2a2f);
--color-danger-hover: light-dark(#c77b8b, #bd8183);
/* Success — b.green on d.green */
--color-success: light-dark(#3a684a, #85b695);
--color-success-bg: light-dark(#d0e9d1, #233524);
--color-success-hover: light-dark(#6e9b72, #78997a);
/* Warning — c.yellow */
--color-warning: light-dark(#bc5c00, #e49b5d);
/* Overlays */
--color-overlay: light-dark(rgb(84 67 58 / 0.3), rgb(32 29 27 / 0.72));
--color-header-user-bg: transparent;
--color-header-user-bg-hover: light-dark(
rgb(84 67 58 / 0.07),
rgb(236 225 215 / 0.07)
);
/* Misc */
--color-option-bg: light-dark(#e9e1db, #34302c);
--color-option-border: light-dark(
rgb(169 138 120 / 0.18),
rgb(134 116 98 / 0.22)
);
/* The rest of the desert — melange's c.* row, dusty rather than bright.
These are the theme's own furniture colours. */
--mel-terracotta: light-dark(#bc5c00, #e49b5d);
--mel-olive: light-dark(#6e9b72, #78997a);
--mel-rose: light-dark(#be79bb, #b380b0);
--mel-clay: light-dark(#c77b8b, #bd8183);
--mel-dust: light-dark(#a98a78, #867462);
/* The mélange: every hue of the desert, blended once, left to right. */
--mel-spice: linear-gradient(
90deg,
var(--color-accent),
var(--mel-terracotta) 32%,
var(--mel-clay) 58%,
var(--mel-rose) 78%,
var(--mel-olive)
);
/* Shadows are warm and wide — sunlight, not a drop shadow. Light needs
more of them than dark: on plaster, shadow is the only thing holding
a card off the page. */
--shadow-color: light-dark(rgb(122 84 46 / 0.26), rgb(12 9 8 / 0.5));
--shadow-color-soft: light-dark(rgb(122 84 46 / 0.16), rgb(12 9 8 / 0.32));
--shadow-color-strong: light-dark(rgb(122 84 46 / 0.36), rgb(12 9 8 / 0.7));
--shadow-1: 0 2px 10px -2px var(--shadow-color-soft);
--shadow-2: 0 14px 34px -10px var(--shadow-color),
0 2px 6px var(--shadow-color-soft);
--shadow-3: 0 32px 70px -16px var(--shadow-color-strong),
0 3px 10px var(--shadow-color-soft);
/* Grain — one tile, two treatments (see body::after). Defaults are the
dark ones; the light scheme overrides them by selector. */
--mel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)' opacity='0.22'/%3E%3C/svg%3E");
--mel-grain-blend: soft-light;
--mel-grain-opacity: 0.55;
/* Glow: the spice catches light where it's interactive. */
--mel-glow: 0 3px 16px -4px
light-dark(rgb(160 109 0 / 0.45), rgb(235 192 109 / 0.35));
/* Component skins — no lines anywhere; warmth and shadow do the work. */
--border-width: 1px;
/* The header is a board laid over the page, lighter than it in both
schemes: a.float on coffee, the bleached tint on plaster. */
--header-bg: light-dark(#fdfbf9, #34302c);
--input-focus-bg: var(--color-surface-raised);
--overlay-filter: blur(6px);
--modal-border: transparent;
--card-border: transparent;
--card-border-width: 1px;
--card-shadow: var(--shadow-1);
--card-hover-border: transparent;
--panel-ring: var(--shadow-1);
--tab-border: transparent;
--btn-primary-shadow: var(--mel-glow);
--btn-primary-shadow-hover: 0 5px 22px -4px
light-dark(rgb(160 109 0 / 0.55), rgb(235 192 109 / 0.45));
}
/* ══════════════════════════════════════════════════════════════════════
Structural overrides
══════════════════════════════════════════════════════════════════════ */
/* ── One cut of Fraunces, at one optical size, for every size of text ── */
:root[data-style="melange"],
[data-style="melange"] body {
font-optical-sizing: none;
font-variation-settings: "opsz" var(--mel-opsz), "SOFT" 40, "WONK" 0;
}
/* ── The page is sunlit: warm light pools at the top, the ground below
is flat desert. ───────────────────────────────────────────────── */
[data-style="melange"] body {
background-image: radial-gradient(
120% 55% at 50% -12%,
var(--color-accent-soft),
transparent 70%
);
background-repeat: no-repeat;
background-attachment: fixed;
}
/* ── Film grain over everything, so no surface reads as flat pixels.
Sits above the page, below modals and toasts.
The two schemes need opposite grain. On coffee, `soft-light` grits
the surface without lifting the blacks. On plaster that same blend
is invisible — pale grey noise on a pale ground has nothing to
work against — so light multiplies instead, which is what paper
tooth actually is: tiny shadows. Neither blend nor opacity takes
light-dark(), so the two parameters are custom properties and the
scheme is picked by selector below. ───────────────────────────── */
[data-style="melange"] body::after {
content: "";
position: fixed;
inset: 0;
z-index: calc(var(--z-modal) - 1);
pointer-events: none;
opacity: var(--mel-grain-opacity);
mix-blend-mode: var(--mel-grain-blend);
background-image: var(--mel-grain);
}
/* Auto (no forced scheme) follows the OS; the attribute forms win over
it because they come later and are more specific. */
@media (prefers-color-scheme: light) {
:root[data-style="melange"] {
--mel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.55' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)' opacity='0.42'/%3E%3C/svg%3E");
--mel-grain-blend: multiply;
--mel-grain-opacity: 0.85;
}
}
:root[data-style="melange"][data-color-scheme="light"] {
--mel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.55' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)' opacity='0.42'/%3E%3C/svg%3E");
--mel-grain-blend: multiply;
--mel-grain-opacity: 0.85;
}
/* Repeats the fine tile too: forcing dark under a light OS would
otherwise inherit the coarse one from the media query above. */
:root[data-style="melange"][data-color-scheme="dark"] {
--mel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)' opacity='0.22'/%3E%3C/svg%3E");
--mel-grain-blend: soft-light;
--mel-grain-opacity: 0.55;
}
@media (prefers-reduced-transparency: reduce) {
[data-style="melange"] body::after {
display: none;
}
}
/* ── Header — a board over the page: no rule under it, just its own
shade, a warm shadow, and the spice laid along its bottom edge as
a single blended stripe. ──────────────────────────────────────── */
[data-style="melange"] .app-header {
border-bottom: none;
box-shadow: var(--shadow-1);
z-index: var(--z-raised);
}
[data-style="melange"] .app-header::after {
content: "";
position: absolute;
inset: auto 0 0 0;
height: 2px;
background: var(--mel-spice);
opacity: 0.7;
}
/* Brand — soft and a little wonky, the way 70s display type was cut. */
[data-style="melange"] .app-header-brand {
color: var(--mel-terracotta);
font-weight: 600;
font-variation-settings: "opsz" var(--mel-opsz), "SOFT" 100, "WONK" 1;
letter-spacing: -0.01em;
}
[data-style="melange"] .form-page-title,
[data-style="melange"] .profile-subpage-title,
[data-style="melange"] .profile-section-title,
[data-style="melange"] .comment-section-title,
[data-style="melange"] .related-section-title {
font-variation-settings: "opsz" var(--mel-opsz), "SOFT" 80, "WONK" 1;
}
/* Tabs keep the shared uppercase cut, but the active one glows. */
[data-style="melange"] .feed-sort-btn.active {
box-shadow: var(--mel-glow);
}
[data-style="melange"] .feed-sort-btn:hover:not(.active) {
background: var(--color-accent-soft);
}
/* ── Cards sit in the light rather than inside a line; they rise a
little when pointed at. ───────────────────────────────────────── */
[data-style="melange"] .dump-card,
[data-style="melange"] .journal-card,
[data-style="melange"] .playlist-card {
transition:
box-shadow var(--dur-slow) var(--ease-out),
transform var(--dur-slow) var(--ease-out);
}
[data-style="melange"] .dump-card:hover,
[data-style="melange"] .journal-card:hover,
[data-style="melange"] .playlist-card:hover {
box-shadow: var(--shadow-2);
transform: translateY(-2px);
}
@media (prefers-reduced-motion: reduce) {
[data-style="melange"] .dump-card:hover,
[data-style="melange"] .journal-card:hover,
[data-style="melange"] .playlist-card:hover {
transform: none;
}
}
/* ── Metadata is dust: quiet, warm, never black ──────────────────── */
[data-style="melange"] .journal-card:not(.journal-card--image) .journal-card-meta,
[data-style="melange"] .dump-card-date,
[data-style="melange"] .comment-time,
[data-style="melange"] .notification-time {
color: var(--mel-dust);
}
/* ── A quote is a printed pull-quote: big soft mark, terracotta ──── */
[data-style="melange"] .journal-card--quote {
background: var(--color-surface);
}
[data-style="melange"] .journal-card-quotemark {
color: var(--mel-terracotta);
opacity: 0.28;
font-variation-settings: "opsz" var(--mel-opsz), "SOFT" 100, "WONK" 1;
}
[data-style="melange"] .journal-card-quote-body {
font-size: var(--text-lg);
line-height: 1.35;
font-variation-settings: "opsz" var(--mel-opsz), "SOFT" 70, "WONK" 0;
}
/* Vote tallies wear the dusty rose of the palette's far end */
[data-style="melange"] .vote-btn:not(.vote-btn--active) {
color: var(--mel-rose);
}