---
title: "v1.7.0"
description: "A Systhema site becomes multilingual end to end, alongside an AI assistant in the Payload admin, analytics widgets, a Cloudflare integration, `systhema setup` and Systhema Design."
requested_language: cs
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/cs/changelog/v1.7.0
version: changelog
docs_index: https://docs.systhema.app/cs/llms.txt
---
> This page isn't translated yet. Showing English.


## ✨ Highlights

This release makes a Systhema site **multilingual end to end**. Three pieces carry it, and each is an independent opt-in:

1. **Frontend locales** declared once in `systhema.config.ts` through `defineSysthemaLocales({ supported, default, … })`, routed by path prefix or one domain per locale, with no generated glue file.
2. **Payload content localization** driven from that same object, plus reviewed Admin and frontend message catalogs for `en`, `hu`, `fr`, `nl`, `cs`, `sk` and `ar`.
3. **Per-locale publishing**, on by default wherever locales are configured, so the publish button offers "Publish in \<locale\>" and each locale carries its own draft or published state.

Alongside the multilingual work: a native **AI assistant** in the Payload admin that now knows the project's own blocks, colour system and content; **first-party analytics dashboard widgets** for GA4, Plausible, Umami, Matomo, Fathom or an adapter you write; a **Cloudflare integration** covering edge purging, zone settings and four dashboard widgets; **`systhema setup`**, which reconfigures an existing project the way `systhema create` configures a new one; **reviewable upgrade plans**, with database-writing hooks behind an explicit `--allow-database`; a **non-interactive PostgreSQL schema migration** for push-only production databases; **Systhema Design**, a design-system generator that runs as a web app and as a CLI command group on one shared engine; **production CSS optimization** with variable and class obfuscation; **composed colours and motion tokens** from Figma; and a **WCAG 2.2 accessibility pass** across every component and page template.

The change that reshapes an existing project most is **per-locale publishing**: `_status` moves out of a shared column into the `_locales` sibling tables of `pages`, `posts` and `components`. `systhema upgrade` runs that migration for you. A bare package update does not, and on a dev push it would come back as a dropped and re-added column with every page a draft in every locale, so upgrade through the CLI (see "Breaking & Behavioral Changes" §2).

### Native multilingual support (#116)

Frontend locales and Payload Admin translations are two separate opt-ins that meet in one config object. Declare the frontend contract once:

```ts
// systhema.config.ts
import { defineSysthemaLocales } from '@systhemaui/core'

export const locales = defineSysthemaLocales({
  supported: ['en', 'hu'],
  default: 'en',
  routing: { localePrefix: 'as-needed' },
})
```

A Payload project passes the same object into `SysthemaPayloadPluginOptions.locales` to drive content localization; a value that diverges from the config is refused rather than silently reconciled. Routing supports path prefixes (`localePrefix: 'always' | 'as-needed'`), one domain per locale via `routing.domains`, custom `subfolder` segments, per-locale `rtl`, and a per-locale `missing` policy of `'fallback'` (the default), `'redirect'` or `'notFound'`.

Enabling locales generates no glue file. `withSysthema(nextConfig)` from `@systhemaui/next/config` injects the routing contract at build time, `@systhemaui/next/gateway` builds the middleware from it, and `SysthemaPage`, static params and metadata resolve the locale from the route internally. The one shape change is the site shell, which becomes `app/(site)/shell.tsx` with two managed boundary files under `app/(site)/(systhema)/[[...segments]]/`. A project with no `locales` config keeps its existing route tree, and the locale-off starter build was verified byte-identical.

Systhema ships reviewed Admin catalogs and frontend message catalogs for seven languages, and a project can register its own Admin catalogs:

```ts
import { definePayloadAdminTranslations } from '@systhemaui/payload'

export const translations = definePayloadAdminTranslations({
  namespace: 'acme',
  baseLanguage: 'en',
  catalogs: { en: { greeting: 'Hello' }, hu: { greeting: 'Szia' } },
})
```

Read them in a custom admin component with `usePayloadAdminTranslations` from `@systhemaui/payload/translations/client`.

CLI entry points are `systhema create --locales en,hu --default-locale en` and `systhema setup locales`. Both write only `supported` and `default` into `systhema.config.ts` and then run the one-time locale migration through `systhema-core sync --migrate-locales`.

> Switching an **existing** site with stored content to locales changes the stored shape of every field that becomes `localized: true`. Nothing is reshaped silently. Follow the explicit, resumable SQLite and PostgreSQL helpers in [`docs/payload/localization/migrating-existing-site.md`](../payload/localization/migrating-existing-site.md); disabling locales reverses an untouched migration byte for byte.

Full reference: [`docs/nextjs/index.md`](../nextjs/index.md) and [`docs/payload/localization/admin-translations.md`](../payload/localization/admin-translations.md).

### Per-locale publishing (#171)

On any project with `locales`, `_status` becomes an ordinary localized field. The publish button offers "Publish in \<locale\>" and "Publish all locales", and each locale's state is its own, so a Hungarian draft can sit behind a published English page. Systhema sets both halves Payload needs (`experimental.localizeStatus` and `versions.drafts.localizeStatus` on Pages, Posts and Components).

Revalidation follows the new scope. `utils/publishScope.ts` reads Payload's `publishAllLocales` and `unpublishAllLocales` request intent, so a single-locale publish revalidates that locale's paths only, while genuinely shared fields (`parent`, `pageTemplate`, a post's `author`) still fan out to every locale. Globals stay on shared status deliberately.

It is on by default and opts out with:

```ts
withSysthema(baseConfig, {
  localization: { localizeStatus: false },
})
```

The database move is automated. `systhema upgrade` runs the `migrate-localize-status` post-upgrade hook, which boots Payload with `PAYLOAD_MIGRATING=true`, performs the whole transition in a transaction, derives each locale's state from version history, and writes the matching Payload migration file when the project keeps a migration directory. It is idempotent: a rerun reports "already migrated" and exits 0. A new slow `localize-status-schema` doctor check compares the config's expectation against the live database in both directions, and the package logs one error line at boot when the option is on and the database still holds a shared status.

> Payload still marks `localizeStatus` experimental, with three open upstream bugs named in [`docs/payload/localization/per-locale-publishing.md`](../payload/localization/per-locale-publishing.md). Read that page before turning it on for a large multi-locale site.

The same PR gives all 26 blocks a 480×320 wireframe thumbnail sized for Payload's 3:2 picker box instead of a stretched 20px glyph, and gives `systhemaPayloadPlugin` a `slug: 'systhema'` so another plugin can read `plugins.systhema?.options` off `config.plugins`.

### AI assistant in the Payload admin (#98)

`@systhemaui/payload` ships a native, Payload-styled AI assistant: field-level text generation, rich-text editing with layout generation, whole-page generation and translation, image generation and refinement, one-click SEO metadata, vision-derived alt text (single and bulk), and a hidden usage log with subscription quotas and a pace-based forecast.

```ts
withSysthema(baseConfig, {
  ai: {
    packageName: 'Acme AI', // optional plan label shown to editors
    text: { provider: 'anthropic', apiKey: process.env.ANTHROPIC_API_KEY },
    image: { provider: 'openai', apiKey: process.env.OPENAI_API_KEY },
    quota: { credits: 5000, period: { interval: 'month', startDay: 1 } },
  },
})
```

Providers are `openai`, `anthropic`, `google`, `groq` and `openai-compatible` for text (Ollama, vLLM, any OpenAI-compatible endpoint, keyless allowed) and `openai` or `google` for images. Pass a `models[]` array to offer several selectable models; editors pick one per generation from an always-visible dropdown, and `locked` entries render as disabled upgrade teasers. Everything is gated by a single `ai.generate` capability, and usage reads by `ai.usage.read`.

The package reads **no environment variables**. Credentials travel through the plugin option from the consumer's environment, the same pattern captcha keys use, and a misconfiguration disables the feature with a console warning instead of crashing. The Vercel AI SDK is vendored into the package at build time, so consumers install nothing and `@opentelemetry/api` never enters their dependency graph.

Full reference: [`docs/payload/ai/index.md`](../payload/ai/index.md).

### The AI assistant learns the project's own vocabulary (#170)

The assistant knew five design blocks and nothing about the project it was running in. Its layout vocabulary is now built per request from what the editor actually registers and from the project's own data.

`section`, `feature`, `columns` and `posts` items take a `background` and a `theme` constrained to the project's own `layoutBg` and colour-system keys, and the prompt teaches the band-merge rule (adjacent sections sharing a theme and background merge into one band). Five native kinds join the vocabulary, `feature`, `image`, `form`, `component` and `posts`, each gated on the editor registering that block and, for the kinds that point at a document, on a non-empty catalogue.

Short catalogues of published pages, forms, reusable components and image uploads are read with the requesting user's access (`overrideAccess: false` plus `req.user`) and capped. Published pages are the only internal link targets, hydrated as document references; an internal-looking path that matches nothing is dropped while its words stay as text. An inline `chip` node is available for tag rows, composed content reserves `h1` for the page title, and whole-page generation treats the hero group as the masthead instead of opening every editor with a second one.

If your project has the AI module on, generated output changes shape, and a kind whose catalogue is empty is withheld from the schema entirely. Nothing to run.

### Analytics dashboard widgets (#106)

A new `analytics` plugin option connects the Payload dashboard to Google Analytics 4, Plausible, Umami, Matomo, Fathom, or a data source you supply yourself:

```ts
withSysthema(baseConfig, {
  analytics: {
    source: 'plausible',
    siteId: 'example.com',
    apiKey: process.env.SYSTHEMA_ANALYTICS_API_KEY,
  },
})
```

Eight native dashboard widgets register (overview, chart, breakdown, devices, locations, age, gender, live). One shared period selector drives every data widget, persisted per user under the `systhema-analytics-range` preference, with GA-style presets from Today to Last year, custom ranges, and a compare-to-previous-period overlay on the chart. Five GET endpoints live under `/systhema/analytics/*`; `settings` is auth-only and the rest are gated by a new `analytics.read` capability held by `admin`, `editor` and `seoManager` by default.

Every adapter is plain `fetch` with **no runtime dependency**, including GA4's RS256 service-account JWT. Credentials stay server-side and responses are cached in `payload.kv`. `source: 'custom'` plugs any backend into the same experience: implement four data methods returning the normalized shapes, declare your own `capabilities`, and the module supplies range resolution, caching, capability gating, widget seeding and access control.

`systhema create` and `systhema setup analytics` carry a questionnaire step with `--analytics`, `--analytics-site-id`, `--analytics-api-key` and `--analytics-host`, generate an env-gated `src/payload/analytics.ts`, and a report-only `analytics-config` doctor check validates the `SYSTHEMA_ANALYTICS_*` convention.

Full reference: [`docs/payload/analytics/index.md`](../payload/analytics/index.md).

### First-party Cloudflare integration (#110)

A new `cloudflare` plugin option connects a site to its zone with either a scoped `apiToken` or the legacy `apiKey` plus `email` pair. `zoneId` is optional and auto-resolves from the site URL's host.

It adds four dashboard widgets (Overview, Traffic, Cache, Security) fed by Cloudflare's GraphQL Analytics API with their own range store and `systhema-cloudflare-range` preference; a real Payload global at slug `cloudflare` whose controls autosave (connection status, an `autoPurge` toggle, a one-click "Optimize for Systhema" bundle, and Cache, Security and Speed tabs covering development mode, purge everything, purge by URL, under-attack mode, security level, HTTPS rewrites, WAF, Always Online and image optimization); and "Clear cache" / "Clear all caches" buttons on the front-end admin bar. Auto-purge mirrors every existing revalidation seam into a targeted or whole-zone edge purge. New capabilities: `cloudflare.read`, `cloudflare.purge` and `global.cloudflare.update`.

**Purging is multi-zone.** Each registrable domain is its own Cloudflare zone, and Cloudflare drops any URL outside the zone it is handed, so a domain-routed multilingual site would otherwise never purge its second domain. Per-URL purges group by host and run one pass per owning zone; whole-site purges cover every host the site publishes on. The admin global is multi-zone too, with a domain selector, per-setting "differs" badges, an apply-to-all, and a `sync-zone-settings` action that copies a curated setting list onto the other domains. Every `host` parameter is checked against the site's own publishing hosts rather than accepting a raw zone id.

`systhema create` and `systhema setup cloudflare` carry the questionnaire step and the `CLOUDFLARE_*` env convention, validated by a report-only `cloudflare-config` doctor check.

> Enabling this module registers new admin components, so the project's Payload import map has to be regenerated once. `systhema upgrade` does not do it for you (see "Breaking & Behavioral Changes" §9).

Full reference: [`docs/payload/cloudflare/index.md`](../payload/cloudflare/index.md).

### `systhema setup` reconfigures an existing project (#107)

`systhema create` could configure a database, an email adapter, Maps and captcha on a new project. `systhema setup [feature]` does the same on an existing one, through a registry of **nine connectors**: `database`, `email`, `maps`, `captcha`, `forms`, `redirects`, `ai`, `cloudflare` and `locales`.

```bash
systhema setup                    # pick a feature interactively
systhema setup ai                 # reconfigure just the AI assistant
systhema setup locales --locales en,hu --default-locale en --yes
systhema setup email --dry-run    # plan only, write nothing
```

It reuses `create`'s flags and env vars verbatim and adds `--dry-run`, `-y`/`--yes`, `--force`, `--no-git-check`, `--no-install`, `--confirm-db-switch` and `--confirm-disable`. It requires a clean git tree, rewrites a pristine generated module silently but shows a diff and asks before touching a hand-edited one, and merges `.env` line by line rather than regenerating it, never touching `PAYLOAD_SECRET` or `SYSTHEMA_API_SECRET` and commenting out the keys of a provider you switch away from. Every off-direction is gated: an interactive confirm defaulting to No, or `--confirm-disable` under `--yes`.

Forms and Redirects are credential-less module toggles backed by generated `src/payload/forms.ts` and `src/payload/redirects.ts` modules, so flipping one is a pristine overwrite rather than AST surgery on `payload.config.ts`.

Full reference: [`docs/cli/index.md`](../cli/index.md).

### Systhema Design, in the browser and in the terminal (#96)

**Systhema Design** is a design-system generator on two surfaces that share one engine. The web app (live at `design.systhema.app`) configures a project's colours, typography, layout, fonts and brand assets in a sidebar and shows them applied live to realistic `@systhemaui/next`-powered example pages. The CLI is the same platform in the terminal, built for people who live there and for agents:

```bash
# A complete starting design from a brand colour and a font pairing:
systhema design new --seed '#0E4DA4' --heading-font 'Space Grotesk' --body-font Inter --yes

# Refine, then see what changed:
systhema design color set foundations.bg '#FAF7F2'
systhema design type scale --base 18 --ratio majorThird
systhema design diff

# Ship it into the current Systhema project:
systhema design export all --out-dir ./out
systhema design apply --dry-run
```

Four export targets produce the same formats on both surfaces: a `systhema.config.ts` carrying the `customTokens` object, a DTCG token `.zip` for the Systhema Figma plugin, a full project overlay `.zip` for a fresh scaffold, and a `systhema.design.json` agent artifact. Every one of them **imports back**, so a design that round-tripped through Figma, a coded project or an agent can keep being edited. `design apply` is the thing the browser cannot do: it writes the design straight into a project (tokens, `customTokens`, fonts, favicons, logos, the SEO module) and then runs the project's `sync`. It requires a clean git tree, so a bad apply is one `git checkout .` away from undone.

Both surfaces run on `@systhemaui/core/design`, a browser-safe, dependency-light engine whose only runtime dependency is `fflate`; the environment-specific pieces (PNG rasterization, SVG optimization, font and asset bytes) are injected seams. The palette generator is promoted into `@systhemaui/core/color`.

Every CLI command is non-interactive-capable and `--json`-outputting, and a bundled `systhema:design` agent skill drives the path from a client's brand book to an applied site.

Full reference: [`docs/design/systhema-design/cli.md`](../design/systhema-design/cli.md) and [`docs/design/systhema-design/index.md`](../design/systhema-design/index.md).

### Custom text styles and design-app polish (#142)

The part that reaches a package consumer is **`customTokens.textStyles`**. A composite can now override a shipped text style or add a new one, and each terminal composite becomes a kebab-cased utility class: a style named `Display Hero` generates `.text-display-hero`. Token aliases inside the composite resolve to the corresponding CSS variables, so a custom style can be wired straight to `customTokens.font.*` and `customTokens.responsiveSizing.*` entries.

Design-authored DTCG exports now identify themselves with the current Systhema version in the manifest's `generator` block, and core's token import accepts Design and Figma generator manifests interchangeably, as does the Figma plugin.

In the app itself: semantic foundation anchors on primitive palettes, so moving a palette's main stop shifts every connected foundation by the same number of ramp positions while preserving offsets and light/dark polarity; protected `theme` and `base` graph roots plus a structural `default` colorSystem slot with a promote-to-default action; transactional undo and redo (100 transactions, working from the shell and from the preview iframe); a **Custom** block in the Typography section for author-defined styles; and a batch of editor polish (Figma-style number stepping, a redesigned font selector, breakpoint tabs above the Scale block, heading letter spacing defaulting to zero).

### Composed colours and motion tokens from Figma (#174)

Two Figma capabilities now travel end to end.

**Composed colours.** A colour built from an alias plus an opacity (Figma's "Control opacity at scale" release) is read from the `com.figma.composedColor` extension and emitted as a live reference:

```css
color-mix(in srgb, var(--color-foundations-text) 25%, transparent)
```

instead of a flattened `#00000040`, so recolouring the source token moves the composed one with it. An opacity that is itself a variable stays a reference. The Figma plugin round-trips the composition through Figma's `COMPOSE_COLOR` variable expression rather than detaching the alias on import, and copies any unrecognised value shape verbatim into `$extensions["com.figma.rawValue"]`.

**Motion.** Figma's `Easing` and `Timing` variable types export and import: easings become `easing` tokens carrying a CSS `cubic-bezier()` (springs keep their raw value in `$extensions["com.figma.easing"]`), timings become `duration` tokens in milliseconds. Core reads two new optional token collections, `easings` and `transition`. When a project's export carries `easings`, its curves become the `--ease-*` custom property values, with the built-in easings.net table staying as the per-curve fallback. Systhema's own library exports `easings` only, 23 curves with no durations, so a stock project emits no `--duration-*` or `--transition-*` and every component computes its fallback in its own `var(--transition-…, <initial>)`.

`systhema upgrade` carries both into existing projects: a token migration writes the `easings` collection into projects that predate it, and an extension-aware `modify` turns five `foundations` colours into composed colours, each guarded on the value Systhema shipped so a project that picked its own colour keeps it and is never offered the change. One of the five is a visible change: `foundations.line-muted` was plain black (white in dark mode) at 25% and becomes `foundations.line` at 25%, so a muted line is now a faded version of the real line colour.

### Production CSS optimization (#101, #123)

A new `optimization` key in `systhema.config.ts` shrinks the generated stylesheet in production builds only. Three levels:

```ts
export default {
  // false (default) | true | an object
  optimization:
    process.env.NODE_ENV === 'production'
      ? { obfuscateVariables: true, obfuscateClasses: { prefix: 'sys-' } }
      : false,
}
```

`true` enables the three structural passes (`pruneUnchangedBreakpoints`, `mergeThemeDuplicates`, `variableReferencesResolution`). The object form adds `obfuscateVariables` and `obfuscateClasses`, each accepting `true` or a style object (`prefix`, `suffix`, `method`, `length`, `ignore`, `seed`). Class obfuscation is a post-build step, `systhema-core obfuscate` (also reachable as `systhema obfuscate`), which templates deliberately do not wire up.

Measured on a real production stylesheet at brotli `-q11`:

|                         |     raw | brotli | Δ over the wire    |
| ----------------------- | ------: | -----: | ------------------ |
| `false`                 | 482,640 | 25,791 | baseline           |
| three structural passes | 418,424 | 25,975 | **+0.7% (a wash)** |
| all five passes         | 224,261 | 21,856 | **−15.3%**         |

The honest reading: the structural passes are a **brotli wash**, and the whole download win comes from obfuscation. Templates ship the structural passes only.

Obfuscation is now safe to enable. `systhema-core obfuscate` runs a variable pass regardless of `obfuscateClasses`, rebuilds the identical rename map from the same `varObfuscationMap()` the Tailwind plugin uses, rewrites surviving `var()` references across the emitted CSS, HTML, RSC and JS including the server bundle, re-points project-authored `--name:` overrides, and reports every orphan it introduced with the exact `optimization.obfuscateVariables.ignore` entry to add. Class obfuscation is **prerender-only** and refuses to run on a CMS-backed or dynamically-rendered site (see "Breaking & Behavioral Changes" §20).

Full reference: [`docs/reference/core.md`](../reference/core.md).

### August 2026 Next.js security refresh (#162)

`next` moves to **16.3.3**, which patches two critical advisories:

- **GHSA-2xp9-vwfh-vxw4**, an unauthenticated remote code execution in the Image Optimization API via AVIF (CVSS 9.5, a libheif heap overflow reached through `sharp`).
- **CVE-2026-75604 / GHSA-p293-qw3h-jr36**, an unauthenticated remote code execution on Windows-hosted servers.

A project on 1.6.x should take this upgrade. In the same pass, `payload` and every `@payloadcms/*` package move to 3.88.0 and `@systhemaui/payload`'s peer floor for the family is raised to `^3.88.0`; the bundled `@payloadcms/richtext-lexical` admin-performance patch is re-targeted to 3.88.0, and consumer projects re-key it automatically through the existing `install-lexical-schema-dedup-patch` codemod.

`sharp` stayed at 0.34.5 in that refresh, since Next 16.3.3 disabled AVIF optimization outright and that alone closed the vector. The September refresh below finishes the job with the patched libheif.

### September 2026 dependency refresh (#197)

`next` moves to **16.3.5**, `payload` and every `@payloadcms/*` package to **3.89.0**, `react` and `react-dom` to **19.3.0**, and the scaffolds and the monorepo to **TypeScript 6.0.3** and **sharp 0.35.4**. The last two are aligned with Payload's own website template, which is now the baseline for what `@systhemaui/payload` runs against. sharp 0.35.4 carries libheif 1.23.2, the release that fixes the AVIF heap overflow, and it is required by Next 16.3.4, which re-enabled AVIF optimization; the two move together from here on. Fifteen further majors landed after being verified one at a time (vitest 5, zod 4, commander 15, `@types/node` 26 among them), and each one held back is recorded with the failure that stopped it. Two consumer-facing consequences: Node 20.9 is now the minimum (§22), and the Payload peer floor moves to `^3.89.0` (§8). `@systhemaui/payload`'s `typescript` peer widens to `^5 || ^6` and its `sharp` peer to `^0.34.2 || ^0.35.0`, and every React peer now carries Payload's own range instead of a stricter one, so no project is pushed off a version Payload accepts.

### Modern browsers only, and nothing legacy on the wire (#203, #201)

Scaffolds declare Chrome 111, Edge 111, Firefox 128 and Safari 16.4, the line Tailwind CSS v4 already requires, and `withSysthema` drops Next's unconditional client polyfill module on any project that declares it. Existing projects are moved onto the line by a codemod. Lighthouse's legacy-JavaScript audit goes quiet on a stock site. See §23 and the New Features entry.

### `systhema upgrade` no longer strands a half-upgraded project (#159, #161)

`systhema upgrade` writes `package.json` in the middle of its pipeline, and three failure modes could leave a project with the dep bumps, codemods and token migrations already on disk and nothing rolled back.

**A pnpm patch key is version-scoped.** `pnpm.patchedDependencies` is keyed `<package>@<version>`, so bumping a patched package orphaned the key and `pnpm install` aborted the whole install with `ERR_PNPM_UNUSED_PATCH`. The new `engine/patchedDeps.ts` re-keys `@systhemaui/*` entries onto the exact target version and refuses to guess for anything else, stopping the run **before the first write** when it cannot reconcile a key.

**Re-keying is only half of it.** The release that implements a consumer's patch natively is exactly the release where the diff stops applying, giving `ERR_PNPM_PATCH_FAILED` and the same half-upgraded tree one release later. So the pre-flight now **test-applies** every `@systhemaui/*` patch against the version it is about to install: it fetches the target with `npm pack` run inside the project (so the project's own `.npmrc` authorizes the private scope) into a temp directory, then runs `git apply --check` with git repository discovery switched off. A patch that still applies is carried forward, and its file is renamed alongside the key. A patch the new version has outgrown is removed behind a per-patch confirm defaulting to Yes, or `--drop-stale-patches` under `--yes`, or the run stops with exit 1. A version that cannot be fetched fails **closed** with exit 1, overridable with `--skip-patch-check`.

**Re-running after an interruption now does work.** `planUpgrade` gains `sameVersion` plus `getMigrationsForVersion()`, so an upgrade re-run against a project already at the target version replays that version's own codemods and post-upgrade hooks instead of answering "Already on X. Nothing to do." `selectCodemods()` narrows that path to codemods whose `applicable()` returns true, so a genuinely finished project still reports nothing to do.

**The third failure mode is the family pin.** `checkPeerDependencies` reads the project's exact Payload family pin from `pnpm.overrides` (then the dep specs) and writes any `@payloadcms/*` peer it adds or bumps at that exact version, so a newly added peer cannot resolve a patch ahead of the pinned family and trip Payload's "Mismatching payload dependency versions" boot check.

### Reviewable upgrade plans and database consent (#179)

`systhema upgrade --dry-run` used to answer with migration counts and titles. It now shows what a project would actually receive: every codemod's file creations, modifications and deletions (both sides of a rename), and every token migration's per-file old and new nodes, extension changes, skipped values and whether `onlyWhen` matched. Filesystem migrations run in a temporary copy of the project in execution order, so the preview is the real result rather than a guess from the definitions.

A plan can be saved with `--json`, reviewed, and applied with `--apply-plan <file> --yes`. The saved plan binds the target, the CLI version, the execution options (`--skip`, `--skip-tokens`, `--no-install`, `--no-skills`, `--drop-stale-patches`, `--allow-database`) and the project inputs; apply verifies that binding and writes the verified source bytes, rejecting an edited file, a drifted project or an overridden option. JSON output carries no banner, spinner or doctor nudge.

Database-writing post-upgrade hooks no longer inherit consent from `--yes`. They run only when `--allow-database` is selected; the plan names each hook, its command, its `DATABASE_URI` target and the exact `--skip <id>` flag, and an interactive run confirms them on their own. Hooks you leave out are listed again after the upgrade with their `systhema migrate <id>` commands. That consent is independent of the rename-or-shape classification the hooks already carry.

### WCAG 2.2 accessibility pass (#105)

Every React and Next component and every Payload page template went through an accessibility pass.

- **Reduced motion** is honored centrally. Fade and `.aos` reveals snap to their visible end state instead of animating, and `ParallaxListener` bails out. Both engines subscribe to the media query's `change` event rather than reading it once, so toggling the OS preference mid-session takes effect without a reload.
- **Landmarks and skip link.** `RootLayout` renders `<html lang>` and a skip link using the new `.skip-link` core class, targeting the `<main id="main-content">` every page template emits. Hero titles are real headings.
- **The header mobile menu** exposes `aria-expanded`, `aria-controls` and `aria-haspopup`, marks the closed menu `inert`, moves focus into the menu on open and restores it on close, closes on Escape, and marks active links `aria-current="page"`.
- **Forms** wire `aria-describedby` and `aria-invalid`, radio and checkbox groups get `role="radiogroup"` or `role="group"` with `aria-labelledby`, and `FormInvalidMessage` defaults to `role="alert"`.
- **Icon-only controls** carry an `sr-only!` label or an `aria-label`, decorative icons default to `aria-hidden="true"`, and new-tab links get `rel="noopener noreferrer"` plus an "opens in new tab" hint.
- **Google Maps embeds** render through a titled `GoogleMapEmbed` instead of `@next/third-parties`, whose title-less iframe failed `frame-title`. That dependency has been removed.

Additive API: `FooterMainNavigationGroupTitle` gains `as`, `List` items gain `checkedLabel` and `uncheckedLabel`, `EmailField` and `PhoneField` default `autoComplete` to `email` and `tel`, and cookie-consent translations gain `opensInNewTabLabel`. In Payload, the Button and Chip blocks gain a "Hide title visually" checkbox and the Icon block gains an Accessible label field with a domain-derived fallback.

The rendered DOM changes on every page as a result; see §11.

> Still deferred: video blocks have no `<track>` caption field, because caption uploads are not supported yet.

## ⬆️ Upgrade

If you're on **1.6.x** and want to land on 1.7.0:

```bash
# Update the global CLI first so it carries the 1.7.0 migrations.
pnpm add -g @systhemaui/cli@latest

# Then upgrade your project. Plan first; commit a clean tree before running for real.
cd <your-project>
systhema upgrade --dry-run               # every file and token effect, before anything is written
systhema upgrade --yes --allow-database  # apply it, data hooks included
```

The upgrade will:

1. Bump `@systhemaui/core`, `react`, `next`, `payload`, and the peer-dep ranges for Payload + Next. One floor moves: `payload` and the whole `@payloadcms/*` family go from `^3.85.0` to `^3.89.0`, and `@payloadcms/plugin-import-export ^3.89.0` is **added** as a required peer of `@systhemaui/payload` (see "Breaking & Behavioral Changes" §6 and §8). The `typescript` and `sharp` peers widen rather than move, so a project on TypeScript 5 or sharp 0.34 is not bumped; a project that wants the patched libheif takes sharp 0.35.4 itself, on Node 20.9 or newer (§22).
2. Run nine codemods, in this order: `add-searchparams-to-systhema-page`, `extract-forms-redirects-modules`, `host-aware-robots`, `rename-seo-capability-strings`, `replace-admin-favicon`, `allow-pinch-zoom-in-root-layout`, `repair-fa-icon-picker-import` (see "Breaking & Behavioral Changes" §21), `raise-browserslist-to-modern-baseline` (see §23), `rewrite-icon-tag-variants` to back-fill the 1.6.0 `Icon.<tag>` breaking change.
3. Apply the token migration: one `addCollection` change that writes the new `easings` collection (23 named curves) plus its `manifest.json` entry, seven `modify` entries that add a `com.figma.composedColor` extension to five `foundations` colours (`text-muted`, `surface-shadow-highlighted`, `surface-shadow-highlighted-hover`, `overlay-bg`, `line-muted`; `text-muted` and `line-muted` get one entry per mode), and two `modify` entries that give the body and heading font families a real fallback stack. Every entry is guarded by `onlyWhen`, so a project that already picked its own value is skipped and never offered the change.
4. Run `pnpm install`, `systhema-core payload create-app-files --override`, then the project's own `sync`, `generate:types` and `generate:importmap` scripts when `package.json` defines them (falling back to the local `systhema-core sync`, `payload generate:types` and `payload generate:importmap` binaries), so a Payload config that imports CSS runs through the project's loader instead of failing with `ERR_UNKNOWN_FILE_EXTENSION`.
5. Run post-upgrade hooks against the freshly-installed packages: `migrate-seo-data` (copies the legacy `seo` global's rows into the `seo` tab of `general-settings`, at the database level, on SQLite, Postgres and Mongo) and `migrate-localize-status` (moves `_status` out of the shared column on `pages`, `posts` and `components` and their `_v` version tables into their `_locales` siblings, in a transaction, writing the matching Payload migration file when the project keeps a migration directory). Both write to the database, so they run only with `--allow-database`; an interactive run confirms them separately from the dependency, code and token changes, and `--yes` alone leaves them out. A hook left out is listed again at the end of the upgrade with its `systhema migrate <id>` command, and both have to run before the updated app starts or deploys. Both are idempotent, and `--skip <hook-id>` still opts one out.
6. Refresh the bundled agent skills the project already has installed, across both `project` and `user` scope and every supported agent. It only refreshes what is already present; skip it with `--no-skills`.

**The patch pre-flight runs before the first write.** `systhema upgrade` now test-applies every `@systhemaui/*` pnpm patch against the version it is about to install, so a patch the new version has outgrown is caught before `package.json` changes rather than halfway through `pnpm install`. A stale patch is dropped behind a per-patch confirm, or with `--drop-stale-patches` under `--yes`. A version that cannot be fetched stops the run with exit 1; `--skip-patch-check` proceeds unverified.

**The plan is reviewable.** `systhema upgrade --dry-run` prints the token changes and the per-file codemod effects a project would receive, not only counts and titles. Save it as JSON to approve it and apply it non-interactively later:

```bash
systhema upgrade --dry-run --json > /tmp/upgrade-plan.json
systhema upgrade --apply-plan /tmp/upgrade-plan.json --yes
```

The saved plan binds the target version, the CLI version, the execution options and the project inputs, so an edited source file or a changed option is rejected before anything is written. Store the JSON outside the project, or the output file becomes a planning input.

**When the upgrade stops halfway, it now says what is left.** A failed run prints the hook that failed and every hook that was deferred or not yet run as `systhema migrate <id>` commands, in recovery order: Header nav renames first, then schema additions, then the settings copies. The `migrate-seo-data` and `migrate-emails-data` scripts also refuse, with exit 1, when their General Settings tab is enabled but its table is missing, instead of reporting success against nothing (#192).

**A push-only PostgreSQL database gets its missing tables without a prompt.** `systhema migrate --schema --dry-run` prints the additive DDL the project's resolved schema needs, and `systhema migrate --schema` applies it in one transaction; run it between the nav renames and the settings copies, and see the New Features entry below for its limits (#193).

**One `systhema-core sync` after the upgrade clears a stale-artifact warning.** `.systhema/artifacts/` now carries a `.generator.json` stamp, and a project whose committed artifacts predate it sees a warning on runtime imports until the first sync writes the stamp. Later package updates do not need another sync unless the artifact format changes (#190).

If you've moved managed Systhema files (for example into a locale segment of your own), the `--override` step in `create-app-files` will recreate the defaults alongside your moved versions. Delete the duplicates after the upgrade completes.

---

## ⚠️ Breaking & Behavioral Changes

The 23 breaking and behavioral changes, with what `systhema upgrade` adopts for you and what is left to do by hand, have their own page: [v1.7.0 breaking and behavioral changes](https://docs.systhema.app/cs/changelog/v1.7.0-breaking-changes.md). The "§" references on this page point at its numbered entries.

## ✨ New Features

### Posts module (#97)

An optional, gated blog capability for `@systhemaui/payload`, behind the `posts: boolean | PostsOptions` plugin option and off by default. When on it registers:

- **Collections.** `Posts`, `Categories` and `Tags`, grouped under a Posts admin sidebar group, with two extra built-in roles (`publisher`, `author`) and the `posts.*` / `categories.*` / `tags.*` capabilities. Public reads gate to published.
- **A permalink engine.** A configurable WordPress-style pattern (`/blog/{slug}`, `/{category}/{slug}`, date tokens), resolved after pages in the `SysthemaPage` chain, with a live permalink preview in the post editor.
- **Post and archive templates.** A `postTemplate` registry mirroring `customPageTemplates`, with built-in `DefaultPost` and `DefaultArchive`. The Archive page renders as top-level editor tabs (Hero, Prepend, Posts, Append, SEO).
- **One unified `PostsList`** in `@systhemaui/react` and `@systhemaui/next`, powering Latest, Archive, Related and the `posts` block, with a lead-posts split, an add-up count model (the highlight and lead grid are added on top of page 1) and `loading: none | pagination | loadMore | infinite`.
- **Three-tier presentation islands.** Figma defaults, then `show` and `classNames` config, then a full TSX `island`, per card, hero and template slot. Default components (`PostCard`, `HighlightCard`, `PostMeta`, `ShareButtons`) consume a serializable `PostView` view-model, and `@systhemaui/next` defaults images to `next/image` with layout-aware `sizes`.
- **Archive filtering.** Search plus taxonomy filters, exported as a reusable pipeline for custom archives. Load-more and filter fetches route through a server resolver, so appended cards keep their author bylines.

Full guide: [`docs/payload/posts/index.md`](../payload/posts/index.md).

### Wildcard redirects (#104)

The Redirects collection accepts `*` in a rule's From path. A trailing `*` captures the rest of the path including slashes; a `*` with more pattern after it captures one segment. Targets use a bare `*` (equivalent to `$1`) or numbered `$1` / `$2` captures, and a reference target collapses every match onto that one document.

Each rule gains a **Preserve query string** checkbox under Advanced Settings, on by default, honoured by exact and wildcard rules alike, with the target's own params winning. Resolution order in the site catch-all is exact redirect, then wildcard redirect, then page, then 404, and the most specific wildcard rule wins. A same-origin guard rejects a target whose off-origin-ness comes from a capture rather than the literal template.

Exact redirects were hardened at the same time to match a `from` value with or without a leading slash, which previously never matched.

Query preservation needs `searchParams` threaded through `SysthemaPage`. Existing projects get that from the `add-searchparams-to-systhema-page` codemod; without it the redirect still fires, the query is just not carried.

### Live preview on locally built sites, and for your own collections (#102)

Live preview now updates on a locally built site. In the default server mode, `/sys/preview` re-sets Next's `__prerender_bypass` draft cookie as `SameSite=Lax` without `Secure`, for loopback-over-http requests only, so Safari and other browsers that reject a `Secure` cookie over plain http keep it. The workaround never weakens HTTPS or a deployment.

A new `livePreview.mode: 'client'` renders the preview from the editor's unsaved form state through `useLivePreview`, with no draft cookie at all, which also covers a cross-origin admin and a domain-routed multilingual site.

The same machinery is exported for consumer collections. The simplest form is one `'use client'` component used for both the published render and the live overlay:

```tsx
import { SysthemaLivePreview } from '@systhemaui/payload/next'

export default function ProductPage({ doc }) {
  return <SysthemaLivePreview view={ProductView} data={doc} />
}
```

The advanced form keeps the published page server-rendered and passes a `'use client'` `clientView` for the overlay only. Also exported: `useSysthemaLivePreview`, `LivePreviewListener`, `LivePreviewClientView<T>`, `getPreviewDepth` from `@systhemaui/payload/next`, and `systhemaPreviewAdmin({ collection, prefix?, slugField? })` from `@systhemaui/payload`.

Every built-in page and post template opts into client-side preview through two optional registry fields, `clientView` and `resolveData(ctx)`, so a custom template that fetches server-side can move that fetch into `resolveData` and get a live-updating preview.

Two things the browser-rendered tree could not do on its own are covered. Material Symbols and Apple emoji picks store an identifier only and resolve their markup from a digest cache the server fills, so client-mode preview fetches the handful of ids a page uses through a new `GET <routes.api>/systhema/icons/resolve` endpoint, icons picked mid-session included (#187). And because client mode makes a project register its `customBlocks` twice, once in `payload.config.ts` and once from `livePreview.clientSetup`, development builds now diff the two registries and name any block the second list forgot, instead of rendering it as `unknown node` in the preview (#185).

> A consumer's `'use client'` view must import the listing client components and `RichText` from **`@systhemaui/payload/next/client`**, not the `@systhemaui/payload/next` barrel. The barrel is a server entry and pulls the plugin graph into the browser bundle.

### Render a form from a client view (#181)

`RenderForm` is exported from `@systhemaui/payload/next/client`, so a `'use client'` view can render a form-builder form. Until now it was reachable only through the server-side `@systhemaui/payload/next` barrel, which locked any template whose page carries a form out of client-mode live preview entirely.

The `country` and `state` option lists live in a server-only store that is empty in the browser by construction, so `RenderForm` takes an optional `fieldOptions` prop and the resolver behind it is public as `resolveFormFieldOptions` on `@systhemaui/payload/next`. Resolve it in the template's `resolveData` hook and hand it back from the view:

```ts
// a server module: the template's resolveData hook
import { resolveFormFieldOptions } from '@systhemaui/payload/next'

export const resolveData = async ({ data }: PageTemplateDataContext) => ({
  fieldOptions: resolveFormFieldOptions(data.form),
})
```

```tsx
'use client'
import { RenderForm } from '@systhemaui/payload/next/client'

export default function EntryClientView({ data, aux }: LivePreviewClientView) {
  return <RenderForm form={data.form} fieldOptions={aux.fieldOptions} />
}
```

Omit the prop on the server, where the store is read directly, and in a client view whose forms use neither field type. The same entry now also carries `resolveIconHtml` and `resolveIconForRender`, so a client view has a documented path to a stored icon value rather than a deep `fields/iconPicker/…` import (#182).

### Card-width figure breakout (#103)

Media can span the full inner width of a card, flush to its rounded edges. `@systhemaui/core` adds `.figure-w-card` and `.w-card` (exact aliases) which consume a `--card-width-offset-x` custom property published by every `.card-*`, and the card drops its own block padding when a breakout figure is its first or last child. `<Figure>` in `@systhemaui/react` accepts `width="card"`.

In Payload, the image, video, youtube, googleMap and embed blocks show a **Width alignment** radio with Default and Card width, but only when the block is a direct child of a Card block or a `layout: 'card'` columns item. A new `TierMarkerFeature` plus a polling tier-detect field makes that gating update in the admin on insert, move and paste, without a publish and reload.

### Form-submission columns and readable exports (#140)

A form submission stores every answer in one `submissionData` array, so the Form Submissions list could only show the form and an answer count, and an export flattened by position with the form as a numeric id.

Individual fields are now addable from the **Columns** menu, in two flavours, both read-only virtual fields resolved from `submissionData` in an `afterRead` hook. Nothing is persisted, so toggling a column needs no migration.

- **Cross-form columns** (Name, Email, Phone, Subject, Company) match ignoring case and separators, try aliases in priority order, and can join fields (Name falls back to `firstName` plus `lastName`). Declare your own with `forms.submissionColumns`; an array replaces the built-in set and `false` removes them.
- **Per-form columns** give every field of every form its own column labelled `"<Form title>: <Field label>"`, discovered from the database at server start and refreshed on every form save, so a newly added form field surfaces without a restart. Turn them off with `forms.autoSubmissionColumns: false`.

`@payloadcms/plugin-import-export` ships wired up wherever the Forms module is on, adding an **Export** action to the list. The default file reads `Submitted at | Form title | one column per question under the field's own label in form order | anything the form does not define, appended last`, with the timestamp in the Admin's own time zone and the heading taken from Payload's own `createdAt` string, so it matches the Fields picker in all seven languages. Exports run synchronously and are not saved. Filtering already works upstream: filter the list, or load a query preset, then export.

Options live under a new `importExport` plugin option, which accepts every upstream option plus `formSubmissionExport.{timeZone,columnLabels,renamedFields}`.

See §6 for the peer dependency and the two new tables.

### Localize every field by default, share by exception (#141)

Content localization decided which fields were per-locale by matching bare field names against hand-maintained allowlists, which produced results nobody chose: `forms.emails` stored the message per locale and the recipient shared, `pages.meta.notListed` was shared so `noindex` was all-or-nothing, and a field called `url` localized wherever it appeared while `emailTo` never did.

```ts
withSysthema(baseConfig, {
  localization: {
    policy: 'all',
    shared: ['pages.customField'], // append your own exceptions
  },
})
```

`policy: 'all'` inverts the rule: every field of every Systhema-owned schema is localized except a **path-matched denylist that carries a written reason per entry** (submission keys, upload identity, access control, structural keys such as `pages.parent` and `pages.pageTemplate`, `**.slugLock`, `**._tier`, the cookie inventory, and records of events such as `form-submissions.**`). `shared`, `sharedRows` and `localized` are per-project exception lists that append. `sharedRows` keeps a container shared while its leaves localize, which is what `forms.fields` needs, since its rows are the submission schema.

The same options can live in `systhema.config.ts` under `packages.payload.localization`; the plugin option wins on `policy` and the exception lists append. `posts.publishedAt` and `posts.author` localize on purpose.

Dry-run before changing anything with `pnpm payload systhema-localization-report`. The migration path is in §7.

### Editor-controlled video preload and shift-free native controls (#129, #134)

Beyond the Preload select (§14), `<Video>` in both `@systhemaui/react` and `@systhemaui/next` gains `deferControls` (default `false`) and `deferControlsLabel` (default `'Play video'`), backed by a shared `useDeferredVideoControls` hook exported from `@systhemaui/react`.

A `<video controls>` bar renders only once metadata loads, and then materialises as an unprompted layout shift. On a real consumer project's page that was 0.185 CLS, its entire CLS. With `deferControls`, the native control bar is withheld from first paint and enabled on the first click, on Enter or Space, or on the `play` event; while deferred the video carries `role="button"`, `tabIndex={0}` and the label, so it stays keyboard-operable. All Systhema-rendered videos with controls opt in.

Autoplaying videos defer too, on their own terms: the reveal rides on the first completed discrete user input anywhere on the page (a `pointerup`, a keypress including the first Tab, or the bare `click` an assistive technology dispatches) plus a click on the video itself. An autoplaying video gets no `role="button"`, no `tabIndex` and no label.

> Deferred markup carries no `controls` attribute, so with JavaScript disabled a deferred **autoplaying** video is moving content with no pause, stop or hide mechanism, a WCAG 2.2.2 failure. Do not set `deferControls` on an autoplaying video on a page that must work without JavaScript.

### Smaller client bundles (#124, #132, #154)

Four payloads were reaching the browser on pages that had no use for them.

**The design-token dataset.** The main `@systhemaui/core` entry statically reaches all 14 token JSON files, and JSON modules cannot be tree-shaken, so a single `import { … } from '@systhemaui/core'` in any client-reachable module shipped the lot. A new **`@systhemaui/core/client`** entry carries only the config accessors, the resolved client-token helpers, the pure utils and the types. It is a conditional export with two implementations: `dist/client.browser.js` under the `browser` condition, backed by an in-memory config store and a forwarded token snapshot, and `dist/client.js` everywhere else, backed by the real loader and real tokens, so server rendering is unchanged. Components read resolved scalars at render time through `clientVariants()`, `clientTokenValue()` and `clientBreakpoints()`. Nothing was removed from `@systhemaui/core` and every existing import keeps working.

**Swiper.** `<Carousel>` and `<GalleryWrapper>` became shells that lazily import their Swiper islands behind a `<Suspense>` fallback replicating Swiper's own markup, so a page with no carousel never fetches Swiper.

**The Payload side.** Eight client-reachable modules in `@systhemaui/payload` still imported the bare core entry; on this repo's built `dist` the core barrel contributed 83 modules and 628,062 bytes to any client graph that touched it, and dragged `fs`, `path`, `module`, `vm`, `process`, `url` and `tailwindcss/plugin` into the public client graph (19 external specifiers down to 8).

**Two optional payloads.** The `country` and `state` form fields render fixed lists (250 countries, 56 states, about 10 KB) that every visitor to every site with the Forms module downloaded, including the default scaffold where both field types are off. They now live in a server-only store, resolved at render time and serialized into the flight payload as a `fieldOptions` prop. Separately, the built-in Posts page and post templates reached the bundle because the registry names their `clientView` components; both are now `React.lazy` shells, which is free because they render only under client-mode live preview.

Measured:

| Build                                       |                 before |                after | Δ          |
| ------------------------------------------- | ---------------------: | -------------------: | ---------- |
| `templates/next` home page, eager client JS | 1,121,984 B (8 chunks) | 800,935 B (8 chunks) | **−28.6%** |
| `templates/payload` home page, raw          |            1,069,321 B |          1,059,849 B | −9,472 B   |
| `templates/payload` home page, brotli       |              264,575 B |            262,178 B | −2,397 B   |

Swiper is isolated in a 114,833-byte chunk the page never references. Both packages carry import-graph guards (`packages/core/src/client.test.ts`, `packages/payload/src/clientBundle.test.ts`) that walk `src` and `dist` and fail on a forbidden edge.

> `RenderForm` became a Server Component in the process. It was `'use client'` with a pure body and is exported only from the server `@systhemaui/payload/next` barrel, so nothing could legally have rendered it from client code.

### Troubleshoot tab in General Settings (#108)

General Settings gains an always-present **Troubleshoot** tab holding a "Revalidate all pages" click-to-confirm action. It posts to a new `POST /api/globals/general-settings/revalidate-site` endpoint, which enumerates published Pages and revalidates each path plus `/` when a homepage is configured, through the same shared helper the machine-facing `POST /sys/revalidate` route uses.

The tab stores nothing, and saving the global does not trigger revalidation. A new `global.general-settings.troubleshoot.update` capability is registered, and the endpoint accepts only that or the bulk `global.general-settings.update`, so a tab-scoped editor cannot trigger a site-wide flush. Editors do not get it by default.

This flushes the Next cache only; edge purging is the Cloudflare module's job.

### Extend a named Lexical editor tier without forking it (#177, #184)

Every editor factory (`rootLexicalEditor`, `regionLexicalEditor`, `fragmentLexicalEditor`, `slotLexicalEditor`) takes an overrides object, so a project can extend a named tier instead of hand-rolling a `lexicalEditor()` and losing what the tier knows:

```ts
slotLexicalEditor({ headings: ['h1'], label: true })
```

`headings` replaces a tier's heading set or removes it with `false`, and `label` adds or removes the Label feature. For anything else, `features: ({ defaultFeatures, tier }) => [...]` hands over the whole array, mirroring Payload's own `lexicalEditor({ features })` convention. The overrides also take `admin`, typed as Payload's own `LexicalFieldAdminProps` and merged key by key over the tier's defaults, so `slotLexicalEditor({ admin: { hideGutter: false } })` brings the gutter back on one field while the tier's `hideInsertParagraphAtEnd` stays in place. `admin` is not a managed key and does not count as an override for the AI vocabulary, since it changes what a field looks like, never which nodes it accepts.

Systhema's own features (`LabelFeature`, `LeadFeature`, `SmallFeature`, `BalanceTextFeature`) are importable from a new **`@systhemaui/payload/lexical/features`** entry point, and the types `SysthemaEditorOverrides`, `SysthemaEditorTier`, `EditorFeature` and `HeadingSize` are exported from `@systhemaui/payload`.

The tier survives the override. Payload deduplicates features by key with the last one winning, so blocks are merged rather than replaced (a transform returning its own `BlocksFeature` would otherwise take every registered `customBlock` with it, and surface as content silently vanishing), and the tier marker, the text states and the AI vocabulary are re-applied afterwards.

`headings` is also the fix for a real trap: a tier's toolbar heading dropdown is built from the enabled heading sizes alone, so an `h1` stored in a `fragment` editor renders fine but reads as the wrong level in the dropdown, and one click there rewrites the page's only `h1` with no way to restore it.

### A Systhema-owned token scope in the admin editors (#186)

A `customTextStates` `css` value is applied as an inline style, so a `var()` in it is substituted on the element the state sits on. Naming a design token directly works everywhere. Putting a project variable in between breaks in the admin: a `var()` inside a custom property is substituted where that property is declared, and in the Payload Admin the design tokens sit on the editor root, not on `:root`. A `--brand-gradient` declared on `:root` looks up token colours that do not exist in that scope, becomes guaranteed-invalid, and the state renders as nothing, with no error and no warning. A state pairing it with a literal `-webkit-text-fill-color: transparent` renders invisible text, which is how a consumer project found it.

Naming that scope used to mean targeting `.ContentEditable__root`, an internal class of `@payloadcms/richtext-lexical`. Every Systhema tier editor now stamps its own contenteditable with `systhema-editor-tokens`, through Lexical's `registerRootListener` rather than an upstream class name, and `richtext.css` seeds the tokens on that class as well. Declare a variable a text state depends on there too:

```css
:root,
.systhema-editor-tokens {
  --brand-gradient: linear-gradient(135deg, var(--color-brand-a), var(--color-brand-b));
}
```

The substitution rule and this example are documented in [`docs/payload/editor/text-states.md`](../payload/editor/text-states.md#a-text-state-is-an-inline-style).

### Editor seams from a backend review (#172)

Four gaps closed, three of them new public seams.

- **`themes.hidden`** keeps a colour-system mode out of every Systhema colour picker while a stored value stays valid, for modes that are page-level accents rather than band colours. A hidden key the colour system does not have logs a warning, and the AI vocabulary never offers one.
- **`SysthemaPayloadPageTemplate.sidebarFields`** places named fields in the document sidebar right after the template select, shown only while that template is selected.
- **`linkFields()`** is exported from `@systhemaui/payload/fields`: the `linkType` / `url` / `reference` / `newTab` set every built-in block uses, with `allowNone`, `defaultLinkType`, `relationTo` and `newTab` options. The built-in blocks keep their inline link definitions for now, so this is purely additive.
- **In-editor styles for the inline blocks** (button, chip, icon, avatar) were hand-written and only correct for the default project. On a project whose spacing config has no `swap` rule they multiplied a length by a length, so a chip kept its colours and lost its padding and an icon measured 0×0; with `optimization.obfuscateVariables` on, the button named variables that no longer existed and rendered as unstyled text. They are now generated from core's CSS-in-JS through `spacingSwapParser`, and, when obfuscation is on, rewritten with the same rename map the build used. Both helpers are newly exported from `@systhemaui/core`. Nothing changes on the public page.

### The Systhema mark on all built-in branding (#117)

Every built-in instance of the old placeholder mark becomes the hexagonal Systhema mark: the admin login logo, the admin sidebar icon, the front-end admin bar, and the Payload template's header and footer logo. The admin favicon asset moves from `public/gg-icon.svg` to `public/systhema-icon.svg`, `withSysthema()` defaults `admin.meta.icons` to the new path, and `systhema-core payload create-app-files` now scaffolds that asset, closing a gap where a project adding Payload to an existing Next app had a 404'ing admin favicon.

`systhema upgrade` runs the `replace-admin-favicon` codemod: it renames the file and writes in the new mark when the artwork is still the shipped placeholder, renames only when the icon was customised, no-ops when the new path already exists, and rewrites a hardcoded `'/gg-icon.svg'` in project source.

Both templates also gain a full front-end favicon set generated from the mark's vector paths (`icon.svg`, `favicon.ico`, `apple-icon.png`, `manifest.json`, maskable PNGs). Those, and the template's header and footer logo, are deliberate **per-project placeholders**: they are your checklist of what a real project replaces, and an upgrade never touches them.

### Scaffold into an existing folder (#175)

`systhema create` refused any target directory that already existed, which is the common case when a brief or a research folder is already on disk.

```bash
systhema create acme-site --path .          # scaffold into the current folder
systhema create --path ./client-work --force
```

`-p, --path <dir>` selects the target directory separately from the package name (relative or absolute, `.` included, defaulting to `[name]`, so every existing invocation is unchanged), and `--force` allows a non-empty one. With `--path` alone the package name comes from the target directory's basename; a positional argument still overrides it.

Because the template copy overwrites silently, the confirmation prompt is preceded by a concrete list of files that would be overwritten (first 10, then a count) and defaults to No. Two cases the walk cannot see are handled explicitly: an existing `.env` is added to the conflict list because the Payload scaffolder writes one unconditionally, and an existing `.git` suppresses `git init`, so `--force` cannot lay a second initial commit over your history. Under `--yes`, passing both flags is itself the confirmation.

### Token readers for resolved values (#138, #139)

A resolved token value is not a plain length, so `parseInt` and `parseFloat` are the wrong tools for it. Three readers ship on both `@systhemaui/core` and `@systhemaui/core/client`:

| Reader                         | Use it for                                                                                   |
| ------------------------------ | -------------------------------------------------------------------------------------------- |
| `resolveTokenPx(value, fb)`    | a layout number where the fallback is a legitimate answer (Swiper's `spaceBetween`)          |
| `tryResolveTokenPx(value)`     | a number where "unresolvable" must be handled distinctly from zero, returns `number \| null` |
| `isPositiveTokenLength(value)` | any boolean gate, reads the sign and needs no viewport                                       |

They resolve `--spacing()` calls, `px` / `rem` / `em`, `vw` / `vh` / `%` against a live viewport, and a `calc()` whose body is a pure multiply-divide chain over a single unit, using a hand-rolled parser rather than `eval` or `new Function`. A value carrying a CSS comment parses too. A `calc()` that adds or subtracts across differing units stays deliberately unparseable.

Two visible consequences: the gallery and carousel slide gaps now apply at `md` and `lg` (the gallery's regex missed the spacing-function form and fell back to a hard-coded 20; the carousel passed `spaceBetween={NaN}`, which Swiper wrote as `marginRight: "NaNpx"`), and the "Container" width option appears in six blocks that had never rendered it (§19). The gallery's Swiper breakpoint keys now use `resolveBreakpointPx`, which throws in development and logs in production for a present-but-unresolvable breakpoint rather than silently relocating where the layout switches.

`MediaWrapper`'s hard-coded `'Play video'` accessible name became a `playVideoLabel` prop on both twins, with the English default as the fallback so the affordance is never nameless.

### Footer band backgrounds and the advanced-footer hairline (#146, #168)

The `colorSystem` collection ships three footer band-fill tokens and core CSS read only one of them, so setting `footer.main.background` or `footer.bottom.background` produced a CSS variable nothing consumed. Both are now read on `.footer-main` and `.footer-bottom` and painted full-bleed with a wide `box-shadow` spread clipped back to the band's own height, so there is no markup change and no horizontal overflow.

`.footer-simple` moved to `:where(.footer-simple)`, specificity 0, so a class a project passes now wins where it had always been silently out-ranked.

The advanced footer's top hairline was a hard-coded `border-t border-black/10` utility pair on the component. It is now a core rule reading `--color-foundations-line-muted` through a `--footer-advanced-border` custom property, so it follows the theme, and `--footer-advanced-border: none` removes it.

### Font families ship a fallback stack (#131, #135)

`font.default.body.fontFamily` and `font.default.heading.fontFamily` held the bare string `Geist` with nothing after it. Webfonts load with `font-display: swap`, so the browser painted its built-in serif on every cold load until the face arrived. Both roots now ship the full list:

```text
Geist, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif
```

which roughly 37 derived font tokens reference. `systhema upgrade` carries it as two `modify` token entries guarded `onlyWhen: 'Geist'`, so a project on its own brand font is skipped and never sees the change offered; [`docs/reference/core.md`](../reference/core.md) has the per-category sans, serif and mono tails to write your own.

The Figma plugin was the source of the same defect, exporting the bare name Figma stores, so the very next export undid the fix. Export now appends the same stack in both paths that carry a family (a `FONT_FAMILY`-scoped or `…/fontFamily`-named variable, and a text style's `$value.fontFamily`), and import reduces a stacked value back to its first family before it reaches Figma, so export, import, export is byte-identical.

### `systhema migrate --schema` for push-only PostgreSQL databases (#193)

A production database that was only ever shaped by dev push has no Payload migration baseline, so `payload migrate:create` cannot diff it, and a dev push against it can hang on Drizzle's rename prompt. `systhema migrate --schema` reads the installed project's resolved Payload schema, derives the unchanged subset from PostgreSQL's catalog, lets the adapter's own Drizzle Kit generate the additions, and applies them in one transaction with an advisory lock and bounded lock waits:

```bash
NODE_ENV=production systhema migrate migrate-header-nav-dbnames </dev/null
NODE_ENV=production systhema migrate --schema --dry-run </dev/null
NODE_ENV=production systhema migrate --schema </dev/null
NODE_ENV=production systhema migrate migrate-seo-data </dev/null
```

It is PostgreSQL-only, explicit (`systhema upgrade` never runs it), additive only, and it neither creates Payload snapshots nor alters migration history. It keeps extra tables such as the legacy `seo` table, refuses known nav-table collisions, incompatible column definitions, unsafe required-column additions and unsupported custom objects, and warns about missing indexes or foreign keys on populated columns. Renames, drops, enum changes and concurrent indexes still need a dedicated helper or a reviewed project-owned migration. Run it in a low-traffic window: a lock wait times out after five seconds and reports how to retry.

The same PR corrects earlier guidance: `PAYLOAD_FORCE_DRIZZLE_PUSH=true` bypasses the schema-cache check, not the confirmation prompts. Worked example and limits: [`docs/payload/database/schema-migration.md`](../payload/database/schema-migration.md), also shipped inside the Payload package.

### Eleven new `systhema doctor` checks

`systhema doctor` gains eleven checks across this release:

| Check                       | What it reports                                                                                                           | Fixable |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------- | :-----: |
| `pending-db-renames`        | legacy table names still in the database after a rename hook's release window                                             |   no    |
| `localize-status-schema`    | `_status` still shared in the database, or per-locale in the database but off in config                                   |   no    |
| `lexical-patch`             | a missing or unapplied `@payloadcms/richtext-lexical` admin patch                                                         |   yes   |
| `stale-token-artifacts`     | the installed `@systhemaui/core` does not carry this project's tokens                                                     |   yes   |
| `stale-references`          | the agent-facing token references are missing, or were generated by a different `@systhemaui/core` than the one installed |   yes   |
| `ai-config`                 | `SYSTHEMA_AI_*` set with an unknown provider, or without a key, base URL or model                                         |   no    |
| `analytics-config`          | `SYSTHEMA_ANALYTICS_*` with an unknown provider or missing per-provider credentials                                       |   no    |
| `cloudflare-config`         | the `CLOUDFLARE_*` convention                                                                                             |   no    |
| `obfuscate-build-step`      | `obfuscateClasses` on with no post-build pass, or on a PayloadCMS project at all                                          |   no    |
| `fluid-spacing-breakpoints` | every configured breakpoint that fluid spacing is not re-based at                                                         |   no    |
| `localization-policy`       | a multi-locale project that has not declared a localization field policy                                                  |   no    |

`pending-db-renames` and `localize-status-schema` open a database connection and are marked `slow`, so they are excluded from the one-line nudge other commands emit. `stale-token-artifacts` and `stale-references` share `artifacts-config`'s fix key, so `--fix` runs the one curing command once rather than twice.

### No more legacy JavaScript from `withSysthema` (#203)

Lighthouse's "Avoid serving legacy JavaScript to modern browsers" audit fired on every scaffolded site for two reasons, and `browserslist` only fixes one. The other is Next's own client polyfill module: `app-globals.js` requires it unconditionally, so `Array.prototype.at`, `Object.hasOwn`, `String.prototype.trimStart`, `URL.canParse` and friends ship in every client bundle at any target. `withSysthema(nextConfig)` now aliases that module to an empty one, for Turbopack and webpack alike, but only when the project's own `browserslist` (`package.json`, then `.browserslistrc`, production list only) declares a `>=` floor at or above the baseline for all four browsers. A project with no list or a lower floor keeps Next's polyfills; `withSysthema(nextConfig, { dropLegacyPolyfills: false })` opts out and `true` forces it on. Verified on a production build of the Payload starter: the polyfill signature disappears from the chunks and the audit has nothing left to flag. The request strings are Next internals, so `@systhemaui/next`'s test suite reads the installed `app-globals.js` and fails when a Next release moves them; a miss costs bytes, never correctness.

### Token references in TOON 4 (#200)

The agent-facing references that `sync` writes to `.systhema/references/tokens/` now come from `@toon-format/toon` 4. Two shapes change in the files: an object whose values are uniform objects is written as a keyed tabular block (`typography[3:]{ref,value}:` with one `key: v1,v2` row per entry), and a string that starts with `#` is quoted, because a line starting with `#` is now a comment. The reference set on the Payload template is 14% smaller. Every file is rewritten on each `sync`, and `systhema upgrade` runs the project's sync, so an existing project gets the new format with no codemod and no manual step; the new `stale-references` doctor check catches a tree left behind by a sync whose reference step failed. The `understanding-tokens` skill's samples are now real generator output instead of approximations.

### Consumer skills rewritten around what the framework decides (#147, #164)

The bundled agent skills that `systhema skills install` distributes opened with STOP guards and NEVER rules and reached actual capabilities late or not at all, so an agent could satisfy every rule and still over-build.

Added: the page skeleton (`<main id="main-content"><Article theme layoutBackground>{sections}</Article></main>`) and why `<Article>` is load-bearing; a hard rule for the section-nesting trap, since `section`, `columns`, `feature`, `gallery` and `posts` each render their own `<section>` band; an inventory gate ahead of the custom-block workflow; "emit editor-shaped nodes" guidance for converters; a reach-past-the-defaults section covering named text styles, deriving sizes from tokens, per-instance arbitrary-class overrides, `colSpan`, `figure-w-screen`, hero `prepend` and `append`, `elementProps` and the generated `safelist.txt` as a class catalogue; brand-book colour derivation; and a Figma pixels-to-tokens table.

A second pass adds a **Rule 0 spacing model**: every content container (a `Section`'s inner slot, `Card`, `Column`, `Row`, `FeatureContent`, hero content, `Article` itself) is a rich-text box that spaces its own children from tokens, with a spacing-ownership table, and every `<Stack gap>`-around-prose example was replaced. `systhema:payload-as-backend` gains a decision ladder for when a project needs a custom block versus a native block, a page template or a collection.

One correction worth naming: Systhema's component classes land in the **`utilities.systhema`** cascade sub-layer, not a `components` sub-layer. Tailwind v4 has no separate `components` output layer, so `addComponents` emits into `utilities`. Cascade-layer order also reverses for `!important` declarations, so an override layer has to be declared **before** the core import:

```css
@layer overrides;
@import '@systhemaui/core/tailwind';
```

`systhema upgrade` re-copies the upgraded skills into exactly the locations a project already has them installed.

---

## 🐛 Fixes & Internal Improvements

### Bug Fixes

- **Custom admin domains route again behind a reverse proxy.** The gateway compared `request.nextUrl.hostname`, which Next can build from the internal bind address, so a proxied deployment hid the CMS. Admin host matching now uses `Host` first, then the request URL, and ignores `x-forwarded-host` unless `systhemaGateway({ trustForwardedHost: true })` says the proxy overwrites that header; with the option on, the first forwarded value wins and an empty or invalid one falls back to `Host`. Deployments that preserve the public `Host` need no opt-in (#188)
- **Server Actions work on sites served through the Systhema Gateway.** cloudflared sets `x-forwarded-host` to the tunnel hostname on the hop into the app, and Next rejects a Server Action whose `origin` host differs from that header, so every Server Action on a gateway-routed site failed with "Invalid Server Actions request". The Payload Admin loads Lexical block forms through one, so an affected block showed stock fields: a visible `_tier` select, a colour field without swatches, a plain Advanced settings collapsible. `withSysthema(nextConfig)` now appends the host of `NEXT_PUBLIC_SERVER_URL` to `experimental.serverActions.allowedOrigins`, keeps consumer entries, and leaves the config untouched when the variable is unset. Sites on Railway's own edge were never affected (#195)
- **Upgrades no longer overwrite a fresh core package with stale token artifacts.** `systhema-core payload create-app-files --override` ran before sync, and its bootstrap mirror copied 1.6-era artifacts (bare JSON imports) over the new package's `dist/tmp`, after which every later command died on `ERR_IMPORT_ATTRIBUTE_MISSING` before it could reach sync or clean. Sync now generates tokens and types from source first and stamps the output, `clean` needs only filesystem calls, the runtime mirror checks the stamp's format rather than the package version, and unstamped or incompatible artifacts are rejected with a regeneration warning (#190)
- **Data hooks and the cookie codemod stop reporting success they did not earn.** The SEO and emails scripts skip a disabled feature explicitly and exit 1 when an enabled feature's General Settings table is missing; they repair destination columns even without legacy data, honour PostgreSQL `schemaName`, and read and write MongoDB globals through Payload's `globals` collection with its `globalType` discriminator. The `optin-cookie-consent-tab` codemod preserves an existing General Settings object, including an explicit `false`, spreads and `satisfies` or `as` wrappers, instead of inserting a duplicate `generalSettings` key that TypeScript rejects; unresolved options produce a visible skipped note. The localized-status migration rejects a target whose versions tables are missing rather than reporting it as already migrated (#192)
- **`@systhemaui/payload/fields` no longer drags core's main entry into a client build.** The icon picker's Font Awesome module imported `deepMerge` from the bare `@systhemaui/core`, so a custom block that keeps its `fields` and `converter` in one module failed the browser build under `livePreview.mode: 'client'` with `Module not found: Can't resolve 'module'`, an error that named core and got blamed on the icon resolver. The import now comes from `@systhemaui/core/utils`, a guard walks the fields barrel and fails on any Node builtin or token dataset it reaches, and the icons docs no longer claim the resolver is server-side (#182)
- **Form and rich-text typography came back.** `splitImportantRules()` kept an at-rule key only when its recursed body still had content, and an `@apply` key's body is empty by definition, so all 23 `@apply` directives in `css/form.ts` and `css/richtext.ts` were discarded before Tailwind ever saw them, taking the whole `.richtext.is-formatted` typography layer with them. Emitted component CSS goes from about 40 KB to about 84 KB uncompressed as a result. A second pass, `pruneUnresolvableApplies()`, now degrades an `@apply` naming a class an off block owns instead of failing the build (#149)
- **Separated font-weight names resolve correctly.** `normalizeFontWeightAndStyle()` tested each weight with a plain `\b<name>\b` regex, so `Extra Bold` resolved to 700 instead of 800, `Semi Bold` to 700 instead of 600, `Extra Light` to 300 instead of 200 and `Extra Black` to 900 instead of 950. Systhema's own tokens use only single-word weights, so the defaults were never affected (#149)
- **The token-artifact mirror stopped serving scaffold defaults.** `refreshTmpFromSystemaDir` skipped any file whose source mtime was not newer than the target's, but a package install stamps `node_modules` with the install time, so on CI, in a Docker layer, or after any reinstall the shipped defaults were newer and every file was skipped. The gate is now a byte comparison, writes go temp-then-rename so a copy cannot write through pnpm's hardlink into the global store, mirrored subdirectories are pruned, and it warns once per process when it cannot run at all (#144)
- **Orphaned `var()` references point at real tokens.** Five custom properties were referenced with no fallback and never defined, which makes the whole declaration invalid at computed-value time with no build error and no browser warning. The gallery and carousel nav buttons and the posts pagination read a variant-less `--font-button-*`, and the avatar rule read a `--avatar-shadow-color` nothing set. A new test walks `generateCss()` and fails on any bare reference the pipeline does not define (#136)
- **`.menu-sub-navigation-wrapper` got its declarations back.** A Sass-style `'&-wrapper'` nested key compiled to the unparsable `:is(.menu-sub-navigation)-wrapper`, which the browser discards silently, so the mobile submenu rendered with no flex column, indentation, row gap, radius or margin compensation. A new test compiles the whole default component tree and re-parses it with lightningcss, so an unparsable selector now fails the suite (#156)
- **Reduced motion outranks the scroll-reveal hiding rule.** Both rules sat in the same layer with `!important`, and a media query adds no specificity, so the (0,3,0) hiding rule beat the (0,1,0) escape hatch and the escape hatch was dead code for exactly the elements it existed to rescue. A reduced-motion visitor without JavaScript could get a blank page below the fold (#153)
- **`<MediaWrapper>` is a play affordance only when it wraps a play target.** The only gate was "is this rendered as a `<div>`", so a still image or a Google Map landed in the tab order announcing itself as a play button that did nothing. It now inspects its own direct children, and a `playAffordance?: boolean` prop overrides the detection in both directions (#150)
- **Reduced-motion parallax leaves no uncovered strip.** `.parallax` carries a static `translateY(-16%) scale(1.1666)` that is a pre-JS crop rather than a layout, and with the engine deliberately silent that crop became the final state, leaving roughly 7.7% of the wrapper uncovered. The stylesheet now nests its own reduced-motion reset, and the HTML-bundle engine gained the gate it never had (#150)
- **The feature band gets an aspect-ratio control.** A portrait upload dictated the whole band's height, and the feature block was the one media-bearing block with no ratio control. `FeatureBlock` and the feature hero now expose the same Aspect ratio select the media blocks have, applied to image, video, YouTube and Google Maps, and to the parallax render path. It defaults to Auto, so existing pages render byte-identically (#150)
- **Lexical regions that start with a block render again.** `hasLexicalContent` looked at only the first top-level node and asked it for `children`, and a serialized block, an upload and a horizontal rule are all childless, so any editor whose first node was one of those read as empty. The Archive template's Prepend and Append fields were the highest-impact case (#143)
- **Revalidation hooks honour `context.disableRevalidate`.** Four hook pairs ignored the flag, and because `revalidatePath()` throws outside a Next request and the hooks run inside Payload's transaction, a seed or migration write to redirects, forms, components or uploads was rolled back while the script reported success. A standing audit test now calls every revalidation hook with the flag (#158)
- **Cookie consent keeps each locale's own footer.** The resolver overwrote every locale's translated `consentModal.footer` with markup composed from `policyLinks`; it now composes only when that locale left the footer unset (#158)
- **`col-mx-*` insets follow every breakpoint.** The generator emitted a base span from the first `responsiveSizing` mode's grid count plus a single `md`-and-up span, so a project whose `lg` grid count differs from its `md` count rendered every Section's content track at half the container above `lg`. It now walks every mode that declares its own count, including a consumer's `customTokens.responsiveSizing.xl`. The shipped counts are 12/24/24, so a default project's CSS is byte-identical (#168)
- **Seven consumer production defects.** Cookie consent init failures are caught and logged, with a notice explaining when `hideFromBots` suppresses the banner in an automation browser and a warning when a second initialiser loses the `run()` race; the banner survives a `defaultLanguage` with no matching `translations` entry; `Header`'s `elementProps.headerContainer` and `elementProps.menuToggle` merge a consumer `className` instead of replacing the component's own; Lexical block validation errors name the failing block and chain nested field messages; the `form` block is registered at the fragment tier, so a form can sit inside a column, card or accordion; a dev warning fires when a `customTokens.responsiveSizing` override writes a unitless value over a px-length token; and the easings.net curves are emitted as `--ease-*` custom properties at `:root`, not only as utility classes (#165)
- **Section theme swatches render their colours.** Section derived its swatch slot from a non-existent top-level `bg` node instead of `layout.bg`, so the colour resolved to `undefined` while the identical pickers on Feature, Gallery and Card worked. All seven pickers now share one helper, and the default page template derives the first slot instead of hardcoding `layout.bg.main` (#114)
- **Emoji icon search matches the typed glyph.** The picker's Emoji tab matched only an icon's name and upstream keyword tags, so typing or pasting the actual emoji never found it. The composed glyph is now indexed, and the query strips variation selectors and skin-tone modifiers so any presentation matches (#115)
- **`customTokens` written in the token-file shape now warns.** An override written with a mode wrapper, a collection root key or DTCG `$`-prefixed leaves merged silently and did nothing, and inspecting the generated artifacts could not reveal it. Core warns in development when a branch lands off the known token paths and carries positive evidence of the file shape, and suggests the corrected path. Extending the tokens, including brand-new groups, never warns (#128)
- **Uncapped fluid spacing above the top breakpoint warns.** A `%screen%` spacing rule expresses every layout token as a ratio of its breakpoint's design width, and Systhema declares six breakpoints but exports `responsiveSizing` for three, so the `lg` ratios keep scaling: 2,225px of container and 1,847px of body copy on a 2,560px display. The cap was already the scaffold default, but nothing said so. A dev and build-time warning now names the affected breakpoints and quotes the real ratio (#145)
- **Pinch-zoom works again.** The generated `src/app/layout.tsx` set `maximumScale: 1` and `userScalable: false`, which fails WCAG 1.4.4. Removing the keys is the only fix that holds, because Next merges `viewport` per field and `/_not-found` sits outside every route group. New scaffolds get the corrected generator; existing projects get the `allow-pinch-zoom-in-root-layout` codemod, which deletes exactly those two properties and leaves everything else, including `viewportFit: 'cover'`, byte for byte (#119, #127)
- **`systhema upgrade` runs the project's generator scripts.** The sync, type and import-map steps invoked `payload` directly, so a Payload config that imports CSS died with `ERR_UNKNOWN_FILE_EXTENSION` even though the project's own `generate:types` script carried the loader that handles it. Non-empty `sync`, `generate:types` and `generate:importmap` scripts are now preferred, with the local binaries as the fallback, and the plan, the text preview and every recovery hint print the command that will actually run (#183)
- **Upgrading off a prerelease plans the release's migrations.** `systhema upgrade` silently skipped an entire release's migrations when a project pinned to an `internal` or `canary` build of that release was upgraded to a later one: the planner coerced both ends of the window to their release form and then required strictly greater. When a project's version is a prerelease of X.Y.Z, that release's migrations are now planned for any target at or above X.Y.Z (#121)
- **The API tab is hidden on General Settings and the AI usage collection.** Both showed the admin edit view's API tab that every other Systhema collection and global hides (#112)
- **The frontend follows the visitor's locale.** The header and footer logo was a literal `href="/"`, so it dropped a visitor into the default locale; `HeaderInstance`, `FooterSimpleInstance` and `FooterAdvancedInstance` now take a `logoHref` that `RootHeader` and `RootFooter` fill with the locale path. The cookie banner picks its language from the page's `<html lang>`, and `ShareButtons` gains `labels.opensInNewTab` instead of a hard-coded English suffix (#160)
- **Blank drafts can be moved to Trash and restored.** A slug that formats to nothing was stored as `''`, and every empty string is the same value to the unique slug index on Posts, Categories and Tags, so a second unfinished draft, or trashing one whose draft version still held an empty string, failed with a slug uniqueness error. `formatSlugHook` now stores `null`, which SQL treats as distinct, on every write path including metadata-only updates. Non-empty URLs are untouched and real duplicates are still rejected (#196)

### Internal / Monorepo

- **Payload 3.85.0 to 3.86.0 and Next 16.2.6 to 16.2.12** in the July refresh, which carried the 16.2.11 security release (four High advisories: DoS via Server Actions, a middleware bypass under Turbopack with a single locale, and two SSRF vectors), plus `tailwindcss` 4.3.0 to 4.3.3 and `react` / `react-dom` 19.2.6 to 19.2.8 (#133)
- **The September refresh took every bump that passes.** Next 16.3.5, Payload 3.89.0, React 19.3.0, TypeScript 6.0.3 and sharp 0.35.4, then vitest 5, zod 4, commander 15, `@types/node` 26, esbuild 0.28, chroma-js 3 and a dozen more majors, each its own verified commit. `@toon-format/toon` 4 (changes the emitted references), `@lexical/utils` 0.50 (Payload pins the lexical family), swiper 14 (raises the browser baseline), eslint 10 (plugins cap at 9) and AI SDK 7 (renames every call site) are held back with the failure recorded. Payload's own website template is the baseline from now on, with sharp and TypeScript aligned to it exactly (#197)
- **Prettier 3.8.3 to 3.9.7 and a repo-wide reformat.** Almost the whole diff is one rule, a union type that fits in the print width now stays on one line; no byte-pinned scaffold twin changed, and two unbalanced inline-code runs in the contributor docs were closed before the new markdown parser could mangle them (#199)
- **`engines.pnpm` accepts pnpm 11**, matching Payload's website template; eslint, typescript, graphql, dotenv and vite were checked against the same template and already sat on its lines (#198)
- **`pnpm migrations:promote` repoints test imports** when it moves `migrations/next/` sources into a versioned directory, and the repo's release documentation was corrected to the real stable-cut flow (#99)
- **Systhema Design deploys from its own image**, using the same consumer-style pattern as the demo app (it installs the published packages from GitHub Packages rather than building the monorepo), and the demo image's Payload family pin was repaired (#151)
- **The public demo gained a real contact form**, accurate admin login and dashboard notices, and a reset that empties `form-submissions` before `forms` and posts a revalidation afterwards (#118)
- **Shipped skill code examples are linted in CI.** `pnpm lint:skills` refreshes the CLI's skill bundle and runs the 14 code assets under `skills/` through the Payload starter's ESLint config, failing on warnings, so a refreshed example can no longer fail a fresh scaffold's first `pnpm lint` (#183)
- **The Figma plugin gained its first tests**, including one that reads core's own font token file and fails when the shared fallback stack drifts (#135)
- **Live-app deploy pins moved twice** through the cycle so the two Railway apps picked up the security bumps and that day's merges (#163, #173)

---

## List of all changes

Every commit between `v1.6.0` and `v1.7.0`, by type and scope, is on its own page: [v1.7.0: all changes](https://docs.systhema.app/cs/changelog/v1.7.0-all-changes.md).
