hex everywhere, because the palettes were half broken

Reported as "the ellipsis doesn't show in the streaming bubble". It was the visible
corner of something much larger: every Tailwind semantic utility was broken under all
eleven ported palettes.

`@theme` mapped `--color-muted-foreground: hsl(var(--muted-foreground))`, which needs an
unwrapped triplet. A ported palette publishes `#6e6a86`, so the mapping produced
`hsl(#6e6a86)` — invalid — and the utility fell back to white or transparent:

  duck-dark    text-muted-foreground=rgb(148,163,184)   bg-muted=rgb(30,41,59)
  rose-pine    text-muted-foreground=rgb(255,255,255)   bg-muted=rgba(0,0,0,0)

The themes looked convincing because panels, the page and the header read their
variables DIRECTLY rather than through a utility class. Everything that went through
Tailwind was quietly wrong, and the thinking dots — `bg-muted-foreground/60` — were
simply invisible.

So: 123 HSL triplets converted to hex across the three hand-written themes, and 37
`hsl(var(--x))` mappings unwrapped. One format everywhere, which is also the format
every published palette ships in.

The triplets only ever existed so `bg-card/50` could get opacity. That was tested
earlier tonight and turned out unnecessary — Tailwind v4 emits
`color-mix(in oklab, var(--card) 50%, transparent)` for any colour format. Having proved
it, I then left the mappings wrapped, which is what broke the palettes.

Verified across six themes: `bg-muted` and `bg-muted-foreground/60` resolve in every one
of them, and Duck's values are unchanged through the conversion — `bg-muted` is
`rgb(30,41,59)` before and after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 01:08:54 +00:00
co-authored by Claude Opus 5
parent 94a55fe679
commit 8fa3f7da3e
4 changed files with 177 additions and 164 deletions
+50 -37
View File
@@ -14,7 +14,7 @@
── How a value reaches a component ──
component .bg-surface
@theme (here) --color-surface: hsl(var(--surface))
@theme (here) --color-surface: var(--surface)
theme file --surface: 0 0% 100%
The indirection through `var()` is load-bearing: Tailwind resolves `@theme` at BUILD time, so a theme
@@ -57,64 +57,77 @@
}
}
/*
Values are HEX, not unwrapped HSL triplets.
============================================================================================
This block used to read `--color-card: hsl(var(--card))`, which requires every theme to publish an
unwrapped triplet (`0 0% 100%`). That was done so `bg-card/50` could get opacity — and it turned out to
be unnecessary: Tailwind v4 emits `color-mix(in oklab, var(--card) 50%, transparent)` for any colour
format, with a `@supports` fallback. Tested before changing it.
It was also actively wrong the moment a theme arrived in hex: `hsl(#6e6a86)` is invalid, so every
semantic utility silently fell back to white or transparent. Eleven ported palettes looked correct only
because panels and the page read their variables directly rather than through a utility class.
*/
@theme {
/* ── Semantic colour, the whole vocabulary a component is allowed to use ──────────────────────── */
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-brand: hsl(var(--brand));
--color-brand-foreground: hsl(var(--brand-foreground));
--color-brand-muted: hsl(var(--brand-muted));
--color-brand: var(--brand);
--color-brand-foreground: var(--brand-foreground);
--color-brand-muted: var(--brand-muted);
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: hsl(var(--success));
--color-success-foreground: hsl(var(--success-foreground));
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: hsl(var(--warning));
--color-warning-foreground: hsl(var(--warning-foreground));
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-info: hsl(var(--info));
--color-info-foreground: hsl(var(--info-foreground));
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
/* The sixth state, and the reason it exists: `running` is neither good news nor a warning. The chat
tray, the send button and the queue all wore amber for it, and the token set had nothing to offer —
`--status-in-progress` is blue, and `warning` would make a working job look like a failing one.
`-ink` is the readable-on-a-tint variant: the fill is bright enough to sit behind black text, which
makes it far too light to BE text. Every tone will want this eventually; busy needed it first. */
--color-busy: hsl(var(--busy));
--color-busy-foreground: hsl(var(--busy-foreground));
--color-busy-ink: hsl(var(--busy-ink));
--color-busy: var(--busy);
--color-busy-foreground: var(--busy-foreground);
--color-busy-ink: var(--busy-ink);
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-cta: hsl(var(--cta));
--color-cta-foreground: hsl(var(--cta-foreground));
--color-ctahover: hsl(var(--ctahover));
--color-cta: var(--cta);
--color-cta-foreground: var(--cta-foreground);
--color-ctahover: var(--ctahover);
--color-naturegreen: hsl(var(--naturegreen));
--color-naturegreen-foreground: hsl(var(--naturegreen-foreground));
--color-naturegreen: var(--naturegreen);
--color-naturegreen-foreground: var(--naturegreen-foreground);
/*
── The brand slots ────────────────────────────────────────────────────────────────────────────