**Minor release.** Strict-by-default install hygiene, full pre-flight validation, token-clobber detection, plus five new install flags (`--no-strict`, `--force`, `--ignore-failed-flags`, `--diff`, `--rollback`) for explicit developer control over the install lifecycle. Public CSS API catalog (`/extending/public-css-api`) + content-edge spine contract (`/extending/spine-contract`) ship as the canonical AI-tooling discovery surfaces for `wk-*` classes and layout participation. New `<x-wirekit::spine-aware>` helper wrapper + `WireKit::spinePadding()` static let developer-authored components opt into the page-edge spine with one line. ALSO ships marketing-primitive composition ergonomics: brand-bar gains `container` + `max` props, hero/cta gain responsive `size` props with mobile-tightened vertical padding, hero's gradient overlay is contained to the content wrapper (no more empty-dark-area class of bug on mobile), footer gains a `max` prop replacing the prior hardcoded `xl` inner width. A new canonical recipe at `/recipes/marketing-landing-page` shows the full composition, and a new explainer at `/extending/composition-patterns` documents the chrome-vs-content axis as a design contract. Command-surface consistency: every "Unknown X" error now emits a uniform Levenshtein-ranked Did-you-mean hint, `wirekit:theme default` succeeds (and removes any existing preset block) instead of returning FAILURE, `wirekit:list --category=Marketing` lists the four canonical conversion primitives (cta, feature, feature-grid, hero), `wirekit:component my-button` derives `--base=button` automatically, `wirekit:doctor` becomes a Symfony alias of `wirekit:verify` (both names resolve to the SAME command instance), and `<x-wirekit::cta>` / `<x-wirekit::hero>` runtime prop validation reuses the same suggestion contract. **Two behavior changes** flagged below: `wirekit:install` now aborts (exit 2) on invalid flag values or token-clobber warnings, where v2.0.0 printed a warning and continued with exit 0; AND the `cta` / `feature` / `feature-grid` / `hero` components migrated from category `Display` to category `Marketing` — developers reading `wirekit:export-json` see the changed category strings. Developers who depended on the legacy "warn-and-proceed" semantics combine `--no-strict --ignore-failed-flags` for byte-stable v2.0.0 behavior. Every other change is additive.

### Added

- **Comprehensive mobile-viewport browser-test coverage across every category where viewport behavior matters.** Builds on the marketing-primitive mobile coverage with Tier-1 PR-gate `Tier1MobileGateTest.php` (6 cross-category representatives — hero, modal, sidebar, input, table, reading-spine) plus Tier-2 on-demand sweeps for **Overlays** (modal / drawer / popover / tooltip / dropdown / alert-dialog / command-palette / hover-card / context-menu), **Navigation** (navbar / sidebar / tabs / breadcrumb / stepper / pagination / menubar / navigation-menu / brand-bar + reading-toc hideBelow breakpoint test), **Forms** (input / select / combobox / textarea / checkbox / toggle / date-picker / time-picker / segmented-control / slider / file-upload / field — with WCAG 2.5.5 AAA 44×44 touch-target audit), **Reading family** (progress / spine / toc / minimap / bookmark / meta / shell), **Data display** (table / kanban / data-list / timeline / tree-view / stat / stats / sparkline / pagination / progress / ticker / price / calendar — with horizontal-overflow guard), AND a generic `ComponentPreviewMobileSweepTest` that scans every `docs/components/**` `:::preview` block at iPhone 14 Pro viewport. New shared `AssertsMobileViewport` trait centralizes the `matchMedia` override + console-error capture + scroll-settle scaffold so per-test boilerplate stays minimal.
- **`docs/extending/public-css-api.md` — canonical Public CSS API catalog.** Single source of truth for every `wk-*` class WireKit emits to developers — via `dist/wirekit.css` literal selectors AND via Blade-template static-string emissions. ~40 catalogd classes across four groups (Layout / chrome markers, Reading family, Animation / motion, Display / loading), each with a stability tier (Stable / Provisional / Internal-with-exception) and customization contract. The catalog enforces lockstep both directions — every shipped class has a row, every row references a real shipped class. Surfaces as machine-readable JSON via `wirekit:export-api-map`'s new `css-classes` group for AI tooling / IDE-extension consumption.
- **`docs/extending/spine-contract.md` — content-edge spine participation contract.** Documents the per-component inline-padding spine — which components join `--padding-wk-x-lg` (brand-bar, main, container, header, footer, cta, hero outer-only, spine-aware) and why others (sidebar, reading-toc, section's non-lg tiers) deliberately don't. Hero's "outer-yes / inner-no" asymmetry is now explicit; developers no longer measure the discrepancy by hand. Every documented participant carries a `// wirekit:spine-participant` marker comment in source so adding a new spine-aware component without the marker fails the build upstream.
- **`<x-wirekit::spine-aware>` — NEW opt-in spine wrapper for developer-authored components.** Pass any slot content; the outer `<div>` reads `--padding-wk-x-lg` so the content aligns with brand-bar / main / footer / etc. on the same vertical content edge. The `tier` prop (`sm` / `md` / `lg` / `xl`) reads a different `--padding-wk-x-*` tier when the developer wants to deliberately step off the spine. Helper-driven (uses the new `WireKit::spinePadding()` static) so a future spine-padding refactor propagates automatically.
- **`Pushery\WireKit\WireKit::spinePadding(string $tier = 'lg'): string` — NEW public helper.** Returns the canonical Tailwind utility string for the named padding tier (`px-[var(--padding-wk-x-{tier})]`). Use directly in developer-authored Blade class strings to opt INTO spine participation with one helper call — protects against tier typos AND gives the drift-audit guard a single canonical detection target. Tier values mirror the `--padding-wk-x-*` token family.
- **`wirekit:doctor --tier={package|environment}` filter.** Run only the package-tier checks (asset / config / directive / Tailwind-source / token-alignment / Alpine-cleanup) OR only the environment-tier checks (currently just compiled-view freshness). Default behavior unchanged — running without `--tier` walks every check. Useful for interactive dev sessions where the developer cares about only one tier's signal.
- **`wirekit:doctor` compiled-views-freshness check.** Detects when `resources/views/` mtimes exceed `storage/framework/views/` by ≥60 seconds (the canonical "your test asserts a new prop but the assertion fails" failure mode caused by Laravel's view-cache lag). Emits a WARN with the actionable `php artisan view:clear` fix, short-circuits the diagnostic chain to the real cause. Honors the 60-second buffer to avoid false-positives on fast file-edit cycles. Categorized under the new `environment` tier.
- **`wirekit:export-api-map` new `css-classes` group.** Machine-readable enumeration of every public `wk-*` class — same inventory as `docs/extending/public-css-api.md` table, one entry per class with `tier` + `docs_url`. AI tooling consumes via `php artisan wirekit:export-api-map --pretty | jq '.groups[] | select(.id == "css-classes")'` for autocomplete / type-checker awareness.
- **`--wk-stagger-step` token default in `:root {}`.** The CSS variable now ships with a `75ms` default in `dist/wirekit.css` (previously developers needed to set it via the prop OR risk a silent `0ms` cascade). Documented in `docs/theming.md` under the new "Motion tokens" subsection alongside the existing `--motion-wk-*` token family.
- **`Marketing` category in `Pushery\WireKit\ComponentRegistry`.** Four conversion-focused primitives (`cta`, `feature`, `feature-grid`, `hero`) migrate from category `Display` to category `Marketing`. `footer` stays `Layout` (structural primitive), `brand-bar` stays `Navigation` (header chrome), `reveal` stays `Display` (generic animation). `wirekit:list --category=Marketing` now exits 0 and prints the four primitives; the canonical category enum becomes eight values (`Form`, `Layout`, `Typography`, `Navigation`, `Overlay`, `Display`, `Marketing`, `System`). See [`/extending/component-registry#categories`](https://docs.wirekit.app/extending/component-registry#categories) for the per-category definition + usage criteria.
- **`Pushery\WireKit\Theming\ThemePresetRegistry`** — public single source of truth for every theme preset. `keys()` / `get(string $key)` / `all()` / `isValid(string $key)` / `isDefault(string $key)` cover read access; `register(string $key, array $preset)` lets downstream packages add custom presets at runtime from a service-provider boot hook. `wirekit:theme`, `wirekit:install --preset=`, and `wirekit:export-api-map themesGroup()` all read from this class — drift between their lists is impossible.
- **`Pushery\WireKit\Support\SuggestSimilar`** — public Levenshtein helper underpinning every CLI "Did you mean?" hint AND the runtime `WireKit::validateProp()` suggestion line. `byLevenshtein(string $needle, array $haystack, int $max = 3, int $maxDistance = 3)` returns up to N ranked candidates; `byLevenshteinScored(...)` exposes the distance scores for ranked-weighting developers (MCP servers, recently-used-bias suggestions); `format(array $suggestions)` pretty-prints the result. Cross-cutting use across `wirekit:show`, `wirekit:theme`, `wirekit:list`, `wirekit:publish-icons`, `wirekit:component`, `wirekit:install`, plus `WireKit::validateProp()`.
- **`wirekit:list --category=Marketing,Display` multi-category filter.** Comma-separated list returns the union of the per-category sets. Useful for "show me everything that could go on a landing page" queries.
- **`wirekit:list --format=...` alias for `--as=...`.** Matches the `--format=json` convention used by other Laravel commands. Both flags accept the same value set (`count|slugs|categories|json`) and resolve to the same code path; passing both simultaneously with different values is rejected.
- **`wirekit:component --interactive` flag.** Forces the `--base` chooser prompt even when TTY detection misfires (Herd / Docker / WSL). Pairs with the new derivation chain so `wirekit:component customer-dashboard` walks: right-segment strip → Levenshtein suggestion → interactive choice() prompt → fail-fast with actionable error when no candidate matches.
- **`<x-wirekit::brand-bar>` `container` + `max` props.** When `container=true`, the brand-bar's CHROME (background, border, sticky behavior) stays edge-to-edge while CONTENT (brand, tagline, actions) aligns with the body's container-wrapped column via the inner max-width wrapper. `max` accepts the six-value container enum (`sm/md/lg/xl/2xl/full`), default `xl`. Reads `--size-wk-container-*` tokens so brand-bar + body align on the same vertical content-edge spine. Default `container=false` preserves the v2.0.0 edge-to-edge render.
- **`<x-wirekit::navbar>` `container` + `max` props.** Same shape as brand-bar's pair — when `container=true`, the navbar's outer `<nav>` (background, border, sticky behavior) stays viewport-edge-to-viewport-edge while the inner flex row (brand, nav items, actions) AND the mobile-menu panel wrap to `max-w-[var(--size-wk-container-{tier})]` + `mx-auto`. `max` accepts `sm/md/lg/xl/2xl/full`, defaults to `xl`. Lets a marketing-landing-page navbar align its brand + nav items on the same content-edge spine as the body below without losing the edge-to-edge chrome. Default `container=false` preserves the v2.0.0 full-bleed content render.
- **`<x-wirekit::hero>` `size` prop + responsive vertical padding.** `size="sm|md|lg"` (default `lg`) names the `sm+` tier of `--space-wk-section-*`; the mobile viewport (< sm breakpoint) automatically drops one tier so the section never overshoots a small viewport. Closes the historical "14 rem vertical padding on every viewport" footgun without breaking the default `lg` semantic on tablet and up.
- **`<x-wirekit::hero>` gradient overlay containment.** When `gradient` is set, the overlay is now anchored INSIDE the content wrapper (max-w-xl) instead of as a direct child of the `<section>`. The previous shape filled the entire section including the vertical-padding band, producing a visible-empty-dark-area below content on mobile under `variant="dark"` + `gradient`. The overlay carries `pointer-events-none` so it never intercepts clicks.
- **`<x-wirekit::cta>` `size` prop.** Same shape as hero's `size` — three-tier responsive vertical padding (default `md`). Mobile drops one tier automatically.
- **`<x-wirekit::footer>` `max` prop.** Replaces the prior hardcoded `xl` inner-content max-width. Same six-value enum as brand-bar / container. All three inner wrappers (columns grid, brand+legal row, default slot) read from a single shared value — set `max` once and every wrapper updates. Default `xl`. Hardcoded fallback values in `var(--token, fallback)` pairs are removed (per user direction "kein hardcoded") — the tokens ARE the source of truth.
- **Two new documentation pages** under the "Extending" + "Recipes" sidebar groups: [`Composition Patterns — Chrome vs. Content`](https://docs.wirekit.app/extending/composition-patterns) (canonical design contract explainer for the chrome-vs-content composition axis across every page-edge primitive) and [`Marketing Landing Page`](https://docs.wirekit.app/blueprints/recipes/marketing-landing-page) (copy-paste-ready worked example composing brand-bar + hero + feature-grid + stats + cta + footer with full visual alignment on the same content-edge spine).
- **`wirekit:install --diff` dry-run mode.** Reports what WOULD change in `resources/css/app.css`, the layout file, `config/wirekit.php`, and `public/vendor/wirekit/` without writing any files. Read-only preview. Exits 0 after rendering. Use to preview an install before committing — pairs with code-review workflows.
- **`wirekit:install --rollback` mode.** Reverses the most-recent install session by replaying recorded before-snapshots from `.wirekit-install.log` at the developer project root. Per-file restore. Returns 0 on full success, 1 on partial restore. Mutually exclusive with every other install flag.
- **`wirekit:install --no-strict` opt-out flag.** Pre-flight warnings print but do not abort. Use only for legacy CI scripts that depended on the v2.0.0 warnings-as-success semantic. Mutually exclusive with `--force`.
- **`wirekit:install --force` flag.** Bypasses pre-flight warnings (token clobber, hand-edited marker blocks). Errors still abort. Use only when you understand the consequences. Mutually exclusive with `--no-strict`.
- **`wirekit:install --ignore-failed-flags` flag.** Per-flag failures report but do not abort the install. Other flags still apply. Exit code reflects partial failure (non-zero so CI still detects it). Requires `--no-strict` (since strict-default would abort first).
- **`.wirekit-install.log` written by `wirekit:install` on success.** Append-only JSON-Lines audit trail at the developer project root. Each tracked side-effect records its file path + before-snapshot. Enables `--rollback`. Failed installs discard pending entries (don't pollute the log with partial state).
- **Token-clobber scan** during pre-flight validation. When a `--font*` flag is set AND `resources/css/app.css` already declares the matching `--font-wk-{category}` token OUTSIDE the WireKit marker block, the install aborts (or warns under `--no-strict`/`--force`) with an actionable error message naming the conflicting declaration's line + value. Closes a long-standing silent-overwrite footgun where developer-set tokens lost cleanly to WireKit's marker block on cascade order.
- **Pre-flight validation pass** in `wirekit:install`. Every flag is validated BEFORE any side-effect runs. Errors are aggregated in one pass — the user sees the full picture in one round-trip, not one error at a time. Filesystem is byte-stable across a failed validation.
- **CI / Deploy script discipline** section in `docs/integration.md` — canonical fail-fast install shape, exit-code reference, GitHub Actions example, legacy opt-out recipe, dry-run preview pattern, rollback workflow.
- **`wirekit:doctor` cleanup-hygiene check for custom Alpine plugins.** Static-analysis scan of the developer's `resources/js/` tree that flags two anti-patterns: an `IntersectionObserver` / `MutationObserver` / `ResizeObserver` instantiation without a `destroy()` cleanup hook, and a `disconnect()` call inside an observer callback without a preceding null-guard against post-teardown fire. Heuristic-based — emits a soft warning with a docs cross-link and respects a `// wirekit-doctor: cleanup-ok` opt-out comment for intentional patterns the heuristic doesn't recognize.
- **Three new documentation pages** under the "Extending" sidebar group: [`Authoring Custom Alpine Plugins`](https://docs.wirekit.app/extending/authoring-custom-alpine-plugins) (defensive-cleanup pattern + worked example for developer-authored Alpine plugins), [`Pest Browser-Test Setup`](https://docs.wirekit.app/extending/pest-browser-setup) (recipe for wiring `pestphp/pest-plugin-browser` into a WireKit-using Laravel app, including known-issue workarounds), and [`Console Output Baseline`](https://docs.wirekit.app/extending/console-output-baseline) (catalog of what a clean WireKit install should emit on first paint, during interaction, and from optional-dependency warnings — helps developers triage unexpected output).
- **`Pushery\WireKit\Support\BladeParser`** — companion to `PropsParser` for the broader Blade-content surface (named slots, directive enumeration, comment extraction, component-reference discovery). Replaces the inline slot-detection regex previously embedded in `ExportJsonCommand`.
- **`wirekit:list --as=count|json|slugs|categories`** flag emits machine-readable output for script + AI consumption. `--as=count` produces a single integer with no decoration (`WK_COUNT=$(php artisan wirekit:list --as=count)`), `--as=slugs` newline-separated component names, `--as=categories` a JSON object mapping each category to its component count, `--as=json` the full per-component array with name + tag + category + description. Default human-readable output unchanged.
- **`wirekit:show <name> --as=json`** flag emits the per-component structured schema as JSON to stdout (no decoration). Includes `name`, `tag`, `category`, `description`, `docs_url`, `props[]` (full prop records with `default`, `default_normalized`, `comment`), and `sub_components[]`. Tooling that needs one component's schema without loading the full export-json output.
- **`wirekit:show <name> --validate-against=<path>`** flag lints a developer Blade file against the component's known prop set. Walks every `<x-wirekit::name ...>` usage, warns on unknown attributes with the closest matching prop name (Levenshtein-ranked) so typos like `intnet` → `intent` surface immediately. Pre-runtime catch of "did I typo a prop?". Exits 1 on any unknown attribute — wire into pre-commit hooks for prop-spelling enforcement.
- **`.wirekit-schema.json` written by `wirekit:install`.** Drop-in IDE-extension + AI-tool feeder file at the developer's project root. Contains the same JSON manifest as `wirekit:export-json --pretty` (every component's name + tag + category + description + full prop records + slots). Re-running `wirekit:install` regenerates the file — checked into git for stable autocomplete across teammates, or gitignored to keep installs fresh. Non-fatal on write-failure (read-only filesystem etc.).
- **Two new documentation pages** under the "Extending" sidebar group: [`ComponentRegistry — Programmatic Discovery`](https://docs.wirekit.app/extending/component-registry) (canonical PHP-API for reading the component catalog at runtime) and [`Authoring Custom Blade Components`](https://docs.wirekit.app/extending/authoring-custom-components) (eight tested `@props` syntax shapes WireKit's tooling understands, plus CLI tooling for custom components).

### Changed

- **`wirekit:install` is now strict-by-default.** Invalid font keys, category mismatches, unknown preset names, invalid `--apex-license` tiers, AND detected token-clobbers all abort with exit 2 (INVALID) before any filesystem mutation. **v2.0.0 BEHAVIOR CHANGE.** Previously these conditions printed a warning to stderr and the install continued with exit 0 — a silent failure mode that masked install bugs from CI. To preserve the legacy semantic, combine `--no-strict` (lets pre-flight warnings pass) + `--ignore-failed-flags` (lets per-flag exceptions pass).
- **`wirekit:install` exit codes are now precise:** `0` = success, `1` = runtime failure mid-install (verify failed, theme application failed, partial rollback), `2` = pre-flight validation rejected the input (no filesystem mutations happened). Wire these into CI as documented in [docs/integration.md "CI / Deploy script discipline"](https://docs.wirekit.app/getting-started/integration#ci--deploy-script-discipline).
- **Four components recategorised from `Display` to `Marketing`.** `cta`, `feature`, `feature-grid`, and `hero` carry their conversion-funnel intent in the category field now. **API-VISIBLE for developers reading `wirekit:export-json` / `wirekit:list --as=json` / `Pushery\WireKit\ComponentRegistry::all()`** — the four entries' `category` field changed value. Rendering, props, slots, and tags unchanged; no other API surface affected.
- **`wirekit:doctor` is now a Symfony alias of `wirekit:verify`.** Both names resolve to the SAME command instance — running either walks the identical check pipeline. `php artisan list wirekit` still shows two rows (the alias appears with an `[wirekit:doctor]` annotation on the verify row), but the standalone `DoctorCommand` class file is removed and both invocations share one underlying handler. **Internal API change** for developers who imported `Pushery\WireKit\Console\DoctorCommand` directly in a custom service provider's command-registration array — switch to `VerifyInstallationCommand` (which now declares `wirekit:doctor` via `setAliases()`).
- **`Pushery\WireKit\ComponentRegistry::extractProps()` return shape.** Was `array<string, string>` (flat name → default-string map). Now returns the structured `list<array{name, default, default_normalized, type_hint, comment}>` shape produced by `PropsParser`. **BREAKING for developers of the prior shape.** Migration: iterate the returned list, read each entry's `name` / `default` keys, and the inline-comment metadata (previously hidden) is now available via the `comment` field. No back-compat shim — the structured shape is the canonical surface going forward.

### Fixed

- **Silent exit-code lie in `wirekit:install --font=<invalid-key>`.** Previously the catch-block printed `✗ Unknown font key '...'` to stderr but did NOT propagate a non-zero exit code; the install continued through publishing / theming / verification and returned `self::SUCCESS`. CI scripts wrapped in `set -e` saw exit 0 and continued. Now pre-flight aborts immediately with exit 2 and the actionable error message.
- **`wirekit:theme default` returned FAILURE.** The `default` preset wasn't in `ThemeCommand`'s inline PRESETS array, so running `wirekit:theme default` exited 1 with "Unknown preset" — despite `wirekit:install --preset=default` being a valid no-op. Symmetry restored: `wirekit:theme default` now exits 0, actively removes any existing `wirekit:theme start/end` block from `app.css` (returning to the bundled token values), and emits an actionable status line either way.
- **`wirekit:show buttn` (and every other "Unknown X" error) had unreliable suggestions.** The historical `str_contains` bidirectional match in `ShowComponentCommand` produced suggestions only when the typo's characters were a substring of a real name (or vice versa). Single-char typos like `buttn` (distance 1 from `button`) never matched. The new `Pushery\WireKit\Support\SuggestSimilar::byLevenshtein` covers every "Unknown X" report uniformly across `wirekit:show`, `wirekit:theme`, `wirekit:list`, `wirekit:publish-icons`, `wirekit:component`, AND `WireKit::validateProp()` runtime messages.
- **`wirekit:component <name>` defaulted `--base` to `<name>` itself.** When `--base` wasn't passed, the default pointed at the new component's own (non-existent) source — every invocation without explicit `--base` failed with "Unknown base component". The new derivation chain (right-segment strip → Levenshtein fallback → interactive prompt → fail-fast with actionable error) handles the common `my-X → X` pattern automatically (e.g. `wirekit:component my-button` derives `--base=button` and copies `button.blade.php`).
- **Two regex-based `@props([...])` parsers consolidated into a single tokenizer-backed `Pushery\WireKit\Support\PropsParser`.** The historical parsers (`ComponentRegistry::extractProps` and `Console\ExportJsonCommand::extractProps`) silently truncated prop defaults that contained commas inside function-call argument lists (`config('x.y', null)` was split into two phantom props) AND leaked trailing `// comment` blocks into the captured default value. Both bug classes broke `wirekit:show` output AND the `wirekit:export-json` manifest — IDE tooling and MCP servers reading the export got incomplete prop catalogs. The replacement uses PHP's own `token_get_all()` tokenizer and handles every legitimate `@props` shape: `config(...)` defaults, multi-line array literals, `match(...)` expressions, mixed-quote keys, heredoc / nowdoc values. New regex-based `@props` parsers in `src/` are blocked from being added going forward.
- **Unguarded observer callbacks in `wirekitStatAnimate` and `wirekitAnimate` Alpine plugins.** Browser-queued `IntersectionObserver` callbacks could fire AFTER Alpine teardown set `this._observer = null` (Livewire morph removing the host element pre-intersection is the canonical trigger), causing `TypeError: Cannot read properties of null (reading 'disconnect')` to log to the console. The page rendered correctly, but the silent error reded every developer's `assertNoSmoke()` / `assertNoJavascriptErrors()` browser-test against any WireKit page using `<x-wirekit::stat animate>` or `<x-wirekit::reveal>`. The fix adds a null-guard immediately before the in-callback `disconnect()` call in both plugins plus the `reading-minimap` initialization `IntersectionObserver` that shared the bug shape. Sweep audit of the remaining observer instantiations across `reading-spine`, `reading-toc`, `reading-minimap` (4 sites), `chart`, and `chart-apex` confirmed they don't access the observer reference inside the callback, so no race exists.