// SPDX-FileCopyrightText: 2025-2026 Kat Suricata
// SPDX-License-Identifier: (CC-BY-4.0+ OR Apache-2.0+)
export interface DrColor {
/** Kebab-case identifier. Becomes the CSS custom property `--`. */
id: string;
/** Display name. Stable across the spec, the site, and every port. */
name: string;
hex: string;
/** Role note from the spec palette tables. */
description?: string;
}
export const rustRamp: DrColor[] = [
{
id: 'ghost-pixel',
name: 'Ghost Pixel',
hex: '#F7EAE8',
description: 'Lightest / White Point (text)'
},
{ id: 'phosphor-smoke', name: 'Phosphor Smoke', hex: '#EFD5D1' },
{
id: 'phantom-current',
name: 'Phantom Current',
hex: '#E7C0BA',
description: 'Link Colour/Footer Text'
},
{ id: 'corrupted-cache', name: 'Corrupted Cache', hex: '#DFABA3' },
{ id: 'copper-trace', name: 'Copper Trace', hex: '#D7968C' },
{ id: 'fatal-error', name: 'Fatal Error', hex: '#CF8175' },
{ id: 'rouge-signal', name: 'Rouge Signal', hex: '#C76C5E' },
{ id: 'amber-alert', name: 'Amber Alert', hex: '#BF5747' },
{ id: 'firewall-breach', name: 'Firewall Breach', hex: '#B74230' },
{
id: 'digital-rust',
name: 'Digital Rust',
hex: '#AF2E1A',
description: 'Primary Colour/Primary Accent'
},
{ id: 'kernel-panic', name: 'Kernel Panic', hex: '#9D2917' },
{ id: 'data-rot', name: 'Data Rot', hex: '#8C2414' },
{ id: 'bricked-board', name: 'Bricked Board', hex: '#7A2012' },
{ id: 'dark-packet', name: 'Dark Packet', hex: '#691B0F' },
{ id: 'daemon-blood', name: 'Daemon Blood', hex: '#57170D' },
{ id: 'bad-sector', name: 'Bad Sector', hex: '#46120A' },
{
id: 'core-meltdown',
name: 'Core Meltdown',
hex: '#340D07',
description: 'Secondary Background Colour'
},
{ id: 'null-pointer', name: 'Null Pointer', hex: '#230905' },
{
id: 'event-horizon',
name: 'Event Horizon',
hex: '#110402',
description: 'Darkest/Black Point (background)'
}
];
// One-off hexes that have no display name because they are not part of the public palette.
export const internalHex = {
/* Mid-rust surface for active/pressed fills, between Daemon Blood and Dark Packet. */
surfaceActive: '#5A1810',
/*
* Text on a hot panel (Bricked Board). The pastel accents fail WCAG AA there:
* Memory Leek measures 3.83:1, Dead Thread 3.70:1, and Amber Alert Text 2.67:1.
* Each value below clears 4.60:1 and keeps the family hue.
*/
onHotSuccess: '#9AB867',
onHotWarning: '#D9A540',
onHotDanger: '#FF8E7B',
onHotInfo: '#73BAB4',
/*
* Safe Mode role values. Safe Mode adds no public colour name, so the
* projected values live here. The syntax specification derives each one with
* the projection rule: keep the hue, hold the ratio against the strictest
* surface the role can meet, and keep the chroma near the dark value.
*/
quietText: '#664C49',
heading: '#75322A',
quietBorder: '#D7968C',
strongBorder: '#D7968C',
invalidBorder: '#8D362A',
syntaxKeyword: '#75322A',
syntaxFunction: '#4F3D1C',
syntaxConstant: '#3E2C00',
syntaxBuiltin: '#6B5122',
syntaxString: '#324513',
syntaxNumber: '#6E320C',
syntaxType: '#194946',
syntaxComment: '#7C523A',
syntaxOperator: '#624843',
syntaxPunctuation: '#705A57',
syntaxBlue: '#3F4965',
syntaxMagenta: '#934461',
syntaxError: '#8D362A',
feedbackSuccess: '#455925',
feedbackWarning: '#6C4D06',
feedbackDanger: '#8D362A',
feedbackInfo: '#2B5A56',
/* Scrollbar thumb, and its hover. Both clear 3:1 against the page. */
scrollbarThumb: '#9D2917',
scrollbarThumbHover: '#8C2414',
/* Safe Mode terminal slots, from the projection rule against Ghost Pixel. */
termRed: '#BF3C26',
termGreen: '#3B5218',
termBlue: '#495370',
termMagenta: '#A84A6D',
termCyan: '#1E5451',
termBrightRed: '#63231D',
termBrightGreen: '#3F4F27',
termBrightYellow: '#4C3600',
termBrightBlue: '#434857',
termBrightMagenta: '#934865',
termBrightCyan: '#2C504D',
/* The light Stack Overglow stop: 9.16:1 on the page, and Ghost Pixel on it passes AA. */
overglowLight: '#533900',
/* The rim scorch that replaces the scanline band on a light page. */
pageScorch: 'radial-gradient(ellipse at 50% 50%, transparent 55%, rgba(175, 46, 26, 0.1) 100%)'
} as const;
// Raw Amber Alert fails WCAG AA for text on base and elevated surfaces. This variant passes.
export const amberAlertText: DrColor = {
id: 'amber-alert-text',
name: 'Amber Alert (text)',
hex: '#CB6353',
description: 'WCAG AA compliant text variant of Amber Alert'
};
export interface DrAccentFamily {
/** Family display name, for example "Memory Leek". */
name: string;
/** ANSI slot the base colour fills, for example "Green". */
ansi: string;
base: DrColor;
dim: DrColor;
bright: DrColor;
}
export const accentFamilies: DrAccentFamily[] = [
{
name: 'Memory Leek',
ansi: 'Green',
base: { id: 'memory-leek', name: 'Memory Leek', hex: '#8FA667' },
dim: { id: 'dim-memory-leek', name: 'Dim Memory Leek', hex: '#627247' },
bright: { id: 'bright-memory-leek', name: 'Bright Memory Leek', hex: '#94A577' }
},
{
name: 'Stack Overglow',
ansi: 'Yellow',
base: { id: 'stack-overglow', name: 'Stack Overglow', hex: '#D4A759' },
dim: { id: 'dim-stack-overglow', name: 'Dim Stack Overglow', hex: '#A07E43' },
bright: { id: 'bright-stack-overglow', name: 'Bright Stack Overglow', hex: '#D3AE6E' }
},
{
name: 'Blue Screen',
ansi: 'Blue',
base: { id: 'blue-screen', name: 'Blue Screen', hex: '#8791B0' },
dim: { id: 'dim-blue-screen', name: 'Dim Blue Screen', hex: '#60677C' },
bright: { id: 'bright-blue-screen', name: 'Bright Blue Screen', hex: '#999EAF' }
},
{
name: 'Fractured Shell',
ansi: 'Magenta',
base: { id: 'fractured-shell', name: 'Fractured Shell', hex: '#BA5A7D' },
dim: { id: 'dim-fractured-shell', name: 'Dim Fractured Shell', hex: '#87405A' },
bright: { id: 'bright-fractured-shell', name: 'Bright Fractured Shell', hex: '#BA6B88' }
},
{
name: 'Dead Thread',
ansi: 'Cyan',
base: { id: 'dead-thread', name: 'Dead Thread', hex: '#6FA5A0' },
dim: { id: 'dim-dead-thread', name: 'Dim Dead Thread', hex: '#4C726E' },
bright: { id: 'bright-dead-thread', name: 'Bright Dead Thread', hex: '#7FA5A1' }
},
{
name: 'Thermal Throttle',
ansi: 'Orange',
base: { id: 'thermal-throttle', name: 'Thermal Throttle', hex: '#D78556' },
dim: { id: 'dim-thermal-throttle', name: 'Dim Thermal Throttle', hex: '#AC6D4A' },
bright: { id: 'bright-thermal-throttle', name: 'Bright Thermal Throttle', hex: '#D6926B' }
}
];
export interface AnsiSlot {
slot: string;
colorId: string;
}
// Canonical 16-colour terminal mapping from the spec's ANSI Color Mapping table.
export const ansiSlots: AnsiSlot[] = [
{ slot: 'Black', colorId: 'event-horizon' },
{ slot: 'Red', colorId: 'rouge-signal' },
{ slot: 'Green', colorId: 'memory-leek' },
{ slot: 'Yellow', colorId: 'stack-overglow' },
{ slot: 'Blue', colorId: 'blue-screen' },
{ slot: 'Magenta', colorId: 'fractured-shell' },
{ slot: 'Cyan', colorId: 'dead-thread' },
{ slot: 'White', colorId: 'phosphor-smoke' },
{ slot: 'Bright Black', colorId: 'thermal-throttle' },
{ slot: 'Bright Red', colorId: 'fatal-error' },
{ slot: 'Bright Green', colorId: 'bright-memory-leek' },
{ slot: 'Bright Yellow', colorId: 'bright-stack-overglow' },
{ slot: 'Bright Blue', colorId: 'bright-blue-screen' },
{ slot: 'Bright Magenta', colorId: 'bright-fractured-shell' },
{ slot: 'Bright Cyan', colorId: 'bright-dead-thread' },
{ slot: 'Bright White', colorId: 'ghost-pixel' }
];
export const allColors: DrColor[] = [
...rustRamp,
amberAlertText,
...accentFamilies.flatMap((family) => [family.base, family.dim, family.bright])
];
export const colorsById: ReadonlyMap = new Map(
allColors.map((color) => [color.id, color])
);
export function color(id: string): DrColor {
const found = colorsById.get(id);
if (!found) throw new Error(`Unknown Digital Rust colour id: ${id}`);
return found;
}
function hexToRgb(hex: string): [number, number, number] {
const value = parseInt(hex.slice(1), 16);
return [(value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff];
}
function rgbToHex(rgb: [number, number, number]): string {
return `#${rgb.map((channel) => channel.toString(16).toUpperCase().padStart(2, '0')).join('')}`;
}
export function hexToRgba(hex: string, alpha: number): string {
const [r, g, b] = hexToRgb(hex);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
function relativeLuminance(hex: string): number {
const value = parseInt(hex.slice(1), 16);
const channel = (shift: number) => {
const c = ((value >> shift) & 0xff) / 255;
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
return 0.2126 * channel(16) + 0.7152 * channel(8) + 0.0722 * channel(0);
}
export function contrastRatio(hexA: string, hexB: string): number {
const a = relativeLuminance(hexA);
const b = relativeLuminance(hexB);
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
}
// Text on a swatch uses whichever of the palette endpoints reads better.
export function bestTextColor(bgHex: string): DrColor {
const light = color('ghost-pixel');
const dark = color('event-horizon');
return contrastRatio(bgHex, light.hex) >= contrastRatio(bgHex, dark.hex) ? light : dark;
}
/** Appends an 8-bit alpha channel to a palette hex. For example, withAlpha('dark-packet', 0.7) returns '#691B0FB3'. */
export function withAlpha(id: string, alpha: number): string {
const byte = Math.round(alpha * 255)
.toString(16)
.toUpperCase()
.padStart(2, '0');
return `${color(id).hex}${byte}`;
}
function tint(id: string, alpha: number): string {
return hexToRgba(color(id).hex, alpha);
}
/**
* Composites a family colour over the Safe Mode page and returns a solid value.
* A translucent wash cannot hold its own text contrast on a light surface,
* because the panel behind it shows through.
*/
function solidTint(id: string, alpha: number): string {
const base = hexToRgb(color(id).hex);
const page = hexToRgb(color('ghost-pixel').hex);
return rgbToHex(
base.map((channel, index) => Math.round(channel * alpha + page[index] * (1 - alpha))) as [
number,
number,
number
]
);
}
function emitNamedColors(): string {
const ramp = rustRamp.map((c) => `\t--${c.id}: ${c.hex};`).join('\n');
const accents = accentFamilies
.flatMap((f) => [f.base, f.dim, f.bright])
.map((c) => `\t--${c.id}: ${c.hex};`)
.join('\n');
return `\t/* The rust ramp is the neutral scale. There are no separate grays. */\n${ramp}\n\t--${amberAlertText.id}: ${amberAlertText.hex}; /* ${amberAlertText.description} */\n\n\t/* Accent colours, each with dim and bright variants. */\n${accents}`;
}
export type ModeId = 'dark' | 'safe-mode';
interface RoleToken {
/** Custom property name, without the leading dashes. */
token: string;
dark: string;
safe: string;
comment?: string;
}
interface RoleGroup {
title: string;
tokens: RoleToken[];
}
/** A token with the same value in both modes. It is emitted once, in the base block. */
const invariant = (token: string, value: string, comment?: string): RoleToken => ({
token,
dark: value,
safe: value,
comment
});
/*
* One table holds both modes, so the two blocks cannot drift apart. The dark
* column is the base. The Safe Mode column is the light projection. Every value
* that fills a role lives here, and a component reads the role.
*
* A named palette colour is never re-pointed by a mode, because other tokens
* derive from it.
*/
const roleGroups: RoleGroup[] = [
{
title: 'Semantic surface tokens',
tokens: [
{
token: 'dr-surface-base',
dark: 'var(--event-horizon)',
safe: 'var(--ghost-pixel)',
comment: 'page, wells, code blocks'
},
{
token: 'dr-surface-secondary',
dark: 'var(--null-pointer)',
safe: 'var(--phosphor-smoke)',
comment: 'chrome: tab bars, gutters, sidebars, panels'
},
{
token: 'dr-surface-elevated',
dark: 'var(--core-meltdown)',
safe: 'var(--phantom-current)',
comment: 'cards, menus, modals, code block headers'
},
{
token: 'dr-surface-highlight',
dark: 'var(--bad-sector)',
safe: 'var(--corrupted-cache)',
comment: 'hover fills, active list items'
},
{
token: 'dr-surface-active',
dark: internalHex.surfaceActive,
safe: 'var(--copper-trace)',
comment: 'pressed wells, scrollbar drag'
},
{
token: 'dr-machined-fill',
dark: 'var(--ghost-pixel)',
safe: 'var(--ghost-pixel)',
comment:
'the light part of a machined control: a toggle thumb, a slider thumb, a check mark'
},
invariant(
'dr-machined-track',
'var(--bad-sector)',
'the dark rail of a toggle. It holds Bad Sector in both modes, so a Ghost Pixel thumb stays visible'
),
invariant(
'dr-machined-rail',
'var(--event-horizon)',
'the recessed rail of a slider. It holds Event Horizon in both modes, so a Ghost Pixel thumb stays visible'
),
{
token: 'dr-surface-hot',
dark: 'var(--bricked-board)',
safe: 'var(--bricked-board)',
comment: 'a hot panel: a toast, a status bar. It keeps a dark fill in both modes.'
}
]
},
{
title: 'Semantic text tokens',
tokens: [
{ token: 'dr-text-primary', dark: 'var(--ghost-pixel)', safe: 'var(--event-horizon)' },
{
token: 'dr-text-secondary',
dark: 'var(--phosphor-smoke)',
safe: 'var(--bad-sector)'
},
{
token: 'dr-text-link',
dark: 'var(--phantom-current)',
safe: 'var(--kernel-panic)',
comment:
'Phantom Current measures 1.41:1 on a light page, so a link needs a darker value'
},
{ token: 'dr-text-link-hover', dark: 'var(--fatal-error)', safe: 'var(--data-rot)' },
{
token: 'dr-text-link-visited',
dark: 'var(--corrupted-cache)',
safe: 'var(--bricked-board)'
},
invariant('dr-text-on-accent', 'var(--ghost-pixel)'),
{
token: 'dr-text-muted',
dark: tint('phosphor-smoke', 0.55),
safe: tint('event-horizon', 0.6),
comment:
'disabled and muted text. At 55% the value clears 4.5:1 on every dark surface, so a placeholder meets AA and not only the disabled-text exemption'
},
{
token: 'dr-text-quiet',
dark: 'var(--phantom-current)',
safe: internalHex.quietText,
comment: 'prompts, removed diff lines, footer copy'
},
{
token: 'dr-heading',
dark: 'var(--fatal-error)',
safe: internalHex.heading,
comment: 'the one heading colour, in both modes'
},
{
token: 'dr-heading-sub',
dark: 'var(--copper-trace)',
safe: internalHex.quietText,
comment: 'a sub-heading. Copper Trace measures 2.07:1 on a light page.'
},
{
token: 'dr-cursor',
dark: 'var(--stack-overglow)',
safe: internalHex.overglowLight,
comment: 'the block cursor. Ghost Pixel on this passes AA in both modes.'
}
]
},
{
title: 'Semantic action tokens',
tokens: [
invariant('dr-accent', 'var(--digital-rust)'),
invariant(
'dr-accent-hover',
'var(--firewall-breach)',
'Ghost Pixel on this passes AA (4.66:1). Raw Amber Alert does not.'
)
]
},
{
title: 'Semantic feedback tokens',
tokens: [
{ token: 'dr-success', dark: 'var(--memory-leek)', safe: internalHex.feedbackSuccess },
{
token: 'dr-warning',
dark: 'var(--stack-overglow)',
safe: internalHex.feedbackWarning
},
{ token: 'dr-danger', dark: 'var(--amber-alert)', safe: internalHex.feedbackDanger },
{
token: 'dr-danger-text',
dark: 'var(--amber-alert-text)',
safe: internalHex.feedbackDanger
},
{ token: 'dr-info', dark: 'var(--dead-thread)', safe: internalHex.feedbackInfo }
]
},
{
title: 'Text on a hot panel (Bricked Board)',
tokens: [
invariant('dr-on-hot-success', internalHex.onHotSuccess),
invariant('dr-on-hot-warning', internalHex.onHotWarning),
invariant('dr-on-hot-danger', internalHex.onHotDanger),
invariant('dr-on-hot-info', internalHex.onHotInfo),
invariant('dr-on-hot-text', 'var(--ghost-pixel)'),
{
token: 'dr-on-hot-text-muted',
dark: 'var(--phantom-current)',
safe: 'var(--phosphor-smoke)',
comment: 'a resting or secondary item on a hot panel'
}
]
},
{
title: 'Semantic border colour tokens',
tokens: [
{
token: 'dr-border-color',
dark: 'var(--rouge-signal)',
safe: 'var(--dark-packet)',
comment:
'the 2px wall on an interactive control. A control identifies itself by this wall, so the wall clears 3:1. Firewall Breach is the first ramp stop that clears 3:1 against a panel, at 3.18:1, so it is the floor for a dark control.'
},
invariant(
'dr-border-color-container',
'var(--dark-packet)',
'the 2px wall on a container: a card, a panel, a code block, a toast. Content identifies a container, so the wall stays recessed'
),
{
token: 'dr-border-color-hover',
dark: 'var(--firewall-breach)',
safe: 'var(--digital-rust)',
comment: 'the wall on hover, one step further from the surface in both modes'
},
{
token: 'dr-border-color-strong',
dark: 'var(--daemon-blood)',
safe: internalHex.strongBorder,
comment: 'the wall on a container hover'
},
{
token: 'dr-border-color-invalid',
dark: 'var(--amber-alert)',
safe: internalHex.invalidBorder
},
{
token: 'dr-border-color-warm',
dark: 'var(--stack-overglow)',
safe: internalHex.overglowLight,
comment: 'the warm rule on a prose block'
}
]
},
{
title: 'Border tokens',
tokens: [
invariant('dr-border', '2px solid var(--dr-border-color-container)'),
invariant(
'dr-border-thick',
'4px solid var(--dr-border-color-container)',
'page and site chrome only (headers, section rules). Never on components.'
),
{
token: 'dr-border-subtle',
dark: '1px solid var(--bad-sector)',
safe: `1px solid ${internalHex.quietBorder}`
}
]
},
{
title: 'Shadow tokens. No blur shadows: offset solids only.',
tokens: [
{
token: 'dr-shadow-color',
dark: 'var(--bad-sector)',
safe: 'var(--bad-sector)',
comment:
'the shadow stays Bad Sector in both modes, so it never follows the hover fill'
},
invariant('dr-shadow-offset-sm', '2px 2px 0 0 var(--dr-shadow-color)'),
invariant('dr-shadow-offset-md', '3px 3px 0 0 var(--dr-shadow-color)'),
invariant('dr-shadow-offset-lg', '4px 4px 0 0 var(--dr-shadow-color)'),
invariant('dr-shadow-hover-md', '4px 4px 0 0 var(--dr-shadow-color)'),
invariant('dr-shadow-active', '0 0 0 0 var(--dr-shadow-color)'),
{
token: 'dr-shadow-glow-sm',
dark: `0 0 10px ${tint('digital-rust', 0.4)}`,
safe: 'none',
comment: 'radiated heat reads as a smudge on a lit page'
},
{
token: 'dr-shadow-glow-md',
dark: `0 0 20px ${tint('digital-rust', 0.4)}`,
safe: 'none'
},
{
token: 'dr-shadow-inner-highlight',
dark: 'inset 0 1px 0 rgba(255, 255, 255, 0.1)',
safe: 'inset 0 1px 0 rgba(255, 255, 255, 0.5)'
},
{
token: 'dr-shadow-inner-lowlight',
dark: `inset 0 -1px 0 ${tint('event-horizon', 0.6)}`,
safe: 'inset 0 -1px 0 rgba(70, 18, 10, 0.3)'
}
]
},
{
title: 'Focus ring',
tokens: [
{
token: 'dr-focus-color',
dark: 'var(--stack-overglow)',
safe: 'var(--digital-rust)',
comment: 'both clear 3:1 against every surface in their mode'
},
{
token: 'dr-focus-ring',
dark: '2px solid var(--dr-focus-color)',
safe: '2px solid var(--dr-focus-color)'
},
invariant('dr-focus-ring-offset', '2px')
]
},
{
title: 'Surface texture',
tokens: [
{
token: 'dr-texture-overlay',
dark: 'repeating-linear-gradient(0deg, transparent, transparent 2px, var(--dr-scanline) 2px, var(--dr-scanline) 4px)',
safe: internalHex.pageScorch,
comment:
'scanlines draw the gaps between lit rows, so Safe Mode moves the texture to the rim'
},
{
token: 'dr-vignette',
dark: `color-mix(in srgb, ${'var(--event-horizon)'} 40%, transparent)`,
safe: 'transparent',
comment: 'a corner shade reads as soot on a light page'
},
{
token: 'dr-glow-accent',
dark: 'var(--digital-rust)',
safe: 'transparent',
comment:
'emitted heat. The chromatic split on a heading stays. The glow behind it does not.'
},
{
token: 'dr-glow-success',
dark: 'var(--memory-leek)',
safe: 'transparent'
}
]
},
{
title: 'Syntax role tokens',
tokens: [
{
token: 'dr-syntax-keyword',
dark: 'var(--fatal-error)',
safe: internalHex.syntaxKeyword
},
{
token: 'dr-syntax-function',
dark: 'var(--stack-overglow)',
safe: internalHex.syntaxFunction
},
{
token: 'dr-syntax-constant',
dark: 'var(--bright-stack-overglow)',
safe: internalHex.syntaxConstant
},
{
token: 'dr-syntax-builtin',
dark: 'var(--dim-stack-overglow)',
safe: internalHex.syntaxBuiltin
},
{
token: 'dr-syntax-string',
dark: 'var(--memory-leek)',
safe: internalHex.syntaxString
},
{
token: 'dr-syntax-number',
dark: 'var(--thermal-throttle)',
safe: internalHex.syntaxNumber
},
{ token: 'dr-syntax-type', dark: 'var(--dead-thread)', safe: internalHex.syntaxType },
{
token: 'dr-syntax-comment',
dark: 'var(--dim-thermal-throttle)',
safe: internalHex.syntaxComment
},
{
token: 'dr-syntax-operator',
dark: 'var(--copper-trace)',
safe: internalHex.syntaxOperator
},
{
token: 'dr-syntax-punctuation',
dark: 'var(--copper-trace)',
safe: internalHex.syntaxPunctuation
},
{ token: 'dr-syntax-blue', dark: 'var(--blue-screen)', safe: internalHex.syntaxBlue },
{
token: 'dr-syntax-magenta',
dark: 'var(--fractured-shell)',
safe: internalHex.syntaxMagenta
},
{
token: 'dr-syntax-error',
dark: 'var(--amber-alert-text)',
safe: internalHex.syntaxError
}
]
},
{
title: 'Editor chrome',
tokens: [
invariant('dr-editor-find-match-border', 'var(--digital-rust)'),
{
token: 'dr-gutter-fg',
dark: 'var(--bad-sector)',
safe: 'var(--phantom-current)',
comment: 'a line number is decoration, not text'
},
{
token: 'dr-gutter-fg-active',
dark: 'var(--phantom-current)',
safe: 'var(--copper-trace)'
},
{
token: 'dr-editor-selection-bg',
dark: tint('dark-packet', 0.7),
safe: tint('phantom-current', 0.7)
},
{
token: 'dr-editor-selection-inactive-bg',
dark: tint('dark-packet', 0.4),
safe: tint('phantom-current', 0.4)
},
{
token: 'dr-editor-indent-guide',
dark: 'var(--bad-sector)',
safe: 'var(--phosphor-smoke)'
},
{
token: 'dr-editor-indent-guide-active',
dark: 'var(--dark-packet)',
safe: 'var(--copper-trace)'
},
{
token: 'dr-editor-whitespace',
dark: tint('bad-sector', 0.5),
safe: tint('copper-trace', 0.5)
},
{
token: 'dr-editor-line-highlight-bg',
dark: tint('core-meltdown', 0.5),
safe: tint('phosphor-smoke', 0.5)
},
{
token: 'dr-editor-find-match-bg',
dark: tint('stack-overglow', 0.5),
safe: tint('corrupted-cache', 0.6)
},
{
token: 'dr-editor-find-match-highlight-bg',
dark: tint('stack-overglow', 0.3),
safe: tint('corrupted-cache', 0.35)
},
{
token: 'dr-editor-bracket-match-bg',
dark: 'var(--bad-sector)',
safe: 'var(--corrupted-cache)'
},
{
token: 'dr-editor-unexpected-bracket',
dark: 'var(--amber-alert)',
safe: internalHex.invalidBorder
}
]
},
{
title: 'Terminal slots',
tokens: [
invariant('dr-term-fg', 'var(--dr-text-primary)', 'default foreground'),
invariant('dr-term-bg', 'var(--dr-surface-base)', 'default background'),
invariant('dr-term-cursor', 'var(--dr-cursor)'),
invariant('dr-term-white', 'var(--phosphor-smoke)'),
{
token: 'dr-term-red',
dark: 'var(--rouge-signal)',
safe: internalHex.termRed,
comment:
'Digital Rust measures 3.10:1 on the dark page, so it cannot carry terminal text'
},
{ token: 'dr-term-green', dark: 'var(--memory-leek)', safe: internalHex.termGreen },
{
token: 'dr-term-yellow',
dark: 'var(--stack-overglow)',
safe: internalHex.overglowLight
},
{ token: 'dr-term-blue', dark: 'var(--blue-screen)', safe: internalHex.termBlue },
{
token: 'dr-term-magenta',
dark: 'var(--fractured-shell)',
safe: internalHex.termMagenta
},
{ token: 'dr-term-cyan', dark: 'var(--dead-thread)', safe: internalHex.termCyan },
{
token: 'dr-term-bright-black',
dark: 'var(--thermal-throttle)',
safe: tint('event-horizon', 0.6),
comment:
'the dim ink. Bad Sector measures 1.30:1 on the dark page, and a dim variant cannot clear 4.5:1 there, so dim reads as desaturated instead'
},
{
token: 'dr-term-bright-red',
dark: 'var(--fatal-error)',
safe: internalHex.termBrightRed
},
{
token: 'dr-term-bright-green',
dark: 'var(--bright-memory-leek)',
safe: internalHex.termBrightGreen
},
{
token: 'dr-term-bright-yellow',
dark: 'var(--bright-stack-overglow)',
safe: internalHex.termBrightYellow
},
{
token: 'dr-term-bright-blue',
dark: 'var(--bright-blue-screen)',
safe: internalHex.termBrightBlue
},
{
token: 'dr-term-bright-magenta',
dark: 'var(--bright-fractured-shell)',
safe: internalHex.termBrightMagenta
},
{
token: 'dr-term-bright-cyan',
dark: 'var(--bright-dead-thread)',
safe: internalHex.termBrightCyan
},
invariant(
'dr-term-bright-white',
'var(--ghost-pixel)',
'the paper slot: the page colour in both modes'
)
]
},
{
title: 'Chips',
tokens: [
{
token: 'dr-inline-code-bg',
dark: 'var(--bad-sector)',
safe: 'var(--phosphor-smoke)',
comment:
'a fill on dark, a well on light, so the quiet syntax roles stay above 4.5:1'
}
]
},
{
title: 'Scrollbar',
tokens: [
{
token: 'dr-scrollbar-thumb',
dark: 'var(--rouge-signal)',
safe: internalHex.scrollbarThumb,
comment: 'both clear 3:1 against the page'
},
{
token: 'dr-scrollbar-thumb-hover',
dark: 'var(--firewall-breach)',
safe: internalHex.scrollbarThumbHover
}
]
},
{
title: 'Semantic tints (badge and diagnostic washes)',
tokens: [
{
token: 'memory-leek-10',
dark: tint('memory-leek', 0.1),
safe: solidTint('memory-leek', 0.1)
},
{
token: 'memory-leek-15',
dark: tint('memory-leek', 0.15),
safe: solidTint('memory-leek', 0.15)
},
{
token: 'memory-leek-30',
dark: tint('memory-leek', 0.3),
safe: solidTint('memory-leek', 0.3)
},
{
token: 'dead-thread-10',
dark: tint('dead-thread', 0.1),
safe: solidTint('dead-thread', 0.1)
},
{
token: 'dead-thread-15',
dark: tint('dead-thread', 0.15),
safe: solidTint('dead-thread', 0.15)
},
{
token: 'dead-thread-30',
dark: tint('dead-thread', 0.3),
safe: solidTint('dead-thread', 0.3)
},
{
token: 'amber-alert-10',
dark: tint('amber-alert', 0.1),
safe: solidTint('amber-alert', 0.1)
},
{
token: 'amber-alert-15',
dark: tint('amber-alert', 0.15),
safe: solidTint('amber-alert', 0.15)
},
{
token: 'amber-alert-25',
dark: tint('amber-alert', 0.25),
safe: solidTint('amber-alert', 0.25)
},
{
token: 'stack-overglow-10',
dark: tint('stack-overglow', 0.1),
safe: solidTint('stack-overglow', 0.1)
},
{
token: 'stack-overglow-15',
dark: tint('stack-overglow', 0.15),
safe: solidTint('stack-overglow', 0.15)
},
{
token: 'stack-overglow-30',
dark: tint('stack-overglow', 0.3),
safe: solidTint('stack-overglow', 0.3)
},
{
token: 'core-meltdown-40',
dark: tint('core-meltdown', 0.4),
safe: 'var(--phosphor-smoke)',
comment: 'translucent panel tint (blockquotes)'
},
{
token: 'bad-sector-20',
dark: tint('bad-sector', 0.2),
safe: 'var(--phosphor-smoke)',
comment: 'inlay hint fill'
},
invariant(
'event-horizon-95',
tint('event-horizon', 0.95),
'near-opaque backdrop (lightboxes)'
)
]
}
];
/**
* Reads a role value for a mode. A value that points at a named palette colour
* comes back as the `var()` reference, which is what a style rule needs.
*/
export function roleValue(token: string, mode: ModeId): string {
const name = token.startsWith('--') ? token.slice(2) : token;
for (const group of roleGroups) {
const found = group.tokens.find((entry) => entry.token === name);
if (found) return mode === 'dark' ? found.dark : found.safe;
}
throw new Error(`Unknown Digital Rust role token: ${token}`);
}
/**
* Reads a role value as a literal colour. Use this when a consumer needs hex,
* such as the Shiki themes. A `var()` reference resolves through the palette.
*/
export function resolveRoleValue(token: string, mode: ModeId): string {
const raw = roleValue(token, mode);
const reference = /^var\(--([a-z0-9-]+)\)$/.exec(raw);
return reference ? color(reference[1]).hex : raw;
}
/** Every role token name, without the leading dashes. */
export const roleTokenNames: readonly string[] = roleGroups.flatMap((group) =>
group.tokens.map((entry) => entry.token)
);
function emitRoles(mode: ModeId): string {
return roleGroups
.map((group) => {
const lines = group.tokens
.filter((entry) => mode === 'dark' || entry.dark !== entry.safe)
.map((entry) => {
const value = mode === 'dark' ? entry.dark : entry.safe;
const comment = entry.comment ? ` /* ${entry.comment} */` : '';
return `\t--${entry.token}: ${value};${comment}`;
});
return lines.length === 0 ? '' : `\t/* ${group.title} */\n${lines.join('\n')}`;
})
.filter(Boolean)
.join('\n\n');
}
function emitStaticTokens(): string {
return `\t/* Interaction tokens (the press cycle) */
--dr-press-rest: translate(0, 0);
--dr-press-hover: translate(-2px, -2px);
--dr-press-active: translate(2px, 2px);
/* Radius tokens. Nothing above 6px. */
--dr-radius-sm: 2px;
--dr-radius: 4px;
--dr-radius-lg: 6px;
/* Spacing tokens */
--dr-space-xs: 0.25rem;
--dr-space-sm: 0.5rem;
--dr-space-md: 1rem;
--dr-space-lg: 1.5rem;
--dr-space-xl: 2.5rem;
--dr-space-2xl: 4rem;
--dr-space-3xl: 6rem;
/* Type scale tokens */
--dr-text-xs: 0.75rem;
--dr-text-sm: 0.875rem;
--dr-text-base: 1rem;
--dr-text-lg: 1.25rem;
--dr-text-xl: 1.75rem;
--dr-text-2xl: 2.5rem;
--dr-text-3xl: 4rem;
/* Motion tokens */
--dr-duration-fast: 150ms;
--dr-duration-normal: 250ms;
--dr-duration-slow: 400ms;
--dr-ease-default: cubic-bezier(0.4, 0, 0.2, 1);
--dr-ease-out-expo: cubic-bezier(0.22, 1, 0.36, 1);
--dr-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--dr-transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1);
/* Z-index tokens */
--dr-z-content: 10;
--dr-z-noise: 1000;
/* Scanline colour. Safe Mode paints no scanline band, but the value stays
defined so a component can reference it without a mode check. */
--dr-scanline: ${tint('digital-rust', 0.03)};
/* Font stacks. Illustrative only, NOT a constraint. Any families of
equivalent character (condensed uppercase for heading, slab/humanist
serif or humanist sans for body, tabular monospace for readouts)
satisfy the role; substitute freely to suit the project's font
license and locale. */
--dr-font-heading: "Rajdhani", "Saira SemiCondensed", "Trebuchet MS", sans-serif;
--dr-font-body: "Zilla Slab", "Bitter", "Source Serif 4", Georgia, serif;
--dr-font-mono: "IBM Plex Mono", "Space Mono", ui-monospace, "Fira Code",
"Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono",
monospace;`;
}
export function toTokensCss(): string {
return `/* GENERATED FILE. Edit src/lib/palette.ts, not this file. */
:root {
color-scheme: dark;
${emitNamedColors()}
${emitRoles('dark')}
${emitStaticTokens()}
}
/*
* Safe Mode, the light projection. Only role values change. The named palette
* colours do not, because another token derives from each of them.
*
* The mode is selected in this order: the saved preference, then the platform
* preference, then the dark theme. The attribute covers a saved preference. The
* media query covers a platform preference.
*/
:root[data-scheme='safe-mode'] {
color-scheme: light;
${emitRoles('safe-mode')}
}
@media (prefers-color-scheme: light) {
:root:not([data-scheme='dark']) {
color-scheme: light;
${emitRoles('safe-mode')}
}
}
`;
}