remarque-tokens 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_RULES.md +218 -0
- package/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/REMARQUE.md +356 -0
- package/fonts/inter-italic-latin.woff2 +0 -0
- package/fonts/inter-latin.woff2 +0 -0
- package/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/fonts/newsreader-italic-latin.woff2 +0 -0
- package/fonts/newsreader-latin.woff2 +0 -0
- package/fonts.css +59 -0
- package/package.json +55 -0
- package/scripts/audit.mjs +236 -0
- package/tailwind.config.js +144 -0
- package/tokens-core.css +330 -0
- package/tokens-palette.css +128 -0
- package/tokens.css +24 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* remarque audit — enforces REMARQUE.md's checklist mechanically.
|
|
4
|
+
*
|
|
5
|
+
* node scripts/audit.mjs [--palette tokens-palette.css] [--src <dir>]
|
|
6
|
+
* npx remarque-audit --palette your-palette.css --src src
|
|
7
|
+
*
|
|
8
|
+
* Checks (exit 1 on any failure):
|
|
9
|
+
* 1. CONTRAST — extracts custom properties from the palette file with a
|
|
10
|
+
* brace-aware scanner (light = top-level :root; dark = :root inside
|
|
11
|
+
* @media (prefers-color-scheme: dark) and/or [data-theme="dark"],
|
|
12
|
+
* cascaded over the light values), resolves var() aliases, and
|
|
13
|
+
* computes WCAG 2.x ratios via OKLCH → linear sRGB (Ottosson
|
|
14
|
+
* matrices, gamut-clipped) → relative luminance. Thresholds come
|
|
15
|
+
* from the spec's Enforcement Checklist. Unresolvable tokens in a
|
|
16
|
+
* required pairing are failures, not skips.
|
|
17
|
+
* Also fails on out-of-sRGB-gamut colors (browsers clip them, so
|
|
18
|
+
* the authored value is not what renders).
|
|
19
|
+
* 2. FONT FLOOR — no font-size declaration below 0.8125rem / 13px in src.
|
|
20
|
+
* 3. NO HARDCODED COLORS — no hex/rgb()/hsl() literals in src styles;
|
|
21
|
+
* oklch() literals are allowed only in token files.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
25
|
+
import { join, relative } from 'node:path';
|
|
26
|
+
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
function argOf(flag, dflt) {
|
|
29
|
+
const i = args.indexOf(flag);
|
|
30
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : dflt;
|
|
31
|
+
}
|
|
32
|
+
const PALETTE = argOf('--palette', 'tokens-palette.css');
|
|
33
|
+
const SRC = argOf('--src', existsSync('site/src') ? 'site/src' : '.');
|
|
34
|
+
|
|
35
|
+
let failures = 0;
|
|
36
|
+
function fail(msg) {
|
|
37
|
+
failures++;
|
|
38
|
+
console.error(` ✗ ${msg}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!existsSync(PALETTE)) {
|
|
42
|
+
console.error(`palette file not found: ${PALETTE} (use --palette <file>)`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
if (!existsSync(SRC)) {
|
|
46
|
+
console.error(`src directory not found: ${SRC} (use --src <dir>)`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ── OKLCH → sRGB → WCAG luminance ──────────────────────── */
|
|
51
|
+
|
|
52
|
+
function oklchToLinearSrgb(L, C, H) {
|
|
53
|
+
const h = (H * Math.PI) / 180;
|
|
54
|
+
const a = C * Math.cos(h), b = C * Math.sin(h);
|
|
55
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
56
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
57
|
+
const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
|
|
58
|
+
const l = l_ ** 3, m = m_ ** 3, s = s_ ** 3;
|
|
59
|
+
return [
|
|
60
|
+
+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
61
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
62
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
const inGamut = (rgb) => rgb.every((x) => x >= -0.0005 && x <= 1.0005);
|
|
66
|
+
const clip = (x) => Math.min(1, Math.max(0, x));
|
|
67
|
+
const luminance = (rgb) => {
|
|
68
|
+
const [r, g, b] = rgb.map(clip);
|
|
69
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
70
|
+
};
|
|
71
|
+
function ratio(c1, c2) {
|
|
72
|
+
const l1 = luminance(oklchToLinearSrgb(...c1));
|
|
73
|
+
const l2 = luminance(oklchToLinearSrgb(...c2));
|
|
74
|
+
const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1];
|
|
75
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ── Brace-aware CSS block extraction ───────────────────── */
|
|
79
|
+
/* Returns [{ prelude, body, context }] where context is the enclosing
|
|
80
|
+
at-rule prelude ('' at top level). One level of at-rule nesting is
|
|
81
|
+
supported — enough for token files; deeper nesting is flattened with
|
|
82
|
+
its outermost context. Comments are stripped first. */
|
|
83
|
+
|
|
84
|
+
function extractBlocks(css) {
|
|
85
|
+
const src = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
86
|
+
const blocks = [];
|
|
87
|
+
function scan(text, context) {
|
|
88
|
+
let i = 0;
|
|
89
|
+
while (i < text.length) {
|
|
90
|
+
const open = text.indexOf('{', i);
|
|
91
|
+
if (open === -1) break;
|
|
92
|
+
const prelude = text.slice(i, open).trim().replace(/^[;\s]+/, '');
|
|
93
|
+
let depth = 1, j = open + 1;
|
|
94
|
+
while (j < text.length && depth > 0) {
|
|
95
|
+
if (text[j] === '{') depth++;
|
|
96
|
+
else if (text[j] === '}') depth--;
|
|
97
|
+
j++;
|
|
98
|
+
}
|
|
99
|
+
const body = text.slice(open + 1, j - 1);
|
|
100
|
+
if (prelude.startsWith('@')) {
|
|
101
|
+
scan(body, prelude);
|
|
102
|
+
} else {
|
|
103
|
+
blocks.push({ prelude, body, context });
|
|
104
|
+
}
|
|
105
|
+
i = j;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
scan(src, '');
|
|
109
|
+
return blocks;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function declsOf(blocks, filterFn) {
|
|
113
|
+
const decls = {};
|
|
114
|
+
for (const b of blocks) {
|
|
115
|
+
if (!filterFn(b)) continue;
|
|
116
|
+
for (const m of b.body.matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/g)) {
|
|
117
|
+
decls[m[1]] = m[2].trim();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return decls;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const blocks = extractBlocks(readFileSync(PALETTE, 'utf8'));
|
|
124
|
+
const lightDecls = declsOf(blocks, (b) => b.context === '' && b.prelude.includes(':root'));
|
|
125
|
+
const darkOverrides = declsOf(blocks, (b) =>
|
|
126
|
+
(b.context.includes('prefers-color-scheme') && b.context.includes('dark') && b.prelude.includes(':root')) ||
|
|
127
|
+
b.prelude.includes('[data-theme="dark"]')
|
|
128
|
+
);
|
|
129
|
+
if (Object.keys(lightDecls).length === 0) fail(`no top-level :root declarations found in ${PALETTE}`);
|
|
130
|
+
if (Object.keys(darkOverrides).length === 0) fail(`no dark-theme declarations found in ${PALETTE} (@media prefers-color-scheme: dark or [data-theme="dark"])`);
|
|
131
|
+
// CSS cascade: dark blocks override light; unset dark tokens inherit light values.
|
|
132
|
+
const darkDecls = { ...lightDecls, ...darkOverrides };
|
|
133
|
+
|
|
134
|
+
/* Resolve a token to [L, C, H], following var(--x) chains. Returns null
|
|
135
|
+
(and records why) when unresolvable. */
|
|
136
|
+
function resolveColor(decls, name, seen = new Set()) {
|
|
137
|
+
if (seen.has(name)) return { err: `circular var() chain at --${name}` };
|
|
138
|
+
seen.add(name);
|
|
139
|
+
const raw = decls[name];
|
|
140
|
+
if (raw === undefined) return { err: `--${name} is not defined` };
|
|
141
|
+
const varRef = raw.match(/^var\(\s*--([a-z0-9-]+)\s*\)$/);
|
|
142
|
+
if (varRef) return resolveColor(decls, varRef[1], seen);
|
|
143
|
+
const ok = raw.match(/^oklch\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*\)$/);
|
|
144
|
+
if (ok) return { color: [+ok[1], +ok[2], +ok[3]] };
|
|
145
|
+
return { err: `--${name} has unsupported value "${raw}" (expected oklch(L C H) or var(--token))` };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ── Contrast checks (from REMARQUE.md Enforcement Checklist) ── */
|
|
149
|
+
|
|
150
|
+
const CHECKS = [
|
|
151
|
+
// [fg-token, bg-token, min-ratio, label]
|
|
152
|
+
['color-fg', 'color-bg', 4.5, 'primary text'],
|
|
153
|
+
['color-fg', 'color-surface', 4.5, 'primary text on surface'],
|
|
154
|
+
['color-fg-muted', 'color-bg', 7.0, 'secondary text (spec AAA line)'],
|
|
155
|
+
['color-muted', 'color-bg', 4.5, 'tertiary text'],
|
|
156
|
+
['color-muted', 'color-surface', 4.5, 'tertiary text on surface'],
|
|
157
|
+
['color-accent', 'color-bg', 4.5, 'links'],
|
|
158
|
+
['color-accent-hover', 'color-bg', 4.5, 'link hover'],
|
|
159
|
+
['color-code-fg', 'color-code-bg', 4.5, 'code text'],
|
|
160
|
+
['color-border-bold', 'color-bg', 3.0, 'functional borders (WCAG 1.4.11)'],
|
|
161
|
+
['color-selection-fg', 'color-selection-bg', 4.5, 'selected text'],
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
for (const [themeName, decls] of [['light', lightDecls], ['dark', darkDecls]]) {
|
|
165
|
+
console.log(`\n${themeName} theme (${PALETTE})`);
|
|
166
|
+
// gamut check on every color-valued token
|
|
167
|
+
for (const name of Object.keys(decls)) {
|
|
168
|
+
const r = resolveColor(decls, name);
|
|
169
|
+
if (r.color && !inGamut(oklchToLinearSrgb(...r.color))) {
|
|
170
|
+
fail(`--${name} oklch(${r.color.join(' ')}) is outside sRGB gamut — browsers will clip it; author an in-gamut value`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const [fgName, bgName, min, label] of CHECKS) {
|
|
174
|
+
const fg = resolveColor(decls, fgName);
|
|
175
|
+
const bg = resolveColor(decls, bgName);
|
|
176
|
+
if (fg.err || bg.err) {
|
|
177
|
+
fail(`${fgName}/${bgName} (${label}): ${fg.err || bg.err}`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const r = ratio(fg.color, bg.color);
|
|
181
|
+
if (r < min) fail(`${fgName}/${bgName} = ${r.toFixed(2)}:1 < ${min}:1 (${label})`);
|
|
182
|
+
else console.log(` ✓ ${fgName}/${bgName} = ${r.toFixed(2)}:1 (${label})`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* ── Source scans ───────────────────────────────────────── */
|
|
187
|
+
|
|
188
|
+
function* walk(dir) {
|
|
189
|
+
for (const name of readdirSync(dir)) {
|
|
190
|
+
const p = join(dir, name);
|
|
191
|
+
if (name === 'node_modules' || name === 'dist' || name.startsWith('.')) continue;
|
|
192
|
+
if (statSync(p).isDirectory()) yield* walk(p);
|
|
193
|
+
else if (/\.(css|astro|svelte|jsx|tsx|html)$/.test(name)) yield p;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log(`\nsource scans (${SRC})`);
|
|
198
|
+
const FONT_FLOOR_REM = 0.8125, FONT_FLOOR_PX = 13;
|
|
199
|
+
// tokens.astro is the token *reference page* — its job is displaying literal
|
|
200
|
+
// values (issue #48 tracks generating it from a machine-readable source).
|
|
201
|
+
const isTokenFile = (p) => /tokens(-core|-palette)?\.css$|fonts\.css$|globals\.css$|pages[\/\\]tokens\.astro$/.test(p);
|
|
202
|
+
|
|
203
|
+
for (const file of walk(SRC)) {
|
|
204
|
+
const rel = relative('.', file);
|
|
205
|
+
const text = readFileSync(file, 'utf8');
|
|
206
|
+
const lines = text.split('\n');
|
|
207
|
+
lines.forEach((line, i) => {
|
|
208
|
+
// 2. font-size floor (static rem/px values), plus statically
|
|
209
|
+
// unverifiable sizes (% / clamp) outside token files — those bypass
|
|
210
|
+
// any floor check, so require the type-scale tokens instead.
|
|
211
|
+
for (const m of line.matchAll(/font-size:\s*([\d.]+)(rem|px)/g)) {
|
|
212
|
+
const v = parseFloat(m[1]);
|
|
213
|
+
if ((m[2] === 'rem' && v < FONT_FLOOR_REM) || (m[2] === 'px' && v < FONT_FLOOR_PX)) {
|
|
214
|
+
fail(`${rel}:${i + 1} font-size ${m[1]}${m[2]} below the 13px floor`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (!isTokenFile(rel) && /font-size:\s*(clamp\(|[\d.]+%)/.test(line)) {
|
|
218
|
+
fail(`${rel}:${i + 1} statically unverifiable font-size (clamp/%) — use var(--text-*) tokens: ${line.trim().slice(0, 80)}`);
|
|
219
|
+
}
|
|
220
|
+
// 3. hardcoded colors (hex / rgb / hsl anywhere; oklch outside token files).
|
|
221
|
+
// Note: hex fills/strokes in inline SVG are flagged deliberately —
|
|
222
|
+
// Remarque icons use currentColor, per the no-hardcoded-colors rule.
|
|
223
|
+
if (/(#[0-9a-fA-F]{3,8}\b(?![0-9a-zA-Z]))|(\brgba?\()|(\bhsla?\()/.test(line) && !/xmlns|href|url\(#|\{#/.test(line)) {
|
|
224
|
+
fail(`${rel}:${i + 1} hardcoded hex/rgb/hsl color: ${line.trim().slice(0, 80)}`);
|
|
225
|
+
}
|
|
226
|
+
if (!isTokenFile(rel) && /\boklch\(/.test(line) && !line.includes('var(--')) {
|
|
227
|
+
fail(`${rel}:${i + 1} oklch() literal outside token files: ${line.trim().slice(0, 80)}`);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (failures) {
|
|
233
|
+
console.error(`\naudit FAILED — ${failures} problem(s)\n`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
console.log('\naudit passed ✓\n');
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remarque — Tailwind CSS Configuration
|
|
3
|
+
* ──────────────────────────────────────
|
|
4
|
+
* This config extends Tailwind with Remarque's design tokens.
|
|
5
|
+
*
|
|
6
|
+
* TAILWIND VERSION STORY (one canonical answer):
|
|
7
|
+
* - Tailwind v3.x → use this file as your tailwind.config.js.
|
|
8
|
+
* - Tailwind v4.x → do NOT use this file. Write an @theme block in CSS
|
|
9
|
+
* using the values from tokens.css (see site/src/styles/globals.css in
|
|
10
|
+
* this repo for the reference @theme; a shipped theme.css subpath export
|
|
11
|
+
* is tracked in issue #48).
|
|
12
|
+
*
|
|
13
|
+
* Tokens are defined in tokens.css as CSS custom properties; this config
|
|
14
|
+
* maps them into Tailwind utilities (font-display, text-title, etc.).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** @type {import('tailwindcss').Config} */
|
|
18
|
+
module.exports = {
|
|
19
|
+
content: [
|
|
20
|
+
"./src/**/*.{js,ts,jsx,tsx,mdx,astro}",
|
|
21
|
+
"./pages/**/*.{js,ts,jsx,tsx,mdx,astro}",
|
|
22
|
+
"./components/**/*.{js,ts,jsx,tsx,mdx,astro}",
|
|
23
|
+
"./app/**/*.{js,ts,jsx,tsx,mdx,astro}",
|
|
24
|
+
],
|
|
25
|
+
|
|
26
|
+
// Match Remarque's actual theming mechanism ([data-theme] attribute).
|
|
27
|
+
// The bare "class" strategy only matches a literal `dark` class, which
|
|
28
|
+
// Remarque never sets — dark: utilities would silently never activate.
|
|
29
|
+
darkMode: ["selector", '[data-theme="dark"]'],
|
|
30
|
+
|
|
31
|
+
theme: {
|
|
32
|
+
extend: {
|
|
33
|
+
|
|
34
|
+
/* ─── Font Families ──────────────────────────────── */
|
|
35
|
+
fontFamily: {
|
|
36
|
+
display: ["Newsreader", "Georgia", "Times New Roman", "serif"],
|
|
37
|
+
body: ["Inter", "ui-sans-serif", "system-ui", "-apple-system", "sans-serif"],
|
|
38
|
+
mono: ["JetBrains Mono", "ui-monospace", "Cascadia Code", "Fira Code", "monospace"],
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/* ─── Font Sizes ─────────────────────────────────── */
|
|
42
|
+
/* Maps to: text-display, text-title, text-section, etc. */
|
|
43
|
+
fontSize: {
|
|
44
|
+
"display": ["var(--text-display)", { lineHeight: "var(--leading-display)", letterSpacing: "var(--tracking-display)" }],
|
|
45
|
+
"title": ["var(--text-title)", { lineHeight: "var(--leading-title)", letterSpacing: "var(--tracking-title)" }],
|
|
46
|
+
"section": ["var(--text-section)", { lineHeight: "var(--leading-section)", letterSpacing: "var(--tracking-title)" }],
|
|
47
|
+
"body-lg": ["var(--text-body-lg)", { lineHeight: "var(--leading-body)" }],
|
|
48
|
+
"body": ["var(--text-body)", { lineHeight: "var(--leading-body)" }],
|
|
49
|
+
"meta": ["var(--text-meta)", { lineHeight: "var(--leading-meta)", letterSpacing: "var(--tracking-meta)" }],
|
|
50
|
+
"micro": ["var(--text-micro)", { lineHeight: "var(--leading-meta)" }],
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/* ─── Spacing ────────────────────────────────────── */
|
|
54
|
+
/*
|
|
55
|
+
* Namespaced under remarque-* so Tailwind's default numeric scale is
|
|
56
|
+
* NEVER overridden (mt-12 stays 3rem). Use mt-remarque-9 (6rem) etc.
|
|
57
|
+
* for Remarque's generous editorial spacings, or var(--space-N)
|
|
58
|
+
* directly in arbitrary values: mt-[var(--space-9)].
|
|
59
|
+
*/
|
|
60
|
+
spacing: {
|
|
61
|
+
"remarque-1": "var(--space-1)",
|
|
62
|
+
"remarque-2": "var(--space-2)",
|
|
63
|
+
"remarque-3": "var(--space-3)",
|
|
64
|
+
"remarque-4": "var(--space-4)",
|
|
65
|
+
"remarque-5": "var(--space-5)",
|
|
66
|
+
"remarque-6": "var(--space-6)",
|
|
67
|
+
"remarque-7": "var(--space-7)",
|
|
68
|
+
"remarque-8": "var(--space-8)",
|
|
69
|
+
"remarque-9": "var(--space-9)",
|
|
70
|
+
"remarque-10": "var(--space-10)",
|
|
71
|
+
"remarque-11": "var(--space-11)",
|
|
72
|
+
"remarque-12": "var(--space-12)",
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/* ─── Max Widths ─────────────────────────────────── */
|
|
76
|
+
/*
|
|
77
|
+
* Width-constraint utilities ONLY — they do not center.
|
|
78
|
+
* Per AGENT_RULES.md, page sections must use the .content-reading /
|
|
79
|
+
* .content-standard classes from tokens.css (which add
|
|
80
|
+
* margin-inline: auto). Reach for max-w-reading only on elements
|
|
81
|
+
* that are intentionally left-aligned inside an already-constrained
|
|
82
|
+
* container (e.g. a lead paragraph).
|
|
83
|
+
*/
|
|
84
|
+
maxWidth: {
|
|
85
|
+
"reading": "var(--content-reading)", // 46rem — prose
|
|
86
|
+
"standard": "var(--content-standard)", // 72rem — project pages
|
|
87
|
+
"wide": "var(--content-wide)", // 88rem — outer container
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/* ─── Colors ─────────────────────────────────────── */
|
|
91
|
+
/* Maps CSS custom properties so Tailwind utilities track theme changes */
|
|
92
|
+
colors: {
|
|
93
|
+
bg: {
|
|
94
|
+
DEFAULT: "var(--color-bg)",
|
|
95
|
+
subtle: "var(--color-bg-subtle)",
|
|
96
|
+
},
|
|
97
|
+
fg: {
|
|
98
|
+
DEFAULT: "var(--color-fg)",
|
|
99
|
+
muted: "var(--color-fg-muted)",
|
|
100
|
+
},
|
|
101
|
+
muted: "var(--color-muted)",
|
|
102
|
+
border: {
|
|
103
|
+
DEFAULT: "var(--color-border)",
|
|
104
|
+
bold: "var(--color-border-bold)",
|
|
105
|
+
},
|
|
106
|
+
surface: "var(--color-surface)",
|
|
107
|
+
accent: {
|
|
108
|
+
DEFAULT: "var(--color-accent)",
|
|
109
|
+
hover: "var(--color-accent-hover)",
|
|
110
|
+
subtle: "var(--color-accent-subtle)",
|
|
111
|
+
},
|
|
112
|
+
code: {
|
|
113
|
+
bg: "var(--color-code-bg)",
|
|
114
|
+
fg: "var(--color-code-fg)",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
/* ─── Border Radius ──────────────────────────────── */
|
|
119
|
+
borderRadius: {
|
|
120
|
+
sm: "var(--radius-sm)",
|
|
121
|
+
md: "var(--radius-md)",
|
|
122
|
+
none: "0",
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/* ─── Border Color ───────────────────────────────── */
|
|
126
|
+
borderColor: {
|
|
127
|
+
DEFAULT: "var(--color-border)",
|
|
128
|
+
bold: "var(--color-border-bold)",
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/* ─── Transitions ────────────────────────────────── */
|
|
132
|
+
transitionDuration: {
|
|
133
|
+
fast: "var(--motion-fast)",
|
|
134
|
+
normal: "var(--motion-normal)",
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
transitionTimingFunction: {
|
|
138
|
+
remarque: "var(--motion-easing)",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
plugins: [],
|
|
144
|
+
};
|