# Changelog

**English** · [简体中文](./CHANGELOG.zh-CN.md)

## 0.7.0 — 2026-08-11

### JSON configuration

- The CLI now accepts `adaptive.config.json`, strips its `$schema` metadata before compilation, and reports malformed or non-object JSON without exposing an internal stack trace.
- The published JSON Schema describes `$schema`, so editors can complete and validate a JSON configuration against the same option model used by the documentation site.
- JSON files are read with `readFile` and `JSON.parse`, preserving the package's Node 18 support; JavaScript configuration remains available for regular expressions and predicate functions.

### Quality gates

- Added ESLint, Prettier, VitePress source type-checking, documentation-site builds, and stricter coverage thresholds to `npm run check` and CI.
- Added a ratio-based performance budget that compares compiler cost with PostCSS parse-and-print cost, avoiding unstable absolute timing limits on shared runners.
- Expanded evaluator, selector scanner, CLI, schema, stdin, compatibility and error-path coverage; coverage now reaches 98.10% statements, 93.59% branches and 99.35% lines.
- Fixed two issues found by the new checks: serialising a value-parser node through an unsupported path and allowing an unknown value to stringify as `[object Object]` in generated output.

## 0.6.0 — 2026-08-11

### Breakpoints have a canvas now

- **New `media` route matcher.** A responsive stylesheet is one file holding two design files: the phone numbers were measured on a 750 mock and the ones inside `@media (min-width: 1024px)` on a 1440 one. Nothing in the CSS says so, and until now nothing could. `routes: [{ media: { minWidth: 1024 }, profile: 'pc' }]` gives the breakpoint the design file it was drawn on.
- This was not a near miss, which is why it is worth a feature rather than a note. `@media (min-width: 1024px) { .hero { padding: 40px } }` on a 750 canvas bounded at 600px compiled to `clamp(17.07px, 5.33vw, 32px)` — and that rule is only ever live from 1024px up, past where the canvas stops scaling, so the `clamp()` was already pinned to its maximum everywhere the rule applies. The padding was a constant 32px at every width, forever. The compiler ran, the output looked compiled, and not one value moved.
- **Matching is by implication rather than by text.** `{ minWidth: 1024 }` claims any rule that cannot apply below 1024px, so `screen and (min-width: 1200px)` and `(min-width: 1024px) and (max-width: 1600px)` are both claimed, and nesting composes because nesting is conjunction. Comparing the params as a string would have missed the `screen and` — and a design canvas is a fact about which widths a rule reaches, not about how the query was spelled.
- **`rem` and `em` in a media query resolve at 16px**, not at `rootValue` and not at the root element's font size. A query is evaluated before any declaration could change `font-size`, so it cannot depend on the cascade it selects: `64rem` is 1024px even in a stylesheet whose `html` is `62.5%`. This is a fact about the query rather than an assumption about the page, and reading only `px` would have left every Tailwind and UnoCSS project — which write every breakpoint in `rem` — unreadable. The continuity check reads the same parser and gained the same reach.
- A query the compiler cannot read — a comma, `not`, `only`, or any non-width feature — is claimed by **nothing**. That is a refusal, not a "matches everything": routing a rule on a condition nobody verified is how a canvas mistake gets made rather than caught. `@container` never counts either; it bounds an element, and `vw` has never been about the element.
- A `selector` route still outranks a width band, because a component library is drawn on its own canvas at every viewport width and a breakpoint does not change which design file a component came from. A route may name both — `{ selector: ['.van-'], media: { minWidth: 1024 }, profile: 'pc' }` — which is how "this component is redrawn at the desktop breakpoint" is said.
- An empty band (`{}`) and a reversed one (`minWidth: 1024, maxWidth: 600`) are configuration errors that throw. The first matches every rule in the stylesheet, which is a slow way of changing `defaultProfile`; the second matches nothing. Both read as working configuration, so neither may be left to be discovered from the output.

### The dead-band warning

- **You do not have to know any of the above to find the problem.** When a rule converts a length and the band it is live in lies entirely outside its canvas's fluid range, the compiler now says so, names both ranges, and gives the route that fixes it. This is arithmetic rather than a heuristic — two intervals that do not overlap — and it is the failure the multi-canvas model exists to prevent, arriving through the one door the model never watched.
- Reported once per canvas per band per file, and **only for rules that actually converted something**. A breakpoint that changes `display` and `color` is ordinary CSS with no lengths to be constant about, and warning on those would bury the report in the places it does not apply. The first version did exactly that, which is what the test for it was written from.
- The suggested fix adapts: when a selector route chose the canvas, the message asks for `{ selector: […], media: … }`, because a bare media route loses to a selector route and would have changed nothing. Advice that does not work is worse than no advice.
- **`appPcPreset` now reads its own breakpoint as well as writing it.** The preset already said "the desktop design file takes over at 768px" — that number set each profile's `query`, so an `@adaptive pc` block came out wrapped correctly. It just never *read* a media query, so a hand-written `@media (min-width: 768px)` block compiled against the phone canvas: the preset disagreeing with itself. Both directions are now routed, and the `max-width` half is stated explicitly rather than left to `defaultProfile`, so the pair keeps meaning what it says if the preset is spread over a desktop-first configuration.
- Two conformance cases, `breakpoints/media-route` and `breakpoints/dead-band`, covering what is claimed, what is refused, and the one-report-per-band rule.

### Selector routing

- Fixed **`:not()` and `:has()` arguments deciding which canvas a rule lands on**. `.page-hero:not(.van-cell)` styles page elements — precisely the ones that are *not* Vant cells — and was being sent to Vant's 375 canvas because the substring `.van-cell` appeared somewhere in the text. Every length in such a rule came out at exactly twice its intended size, silently. Routing now reads the **subject** of the selector: the arguments of `:not()` and `:has()` name an element other than the one being styled and are removed before matching, while `:is()` / `:where()` / `:matches()` / `-*-any()` arguments are kept, since those genuinely are alternatives for the subject. Measured against 22,761 rules in 11 published component-library stylesheets, 1,035 of which contain `:not()` or `:has()`: **zero** change canvas, because library CSS always carries its own prefix outside the exclusion. The fix costs nothing and closes a hole that application code walks into.
- **`:is()` and `:where()` lists spanning two canvases now warn.** This was previously documented as undetectable. `:is(.van-cell, .page-hero) { padding: 16px }` is one declaration wanted on two canvases, the same problem as a comma-separated list one bracket deeper, and it was going through unreported.
- The warning states **what splitting would cost**, computed rather than left to the reader: `:is()` matches every branch at the specificity of its highest one, so pulling the branches apart is free only when they already agree. When they do not, the warning names the drop — `:is() matches every branch at its highest, 1-1-0, so ".page-hero" would drop to 0-1-0`. This is what makes the advice actionable; a warning whose fix silently changes the cascade is a warning people learn to ignore. One report per rule: the remaining lists have the same cause and the same fix.
- New `src/core/selectors.ts`, dependency-free: `splitSelectorList`, `routingSelector`, `nestedSelectorLists`, `specificity`, `compareSpecificity`, `formatSpecificity`, `splitIsSpecificityNeutral`. Splitting is string-aware and attribute-aware, so `[data-x="a)b"], .c` splits into two — naive bracket counting gets that wrong. Specificity follows Selectors Level 4, including `:where()` counting zero, `:is()` / `:not()` / `:has()` taking their highest branch, `:nth-child(n of S)` counting as a pseudo-class plus the highest of `S`, and the four legacy single-colon pseudo-elements (`:before`, `:after`, `:first-line`, `:first-letter`) counting as elements. 40 new unit tests.

### Browser support audit

- **`:has()` is now audited.** It is the newest selector in everyday use and the four engines are years apart on it — Chrome 105 (Aug 2022), Safari 15.4, and Firefox not until 121 (Dec 2023). That spread is the widest in the table, and the failure is selector-level: the whole rule is dropped. A stylesheet reviewed in Chrome and Safari can be silently missing rules in an older Firefox. The compiler emits no `:has()`; it arrives from your own CSS or a component library and survives the pass, which is exactly why an audit that reads the *output* sees it. Unlike native nesting it is unmistakable in the text, so it is reported rather than guessed at.
- `css-has` added to `scripts/capture-compat.mjs` and regenerated into `src/core/compat-data.ts`; still no runtime dependency.

### A documentation site

- **The documentation is published as a site**, in both languages, at `https://moresyl.github.io/postcss-adaptive-matrix/`. It is built out of the repository rather than out of a copy of it: `srcDir` is the repository root, so `docs/README.md` linking to `../conformance/README.md` reaches a real page here for the same reason it does on GitHub, and no file moved. Search, dark mode, an edit link and a last-updated stamp come with it.
- The one thing that had to be designed rather than configured is the language switcher. VitePress resolves a relative link against the page's *rewritten* location, so `[English](./README.md)` at the top of a Chinese page — correct on GitHub — resolves back to the Chinese page on the site. Links are therefore resolved in the **repository**, against the source path, and emitted as absolute site URLs. Moving a page cannot break them.
- **Every option is published as data** at `/schema/options.json`: a JSON Schema 2020-12 document with each option's type, permitted values, range and default. Prose is the wrong shape for "is `precision` an integer and what is its ceiling".
- Two things stop it being decoration. Its property tables are typed over the source interfaces, so an option that exists but is not described fails `tsc`, and so does a described option that no longer exists. And every default is read out of `resolveOptions()` when the file is generated rather than transcribed, so a default that changes in the code changes here in the same commit. A test closes the gap the type system cannot see — that the default printed in the configuration reference is that same default — and found one disagreement immediately: `unitToConvert` resolves to a list, and both reference tables said `'px'`.
- Both languages travel inside the one schema: `description` is English, `x-description-zh` is Chinese. A type is not a translation. `x-also` names the JavaScript-only forms — a `RegExp`, a predicate function — that JSON cannot express but a config file accepts.
- **The compiler runs in the reader's browser.** The plugin has one runtime dependency and no Node API in its path, so the published source is imported straight into the playground page and PostCSS runs client-side. There is no service behind it and nothing to keep in sync with a release. The options pane evaluates as a JavaScript expression rather than parsing as JSON, because half of what is worth trying cannot be written in JSON. Six samples, one per question people actually ask, including a configuration the compiler warns about.
- **`llms.txt` and `llms-full.txt` per language**, following the llms.txt convention, and the raw Markdown of every page served at the page's path plus `.md` — so "give this page to a model" is a fetch rather than a scrape of rendered HTML. Three buttons above the outline use it: copy as Markdown, view raw, and open the page in a conversation.
- New `docs/agents.md` / `docs/agents.zh-CN.md` gathers those endpoints in one place, including what belongs in a prompt: an agent that assumes one global design width writes a configuration that compiles and is wrong.
- Deployed by `.github/workflows/docs.yml` on push to `main`. `tsconfig.json` now covers `docs/.vitepress`, so the site's own source is type-checked with everything else.

### Documentation

- New `scripts/check-docs.mjs`, wired into `npm run check` and therefore into CI. It verifies that every local link resolves, that every `#anchor` matches a real heading, and that **every page has its counterpart in the other language and links to it near the top**. The pairing check is the one that earns its keep: the two languages are written separately rather than translated, which is what makes them read properly and also what lets one of them quietly not exist. It found two — `CODE_OF_CONDUCT` had only a Chinese version under an English filename, and the example README had both languages stacked in one file.
- The anchor check splits on `/\r?\n/`. On Windows every file here is CRLF, and a trailing `\r` defeats `$` in a non-multiline pattern while also not being matched by `.` — so a naive split finds no headings at all and reports every anchor in the repository as broken. That is not hypothetical; it is what the first version of this check did.
- `examples/app-pc` gained `adaptive.config.mjs`, exporting the options alone; `postcss.config.mjs` now imports them. The two runners want different shapes — PostCSS wants `{ plugins: [...] }`, the CLI wants the options — and writing the canvases out twice is how the two end up describing different designs. The example READMEs now give a command that actually runs, including `--targets`.

### Performance

- `routingSelector` returns immediately for a selector with no `:` in it, which is most of them, and the reduction now happens inside `forSelector` — after the check that skips selector routing entirely. A project with `libraries: false` no longer pays anything at all for the feature. Compiler time on the utility-framework corpus: 4.38ms → 3.58ms.

### Conformance

- The atomic fixtures gained `space-x-4` and `divide-y-2`, which had left the suite with **no functional pseudo-classes at all** despite being everyday utilities. One utility, three unrelated real shapes: Tailwind 4 wraps the whole thing in `:where(...)`, UnoCSS wind3 writes a flat `> :not([hidden]) ~ :not([hidden])`, and UnoCSS wind4 emits *native nesting* with `&`. All three now produce an identical `clamp(1.70667px, 0.53333vw, 2.56px)` for the same 2px, the `border` hairline survives in all three, and wind4's native nesting is written back unchanged.
- New `test/idempotence.test.ts`: **compiling compiled output changes nothing**, across nine configurations × eight stylesheets. The conformance suite already asserted this per fixture, but only under the options that fixture declares — what was uncovered is the cross product of the switches that change the *shape* of the output, and the shape is what a second pass has to survive. A second pass is not hypothetical: a package that ships pre-compiled CSS goes through the consuming application's pipeline again, and so does a monorepo that compiles a shared component library and then the app importing it.
- The case that wanted pinning down is atomic mode with static text. Atomic mode adds `rem` to `unitToConvert`, and text is normally written as `rem + vw` — the `vw` is what tells a second pass the value is already compiled. With `fontFluidity: 0` there is no `vw`: `32px` becomes a bare `2rem`, which the next pass reads as a design length and converts again. It survives, but nothing defends it — `rootValue` is used at both ends, so writing ÷16 and reading ×16 are exact inverses and the value is its own fixed point. That is a property of the arithmetic rather than a rule anyone wrote down, and if either end of that division moves the failure is silent: no error, no warning, just text a little smaller on every save in a watch loop.

### Types

- Every array-valued option now accepts a `readonly` array: `routes`, `libraries`, `textProperties`, `propList`, `selectorExclude`, `valueExclude`, `include`, `exclude`, `root.injectTo`, and the `file` / `selector` / `property` / `prefix` / `tokenPrefix` fields of routes and library definitions. `unitToConvert` already did, which made the API inconsistent: a configuration written with `as const` — the natural way to write one in TypeScript — type-errored on every field but that one.

## 0.5.0 — 2026-08-10

### Documentation

- **Every documentation file is now bilingual**: `X.md` in English, `X.zh-CN.md` in Chinese, cross-linked at the top of each page. That covers the README, all 11 docs pages, the conformance suite description, the contributing guide, the security policy and this file. The English is not a machine translation of the Chinese — the same point wants different sentences in each language, so the two versions were written separately.
- The English artwork lives in `docs/assets/en/` and was redrawn for English: Chinese labels are short, so substituting the text would overflow the cards, and the dashed plain-vw line would run under `tracks the viewport`.
- The canvas-model diagram no longer names third-party libraries. The model has nothing to do with whose library it is; "a mobile component library / 375 design file" and "a desktop component library / no design file · real pixels" say exactly the same thing, and the diagram is about this compiler from start to finish.

### Browser support audit and degradation

- New `auditCompatibility(css, targets)`: give it "browser + the oldest version you support" and it lists every piece of syntax in the output beyond your targets, what is lost when it is unsupported, and the switch that turns it off. The audit reads the **compiled stylesheet text** rather than the configuration — the only way audit and output can never drift apart, and it sees features that arrive via a preset, via a component-library route or via hand-written CSS alike. Also exported: `detectFeatures`, `COMPAT_FEATURES`, `FEATURE_SUPPORT`, `compatFeature`, fully typed.
- New CLI flag `--targets "safari 14, ios_saf 13"`, printing a `needs` section after the comparison. The order of those lines is deliberate: what is lost first, what to switch to second — "iOS Safari 13 is too old" is not actionable on its own, and what has always mattered about a CSS support gap is **how much goes with it**. CSS does not error, it discards: an unreadable value takes its declaration, an unreadable selector takes its rule, an unreadable at-rule takes its whole block, all silently. So the feature table is ordered by how much is lost, not by which feature is newest.
- Covers 11 features: `@layer`, `:where()`, `@container` / container units, `clamp()` / `min()` / `max()`, `vi`, logical properties, `var()`, `env(safe-area-inset-*)`, `vw`, and native nesting. The compiler does not emit native nesting (nesting it reads is written back as it was); it is listed because it is a question that gets asked, and because pattern-matching it would misread ordinary CSS as nested — a false report is worse than no report.
- Version data is baked into `src/core/compat-data.ts` from caniuse-lite by `scripts/capture-compat.mjs`. caniuse-lite stays a devDependency, the plugin adds no runtime dependency, and the audit works offline. "Which version a feature became usable in" is history and will not change again; **usage share is deliberately not consulted** — whether 0.4% of users count is a decision about your project, and `browserslist` is already the place that answers it. The scan takes the version from which support was never lost again, not the first version showing a `y` (a few features shipped and were then withdrawn).
- `--targets` takes explicit names and versions, not a browserslist query: a query would pull in the browserslist package, answers a question about your users rather than about this stylesheet, and **the same query changes meaning as the database updates** — not one character of code changes and next month the build goes red. An unrecognised target name is an error that exits `1` rather than being skipped silently: a target quietly dropped is worse than no audit, because it reads like a pass.
- caniuse has no separate entries for `:where()` or `vi`, so the `:is()` and `svh`/`lvh`/`dvh` entries are used, marked in the data as proxies rather than measurements. Both pairs come from the same section of the specification and shipped together (`:where()` / `:is()`: Chrome 88, Firefox 78, Safari 14).
- New `root.logical` (preset field `rootLogical`): set to `false`, the foundation writes `width` / `margin-left` / `margin-right` / `max-width` instead of `inline-size` / `margin-inline` / `max-inline-size`. **This is a real gap found while building the audit** — logical properties are the only syntax the compiler emits whose failure still leaves a page that looks fine: without `margin-inline: auto` the column is exactly the right width, sitting against the left edge of the screen; without `max-inline-size` it goes full-bleed. Neither looks like a fault, so it is likelier to ship than a visible collapse, and until now no switch avoided it. The two spellings are equivalent on a horizontal page, so switching costs nothing. The preset also gained `rootLayer`, passed through to `layer` — also a support switch, and reaching it should not cost you the preset.
- New documentation page: [Browser support and degradation](docs/compatibility.md) — the feature × minimum-version matrix, a per-feature "what emits it / what is lost / how to switch it off and what that costs", and the boundary of what this audit **cannot** replace (it only proves your target browsers can parse the syntax in this CSS; rendering differences, keyboards and address bars are different problems). Conversely, a real device cannot test a version threshold either — the iOS 17 in your hand reads `@layer`, which says nothing about 15.4 and below.

### Breakpoint check

- Fixed **two `shrinks` false positives that fired reliably on the default preset with no configuration at all**: `root.fixedContainingBlock` rewrites a fixed element's `left: 0` into `left: var(--adaptive-root-gutter)`, and that gutter is **meant** to step at a breakpoint — as the column goes from 480 to 1920, the gutter correctly drops from 143.96px to 0. Read as a design length that is a regression; read as itself it is the correction working. The variable is now substituted with `0px` before comparison rather than skipping the group entirely, so the `clamp()` in `left: calc(clamp(…) + var(--adaptive-root-gutter))` is still checked. A check that shouts on the default configuration is a check people learn to skip.

### Atomic CSS (Tailwind / UnoCSS)

- Fixed **no utility class being converted at all**, silently: hand-written CSS scaled, utilities did not, and the two size systems drifted apart with no error. Each major version was blocked in a different place. The older ones (Tailwind 3, UnoCSS `presetUno` / `presetWind3`) write lengths in `rem` while the compiler only read `px`; the newer ones (Tailwind 4, UnoCSS `presetWind4`) moved lengths entirely into theme tokens, so `.p-4` compiles to `padding: calc(var(--spacing) * 4)` — there is no length in the utility to read, and custom properties are not converted by default.
- `unitToConvert` now accepts an array, reading several units in one pass. This is not a nicety: both frameworks emit both units in the same stylesheet — spacing and font sizes in `rem`, border widths and bracketed arbitrary values like `p-[13px]` in `px`, all describing the same design file. Reading only `rem` misses every border, reading only `px` misses every gap, and both single choices are wrong. `rem` becomes pixels via `rootValue`; every other unit is read at face value.
- Fixed `unitToConvert: 'rem'` having always treated `1.5rem` as 1.5 pixels: with no conversion before comparison it fell below the `minPixelValue` and `hairline` thresholds and whole stylesheets were skipped. Both thresholds are now judged in **pixels** — `0.0625rem` and `1px` are the same hairline, and how you spell it does not change how thin it is.
- New `rootValue` (default 16), one ruler shared by both ends: it decides how many pixels a `rem` input is worth, and how many `rem` a text size's static part is written as. A project with `html { font-size: 62.5% }` sets `rootValue: 10`.
- New `withAtomicCss(base, options?)`: **wraps** rather than replaces an existing configuration, adding `rem` to `unitToConvert` and claiming the theme token prefixes `--spacing`, `--text-`, `--leading-`, `--radius-`, `--container-`. Claiming the source is enough and the utilities need no changes — `calc(clamp(a, b, c) * 4)` is identically `clamp(4a, 4b, 4c)` (multiplication by a positive coefficient passes through a clamp), and the output is digit-for-digit identical to converting `16px` directly. Three families are deliberately not claimed: `--breakpoint-*` is the width at which a canvas **switches**, so scaling it moves the breakpoint itself; `--tracking-*` is published in `em`, and the font size it hangs off is already fluid, so scaling again compounds it; `--shadow-*` pixels are depth drawn at screen scale. Add your own length families with `tokenPrefixes`.
- The default `textProperties` now includes `--text-*` and `--leading-*`: when a font size is published as a token its name does not look like a font property, and missing it means that font size **loses browser zoom**. This only decides how an already-converting length is written, not whether it converts, so it does nothing for an unclaimed token.
- New conformance cases `atomic/{tailwind-v4,unocss-wind3,unocss-wind4}`, whose input is the **actual published output** of all three (Tailwind 4.3.3 and two UnoCSS 66.7.5 presets), re-capturable with `scripts/capture-atomic.mjs`. The two major-version shapes differ far too much for a hand-written copy to be anything but what you imagined. Neither framework is a devDependency: the captured CSS is the entire input, and tying `npm test` to someone else's release schedule buys no extra information.
- A regular expression passed to a route's `property` channel now throws a `TypeError` immediately, explaining the correct form. The other two channels accept regexes; this one takes string prefixes only, and the previous behaviour was a mid-run `prefix.toLowerCase is not a function`.

**Breaking (types)**: `ResolvedAdaptiveMatrixOptions.unitToConvert` changes from `string` to `string[]`; `AdaptiveMatrixOptions.unitToConvert` widens to `string | readonly string[]`, so passing a string still works. `findContinuityIssues` gained an optional second parameter, `rootFontSize`.

### Compiler

- Fixed cross-canvas text sizing: **when the project canvas and a library canvas differ, all of that library's text was wrong**. Ordinary lengths always agreed (both reduce to `value ÷ canvas`); text did not — text keeps a fixed `rem` component so browser zoom stays effective, and that fixed length used to anchor to each canvas separately. With Vant on 375 and the page on 750 the two describe the same design in two sets of units, so Vant's 16px and the page's 32px are the same size — yet at a 390px viewport they rendered as 16.22px and 26.62px, **about 40% too small**, and invisible at both 375 and 1440. A 750-file project using Vant is one of the most common mobile combinations there is; antd-mobile's 1x/2x pair of artifacts is the same situation.
- New profile field `textAnchorWidth`: the width the static part of text anchors to, defaulting to `designWidth`. A library canvas always inherits the anchor of the profile it belongs to, so there is nothing to configure. The equivalent form is "convert the length into the anchor canvas's units first, then apply the formula as usual": the fluid term is exactly unchanged (`P × F / D` cancels proportionally with the canvas), and only the static term is normalised. For non-text lengths `fontFluidity = 1` and the static term is always 0, so the output is byte-for-byte unchanged, and `strategy: 'viewport'` is unaffected too.
- New property test: "the same design on a different canvas is the same size". Across random canvases and random scale factors, `V px on D` and `V×k px on D×k` must agree at every viewport. The conformance suite can only cover canvas combinations someone thought to write down, and this bug only surfaced when two canvases were present at once.

### Compiler and validation

- `@adaptive <canvas>` landing on a profile with no `query` now warns. Projects split by folder (`src/mobile/**` and `src/pc/**`, one page tree each) usually have no `query` on either profile — switching is not CSS's job there. Writing `@adaptive pc { ... }` in a shared component then reads as "these rules are for desktop", but compiles to unconditional rules that come later in the file and therefore **win at every viewport**, with nothing anywhere saying so. `query: false` does not warn: that is an author stating explicitly that switching happens outside CSS. An `@adaptive` pointing at its own canvas does not warn either: there is no switch, and unwrapping loses nothing.
- New documentation section, "two page trees, split by folder", stating that file routing only decides which design file to convert against and never adds a media query, plus the trade-offs around shared components, component libraries and route specificity.

### Build tool integration

- New real Vite build test: the scaffold has a `postcss.config.mjs` that Vite discovers for itself, a dependency stylesheet imported from `node_modules`, and a `<style>` block supplied the way `@vitejs/plugin-vue` supplies one. Until now nothing verified a single claim in the integration documentation — and when those claims stop holding, the build still succeeds and only the stylesheet is wrong. Now verified: the config is found and applied; a dependency lands on the component-library canvas via its `node_modules` path with output byte-for-byte identical to the equivalent size on the page; an SFC id with a query string is matched by a contains-style `file` route; and a second build produces identical output.
- The documented trap "an end-anchored regex never matches an SFC" is now a test too: building with `/\.mobile\.css$/` asserts that the `<style>` block really is left silently on the default canvas while the rest of the output is unchanged. A claim like that only counts once it has been verified from the other side.

### Optional runtime

- Fixed a frame leak in `observeAdaptiveViewport`: `update()` is a public method, yet it cleared the scheduler's frame handle. Calling `update()` manually and then `destroy()` left an already-queued frame neither recorded nor cancelled, so it landed on a destroyed observer on the next tick. Only the scheduler clears the handle now, `destroy()` zeroes it, and a repeated `destroy()` no longer cancels a handle the host has already recycled and reissued.
- Filled in the runtime tests: the fallback for old WebViews with no `visualViewport`, negative keyboard heights from iOS rubber-band scrolling, per-field non-numeric readings, event coalescing, destroy timing, and reading the globals when called with no arguments — the path every browser user actually takes. Branch coverage 72% → 100%.

### Component libraries

- New `scripts/verify-libraries.ts`: downloads each built-in library's **published artifact**, compiles it with a realistic `node_modules` path, and checks whether the prefix really exists, which canvas the route lands on, whether the result is idempotent, whether there are warnings, and whether the seam check produces a false positive. Until now every registry entry except Vant was written from documentation with no evidence; the three items below were all found by this script.
- Fixed antd-mobile: the library publishes the same stylesheet twice, `bundle/` drawn on 375 and `2x/bundle/` on 750, with identical class and token names (measured on 5.42.3, every length in the latter exactly double the former). The `.adm-` prefix route used to convert the 2x artifact against 375, making every size on the page exactly twice what it should be, with no error and no warning. New `antd-mobile-2x` entry, active in automatic mode with nothing to configure.
- New `scoped`: restricts `prefix` and `tokenPrefix` to only count when `file` matches too. When one prefix maps to two canvases, only the path tells them apart. Path-scoped routes are tested before unscoped ones because they are more specific; when the path does not exist (a bundler inlined the dependency), it falls back to the unscoped one. `scoped` without `file` is an error.
- Removed Varlet's `tokenPrefix: '--var-'`: that library's custom properties carry no prefix at all — they are `--field-padding`, `--icon-size-md`, declared on a bare `:root`, and the rule matched nothing. Claiming those names means claiming `--card-width` itself, and the registry only takes unambiguous prefixes, so nothing was added; the documentation shows the explicit route instead.
- The documentation now carries measured prefix hit rates, and states that **the design width column cannot be checked** (a stylesheet does not reveal how wide the file it was drawn on was), and that `naive-ui` and `mui` generate their styles at runtime with no stylesheet on disk.

### Breakpoint seam check

- Only reports when **at least one side is a compiler-produced formula**. It used to read a library's own deliberate breakpoint difference as a seam: Quasar's `.q-tooltip` is `padding: 8px 16px` on a phone and `6px 10px` above 600px, a tap-target trade-off where both numbers were written and compared by a person. The completeness argument "a regression can only come from a canvas change" covers only formulas this compiler produced, and does not hold for a stylesheet left as authored. One side converted and the other not still reports.
- Conformance cases now use compiler-shaped values. Over a dozen negative cases used a bare `40px`, which after this gate would pass because they were never compiled rather than because of the thing each was meant to verify.

## 0.4.0 — 2026-08-09

### Breakpoint seam check

- New breakpoint seam check: the CLI points out declarations where widening the viewport makes a size smaller, with the actual pixel values on both sides. Two design files can each be right and still disagree at the seam, and this class of problem only appears at one width — the 375 and 1440 you debug at every day are both fine. The check is also exported as `findContinuityIssues(root)` so a build can fail on it.
- The comparison is on **absolute value**, and a sign change across the breakpoint is never reported. The absolute value of every formula the compiler emits is non-decreasing in viewport width and never changes sign, so an absolute-value regression can only come from a canvas change across a breakpoint. Comparing signed numbers was inverted for every negative length: negative margins and bleeds grow by moving away from zero.
- The check substitutes theme tokens from the same stylesheet. Component libraries almost never write literal sizes — of Vant 4.10.0's 3198 ordinary declarations, 1173 read entirely through `var()`, so giving up at the first `var(` means avoiding precisely the layer this plugin adapts. Substitution happens only when the value is decided by viewport width alone: the token must be declared only on `:root` / `:host` / `html` with no second copy elsewhere, and every declaration must be either unconditional or inside a pure pixel-width `@media`. A token rewritten at a breakpoint is itself a seam, even if the rule consuming it is written once. Measured (the complete Vant 4.10.0 stylesheet): evaluable value components 622 → 1309 (17.6% → 36.9%), with 779 tokens collected.
- Measured false positives: across the 69 conformance fixtures, the Vant stylesheet above, and this repository's example project, the `shrinks` count is 0 everywhere.
- New `evaluateLength`: evaluates the compiler's `clamp()` / `min()` / `max()` / `calc()` at a given viewport width. `env()`, `%` and container units return `null` rather than a guessed number.

### Packaging and types

- Fixed the CommonJS entry: `require('postcss-adaptive-matrix')` returned a namespace object, so calling it directly threw `plugin is not a function` and `.default` was mandatory. But `plugins: [require('postcss-adaptive-matrix')({ ... })]` is the universal shape of every `postcss.config.js` — this repository's own Webpack documentation example was wrong. `module.exports` is now the plugin itself, with `.default` and the named exports still available as properties.
- Fixed the CommonJS type entry, in two places: `types` in `exports` existed only at the top level, so CJS consumers got the ESM `.d.ts`; and `.d.cts` itself only declared named exports, so `import x = require('postcss-adaptive-matrix')` reported "has no call signatures" — code that ran while the editor showed red. `require` now points at `.d.cts`, which describes the real shape with `export =`, types are preserved through a merged namespace, and `import type { AdaptiveMatrixOptions }` still works. The ESM entry and its types are unaffected.
- New tests against the built artifact itself: every other test imports from `src`, but what `require` receives and which file types resolve to are decided by the build and by `package.json`, and importing from source can never test that. `npm run check` therefore builds before testing.

### Compiler and validation

- A selector list that straddles canvases (`.van-cell, .page-hero { ... }`) now warns and names the selector that lost. One declaration can only have one answer, and the previous behaviour silently compiled the whole rule on the first canvas that matched.
- `@adaptive pc;` (no block) used to be rewritten as `@media (min-width: 768px);` — not valid CSS, while the rules the author meant to put on that canvas stayed on the original one. It now warns and is left as written.
- The CLI's `-c` no longer falls back silently to the built-in defaults when the config module is missing `default` or the preset was never called. Both used to run to completion and print a comparison that looked correct, with nothing anywhere saying the configuration had not been read.
- New configuration validation: the values of `unit` and `strategy`, an empty `unitToConvert`, an `atRuleName` colliding with an at-rule CSS already defines, and an empty `root.selector`. Getting any of these wrong fails silently, and a wrong `unit` emits outright invalid CSS.
- New property-based random tests: using `evaluateLength` as the oracle, design-width identity, absolute-value monotonicity, constancy outside the range, linearity inside it and idempotence are verified across hundreds of generated canvases. The conformance suite can only cover design widths someone thought to write down.

## 0.3.0

- New `adaptive-matrix` CLI preview: a per-declaration before/after, with `--from` to verify file routing before you ship and `--css` to emit the full output while warnings go to stderr.
- Fixed nesting: declarations inside `@media` / `@supports` / `@layer` / `@container` / `@scope` / `@starting-style` are now converted, while lengths inside `@font-face` / `@page` / `@property` / `@counter-style` are left as written.
- Fixed idempotence: `clamp()` / `min()` / `max()` that already carry a viewport unit are no longer converted a second time.
- Fixed idempotence of the root foundation: the output carries a `/* postcss-adaptive-matrix foundation */` marker, so recompiling neither scales a fixed ceiling like `max-inline-size: 480px` as if it were a design-file size, nor stacks on a second copy.
- Ignore comments (the `adaptive-ignore` family) are no longer removed from the output. An ignored value carries no trace of itself, so once the comment is gone a second pass converts the size the author explicitly excluded; minifiers remove the comments, so shipped size is unaffected.
- The conformance suite gained an idempotence assertion for every case: recompiling the output must return it unchanged.
- Fixed duplicate declarations: every repetition of a declaration within one rule is now converted. Only the first used to be, while the cascade uses the last — so the conversion was effectively lost.
- Numbers with exponents are supported: `1e2px` is 100px, which used to be skipped silently, and `min(1e2vw, 50px)` is no longer misjudged as having no viewport unit.
- Fixed at-rule casing: `@ADAPTIVE` / `@Adaptive` are equivalent to `@adaptive`, matching CSS's case-insensitivity for at-keywords. They used to go unrecognised, and the browser discarded the whole block with no indication.
- New `root.injectTo` (`rootInjectTo` on the preset): limits which files receive the root foundation. In a component-based project every `<style>` block is a separate file, so the default injects one copy into each.
- An exclude-only `propList` (such as `['!border*']`) is now an error — it matches no property, which means the entire stylesheet goes unconverted.
- Using the reserved `library:` prefix in `profiles` is now an error pointing back at `libraries: [{ extends }]` — it used to be silently overwritten by the synthesised library canvas.
- The unknown-canvas warning no longer lists internally synthesised library canvases, and explains that the browser discards the whole at-rule; under `unknownProfile: 'error'` the error no longer suggests enabling an option that is already enabled.
- New registry invariant tests, covering prefix ambiguity, canvas values and the short-prefix policy in automatic mode.
- Restructured the documentation, adding getting started, build tool integration, component libraries, optional runtime and CLI preview, with SVG diagrams.

## 0.2.0

- Added component-library canvases, automatic recognition, route overrides and design-token adaptation.
- Added the fixed root containing block correction and desktop offset handling.
- Decoupled the compilation core from the PostCSS adapter layer, exposing the resolved multi-canvas configuration.
- Established 50+ conformance fixtures, 133 tests and a benchmarking tool.
- Rewrote the multi-canvas model, component library, architecture and migration documentation.

## 0.1.0

- First implementation of the app/desktop multi-canvas compilation model.
- Bounded fluid sizing, zoomable text, and media-query and container-query profiles.
- Dynamic design widths, file/property/selector/value filters and ignore directives.
- Optional root layout, safe-area variables and the VisualViewport runtime.
- Published ESM, CommonJS and TypeScript types.
