@keenmate/web-components-devkit 1.0.0-rc07 → 1.0.0-rc08

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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0-rc08] - 2026-11-04 [PUBLISHED]
4
+
5
+ ### Added
6
+
7
+ - **Doctor: hard-coded colours in page CSS** (warning). A colour literal (`#hex`, `rgb()` / `hsl()` /
8
+ `oklch()`, a named colour) on a colour property in a page `<style>`, a `style="…"` or
9
+ `examples-local.css` stays the same when the theme switch flips the kit to dark. web-treeview's
10
+ migration kept 10 pages' own `<style>` blocks, and 8 of them had search bars, status bars, badges and
11
+ panels that didn't follow dark mode. rc07 only flagged rules that restyle a kit class, so nothing
12
+ reported them. `var()` fallbacks and `light-dark()` pairs pass. A block that shows fixed colours on
13
+ purpose (a theme gallery, an iOS mockup) opts out with `<style data-kmwc-fixed-colors>`, or one
14
+ rule with a `/* kmwc-fixed-colors: why */` comment before it (the playground's e2e fixture red).
15
+ - **Doctor: the loader class matches the installed core** (error). Core 6d6b768 renamed the default
16
+ loader's class from `<prefix>-loader` to `<prefix>__loader`, with no release that emits both. A
17
+ component that renames its selector before the core bump, or forgets to after it, ships an unstyled
18
+ loader. No rulebook check sees that, and a VALIDATION-NOTES `## C-NC-8` hides a leftover `.x-loader`
19
+ (drp has one for other classes). Doctor reads which class the installed core's `render-loader.js`
20
+ draws and fails on a `src/**/*.css` selector for the other one.
21
+
22
+ ### Fixed
23
+
24
+ - **Inline `<code>` in dark mode** was the accent colour on a neutral `#2b2b2b` chip: about 3:1, and
25
+ web-treeview's `#059669` green read dim in prose and tables. The chip is now a step up from the main
26
+ surface (`oklch(from --kmwc-main-bg calc(l + 0.07) …)`, slate on slate), and the text is a new
27
+ `--kmwc-inline-code-color`: the accent hue at a lightness of at least 0.8 in dark, the accent as-is in
28
+ light. An e2e spec checks 4.5:1 for the default, green, navy and amber accents.
29
+
3
30
  ## [1.0.0-rc07] - 2026-11-03 [PUBLISHED]
4
31
 
5
32
  Found migrating web-treeview — each item is something doctor missed or got wrong there.
package/README.md CHANGED
@@ -19,6 +19,11 @@ them in the same shape: `@keenmate/web-components-devkit`.
19
19
  > positioning) lives in [`@keenmate/web-components-core`](../web-components-core).
20
20
  > Nothing here ships inside a component's bundle.
21
21
 
22
+ ## What's New in v1.0.0-rc08
23
+
24
+ - **Doctor — hard-coded colours in example pages** — web-treeview's devkit migration kept each page's own `<style>` block, and on 8 of its 14 pages the search bars, status bars, badges and panels kept their light colours when the theme switch went dark. rc07 only flagged page CSS that restyles a kit class, so nothing reported them. Doctor now warns about a colour literal (`#hex`, `rgb()` / `hsl()` / `oklch()`, a named colour) on any colour property in a page `<style>`, a `style="…"` attribute or `examples-local.css`. `var()` fallbacks, `light-dark()` pairs, custom properties and `rgb(from var(…))` pass, because they follow the theme. A block that shows fixed colours on purpose, such as a theme gallery or a mockup, opts out with `<style data-kmwc-fixed-colors>`, and a single rule with a `/* kmwc-fixed-colors: why */` comment before it.
25
+ - **Doctor — the loader class matches the installed core** — Core 6d6b768 renamed its default loader's class from `<prefix>-loader` to `<prefix>__loader`, with no release that emits both. A component that renames its selector before the core bump, or forgets to after it, ships a loader with no styles. No rulebook check sees that, and a `## C-NC-8` heading in VALIDATION-NOTES.md would hide a leftover `.x-loader` from the BEM check. Doctor reads which class the installed core's `dist/dom/render-loader.js` draws and fails on any selector in the component's `src/**/*.css` that styles the other one. The error names the file and says which side should change.
26
+
22
27
  ## What's New in v1.0.0-rc07
23
28
 
24
29
  - **CLI — `kmwc check-pages` finds what only a browser can see** — Migrating web-treeview turned up pages that pushed a phone sideways: an iOS-Files mockup 420px wide and a theme gallery built on `minmax(500px, 1fr)`. No static check could have caught that. `kmwc check-pages` loads every example page in Chromium at 1440, 390 and 320px. It reports page and console errors, failed requests, custom elements that never get defined, and pages wider than the viewport, naming the widest elements. It looks inside shadow roots and at long words that overflow their box. It ignores layers that can't widen a page: scrolling code blocks, hidden or fixed layers, a closed chapter-nav panel. It starts the repo's dev server, or `--url` points it at one already running (for example the built site on the preview port), and `--widths` changes the widths. `kmwc sync` adds a `check:pages` script and a `make check-pages` target.
@@ -27,13 +32,6 @@ them in the same shape: `@keenmate/web-components-devkit`.
27
32
  - **check-vars — input variables are not dead** — Some component variables are public knobs that are read with a fallback and never declared. Treeview forwards `--wtv-context-menu-*` to core's private `--_km-menu-*` so a page-wide `--km-menu-*` still applies, and declaring them would break that chain. check-vars called all 26 of them dead. In the default mode a variable now counts as present when it is declared or read, and a read-with-fallback missing from the manifest is reported as undocumented. `cssVariables.sources` also takes a package path (`@keenmate/web-components-core/dist/menu/styles.js`) for the `--base-*` reads of a core module the component renders.
28
33
  - **Example pages — fit a 320px phone, and injected scripts land in the right place** — A chapter heading that names an API outside `<code>` now breaks instead of widening the page. On a phone the index title is 2rem. Below 400px, body, card and demo padding shrink, so a 250px inline calendar fits inside a card. `mark` no longer splits a word. The Vite plugin now puts its head block after the real `<head>` tag (not `<header>` or the text "<head>"). It puts the kit script before the last `</body>`, so it can no longer land inside a page's inline script and break it. That's the failure web-multiselect's sed-based Dockerfile caused on its live action-buttons page.
29
34
 
30
- ## What's New in v1.0.0-rc06
31
-
32
- - **Example pages — `<kbd>`, `<mark>`, field widths and `.push-end`** — Every component documents a keyboard map and most highlight search matches. Pages either left `<kbd>` in the browser's look (core's Interaction page had 18 bare keys) or styled `<mark>` themselves. A `<kbd>` now looks like the spotlight's key hints and a `<mark>` like its match highlight. Native fields are about 20 characters wide, so pages set inline widths; core had 14. A number field is now 7rem by default. `.w-xs` / `.w-sm` / `.w-md` / `.w-lg` (4 / 6 / 9 / 12rem) size a short field, and a `.form-group` still stretches it to the row. `.push-end` moves an item to the end of a `.controls` row. See the playground's WG08 and FC12.
33
- - **Example pages — phone and contrast fixes** — Inline `<code>` now breaks anywhere. Before, a long identifier such as `resolvePresentation(mode, env, map)` in a heading made a page 350px wide on a 320px screen; code inside a `<pre>` keeps its lines. Muted log text on the light `.log-panel` (`--muted` lines, the seq / time / source cells, the empty placeholder) measured 2.83:1 in the light theme. It now uses `--kmwc-text-color-2`, and a spec checks every tone on both panels in both themes against WCAG AA's 4.5:1. Timeline rows wrap, so on a phone or with a long source name the detail drops under the cells instead of showing one letter per line. `.css-readout` gets a top margin after the demo it measures.
34
- - **CLI — `kmwc <command> --help` no longer runs the command** — Only a top-level `--help` was handled, and unknown flags were silently ignored, so `kmwc sync --help` did a full sync and overwrote core's hand-written Makefile. Every command now lists the flags it accepts. `--help` / `-h` after any command prints that command's usage and exits 0, and an unknown option is an error (exit 2). A boolean flag no longer takes the next word as its value.
35
- - **Libraries — `library: false`, `cssVariables: false`, and a doctor that knows the kind** — A repo like web-components-core builds its package with `tsc`. A plain `vite build` (the devkit's library mode) would empty its `dist/`, so `"kmwc": { "library": false }` now makes it refuse to run, while `vite build --mode examples` still builds the site. `"cssVariables": false` turns off the variables-manifest check, so sync stops re-adding `check:vars`. Doctor reads the registry kind (or `kmwc.kind`): a `library` runs only naming-conventions, readme-structure, publish-command and example-pages. On core that drops 12 errors and 4 "suite could not run" warnings that didn't apply to a library.
36
-
37
35
  ## Catalogue
38
36
 
39
37
  <!-- kmwc:catalogue:start — generated by `npm run readme`, do not edit inside -->
@@ -416,6 +414,16 @@ generated files:
416
414
  selector (`.kbd`, `kbd`, `mark`, `.pill`, `.card` …) and single-declaration
417
415
  inline styles a kit class covers (`width` on a field → `.w-*`,
418
416
  `margin-left: auto` → `.push-end`, `margin-top: 1rem` → `.mt-1`) are warnings.
417
+ So is a hard-coded colour (`#hex`, `rgb()`, `white` …) in page CSS or a `style="…"`:
418
+ it stays put when the theme switch flips the kit to dark. `var()` fallbacks and
419
+ `light-dark()` pairs follow the theme. Fixed colours on purpose (a theme gallery, a
420
+ mockup, a test fixture) go in a `<style data-kmwc-fixed-colors>` block, or one rule
421
+ gets a `/* kmwc-fixed-colors: why */` comment right before it.
422
+ - **Loader class** — the component CSS styles the class the *installed* core gives
423
+ its default loader (`<prefix>__loader` from core 6d6b768 on, `<prefix>-loader`
424
+ before). Renaming the selector on one side of the core bump leaves the loader
425
+ unstyled, and a VALIDATION-NOTES `## C-NC-8` would hide the old class from the
426
+ rulebook.
419
427
 
420
428
  `kmwc check-pages` is the browser half: it loads every example page and reports
421
429
  what doctor can't see in the source — a script error, a 404, a fixed width that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keenmate/web-components-devkit",
3
- "version": "1.0.0-rc07",
3
+ "version": "1.0.0-rc08",
4
4
  "description": "Shared dev, example-site and build infrastructure for Keenmate web components: Vite preset, examples kit (chapter nav, copy-link, controls store, shared styles), Playwright preset and the `kmwc` CLI (sync, doctor, check-vars, new, readme).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,6 +34,11 @@ import {
34
34
  loadSharedPlausibleSiteIds,
35
35
  loadKitClassNames,
36
36
  getKitDuplicates,
37
+ FIXED_COLORS_ATTRIBUTE,
38
+ FIXED_COLORS_MARKER,
39
+ getHardcodedColors,
40
+ loadCoreLoader,
41
+ loadStyledLoaderClasses,
37
42
  } from '../repo-checks.js';
38
43
 
39
44
  const DEVKIT_PACKAGE_NAME = '@keenmate/web-components-devkit';
@@ -197,6 +202,23 @@ export function checkComponent(config, doctorConfig = {}) {
197
202
  }
198
203
  if (pageFiles.length && !kitDuplicateSources.length) addPass('pages', 'no page CSS re-implements the kit');
199
204
 
205
+ // Page CSS colours that stay put when the theme switch flips the kit to dark.
206
+ const hardcodedColorSources = [
207
+ ...pageFiles.map((pageFile) => ({ file: pageFile, hardcodedColors: getHardcodedColors(readFileSync(join(config.root, pageFile), 'utf8')) })),
208
+ ...(existsSync(localStylesheetPath)
209
+ ? [{ file: LOCAL_STYLESHEET_FILE, hardcodedColors: getHardcodedColors(readFileSync(localStylesheetPath, 'utf8'), { isStylesheet: true }) }]
210
+ : []),
211
+ ].filter((source) => source.hardcodedColors.length);
212
+ for (const { file, hardcodedColors } of hardcodedColorSources) {
213
+ const shownColors = hardcodedColors.slice(0, 3).map(({ found }) => found).join('; ');
214
+ addWarning(
215
+ 'pages',
216
+ `${file}: ${hardcodedColors.length} hard-coded colour(s) that don't follow dark mode — ${shownColors}${hardcodedColors.length > 3 ? ' …' : ''}`,
217
+ `use the kit's --kmwc-* tokens or light-dark(); fixed colours on purpose: <style ${FIXED_COLORS_ATTRIBUTE}>, or /* ${FIXED_COLORS_MARKER} */ before one rule`
218
+ );
219
+ }
220
+ if (pageFiles.length && !hardcodedColorSources.length) addPass('pages', 'page CSS colours follow the theme');
221
+
200
222
  // ── generated files ──────────────────────────────────────────────────────
201
223
  if (config.hasPackageConfig) {
202
224
  for (const planItem of getSyncPlan(config)) {
@@ -252,6 +274,9 @@ export function checkComponent(config, doctorConfig = {}) {
252
274
  if (!outputMismatches.length) addPass('library', 'package.json targets match the library build outputs');
253
275
  }
254
276
 
277
+ // ── core's default loader class ──────────────────────────────────────────
278
+ if (config.prefix && config.kind !== LIBRARY_KIND) checkLoaderClass(config, { addPass, addWarning, addError });
279
+
255
280
  // ── analytics ────────────────────────────────────────────────────────────
256
281
  const plausibleSiteId = loadPlausibleSiteId(config.root);
257
282
  const sharingRepoIds = loadSharedPlausibleSiteIds(config, getRegistry(), plausibleSiteId);
@@ -299,6 +324,30 @@ function checkPorts(config, portBlock, { addPass, addWarning, addError }) {
299
324
  }
300
325
  }
301
326
 
327
+ /**
328
+ * The CSS styles the loader class the INSTALLED core draws. Core 6d6b768 renamed
329
+ * `<prefix>-loader` → `<prefix>__loader` with no overlap release; renaming the CSS
330
+ * before (or after) the core bump leaves the default loader unstyled, and no rulebook
331
+ * check sees that (a VALIDATION-NOTES `## C-NC-8` even hides a leftover old class).
332
+ */
333
+ function checkLoaderClass(config, { addPass, addError }) {
334
+ const coreLoader = loadCoreLoader(config.root);
335
+ if (!coreLoader?.separator) return;
336
+ const coreClassName = `${config.prefix}${coreLoader.separator}loader`;
337
+ const styledLoaderClasses = loadStyledLoaderClasses(config.root, config.prefix);
338
+ const mismatchedClasses = styledLoaderClasses.filter(({ separator }) => separator !== coreLoader.separator);
339
+ for (const { file, className } of mismatchedClasses) {
340
+ addError(
341
+ 'loader',
342
+ `${file} styles .${className}, but core ${coreLoader.version} draws the default loader as <span class="${coreClassName}"> — it renders unstyled`,
343
+ coreLoader.separator === '__'
344
+ ? `rename .${className} → .${coreClassName} (CSS, tests, docs; keep the --${config.prefix}-loader-* tokens)`
345
+ : `keep .${coreClassName} until the component builds on a core that draws <prefix>__loader, then rename in the same change`
346
+ );
347
+ }
348
+ if (styledLoaderClasses.length && !mismatchedClasses.length) addPass('loader', `CSS styles .${coreClassName}, the class core ${coreLoader.version} draws`);
349
+ }
350
+
302
351
  /** Packed paths that break PACKAGE_LEAK_RULES → [{ path, label }]. Pure — exported for tests. */
303
352
  export function getPackageLeaks(packedPaths) {
304
353
  return packedPaths
@@ -247,3 +247,120 @@ export function getKitDuplicates(pageText, kitClassNames, { isStylesheet = false
247
247
  }
248
248
  return duplicates;
249
249
  }
250
+
251
+ // ── hard-coded colours in page CSS ───────────────────────────────────────────
252
+
253
+ /**
254
+ * Fixed colours on purpose (a theme gallery, a mockup, a test fixture): a page `<style>`
255
+ * block with the attribute, or one rule right after a `/* kmwc-fixed-colors *\/` comment.
256
+ */
257
+ export const FIXED_COLORS_ATTRIBUTE = 'data-kmwc-fixed-colors';
258
+ export const FIXED_COLORS_MARKER = 'kmwc-fixed-colors';
259
+ const MARKED_RULE_PATTERN = new RegExp(`/\\*[^*]*${FIXED_COLORS_MARKER}[\\s\\S]*?\\*/\\s*[^{}]*\\{[^{}]*\\}`, 'g');
260
+ const COLOR_PROPERTY_PATTERN = /color|background|border|outline|shadow|fill|stroke|text-decoration/i;
261
+ const COLOR_LITERAL_PATTERN =
262
+ /#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch)\(\s*(?!from\b)|\b(?:white|black|red|green|blue|gray|grey|silver|orange|yellow|purple|pink|navy|teal|maroon|olive|lime|aqua|fuchsia|gold|crimson)\b/i;
263
+
264
+ /** A declaration value with every `var(…)` collapsed — a literal fallback never shows while the kit token exists. */
265
+ function mapWithoutVarFallbacks(value) {
266
+ let previous;
267
+ let current = value;
268
+ do {
269
+ previous = current;
270
+ current = current.replace(/var\([^()]*\)/gi, 'var()');
271
+ } while (current !== previous);
272
+ return current;
273
+ }
274
+
275
+ /** True when a colour declaration's value is a literal that ignores dark mode. Pure. */
276
+ export function checkIsHardcodedColor(property, value) {
277
+ if (!COLOR_PROPERTY_PATTERN.test(property) || property.startsWith('--')) return false;
278
+ const plainValue = mapWithoutVarFallbacks(value);
279
+ return !/light-dark\(/i.test(plainValue) && COLOR_LITERAL_PATTERN.test(plainValue);
280
+ }
281
+
282
+ /**
283
+ * Hard-coded colours in one page's `<style>` blocks and `style="…"` attributes (or in
284
+ * examples-local.css): they stay the same in dark mode while the kit around them flips.
285
+ * `var()` fallbacks and `light-dark()` pairs follow the theme; a `<style data-kmwc-fixed-colors>`
286
+ * block and a rule marked `/* kmwc-fixed-colors *\/` are skipped. Returns [{ found }]. Pure.
287
+ */
288
+ export function getHardcodedColors(pageText, { isStylesheet = false } = {}) {
289
+ const styleText = isStylesheet
290
+ ? pageText
291
+ : [...pageText.matchAll(/<style\b([^>]*)>([\s\S]*?)<\/style>/gi)]
292
+ .filter((match) => !match[1].includes(FIXED_COLORS_ATTRIBUTE))
293
+ .map((match) => match[2])
294
+ .join('\n');
295
+ const hardcodedColors = [];
296
+ const checkedText = styleText.replace(MARKED_RULE_PATTERN, '').replace(/\/\*[\s\S]*?\*\//g, '');
297
+ for (const [, selector, body] of checkedText.matchAll(/([^{};@]+?)\s*\{([^{}]*)\}/g)) {
298
+ for (const declaration of body.split(';')) {
299
+ const [property, ...valueParts] = declaration.split(':');
300
+ const value = valueParts.join(':').trim();
301
+ if (value && checkIsHardcodedColor(property.trim(), value)) hardcodedColors.push({ found: `${selector.trim().replace(/\s+/g, ' ')} { ${property.trim()}: ${value} }` });
302
+ }
303
+ }
304
+ if (isStylesheet) return hardcodedColors;
305
+ for (const [, inlineStyle] of pageText.matchAll(/\sstyle="([^"]*)"/g)) {
306
+ for (const declaration of inlineStyle.split(';')) {
307
+ const [property, ...valueParts] = declaration.split(':');
308
+ const value = valueParts.join(':').trim();
309
+ if (value && checkIsHardcodedColor(property.trim(), value)) hardcodedColors.push({ found: `style="${property.trim()}: ${value}"` });
310
+ }
311
+ }
312
+ return hardcodedColors;
313
+ }
314
+
315
+ // ── core's default loader class ──────────────────────────────────────────────
316
+
317
+ const CORE_PACKAGE_PATH = join('node_modules', '@keenmate', 'web-components-core');
318
+ const CORE_RENDER_LOADER_FILE = join('dist', 'dom', 'render-loader.js');
319
+ const SOURCE_DIR = 'src';
320
+ const SKIPPED_SOURCE_DIRS = new Set(['node_modules', 'dist']);
321
+
322
+ /**
323
+ * The separator in the class core's default loader gets: `'__'` (`<prefix>__loader`),
324
+ * `'-'` (`<prefix>-loader`, before core 6d6b768), or null when the build doesn't say. Pure.
325
+ */
326
+ export function getCoreLoaderSeparator(renderLoaderText) {
327
+ const match =
328
+ renderLoaderText.match(/\$\{\s*[\w$.]+\s*\}(__|-)loader(?![\w-])/) || renderLoaderText.match(/\+\s*["'`](__|-)loader["'`]/);
329
+ return match ? match[1] : null;
330
+ }
331
+
332
+ /** The loader classes a stylesheet styles: [{ className, separator }]. Pure. */
333
+ export function getStyledLoaderClasses(cssText, prefix) {
334
+ const pattern = new RegExp(`\.(${prefix}(__|-)loader)(?![\w-])`, 'g');
335
+ const plainText = cssText.replace(/\/\*[\s\S]*?\*\//g, '');
336
+ const classes = new Map([...plainText.matchAll(pattern)].map((match) => [match[1], match[2]]));
337
+ return [...classes].map(([className, separator]) => ({ className, separator }));
338
+ }
339
+
340
+ /** The installed core (walking up for hoisted installs): { version, separator } or null. */
341
+ export function loadCoreLoader(root) {
342
+ for (let directory = resolve(root); ; directory = resolve(directory, '..')) {
343
+ const packageText = readText(join(directory, CORE_PACKAGE_PATH, 'package.json'));
344
+ if (packageText) {
345
+ const renderLoaderText = readText(join(directory, CORE_PACKAGE_PATH, CORE_RENDER_LOADER_FILE));
346
+ return { version: JSON.parse(packageText).version, separator: renderLoaderText ? getCoreLoaderSeparator(renderLoaderText) : null };
347
+ }
348
+ if (resolve(directory, '..') === directory) return null;
349
+ }
350
+ }
351
+
352
+ function getSourceStylesheets(directory, relativeDirectory = SOURCE_DIR) {
353
+ if (!existsSync(directory)) return [];
354
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
355
+ const relativePath = `${relativeDirectory}/${entry.name}`;
356
+ if (entry.isDirectory()) return SKIPPED_SOURCE_DIRS.has(entry.name) ? [] : getSourceStylesheets(join(directory, entry.name), relativePath);
357
+ return entry.name.endsWith('.css') ? [relativePath] : [];
358
+ });
359
+ }
360
+
361
+ /** Loader classes styled in the component's src CSS: [{ file, className, separator }]. */
362
+ export function loadStyledLoaderClasses(root, prefix) {
363
+ return getSourceStylesheets(join(root, SOURCE_DIR)).flatMap((file) =>
364
+ getStyledLoaderClasses(readFileSync(join(root, file), 'utf8'), prefix).map((styledClass) => ({ file, ...styledClass }))
365
+ );
366
+ }
@@ -118,7 +118,7 @@
118
118
  padding: 0.2rem 0.4rem;
119
119
  border-radius: 3px;
120
120
  font-size: 0.9em;
121
- color: var(--kmwc-accent-color);
121
+ color: var(--kmwc-inline-code-color);
122
122
  font-family: var(--kmwc-mono-font-family);
123
123
  /* A long identifier has no break opportunity — break it anywhere
124
124
  rather than widen the page on a phone. */
@@ -118,7 +118,11 @@
118
118
  --kmwc-console-info-color: #7dd3fc;
119
119
  --kmwc-console-warn-color: #fcd34d;
120
120
  --kmwc-console-err-color: #fca5a5;
121
- --kmwc-inline-code-bg: light-dark(#edf2f7, #2b2b2b);
121
+ /* Dark: the chip is a step up from the main surface (slate on a slate page, not a
122
+ neutral grey), and the text keeps the accent hue but at least 0.8 lightness, so a
123
+ mid-dark accent (treeview's #059669) stays readable on it. */
124
+ --kmwc-inline-code-bg: light-dark(#edf2f7, oklch(from var(--kmwc-main-bg) calc(l + 0.07) c h));
125
+ --kmwc-inline-code-color: light-dark(var(--kmwc-accent-color), oklch(from var(--kmwc-accent-color) max(l, 0.8) c h));
122
126
 
123
127
  /* ======================================================================
124
128
  COLORS — SEMANTIC
@@ -28,7 +28,10 @@ locally — every local fork is how they drifted apart.
28
28
  element out. Log lines take the kit tones (`.log-line--ok|info|warn|err|muted`,
29
29
  `--action`, timeline cells `.log-line__time` …) — don't colour them per page.
30
30
  `<kbd>`, `<mark>`, field widths (`.w-xs` … `.w-lg`) and `.push-end` are kit
31
- too — no inline styles for them.
31
+ too — no inline styles for them. Page colours come from the kit's `--kmwc-*`
32
+ tokens or `light-dark()` — a literal doesn't follow dark mode (doctor warns);
33
+ a demo that shows fixed colours on purpose goes in `<style data-kmwc-fixed-colors>`
34
+ (one rule: a `/* kmwc-fixed-colors: why */` comment before it).
32
35
  If a second component needs a pattern, it belongs in the devkit.
33
36
  - **Example pages are fragments:** no `<html>`/`<head>`/`<header>` — start with
34
37
  `<meta name="kmwc:title" content="…">`, the devkit layout adds the rest. Shared text