---
title: "v1.7.0 breaking and behavioral changes"
description: "The 23 changes a 1.6 project meets when it upgrades to 1.7.0."
requested_language: fr
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/fr/changelog/v1.7.0-breaking-changes
version: changelog
docs_index: https://docs.systhema.app/fr/llms.txt
---
> This page isn't translated yet. Showing English.


Part of the [v1.7.0 release notes](https://docs.systhema.app/fr/changelog/v1.7.0.md).

**Running `systhema upgrade` adopts most of these for you**, via seven codemods, the token migration and two post-upgrade database hooks (including the `_status` column move, which runs as a hook rather than a manual SQL step). The steps you do by hand are the ones a tool can't safely touch: installing the new `@payloadcms/plugin-import-export` peer (§6), opting into the inverted localization policy and writing the Payload migration that backs it (§7), passing `--allow-database` so the two data hooks run at all (§3), regenerating the Payload import map after enabling the Cloudflare module (§9), running `systhema migrate migrate-header-nav-dbnames` on a SQLite project that came through 1.6.0 (§10), the one-line footer prop replacement (§5), role definitions stored outside `.ts`/`.tsx` (§1), in-page anchors pointing at an old block id (§18), and re-saving a block whose Container width was dropped while the option was hidden (§19). Each entry below marks what, if anything, is left for you.

## 1. SEO global folded into General Settings, `global.seo.*` capabilities renamed (#111)

The standalone `seo` global is gone. Its fields now live on a General Settings **SEO** tab placed after Pages, and `generatePageMetadata` reads the `seo` subtree of `general-settings`.

| Removed                                  | Replacement                                     |
| ---------------------------------------- | ----------------------------------------------- |
| `global.seo.read`                        | `global.general-settings.seo.read`              |
| `global.seo.update`                      | `global.general-settings.seo.update`            |
| `global.seo.*`                           | `global.general-settings.seo.*`                 |
| `GET /api/globals/seo` (unauthenticated) | Local API server-side, or an authenticated read |

General Settings tabs are now read-gated per tab. New capabilities `global.general-settings.{pages,seo,emails,cookieConsent}.read` are registered, and `seoManager` sees only the SEO tab's fields.

**`systhema upgrade` does the automatic half.** The `rename-seo-capability-strings` codemod rewrites the quoted capability strings across the project's `src` and `app` directories, and the `migrate-seo-data` post-upgrade hook copies the legacy global's rows at the database level (idempotent, `--force` to overwrite). The legacy `seo` table is deliberately left in place for you to drop.

**What's left for you:** role definitions stored outside `.ts`/`.tsx` (JSON, YAML, database rows) need a manual sweep, and a decoupled frontend that fetched `/api/globals/seo` anonymously must read server-side through the Local API or authenticate with a user holding `global.general-settings.seo.read`. Setting `generalSettings: false` with `seo: true` now removes the SEO defaults UI and logs a startup warning.

The tab also gains Yoast-style title templating (a Website Name field, a Title Separator picker, and a Title Template defaulting to `%page_title% %title_separator% %website_name%`), a token-chip editor with a `%`-triggered variable picker, a length counter measuring the resolved title, and a social preview covering Google, Facebook, X, LinkedIn, Discord and Slack.

## 2. Per-locale publishing is on by default for projects with locales (#171)

On any project with `locales` configured, `_status` moves out of a shared column into the `_locales` sibling tables of `pages`, `posts`, `components` and their `_v` version tables.

**This is automated.** `systhema upgrade` runs the `migrate-localize-status` post-upgrade hook, gated on the project being a Payload project with locales that has not opted out. The hook performs the whole transition in a transaction, derives each locale's state from version history, writes the matching Payload migration file when the project keeps a migration directory, and is idempotent.

Opt out with:

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

and skip the hook with `--skip migrate-localize-status`.

**The rule, stated plainly: upgrade with `systhema upgrade`, never with a bare package update.** A dev schema push cannot move a column; it drops it and re-adds it. Ship the new code without the hook and every page comes back as a draft in every locale.

A new slow, report-only `localize-status-schema` doctor check compares the config's expectation against the live database in both directions, and `@systhemaui/payload` logs one error line at boot when the option is on and the database still holds a shared status.

## 3. Database-writing upgrade hooks need explicit consent (#179)

`systhema upgrade --yes` used to run every post-upgrade hook, including the two that write to the database (`migrate-seo-data` and `migrate-localize-status`). Database-writing hooks are now excluded unless `--allow-database` is selected. The plan names each such hook, its command, its `DATABASE_URI` target and the exact `--skip <id>` flag, and an interactive run confirms them separately from the dependency, code and token changes.

**What's left for you:** pass `--allow-database` (or confirm the hooks when prompted), or run each deferred hook yourself with `systhema migrate <id>` before the updated app starts or deploys. The upgrade lists the hooks it left out, with their commands, at the end of its output. A CI script that ran `systhema upgrade --yes` and relied on the hooks running needs the extra flag.

## 4. `robots.ts` replaced by a `robots.txt` Route Handler (#116)

Next's static `robots.ts` metadata file has no request context, so its `Sitemap:` line could only ever carry one global `NEXT_PUBLIC_SERVER_URL`. On a site serving one domain per locale, every domain advertised the primary domain's sitemap.

The scaffold is now `src/app/robots.txt/route.ts`, a Route Handler deriving the origin from `x-forwarded-host`, then `host`, then `NEXT_PUBLIC_SERVER_URL`. It must keep `export const dynamic = 'force-dynamic'`: reading `request.headers` is not enough on its own, because Next prerenders the handler anyway and bakes one host's body.

This applies to **every project**, locales or not. `systhema upgrade` runs the `host-aware-robots` codemod, which swaps the file only when the old one's whitespace-normalized digest matches a known scaffolded version. A customised `robots.ts` is left in place with a note, and Next refuses to build a project carrying both, so remove one by hand in that case.

## 5. Footer components take a `theme`, `layoutBackground` is removed (#176)

The `layoutBackground` prop is removed from `FooterSimpleInstance` and `FooterAdvancedInstance` in both `@systhemaui/react` and `@systhemaui/next`, and from `RootFooter` in `@systhemaui/payload`. It is now a TypeScript error rather than a prop that quietly did nothing.

Core paints the footer surface from three colour tokens instead: `footer.simple.background` on the simple footer's root `<footer>`, `footer.main.background` on `.footer-main`, and `footer.bottom.background` on `.footer-bottom`. A footer's surface is its `theme` plus those tokens; anything else comes from a `className` or the project's own CSS.

**Migration is manual and one line.** No codemod ships for it:

```diff
- <FooterSimpleInstance layoutBackground="alternative" />
+ <FooterSimpleInstance className="bg-layout-alternative" />
```

`theme` is unchanged and is the supported way to switch the footer's palette; a theme on the root footer re-resolves the band tokens inside the themed scope. `:where(.footer-simple)` stays at zero specificity, so a class you pass still wins.

> The footer band background tokens need **no** token migration. After this removal, `footer.main.background` and `footer.bottom.background` default to the page background exactly as the Figma library defines them, and #174 removed the previously planned migration on purpose.

## 6. Form submissions require `@payloadcms/plugin-import-export`, and add two tables (#140)

Import/export is registered by `withSysthema` wherever the Forms module is on, so the export drawer works with no per-project setup.

**Install the peer.** `@payloadcms/plugin-import-export` contributes admin components that land in the consumer's import map, so it must be a **peer dependency**, like `plugin-seo`, `plugin-redirects` and `plugin-nested-docs`. A missing one gives `Module not found: @payloadcms/plugin-import-export/rsc`. There is no codemod for it; `systhema upgrade`'s dependency handling and the `peer-deps` doctor check are the surfaces that report it.

**Two new tables.** Enabling import/export registers the plugin's `exports` and `imports` collections, which existing projects pick up on their next schema push. Import stays off for submissions, so the `imports` collection is hidden and create/update locked, but the tables exist.

**Two new job tasks, and on PostgreSQL that means two enum labels.** The plugin also pushes `createCollectionExport` and `createCollectionImport` onto `jobs.tasks`. Every registered task slug is a label of the `payload_jobs` task-slug enums, so on PostgreSQL both `enum_payload_jobs_task_slug` and `enum_payload_jobs_log_task_slug` gain the two labels (alongside the built-in `inline`). A project running with Drizzle push enabled gets them on the next boot. A **push-off** project gets them from `systhema migrate --schema`, which adds missing enum labels as part of its additive plan — run it before the new code deploys, or a job write fails on a label the type does not have.

One more thing to know: `importExportPlugin` is an **async** Payload plugin, which matters for any project code that resolves `config.plugins` synchronously.

## 7. `localization.policy: 'all'` is opt-in and needs a back-fill migration (#141)

The default stays `'legacy'`, so upgrading the package changes nothing on its own. That default is load-bearing: an existing multi-locale project already has locales enabled, so a policy that widened on package upgrade would move columns into `_locales` siblings on a routine `pnpm update`, and a production deploy runs with Drizzle push disabled, so the new code would query columns nothing created and the site would 500 on every read, in a way that never reproduces in dev.

Switching to `'all'` is a schema change and is **not automated**. The required order:

1. Run the dry run: `pnpm payload systhema-localization-report` (add `--json` for machine output). It prints exactly which fields would widen.
2. Write a Payload migration that calls `backfillSysthemaLocalization`, exported from `@systhemaui/payload`. It copies the shared value into **every** locale, is idempotent and resumable, refuses to overwrite a translation, and offers `missingParentRow: 'cloneDefaultLocale'` for a locale row that does not exist yet.
3. Apply that migration **before** the new code deploys.

A multi-locale project that has not declared a policy gets a one-time startup notice and a `localization-policy` doctor warning; an explicit `policy: 'legacy'` silences both.

Measured cost on a real five-locale SQLite instance with 120 pages: the `_locales` row count did not change at all (2,520 rows before and after), `pages_locales` went from 19 to 36 columns, the database file grew 5.9%, and read latency was not measurable above process-order noise.

Full reference: [`docs/payload/localization/field-policy.md`](../payload/localization/field-policy.md).

## 8. Payload peer floor moves to `^3.89.0`, and AVIF comes back with sharp 0.35.4 (#197, #162, #133)

`@systhemaui/payload`'s peer floor for `payload` and every `@payloadcms/*` package moves from `^3.85.0` to `^3.89.0` (through `^3.86.0` in July and `^3.88.0` in August). A project pinned below 3.89.0 gets a peer warning; `systhema upgrade` carries the bumps and re-keys the bundled lexical patch. The floor follows the version Systhema is tested on, and it is the one peer Systhema sets above Payload's own: every other peer (`next`, `react`, `react-dom`) now mirrors the range Payload's packages declare, so `@systhemaui/payload` accepts React `^19.0.1 || ^19.1.2 || ^19.2.1` and `@systhemaui/react` and `@systhemaui/next` accept that or React 18.

The Next.js peer range is deliberately left at upstream's own floor. The recommendation to take 16.3.5 travels through the dependency bump, not the peer range.

**AVIF image optimization was off in Next 16.3.3 and is back from 16.3.4**, which requires sharp 0.35.4 or newer, the release with the patched libheif. A project that opted into `images.formats: ['image/avif', …]` serves AVIF again once both are in place. A project on Next 16.3.4 or newer with sharp still at 0.34 has the vulnerable decoder reachable again, so take the sharp bump with the Next one. No Systhema template or scaffold ever set `images.formats`, so a stock project is unaffected either way.

The scaffolds now ship TypeScript 6.0.3, and `@systhemaui/payload`'s `typescript` peer is `^5 || ^6`. A project that moves itself to TypeScript 6 deletes `baseUrl` from its `tsconfig.json` (a deprecation error in 6, and redundant next to `paths`); that file is project-owned, so it is a one-line manual step, not a codemod. `@payloadcms/plugin-mcp` is incompatible with TypeScript 6 at runtime. Keep TypeScript 5 when that plugin is enabled until its upstream fix lands.

## 9. The Cloudflare module needs an import-map regeneration (#110)

Enabling the `cloudflare` plugin option registers an admin global and four dashboard widgets, all of which contribute components to the Payload import map. The import map is generated from the static config, so a newly registered component path resolves to nothing until it is regenerated.

**This is manual.** After enabling the module, run `payload generate:importmap` (or start `next dev` once). `systhema upgrade` does not do it for you, because the module is off by default and the upgrade has no way to know you are about to turn it on.

## 10. SQLite projects that came through 1.6.0 still carry the old Header nav tables (#125)

The Header nav `dbName` reconciliation shipped in v1.6.0 was Postgres-only and printed a false all-clear on SQLite, leaving those projects to meet Drizzle's interactive "created or renamed?" prompt on their next schema push. `createTableName` returns `dbName` verbatim for every adapter, so SQLite carries exactly the same stale tables.

The reconciliation engine is now shared and handles both adapters: Postgres renames constraints, indexes, enums and sequences from the catalog, SQLite drops and recreates indexes from stored DDL and lets SQLite rewrite child foreign keys itself. Both paths are transactional and idempotent.

**What's left for you:** a SQLite project that upgraded through v1.6.0 should run

```bash
systhema doctor              # the pending-db-renames check names the remediation
systhema migrate migrate-header-nav-dbnames
```

The new `pending-db-renames` doctor check connects read-only through the project's `DATABASE_URI` and compares the live catalog against the expected names. It is version-independent, so it also catches a project that jumped several versions and can no longer be healed by the upgrade path.

Post-upgrade hooks also gained a `kind` discriminator of `'rename'` or `'shape'`, and `systhema upgrade` blocks before touching anything when a `'shape'` hook is planned and no Payload migration directory exists. No `'shape'` hook exists yet, so that block cannot fire today.

## 11. The accessibility pass changes rendered markup (#105)

The DOM changes on every page. Nothing needs a codemod; the changes ship with the package upgrade. What to expect:

- New ARIA attributes throughout (`aria-expanded`, `aria-controls`, `aria-haspopup`, `aria-current`, `aria-describedby`, `aria-invalid`, `role="alert"`, `role="radiogroup"`).
- Footer link collections wrapped in `<ul>` and `<li>` with `display: contents`, so the layout is byte-identical and only the accessibility tree gains the list.
- New-tab links carry `rel="noopener noreferrer"` plus an "opens in new tab" hint, as visually-hidden text or folded into the `aria-label`.
- Google Maps embeds render through `GoogleMapEmbed` instead of `@next/third-parties`, which has been removed as a dependency.
- Visitors with `prefers-reduced-motion` see reveals snap to their end state rather than animate.

If you have CSS or tests selecting on the old markup, check them.

> `@next/third-parties` globally augmented `Window` with `[key: string]: any`. Any `window as Record<string, unknown>` cast in your own code that silently relied on that now needs to be written `window as unknown as Record<…>`.

## 12. Scroll reveals fire at the element's top edge (#122)

`AosListener` is rewritten on `IntersectionObserver` and no longer measures during scroll. The trigger point moves as a result: on desktop, elements previously revealed at their midpoint and now reveal once the top edge is 20px inside the viewport bottom, so reveals fire earlier and a tall section no longer sits visible but transparent. Re-arm now happens once the element is more than 10% of the viewport height below the fold, rather than the moment it clears the viewport.

The public API and the class vocabulary (`.aos`, `.animates-on-scroll`, `.animated`, `.animates-once`, `.animate-disable`, `.aos-disable-children`) are unchanged, and the engine is newly exported as `startAosListener()`. Browsers without `IntersectionObserver` keep a measured fallback at the same trigger points.

## 13. Scroll-reveal keyframes end at `transform: none` (#165)

The reveal keyframes ended at `translate3d(0, 0, 0)`. With `animation-fill-mode: forwards` that left every revealed element a containing block for absolutely positioned descendants forever, so an `overflow: hidden` ancestor could not clip them. They now end at `transform: none`.

Motion is visually identical. **What's left for you:** a layout that relied on that retained containing block needs an explicit `position: relative` plus a `z-index` on the element in question. No codemod, since only your own CSS knows which element that is.

## 14. Video `preload` resolves to `metadata` for existing content (#129)

Every uploaded video rendered by the built-in page and post templates and by the Lexical converters hardcoded `preload="auto"`, so a large hero video downloaded in full during the LCP window. A shared Preload select (Metadata, None, Auto) is added to `VideoBlock`, `HeroSimpleBlock`, `HeroFeatureBlock`, the combined page hero, and `FeatureBlock` when `mediaType` is `video`.

A `defaultValue` only reaches documents created after the field exists, so **stored content resolves to `metadata`, not the `auto` it used to get.** This is deliberate: `preload` is an advisory hint with no visual consequence, `metadata` still paints the first frame and reads the duration, and preserving `auto` would mean the fix reached nobody until every video block was re-saved by hand. An editor who wants eager buffering picks Auto.

## 15. Captcha scripts load lazily by default (#130)

reCAPTCHA and Turnstile scripts used to fetch during hydration wherever the form sat on the page. On a real production site that was roughly 555 KB of Turnstile payload, about a quarter of the page weight, for a control most visitors never scrolled to.

A per-provider `lazy` flag under `forms.fields.<provider>` now defaults to `true`. Loading is deferred until one of three signals opens a form-scoped gate: an `IntersectionObserver` on the widget with a 300px margin, focus or pointer-down or a value change on any field of that form, or submit, which opens the gate and waits for the widget before validating (bounded by a 10s timeout, with an extra 2.5s token grace for Turnstile).

An above-the-fold form is unaffected, because `IntersectionObserver` delivers its first callback for an already-intersecting target in the same frame the effect runs, and browsers without it resolve to eager. The visible change is that a below-the-fold widget appears as the visitor approaches it. Restore the old behaviour with `lazy: false`.

## 16. Built-in hero titles render as `<h1>` (#169)

The built-in page template rendered the feature hero's title as an `<h2>`, and the post and archive templates rendered every hero title as a styled `<div>`, so a page on a feature hero, every post and every archive shipped with no `<h1>`. `DefaultPage`, `DefaultPost` and `DefaultArchive` now emit a real `<h1>` for all three hero types.

The feature hero keeps its `text-h2` class, so nothing moves visually. **What's left for you:** any project CSS or test selecting `div.hero-feature-title` or an `h2` in that position needs updating by hand. No codemod.

## 17. Post titles go through the General Settings title template (#158)

`generatePostMetadata` now runs a post title through `resolveSeoTitle` with General Settings SEO's `websiteName`, `titleSeparator` and `titleTemplate`, the same as pages do. A post titled "Ten ways to ship faster" becomes "Ten ways to ship faster – Acme" in `<title>`, `openGraph.title` and `twitter.title`.

An explicit SEO Meta Title on the post is still passed through untemplated. No codemod; the change is the intended behaviour and needs no project action.

## 18. Anchor ids change for a `blockName` containing a run of capitals (#166)

`toUrlCase` derives the `#id` every Systhema block emits from its editor-facing `blockName`, and its `/([A-Z])/g` rule split a run of capitals letter by letter, so a name like "ESG report" produced `e-s-g-report`. Two rules replace the one: break on a lowercase-to-capital boundary, and break before the last capital of a run followed by a capitalised word. `ABCDef` becomes `abc-def`, and a trailing or standalone run stays one word. camelCase and PascalCase splitting is unchanged, and `toKebabCase` (which generates CSS class and variable names) deliberately keeps the old pattern.

**What's left for you:** a block whose `blockName` contains a capital run derives a new anchor id, so any in-page anchor or bookmark pointing at the old letter-split id needs updating. No codemod exists and none is possible: the ids come from editor-authored content in the database. Styling keyed to these ids was never supported.

`toUrlCase` also sanitises form-upload filenames, which get the same improvement.

## 19. The Container width option reappears in six blocks (#138, #139)

Six Payload blocks (card, carousel, embed, form, googleMaps, media) gated their "Container" width option on `parseInt(size) > 0` over `article.paddingX`. A resolved token value is not a plain length: the pipeline emits `--spacing(60 / 4)`, or a viewport unit, or a bare number under a `swap` rule, and `parseInt` returns `NaN` on all of them. The option had never rendered, even though the shipped padding is 60px at `md` and 88px at `lg`.

The gate is now `isPositiveTokenLength(value)`, which reads the token's numeric **sign**, identical in every unit Systhema emits and needing no viewport, so it answers the same on the server, in a build step and in the browser.

**What's left for you:** a block that was re-saved through the editor while the option was missing dropped back to `'default'` and needs its `width` set back to Container by hand. A block that was never re-saved keeps its stored `'container'` value, which is valid again the moment the option reappears.

## 20. `systhema-core obfuscate` refuses to obfuscate classes on a CMS-backed site (#123)

Class obfuscation is **prerender-only**. It never rewrites `.next/server/**` JavaScript, because those chunks carry Payload block and collection slugs whose strings collide with class names, so anything rendered at runtime would keep the original names and lose its styling while the build stayed green.

The command therefore refuses and exits non-zero when it detects a PayloadCMS project, ISR routes, on-demand dynamic routes, or a `force-dynamic` / `revalidate` export. `--allow-dynamic` overrides it for a genuinely static site. A CMS-backed site now gets a loud build failure instead of a silently unstyled page, and no config change is required to get it.

A new `obfuscate-build-step` doctor check warns when `obfuscateClasses` is on but no `package.json` script chains `systhema-core obfuscate` after the build, and errors when the flag is set on a PayloadCMS project at all.

## 21. FontAwesome picker fields keep `faIconPickerField` (#189)

The 1.6 `adopt-icon-picker` codemod renamed `faIconPickerField` to `iconPickerField` without changing its import path. The FontAwesome subpath exports only `faIconPickerField`, so a deep import came out invalid and `payload generate:types` failed with a missing export. The codemod now preserves FontAwesome fields: their icon map, custom-option merging, field defaults and raw SVG storage stay as they were, because the generic picker inherits plugin packs and stores a different shape by default.

**Projects already past the 1.6 migration are repaired automatically.** Editing the old codemod cannot reach them, so the 1.7 upgrade runs a separate `repair-fa-icon-picker-import` codemod that restores the deep import and its bound references:

```diff
-import { iconPickerField } from '@systhemaui/payload/fields/iconPicker/fontAwesome'
+import { faIconPickerField } from '@systhemaui/payload/fields/iconPicker/fontAwesome'

-export const field = iconPickerField({ name: 'icon' })
+export const field = faIconPickerField({ name: 'icon' })
```

Explicit aliases, shadowed locals, property names and public export names are preserved. If `faIconPickerField` already exists anywhere in the file, the repair imports under an alias to avoid a collision. It supports `--dry-run` and is idempotent.

**What's left for you:** `iconPickerField` imported from the `@systhemaui/payload/fields` barrel is valid and may be intentional, so the repair reports those call sites with file and line and does not rewrite them. Compare against version control and restore `faIconPickerField` only where an earlier upgrade renamed it. The `adopt-icon-picker` warnings are also quieter: a local render helper whose first parameter is an `IconDefinition` from any `@fortawesome/*` package no longer triggers advice, and the remaining advice is conditional on the value coming from a CMS picker. Helpers imported from another file, `IconProp`-typed helpers, untyped JavaScript helpers and results first stored in a variable can still receive it; leave already rendered SVG and local FontAwesome definitions alone. See [existing FontAwesome fields](../payload/editor/icons.md#existing-fontawesome-fields-and-upgrades).

---

## 22. Node 20.9 is the minimum (#197)

`engines.node` drops its `^18.20.2` half and reads `>=20.9.0` at the monorepo root, in `@systhemaui/cli` and in the Payload scaffold. sharp 0.35 needs Node 20.9, and both the CLI and the scaffold depend on it directly. Node 18 left maintenance in April 2025 and the install docs already asked for Node 20+, so no supported setup changes; a Node 18 machine now gets an engines error on install instead of a failed sharp build. Payload's own template asks for Node 24.

## 23. The browser baseline is Chrome 111, Edge 111, Firefox 128, Safari 16.4 (#203, #201)

Every scaffold's `browserslist` now declares that line, and it is written down once, in `packages/cli/src/engine/browserBaseline.ts`, which the templates, the 1.6.0 `add-browserslist-to-package-json` codemod and the new `raise-browserslist-to-modern-baseline` codemod all read. The numbers are the union of three floors a Systhema site already sits on: Tailwind CSS v4 (a hard requirement of `@systhemaui/core`) supports Safari 16.4, Chrome 111 and Firefox 128, so the CSS does not render correctly below that line whatever the JavaScript is compiled for; Next 16's own modern target is Chrome 111, Edge 111, Firefox 111, Safari 16.4; and Swiper 14, which the gallery and carousel run on, removed its code paths for anything older than Chrome/Edge/Firefox 110 and Safari 16.4. Declaring anything lower ships transpiled code and polyfills to browsers the CSS never supported.

`systhema upgrade` runs the codemod, which raises any `>=` floor in the root `package.json` `browserslist` that sits below the line and leaves higher floors, absent browsers and other query shapes (`last 2 versions`, `defaults`, `> 0.5%`) alone. A project that keeps its targets in a `.browserslistrc` raises them by hand. Swiper's `snapGrid` semantics, which the gallery's `{current} / {total}` counter depends on, were checked against 14 by source diff and by measuring a real build; they are unchanged.
