Reactive Color in SVG: From Static Paths to Dynamic Themes
SVG is the web's vector format. It lives in the DOM, responds to CSS, and can be styled with custom properties. Yet most tools treat SVG as a static export — a snapshot frozen at build time. Colors baked in. Themes impossible without regeneration.
Pathogen's Color system changes this. By combining a first-class Color type with CSS custom properties, Pathogen compiles SVG illustrations that are reactive — change a CSS variable at runtime and every derived color updates instantly. No JavaScript. No recompilation. Just CSS doing what CSS does best.
This post walks through the system from first principles, building up from a single color to full light/dark adaptive themes. The demos below are live — pick a color and watch the SVG respond.
Starting with Color
Color in Pathogen is a first-class type backed by the OKLCH color space. OKLCH stands for Okay Lightness, Chroma, Hue — a perceptually uniform color model where equal numeric steps produce equal visual differences. This matters: lightening a red by 0.15 and lightening a blue by 0.15 should look like the same amount of change. In sRGB hex they don't. In OKLCH they do.
Creating a color is straightforward:
let c = Color('#e63946');
// Access OKLCH components
log(c.lightness); // ~0.52
log(c.chroma); // ~0.19
log(c.hue); // ~27
// Output in any format
log(c.hex); // #e63946
log(c.oklch); // oklch(0.52 0.19 27)
log(c.hsl); // hsl(355, 78%, 56%)
Pathogen accepts any CSS color format as input — hex, rgb(), hsl(), named colors — and immediately converts to OKLCH internally. From there, all manipulation happens in perceptual space.
You can also construct colors directly in OKLCH:
let sky = Color(0.75, 0.12, 230); // L, C, H
Color Manipulation
Every Color value exposes a set of manipulation methods. Each returns a new Color — nothing mutates.
let base = Color('#e63946');
let lighter = base.lighten(0.18); // bump lightness
let darker = base.darken(0.18); // reduce lightness
let vivid = base.saturate(1.5); // scale chroma up
let muted = base.desaturate(0.4); // scale chroma down
let shifted = base.hueShift(90); // rotate hue
let comp = base.complement(); // hue + 180
let semi = base.alpha(0.5); // set transparency
let blended = base.mix(Color('#457b9d'), 0.5); // interpolate
All of this operates in OKLCH, so a .lighten(0.18) on a deeply saturated red doesn't accidentally desaturate it — it shifts only lightness while preserving chroma and hue. Try it: pick a color below and watch each method derive its swatch.
What makes this reactive? The SVG above was compiled once. There is no JavaScript updating colors. The fill values use CSS relative color syntax:
fill="oklch(from var(--demo-color, #e63946) calc(l + 0.18) c h)"
The browser resolves var(--demo-color), extracts its OKLCH components, applies calc(l + 0.18), and renders. When the color picker updates --demo-color, the browser recalculates everything automatically.
Harmonies and Palettes
Color theory provides recipes for colors that work well together. Pathogen generates these directly:
let base = Color('#e63946');
// Harmony groups
let analog = base.analogous(); // 3 colors: hue -30, 0, +30
let triad = base.triadic(); // 3 colors: hue 0, +120, +240
let tetrad = base.tetradic(); // 4 colors: hue 0, +90, +180, +270
let split = base.splitComplementary(); // 3 colors: hue 0, +150, +210
Each harmony method returns an array of Colors you can iterate:
for ([color, i] in base.triadic()) {
define PathLayer(`swatch-${i}`) ${ fill: color; }
layer(`swatch-${i}`).apply { roundRect(x, y, 40, 40, 6) }
}
Palettes go further. Color.palette() generates lightness ramps or interpolation sequences:
let ramp = Color.palette(base, 5); // 5-step lightness ramp
let blend = Color.palette(base, accent, 5); // 5-step interpolation
The lightness ramp spreads evenly from dark (L=0.15) to light (L=0.95). The interpolation variant uses color-mix() when backed by CSS variables, so the browser handles the blending at render time.
Notice the palette rows. The lightness ramp uses CSS relative color syntax to override the l component at fixed steps:
fill="oklch(from var(--harmony-color) 0.35 c h)"
The interpolation row uses color-mix():
fill="color-mix(in oklch, var(--harmony-color), #457b9d 50%)"
Both are resolved by the browser at render time. Change the base color and five lightness steps and five interpolation steps recalculate instantly.
CSSVar: The Reactive Layer
The magic behind these live demos is CSSVar() — Pathogen's bridge between compile-time computation and runtime CSS.
let base = Color(CSSVar('--base-color', '#e63946'));
This does two things. At compile time, Color('#e63946') resolves to an OKLCH value for use in any computation that needs concrete color data. At render time, the SVG references var(--base-color, #e63946) — a CSS custom property with a fallback.
When you call methods on a CSSVar-backed Color, Pathogen emits CSS relative color expressions instead of baking the result:
let lighter = base.lighten(0.15);
// Compiled output: oklch(from var(--base-color, #e63946) calc(l + 0.15) c h)
let blended = base.mix(accent, 0.5);
// Compiled output: color-mix(in oklch, var(--base-color), var(--accent-color) 50%)
This is the key insight: compile once, theme at runtime. A Pathogen source file is compiled to a static SVG that contains no JavaScript. But because colors are expressed as CSS functions referencing custom properties, any container that sets those properties will see the SVG adapt.
The pattern works for entire illustrations. Define a few CSSVar-backed colors, derive everything from them, and the compiled SVG becomes a themeable asset:
let bg = Color(CSSVar('--bg', '#f5f5f5'));
let primary = Color(CSSVar('--primary', '#e63946'));
let secondary = Color(CSSVar('--secondary', '#457b9d'));
let accent = Color(CSSVar('--accent', '#2a9d8f'));
// Derived colors — all reactive
let primaryLight = primary.lighten(0.2);
let primaryDark = primary.darken(0.15);
let secondaryMuted = secondary.desaturate(0.5);
let accentShift = accent.hueShift(60);
This SVG has a central star (primary), orbiting circles (secondary), corner diamonds (accent), and a background — all themeable from four CSS variables. The star fill uses oklch(from var(--primary) calc(l + 0.2) c h) for a lighter shade, the corner diamond fills use oklch(from var(--accent) l c calc(h + 60)) for a hue-shifted variation. One source file, infinite themes.
@property: Enabling Transitions
When using CSSVar-backed colors, the compiler automatically generates CSS @property declarations:
@property --base-color {
syntax: "<color>";
inherits: true;
initial-value: #e63946;
}
This tells the browser that --base-color is a color, not an arbitrary string. Without @property, CSS custom properties are opaque tokens — the browser can't interpolate between #e63946 and #457b9d because it doesn't know they're colors. With @property, CSS transitions and animations work on custom properties, meaning you can smoothly animate theme changes:
.svg-container {
transition: --primary 0.3s ease;
}
Pathogen collects these declarations during compilation and embeds them in the SVG's <style> block. Each Color(CSSVar('--name', fallback)) call registers one @property rule — first occurrence wins, duplicates are skipped.
Light/Dark: Adaptive SVGs
Modern CSS has light-dark(), a function that resolves to one of two values depending on the document's color scheme. Pathogen exposes this through Color.lightDark():
let fg = Color.lightDark(Color('#333'), Color('#eee'));
let accent = Color.lightDark(accent, darkAccent);
In the compiled SVG, the fill becomes:
fill="light-dark(#333333, #eeeeee)"
At compile time, properties like .hex and .lightness resolve against the light variant so your code can do concrete math. At render time, the browser picks the appropriate value based on the user's prefers-color-scheme setting. You can also combine lightDark() with CSSVar():
let themed = Color.lightDark(
Color(CSSVar('--fg-light', '#333')),
Color(CSSVar('--fg-dark', '#eee'))
);
// Output: light-dark(var(--fg-light, #333), var(--fg-dark, #eee))
This gives you theme-aware SVGs that respond to both system preferences and runtime CSS variable overrides — two axes of customization from a single compiled file.
The Full Picture
Everything comes together in a comprehensive swatch showcase. Three CSSVar-backed colors generate an entire dual-panel visualization: methods, harmonies, palettes, and theme-aware colors, rendered side by side for light and dark contexts.
let base = Color(CSSVar('--base-color', '#e63946'));
let accent = Color(CSSVar('--accent-color', '#457b9d'));
let darkAccent = Color(CSSVar('--dark-accent', '#f4a261'));
// Light panel: derive from base
let lighter = base.lighten(0.15);
let darker = base.darken(0.15);
let triad = base.triadic();
let ramp = Color.palette(base, accent, 5);
// Dark panel: derive from darkAccent
let dkTriad = darkAccent.triadic();
let dkRamp = Color.palette(darkAccent, base, 5);
// Theme-aware colors
let themeFg = Color.lightDark(Color('#333'), Color('#eee'));
let themeAccent = Color.lightDark(accent, darkAccent);
The light panel derives from --base-color, the dark panel from --dark-accent. The palette rows use color-mix() to interpolate between the panel's base and the other panel's color. The lightDark() row shows theme-aware colors that automatically switch based on system color scheme preference.
Three CSS variables. Two panels. Seven method variants, three harmony colors, five palette steps, and two theme-adaptive colors per panel. All compiled from a single Pathogen source file, all reactive at runtime.
What This Means
The traditional workflow for themeable SVG is painful: generate variants, swap files, or embed JavaScript to manipulate the DOM. Pathogen's approach eliminates all of that. You write color logic at a high level — harmonies, palettes, lightness ramps — and the compiler translates it into CSS that browsers already know how to execute.
The result is SVG illustration that participates in the web platform's theming infrastructure. Set CSS custom properties from your design system. Let prefers-color-scheme drive light and dark variants. Animate color transitions with CSS. No runtime JavaScript needed, no asset pipeline for variants.
SVG was always a dynamic format hiding behind static tooling. The Color system gives it the vocabulary to express what it was designed for.
View All Sources
The Pathogen source files that produce the interactive demos above. Each is compiled once to a static SVG containing CSS var() references — the color pickers simply set CSS custom properties on the container.
Color Methods Demo
Produces the manipulation swatches — one CSSVar-backed color, eight derived swatches.
// Color Methods Demo — base color → 8 derived swatches
// CSSVar-backed for reactive color picker updates
let base = Color(CSSVar('--demo-color', '#e63946'));
let lighter = base.lighten(0.18);
let darker = base.darken(0.18);
let vivid = base.saturate(1.5);
let muted = base.desaturate(0.4);
let shifted = base.hueShift(90);
let comp = base.complement();
let semi = base.alpha(0.5);
let mixed = base.mix(Color('#457b9d'), 0.5);
// Layout
let sw = 50;
let sh = 50;
let sr = 8;
let gap = 14;
let startX = 30;
let row1Y = 40;
let row2Y = 130;
// Row 1: base + lighten, darken, saturate, desaturate
let row1 = [base, lighter, darker, vivid, muted];
let row1names = ['base', 'lighten', 'darken', 'saturate', 'desaturate'];
define TextLayer('labels') ${
font-family: system-ui, sans-serif;
font-size: 10;
fill: #888;
text-anchor: middle;
}
for ([color, i] in row1) {
let x = calc(startX + i * (sw + gap));
define PathLayer(`r1_${i}`) ${ fill: color; stroke: #ddd; stroke-width: 1; }
layer(`r1_${i}`).apply { roundRect(x, row1Y, sw, sh, sr) }
}
layer('labels').apply {
for ([name, i] in row1names) {
text(calc(startX + i * (sw + gap) + sw / 2), calc(row1Y + sh + 16))`${name}`
}
}
// Row 2: hueShift, complement, alpha, mix
let row2 = [shifted, comp, semi, mixed];
let row2names = ['hueShift', 'complement', 'alpha(0.5)', 'mix'];
for ([color, i] in row2) {
let x = calc(startX + i * (sw + gap));
define PathLayer(`r2_${i}`) ${ fill: color; stroke: #ddd; stroke-width: 1; }
layer(`r2_${i}`).apply { roundRect(x, row2Y, sw, sh, sr) }
}
layer('labels').apply {
for ([name, i] in row2names) {
text(calc(startX + i * (sw + gap) + sw / 2), calc(row2Y + sh + 16))`${name}`
}
}
Harmonies and Palettes Demo
Produces the harmony and palette rows — four harmony types plus lightness ramp and color interpolation.
// Harmonies Demo — base color → harmony rows + palette rows
// CSSVar-backed for reactive color picker updates
let base = Color(CSSVar('--harmony-color', '#e63946'));
// Harmonies
let analog = base.analogous();
let triad = base.triadic();
let tetrad = base.tetradic();
let split = base.splitComplementary();
// Palettes
let ramp = Color.palette(base, 5);
let interp = Color.palette(base, Color('#457b9d'), 5);
// Layout
let sw = 40;
let sh = 40;
let sr = 6;
let gap = 12;
let startX = 140;
let labelX = 20;
define TextLayer('labels') ${
font-family: system-ui, sans-serif;
font-size: 11;
fill: #777;
}
define TextLayer('section') ${
font-family: system-ui, sans-serif;
font-size: 13;
font-weight: bold;
fill: #555;
}
// Section: Harmonies
layer('section').apply { text(labelX, 28)`Harmonies` }
let rows = [analog, triad, tetrad, split];
let rowNames = ['analogous', 'triadic', 'tetradic', 'splitComp'];
let rowYs = [50, 108, 166, 224];
for ([harmony, ri] in rows) {
let y = rowYs[ri];
layer('labels').apply { text(labelX, calc(y + sw / 2 + 4))`${rowNames[ri]}` }
for ([color, ci] in harmony) {
let x = calc(startX + ci * (sw + gap));
define PathLayer(`h${ri}_${ci}`) ${ fill: color; stroke: #ddd; stroke-width: 0.5; }
layer(`h${ri}_${ci}`).apply { roundRect(x, y, sw, sh, sr) }
}
}
// Divider
define PathLayer('div1') ${ stroke: #e0e0e0; stroke-width: 1; fill: none; }
layer('div1').apply { M labelX 282 L 440 282 }
// Section: Palettes
layer('section').apply { text(labelX, 306)`Palettes` }
layer('labels').apply { text(labelX, calc(330 + sw / 2 + 4))`lightness` }
for ([color, i] in ramp) {
let x = calc(startX + i * (sw + gap));
define PathLayer(`ramp_${i}`) ${ fill: color; stroke: #ddd; stroke-width: 0.5; }
layer(`ramp_${i}`).apply { roundRect(x, 330, sw, sh, sr) }
}
layer('labels').apply { text(labelX, calc(388 + sw / 2 + 4))`interpolate` }
for ([color, i] in interp) {
let x = calc(startX + i * (sw + gap));
define PathLayer(`interp_${i}`) ${ fill: color; stroke: #ddd; stroke-width: 0.5; }
layer(`interp_${i}`).apply { roundRect(x, 388, sw, sh, sr) }
}
Theme Demo
Produces the themeable geometric illustration — four CSSVar-backed colors drive a star, orbiting circles, corner diamonds, and decorative arcs.
// Theme Demo — geometric composition with CSSVar theming
// Multiple CSS variables drive the entire illustration
let bg = Color(CSSVar('--bg', '#f5f5f5'));
let primary = Color(CSSVar('--primary', '#e63946'));
let secondary = Color(CSSVar('--secondary', '#457b9d'));
let accent = Color(CSSVar('--accent', '#2a9d8f'));
// Derived reactive colors
let primaryLight = primary.lighten(0.2);
let primaryDark = primary.darken(0.15);
let secondaryMuted = secondary.desaturate(0.5);
let accentShift = accent.hueShift(60);
// Background
define PathLayer('bg') ${ fill: bg; stroke: none; }
layer('bg').apply { rect(0, 0, 500, 260) }
// Central star — primary color
define PathLayer('star') ${ stroke: primary; fill: primaryLight; stroke-width: 2; stroke-linejoin: round; }
layer('star').apply { star(250, 130, 55, 28, 5) }
// Orbiting circles — secondary color
define PathLayer('orbits') ${ stroke: secondary; fill: secondaryMuted; stroke-width: 1.5; }
layer('orbits').apply {
for (i in 0..4) {
let angle = calc(i / 4 * TAU());
let cx = calc(250 + cos(angle) * 100);
let cy = calc(130 + sin(angle) * 70);
circle(cx, cy, 14)
}
}
// Corner diamonds — accent color
define PathLayer('diamonds') ${ stroke: accent; fill: accentShift; stroke-width: 1; }
layer('diamonds').apply {
polygon(40, 40, 18, 4)
polygon(460, 40, 18, 4)
polygon(40, 220, 18, 4)
polygon(460, 220, 18, 4)
}
// Decorative arcs — dark primary
define PathLayer('arcs') ${ stroke: primaryDark; fill: none; stroke-width: 2; stroke-linecap: round; }
layer('arcs').apply {
M 70 130
A 40 40 0 0 1 110 130
M 390 130
A 40 40 0 0 1 430 130
}
// Inner ring — accent
define PathLayer('ring') ${ stroke: accent; fill: none; stroke-width: 1; stroke-dasharray: 4 3; }
layer('ring').apply { circle(250, 130, 42) }
Dual-Panel Swatch Showcase
Produces the full picture — light and dark panels side by side, featuring methods, harmonies, palettes, and Color.lightDark() theme-aware colors.
// Swatches Demo — simplified version of the full swatch showcase
// Light panel on left, dark panel on right
// CSSVar-backed for reactive color picker updates
let base = Color(CSSVar('--base-color', '#e63946'));
let accent = Color(CSSVar('--accent-color', '#457b9d'));
let darkAccent = Color(CSSVar('--dark-accent', '#f4a261'));
// Derived colors (light panel)
let lighter = base.lighten(0.15);
let darker = base.darken(0.15);
let vivid = base.saturate(1.4);
let muted = base.desaturate(0.5);
let shifted = base.hueShift(60);
let comp = base.complement();
// Harmonies
let triad = base.triadic();
let ramp = Color.palette(base, accent, 5);
// Dark panel derived
let dkLighter = darkAccent.lighten(0.15);
let dkDarker = darkAccent.darken(0.15);
let dkVivid = darkAccent.saturate(1.4);
let dkMuted = darkAccent.desaturate(0.5);
let dkShifted = darkAccent.hueShift(60);
let dkComp = darkAccent.complement();
let dkTriad = darkAccent.triadic();
let dkRamp = Color.palette(darkAccent, base, 5);
// Theme-aware colors
let themeFg = Color.lightDark(Color('#333'), Color('#eee'));
let themeAccent = Color.lightDark(accent, darkAccent);
// Layout
let panelW = 280;
let gapX = 10;
let darkX = calc(panelW + gapX);
let sw = 24;
let sh = 22;
let sr = 3;
let colGap = 5;
let startX = 14;
// Light panel background
define PathLayer('light-bg') ${ fill: #f5f5f5; stroke: none; }
layer('light-bg').apply { roundRect(0, 0, panelW, 340, 0) }
// Dark panel background
define PathLayer('dark-bg') ${ fill: #222266; stroke: none; }
layer('dark-bg').apply { roundRect(darkX, 0, panelW, 340, 0) }
// Shared text styles
define TextLayer('section') ${
font-family: system-ui, sans-serif;
font-size: 9;
font-weight: bold;
fill: #555;
}
define TextLayer('labels') ${
font-family: system-ui, sans-serif;
font-size: 7;
fill: #888;
text-anchor: middle;
}
define TextLayer('dk-section') ${
font-family: system-ui, sans-serif;
font-size: 9;
font-weight: bold;
fill: #aab;
}
define TextLayer('dk-labels') ${
font-family: system-ui, sans-serif;
font-size: 7;
fill: #889;
text-anchor: middle;
}
// ── LIGHT PANEL ──────────────────────────────────
// Methods row
layer('section').apply { text(8, 18)`Methods` }
let ltRow = [base, lighter, darker, vivid, muted, shifted, comp];
let ltNames = ['base', 'light', 'dark', 'vivid', 'muted', 'shift', 'compl'];
for ([color, i] in ltRow) {
let x = calc(startX + i * (sw + colGap));
define PathLayer(`lt_m${i}`) ${ fill: color; stroke: #ccc; stroke-width: 0.5; }
layer(`lt_m${i}`).apply { roundRect(x, 24, sw, sh, sr) }
}
layer('labels').apply {
for ([name, i] in ltNames) {
text(calc(startX + i * (sw + colGap) + sw / 2), calc(24 + sh + 10))`${name}`
}
}
// Divider
define PathLayer('lt-div') ${ stroke: #ddd; stroke-width: 0.5; fill: none; }
layer('lt-div').apply { M 8 70 L calc(panelW - 8) 70 }
// Harmony row
layer('section').apply { text(8, 86)`Triadic` }
for ([color, i] in triad) {
let x = calc(startX + i * (sw + colGap));
define PathLayer(`lt_h${i}`) ${ fill: color; stroke: #ccc; stroke-width: 0.5; }
layer(`lt_h${i}`).apply { roundRect(x, 92, sw, sh, sr) }
}
// Palette row
layer('lt-div').apply { M 8 130 L calc(panelW - 8) 130 }
layer('section').apply { text(8, 146)`Palette` }
for ([color, i] in ramp) {
let x = calc(startX + i * (sw + colGap));
define PathLayer(`lt_p${i}`) ${ fill: color; stroke: #ccc; stroke-width: 0.5; }
layer(`lt_p${i}`).apply { roundRect(x, 152, sw, sh, sr) }
}
// Theme-aware colors
layer('lt-div').apply { M 8 194 L calc(panelW - 8) 194 }
layer('section').apply { text(8, 210)`lightDark()` }
define PathLayer('lt-ld-fg') ${ fill: themeFg; stroke: #ccc; stroke-width: 0.5; }
layer('lt-ld-fg').apply { roundRect(startX, 216, sw, sh, sr) }
define PathLayer('lt-ld-accent') ${ fill: themeAccent; stroke: #ccc; stroke-width: 0.5; }
layer('lt-ld-accent').apply { roundRect(calc(startX + sw + colGap), 216, sw, sh, sr) }
layer('labels').apply {
text(calc(startX + sw / 2), calc(216 + sh + 10))`fg`
text(calc(startX + sw + colGap + sw / 2), calc(216 + sh + 10))`accent`
}
// ── DARK PANEL ──────────────────────────────────
// Methods row
layer('dk-section').apply { text(calc(darkX + 8), 18)`Methods` }
let dkRow = [darkAccent, dkLighter, dkDarker, dkVivid, dkMuted, dkShifted, dkComp];
let dkNames = ['base', 'light', 'dark', 'vivid', 'muted', 'shift', 'compl'];
for ([color, i] in dkRow) {
let x = calc(darkX + startX + i * (sw + colGap));
define PathLayer(`dk_m${i}`) ${ fill: color; stroke: #444; stroke-width: 0.5; }
layer(`dk_m${i}`).apply { roundRect(x, 24, sw, sh, sr) }
}
layer('dk-labels').apply {
for ([name, i] in dkNames) {
text(calc(darkX + startX + i * (sw + colGap) + sw / 2), calc(24 + sh + 10))`${name}`
}
}
// Divider
define PathLayer('dk-div') ${ stroke: #445; stroke-width: 0.5; fill: none; }
layer('dk-div').apply { M calc(darkX + 8) 70 L calc(darkX + panelW - 8) 70 }
// Harmony row
layer('dk-section').apply { text(calc(darkX + 8), 86)`Triadic` }
for ([color, i] in dkTriad) {
let x = calc(darkX + startX + i * (sw + colGap));
define PathLayer(`dk_h${i}`) ${ fill: color; stroke: #444; stroke-width: 0.5; }
layer(`dk_h${i}`).apply { roundRect(x, 92, sw, sh, sr) }
}
// Palette row
layer('dk-div').apply { M calc(darkX + 8) 130 L calc(darkX + panelW - 8) 130 }
layer('dk-section').apply { text(calc(darkX + 8), 146)`Palette` }
for ([color, i] in dkRamp) {
let x = calc(darkX + startX + i * (sw + colGap));
define PathLayer(`dk_p${i}`) ${ fill: color; stroke: #444; stroke-width: 0.5; }
layer(`dk_p${i}`).apply { roundRect(x, 152, sw, sh, sr) }
}
// Theme-aware colors
layer('dk-div').apply { M calc(darkX + 8) 194 L calc(darkX + panelW - 8) 194 }
layer('dk-section').apply { text(calc(darkX + 8), 210)`lightDark()` }
define PathLayer('dk-ld-fg') ${ fill: themeFg; stroke: #555; stroke-width: 0.5; }
layer('dk-ld-fg').apply { roundRect(calc(darkX + startX), 216, sw, sh, sr) }
define PathLayer('dk-ld-accent') ${ fill: themeAccent; stroke: #555; stroke-width: 0.5; }
layer('dk-ld-accent').apply { roundRect(calc(darkX + startX + sw + colGap), 216, sw, sh, sr) }
layer('dk-labels').apply {
text(calc(darkX + startX + sw / 2), calc(216 + sh + 10))`fg`
text(calc(darkX + startX + sw + colGap + sw / 2), calc(216 + sh + 10))`accent`
}
// Panel labels
define TextLayer('panel-light') ${
font-family: system-ui, sans-serif;
font-size: 9;
font-weight: bold;
fill: #bbb;
text-anchor: middle;
}
layer('panel-light').apply { text(calc(panelW / 2), 330)`LIGHT` }
define TextLayer('panel-dark') ${
font-family: system-ui, sans-serif;
font-size: 9;
font-weight: bold;
fill: #557;
text-anchor: middle;
}
layer('panel-dark').apply { text(calc(darkX + panelW / 2), 330)`DARK` }