@motion-proto/live-tokens 0.60.1 → 0.62.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.
Files changed (49) hide show
  1. package/.claude/skills/live-tokens-build-page/SKILL.md +1 -1
  2. package/.claude/skills/live-tokens-pair-fonts/SKILL.md +1 -1
  3. package/CHANGELOG.md +107 -0
  4. package/dist-plugin/{chunk-XWXIMTWZ.js → chunk-7TQQTHI6.js} +10 -1
  5. package/dist-plugin/{chunk-LW4SR7AZ.js → chunk-O6GQ6GBH.js} +1 -1
  6. package/dist-plugin/{chunk-E5QYON4L.js → chunk-OPYOK2CA.js} +77 -26
  7. package/dist-plugin/generateColorsAndType/index.js +2 -2
  8. package/dist-plugin/index.cjs +179 -25
  9. package/dist-plugin/index.js +99 -4
  10. package/dist-plugin/tokensCssMigrations/index.cjs +76 -25
  11. package/dist-plugin/tokensCssMigrations/index.js +2 -2
  12. package/package.json +6 -1
  13. package/src/app/site.css +16 -0
  14. package/src/editor/component-editor/CardEditor.svelte +7 -1
  15. package/src/editor/component-editor/ImageLightboxEditor.svelte +2 -1
  16. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +25 -2
  17. package/src/editor/core/sketch/maskField.ts +70 -34
  18. package/src/editor/core/sketch/sketchLayer.ts +24 -3
  19. package/src/editor/core/sketch/sketchPresets.ts +10 -11
  20. package/src/editor/core/sketch/sketchStore.ts +89 -18
  21. package/src/editor/docs/Docs.svelte +1 -0
  22. package/src/editor/docs/chapters.ts +1 -0
  23. package/src/editor/docs/content/light-and-dark.md +90 -0
  24. package/src/editor/docs/content/sketch-mode.md +7 -2
  25. package/src/editor/docs/content.generated.ts +2 -1
  26. package/src/editor/overlay/LiveTokensRouter.svelte +8 -0
  27. package/src/editor/ui/sections/textStyles.ts +15 -1
  28. package/src/editor/ui/sketch/SketchTab.svelte +33 -81
  29. package/src/live-tokens/data/colors-and-type/midnight-study.json +40 -30
  30. package/src/live-tokens/data/themes/autumn.json +4 -3
  31. package/src/live-tokens/data/themes/halloween.json +4 -3
  32. package/src/live-tokens/data/themes/midnight-study.json +62 -49
  33. package/src/live-tokens/data/themes/ocean.json +4 -3
  34. package/src/live-tokens/data/themes/royal-velvet.json +4 -3
  35. package/src/live-tokens/data/themes/sketchy.json +4 -3
  36. package/src/live-tokens/data/themes/spring-meadow.json +4 -3
  37. package/src/live-tokens/data/themes/sunset.json +4 -3
  38. package/src/live-tokens/data/tokens.generated.css +5 -0
  39. package/src/system/backdrop/backdrop.ts +197 -0
  40. package/src/system/backdrop/index.ts +16 -0
  41. package/src/system/backdrop/watch.ts +139 -0
  42. package/src/system/components/Card.svelte +27 -9
  43. package/src/system/components/FloatingTokenTags.svelte +1 -1
  44. package/src/system/components/ImageLightbox.svelte +8 -4
  45. package/src/system/components/SectionDivider.svelte +3 -3
  46. package/src/system/components/SegmentedControl.svelte +11 -9
  47. package/src/system/styles/tokens.css +33 -5
  48. package/template/src/pages/Home.svelte +1 -11
  49. package/src/system/internal/backgroundContrast.ts +0 -62
@@ -10,7 +10,7 @@ Two rules above all else:
10
10
  1. **Use a shipped component if one fits.** Import from `@motion-proto/live-tokens/components/<Name>.svelte`. See [[live-tokens-pick-component]] for the catalogue and the confusing-pair decisions. Author custom markup only when nothing fits, and then consider [[live-tokens-create-component]] so the new piece is editable too.
11
11
  2. **Use theme tokens for every value.** Every color, spacing, radius, font-size, and font-family in page CSS is a `var(--token-*)`. No hex literals. No pixel literals. A change in `/live-tokens/editor` should repaint your page.
12
12
 
13
- For text, reach for a whole text style rather than assembling one: `--heading-xl` through `--heading-sm`, `--body-md`, `--body-sm`, `--eyebrow`, and `--code` each carry a `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`. A heading set from `--heading-lg-*` retypes when the theme's fonts change; one set from a raw `font-size` does not.
13
+ For text, reach for a whole text style rather than assembling one: `--heading-xl` through `--heading-sm`, `--body-md`, `--body-sm`, `--editorial-md`, `--editorial-sm`, `--eyebrow`, and `--code` each carry a `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`. A heading set from `--heading-lg-*` retypes when the theme's fonts change; one set from a raw `font-size` does not.
14
14
 
15
15
  ## Layout
16
16
 
@@ -24,7 +24,7 @@ Flags: `--dry-run` reports without writing. `--no-verify` skips the network and
24
24
  { "display": "Fraunces", "body": "Nunito Sans" }
25
25
  ```
26
26
 
27
- Every slot is optional and an omitted slot is left exactly as it is. `display` is `--font-display`, `body` is `--font-sans`; `serif`, `mono` and `editorial` exist when a theme needs them. `editorial` is `--font-editorial`, the long-reading face behind the `--editorial-*` text style: it tracks the body face until a theme repoints it, so set it only when essays and articles should not carry the body face. A slot may be `{ "name": "...", "url": "..." }` to pin an exact URL. Spell families as Google does; the CLI reports the canonical spelling back.
27
+ Every slot is optional and an omitted slot is left exactly as it is. `display` is `--font-display`, `body` is `--font-sans`; `serif`, `mono` and `editorial` exist when a theme needs them. `editorial` is `--font-editorial`, the long-reading face behind the `--editorial-md-*` and `--editorial-sm-*` text styles: it tracks the body face until a theme repoints it, so set it only when essays and articles should not carry the body face. A slot may be `{ "name": "...", "url": "..." }` to pin an exact URL. Spell families as Google does; the CLI reports the canonical spelling back.
28
28
 
29
29
  ## Choose the body face first
30
30
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,112 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.62.0 — Editorial type reads as a pair
4
+
5
+ ### Added
6
+
7
+ - **The editorial role has its own sizes.** The role shipped with a single
8
+ unsized bundle, so an editorial surface could only take the body size. It now
9
+ reads as a pair, matching body: `--editorial-md-*` and `--editorial-sm-*`,
10
+ both editable in the editor's Text Styles section beside the other styles, and
11
+ both carried by an `.editorial-md` / `.editorial-sm` class in `site.css`. Each
12
+ step mirrors its body counterpart apart from the family, so a project that
13
+ never repoints `--font-editorial` renders exactly as it did.
14
+ - **`Card` takes `variant="bare"`.** A card with no icon and no title dropped
15
+ its header silently, which read the same as a card whose title had gone
16
+ missing. `bare` says the headerless card is the intent; the frame and body
17
+ keep every token the default variant uses, so padding, stroke and fill stay
18
+ editable. A default-variant card with nothing to put in its header now warns
19
+ in dev.
20
+ - **Sketch mode reaches the component editor.** A component demo draws itself
21
+ the way the page will, sketch included, while the tab strips and property rows
22
+ around it stay chrome: they run on `--ui-*` tokens the layer knows nothing
23
+ about. The router hands the layer the root to paint, so a sketch style never
24
+ bleeds onto the editor's own routes.
25
+
26
+ ### Changed (breaking)
27
+
28
+ - **`--editorial-*` is now `--editorial-md-*`.** The unsized bundle became the
29
+ medium step. A `breaking` tokens.css migration
30
+ (`2026-08-27-editorial-size-steps`) renames it and backfills both steps,
31
+ carrying any value already tuned. It never auto-applies: run
32
+ `npx live-tokens migrate`.
33
+
34
+ ### Changed
35
+
36
+ - **Ink coverage is a levels control.** The dials read as a pile of unrelated
37
+ knobs because each one meant something different depending on the grain and
38
+ octave count under it. The field is now normalized to run black to white
39
+ whatever the noise beneath, so **Steps** flattens it into tones and **Output**
40
+ squeezes the whole of it into the range the ink covers, from palest to
41
+ densest. The field is squeezed into that gap, never clipped at it, so both
42
+ ends mean the same thing at every setting. The seven shipped presets are
43
+ retuned against the new response.
44
+ - **Menus and tooltips are drawn solid whatever the coverage dials say.** They
45
+ float over arbitrary page content, so a fill worn through in patches let the
46
+ page show through them and they stopped reading as a surface the pointer can
47
+ land on.
48
+ - **`SectionDivider` titles take no outline by default.** The three sizes each
49
+ carried a themed outline color, which drew a halo around every subsection
50
+ title on a page that never asked for one. All three default to transparent;
51
+ a theme that wants the halo sets the color.
52
+
53
+ ### Fixed
54
+
55
+ - **Per-side padding no longer strands the base token.** `themed-padding`
56
+ emits `var(--x-side, <base>)` for each side, so a per-side token is an
57
+ override slot the editor fills when the user splits the padding. `Card` and
58
+ `SegmentedControl` declared sides that named a literal step instead, which
59
+ always beat that fallback and left the base inert: merging split padding back
60
+ to one value cleared the side aliases, the fallback never took over, and the
61
+ padding control moved nothing. Both now track their own base, and
62
+ `paddingBaseStaysLive` holds every component that uses `themed-padding` to
63
+ the same rule. `Card`'s header and body padding are symmetric as a result;
64
+ a theme that wants the asymmetry back sets the per-side aliases.
65
+ - **`ImageLightbox`'s tile fill stays on the closed tile.** The fill also
66
+ painted the opened stage, where it had nothing to do: the stage rides a
67
+ near-opaque overlay that already backs transparent art, so the fill only
68
+ showed as a slab of theme color in the gap the image's aspect left inside the
69
+ stage box. The open stage now takes no fill of its own, matching the tile
70
+ shadow, which already stopped at the same edge.
71
+ - **Sketch settings no longer accumulate a trace on `globalThis`.** Debug
72
+ instrumentation left in `sketchStore` pushed an entry onto an unbounded
73
+ global array on every settings write, every share and every cross-frame
74
+ adopt. It is gone.
75
+
76
+ ## 0.61.0 — A surface says which way it leans
77
+
78
+ ### Added
79
+
80
+ - **One answer to "is this surface light or dark".** A wordmark drawn in white
81
+ disappears on a pale theme, ink that multiplies onto paper vanishes on a dark
82
+ one, and a headline over a photograph is on a dark ground whatever the palette
83
+ says. Every one of those needs the same fact, and every one of them was
84
+ answering it for itself: a luminance probe here, a `--page-bg` poll there, a
85
+ hand-picked token pair somewhere else. `@motion-proto/live-tokens/backdrop`
86
+ answers it once. `data-backdrop` is the single channel — state it in markup
87
+ where the surface knows its own tone, or let `use:backdrop` measure what
88
+ actually paints and stamp it — and `tokens.css` turns the attribute into
89
+ `color-scheme`, so `light-dark()` under it resolves the half that reads. The
90
+ module also answers in JavaScript, for a canvas, a WebGL uniform, or an image
91
+ that comes in two versions: `isLightBackdrop`, `watchBackdrop`, `polarityOf`,
92
+ `contrastTokenFor`, and `cssColorToHex`. A new guide chapter, **Light and
93
+ dark**, covers the whole of it.
94
+ - **A theme's own polarity is baked, not probed.** Nothing in a saved theme
95
+ recorded whether it was light or dark — the generator takes `scheme` as a
96
+ brief and throws it away — so anything that needed to know had to measure at
97
+ runtime and repaint. `tokens.generated.css` now carries a `color-scheme`
98
+ measured from the production theme's `--page-bg`, at zero specificity: it is
99
+ the answer at first paint, and any `data-backdrop` stamp — the editor
100
+ switching themes live, a section stating its own tone — outranks it.
101
+
102
+ - **`ImageLightbox` has a tile fill.** A diagram drawn in light ink, or any
103
+ cut-out with a transparent ground, disappeared into a light page: the tile is
104
+ exactly the image's own box, so nothing stood behind the art to separate it
105
+ from the section. `--imagelightbox-tile-surface` paints behind the image on
106
+ the closed tile and the opened stage alike, inside the radius and border the
107
+ tile already carries. It takes any fill the editor offers, solid or gradient,
108
+ and defaults to none, so every existing lightbox renders as it did.
109
+
3
110
  ## 0.60.1 — A drawn heading is type, not an icon
4
111
 
5
112
  ### Fixed
@@ -113,6 +113,13 @@ function cssColorToOklch(value) {
113
113
  if (HEX6_RE.test(raw)) return hexToOklch(raw);
114
114
  return parseOklchCss(raw);
115
115
  }
116
+ function oklchToRgb255(l, c, h) {
117
+ const clamped = gamutClamp(l, c, h);
118
+ const [L, a, bVal] = oklchToOklab(clamped.l, clamped.c, clamped.h);
119
+ const [r, g, b] = oklabToLinearRgb(L, a, bVal);
120
+ const to255 = (channel) => Math.round(Math.max(0, Math.min(1, linearToSrgb(channel))) * 255);
121
+ return { r: to255(r), g: to255(g), b: to255(b) };
122
+ }
116
123
 
117
124
  export {
118
125
  hexToOklch,
@@ -120,5 +127,7 @@ export {
120
127
  oklchToHexClamped,
121
128
  gamutClamp,
122
129
  oklchToCss,
123
- cssColorToOklch
130
+ parseOklchCss,
131
+ cssColorToOklch,
132
+ oklchToRgb255
124
133
  };
@@ -2,7 +2,7 @@ import {
2
2
  cssColorToOklch,
3
3
  gamutClamp,
4
4
  oklchToCss
5
- } from "./chunk-XWXIMTWZ.js";
5
+ } from "./chunk-7TQQTHI6.js";
6
6
 
7
7
  // src/editor/core/storage/files/versionedFileResourceClient.ts
8
8
  function sanitizeFileName(name) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  hexToOklch,
3
3
  oklchToCss
4
- } from "./chunk-XWXIMTWZ.js";
4
+ } from "./chunk-7TQQTHI6.js";
5
5
 
6
6
  // src/editor/core/themes/parsers/globalRootBlock.ts
7
7
  function extractGlobalRootBody(source) {
@@ -377,37 +377,15 @@ var tokensCssMigration_2026_08_19_oklchColorValues = {
377
377
 
378
378
  // vite-plugin/tokensCssMigrations/migrations/2026-08-25-editorial-type-role.ts
379
379
  var STACK = [{ name: "--font-editorial", value: "var(--font-sans)" }];
380
- var BUNDLE2 = [
381
- { name: "--editorial-font-family", value: "var(--font-editorial)" },
382
- { name: "--editorial-font-size", value: "var(--font-size-md)" },
383
- { name: "--editorial-font-weight", value: "var(--font-weight-normal)" },
384
- { name: "--editorial-line-height", value: "var(--line-height-normal)" },
385
- { name: "--editorial-letter-spacing", value: "var(--letter-spacing-normal)" }
386
- ];
387
380
  var tokensCssMigration_2026_08_25_editorialTypeRole = {
388
381
  id: "2026-08-25-editorial-type-role",
389
382
  kind: "additive",
390
- description: "Add the editorial type role (--font-editorial and the --editorial-* bundle)",
383
+ description: "Add the editorial type role (--font-editorial)",
391
384
  apply(css) {
392
- const withStack = ensureScale(css, {
385
+ return ensureScale(css, {
393
386
  anchorPrefixes: ["--font-mono", "--font-serif", "--font-sans", "--font-display"],
394
387
  entries: STACK
395
388
  });
396
- return ensureScale(withStack, {
397
- sectionComment: "Editorial \u2014 the long-reading role, body face until repointed",
398
- anchorPrefixes: [
399
- "--eyebrow-",
400
- "--code-",
401
- "--body-",
402
- "--heading-",
403
- "--letter-spacing-",
404
- "--line-height-",
405
- "--font-weight-",
406
- "--font-size-",
407
- "--font-"
408
- ],
409
- entries: BUNDLE2
410
- });
411
389
  }
412
390
  };
413
391
 
@@ -430,6 +408,77 @@ var tokensCssMigration_2026_08_26_gradientStops = {
430
408
  }
431
409
  };
432
410
 
411
+ // vite-plugin/tokensCssMigrations/migrations/2026-08-27-backdrop-polarity.ts
412
+ var MARKER = "[data-backdrop='dark']";
413
+ var RULES = `/* BACKDROP POLARITY \u2014 one attribute says which way a surface leans. State it
414
+ in markup (\`<section data-backdrop="dark">\`) or let \`use:backdrop\` measure
415
+ and stamp it. Either way \`light-dark()\` inside resolves the right half, and
416
+ a rule can key on the attribute for what colour alone cannot carry. */
417
+
418
+ [data-backdrop='light'] {
419
+ color-scheme: light;
420
+ }
421
+
422
+ [data-backdrop='dark'] {
423
+ color-scheme: dark;
424
+ }
425
+ `;
426
+ var tokensCssMigration_2026_08_27_backdropPolarity = {
427
+ id: "2026-08-27-backdrop-polarity",
428
+ kind: "additive",
429
+ description: "Add the [data-backdrop] \u2192 color-scheme rules",
430
+ apply(css) {
431
+ if (css.includes(MARKER)) return css;
432
+ const at = css.indexOf(":root");
433
+ if (at === -1) return `${css.trimEnd()}
434
+
435
+ ${RULES}`;
436
+ return `${css.slice(0, at)}${RULES}
437
+ ${css.slice(at)}`;
438
+ }
439
+ };
440
+
441
+ // vite-plugin/tokensCssMigrations/migrations/2026-08-27-editorial-size-steps.ts
442
+ var AXES = ["font-family", "font-size", "font-weight", "line-height", "letter-spacing"];
443
+ var BUNDLE2 = [
444
+ { name: "--editorial-md-font-family", value: "var(--font-editorial)" },
445
+ { name: "--editorial-md-font-size", value: "var(--font-size-md)" },
446
+ { name: "--editorial-md-font-weight", value: "var(--font-weight-normal)" },
447
+ { name: "--editorial-md-line-height", value: "var(--line-height-normal)" },
448
+ { name: "--editorial-md-letter-spacing", value: "var(--letter-spacing-normal)" },
449
+ { name: "--editorial-sm-font-family", value: "var(--font-editorial)" },
450
+ { name: "--editorial-sm-font-size", value: "var(--font-size-sm)" },
451
+ { name: "--editorial-sm-font-weight", value: "var(--font-weight-normal)" },
452
+ { name: "--editorial-sm-line-height", value: "var(--line-height-tight)" },
453
+ { name: "--editorial-sm-letter-spacing", value: "var(--letter-spacing-normal)" }
454
+ ];
455
+ var tokensCssMigration_2026_08_27_editorialSizeSteps = {
456
+ id: "2026-08-27-editorial-size-steps",
457
+ kind: "breaking",
458
+ description: "Split the editorial bundle into --editorial-md-* and --editorial-sm-* steps",
459
+ apply(css) {
460
+ let out = css;
461
+ for (const axis of AXES) {
462
+ out = renameToken(out, `--editorial-${axis}`, `--editorial-md-${axis}`);
463
+ }
464
+ return ensureScale(out, {
465
+ sectionComment: "Editorial \u2014 the long-reading role, body face until repointed",
466
+ anchorPrefixes: [
467
+ "--eyebrow-",
468
+ "--code-",
469
+ "--body-",
470
+ "--heading-",
471
+ "--letter-spacing-",
472
+ "--line-height-",
473
+ "--font-weight-",
474
+ "--font-size-",
475
+ "--font-"
476
+ ],
477
+ entries: BUNDLE2
478
+ });
479
+ }
480
+ };
481
+
433
482
  // vite-plugin/tokensCssMigrations/index.ts
434
483
  var TOKENS_CSS_MIGRATIONS = [
435
484
  tokensCssMigration_2026_05_29_typographyScaleAdditions,
@@ -441,7 +490,9 @@ var TOKENS_CSS_MIGRATIONS = [
441
490
  tokensCssMigration_2026_07_20_semanticTextStyles,
442
491
  tokensCssMigration_2026_08_19_oklchColorValues,
443
492
  tokensCssMigration_2026_08_25_editorialTypeRole,
444
- tokensCssMigration_2026_08_26_gradientStops
493
+ tokensCssMigration_2026_08_26_gradientStops,
494
+ tokensCssMigration_2026_08_27_backdropPolarity,
495
+ tokensCssMigration_2026_08_27_editorialSizeSteps
445
496
  ];
446
497
  function runTokensCssMigrations(css) {
447
498
  return foldMigrations(css, () => true);
@@ -15,7 +15,7 @@ import {
15
15
  scaleToCssVar,
16
16
  setCurveAnchor,
17
17
  syncBaseAnchor
18
- } from "../chunk-LW4SR7AZ.js";
18
+ } from "../chunk-O6GQ6GBH.js";
19
19
  import {
20
20
  CURRENT_COLORS_AND_TYPE_SCHEMA_VERSION,
21
21
  CURRENT_COMPONENT_SCHEMA_VERSION
@@ -26,7 +26,7 @@ import {
26
26
  hexToOklch,
27
27
  oklchToHex,
28
28
  oklchToHexClamped
29
- } from "../chunk-XWXIMTWZ.js";
29
+ } from "../chunk-7TQQTHI6.js";
30
30
  import {
31
31
  readLiveTokensConfig,
32
32
  resolveDataDirs
@@ -203,6 +203,9 @@ function sanitizeFileName(name) {
203
203
  function srgbToLinear(c) {
204
204
  return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
205
205
  }
206
+ function linearToSrgb(c) {
207
+ return c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
208
+ }
206
209
  function hexToLinearRgb(hex) {
207
210
  const r = parseInt(hex.slice(1, 3), 16) / 255;
208
211
  const g = parseInt(hex.slice(3, 5), 16) / 255;
@@ -295,6 +298,13 @@ function cssColorToOklch(value) {
295
298
  if (HEX6_RE.test(raw)) return hexToOklch(raw);
296
299
  return parseOklchCss(raw);
297
300
  }
301
+ function oklchToRgb255(l, c, h) {
302
+ const clamped = gamutClamp(l, c, h);
303
+ const [L, a, bVal] = oklchToOklab(clamped.l, clamped.c, clamped.h);
304
+ const [r, g, b] = oklabToLinearRgb(L, a, bVal);
305
+ const to255 = (channel2) => Math.round(Math.max(0, Math.min(1, linearToSrgb(channel2))) * 255);
306
+ return { r: to255(r), g: to255(g), b: to255(b) };
307
+ }
298
308
 
299
309
  // src/editor/ui/curveEngine.ts
300
310
  function makeAnchor(x, y, tangentLen = 15) {
@@ -1520,6 +1530,87 @@ function runMigrations(kind, fileVersion, rawVars, meta = {}) {
1520
1530
  return out;
1521
1531
  }
1522
1532
 
1533
+ // src/system/backdrop/backdrop.ts
1534
+ var LIGHT_BACKDROP = 0.2;
1535
+ var OPAQUE = 0.98;
1536
+ var HEX_RE = /#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})\b/gi;
1537
+ var OKLCH_RE2 = /oklch\(\s*[\d.]+%?\s+[\d.]+%?\s+[\d.]+(?:deg)?\s*(?:\/\s*([\d.]+%?)\s*)?\)/gi;
1538
+ var RGB_RE = /rgba?\(\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)(?:\s*[,/]\s*([\d.]+%?))?\s*\)/gi;
1539
+ var EXOTIC_RE = /\b(?:color-mix|hwb|lab|lch|oklab|color)\([^()]*\)/gi;
1540
+ function channel(value) {
1541
+ const parsed = Number.parseFloat(value);
1542
+ return value.endsWith("%") ? parsed * 2.55 : parsed;
1543
+ }
1544
+ function alpha(value) {
1545
+ if (value === void 0) return 1;
1546
+ const parsed = Number.parseFloat(value);
1547
+ return value.endsWith("%") ? parsed / 100 : parsed;
1548
+ }
1549
+ function parseHex(hex) {
1550
+ const digits = hex.slice(1);
1551
+ const full = digits.length <= 4 ? digits.split("").map((part) => part + part).join("") : digits;
1552
+ return {
1553
+ r: Number.parseInt(full.slice(0, 2), 16),
1554
+ g: Number.parseInt(full.slice(2, 4), 16),
1555
+ b: Number.parseInt(full.slice(4, 6), 16),
1556
+ a: full.length >= 8 ? Number.parseInt(full.slice(6, 8), 16) / 255 : 1
1557
+ };
1558
+ }
1559
+ function colorsIn(background) {
1560
+ const colors = [];
1561
+ for (const match of background.matchAll(HEX_RE)) colors.push(parseHex(match[0]));
1562
+ for (const match of background.matchAll(OKLCH_RE2)) {
1563
+ const parsed = parseOklchCss(match[0]);
1564
+ if (parsed) colors.push({ ...oklchToRgb255(parsed.l, parsed.c, parsed.h), a: alpha(match[1]) });
1565
+ }
1566
+ for (const match of background.matchAll(RGB_RE)) {
1567
+ colors.push({
1568
+ r: channel(match[1]),
1569
+ g: channel(match[2]),
1570
+ b: channel(match[3]),
1571
+ a: alpha(match[4])
1572
+ });
1573
+ }
1574
+ for (const match of background.matchAll(EXOTIC_RE)) {
1575
+ const resolved = viaCanvas(match[0]);
1576
+ if (resolved) colors.push(resolved);
1577
+ }
1578
+ return colors;
1579
+ }
1580
+ function linearChannel(value) {
1581
+ const srgb = Math.max(0, Math.min(255, value)) / 255;
1582
+ return srgb <= 0.04045 ? srgb / 12.92 : ((srgb + 0.055) / 1.055) ** 2.4;
1583
+ }
1584
+ function luminance({ r, g, b }) {
1585
+ return 0.2126 * linearChannel(r) + 0.7152 * linearChannel(g) + 0.0722 * linearChannel(b);
1586
+ }
1587
+ var SEED = "#010203";
1588
+ var probe;
1589
+ function viaCanvas(value) {
1590
+ if (typeof document === "undefined") return null;
1591
+ if (probe === void 0) {
1592
+ probe = document.createElement("canvas").getContext?.("2d", { willReadFrequently: true }) ?? null;
1593
+ }
1594
+ if (!probe) return null;
1595
+ probe.fillStyle = SEED;
1596
+ probe.fillStyle = value;
1597
+ if (probe.fillStyle === SEED) return null;
1598
+ probe.clearRect(0, 0, 1, 1);
1599
+ probe.fillRect(0, 0, 1, 1);
1600
+ const [r, g, b, a] = probe.getImageData(0, 0, 1, 1).data;
1601
+ return { r, g, b, a: a / 255 };
1602
+ }
1603
+ function backgroundLuminance(background) {
1604
+ const opaque = colorsIn(background).filter((color) => color.a >= OPAQUE);
1605
+ if (!opaque.length) return null;
1606
+ return opaque.reduce((total, color) => total + luminance(color), 0) / opaque.length;
1607
+ }
1608
+ function polarityOfBackground(background) {
1609
+ const measured = backgroundLuminance(background);
1610
+ if (measured === null) return null;
1611
+ return measured >= LIGHT_BACKDROP ? "light" : "dark";
1612
+ }
1613
+
1523
1614
  // vite-plugin/files/versionedFileResourceServer.ts
1524
1615
  var import_fs = __toESM(require("fs"), 1);
1525
1616
  var import_path = __toESM(require("path"), 1);
@@ -2419,37 +2510,15 @@ var tokensCssMigration_2026_08_19_oklchColorValues = {
2419
2510
 
2420
2511
  // vite-plugin/tokensCssMigrations/migrations/2026-08-25-editorial-type-role.ts
2421
2512
  var STACK = [{ name: "--font-editorial", value: "var(--font-sans)" }];
2422
- var BUNDLE2 = [
2423
- { name: "--editorial-font-family", value: "var(--font-editorial)" },
2424
- { name: "--editorial-font-size", value: "var(--font-size-md)" },
2425
- { name: "--editorial-font-weight", value: "var(--font-weight-normal)" },
2426
- { name: "--editorial-line-height", value: "var(--line-height-normal)" },
2427
- { name: "--editorial-letter-spacing", value: "var(--letter-spacing-normal)" }
2428
- ];
2429
2513
  var tokensCssMigration_2026_08_25_editorialTypeRole = {
2430
2514
  id: "2026-08-25-editorial-type-role",
2431
2515
  kind: "additive",
2432
- description: "Add the editorial type role (--font-editorial and the --editorial-* bundle)",
2516
+ description: "Add the editorial type role (--font-editorial)",
2433
2517
  apply(css) {
2434
- const withStack = ensureScale(css, {
2518
+ return ensureScale(css, {
2435
2519
  anchorPrefixes: ["--font-mono", "--font-serif", "--font-sans", "--font-display"],
2436
2520
  entries: STACK
2437
2521
  });
2438
- return ensureScale(withStack, {
2439
- sectionComment: "Editorial \u2014 the long-reading role, body face until repointed",
2440
- anchorPrefixes: [
2441
- "--eyebrow-",
2442
- "--code-",
2443
- "--body-",
2444
- "--heading-",
2445
- "--letter-spacing-",
2446
- "--line-height-",
2447
- "--font-weight-",
2448
- "--font-size-",
2449
- "--font-"
2450
- ],
2451
- entries: BUNDLE2
2452
- });
2453
2522
  }
2454
2523
  };
2455
2524
 
@@ -2472,6 +2541,77 @@ var tokensCssMigration_2026_08_26_gradientStops = {
2472
2541
  }
2473
2542
  };
2474
2543
 
2544
+ // vite-plugin/tokensCssMigrations/migrations/2026-08-27-backdrop-polarity.ts
2545
+ var MARKER = "[data-backdrop='dark']";
2546
+ var RULES = `/* BACKDROP POLARITY \u2014 one attribute says which way a surface leans. State it
2547
+ in markup (\`<section data-backdrop="dark">\`) or let \`use:backdrop\` measure
2548
+ and stamp it. Either way \`light-dark()\` inside resolves the right half, and
2549
+ a rule can key on the attribute for what colour alone cannot carry. */
2550
+
2551
+ [data-backdrop='light'] {
2552
+ color-scheme: light;
2553
+ }
2554
+
2555
+ [data-backdrop='dark'] {
2556
+ color-scheme: dark;
2557
+ }
2558
+ `;
2559
+ var tokensCssMigration_2026_08_27_backdropPolarity = {
2560
+ id: "2026-08-27-backdrop-polarity",
2561
+ kind: "additive",
2562
+ description: "Add the [data-backdrop] \u2192 color-scheme rules",
2563
+ apply(css) {
2564
+ if (css.includes(MARKER)) return css;
2565
+ const at = css.indexOf(":root");
2566
+ if (at === -1) return `${css.trimEnd()}
2567
+
2568
+ ${RULES}`;
2569
+ return `${css.slice(0, at)}${RULES}
2570
+ ${css.slice(at)}`;
2571
+ }
2572
+ };
2573
+
2574
+ // vite-plugin/tokensCssMigrations/migrations/2026-08-27-editorial-size-steps.ts
2575
+ var AXES = ["font-family", "font-size", "font-weight", "line-height", "letter-spacing"];
2576
+ var BUNDLE2 = [
2577
+ { name: "--editorial-md-font-family", value: "var(--font-editorial)" },
2578
+ { name: "--editorial-md-font-size", value: "var(--font-size-md)" },
2579
+ { name: "--editorial-md-font-weight", value: "var(--font-weight-normal)" },
2580
+ { name: "--editorial-md-line-height", value: "var(--line-height-normal)" },
2581
+ { name: "--editorial-md-letter-spacing", value: "var(--letter-spacing-normal)" },
2582
+ { name: "--editorial-sm-font-family", value: "var(--font-editorial)" },
2583
+ { name: "--editorial-sm-font-size", value: "var(--font-size-sm)" },
2584
+ { name: "--editorial-sm-font-weight", value: "var(--font-weight-normal)" },
2585
+ { name: "--editorial-sm-line-height", value: "var(--line-height-tight)" },
2586
+ { name: "--editorial-sm-letter-spacing", value: "var(--letter-spacing-normal)" }
2587
+ ];
2588
+ var tokensCssMigration_2026_08_27_editorialSizeSteps = {
2589
+ id: "2026-08-27-editorial-size-steps",
2590
+ kind: "breaking",
2591
+ description: "Split the editorial bundle into --editorial-md-* and --editorial-sm-* steps",
2592
+ apply(css) {
2593
+ let out = css;
2594
+ for (const axis of AXES) {
2595
+ out = renameToken2(out, `--editorial-${axis}`, `--editorial-md-${axis}`);
2596
+ }
2597
+ return ensureScale(out, {
2598
+ sectionComment: "Editorial \u2014 the long-reading role, body face until repointed",
2599
+ anchorPrefixes: [
2600
+ "--eyebrow-",
2601
+ "--code-",
2602
+ "--body-",
2603
+ "--heading-",
2604
+ "--letter-spacing-",
2605
+ "--line-height-",
2606
+ "--font-weight-",
2607
+ "--font-size-",
2608
+ "--font-"
2609
+ ],
2610
+ entries: BUNDLE2
2611
+ });
2612
+ }
2613
+ };
2614
+
2475
2615
  // vite-plugin/tokensCssMigrations/index.ts
2476
2616
  var TOKENS_CSS_MIGRATIONS = [
2477
2617
  tokensCssMigration_2026_05_29_typographyScaleAdditions,
@@ -2483,7 +2623,9 @@ var TOKENS_CSS_MIGRATIONS = [
2483
2623
  tokensCssMigration_2026_07_20_semanticTextStyles,
2484
2624
  tokensCssMigration_2026_08_19_oklchColorValues,
2485
2625
  tokensCssMigration_2026_08_25_editorialTypeRole,
2486
- tokensCssMigration_2026_08_26_gradientStops
2626
+ tokensCssMigration_2026_08_26_gradientStops,
2627
+ tokensCssMigration_2026_08_27_backdropPolarity,
2628
+ tokensCssMigration_2026_08_27_editorialSizeSteps
2487
2629
  ];
2488
2630
  function runTokensCssMigrations(css) {
2489
2631
  return foldMigrations(css, () => true);
@@ -2724,6 +2866,7 @@ function themeFileApi(opts) {
2724
2866
  }
2725
2867
  const colorsAndTypeData = productionTheme?.colorsAndType;
2726
2868
  let colorsAndTypeVarCount = 0;
2869
+ let pageBackground = "";
2727
2870
  if (colorsAndTypeData) {
2728
2871
  const cssVars = { ...colorsAndTypeData.cssVariables || {} };
2729
2872
  Object.assign(cssVars, palettesToVars(migratePaletteColorsToOklch(colorsAndTypeData.editorConfigs ?? {})));
@@ -2731,6 +2874,7 @@ function themeFileApi(opts) {
2731
2874
  for (const [name, value] of Object.entries(resolvedFontVars)) {
2732
2875
  cssVars[name] = value;
2733
2876
  }
2877
+ pageBackground = cssVars["--page-bg"] ?? "";
2734
2878
  colorsAndTypeVarCount = Object.keys(cssVars).length;
2735
2879
  if (colorsAndTypeVarCount > 0) {
2736
2880
  lines.push(`/* Production theme: ${productionThemeName} */`);
@@ -2742,6 +2886,16 @@ function themeFileApi(opts) {
2742
2886
  lines.push("");
2743
2887
  }
2744
2888
  }
2889
+ {
2890
+ const polarity = pageBackground ? polarityOfBackground(pageBackground) : null;
2891
+ if (polarity) {
2892
+ lines.push(`/* Page polarity, from --page-bg */`);
2893
+ lines.push(":where(:root) {");
2894
+ lines.push(` color-scheme: ${polarity};`);
2895
+ lines.push("}");
2896
+ lines.push("");
2897
+ }
2898
+ }
2745
2899
  let componentOverrideCount = 0;
2746
2900
  if (import_fs4.default.existsSync(COMPONENT_CONFIGS_DIR)) {
2747
2901
  const blocks = [];