Something went wrong. Try again.
A warm, tech-dystopian color scheme inspired by corrupted hardware and failing systems. digitalrust.katsuricata.com
theme color-scheme color-theme warm color
Something went wrong. Try again.
32 kB · 1079 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080// SPDX-FileCopyrightText: 2025-2026 Kat Suricata <https://KatSuricata.com/>// SPDX-License-Identifier: (CC-BY-4.0+ OR Apache-2.0+)
export interface DrColor { /** Kebab-case identifier. Becomes the CSS custom property `--<id>`. */ 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<string, DrColor> = 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')} }}`;}