From d4ab1c5cc41c75225b7f6c5d2f7d197a2a9aec56 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 22:08:42 +0000 Subject: [PATCH 1/3] docs(layout): clarify .sf-center is a primitive, not a `.center` utility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .sf-center collides with the utility-framework convention where `.center` centres an element's content. SLASHED is BEM-first and ships no generic centring utility: .sf-center is the Every-Layout Center primitive that centres *itself*. Add a "Centring" section mapping each centring intent to its primitive/modifier (cluster--center, stack--center, imposter, cover__center, center--intrinsic, …) and spelling out that .sf-center centres itself, needs a block-level element, and when to pick it over .sf-container. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LywrnXafVz2rVBNF4TuDbi --- docs/layout.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/layout.md b/docs/layout.md index 3170df67..514c9b5d 100644 --- a/docs/layout.md +++ b/docs/layout.md @@ -164,3 +164,35 @@ explicitly by composing `.sf-cq`:
``` + +## Centring: pick the primitive, not a `.center` utility + +Coming from a utility framework, `.sf-center` is a false friend. There a +`.center` class usually *centres the content inside it* (flex `place-items`, +`margin: auto`, `text-align`). SLASHED is BEM-first and layout-primitive-driven, +so it ships **no** generic centring utility. `.sf-center` is the Every-Layout +**Center primitive**: a `max-width`, guttered wrapper that centres **itself** in +its parent — it does not centre its own children (unless you add `--intrinsic`). + +Centring is always expressed as intent on the right primitive: + +| To centre… | Use | Mechanism | +|---|---|---| +| a block wrapper horizontally | `.sf-center` (prose measure) or `.sf-container` (app wrapper, adds CQ) | `margin-inline: auto` | +| children in a row | `.sf-cluster--center` | `justify-content` | +| children in a column | `.sf-stack--center` | `align-items` | +| leftover grid items | `.sf-grid-flex--center` | `justify-content` | +| an element on both axes (overlay) | `.sf-imposter` | absolute + translate | +| content in a full-height region | `.sf-cover__center` | `margin-block: auto` | +| a wrapper's children by their intrinsic width | `.sf-center--intrinsic` | flex column + `align-items` | + +Two things to remember about `.sf-center` specifically: + +- **It centres itself, not its content.** For content-centring reach for the + modifiers above. +- **It needs a block-level element in normal flow.** `margin-inline: auto` + doesn't centre an inline element, and a flex/grid item is sized by its parent + instead. `.sf-center` is `content-box`, so `--sf-center-max` bounds the + *content* and the gutter sits outside it — pick it over `.sf-container` when + you want a typographic measure; pick `.sf-container` when you want an + app-level wrapper that also establishes a container-query scope. From a4278f28ab7c2e5fe1fd75520bdb87205dc6c760 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 07:16:06 +0000 Subject: [PATCH 2/3] =?UTF-8?q?docs(layout):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20grammar=20fix=20and=20narrow=20the=20flex/grid=20si?= =?UTF-8?q?zing=20note?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two valid CodeRabbit nits on the new "Centring" section: - "There a `.center` class" → "There, a `.center` class". - The "sized by its parent" clause over-claimed (implied all flex/grid items stretch). Rephrase to say the parent's layout takes over sizing/alignment so .sf-center's own width/margin model no longer drives the result. Left `.center` as-is (not allowlisted): check:doc-refs only scans `.sf-*` classes and `--sf-*` tokens (CLASS_REF_RE = /\.sf-.../), so a bare `.center` comparative mention is never flagged — CI check:doc-refs is green. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LywrnXafVz2rVBNF4TuDbi --- docs/layout.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/layout.md b/docs/layout.md index 514c9b5d..2c230f62 100644 --- a/docs/layout.md +++ b/docs/layout.md @@ -167,7 +167,7 @@ explicitly by composing `.sf-cq`: ## Centring: pick the primitive, not a `.center` utility -Coming from a utility framework, `.sf-center` is a false friend. There a +Coming from a utility framework, `.sf-center` is a false friend. There, a `.center` class usually *centres the content inside it* (flex `place-items`, `margin: auto`, `text-align`). SLASHED is BEM-first and layout-primitive-driven, so it ships **no** generic centring utility. `.sf-center` is the Every-Layout @@ -191,8 +191,10 @@ Two things to remember about `.sf-center` specifically: - **It centres itself, not its content.** For content-centring reach for the modifiers above. - **It needs a block-level element in normal flow.** `margin-inline: auto` - doesn't centre an inline element, and a flex/grid item is sized by its parent - instead. `.sf-center` is `content-box`, so `--sf-center-max` bounds the + doesn't centre an inline element, and inside a flex or grid container the + parent's layout takes over the item's sizing and alignment, so the primitive's + own `width`/`margin` model no longer drives the result. `.sf-center` is + `content-box`, so `--sf-center-max` bounds the *content* and the gutter sits outside it — pick it over `.sf-container` when you want a typographic measure; pick `.sf-container` when you want an app-level wrapper that also establishes a container-query scope. From bbc909a755ddb035546d99a66a6eb48cd45af9da Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 08:13:39 +0000 Subject: [PATCH 3/3] =?UTF-8?q?feat(layout):=20add=20.sf-place-center=20?= =?UTF-8?q?=E2=80=94=20box-neutral=20centring=20primitive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every centring path in SLASHED forced you to adopt a specific primitive — .sf-cluster--center (wrap row), .sf-stack--center (column), .sf-imposter (absolute), .sf-cover__center (full-height). There was no way to just centre a plain element's content on both axes without inheriting that primitive's semantics, so people reached for a generic `.center` that doesn't exist here. Add .sf-place-center (`display: grid; place-items: center`) — the framework's named form of the community grid-place-items-center idiom (cf. Tailwind's place-items-center). It pairs with .sf-center by name and behaviour: .sf-center centres ITSELF (a max-width wrapper), .sf-place-center centres its CONTENT. Every-Layout parallel: the "Center" primitive is horizontal self-centring (our .sf-center); centring one thing in a box has no named primitive there either — everyone falls back to grid + place-items. This gives it a name. Includes: annotation, demo coverage card + cov-data entry, layout.md primitives table + Centring section, behaviour tests, CHANGELOG, and regenerated API artifacts. Purely additive — no rename, no breaking change. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LywrnXafVz2rVBNF4TuDbi --- CHANGELOG.md | 3 ++ configurator/src/data/classes.generated.json | 10 ++++++ core/layout.css | 14 +++++++- demo/index.html | 6 +++- docs/api-index.json | 35 ++++++++++++++++---- docs/api-index.md | 9 ++--- docs/classes.md | 5 +-- docs/layout.md | 25 +++++++++++--- docs/registry.json | 3 +- docs/token-annotations.json | 1 + llms.txt | 5 +-- tests/layout.spec.js | 34 +++++++++++++++++++ 12 files changed, 128 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17a9776f..4738f526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## Unreleased +### Features +- **layout:** add `.sf-place-center` — box-neutral centring primitive (`grid` + `place-items:center`) that centres content on both axes without adopting cluster/stack/imposter semantics + ## [0.7.27] - 2026-07-23 ### Breaking Changes diff --git a/configurator/src/data/classes.generated.json b/configurator/src/data/classes.generated.json index 6e6ecb76..b7afc728 100644 --- a/configurator/src/data/classes.generated.json +++ b/configurator/src/data/classes.generated.json @@ -2304,6 +2304,16 @@ "optional": false, "layer": "slashed.layout" }, + { + "name": "sf-place-center", + "selector": ".sf-place-center", + "kind": "layout", + "category": "Layout primitives", + "group": "", + "description": "Box-neutral centring primitive: display:grid + place-items:center centres an element's content on both axes without adopting cluster/stack/imposter semantics. Unlike sf-center (which centres itself), this centres the content; give the box a height for block-axis centring. The named form of the display:grid; place-items:center idiom.", + "optional": false, + "layer": "slashed.layout" + }, { "name": "sf-prose", "selector": ".sf-prose", diff --git a/core/layout.css b/core/layout.css index ebedefb9..ceafed57 100644 --- a/core/layout.css +++ b/core/layout.css @@ -246,7 +246,19 @@ align-items: center; } - + /* Place-center — the box-neutral centring primitive. Unlike .sf-center (which + centres ITSELF as a max-width wrapper) this centres an element's CONTENT on + both axes, and unlike the flow primitives' --center modifiers it imposes no + row/column/wrap semantics: a plain element becomes a centring box. It is + the framework's named form of the community `display:grid; place-items: + center` idiom (cf. Tailwind's place-items-center). Give the box a size + (min-block-size / height) for the block-axis centring to have room. */ + .sf-place-center { + display: grid; + place-items: center; + } + + /* Cluster */ .sf-cluster { display: flex; diff --git a/demo/index.html b/demo/index.html index c03fcbf7..b0b22d3c 100644 --- a/demo/index.html +++ b/demo/index.html @@ -3166,6 +3166,10 @@

Layout primitives .sf-center--intrinsic public

Modifier for sf-center: constrains the container to the natural width of its children (no stretching to fill available space).

centered & width-limited
+
+
.sf-place-center public

Box-neutral centring primitive: display:grid + place-items:center centres an element's content on both axes, without adopting cluster/stack/imposter semantics. Give the box a height for block-axis centring.

+
centred both axes
+
.sf-cluster public

Flex-wrap row of variable-width items with automatic gap. Children wrap when the container is too narrow. Use for tag lists, button groups, and icon rows.

OneTwoThreeFourFive
@@ -5077,7 +5081,7 @@

Design tokens 7
--sf-z-tooltip
- + diff --git a/docs/api-index.json b/docs/api-index.json index 47fa5d25..94e1c870 100644 --- a/docs/api-index.json +++ b/docs/api-index.json @@ -25,14 +25,14 @@ "optimal" ], "counts": { - "total": 1068, + "total": 1069, "by_type": { - "class": 327, + "class": 328, "token": 741 }, "by_tier": { "INTERNAL": 1, - "PUBLIC": 1018, + "PUBLIC": 1019, "PUBLIC-ADVANCED": 49 }, "by_role": { @@ -40,8 +40,8 @@ "knob": 253 }, "tokens": 741, - "classes": 327, - "sf_classes": 304, + "classes": 328, + "sf_classes": 305, "is_classes": 15, "unprefixed_classes": 8, "by_category": { @@ -50,7 +50,7 @@ "Components": 30, "Core tokens": 610, "Forms": 1, - "Layout primitives": 145, + "Layout primitives": 146, "Layout tokens": 53, "Macro classes": 51, "Macro tokens": 32, @@ -24555,6 +24555,29 @@ "optimal" ] }, + { + "name": "sf-place-center", + "type": "class", + "tier": "PUBLIC", + "selector": ".sf-place-center", + "prefix": "sf", + "kind": "layout", + "category": "Layout primitives", + "area": "layout", + "group": "", + "description": "Box-neutral centring primitive: display:grid + place-items:center centres an element's content on both axes without adopting cluster/stack/imposter semantics. Unlike sf-center (which centres itself), this centres the content; give the box a height for block-axis centring. The named form of the display:grid; place-items:center idiom.", + "isVariant": false, + "baseClass": null, + "optional": false, + "layer": "slashed.layout", + "sourceFiles": [ + "core/layout.css" + ], + "bundles": [ + "full", + "optimal" + ] + }, { "name": "sf-prose", "type": "class", diff --git a/docs/api-index.md b/docs/api-index.md index 2f2f3f2f..ea0acb94 100644 --- a/docs/api-index.md +++ b/docs/api-index.md @@ -10,11 +10,11 @@ and a short description. The machine-readable companion (with all columns) is [registry.json](registry.json); for the tier contract see [architecture.md](architecture.md). -**1068 elements** — 741 tokens, 327 classes. +**1069 elements** — 741 tokens, 328 classes. | Tier | Count | Meaning | |---|---|---| -| PUBLIC | 1018 | Everyday surface. SemVer-stable. | +| PUBLIC | 1019 | Everyday surface. SemVer-stable. | | PUBLIC-ADVANCED | 49 | Same SemVer guarantee; niche/powerful. | | INTERNAL | 1 | Implementation detail; may change without a major bump. | @@ -781,7 +781,7 @@ and a short description. The machine-readable companion (with all columns) is | `--sf-surface-bg-size` | PUBLIC | knob | surface | `cover` | background-size for the .sf-surface-bg image. Default: cover. | | `--sf-surface-color` | PUBLIC | consumption | surface | `var(--sf-color-base)` | Input for the generic .sf-surface macro. Set any color (including palette shades); the macro derives background, auto-contrast foreground, and the contextual token set from it. | -## Classes (327) +## Classes (328) ### Accessibility (9) @@ -838,7 +838,7 @@ and a short description. The machine-readable companion (with all columns) is |---|---|---|---|---| | `.sf-live-validate` | PUBLIC | form | — | Scopes native constraint-validation feedback to this subtree: within it, :user-invalid/:user-valid on input/select/textarea drive --sf-field-border-color (the same token .sf-is-invalid/.sf-is-valid set explicitly). Apply to a
or
. Off by default so a still-empty required field isn't marked invalid before a submit is attempted. | -### Layout primitives (145) +### Layout primitives (146) | Class | Tier | Kind | Group | Description | |---|---|---|---|---| @@ -957,6 +957,7 @@ and a short description. The machine-readable companion (with all columns) is | `.sf-imposter--fixed` | PUBLIC | layout | — | Imposter variant that uses position:fixed instead of absolute. Use for viewport-level overlays and modals. | | `.sf-overlay` | PUBLIC | layout | — | Overlay layout helper — absolutely fills its positioned parent (`position: absolute; inset: 0`) for overlays, backdrops, and media scrims. | | `.sf-pancake` | PUBLIC | layout | — | Three-row layout (header, main, footer) where main takes all remaining vertical space via flex-grow. The footer always sticks to the bottom on tall viewports. | +| `.sf-place-center` | PUBLIC | layout | — | Box-neutral centring primitive: display:grid + place-items:center centres an element's content on both axes without adopting cluster/stack/imposter semantics. Unlike sf-center (which centres itself), this centres the content; give the box a height for block-axis centring. The named form of the display:grid; place-items:center idiom. | | `.sf-reel` | PUBLIC | layout | — | Horizontally scrollable row of fixed-height items with snap scrolling. Children don't shrink below their intrinsic width, creating a card carousel effect. | | `.sf-section` | PUBLIC | layout | — | Full-width page section with vertical padding (--sf-section-pad-default). Use to create visually distinct content bands. | | `.sf-section--2xl` | PUBLIC | layout | — | Section variant with 2xl vertical padding. | diff --git a/docs/classes.md b/docs/classes.md index 06c3ebd6..f7dc4ea0 100644 --- a/docs/classes.md +++ b/docs/classes.md @@ -3,12 +3,12 @@ > **Generated** from source by `scripts/gen-class-reference.js` — > run `npm run docs:classes` to refresh. Do not edit by hand. -**304 .sf-classes, 15 .sf-is-classes.** Every `.sf-*` layout/macro class and `.sf-is-*` state class. +**305 .sf-classes, 15 .sf-is-classes.** Every `.sf-*` layout/macro class and `.sf-is-*` state class. See [architecture.md](architecture.md) for layer order and naming conventions. ## Layout primitives (`core/layout.css`) -145 classes. +146 classes. | Class | |---| @@ -127,6 +127,7 @@ See [architecture.md](architecture.md) for layer order and naming conventions. | `.sf-imposter--fixed` | | `.sf-overlay` | | `.sf-pancake` | +| `.sf-place-center` | | `.sf-reel` | | `.sf-section` | | `.sf-section--2xl` | diff --git a/docs/layout.md b/docs/layout.md index 2c230f62..cdc49683 100644 --- a/docs/layout.md +++ b/docs/layout.md @@ -16,6 +16,7 @@ All primitives are exercised live in the [demo](/demo/). | `.sf-container` | centered max-width wrapper; declares the named `sf-layout` container; `--narrow` | `--sf-container-*`, `--sf-gutter` | | `.sf-box` | isolated unit with padding and optional border outline | `--sf-box-padding`, `--sf-box-border-width`, `--sf-box-border-color` | | `.sf-center` | intrinsic centering with max-width and gutters; `--intrinsic` | `--sf-center-max`, `--sf-center-gutter` | +| `.sf-place-center` | box-neutral centring: centres content on both axes (`grid` + `place-items:center`) in a plain element | — | | `.sf-stack` | vertical flow with consistent gap (the "owl") | `--sf-stack-gap` | | `.sf-cluster` | wrapping inline group; `--no-wrap` | `--sf-cluster-gap/-align/-justify` | | `.sf-sidebar` | content + fixed-ish side panel that wraps when narrow | `--sf-sidebar-*` | @@ -169,15 +170,29 @@ explicitly by composing `.sf-cq`: Coming from a utility framework, `.sf-center` is a false friend. There, a `.center` class usually *centres the content inside it* (flex `place-items`, -`margin: auto`, `text-align`). SLASHED is BEM-first and layout-primitive-driven, -so it ships **no** generic centring utility. `.sf-center` is the Every-Layout -**Center primitive**: a `max-width`, guttered wrapper that centres **itself** in -its parent — it does not centre its own children (unless you add `--intrinsic`). +`margin: auto`, `text-align`). In SLASHED that job belongs to +**`.sf-place-center`**; `.sf-center` is the Every-Layout **Center primitive**: a +`max-width`, guttered wrapper that centres **itself** in its parent — it does not +centre its own children (unless you add `--intrinsic`). The two are a pair: +`.sf-center` centres *itself*, `.sf-place-center` centres *its content*. + +`.sf-place-center` is the box-neutral centring primitive — `display: grid; +place-items: center` — the named form of the community +`display:grid; place-items:center` idiom. Reach for it when you have a plain +element and just want its content centred on both axes, without adopting a +row/column/wrap primitive: -Centring is always expressed as intent on the right primitive: +```html +
+

centred on both axes

+
+``` + +Otherwise centring is expressed as intent on the right primitive: | To centre… | Use | Mechanism | |---|---|---| +| content in a plain box, both axes | `.sf-place-center` | `grid` + `place-items` | | a block wrapper horizontally | `.sf-center` (prose measure) or `.sf-container` (app wrapper, adds CQ) | `margin-inline: auto` | | children in a row | `.sf-cluster--center` | `justify-content` | | children in a column | `.sf-stack--center` | `align-items` | diff --git a/docs/registry.json b/docs/registry.json index c7394152..29bc1b52 100644 --- a/docs/registry.json +++ b/docs/registry.json @@ -20,7 +20,7 @@ ], "counts": { "tokens": 741, - "sf_classes": 304, + "sf_classes": 305, "is_classes": 15 } }, @@ -978,6 +978,7 @@ "sf-overlap-host", "sf-overlay", "sf-pancake", + "sf-place-center", "sf-prose", "sf-reel", "sf-scale-down", diff --git a/docs/token-annotations.json b/docs/token-annotations.json index 98471a68..ea8113aa 100644 --- a/docs/token-annotations.json +++ b/docs/token-annotations.json @@ -907,6 +907,7 @@ "sf-breakout": "Breaks a child element out of a parent sf-content-grid, spanning full bleed (or a named grid area) regardless of the parent column. Useful for full-width sections inside constrained prose.", "sf-center": "Centers children both horizontally and vertically using flexbox. Optionally add sf-center--intrinsic to size the container to its content.", "sf-center--intrinsic": "Modifier for sf-center: constrains the container to the natural width of its children (no stretching to fill available space).", + "sf-place-center": "Box-neutral centring primitive: display:grid + place-items:center centres an element's content on both axes without adopting cluster/stack/imposter semantics. Unlike sf-center (which centres itself), this centres the content; give the box a height for block-axis centring. The named form of the display:grid; place-items:center idiom.", "sf-cluster": "Flex-wrap row of variable-width items with automatic gap. Children wrap when the container is too narrow. Use for tag lists, button groups, and icon rows.", "sf-cluster--2xl": "Cluster variant with 2xl gap between items.", "sf-cluster--between": "Cluster variant that distributes items with space-between alignment.", diff --git a/llms.txt b/llms.txt index 17cebea5..486bfe4d 100644 --- a/llms.txt +++ b/llms.txt @@ -224,7 +224,7 @@ Generated from source by `npm run docs` — do not edit by hand. Full prose guid ## Classes -327 classes, grouped by area, each with a one-line purpose. `--variant` names are BEM modifiers of the base class above them. +328 classes, grouped by area, each with a one-line purpose. `--variant` names are BEM modifiers of the base class above them. ### Accessibility (9) @@ -245,7 +245,7 @@ Generated from source by `npm run docs` — do not edit by hand. Full prose guid - `.print-no-color` — Strips color information in print (print-color-adjust: economy). Defers to the printer's ink-saving mode for backgrounds and non-critical color fills. - `.print-only` — Hides the element on screen (display: none) but shows it in @media print. Use for print-only headers, footers, and supplementary content not needed on screen. -### Layout primitives (145) +### Layout primitives (146) - `.sf-alternate` — Zigzag / alternating two-column layout. Even children align image-left/text-right; odd children flip. Collapses to a single column below the breakpoint. - `.sf-bento` — Auto-fill bento grid for card dashboards. Children span 1 column by default; use span modifiers (sf-bento-wide, sf-bento-tall, sf-bento-full, sf-bento-featured) to break the grid. @@ -362,6 +362,7 @@ Generated from source by `npm run docs` — do not edit by hand. Full prose guid - `.sf-imposter--fixed` — Imposter variant that uses position:fixed instead of absolute. Use for viewport-level overlays and modals. - `.sf-overlay` — Overlay layout helper — absolutely fills its positioned parent (`position: absolute; inset: 0`) for overlays, backdrops, and media scrims. - `.sf-pancake` — Three-row layout (header, main, footer) where main takes all remaining vertical space via flex-grow. The footer always sticks to the bottom on tall viewports. +- `.sf-place-center` — Box-neutral centring primitive: display:grid + place-items:center centres an element's content on both axes without adopting cluster/stack/imposter semantics. Unlike sf-center (which centres itself), this centres the content; give the box a height for block-axis centring. The named form of the display:grid; place-items:center idiom. - `.sf-reel` — Horizontally scrollable row of fixed-height items with snap scrolling. Children don't shrink below their intrinsic width, creating a card carousel effect. - `.sf-section` — Full-width page section with vertical padding (--sf-section-pad-default). Use to create visually distinct content bands. - `.sf-section--2xl` — Section variant with 2xl vertical padding. diff --git a/tests/layout.spec.js b/tests/layout.spec.js index b867095a..0fd4050b 100644 --- a/tests/layout.spec.js +++ b/tests/layout.spec.js @@ -506,6 +506,40 @@ test.describe('layout: .sf-center', () => { }); }); +// ── .sf-place-center ──────────────────────────────────────────── +test.describe('layout: .sf-place-center', () => { + test('is a grid with place-items: center', async ({ page }) => { + await setup(page, `

x

`); + const cs = await page.locator('#t').evaluate(el => ({ + display: getComputedStyle(el).display, + align: getComputedStyle(el).alignItems, + justify: getComputedStyle(el).justifyItems, + })); + expect(cs.display).toBe('grid'); + expect(cs.align).toBe('center'); + expect(cs.justify).toBe('center'); + }); + + test('centres its content on both axes when the box has a size', async ({ page }) => { + // margin/place resolve to pixel positions; verify centring via bounding boxes. + await setup(page, ` +
+
x
+
+ `); + const res = await page.evaluate(() => { + const box = document.getElementById('box').getBoundingClientRect(); + const c = document.getElementById('c').getBoundingClientRect(); + return { + dx: Math.abs((c.left - box.left) - (box.right - c.right)), + dy: Math.abs((c.top - box.top) - (box.bottom - c.bottom)), + }; + }); + expect(res.dx).toBeLessThan(2); + expect(res.dy).toBeLessThan(2); + }); +}); + // ── .sf-box ───────────────────────────────────────────────────── test.describe('layout: .sf-box', () => { test('has non-zero padding', async ({ page }) => {