Foundations
Colors
Curation's palette divides along one line. Surfaces, hairlines and hover plates are a single base colour plus an opacity modifier, so they invert cleanly between themes. Text names a rung of a fixed ladder, because alpha does not survive the flip where legibility is the point. Every swatch below is painted with the live token — use the theme toggle above to read both palettes.
Surfaces
The canvas is a mid grey, not an off-white — one hue (213) at 16% saturation across every step, which is why the greys cohere. Containers rise above it: a card is lighter than the page in light mode. Dark keeps the legacy app's warm near-black unchanged, so the two themes are deliberately in different hue families.
| Token | Light | Dark | Role |
|---|---|---|---|
--canvas | #e2e5ea | #110f0d | The page ground. |
--canvas-raised | #f1f3f5 | #131a16 | Cards and containers, a step above the page. |
--canvas-deep | #d6dbe1 | #0d110f | Recessed wells and controls. |
The ink ladder
--ink is deliberately not white or black: in light mode text-white
would have to mean near-black. Ink names the role, and reads correctly in both
themes. Text takes a rung, not an alpha — black at 40% on light reaches 2.4:1
where white at 40% on dark reaches 3.8:1, so a shared alpha cannot hold a
legibility contract. The dark column lists the old alpha equivalents. Those
colours are now mixed with the canvas into opaque paint, preserving their tone
on the page while keeping inherited SVG strokes uniform where paths overlap.
| Token | Light | Dark | Contrast (light / dark) |
|---|---|---|---|
--ink | #15191e | #ffffff | 13.98 / 19.13 — primary text. |
--ink-strong | #22282f | white / 85% | 11.77 / 13.82 — emphatic secondary text. |
--ink-secondary | #3e4956 | white / 65% | 7.25 / 8.34 — body and supporting copy. |
--ink-tertiary | #4f5d6d | white / 40% | 5.33 / 3.82 — inactive-but-clickable labels; held above AA on light on purpose. |
--ink-quiet | #8693a3 | white / 28% | 2.48 / 2.46 — captions and incidental marks. |
--ink-inverse | #f8f9fa | #121212 | Text on a plate of solid ink. |
The ink ladder, top rung
Body copy sits one rung down
Inactive but clickable
A quiet caption
<p className="text-ink">Primary</p> <p className="text-ink-secondary">Secondary</p> <p className="text-ink-tertiary">Tertiary</p> <p className="text-ink-quiet">Quiet</p>
Tints and hairlines
Everything that is not text stays on the alpha model: a white tint raising a
surface off a dark ground becomes a black tint recessing it into a light one.
The working strengths are legacy's own — bg-ink/[0.03] for a resting plate,
bg-ink/5 for a hover, border-ink/10 for a hairline. Dark's hairlines are
finer (--border drops to 5%), because a white tint on near-black reads
heavier than a black tint on grey at the same alpha.
ink / 3%
ink / 6%
border
<div className="rounded-lg bg-ink/[0.03]" /> // resting plate <div className="rounded-lg bg-ink/[0.06]" /> // hover / highlighted <div className="rounded-lg border-[0.09rem] border-ink/10" />
Brand
The brand is black, as the mobile app's is — black primary actions and
neutral emphasis, inverting to white in dark mode. It is named brand, not
accent, because shadcn reserves accent for its subtle hover surface. It has
three jobs: a decoration tone for tints, a foreground (mobile's accent) and
a plate (mobile's inverse). The plate sits a step under the ink on light, so
a button and the type beside it need not be one black.
| Token | Light | Dark | Job |
|---|---|---|---|
--brand | #15191e | #ffffff | Decoration and tints — the ink itself. |
--brand-ink | #15191e | #ffffff | The brand as a foreground — links, active marks, the focus ring. |
--brand-fill | #22282f | #ffffff | The brand as a solid plate. Two names because a plate and a letterform need not stay in step. |
--brand-foreground | #f8f9fa | #121212 | The on-colour of the plate, so it flips with it. |
--brand-fill-sheen | #22282f | #ffffff | The lighter stop of the marketing CTA gradient — the plate itself, so the CTA draws flat. |
Publish
A brand-coloured link
A brand tint
<button className="bg-brand-fill text-brand-foreground">Publish</button> <a className="text-brand-ink">A brand-coloured link</a> <span className="bg-brand/20 text-brand-ink">A brand tint</span>
Two further display tones — --ink-warm and --brand-soft — belong to the
marketing home alone: a quieter ink for the hero headline and a gentler brand
for the manifesto, each quieter than its base in either theme.
Status
The reds differ by theme for the same reason the greys do: legacy's #f44e4e
is tuned against near-black and reaches only 3.3:1 on a light ground, too thin
for the error copy it carries. The warning tone is stated at reading strength
rather than as an amber to be faded — it is a text colour.
| Token | Light | Dark | Role |
|---|---|---|---|
--danger | #be2f27 | #f44e4e | Errors and destructive actions. |
--success | #1f9d55 | #27ae60 | Confirmations. |
--warning | #7a5008 | #a29357 | Caution notes — “couldn't verify”, “upload failed”. |
--vip | #8a7649 | #ae9c75 | Membership marks. |
The semantic layer
shadcn/ui components are written against a fixed set of semantic names. Those
names are mapped onto Curation's colours in app/globals.css, so an
unmodified shadcn component already belongs. The tints are derived from
--ink rather than restated per theme — one definition that follows whichever
palette is live.
| Semantic token | Resolves to |
|---|---|
--background / --foreground | --canvas / --ink |
--card | --canvas-raised |
--popover / --panel | A step off the canvas, not a tint of it — these float above their own scrim, and a translucent tint would show the page through. |
--primary | --brand-fill, with --brand-foreground on it. |
--secondary / --accent | ink / 5% — shadcn's accent is the subtle hover surface, not the brand. |
--muted | ink / 3%, with --ink-tertiary as its foreground. |
--destructive | --danger |
--border / --input | ink / 10% on light, ink / 5% on dark. |
--ring | --brand-ink |