From 2d7a68a05cb3daf7617f48c1b9c7f56c622c6153 Mon Sep 17 00:00:00 2001 From: Foscat Date: Tue, 25 Aug 2026 14:26:01 -0500 Subject: [PATCH 1/7] docs: specify 3.0.2 and 3.1.0 refinements --- ...5-layout-style-css-3-1-usability-design.md | 544 ++++++++++++++++++ 1 file changed, 544 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-25-layout-style-css-3-1-usability-design.md diff --git a/docs/superpowers/specs/2026-08-25-layout-style-css-3-1-usability-design.md b/docs/superpowers/specs/2026-08-25-layout-style-css-3-1-usability-design.md new file mode 100644 index 0000000..5b81151 --- /dev/null +++ b/docs/superpowers/specs/2026-08-25-layout-style-css-3-1-usability-design.md @@ -0,0 +1,544 @@ +# Layout Style CSS 3.0.2 And 3.1.0 Usability Refinement Design + +## Status + +Approved for implementation planning on August 25, 2026. This specification +defines two sequential public releases: a `3.0.2` correctness patch followed by +a `3.1.0` usability release. + +## Context + +`layout-style-css@3.0.1` has a sound structural architecture. It separates +foundation tokens, wrappers, intrinsic primitives, recipes, utilities, and +spatial personalities. It remains dependency-free at runtime, keeps semantic DOM +order, uses container queries for recipe topology, and avoids visual or +interaction ownership. + +The usability audit identified a consistent source of friction: the library is +appropriately opinionated about structural mechanics but too opinionated about +spatial density. Downstream applications compensate for universal section +spacing, narrow content measures, inherited gap changes, bounded Scroll height, +and fixed recipe thresholds. The audit also identified correctness defects in +App Shell row geometry, compact section behavior, and public Wrapper gutter +tokens. + +The implementation must retain the v3 architecture. It must improve application +development without moving color, typography, component paint, or interaction +states into this package. + +## Design Principle + +Layout Style CSS should make structurally correct layouts easy without making +aesthetic density decisions that consumers routinely need to undo. + +The library should continue to own: + +- shrink-safe Grid and Flex behavior +- semantic areas and recipes +- container-query topology +- safe-area handling +- intrinsic wrapping and sizing +- deliberate scroll containment + +The library should make the following behavior conservative, explicit, and +locally overridable: + +- section rhythm +- workspace measure +- nested layout gaps +- scroll-height constraints +- density context +- recipe threshold metadata + +## Goals + +1. Publish the correctness fixes independently as `3.0.2`. +2. Publish the application-usability improvements as `3.1.0` only after `3.0.2` + is available from npm. +3. Keep all changes within the existing v3 module and ownership boundaries. +4. Preserve zero-configuration intrinsic behavior while adding explicit local + controls. +5. Improve portrait, landscape, short-height, and large-workspace behavior. +6. Make every documented public geometry token effective or explicitly + extension-only. +7. Verify authored CSS, generated CSS, the package manifest, documentation, + rendered behavior, package contents, and registry results as one release + contract. + +## Non-Goals + +1. Do not rewrite the package or replace its v3 architecture. +2. Do not split Wrapper into Container, Measure, and Scope APIs in v3. That + remains potential v4 work. +3. Do not add framework-specific components or JavaScript runtime behavior. +4. Do not move UI Style Kit paint or Interactive Surface state ownership into + this package. +5. Do not change the companion-package version ranges without evidence from the + ecosystem release preflight. +6. Do not modify downstream application repositories as part of these releases. +7. Do not publish `3.1.0` until the `3.0.2` registry artifact is independently + verified. + +## Release Architecture + +The work uses two sequential release branches and two independent release gates. + +### Release 1: `3.0.2` + +1. Start from current protected `main` at `3.0.1`. +2. Implement only the correctness patch and its documentation. +3. Run focused checks during development and the complete release gate at the + end. +4. Commit, push, review, and merge the patch through the repository's normal PR + policy. +5. Create immutable tag `v3.0.2` from the merged commit and publish a GitHub + Release. +6. Let the protected npm workflow publish with provenance. +7. Confirm npm version, `latest` dist-tag, package contents, and provenance. + +### Release 2: `3.1.0` + +1. Start a new branch from protected `main` after `v3.0.2` is merged and + published. +2. Implement the usability features on top of the verified patch. +3. Repeat the complete local, PR, tag, GitHub Release, and npm verification + sequence for `v3.1.0`. + +The machine's local npm session is not authenticated. Publication therefore +uses `.github/workflows/npm-publish.yml`, including its protected environment, +main-ancestry requirement, exact tag/version match, ecosystem preflight, and npm +provenance. If the protected environment requests a human reviewer, work pauses +at that approval gate without substituting a direct local publish. + +## `3.0.2` Correctness Patch + +### App Shell Row Geometry + +Every App Shell topology receives an explicit row token: + +```css +--ly-app-shell-base-rows: auto auto minmax(0, 1fr) auto auto; +--ly-app-shell-medium-rows: auto auto minmax(0, 1fr) auto; +--ly-app-shell-wide-rows: auto minmax(0, 1fr) auto; +``` + +The base recipe consumes the base token. The `52rem` App Shell query consumes +the medium token, and the `72rem` query consumes the wide token alongside the +matching area and column tokens. + +Personality profiles that replace the default three-row wide topology with four +rows must also replace `--ly-app-shell-wide-rows`. The expected four-row profiles +are Bento, Neumorphism, Split Screen, and Tactile. Their flexible row must align +with the row occupied or spanned by `main`, while header and footer tracks remain +intrinsic. + +Tests must derive the number of rows from rendered computed styles rather than +only matching source text. At base, medium, and wide sizes: + +- the number of explicit row tracks matches the area topology +- `main` receives the flexible block-space track +- header and footer remain content-driven +- no unnamed track receives intentional free space +- no personality introduces internal or document overflow + +### Compact Section Semantics + +Normal and compact section padding become separate public tokens: + +```css +--ly-section-padding-block: clamp(3rem, 7vh, 6rem); +--ly-section-padding-block-compact: clamp(1.5rem, 3.5vh, 3rem); +``` + +`3.0.2` preserves the existing normal-section default. `.ly-section--compact` +consumes the compact token directly, and `.ly-section--flush` remains zero. + +Both viewport-height tiers tune both tokens. At `44rem` and `30rem`, the compact +computed value must remain strictly smaller than the normal value. This is a +behavioral invariant across all supported viewport heights rather than an +assertion of one exact CSS string. + +### Canonical Wrapper Gutter + +`--ly-wrapper-gutter` becomes the canonical public Wrapper gutter. The default +continues to derive from `--ly-page-padding-inline`, with safe-area compensation +applied exactly once by the Wrapper implementation. + +The universally valid viewport fallback remains available. Container-relative +units remain a feature-detected enhancement. Both public tokens must affect +rendered Wrapper padding when overridden from `.ly-root` or application CSS. + +Internal variables may remain for calculation clarity, but they must derive from +the public gutter instead of bypassing it. + +### Patch Compatibility + +`3.0.2` changes no selector names, exports, recipe thresholds, or default normal +section rhythm. It corrects behavior that contradicts the existing public API. +The manifest changes are additive. + +## `3.1.0` Usability Release + +### Conservative Default Rhythm + +The normal section default becomes: + +```css +--ly-section-padding-block: clamp(2rem, 4vh, 4rem); +--ly-section-padding-block-compact: clamp(1rem, 2.5vh, 2rem); +``` + +The `44rem` and `30rem` height tiers cap both tokens more aggressively while +preserving `compact < normal`. The exact source expressions may be adjusted +during implementation if rendered tests show discontinuity at a boundary, but +the following observable rules are fixed: + +- normal sections are smaller than `3.0.2` normal sections +- compact sections remain smaller than normal sections +- the change at a height boundary is not visually abrupt +- short-landscape pages retain useful workspace height + +### Contextual Density + +Add `data-ly-density` with three values: + +```html +
+ ... +
+``` + +```text +compact +normal +spacious +``` + +The attribute is valid on `.ly-root` and on a descendant that begins a local +layout subtree. Density tokens inherit within that subtree. A nested `normal` +context resets a compact or spacious parent to normal semantics. + +Density rules live in a final `ly.context` cascade layer after personalities. +This makes an explicit density choice stronger than a personality's default +spacing profile while keeping ordinary unlayered application CSS stronger than +the library. + +Density affects: + +- normal and compact section padding +- generic recipe and primitive gaps +- Grid and Card Grid gaps +- Stack gaps +- Cluster gaps +- App Shell route spacing through the recipe gap + +The density profiles are: + +| Profile | Section rhythm | Generic gap | Stack gap | Cluster gap | +| --- | --- | --- | --- | --- | +| compact | `clamp(1rem, 2.5vh, 2rem)` | `--ly-space-3` | `--ly-space-3` | `--ly-space-2` | +| normal | `clamp(2rem, 4vh, 4rem)` | personality default | `--ly-space-4` | `--ly-space-3` | +| spacious | `clamp(3rem, 7vh, 6rem)` | at least `--ly-space-6` | `--ly-space-5` | `--ly-space-4` | + +Each profile also defines a smaller compact-section value and applies the +short-height caps. The spacious profile intentionally preserves the former +marketing-oriented section rhythm. + +Density changes geometry only. It does not alter color, typography, borders, +shadows, component paint, visibility, or interaction behavior. + +### Workspace Wrapper + +Add a public token and modifier: + +```css +--ly-wrapper-workspace: 96rem; +``` + +```html +
...
+``` + +The Wrapper scale becomes compact, prose, content, workspace, wide, full, and +breakout. Workspace is explicitly for tables, dashboards, forms, inventory, +administration, and other task-oriented interfaces. Content remains optimized +for conventional content, and wide remains available for unusually broad +compositions. + +The new token, modifier, manifest entry, demo control, docs, and package tests +are additive. + +### Local Gap Utilities + +Expose the complete spatial scale as `.ly-gap-0` through `.ly-gap-9`. + +Each utility assigns the `gap` property on the element carrying the class. It +does not redefine inherited Grid, Stack, Cluster, or generic gap defaults. This +makes the utility's meaning local and prevents a parent utility from silently +reconfiguring nested layouts. + +Consumers that intentionally want a shared spacing context may continue to set +the public custom properties explicitly in application CSS. The migration guide +must identify that explicit custom-property path for code that relied on the +former accidental utility inheritance. + +Rendered tests must prove that: + +- every tier maps to the matching `--ly-space-*` token +- the utility changes its own Grid, Stack, Cluster, intrinsic primitive, or + recipe gap +- a nested layout without its own utility retains its normal default +- a nested layout with its own utility receives its own local value + +### Explicit Scroll Constraints + +`.ly-scroll` becomes the least opinionated primitive. It provides overflow and +overscroll behavior but no maximum block size. + +Add two modifiers: + +```text +ly-scroll--bounded +ly-scroll--viewport +``` + +`.ly-scroll--bounded` consumes `--ly-scroll-max`, whose default becomes a stable +length cap. `.ly-scroll--viewport` consumes a new +`--ly-scroll-viewport-max` token that retains the former viewport-relative +behavior and short-height tuning. + +This creates three distinct contracts: + +1. `.ly-scroll`: scroll if the containing layout constrains the region. +2. `.ly-scroll--bounded`: cap the region to a stable or application-overridden + maximum. +3. `.ly-scroll--viewport`: cap the region relative to the current viewport and + height tier. + +The `3.1.0` migration guide must call out that consumers wanting the `3.0.x` +Scroll height should add `.ly-scroll--viewport`. + +### Recipe Threshold Metadata + +Keep the existing generic manifest arrays and add additive recipe ownership: + +```json +{ + "thresholds": { + "containerMinWidths": ["42rem", "44rem", "48rem", "52rem", "72rem"], + "viewportMaxHeights": ["44rem", "30rem"], + "recipes": { + "splitHero": { "wide": "42rem" }, + "listDetail": { "wide": "44rem" }, + "docs": { "wide": "48rem" }, + "appShell": { "medium": "52rem", "wide": "72rem" }, + "dashboard": { "medium": "52rem", "wide": "72rem" } + } + } +} +``` + +The manifest contract verifies that the structured values match the actual +container queries. Existing consumers of the generic arrays remain compatible. + +### Public Token Accountability + +The manifest contract classifies every public geometry token as one of: + +- runtime-consumed by authored CSS +- used as a documented fallback or alias that reaches a runtime consumer +- explicitly extension-only for application or personality authors + +An unclassified declaration-only public token fails the static contract. The +test should use parsed CSS data where practical and a narrow explicit allowlist +for intentionally extension-only tokens. It must not infer liveness from a token +appearing only in generated output, documentation, or the manifest itself. + +## Demo And Documentation + +The existing Interactive Layout Lab remains the rendered verification surface. +It gains or documents: + +- compact, normal, and spacious density contexts +- the workspace Wrapper +- all ten local gap tiers +- unconstrained, bounded, and viewport-relative Scroll behavior +- App Shell base, medium, and wide geometry +- a dense application fixture +- a nested section fixture +- a nested wrapper fixture +- a nested gap fixture +- a long-scroll fixture + +Required maintained documentation includes: + +- `README.md` +- `CHANGELOG.md` +- `docs/wiki/Home.md` +- `docs/wiki/Getting-Started.md` +- `docs/wiki/Layout-Primitives.md` +- `docs/wiki/Layout-Recipes.md` +- `docs/wiki/Layout-Styles.md` +- `docs/wiki/Migrating-To-3.0.md` for the `3.0.2` clarification when relevant +- a new `docs/wiki/Migrating-To-3.1.md` +- `docs/wiki/Release-And-Publishing.md` +- package and manifest metadata + +Authored CSS remains under `styles/`. Generated CSS under `dist/` and generated +Pages artifacts are rebuilt rather than edited by hand. If implementation adds +or changes JavaScript functions, their professional API comments must be JSDoc +compatible. The current repository has no jsdoc2md generation command; if one is +introduced or discovered before publication, its granular generated references +must be refreshed before pushing either release. + +## Verification Strategy + +### Development Checkpoints + +Do not run the broad CI-equivalent chain during implementation. After each +coherent slice, run only its affected static contract and a focused Chromium +fixture. Preserve passing evidence and rerun only the failing section after a +repair. + +Suggested focused checkpoints are: + +1. App Shell row token and rendered topology contracts. +2. Section and Wrapper token contracts. +3. Density and workspace contracts. +4. Local gap and Scroll contracts. +5. Manifest, docs, generated-output, and package contracts. + +### Rendered Matrix + +The focused fixtures cover these dimensions without requiring every possible +Cartesian combination at each checkpoint: + +- widths: `320`, `375`, `768`, `1024`, `1280`, `1440`, `1920` +- heights: `480`, `600`, `704`, `768`, `900`, `1080` +- portrait and landscape phone/tablet cases +- desktop and wide workspace cases +- App Shell base, medium, and wide thresholds +- all personality-specific App Shell signatures +- marketing and dense-application density contexts +- nested sections, wrappers, gaps, and scroll regions + +The final release gate runs Chromium, Firefox, and WebKit through the repository's +existing full browser matrix. In-app Browser verification adds a desktop and +mobile interaction spot check with page identity, meaningful DOM, console, +overlay, screenshot, and control-state evidence. + +### Final Gate Per Release + +Immediately before each release commit and push: + +1. Rebuild all generated CSS and Pages assets. +2. Run ownership checks and Stylelint. +3. Run JavaScript syntax checks. +4. Run static, documentation, manifest, package, and Pages contracts. +5. Run the complete three-browser rendered matrix. +6. Run the immutable ecosystem release preflight. +7. Run dependency audit, pack dry-run, and publish dry-run. +8. Run `git diff --check` and inspect the exact diff and staged scope. + +The full chain runs only at the end of each release candidate. Remote CI and the +protected npm workflow must also pass before the registry result is considered +successful. + +## Compatibility And Migration + +`3.0.2` is a defect-correction patch. It adds tokens but removes no selector, +export, or manifest field. + +`3.1.0` is additive except for three intentional behavioral refinements: + +1. Normal section spacing becomes more conservative. Use + `data-ly-density="spacious"` to retain the former marketing rhythm. +2. `.ly-gap-*` becomes local. Set inherited public gap tokens explicitly when a + shared spacing context is intentional. +3. `.ly-scroll` no longer imposes a maximum height. Add + `.ly-scroll--viewport` to retain the former behavior or + `.ly-scroll--bounded` for a stable application-controlled cap. + +These changes are documented prominently in the changelog and migration guide. +They correct surprising defaults while retaining clear opt-in paths for the old +behavior. No deprecated alias is required because the old behavior remains +expressible through the new explicit contracts. + +## Risks And Mitigations + +### Risk: A Global Row Fix Breaks Personality Topologies + +Mitigation: use row tokens and personality-specific overrides, then render every +App Shell personality at the wide threshold. + +### Risk: Density Erases Personality Intent + +Mitigation: keep personality values as the normal profile source and apply an +explicit density context afterward. Test every personality with no density +attribute and representative compact and spacious contexts. + +### Risk: Local Gap Semantics Surprise Existing Consumers + +Mitigation: document the behavior change, provide the explicit inherited-token +path, and encode nested-gap examples in the demo and migration guide. + +### Risk: Scroll Changes Create Unbounded Page Growth + +Mitigation: test Scroll inside constrained Grid/Flex tracks, provide bounded and +viewport modifiers, and include a long-content fixture at short heights. + +### Risk: Two Releases Become Interleaved + +Mitigation: treat verified npm publication of `3.0.2` as a hard gate before +branching `3.1.0`. Do not prepare or tag both candidates simultaneously. + +### Risk: Protected npm Publication Requires External Approval + +Mitigation: use the repository workflow as designed, report the exact waiting +environment, and resume after approval. Do not bypass provenance or publish from +an unauthenticated local npm session. + +## Acceptance Criteria + +### `3.0.2` + +- App Shell row tracks match base, medium, wide, and personality-specific area + topologies. +- Primary content receives flexible block space while header and footer remain + intrinsic. +- Compact section padding is strictly smaller than normal padding at all tested + heights. +- Overriding either documented public Wrapper gutter token changes rendered + Wrapper padding. +- Authored and generated CSS, manifest, docs, demo, and package contents agree on + version `3.0.2`. +- Focused checks and the final full release gate pass. +- `v3.0.2` is reachable from protected `main`, the GitHub Release succeeds, npm + reports `3.0.2` as `latest`, and the published tarball and provenance are + verified. + +### `3.1.0` + +- Normal section spacing is more conservative than `3.0.2`, and spacious density + restores the former marketing rhythm. +- Compact, normal, and spacious density contexts work at root and nested scopes + across representative heights. +- Workspace Wrapper provides a documented `96rem` application measure. +- All ten gap utilities are local and nested layouts retain their own defaults. +- Scroll, bounded Scroll, and viewport Scroll have distinct rendered behavior. +- Structured recipe threshold metadata matches authored container queries. +- Every public geometry token is runtime-consumed, reaches a runtime consumer as + an alias, or is explicitly extension-only. +- Dense application fixtures make materially better use of `1440x900` and + `1920x1080` viewports without overflow. +- Authored and generated CSS, manifest, docs, demo, and package contents agree on + version `3.1.0`. +- Focused checks and the final full release gate pass. +- `v3.1.0` is reachable from protected `main`, the GitHub Release succeeds, npm + reports `3.1.0` as `latest`, and the published tarball and provenance are + verified. + +The design is complete when this specification is reviewed and the implementation +plan decomposes the work into the `3.0.2` release, registry verification gate, +and subsequent `3.1.0` release without overlapping their mutation or publication +steps. From 64c8e4a620ac177ccae70e753ec365c873725e0d Mon Sep 17 00:00:00 2001 From: Foscat Date: Tue, 25 Aug 2026 15:10:58 -0500 Subject: [PATCH 2/7] docs: plan layout style 3.0.2 and 3.1.0 releases --- ...8-25-layout-style-css-3-0-2-correctness.md | 755 +++++++++ ...-08-25-layout-style-css-3-1-0-usability.md | 1359 +++++++++++++++++ 2 files changed, 2114 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-25-layout-style-css-3-0-2-correctness.md create mode 100644 docs/superpowers/plans/2026-08-25-layout-style-css-3-1-0-usability.md diff --git a/docs/superpowers/plans/2026-08-25-layout-style-css-3-0-2-correctness.md b/docs/superpowers/plans/2026-08-25-layout-style-css-3-0-2-correctness.md new file mode 100644 index 0000000..83553fa --- /dev/null +++ b/docs/superpowers/plans/2026-08-25-layout-style-css-3-0-2-correctness.md @@ -0,0 +1,755 @@ +# Layout Style CSS 3.0.2 Correctness Release Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Publish `layout-style-css@3.0.2` with correct App Shell row geometry, consistently compact sections, and effective public Wrapper gutter tokens. + +**Architecture:** Extend the existing v3 token model without changing selectors, exports, thresholds, or the normal section rhythm. Add row and compact-section tokens in `styles/foundation.css`, consume them through the existing recipe and primitive modules, preserve personality-specific topologies, and verify source, generated CSS, rendered geometry, documentation, and package metadata before the protected GitHub-to-npm release. + +**Tech Stack:** CSS cascade layers and custom properties, Node.js 20/22 ESM, `node:assert`, Playwright 1.61.1, Stylelint 17, npm packaging, GitHub Actions, GitHub CLI, npm provenance. + +**Spec:** `docs/superpowers/specs/2026-08-25-layout-style-css-3-1-usability-design.md` + +## Global Constraints + +- Work inline in the current checkout; do not create a worktree or dispatch sub-agents. +- Preserve the v3 module boundaries: Layout owns structure, UI Style Kit owns paint, and Interactive Surface owns interaction states. +- Keep the package dependency-free at runtime and preserve all 13 package exports. +- Authored CSS lives under `styles/`; rebuild `dist/` and Pages artifacts instead of editing generated output by hand. +- Do not change recipe thresholds `42rem`, `44rem`, `48rem`, `52rem`, or `72rem` in this patch. +- Do not change the `3.0.1` normal section default `clamp(3rem, 7vh, 6rem)` in this patch. +- Add professional JSDoc-compatible comments to every JavaScript function created or modified. +- The repository currently has no jsdoc2md command. If one appears before push, regenerate its granular references before publication. +- Run affected static and Chromium checks at development checkpoints. Run the CI-equivalent full browser and release chain only at the final release gate. +- Preserve unrelated files and inspect the exact staged scope before every commit. +- Stop after npm verification of `3.0.2`; do not start `3.1.0` until the registry reports the patch as `latest`. + +--- + +## File Structure + +### Authored behavior + +- `styles/foundation.css`: public App Shell row tokens, compact section token, short-height token values, and canonical public Wrapper gutter defaults. +- `styles/recipes.css`: base, medium, and wide App Shell row-token consumers. +- `styles/primitives.css`: normal, compact, and flush section padding consumers. +- `styles/wrappers.css`: Wrapper padding derived from `--ly-wrapper-gutter` with safe-area compensation applied once. +- `styles/personalities/bento.css`: four-row wide App Shell override. +- `styles/personalities/neumorphism.css`: four-row wide App Shell override with the flexible `main` row in track two. +- `styles/personalities/split-screen.css`: four-row wide App Shell override. +- `styles/personalities/tactile.css`: four-row wide App Shell override. + +### Contracts and rendered verification + +- `test/layout-css-contract.test.mjs`: static row-token, compact-token, and gutter-consumer contracts. +- `test/manifest-contract.test.mjs`: additive public-token inventory. +- `test/demo-smoke.test.mjs`: computed row geometry, section ordering, footer sizing, and public gutter override checks. +- `test/package-contract.test.mjs`: synchronized patch metadata. +- `test/release-docs-contract.test.mjs`: current release documentation and changelog coverage. +- `test/pages-artifact.test.mjs`: versioned generated URLs and current release metadata. + +### Release metadata and maintained docs + +- `package.json`, `package-lock.json`, `manifest.json`: version `3.0.2` and public token inventory. +- `README.md`, `CHANGELOG.md`: patch behavior and current install examples. +- `docs/wiki/Home.md`, `docs/wiki/Getting-Started.md`, `docs/wiki/Installation-And-CDN.md`: current version references. +- `docs/wiki/Layout-Primitives.md`, `docs/wiki/Layout-Recipes.md`, `docs/wiki/Layout-Styles.md`: corrected section, Wrapper, and App Shell contracts. +- `docs/wiki/Release-And-Publishing.md`: exact `v3.0.2` candidate and publication sequence. +- `demo/index.html`, `demo/sitemap.xml`: demo version URLs, structured metadata, and release date. +- `.github/workflows/npm-publish.yml`: current release-tag example only; publishing behavior stays unchanged. + +### Generated outputs + +- `dist/*.css`, `dist/personalities/*.css`: rebuilt by `npm.cmd run build`. +- `personalities.json`, `demo/personality-metadata.js`: rebuilt by `scripts/build.mjs`. +- `output/github-pages/`: generated and verified by the Pages contract; it remains ignored unless repository policy changes. + +--- + +### Task 1: Correct App Shell Row Geometry + +**Files:** +- Modify: `test/layout-css-contract.test.mjs:221-337` +- Modify: `test/demo-smoke.test.mjs:39-130,567-679,1086-1115` +- Modify: `styles/foundation.css:63-96` +- Modify: `styles/recipes.css:36-48,173-188` +- Modify: `styles/personalities/bento.css` +- Modify: `styles/personalities/neumorphism.css` +- Modify: `styles/personalities/split-screen.css` +- Modify: `styles/personalities/tactile.css` + +**Interfaces:** +- Consumes: existing `--ly-app-shell-*-areas`, `--ly-app-shell-*-columns`, `[data-ly-recipe="app-shell"]`, and `ly-scope` query thresholds. +- Produces: `--ly-app-shell-base-rows`, `--ly-app-shell-medium-rows`, and `--ly-app-shell-wide-rows`; rendered helper `verifyAppShellRowGeometry(page, baseUrl)`. + +- [ ] **Step 1: Add failing static row-token contracts** + +Add the following assertions after the existing recipe threshold assertions in `test/layout-css-contract.test.mjs`: + +```js +const appShellRowTokens = { + base: "--ly-app-shell-base-rows", + medium: "--ly-app-shell-medium-rows", + wide: "--ly-app-shell-wide-rows" +}; + +for (const [topology, token] of Object.entries(appShellRowTokens)) { + assert(foundation.includes(`${token}:`), `App Shell ${topology} rows need a public token.`); + assert( + recipes.includes(`grid-template-rows: var(${token})`), + `App Shell ${topology} topology must consume ${token}.` + ); +} + +for (const personality of ["bento", "neumorphism", "split-screen", "tactile"]) { + assert( + readStyle(`personalities/${personality}.css`).includes("--ly-app-shell-wide-rows:"), + `${personality} must describe its four-row wide App Shell.` + ); +} +``` + +- [ ] **Step 2: Run the static contract and confirm the intended failure** + +Run: + +```powershell +node test/layout-css-contract.test.mjs +``` + +Expected: exit `1` with `App Shell base rows need a public token.` + +- [ ] **Step 3: Add failing rendered geometry coverage** + +Add this JSDoc-compatible helper before `verifyManualAndNearestContainer` in `test/demo-smoke.test.mjs`: + +```js +/** + * Verifies that each automatic App Shell topology exposes one explicit row per + * named area row and gives the primary workspace more height than intrinsic + * header and footer tracks. + * + * @param {import("@playwright/test").Page} page Active browser page. + * @param {string} baseUrl Demo server URL. + * @returns {Promise} + */ +const verifyAppShellRowGeometry = async (page, baseUrl) => { + const cases = [ + { label: "base", width: "51rem", personality: "minimal-saas", rows: 5 }, + { label: "medium", width: "53rem", personality: "minimal-saas", rows: 4 }, + ...personalities.map((personality) => ({ + label: `wide ${personality}`, + width: "73rem", + personality, + rows: ["bento", "neumorphism", "split-screen", "tactile"].includes(personality) + ? 4 + : 3 + })) + ]; + + for (const testCase of cases) { + await page.goto( + `${baseUrl}?ecosystem=layout-only&recipe=app-shell&container=${testCase.width}&personality=${testCase.personality}` + ); + const geometry = await page.locator('[data-ly-recipe="app-shell"]').evaluate((recipe) => { + const style = getComputedStyle(recipe); + const areaRows = style.gridTemplateAreas.match(/"[^"]+"/g) ?? []; + const main = recipe.querySelector('[data-ly-area="main"]').getBoundingClientRect(); + const header = recipe.querySelector('[data-ly-area="header"]').getBoundingClientRect(); + const footer = recipe.querySelector('[data-ly-area="footer"]').getBoundingClientRect(); + return { + areaRows: areaRows.length, + explicitRows: style.gridTemplateRows.split(/\s+/).filter(Boolean).length, + mainHeight: main.height, + headerHeight: header.height, + footerHeight: footer.height + }; + }); + + assert.equal(geometry.areaRows, testCase.rows, `${testCase.label} area-row count drifted.`); + assert.equal( + geometry.explicitRows, + testCase.rows, + `${testCase.label} explicit rows must match its area rows.` + ); + assert( + geometry.mainHeight > geometry.headerHeight && geometry.mainHeight > geometry.footerHeight, + `${testCase.label} must allocate flexible height to the primary workspace.` + ); + } +}; +``` + +Call it immediately after `verifyTopologyEdges(page, server.baseUrl)`. + +- [ ] **Step 4: Run the focused Chromium gate and confirm the geometry failure** + +Run: + +```powershell +npm.cmd run test:demo:quick +``` + +Expected: exit `1` because medium or wide explicit row count remains five. + +- [ ] **Step 5: Define and consume topology row tokens** + +Add these tokens beside the existing App Shell area and column tokens in `styles/foundation.css`: + +```css +--ly-app-shell-base-rows: auto auto minmax(0, 1fr) auto auto; +--ly-app-shell-medium-rows: auto auto minmax(0, 1fr) auto; +--ly-app-shell-wide-rows: auto minmax(0, 1fr) auto; +``` + +Update `styles/recipes.css`: + +```css +[data-ly-recipe="app-shell"] { + min-block-size: 100vh; + min-block-size: var(--ly-shell-min); + grid-template-areas: + "header" + "sidebar" + "main" + "aside" + "footer"; + grid-template-rows: var(--ly-app-shell-base-rows); +} +``` + +Add the matching row consumer to each automatic query: + +```css +@container ly-scope (min-width: 52rem) { + [data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]) { + grid-template-areas: var(--ly-app-shell-medium-areas); + grid-template-columns: var(--ly-app-shell-medium-columns); + grid-template-rows: var(--ly-app-shell-medium-rows); + } +} + +@container ly-scope (min-width: 72rem) { + [data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]) { + grid-template-areas: var(--ly-app-shell-wide-areas); + grid-template-columns: var(--ly-app-shell-wide-columns); + grid-template-rows: var(--ly-app-shell-wide-rows); + } +} +``` + +- [ ] **Step 6: Add four-row personality tokens** + +Add this token beside the App Shell wide-area token in Bento, Split Screen, and Tactile: + +```css +--ly-app-shell-wide-rows: auto auto minmax(0, 1fr) auto; +``` + +Add this token in Neumorphism because its `main` occupies the second row: + +```css +--ly-app-shell-wide-rows: auto minmax(0, 1fr) auto auto; +``` + +- [ ] **Step 7: Rebuild and prove the App Shell slice** + +Run: + +```powershell +npm.cmd run build +node test/layout-css-contract.test.mjs +npm.cmd run test:demo:quick +``` + +Expected: all commands exit `0`; the Chromium output ends with `Layout CSS v3 demo passed in chromium (quick matrix).` + +- [ ] **Step 8: Inspect and commit the row-geometry slice** + +Run: + +```powershell +git diff --check +git diff -- styles/foundation.css styles/recipes.css styles/personalities test/layout-css-contract.test.mjs test/demo-smoke.test.mjs dist +git status --short +git add -- styles/foundation.css styles/recipes.css styles/personalities/bento.css styles/personalities/neumorphism.css styles/personalities/split-screen.css styles/personalities/tactile.css test/layout-css-contract.test.mjs test/demo-smoke.test.mjs dist personalities.json demo/personality-metadata.js +git diff --cached --check +git commit -m "fix: align app shell row geometry" +``` + +Expected: one focused commit with no unrelated files. + +--- + +### Task 2: Separate Section Tokens And Connect Wrapper Gutters + +**Files:** +- Modify: `test/layout-css-contract.test.mjs:238-300` +- Modify: `test/manifest-contract.test.mjs:48-68,205-227` +- Modify: `test/demo-smoke.test.mjs:680-804,882-916,1086-1115` +- Modify: `styles/foundation.css:20-45,123-193` +- Modify: `styles/primitives.css:28-41` +- Modify: `styles/wrappers.css:6-27` +- Modify: `manifest.json:73-95` + +**Interfaces:** +- Consumes: `--ly-section-padding-block`, `--ly-page-padding-inline`, `--ly-safe-area-inline`, and existing Wrapper calculation variables. +- Produces: `--ly-section-padding-block-compact`; runtime-consumed `--ly-wrapper-gutter`; rendered helper `verifySectionAndGutterContracts(browser, baseUrl)`. + +- [ ] **Step 1: Add failing source and manifest contracts** + +Add the compact token to `geometryTokens` in `test/manifest-contract.test.mjs`, immediately after `--ly-section-padding-block`. + +Replace the existing Wrapper gutter source assertion in `test/layout-css-contract.test.mjs` with: + +```js +assert( + /--ly-wrapper-gutter:\s*var\(--ly-page-padding-inline\)/.test(foundation) && + /--ly-wrapper-fluid-gutter:\s*var\(--ly-wrapper-gutter\)/.test(wrappers) && + /padding-inline:\s*var\(--ly-wrapper-local-gutter\)/.test(wrappers), + "Public Wrapper gutter tokens must reach rendered Wrapper padding." +); +assert( + /--ly-section-padding-block-compact:\s*clamp\(1\.5rem,\s*3\.5vh,\s*3rem\)/.test( + foundation + ) && + /\.ly-section--compact\s*\{[^}]*padding-block:\s*var\(--ly-section-padding-block-compact\)/s.test( + primitives + ), + "Compact sections need an independent public padding token." +); +``` + +- [ ] **Step 2: Run the affected static contracts and confirm failure** + +Run: + +```powershell +node test/layout-css-contract.test.mjs +node --test test/manifest-contract.test.mjs +``` + +Expected: both commands exit `1` because the compact token and gutter consumer are absent. + +- [ ] **Step 3: Add failing rendered section and gutter checks** + +Add a helper that creates isolated browser pages at heights `1080`, `704`, and `480`, inserts normal and compact sections, and verifies `compactPadding < normalPadding`. On the `1080` page, also insert a Wrapper and perform these two root overrides in order: + +```js +document.body.style.setProperty("--ly-page-padding-inline", "22px"); +document.body.style.removeProperty("--ly-wrapper-gutter"); +``` + +Expected Wrapper inline padding: `22px`. + +Then apply: + +```js +document.body.style.setProperty("--ly-wrapper-gutter", "34px"); +``` + +Expected Wrapper inline padding: `34px`. + +Name the JSDoc-compatible helper: + +```js +verifySectionAndGutterContracts(browser, baseUrl) +``` + +Call it after `verifyHeightBehavior(page, server.baseUrl)`. + +- [ ] **Step 4: Run the focused Chromium gate and confirm failure** + +Run: + +```powershell +npm.cmd run test:demo:quick +``` + +Expected: exit `1` because the short-height compact section is not smaller than normal or the public gutter override does not reach padding. + +- [ ] **Step 5: Implement separate normal and compact section tokens** + +In the root token block in `styles/foundation.css`, retain the normal token and add: + +```css +--ly-section-padding-block: clamp(3rem, 7vh, 6rem); +--ly-section-padding-block-compact: clamp(1.5rem, 3.5vh, 3rem); +``` + +At `max-height: 44rem`, set both fallbacks: + +```css +--ly-section-padding-block: clamp(1.5rem, 6vh, 3rem); +--ly-section-padding-block-compact: clamp(0.75rem, 3vh, 1.25rem); +``` + +Inside the matching dynamic-viewport support block, use `6dvh` and `3dvh`. + +At `max-height: 30rem`, set: + +```css +--ly-section-padding-block: var(--ly-space-5); +--ly-section-padding-block-compact: var(--ly-space-3); +``` + +Update `styles/primitives.css`: + +```css +.ly-section--compact { + padding-block: var(--ly-section-padding-block-compact); +} + +.ly-section--flush { + padding-block: 0; +} +``` + +- [ ] **Step 6: Route both public gutter tokens to Wrapper padding** + +In `styles/foundation.css`, define: + +```css +--ly-page-padding-inline: clamp(1rem, 3vw, 3rem); +--ly-wrapper-gutter: var(--ly-page-padding-inline); +``` + +Retain safe-area tokens separately. Add a feature-detected root enhancement: + +```css +@supports (width: 1cqi) { + :where(.ly-root) { + --ly-page-padding-inline: clamp(1rem, 3cqi, 3rem); + } +} +``` + +Update the Wrapper calculation in `styles/wrappers.css` and remove its old local `@supports` override: + +```css +.ly-wrapper { + --ly-wrapper-max: var(--ly-personality-wrapper-max, var(--ly-wrapper-content)); + --ly-wrapper-fluid-gutter: var(--ly-wrapper-gutter); + --ly-wrapper-local-gutter: max( + var(--ly-wrapper-fluid-gutter), + var(--ly-safe-area-inline) + ); +``` + +This keeps safe-area compensation in `--ly-wrapper-local-gutter` exactly once. + +- [ ] **Step 7: Update the manifest and rebuild** + +Add `--ly-section-padding-block-compact` after `--ly-section-padding-block` in `manifest.json`, then run: + +```powershell +npm.cmd run build +``` + +Expected: generated CSS and personality metadata rebuild successfully. + +- [ ] **Step 8: Prove the section and gutter slice** + +Run: + +```powershell +node test/layout-css-contract.test.mjs +node --test test/manifest-contract.test.mjs +npm.cmd run test:demo:quick +npm.cmd run lint +``` + +Expected: all commands exit `0`. + +- [ ] **Step 9: Inspect and commit the token slice** + +Run: + +```powershell +git diff --check +git diff -- styles/foundation.css styles/primitives.css styles/wrappers.css manifest.json test dist +git status --short +git add -- styles/foundation.css styles/primitives.css styles/wrappers.css manifest.json test/layout-css-contract.test.mjs test/manifest-contract.test.mjs test/demo-smoke.test.mjs dist personalities.json demo/personality-metadata.js +git diff --cached --check +git commit -m "fix: make section and gutter tokens effective" +``` + +Expected: one focused commit with no version or release-document changes yet. + +--- + +### Task 3: Prepare The 3.0.2 Release Candidate + +**Files:** +- Modify: `package.json:1-5` +- Modify: `package-lock.json:1-12` +- Modify: `manifest.json:1-10` +- Modify: `CHANGELOG.md:1-25` +- Modify: `README.md:1-20,170-185` +- Modify: `demo/index.html:1-100` +- Modify: `demo/sitemap.xml` +- Modify: `docs/wiki/Home.md` +- Modify: `docs/wiki/Getting-Started.md` +- Modify: `docs/wiki/Installation-And-CDN.md` +- Modify: `docs/wiki/Layout-Primitives.md` +- Modify: `docs/wiki/Layout-Recipes.md` +- Modify: `docs/wiki/Layout-Styles.md` +- Modify: `docs/wiki/Release-And-Publishing.md` +- Modify: `.github/workflows/npm-publish.yml:1-15` +- Modify: `test/layout-css-contract.test.mjs:100-115` +- Modify: `test/manifest-contract.test.mjs:77-100` +- Modify: `test/package-contract.test.mjs:112-139` +- Modify: `test/release-docs-contract.test.mjs:1-30,233-326` +- Modify: `test/pages-artifact.test.mjs:75-130` +- Modify: `test/demo-smoke.test.mjs:449-566` + +**Interfaces:** +- Consumes: verified patch behavior from Tasks 1 and 2. +- Produces: one internally consistent `3.0.2` candidate, release documentation, rebuilt generated assets, and version-sensitive contracts. + +- [ ] **Step 1: Make version-sensitive tests expect 3.0.2** + +Update exact current-version assertions in the five version-sensitive test files. Keep historical `3.0.1` changelog coverage and add a separate `3.0.2` section assertion: + +```js +assert.equal(packageJson.version, "3.0.2", "The patch branch must expose version 3.0.2"); +assert.equal(manifest.version, "3.0.2"); +assert.equal(packageLock.version, "3.0.2"); +assert.equal(packageLock.packages[""].version, "3.0.2"); +``` + +Require one changelog heading: + +```js +const currentPatchHeadings = changelog.match(/^## \[3\.0\.2\] - 2026-08-25$/gm) ?? []; +assert.equal(currentPatchHeadings.length, 1, "Changelog needs one dated 3.0.2 section."); +``` + +Require the current release guide to contain `layout-style-css@3.0.2` and `v3.0.2`. + +- [ ] **Step 2: Run version-sensitive contracts and confirm failure** + +Run: + +```powershell +node test/layout-css-contract.test.mjs +node --test test/manifest-contract.test.mjs test/package-contract.test.mjs +node test/release-docs-contract.test.mjs +node test/pages-artifact.test.mjs +``` + +Expected: failures report the still-current `3.0.1` metadata and missing `3.0.2` changelog section. + +- [ ] **Step 3: Update package, lockfile, manifest, and demo metadata** + +Change only the root package versions in `package.json`, `package-lock.json`, and `manifest.json` to `3.0.2`; do not alter dependency versions such as `fast-uri@^3.0.1`. + +Replace current demo asset and metadata versions with `3.0.2`: + +```html + + + + +``` + +Update the module script, personality metadata URL, JSON-LD version, demo tests, Pages fingerprints, and `demo/sitemap.xml` last modification date to `2026-08-25`. + +- [ ] **Step 4: Add the 3.0.2 changelog entry** + +Insert this release summary above `3.0.1`: + +```markdown +## [3.0.2] - 2026-08-25 + +### Fixed + +- Matched App Shell row tracks to base, medium, wide, and personality-specific area topologies so primary content receives flexible height while headers and footers remain intrinsic. +- Separated compact section spacing from normal section spacing and preserved the compact-smaller-than-normal invariant across regular, short, and shallow viewport heights. +- Connected the documented page-padding and Wrapper-gutter tokens to rendered Wrapper padding while retaining safe-area handling. + +### Tests + +- Added static and rendered regression coverage for App Shell row counts, flexible workspace allocation, compact section ordering, public gutter overrides, generated CSS parity, package metadata, and Pages artifacts. +``` + +- [ ] **Step 5: Update maintained user and release documentation** + +Update current install/CDN examples to `3.0.2`. Document these exact contracts: + +```text +--ly-section-padding-block controls normal sections. +--ly-section-padding-block-compact controls compact sections. +--ly-page-padding-inline supplies the default --ly-wrapper-gutter value. +--ly-wrapper-gutter controls rendered Wrapper padding. +App Shell base, medium, and wide topologies own matching row definitions. +``` + +Update `docs/wiki/Release-And-Publishing.md` to inspect package version `3.0.2`, tag `v3.0.2`, and the unchanged protected release process. Update the workflow input example to `v3.0.2` without changing workflow behavior. + +- [ ] **Step 6: Rebuild all maintained generated artifacts** + +Run: + +```powershell +npm.cmd run build +npm.cmd run pages:build +``` + +Expected: the build reports generated CSS files and the Pages output contains fingerprinted `3.0.2` asset URLs. + +- [ ] **Step 7: Run focused candidate contracts** + +Run: + +```powershell +node test/layout-css-contract.test.mjs +node --test --test-concurrency=1 test/manifest-contract.test.mjs test/package-contract.test.mjs +node test/release-docs-contract.test.mjs +node test/pages-artifact.test.mjs +npm.cmd run lint +npm.cmd run check:demo-js +git diff --check +``` + +Expected: all commands exit `0`. + +- [ ] **Step 8: Inspect generated parity and commit the candidate** + +Run: + +```powershell +git status --short +git diff --stat +git diff -- package.json package-lock.json manifest.json CHANGELOG.md README.md demo docs .github test styles dist +git add -- package.json package-lock.json manifest.json CHANGELOG.md README.md demo docs/wiki .github/workflows/npm-publish.yml test styles dist personalities.json +git diff --cached --check +git diff --cached --stat +git commit -m "chore: prepare layout style 3.0.2" +``` + +Expected: the design and implementation-plan commits remain intact, and the release-candidate commit contains only `3.0.2` behavior, generated output, tests, and maintained docs. + +--- + +### Task 4: Verify, Merge, And Publish 3.0.2 + +**Files:** +- Verify only: complete repository and generated package. +- External mutations: release branch, pull request, protected `main`, tag `v3.0.2`, GitHub Release, npm registry. + +**Interfaces:** +- Consumes: clean committed `3.0.2` candidate from Task 3. +- Produces: merged and immutable `v3.0.2`, successful protected publish workflow, npm `latest=3.0.2`, and registry evidence required by the `3.1.0` plan. + +- [ ] **Step 1: Confirm local release preconditions** + +Run: + +```powershell +git status --short --branch +git log --oneline --decorate origin/main..HEAD +node -e "const p=require('./package.json'); if(p.version!=='3.0.2') process.exit(1)" +& 'C:\Program Files\GitHub CLI\gh.exe' auth status +``` + +Expected: clean branch `codex/layout-3.0.2-correctness`, intended commits only, version `3.0.2`, and authenticated GitHub CLI. + +- [ ] **Step 2: Run the one final local release gate** + +Run: + +```powershell +npm.cmd run release:verify +git diff --check +git status --short +``` + +Expected: `release:verify` exits `0`, all three browser engines pass, the ecosystem fixture passes, audit reports no moderate-or-higher vulnerabilities, pack and publish dry-runs pass, and the tree remains clean. + +If a section fails, diagnose and rerun only that section until it passes; rerun `release:verify` once after the repair because publication requires fresh integrated evidence. + +- [ ] **Step 3: Push the branch and open the patch PR** + +Run: + +```powershell +git push -u origin codex/layout-3.0.2-correctness +& 'C:\Program Files\GitHub CLI\gh.exe' pr create --base main --head codex/layout-3.0.2-correctness --title 'fix: release layout style 3.0.2' --body 'Corrects App Shell row allocation, compact section semantics, and public Wrapper gutter behavior. Includes generated CSS, documentation, package contracts, and rendered regression coverage.' +``` + +Expected: one PR URL targeting `main`. + +- [ ] **Step 4: Wait for final PR checks and merge** + +Run: + +```powershell +& 'C:\Program Files\GitHub CLI\gh.exe' pr checks --watch +& 'C:\Program Files\GitHub CLI\gh.exe' pr merge --merge --delete-branch +``` + +Expected: all required checks pass and the PR merges with a merge commit reachable from `origin/main`. + +- [ ] **Step 5: Sync and tag the protected release commit** + +Run: + +```powershell +git switch main +git pull --ff-only origin main +node -e "const p=require('./package.json'); if(p.version!=='3.0.2') process.exit(1)" +git merge-base --is-ancestor HEAD origin/main +git tag -a v3.0.2 -m "layout-style-css 3.0.2" +git push origin v3.0.2 +``` + +Expected: local `main` equals the merged release commit and immutable tag `v3.0.2` points to it. + +- [ ] **Step 6: Publish the GitHub Release and monitor npm publication** + +Run: + +```powershell +& 'C:\Program Files\GitHub CLI\gh.exe' release create v3.0.2 --title 'layout-style-css 3.0.2' --generate-notes --verify-tag +$layoutPatchRunId = & 'C:\Program Files\GitHub CLI\gh.exe' run list --workflow npm-publish.yml --event release --limit 1 --json databaseId --jq '.[0].databaseId' +if (-not $layoutPatchRunId) { throw 'No npm publish run was created for v3.0.2.' } +& 'C:\Program Files\GitHub CLI\gh.exe' run watch $layoutPatchRunId --exit-status +``` + +If the protected `npm` environment waits for a reviewer, report that exact gate and wait for approval. Do not publish locally. + +- [ ] **Step 7: Verify GitHub and npm independently** + +Run: + +```powershell +& 'C:\Program Files\GitHub CLI\gh.exe' release view v3.0.2 --json tagName,isDraft,isPrerelease,publishedAt,url,targetCommitish +npm.cmd view layout-style-css@3.0.2 version dist-tags dist.integrity dist.shasum --json +npm.cmd view layout-style-css version dist-tags --json +npm.cmd pack layout-style-css@3.0.2 --dry-run --json +git ls-remote --tags origin refs/tags/v3.0.2 +``` + +Expected: + +```text +GitHub Release is published, not draft, and not prerelease. +npm package version is 3.0.2. +npm latest dist-tag is 3.0.2. +The tarball contains the maintained v3 exports, manifest, personality metadata, README, license, changelog, and wiki docs. +Remote tag v3.0.2 resolves successfully. +``` + +- [ ] **Step 8: Record the hard handoff to 3.1.0** + +Confirm: + +```powershell +git status --short --branch +git rev-parse HEAD +git rev-parse origin/main +npm.cmd view layout-style-css version +``` + +Expected: local and remote `main` SHAs match, the worktree is clean, and npm prints `3.0.2`. Only then begin `docs/superpowers/plans/2026-08-25-layout-style-css-3-1-0-usability.md`. diff --git a/docs/superpowers/plans/2026-08-25-layout-style-css-3-1-0-usability.md b/docs/superpowers/plans/2026-08-25-layout-style-css-3-1-0-usability.md new file mode 100644 index 0000000..56681ea --- /dev/null +++ b/docs/superpowers/plans/2026-08-25-layout-style-css-3-1-0-usability.md @@ -0,0 +1,1359 @@ +# Layout Style CSS 3.1.0 Usability Release Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Publish `layout-style-css@3.1.0` with conservative defaults, local density contexts, a workspace measure, local gap utilities, explicit Scroll constraints, and machine-readable recipe thresholds. + +**Architecture:** Build additively on the published `3.0.2` patch. Add a final `ly.context` cascade layer for explicit density profiles, extend existing Wrapper and utility modules, split Scroll overflow from height constraints, enrich the public manifest, and drive the existing Interactive Layout Lab from the package contracts instead of demo-only spacing overrides. + +**Tech Stack:** CSS cascade layers, container queries and custom properties, Node.js 20/22 ESM, css-tree 3.2.1, `node:assert`, Playwright 1.61.1, Stylelint 17, npm packaging, GitHub Actions, GitHub CLI, npm provenance. + +**Spec:** `docs/superpowers/specs/2026-08-25-layout-style-css-3-1-usability-design.md` + +## Global Constraints + +- Begin only after npm reports `layout-style-css@3.0.2` and `latest=3.0.2`. +- Create `codex/layout-3.1.0-usability` from protected `main` containing the merged `v3.0.2` release. +- Work inline in the current checkout; do not create a worktree or dispatch sub-agents. +- Preserve the v3 module boundaries and all 13 package exports. +- Keep zero runtime dependencies and do not change companion-package ranges without ecosystem-preflight evidence. +- Keep recipe thresholds `42rem`, `44rem`, `48rem`, `52rem`, and `72rem`; expose them structurally rather than changing them. +- Authored CSS lives under `styles/`; regenerate `dist/`, personality metadata, and Pages output through repository scripts. +- Add professional JSDoc-compatible comments to every JavaScript function created or modified. +- The repository currently has no jsdoc2md command. If one appears before push, regenerate its granular references before publication. +- Use focused static and Chromium checks after each slice. Run the full three-browser and release chain only at the final `3.1.0` gate. +- Preserve unlayered consumer CSS as stronger than every library layer. +- Keep Wrapper/Measure/Scope decomposition out of v3. +- Do not modify downstream application repositories. + +--- + +## File Structure + +### Cascade and authored modules + +- `styles/foundation.css`: conservative normal tokens, density profiles in `ly.context`, workspace and Scroll tokens, short-height behavior. +- `styles/wrappers.css`: `ly-wrapper--workspace` measure consumer. +- `styles/primitives.css`: unconstrained, bounded, and viewport-relative Scroll contracts. +- `styles/utilities.css`: local `.ly-gap-0` through `.ly-gap-9` declarations. +- `styles/core.css`, `styles/personalities.css`, `styles/recipes.css`, and every file under `styles/personalities/`: shared cascade prelude extended with final `ly.context` layer. +- `scripts/build.mjs`: shared cascade prelude and generated metadata updated for `data-ly-density`. + +### Demo and documentation + +- `demo/index.html`: `normal` density, workspace Wrapper option, and `3.1.0` metadata. +- `demo/demo.js`: density allowlist and URL compatibility, `data-ly-density` state, workspace option, explicit bounded Scroll fixture. +- `demo/demo.css`: demo-only density attribute renamed and preview Scroll variables aligned with the public API. +- `README.md`, `CHANGELOG.md`, and maintained wiki docs: new APIs, behavioral changes, migration, and release instructions. +- `docs/wiki/Migrating-To-3.1.md`: exact migration paths for section rhythm, gap locality, and Scroll constraints. +- `docs/wiki/_Sidebar.md`: new migration-guide link. + +### Manifest and tests + +- `manifest.json`: workspace, density, Scroll, row, and structured recipe-threshold contracts; public-token accountability metadata. +- `test/layout-css-contract.test.mjs`: cascade prelude, density, workspace, gap, and Scroll source contracts. +- `test/manifest-contract.test.mjs`: structured thresholds and transitive runtime token-consumer analysis. +- `test/demo-smoke.test.mjs`: density, workspace utilization, nested gaps, Scroll variants, and compatibility URL behavior. +- `test/package-contract.test.mjs`, `test/release-docs-contract.test.mjs`, `test/pages-artifact.test.mjs`: version, package, docs, and Pages release contracts. + +### Generated outputs + +- `dist/*.css`, `dist/personalities/*.css`, `personalities.json`, and `demo/personality-metadata.js`: rebuilt by `npm.cmd run build`. +- `output/github-pages/`: rebuilt and checked through `test/pages-artifact.test.mjs`. + +--- + +### Task 1: Establish The 3.1 Branch And Contextual Density + +**Files:** +- Modify: `test/layout-css-contract.test.mjs:13-20,221-348` +- Modify: `test/demo-smoke.test.mjs:131-192,680-804,917-969,1086-1115` +- Modify: `styles/foundation.css` +- Modify: `styles/core.css` +- Modify: `styles/wrappers.css` +- Modify: `styles/primitives.css` +- Modify: `styles/recipes.css` +- Modify: `styles/utilities.css` +- Modify: `styles/personalities.css` +- Modify: all 16 files under `styles/personalities/` +- Modify: `scripts/build.mjs:5-18,19-44` +- Modify: `demo/index.html:97,208-214,294-320` +- Modify: `demo/demo.js:154-255,329-363,660-689` +- Modify: `demo/demo.css:18-55,350-382` +- Modify: `test/manifest-contract.test.mjs:102-159` + +**Interfaces:** +- Consumes: published `3.0.2` compact-section token and personality `--ly-profile-gap` values. +- Produces: final `ly.context` layer; `data-ly-density="compact|normal|spacious"`; query alias `comfortable -> normal`; generated metadata selector inventory including `data-ly-density`. + +- [ ] **Step 1: Verify the published dependency and create the minor branch** + +Run: + +```powershell +npm.cmd view layout-style-css version dist-tags --json +git switch main +git pull --ff-only origin main +git status --short --branch +git switch -c codex/layout-3.1.0-usability +``` + +Expected: npm reports version and `latest` as `3.0.2`; `main` is clean and current; the new branch starts at the merged patch release. + +- [ ] **Step 2: Add failing cascade and density source contracts** + +Change the expected shared prelude in `test/layout-css-contract.test.mjs` to: + +```js +const layerPrelude = + "@layer ly.reset, ly.tokens, ly.wrappers, ly.primitives, ly.recipes, ly.utilities, ly.personalities, ly.context;"; +``` + +Add these assertions: + +```js +for (const density of ["compact", "normal", "spacious"]) { + assert( + foundation.includes(`[data-ly-density="${density}"]`), + `Foundation must implement the ${density} density context.` + ); +} +assert( + foundation.includes("@layer ly.context"), + "Explicit density contexts must live after personality defaults." +); +assert( + /--ly-section-padding-block:\s*clamp\(2rem,\s*4vh,\s*4rem\)/.test(foundation), + "Normal 3.1 sections need the conservative default rhythm." +); +``` + +In `test/manifest-contract.test.mjs`, expect generated metadata selectors: + +```js +[ + "data-ly-layout", + "data-ly-density", + "data-ui", + "data-theme", + "data-mode" +] +``` + +- [ ] **Step 3: Add failing rendered density contracts** + +Add a JSDoc-compatible helper `verifyDensityContexts(browser, baseUrl)` that creates pages at heights `900`, `600`, and `480`, inserts a root Stack with normal and compact sections, then nests an explicit `normal` density inside a `compact` density. + +For each height, collect these computed values with `parseFloat`: + +```js +{ + compactGap, + normalGap, + spaciousGap, + compactSectionPadding, + normalSectionPadding, + spaciousSectionPadding, + nestedNormalGap +} +``` + +Assert: + +```js +assert(compactGap < normalGap && normalGap < spaciousGap); +assert(compactSectionPadding < normalSectionPadding); +assert(normalSectionPadding < spaciousSectionPadding); +assert.equal(nestedNormalGap, normalGap); +``` + +Add a URL compatibility assertion in `verifyIdentityAndControls`: + +```js +await page.goto(`${baseUrl}?density=comfortable`); +assert.equal(await page.locator("#densitySelect").inputValue(), "normal"); +assert.equal(await page.locator("#previewRoot").getAttribute("data-ly-density"), "normal"); +``` + +Call `verifyDensityContexts(browser, server.baseUrl)` after the existing height checks. + +- [ ] **Step 4: Run focused checks and confirm the missing feature** + +Run: + +```powershell +node test/layout-css-contract.test.mjs +node --test test/manifest-contract.test.mjs +npm.cmd run test:demo:quick +``` + +Expected: failures identify the missing `ly.context` prelude, density selectors, generated selector metadata, and `normal` demo value. + +- [ ] **Step 5: Extend the shared cascade prelude** + +Change the first line of every authored CSS module and `cascadeLayerPrelude` in `scripts/build.mjs` to: + +```css +@layer ly.reset, ly.tokens, ly.wrappers, ly.primitives, ly.recipes, ly.utilities, ly.personalities, ly.context; +``` + +This is a mechanical shared-contract update; do not alter personality declarations while changing their prelude. + +- [ ] **Step 6: Add JSDoc to the modified build helper and density metadata** + +Add this comment above `buildPersonalityMetadata` in `scripts/build.mjs`: + +```js +/** + * Builds generated personality metadata from the public package manifest. + * + * @param {Record} sourceManifest Parsed public manifest. + * @returns {{schemaVersion: number, generatedFrom: string, selector: string, independentSelectors: string[], personalities: object[]}} + * @throws {Error} When personality records are incomplete or inconsistent. + */ +``` + +Set the generated selector list to: + +```js +independentSelectors: [ + "data-ly-layout", + "data-ly-density", + "data-ui", + "data-theme", + "data-mode" +] +``` + +- [ ] **Step 7: Implement conservative defaults and density contexts** + +Change the root defaults in `styles/foundation.css`: + +```css +--ly-section-padding-block: clamp(2rem, 4vh, 4rem); +--ly-section-padding-block-compact: clamp(1rem, 2.5vh, 2rem); +``` + +Retain the `3.0.2` height-tier invariant, with these normal-profile caps: + +```css +@media (max-height: 44rem) { + :where(.ly-root) { + --ly-section-padding-block: clamp(1.25rem, 4vh, 2rem); + --ly-section-padding-block-compact: clamp(0.75rem, 2vh, 1.25rem); + } + + @supports (height: 100dvh) { + :where(.ly-root) { + --ly-section-padding-block: clamp(1.25rem, 4dvh, 2rem); + --ly-section-padding-block-compact: clamp(0.75rem, 2dvh, 1.25rem); + } + } +} + +@media (max-height: 30rem) { + :where(.ly-root) { + --ly-section-padding-block: var(--ly-space-4); + --ly-section-padding-block-compact: var(--ly-space-2); + } +} +``` + +Add the final context layer: + +```css +@layer ly.context { + :where( + .ly-root[data-ly-density="compact"], + .ly-root [data-ly-density="compact"] + ) { + --ly-gap: var(--ly-space-3); + --ly-grid-gap: var(--ly-space-3); + --ly-stack-gap: var(--ly-space-3); + --ly-cluster-gap: var(--ly-space-2); + --ly-section-padding-block: clamp(1rem, 2.5vh, 2rem); + --ly-section-padding-block-compact: clamp(0.5rem, 1.5vh, 1rem); + } + + :where( + .ly-root[data-ly-density="normal"], + .ly-root [data-ly-density="normal"] + ) { + --ly-gap: var(--ly-profile-gap); + --ly-grid-gap: var(--ly-gap); + --ly-stack-gap: var(--ly-space-4); + --ly-cluster-gap: var(--ly-space-3); + --ly-section-padding-block: clamp(2rem, 4vh, 4rem); + --ly-section-padding-block-compact: clamp(1rem, 2.5vh, 2rem); + } + + :where( + .ly-root[data-ly-density="spacious"], + .ly-root [data-ly-density="spacious"] + ) { + --ly-gap: max(var(--ly-profile-gap), var(--ly-space-6)); + --ly-grid-gap: var(--ly-gap); + --ly-stack-gap: var(--ly-space-5); + --ly-cluster-gap: var(--ly-space-4); + --ly-section-padding-block: clamp(3rem, 7vh, 6rem); + --ly-section-padding-block-compact: clamp(1.5rem, 3.5vh, 3rem); + } +} +``` + +Add `44rem` and `30rem` media blocks inside `ly.context` so all three explicit profiles retain the ordering asserted by the rendered test. Use dynamic viewport units in matching `@supports (height: 100dvh)` blocks. + +Use these exact fallback values at `44rem`: + +```css +@media (max-height: 44rem) { + :where( + .ly-root[data-ly-density="compact"], + .ly-root [data-ly-density="compact"] + ) { + --ly-gap: var(--ly-space-3); + --ly-grid-gap: var(--ly-space-3); + --ly-stack-gap: var(--ly-space-3); + --ly-cluster-gap: var(--ly-space-2); + --ly-section-padding-block: clamp(0.75rem, 2vh, 1.25rem); + --ly-section-padding-block-compact: var(--ly-space-2); + } + + :where( + .ly-root[data-ly-density="normal"], + .ly-root [data-ly-density="normal"] + ) { + --ly-gap: min(var(--ly-profile-gap), var(--ly-space-4)); + --ly-grid-gap: var(--ly-gap); + --ly-stack-gap: var(--ly-space-4); + --ly-cluster-gap: var(--ly-space-3); + --ly-section-padding-block: clamp(1.25rem, 4vh, 2rem); + --ly-section-padding-block-compact: clamp(0.75rem, 2vh, 1.25rem); + } + + :where( + .ly-root[data-ly-density="spacious"], + .ly-root [data-ly-density="spacious"] + ) { + --ly-gap: var(--ly-space-5); + --ly-grid-gap: var(--ly-space-5); + --ly-stack-gap: var(--ly-space-4); + --ly-cluster-gap: var(--ly-space-3); + --ly-section-padding-block: clamp(1.5rem, 6vh, 3rem); + --ly-section-padding-block-compact: clamp(0.75rem, 3vh, 1.25rem); + } +} +``` + +Add this dynamic-viewport enhancement inside the same `44rem` media query: + +```css +@supports (height: 100dvh) { + :where( + .ly-root[data-ly-density="compact"], + .ly-root [data-ly-density="compact"] + ) { + --ly-section-padding-block: clamp(0.75rem, 2dvh, 1.25rem); + } + + :where( + .ly-root[data-ly-density="normal"], + .ly-root [data-ly-density="normal"] + ) { + --ly-section-padding-block: clamp(1.25rem, 4dvh, 2rem); + --ly-section-padding-block-compact: clamp(0.75rem, 2dvh, 1.25rem); + } + + :where( + .ly-root[data-ly-density="spacious"], + .ly-root [data-ly-density="spacious"] + ) { + --ly-section-padding-block: clamp(1.5rem, 6dvh, 3rem); + --ly-section-padding-block-compact: clamp(0.75rem, 3dvh, 1.25rem); + } +} +``` + +Use these exact values at `30rem`: + +```css +@media (max-height: 30rem) { + :where( + .ly-root[data-ly-density="compact"], + .ly-root [data-ly-density="compact"] + ) { + --ly-gap: var(--ly-space-2); + --ly-grid-gap: var(--ly-space-2); + --ly-stack-gap: var(--ly-space-2); + --ly-cluster-gap: var(--ly-space-1); + --ly-section-padding-block: var(--ly-space-3); + --ly-section-padding-block-compact: var(--ly-space-2); + } + + :where( + .ly-root[data-ly-density="normal"], + .ly-root [data-ly-density="normal"] + ) { + --ly-gap: var(--ly-space-3); + --ly-grid-gap: var(--ly-space-3); + --ly-stack-gap: var(--ly-space-3); + --ly-cluster-gap: var(--ly-space-2); + --ly-section-padding-block: var(--ly-space-4); + --ly-section-padding-block-compact: var(--ly-space-2); + } + + :where( + .ly-root[data-ly-density="spacious"], + .ly-root [data-ly-density="spacious"] + ) { + --ly-gap: var(--ly-space-4); + --ly-grid-gap: var(--ly-space-4); + --ly-stack-gap: var(--ly-space-4); + --ly-cluster-gap: var(--ly-space-3); + --ly-section-padding-block: var(--ly-space-5); + --ly-section-padding-block-compact: var(--ly-space-3); + } +} +``` + +- [ ] **Step 8: Replace demo-only preview density with the public contract** + +Use `normal` in `demo/index.html`, rename the body-only demo attribute to `data-demo-density`, and set `data-ly-density="normal"` on `#previewRoot`. + +In `demo/demo.js`: + +```js +density: Object.freeze(["compact", "normal", "spacious"]) +``` + +Set the default to `normal`, remove `DENSITY_GAPS`, and replace the two inline gap overrides with: + +```js +body.dataset.demoDensity = state.density; +previewRoot.dataset.lyDensity = state.density; +``` + +Add this JSDoc-compatible normalizer and call it from `readStateFromQuery` before allowlist validation: + +```js +/** + * Maps durable demo query aliases to current allowlisted values. + * + * @param {string} key Query-state key. + * @param {string | null} value Raw query value. + * @returns {string | null} Current value or the unchanged input. + */ +function normalizeQueryValue(key, value) { + return key === "density" && value === "comfortable" ? "normal" : value; +} +``` + +Add JSDoc-compatible comments to the modified state functions: + +```js +/** + * Reads allowlisted demo state from the current query string. + * + * @returns {Record} Normalized demo state. + */ +``` + +Place it above `readStateFromQuery`. + +```js +/** + * Applies current demo state to controls, public layout attributes, preview + * allocation, rendered recipe content, snippets, and URL state. + * + * @param {{updateQuery?: boolean}} [options] State-application options. + * @returns {void} + */ +``` + +Place it above `applyState` and change the signature to: + +```js +function applyState(options = {}) { + const { updateQuery = true } = options; +``` + +Keep the existing function body after the destructuring line. + +Update `demo/demo.css` selectors from `body[data-density]` to `body[data-demo-density]` so demo chrome remains independent from package density behavior. + +- [ ] **Step 9: Rebuild and prove density behavior** + +Run: + +```powershell +npm.cmd run build +node test/layout-css-contract.test.mjs +node --test test/manifest-contract.test.mjs +npm.cmd run test:demo:quick +npm.cmd run lint +npm.cmd run check:demo-js +git diff --check +``` + +Expected: every command exits `0`; old `?density=comfortable` URLs render the normal public density. + +- [ ] **Step 10: Inspect and commit the density slice** + +Run: + +```powershell +git status --short +git diff --stat +git add -- styles scripts/build.mjs demo/demo.js demo/demo.css demo/index.html test/layout-css-contract.test.mjs test/manifest-contract.test.mjs test/demo-smoke.test.mjs dist personalities.json demo/personality-metadata.js +git diff --cached --check +git commit -m "feat: add contextual layout density" +``` + +Expected: one density-focused commit with the shared prelude update and generated parity. + +--- + +### Task 2: Add The Workspace Wrapper + +**Files:** +- Modify: `test/layout-css-contract.test.mjs:250-271` +- Modify: `test/manifest-contract.test.mjs:48-68,161-203` +- Modify: `test/demo-smoke.test.mjs:48-60,863-916,1086-1115` +- Modify: `styles/foundation.css:20-35` +- Modify: `styles/wrappers.css:29-48` +- Modify: `manifest.json:20-28,73-95` +- Modify: `demo/index.html:140-150` +- Modify: `demo/demo.js:154-170` + +**Interfaces:** +- Consumes: Wrapper's existing `--ly-wrapper-max` calculation and demo wrapper allowlist. +- Produces: `--ly-wrapper-workspace: 96rem`, `.ly-wrapper--workspace`, manifest wrapper `workspace`, and demo wrapper option. + +- [ ] **Step 1: Add failing workspace contracts** + +Update wrapper inventories to: + +```js +["compact", "prose", "content", "workspace", "wide", "full", "breakout"] +``` + +Add: + +```js +assert( + /--ly-wrapper-workspace:\s*96rem/.test(foundation) && + /\.ly-wrapper--workspace\s*\{[^}]*--ly-wrapper-max:\s*var\(--ly-wrapper-workspace\)/s.test( + wrappers + ), + "Workspace Wrapper must expose and consume the 96rem application measure." +); +``` + +Add `--ly-wrapper-workspace` to the manifest geometry-token expectation. + +- [ ] **Step 2: Add a failing rendered utilization check** + +Extend `verifyMinimumWidth` or add `verifyWorkspaceMeasure(page, baseUrl)`. At a `1920x1080` viewport, render identical content and workspace wrappers, then assert: + +```js +assert(workspaceWidth > contentWidth, "Workspace must use more width than content."); +assert(workspaceWidth < wideWidth, "Workspace must remain narrower than wide."); +assert(workspaceWidth >= 1536, "Workspace content measure must reach 96rem at a wide viewport."); +``` + +Account for `box-sizing: border-box` by measuring an inner child or subtracting computed inline padding before asserting the `1536px` content measure. + +- [ ] **Step 3: Run affected tests and confirm failure** + +Run: + +```powershell +node test/layout-css-contract.test.mjs +node --test test/manifest-contract.test.mjs +npm.cmd run test:demo:quick +``` + +Expected: failures identify the missing workspace token, modifier, manifest value, or demo option. + +- [ ] **Step 4: Implement the workspace measure across package and demo** + +Add to `styles/foundation.css`: + +```css +--ly-wrapper-workspace: 96rem; +``` + +Add to `styles/wrappers.css` between content and wide: + +```css +.ly-wrapper--workspace { + --ly-wrapper-max: var(--ly-wrapper-workspace); +} +``` + +Add `workspace` to `manifest.json`, the demo allowlist, the demo select, and the static/browser inventories. Label it: + +```html + +``` + +- [ ] **Step 5: Rebuild, prove, and commit the workspace slice** + +Run: + +```powershell +npm.cmd run build +node test/layout-css-contract.test.mjs +node --test test/manifest-contract.test.mjs +npm.cmd run test:demo:quick +npm.cmd run lint +git diff --check +git add -- styles/foundation.css styles/wrappers.css manifest.json demo/index.html demo/demo.js test/layout-css-contract.test.mjs test/manifest-contract.test.mjs test/demo-smoke.test.mjs dist personalities.json demo/personality-metadata.js +git diff --cached --check +git commit -m "feat: add workspace wrapper measure" +``` + +Expected: all checks pass and the commit contains only workspace-related changes. + +--- + +### Task 3: Localize And Complete Gap Utilities + +**Files:** +- Modify: `test/layout-css-contract.test.mjs:338-348` +- Modify: `test/demo-smoke.test.mjs:917-969,1086-1115` +- Modify: `styles/utilities.css:19-53` + +**Interfaces:** +- Consumes: `--ly-space-0` through `--ly-space-9` and existing primitive/recipe `gap` declarations. +- Produces: `.ly-gap-0` through `.ly-gap-9`, each assigning only `gap` on its own element; rendered helper `verifyLocalGapUtilities(page, baseUrl)`. + +- [ ] **Step 1: Replace inherited-variable expectations with failing local-gap contracts** + +Replace the even-tier loop in `test/layout-css-contract.test.mjs` with: + +```js +for (let gap = 0; gap <= 9; gap += 1) { + const rule = utilities.match(new RegExp(`\\.ly-gap-${gap}\\s*\\{([^}]*)\\}`))?.[1] ?? ""; + assert( + rule.includes(`gap: var(--ly-space-${gap})`), + `.ly-gap-${gap} must map directly to --ly-space-${gap}.` + ); + for (const inheritedToken of ["--ly-gap", "--ly-grid-gap", "--ly-stack-gap", "--ly-cluster-gap"]) { + assert(!rule.includes(`${inheritedToken}:`), `.ly-gap-${gap} must not redefine ${inheritedToken}.`); + } +} +``` + +- [ ] **Step 2: Add failing nested-gap browser coverage** + +Replace the current outer Stack/Cluster utility isolation fixture with a helper that creates: + +```html +
+
AB
+
AB
+
+``` + +At the normal density, assert computed gaps: + +```js +assert.equal(outerGap, "64px"); +assert.equal(innerDefaultGap, "16px"); +assert.equal(innerLocalGap, "24px"); +``` + +Apply `.ly-gap-7` to a Card Grid recipe and assert `48px` to prove recipe compatibility. + +- [ ] **Step 3: Run focused checks and confirm failure** + +Run: + +```powershell +node test/layout-css-contract.test.mjs +npm.cmd run test:demo:quick +``` + +Expected: static coverage fails on missing odd tiers and browser coverage shows inherited `64px` on the nested default Stack. + +- [ ] **Step 4: Implement all ten local utility rules** + +Replace the five inherited-variable blocks in `styles/utilities.css` with: + +```css +.ly-gap-0 { gap: var(--ly-space-0); } +.ly-gap-1 { gap: var(--ly-space-1); } +.ly-gap-2 { gap: var(--ly-space-2); } +.ly-gap-3 { gap: var(--ly-space-3); } +.ly-gap-4 { gap: var(--ly-space-4); } +.ly-gap-5 { gap: var(--ly-space-5); } +.ly-gap-6 { gap: var(--ly-space-6); } +.ly-gap-7 { gap: var(--ly-space-7); } +.ly-gap-8 { gap: var(--ly-space-8); } +.ly-gap-9 { gap: var(--ly-space-9); } +``` + +Do not remove the public gap custom properties; they remain the explicit inherited-context API. + +- [ ] **Step 5: Rebuild, prove, and commit the gap slice** + +Run: + +```powershell +npm.cmd run build +node test/layout-css-contract.test.mjs +npm.cmd run test:demo:quick +npm.cmd run lint +git diff --check +git add -- styles/utilities.css test/layout-css-contract.test.mjs test/demo-smoke.test.mjs dist +git diff --cached --check +git commit -m "feat: localize layout gap utilities" +``` + +Expected: all commands pass; no nested primitive inherits a utility's gap unintentionally. + +--- + +### Task 4: Separate Scroll Overflow From Height Constraints + +**Files:** +- Modify: `test/layout-css-contract.test.mjs:273-305` +- Modify: `test/demo-smoke.test.mjs:970-1052,1086-1115` +- Modify: `styles/foundation.css:35-48,123-193` +- Modify: `styles/primitives.css:176-183` +- Modify: `manifest.json:73-95` +- Modify: `demo/demo.js:395-435` +- Modify: `demo/demo.css:360-420` + +**Interfaces:** +- Consumes: existing `.ly-scroll`, `--ly-scroll-max`, short-height viewport behavior, and deliberate vertical-overflow rules. +- Produces: unconstrained `.ly-scroll`, `.ly-scroll--bounded`, `.ly-scroll--viewport`, and `--ly-scroll-viewport-max`. + +- [ ] **Step 1: Add failing static Scroll contracts** + +Replace the old viewport-token assertion with: + +```js +assert( + /--ly-scroll-max:\s*50rem/.test(foundation) && + /--ly-scroll-viewport-max:\s*min\(70vh,\s*50rem\)/.test(foundation), + "Bounded and viewport-relative Scroll need separate public maxima." +); +assert( + /\.ly-scroll\s*\{[^}]*overflow-y:\s*auto/s.test(primitives) && + !/\.ly-scroll\s*\{[^}]*max-block-size:/s.test(primitives), + "Base Scroll must not impose a height cap." +); +assert( + /\.ly-scroll--bounded\s*\{[^}]*max-block-size:\s*var\(--ly-scroll-max\)/s.test(primitives) && + /\.ly-scroll--viewport\s*\{[^}]*max-block-size:\s*var\(--ly-scroll-viewport-max\)/s.test( + primitives + ), + "Scroll modifiers must consume distinct maxima." +); +``` + +Add `--ly-scroll-viewport-max` to the public manifest token inventory. + +- [ ] **Step 2: Add failing rendered Scroll behavior** + +Update `verifyPrimitiveOverflow` to create three equal long-content fixtures: + +```html +
+
+
+``` + +Constrain only the first fixture through a parent Grid track. Set: + +```css +--ly-scroll-max: 20rem; +--ly-scroll-viewport-max: 12rem; +``` + +Assert: + +```js +assert.equal(contained.maxBlockSize, "none"); +assert.equal(bounded.maxBlockSize, "320px"); +assert.equal(viewport.maxBlockSize, "192px"); +assert(contained.scrollHeight > contained.clientHeight); +assert(bounded.scrollHeight > bounded.clientHeight); +assert(viewport.scrollHeight > viewport.clientHeight); +``` + +- [ ] **Step 3: Run focused checks and confirm failure** + +Run: + +```powershell +node test/layout-css-contract.test.mjs +node --test test/manifest-contract.test.mjs +npm.cmd run test:demo:quick +``` + +Expected: failures identify the missing modifiers and the base Scroll's inherited maximum. + +- [ ] **Step 4: Implement distinct Scroll tokens and modifiers** + +In `styles/foundation.css`, define: + +```css +--ly-scroll-max: 50rem; +--ly-scroll-viewport-max: min(70vh, 50rem); +``` + +Change every short-height and dynamic-viewport assignment that formerly targeted `--ly-scroll-max` to `--ly-scroll-viewport-max`. + +In `styles/primitives.css`, implement: + +```css +.ly-scroll { + min-block-size: 0; + overflow-x: clip; + overflow-y: auto; + overscroll-behavior-block: contain; +} + +.ly-scroll--bounded { + max-block-size: var(--ly-scroll-max); +} + +.ly-scroll--viewport { + max-block-size: var(--ly-scroll-viewport-max); +} +``` + +- [ ] **Step 5: Make the demo's detail activity intentionally bounded** + +Change the list-detail fixture in `demo/demo.js` to: + +```js +className: "ly-scroll ly-scroll--bounded demo-list-scroll" +``` + +Add this comment above the modified `createRegion` function: + +```js +/** + * Creates one semantic recipe region and its representative preview content. + * + * @param {string} area Canonical recipe area name. + * @returns {HTMLElement} Populated preview region. + */ +``` + +Keep `.demo-list-scroll { --ly-scroll-max: 10rem; }`. Change preview height-tier variables that represent viewport-relative behavior to `--ly-scroll-viewport-max`. + +- [ ] **Step 6: Update the manifest, rebuild, prove, and commit** + +Run: + +```powershell +npm.cmd run build +node test/layout-css-contract.test.mjs +node --test test/manifest-contract.test.mjs +npm.cmd run test:demo:quick +npm.cmd run lint +npm.cmd run check:demo-js +git diff --check +git add -- styles/foundation.css styles/primitives.css manifest.json demo/demo.js demo/demo.css test/layout-css-contract.test.mjs test/manifest-contract.test.mjs test/demo-smoke.test.mjs dist personalities.json demo/personality-metadata.js +git diff --cached --check +git commit -m "feat: make scroll constraints explicit" +``` + +Expected: all checks pass and each Scroll variant has distinct computed behavior. + +--- + +### Task 5: Publish Recipe Thresholds And Enforce Token Liveness + +**Files:** +- Modify: `manifest.json:50-98` +- Modify: `test/manifest-contract.test.mjs:1-12,161-227` +- Modify: `test/layout-css-contract.test.mjs:221-348` + +**Interfaces:** +- Consumes: authored recipe queries, public manifest geometry tokens, and all core source declarations. +- Produces: `manifest.thresholds.recipes`; empty `manifest.tokens.extensionOnly`; helper `runtimeConsumedCustomProperties(css)` returning `Set`. + +- [ ] **Step 1: Add failing structured-threshold expectations** + +Change the expected manifest threshold object to: + +```js +{ + containerMinWidths: ["42rem", "44rem", "48rem", "52rem", "72rem"], + viewportMaxHeights: ["44rem", "30rem"], + recipes: { + splitHero: { wide: "42rem" }, + listDetail: { wide: "44rem" }, + docs: { wide: "48rem" }, + appShell: { medium: "52rem", wide: "72rem" }, + dashboard: { medium: "52rem", wide: "72rem" } + } +} +``` + +For every structured value, assert that `styles/recipes.css` contains the matching `@container ly-scope (min-width: value)` and recipe selector within that query block. + +Use this JSDoc-compatible helper and exact owner cases: + +```js +/** + * Returns the authored source for one container-query threshold block. + * + * @param {string} css Recipe stylesheet source. + * @param {string} threshold Minimum inline-size value. + * @returns {string} Matching query block source. + */ +function containerQuerySource(css, threshold) { + const start = css.indexOf(`@container ly-scope (min-width: ${threshold})`); + assert(start >= 0, `Missing ${threshold} container query.`); + const next = css.indexOf("@container ly-scope", start + 1); + return css.slice(start, next === -1 ? css.length : next); +} + +for (const [recipe, tiers] of Object.entries(manifest.thresholds.recipes)) { + const selectorName = recipe.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`); + for (const threshold of Object.values(tiers)) { + assert( + containerQuerySource(recipesCss, threshold).includes( + `[data-ly-recipe="${selectorName}"]` + ), + `${recipe} must own its documented ${threshold} threshold.` + ); + } +} +``` + +- [ ] **Step 2: Add a failing transitive token-liveness analyzer** + +Import css-tree: + +```js +import { generate, parse, walk } from "css-tree"; +``` + +Add this helper above the tests: + +```js +/** + * Finds custom properties that contribute transitively to non-custom CSS + * declarations through var() references. + * + * @param {string} css Authored CSS source. + * @returns {Set} Runtime-consumed custom-property names. + */ +function runtimeConsumedCustomProperties(css) { + const dependencies = new Map(); + const directConsumers = new Set(); + const ast = parse(css); + + walk(ast, { + visit: "Declaration", + enter(node) { + const value = generate(node.value); + const references = [...value.matchAll(/var\(\s*(--ly-[a-z0-9-]+)/g)].map( + ([, token]) => token + ); + if (node.property.startsWith("--")) { + const existing = dependencies.get(node.property) ?? new Set(); + references.forEach((token) => existing.add(token)); + dependencies.set(node.property, existing); + } else { + references.forEach((token) => directConsumers.add(token)); + } + } + }); + + const consumed = new Set(); + const pending = [...directConsumers]; + while (pending.length > 0) { + const token = pending.pop(); + if (consumed.has(token)) continue; + consumed.add(token); + for (const dependency of dependencies.get(token) ?? []) pending.push(dependency); + } + return consumed; +} +``` + +Add: + +```js +const consumedTokens = runtimeConsumedCustomProperties(coreSources); +const extensionOnlyTokens = new Set(manifest.tokens.extensionOnly); +for (const token of manifest.tokens.geometry) { + assert( + consumedTokens.has(token) || extensionOnlyTokens.has(token), + `${token} must be runtime-consumed or explicitly extension-only.` + ); +} +for (const token of extensionOnlyTokens) { + assert(manifest.tokens.geometry.includes(token), `${token} must remain a public geometry token.`); + assert(!consumedTokens.has(token), `${token} no longer needs an extension-only classification.`); +} +``` + +- [ ] **Step 3: Run the manifest contract and record unclassified tokens** + +Run: + +```powershell +node --test test/manifest-contract.test.mjs +``` + +Expected: exit `1` for missing `thresholds.recipes`, missing `tokens.extensionOnly`, and any declaration-only tokens revealed by the analyzer. + +- [ ] **Step 4: Add structured threshold metadata** + +Add the exact `recipes` object from Step 1 under `manifest.thresholds` without removing the existing generic arrays. + +- [ ] **Step 5: Classify public tokens from runtime evidence** + +Add: + +```json +"extensionOnly": [] +``` + +under `manifest.tokens`. The completed `3.1.0` source uses all public geometry tokens transitively, including every spatial tier through the ten gap utilities. Keep the array empty; a failing liveness assertion is an implementation defect to correct, not a reason to broaden the allowlist. + +- [ ] **Step 6: Prove manifest-to-source synchronization** + +Run: + +```powershell +npm.cmd run build +node --test test/manifest-contract.test.mjs +node test/layout-css-contract.test.mjs +git diff --check +``` + +Expected: all commands exit `0`; every structured threshold matches authored queries and every public geometry token has evidence. + +- [ ] **Step 7: Commit the manifest accountability slice** + +Run: + +```powershell +git add -- manifest.json test/manifest-contract.test.mjs test/layout-css-contract.test.mjs personalities.json demo/personality-metadata.js dist +git diff --cached --check +git commit -m "feat: publish layout contract metadata" +``` + +Expected: one manifest-and-contract commit with no release-version changes. + +--- + +### Task 6: Document, Version, And Package The 3.1.0 Candidate + +**Files:** +- Modify: `package.json`, `package-lock.json`, `manifest.json` +- Modify: `CHANGELOG.md`, `README.md` +- Modify: `demo/index.html`, `demo/sitemap.xml` +- Modify: `docs/wiki/Home.md` +- Modify: `docs/wiki/Getting-Started.md` +- Modify: `docs/wiki/Installation-And-CDN.md` +- Modify: `docs/wiki/Layout-Primitives.md` +- Modify: `docs/wiki/Layout-Recipes.md` +- Modify: `docs/wiki/Layout-Styles.md` +- Create: `docs/wiki/Migrating-To-3.1.md` +- Modify: `docs/wiki/Release-And-Publishing.md` +- Modify: `docs/wiki/_Sidebar.md` +- Modify: `.github/workflows/npm-publish.yml` +- Modify: `test/layout-css-contract.test.mjs` +- Modify: `test/manifest-contract.test.mjs` +- Modify: `test/package-contract.test.mjs` +- Modify: `test/release-docs-contract.test.mjs` +- Modify: `test/pages-artifact.test.mjs` +- Modify: `test/demo-smoke.test.mjs` + +**Interfaces:** +- Consumes: all green `3.1.0` behavior slices and the verified `3.0.2` history. +- Produces: synchronized `3.1.0` package metadata, maintained docs, migration guide, generated CSS and Pages assets, and a commit-ready minor candidate. + +- [ ] **Step 1: Make current-version contracts expect 3.1.0** + +Change exact current-version assertions to `3.1.0`, keep historical coverage for `3.0.2`, and require one current changelog heading: + +```js +const minorReleaseHeadings = changelog.match(/^## \[3\.1\.0\] - 2026-08-25$/gm) ?? []; +assert.equal(minorReleaseHeadings.length, 1, "Changelog needs one dated 3.1.0 section."); +``` + +Add `docs/wiki/Migrating-To-3.1.md` to packaged-document expectations and require `_Sidebar.md` to link `Migrating To 3.1`. + +Require current docs to name: + +```text +data-ly-density="compact" +data-ly-density="normal" +data-ly-density="spacious" +ly-wrapper--workspace +ly-gap-9 +ly-scroll--bounded +ly-scroll--viewport +manifest thresholds.recipes +``` + +- [ ] **Step 2: Run version and docs contracts and confirm failure** + +Run: + +```powershell +node test/layout-css-contract.test.mjs +node --test --test-concurrency=1 test/manifest-contract.test.mjs test/package-contract.test.mjs +node test/release-docs-contract.test.mjs +node test/pages-artifact.test.mjs +``` + +Expected: failures identify `3.0.2` metadata and missing `3.1.0` documentation. + +- [ ] **Step 3: Update versioned package and demo metadata** + +Change the root package, lockfile, manifest, demo metadata, cache-busting URLs, metadata fetch URLs, Pages tests, workflow input example, release guide, install examples, CDN examples, and sitemap date to `3.1.0` / `v3.1.0` / `2026-08-25`. Do not change dependency versions that happen to contain `3.0.1`. + +- [ ] **Step 4: Add the 3.1.0 changelog entry** + +Use these sections and facts: + +```markdown +## [3.1.0] - 2026-08-25 + +### Added + +- Added compact, normal, and spacious density contexts that can be applied at the root or to a nested layout subtree. +- Added the `96rem` workspace Wrapper, all ten local gap utilities, explicit bounded and viewport-relative Scroll modifiers, and recipe-owned threshold metadata. + +### Changed + +- Made normal section spacing more conservative while retaining the former marketing rhythm through spacious density. +- Made gap utilities local to the element carrying the class and made base Scroll rely on its containing layout for height constraints. + +### Migration + +- Use spacious density for the former section rhythm, explicit inherited gap tokens for a shared spacing context, and `ly-scroll--viewport` for the former viewport-relative Scroll behavior. + +### Tests + +- Added density, workspace utilization, nested-gap isolation, Scroll-mode, public-token liveness, threshold metadata, cross-height, generated-output, package, and Pages contracts. +``` + +- [ ] **Step 5: Write the 3.1 migration guide** + +Create `docs/wiki/Migrating-To-3.1.md` with these concrete before/after paths: + +```html + +
+
Campaign overview
+
+ + +
+
Inventory workspace
+
+ + +
+
Primary
Secondary
+
+ + +
Viewport-relative activity log
+ + +
+ Application-controlled activity log +
+``` + +Explain that no export or recipe threshold was removed or renamed. + +- [ ] **Step 6: Update maintained API documentation** + +Document: + +- normal density is the zero-configuration default +- nested density contexts reset inherited spacing intentionally +- workspace means task-oriented content, while content means conventional content +- `.ly-gap-*` is local and the public variables are the explicit inherited API +- base, bounded, and viewport Scroll have distinct constraints +- `manifest.json` owns both generic threshold arrays and per-recipe mappings +- public-token liveness is release-gated + +Update the release guide to `3.1.0`, preserve the protected environment and provenance instructions, and keep local verification distinct from publication. + +- [ ] **Step 7: Regenerate source-derived and Pages artifacts** + +Run: + +```powershell +npm.cmd run build +npm.cmd run pages:build +``` + +Expected: generated CSS, personality metadata, and Pages URLs carry the `3.1.0` contract. + +- [ ] **Step 8: Run focused candidate checks** + +Run: + +```powershell +node test/layout-css-contract.test.mjs +node --test --test-concurrency=1 test/manifest-contract.test.mjs test/package-contract.test.mjs +node test/release-docs-contract.test.mjs +node test/pages-artifact.test.mjs +npm.cmd run lint +npm.cmd run check:demo-js +npm.cmd run test:demo:quick +git diff --check +``` + +Expected: all commands exit `0`. + +- [ ] **Step 9: Inspect and commit the 3.1.0 candidate** + +Run: + +```powershell +git status --short +git diff --stat +git diff -- package.json package-lock.json manifest.json CHANGELOG.md README.md demo docs .github test styles dist scripts +git add -- package.json package-lock.json manifest.json CHANGELOG.md README.md demo docs/wiki .github/workflows/npm-publish.yml test styles dist scripts personalities.json +git diff --cached --check +git diff --cached --stat +git commit -m "chore: prepare layout style 3.1.0" +``` + +Expected: a complete candidate commit with no untracked release files and no edits outside the approved package scope. + +--- + +### Task 7: Verify, Merge, And Publish 3.1.0 + +**Files:** +- Verify only: complete repository and generated package. +- External mutations: release branch, pull request, protected `main`, tag `v3.1.0`, GitHub Release, npm registry. + +**Interfaces:** +- Consumes: clean committed `3.1.0` candidate from Task 6 and published npm `3.0.2`. +- Produces: merged and immutable `v3.1.0`, successful protected publish workflow, npm `latest=3.1.0`, and final release evidence. + +- [ ] **Step 1: Confirm local release preconditions** + +Run: + +```powershell +git status --short --branch +git log --oneline --decorate origin/main..HEAD +node -e "const p=require('./package.json'); if(p.version!=='3.1.0') process.exit(1)" +npm.cmd view layout-style-css@3.0.2 version +& 'C:\Program Files\GitHub CLI\gh.exe' auth status +``` + +Expected: clean branch `codex/layout-3.1.0-usability`, intended commits only, package version `3.1.0`, published predecessor `3.0.2`, and authenticated GitHub CLI. + +- [ ] **Step 2: Run the one final local release gate** + +Run: + +```powershell +npm.cmd run release:verify +git diff --check +git status --short +``` + +Expected: full static, ownership, docs, package, Pages, Chromium, Firefox, WebKit, ecosystem, audit, pack, and publish-dry-run checks pass with a clean tree. + +If a section fails, repair and rerun only that section first; rerun `release:verify` once after the repair for fresh integrated evidence. + +- [ ] **Step 3: Perform in-app Browser verification** + +Start the repository demo using its existing local server path, then use the available Browser plugin for this flow: + +```text +demo loads -> select workspace Wrapper -> switch compact, normal, and spacious density -> select list-detail -> verify bounded Scroll -> copy markup -> expected state and snippet remain synchronized +``` + +Verify desktop `1440x900` and mobile `375x768` page identity, nonblank DOM, no framework overlay, console health, screenshots, and control-state changes. Keep screenshots outside the repository. + +- [ ] **Step 4: Push the branch and open the minor-release PR** + +Run: + +```powershell +git push -u origin codex/layout-3.1.0-usability +& 'C:\Program Files\GitHub CLI\gh.exe' pr create --base main --head codex/layout-3.1.0-usability --title 'feat: release layout style 3.1.0' --body 'Adds contextual density, a workspace Wrapper, local full-scale gap utilities, explicit Scroll constraints, structured recipe thresholds, public-token liveness checks, generated output, documentation, and rendered regression coverage.' +``` + +Expected: one PR URL targeting `main`. + +- [ ] **Step 5: Wait for final PR checks and merge** + +Run: + +```powershell +& 'C:\Program Files\GitHub CLI\gh.exe' pr checks --watch +& 'C:\Program Files\GitHub CLI\gh.exe' pr merge --merge --delete-branch +``` + +Expected: all required checks pass and a merge commit becomes reachable from protected `main`. + +- [ ] **Step 6: Sync and tag the protected release commit** + +Run: + +```powershell +git switch main +git pull --ff-only origin main +node -e "const p=require('./package.json'); if(p.version!=='3.1.0') process.exit(1)" +git merge-base --is-ancestor HEAD origin/main +git tag -a v3.1.0 -m "layout-style-css 3.1.0" +git push origin v3.1.0 +``` + +Expected: tag `v3.1.0` points to the merged protected-main release commit. + +- [ ] **Step 7: Publish the GitHub Release and monitor npm** + +Run: + +```powershell +& 'C:\Program Files\GitHub CLI\gh.exe' release create v3.1.0 --title 'layout-style-css 3.1.0' --generate-notes --verify-tag +$layoutReleaseRunId = & 'C:\Program Files\GitHub CLI\gh.exe' run list --workflow npm-publish.yml --event release --limit 1 --json databaseId --jq '.[0].databaseId' +if (-not $layoutReleaseRunId) { throw 'No npm publish run was created for v3.1.0.' } +& 'C:\Program Files\GitHub CLI\gh.exe' run watch $layoutReleaseRunId --exit-status +``` + +Expected: the protected publish succeeds. If it waits for a reviewer, report the `npm` environment gate and resume after approval without attempting local publication. + +- [ ] **Step 8: Verify the final release independently** + +Run: + +```powershell +& 'C:\Program Files\GitHub CLI\gh.exe' release view v3.1.0 --json tagName,isDraft,isPrerelease,publishedAt,url,targetCommitish +npm.cmd view layout-style-css@3.1.0 version dist-tags dist.integrity dist.shasum --json +npm.cmd view layout-style-css version dist-tags --json +npm.cmd pack layout-style-css@3.1.0 --dry-run --json +git ls-remote --tags origin refs/tags/v3.1.0 +git status --short --branch +git rev-parse HEAD +git rev-parse origin/main +``` + +Expected: + +```text +GitHub Release is published, not draft, and not prerelease. +npm package version is 3.1.0. +npm latest dist-tag is 3.1.0. +Published files include the full v3 CSS export surface, manifest, personalities metadata, changelog, README, license, and wiki including Migrating-To-3.1.md. +Remote tag v3.1.0 resolves successfully. +Local and remote main SHAs match and the worktree is clean. +``` From 329b0b29d246fdcfb9923cd32a527c192cda18c7 Mon Sep 17 00:00:00 2001 From: Foscat Date: Tue, 25 Aug 2026 15:15:26 -0500 Subject: [PATCH 3/7] fix: align app shell row geometry --- dist/foundation.css | 3 + dist/layout-style-css.css | 11 +++- dist/layout-style-css.min.css | 2 +- dist/personalities/bento.css | 1 + dist/personalities/neumorphism.css | 1 + dist/personalities/split-screen.css | 1 + dist/personalities/tactile.css | 1 + dist/recipes.css | 4 +- ...8-25-layout-style-css-3-0-2-correctness.md | 4 +- styles/foundation.css | 3 + styles/personalities/bento.css | 1 + styles/personalities/neumorphism.css | 1 + styles/personalities/split-screen.css | 1 + styles/personalities/tactile.css | 1 + styles/recipes.css | 4 +- test/demo-smoke.test.mjs | 59 +++++++++++++++++++ test/layout-css-contract.test.mjs | 18 ++++++ 17 files changed, 110 insertions(+), 6 deletions(-) diff --git a/dist/foundation.css b/dist/foundation.css index 9801a80..94929bd 100644 --- a/dist/foundation.css +++ b/dist/foundation.css @@ -64,11 +64,13 @@ --ly-gallery-min: 12rem; --ly-card-grid-min: 16rem; + --ly-app-shell-base-rows: auto auto minmax(0, 1fr) auto auto; --ly-app-shell-medium-areas: "header header" "sidebar main" "aside main" "footer footer"; + --ly-app-shell-medium-rows: auto auto minmax(0, 1fr) auto; --ly-app-shell-medium-columns: minmax(min(100%, var(--ly-recipe-rail)), auto) minmax(0, 1fr); @@ -76,6 +78,7 @@ "sidebar header header" "sidebar main aside" "sidebar footer footer"; + --ly-app-shell-wide-rows: auto minmax(0, 1fr) auto; --ly-app-shell-wide-columns: minmax(min(100%, var(--ly-recipe-rail)), auto) minmax(0, 1fr) diff --git a/dist/layout-style-css.css b/dist/layout-style-css.css index bf12eae..33d90dc 100644 --- a/dist/layout-style-css.css +++ b/dist/layout-style-css.css @@ -67,11 +67,13 @@ --ly-gallery-min: 12rem; --ly-card-grid-min: 16rem; + --ly-app-shell-base-rows: auto auto minmax(0, 1fr) auto auto; --ly-app-shell-medium-areas: "header header" "sidebar main" "aside main" "footer footer"; + --ly-app-shell-medium-rows: auto auto minmax(0, 1fr) auto; --ly-app-shell-medium-columns: minmax(min(100%, var(--ly-recipe-rail)), auto) minmax(0, 1fr); @@ -79,6 +81,7 @@ "sidebar header header" "sidebar main aside" "sidebar footer footer"; + --ly-app-shell-wide-rows: auto minmax(0, 1fr) auto; --ly-app-shell-wide-columns: minmax(min(100%, var(--ly-recipe-rail)), auto) minmax(0, 1fr) @@ -516,7 +519,7 @@ "main" "aside" "footer"; - grid-template-rows: auto auto minmax(0, 1fr) auto auto; + grid-template-rows: var(--ly-app-shell-base-rows); } [data-ly-recipe="dashboard"], @@ -647,6 +650,7 @@ [data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]) { grid-template-areas: var(--ly-app-shell-medium-areas); grid-template-columns: var(--ly-app-shell-medium-columns); + grid-template-rows: var(--ly-app-shell-medium-rows); } [data-ly-recipe="dashboard"]:not([data-ly-responsive="manual"]) { @@ -659,6 +663,7 @@ [data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]) { grid-template-areas: var(--ly-app-shell-wide-areas); grid-template-columns: var(--ly-app-shell-wide-columns); + grid-template-rows: var(--ly-app-shell-wide-rows); } [data-ly-recipe="dashboard"]:not([data-ly-responsive="manual"]) { @@ -793,6 +798,7 @@ "sidebar main main" "aside main main" "footer footer footer"; + --ly-app-shell-wide-rows: auto auto minmax(0, 1fr) auto; --ly-app-shell-wide-columns: minmax(min(100%, var(--ly-recipe-rail)), 0.3fr) minmax(0, 1fr) @@ -855,6 +861,7 @@ "sidebar main" "aside main" "footer footer"; + --ly-app-shell-wide-rows: auto auto minmax(0, 1fr) auto; --ly-app-shell-wide-columns: minmax(min(100%, var(--ly-recipe-rail)), 0.28fr) minmax(0, 1fr); @@ -877,6 +884,7 @@ "main sidebar" "aside sidebar" "footer sidebar"; + --ly-app-shell-wide-rows: auto minmax(0, 1fr) auto auto; --ly-app-shell-wide-columns: minmax(0, 1fr) minmax(min(100%, var(--ly-recipe-rail)), 0.3fr); @@ -1042,6 +1050,7 @@ "main sidebar" "aside sidebar" "footer footer"; + --ly-app-shell-wide-rows: auto minmax(0, 1fr) auto auto; --ly-app-shell-wide-columns: minmax(0, 1fr) minmax(0, 1fr); diff --git a/dist/layout-style-css.min.css b/dist/layout-style-css.min.css index 6e08850..7e84d2a 100644 --- a/dist/layout-style-css.min.css +++ b/dist/layout-style-css.min.css @@ -1 +1 @@ -@layer ly.reset,ly.tokens,ly.wrappers,ly.primitives,ly.recipes,ly.utilities,ly.personalities;@layer ly.tokens{:where(.ly-root){--ly-space-0:0;--ly-space-1:0.25rem;--ly-space-2:0.5rem;--ly-space-3:0.75rem;--ly-space-4:1rem;--ly-space-5:1.5rem;--ly-space-6:2rem;--ly-space-7:3rem;--ly-space-8:4rem;--ly-space-9:6rem;--ly-wrapper-compact:40rem;--ly-wrapper-prose:68ch;--ly-wrapper-content:72rem;--ly-wrapper-wide:112rem;--ly-page-padding-inline:clamp(1rem,4vw,3rem);--ly-safe-area-inline:max( env(safe-area-inset-left,0px),env(safe-area-inset-right,0px) );--ly-safe-area-block-start:env(safe-area-inset-top,0px);--ly-safe-area-block-end:env(safe-area-inset-bottom,0px);--ly-wrapper-gutter:max(var(--ly-page-padding-inline),var(--ly-safe-area-inline));--ly-profile-gap:var(--ly-space-5);--ly-gap:var(--ly-profile-gap);--ly-grid-gap:var(--ly-gap);--ly-stack-gap:var(--ly-space-4);--ly-cluster-gap:var(--ly-space-3);--ly-section-padding-block:clamp(3rem,7vh,6rem);--ly-header-height:4.5rem;--ly-sticky-position:sticky;--ly-cover-min:100vh;--ly-shell-min:100vh;--ly-scroll-max:min(70vh,50rem);--ly-switcher-threshold:42rem;--ly-sidebar-size:18rem;--ly-sidebar-content-min:50%;--ly-grid-columns:12;--ly-grid-min:16rem;--ly-split-min:20rem;--ly-pane-min:18rem;--ly-pane-size:22rem;--ly-media-min:18rem;--ly-media-size:14rem;--ly-reel-item-min:16rem;--ly-reel-item-max:28rem;--ly-frame-ratio:16 / 9;--ly-split-primary:1fr;--ly-split-secondary:1fr;--ly-recipe-rail:18rem;--ly-recipe-aside:22rem;--ly-gallery-min:12rem;--ly-card-grid-min:16rem;--ly-app-shell-medium-areas:"header header" "sidebar main" "aside main" "footer footer";--ly-app-shell-medium-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr);--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "sidebar footer footer";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.35fr);--ly-dashboard-medium-areas:"header header" "nav nav" "main aside" "footer footer";--ly-dashboard-medium-columns:minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.35fr);--ly-dashboard-wide-areas:"nav header header" "nav main aside" "nav footer footer";--ly-dashboard-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.35fr);--ly-docs-wide-areas:"header header" "nav main" "nav aside" "footer footer";--ly-docs-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr);--ly-list-detail-wide-areas:"primary secondary" "actions actions";--ly-list-detail-wide-columns:minmax(min(100%,var(--ly-pane-size)),0.7fr) minmax(0,1fr);--ly-split-hero-wide-areas:"content media" "actions media";--ly-split-hero-wide-columns:minmax(0,var(--ly-split-primary,1fr)) minmax(0,var(--ly-split-secondary,1fr));container-name:ly-scope;container-type:inline-size;min-inline-size:0;min-block-size:0}@supports (height:100dvh){:where(.ly-root){--ly-cover-min:100dvh;--ly-shell-min:100dvh;--ly-scroll-max:min(70dvh,50rem)}}}@layer ly.reset{:where(.ly-root),:where(.ly-root *){box-sizing:border-box}:where(.ly-root){margin:0}:where(.ly-root img),:where(.ly-root svg),:where(.ly-root video),:where(.ly-root canvas),:where(.ly-root iframe){max-inline-size:100%}}@layer ly.tokens{@media (max-height:44rem){:where(.ly-root){--ly-gap:min(var(--ly-profile-gap),var(--ly-space-4));--ly-grid-gap:var(--ly-gap);--ly-stack-gap:min(var(--ly-space-4),3vh);--ly-cluster-gap:min(var(--ly-space-3),2vh);--ly-section-padding-block:clamp(1.5rem,6vh,3rem);--ly-header-height:3.5rem;--ly-scroll-max:min(62vh,34rem)}@supports (height:100dvh){:where(.ly-root){--ly-stack-gap:min(var(--ly-space-4),3dvh);--ly-cluster-gap:min(var(--ly-space-3),2dvh);--ly-section-padding-block:clamp(1.5rem,6dvh,3rem);--ly-scroll-max:min(62dvh,34rem)}}}@media (max-height:30rem){:where(.ly-root){--ly-cover-min:auto;--ly-shell-min:auto;--ly-sticky-position:static;--ly-section-padding-block:var(--ly-space-5);--ly-scroll-max:max(12rem,calc(100vh - 8rem))}@supports (height:100dvh){:where(.ly-root){--ly-scroll-max:max(12rem,calc(100dvh - 8rem))}}}}@layer ly.wrappers{.ly-wrapper{--ly-wrapper-max:var(--ly-personality-wrapper-max,var(--ly-wrapper-content));--ly-wrapper-fluid-gutter:clamp(1rem,3vw,3rem);--ly-wrapper-local-gutter:max( var(--ly-wrapper-fluid-gutter),var(--ly-safe-area-inline) );container-name:ly-scope;container-type:inline-size;inline-size:min(100%,calc(var(--ly-wrapper-max) + (var(--ly-wrapper-local-gutter) * 2)));max-inline-size:100%;margin-inline:auto;padding-inline:var(--ly-wrapper-local-gutter)}@supports (width:1cqi){.ly-wrapper{--ly-wrapper-fluid-gutter:clamp(1rem,3cqi,3rem)}}.ly-wrapper--compact{--ly-wrapper-max:var(--ly-wrapper-compact)}.ly-wrapper--prose{--ly-wrapper-max:var(--ly-wrapper-prose)}.ly-wrapper--content{--ly-wrapper-max:var(--ly-wrapper-content)}.ly-wrapper--wide{--ly-wrapper-max:var(--ly-wrapper-wide)}.ly-wrapper--full{--ly-wrapper-max:100%}.ly-wrapper--breakout{--ly-wrapper-max:100%;--ly-breakout-content-size:min( var(--ly-wrapper-content),calc(100% - (var(--ly-wrapper-local-gutter) * 2)) );--ly-breakout-feature-size:min( var(--ly-wrapper-wide),calc(100% - (var(--ly-wrapper-local-gutter) * 2)) );display:grid;grid-template-columns:[full-start] minmax(min(100%,var(--ly-wrapper-local-gutter)),1fr) [feature-start] minmax( 0,calc((var(--ly-breakout-feature-size) - var(--ly-breakout-content-size)) / 2) ) [content-start] minmax(0,var(--ly-breakout-content-size)) [content-end] minmax( 0,calc((var(--ly-breakout-feature-size) - var(--ly-breakout-content-size)) / 2) ) [feature-end] minmax(min(100%,var(--ly-wrapper-local-gutter)),1fr) [full-end];inline-size:100%;padding-inline:0}.ly-wrapper--breakout>*{grid-column:content;min-inline-size:0;max-inline-size:100%}.ly-wrapper--breakout>:where(.ly-lane--content,[data-ly-lane="content"]){grid-column:content}.ly-wrapper--breakout>:where(.ly-lane--feature,[data-ly-lane="feature"]){grid-column:feature}.ly-wrapper--breakout>:where(.ly-lane--full,[data-ly-lane="full"]){grid-column:full}}@layer ly.primitives{.ly-page{min-block-size:100vh;min-block-size:100dvh;padding-block-start:var(--ly-safe-area-block-start);padding-block-end:var(--ly-safe-area-block-end)}.ly-header,.ly-footer,.ly-main,.ly-section,.ly-surface{min-inline-size:0;min-block-size:0}.ly-header{min-block-size:var(--ly-header-height)}.ly-header--sticky{position:var(--ly-sticky-position,sticky);inset-block-start:0;z-index:var(--ly-z-header,20)}.ly-section{padding-block:var(--ly-section-padding-block)}.ly-section--compact{--ly-section-padding-block:var(--ly-space-7)}.ly-section--flush{--ly-section-padding-block:0}.ly-readable{max-inline-size:var(--ly-wrapper-prose)}.ly-stack{display:flex;flex-direction:column;gap:var(--ly-stack-gap)}.ly-cluster{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ly-cluster-gap)}.ly-center{inline-size:min(100%,var(--ly-center-max,var(--ly-wrapper-content)));margin-inline:auto}.ly-cover{display:flex;flex-direction:column;min-block-size:100vh;min-block-size:var(--ly-cover-min);padding-block:var(--ly-cover-padding,var(--ly-space-5))}.ly-cover>:where(.ly-cover__center,[data-ly-cover-center]){margin-block:auto}.ly-switcher{display:flex;flex-wrap:wrap;gap:var(--ly-gap)}.ly-switcher>*{flex-basis:calc((var(--ly-switcher-threshold) - 100%) * 999);flex-grow:1;min-inline-size:0}.ly-sidebar{display:flex;flex-wrap:wrap;gap:var(--ly-gap)}.ly-sidebar>:where(.ly-sidebar__side,[data-ly-sidebar="side"]){flex-basis:var(--ly-sidebar-size);flex-grow:1;min-inline-size:min(100%,var(--ly-sidebar-size))}.ly-sidebar>:where(.ly-sidebar__content,[data-ly-sidebar="content"]){flex-basis:0;flex-grow:999;min-inline-size:min(100%,var(--ly-sidebar-content-min))}.ly-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-grid-min)),1fr));gap:var(--ly-grid-gap)}.ly-grid--fixed{grid-template-columns:repeat(var(--ly-grid-columns),minmax(0,1fr))}.ly-split{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-split-min)),1fr));gap:var(--ly-gap);align-items:var(--ly-split-align,center)}.ly-panes{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-pane-min)),1fr));gap:var(--ly-gap)}.ly-media{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-media-min)),1fr));gap:var(--ly-gap);align-items:var(--ly-media-align,start)}.ly-media>:where(.ly-media__actions,[data-ly-media="actions"]){min-inline-size:min(100%,var(--ly-media-size))}.ly-grid>*,.ly-split>*,.ly-panes>*,.ly-media>*{min-inline-size:0;min-block-size:0}.ly-reel{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(min(100%,var(--ly-reel-item-min)),var(--ly-reel-item-max));gap:var(--ly-grid-gap);overflow-x:auto;overscroll-behavior-inline:contain;scroll-snap-type:inline proximity}.ly-reel>*{min-inline-size:0;scroll-snap-align:start}.ly-frame{aspect-ratio:var(--ly-frame-ratio);overflow:hidden}.ly-frame>*{inline-size:100%;block-size:100%;object-fit:cover}.ly-scroll{min-block-size:0;max-block-size:var(--ly-scroll-max);overflow-x:clip;overflow-y:auto;overscroll-behavior-block:contain}}@layer ly.recipes{:where( [data-ly-recipe="app-shell"],[data-ly-recipe="dashboard"],[data-ly-recipe="docs"],[data-ly-recipe="list-detail"],[data-ly-recipe="split-hero"],[data-ly-recipe="gallery"],[data-ly-recipe="card-grid"] ){container-name:ly-scope;container-type:inline-size;min-inline-size:0;min-block-size:0}:where( [data-ly-recipe="app-shell"],[data-ly-recipe="dashboard"],[data-ly-recipe="docs"],[data-ly-recipe="list-detail"],[data-ly-recipe="split-hero"] ){display:grid;grid-template-columns:minmax(0,1fr);gap:var(--ly-gap)}[data-ly-recipe="app-shell"]{min-block-size:100vh;min-block-size:var(--ly-shell-min);grid-template-areas:"header" "sidebar" "main" "aside" "footer";grid-template-rows:auto auto minmax(0,1fr) auto auto}[data-ly-recipe="dashboard"],[data-ly-recipe="docs"]{grid-template-areas:"header" "nav" "main" "aside" "footer"}[data-ly-recipe="list-detail"]{grid-template-areas:"primary" "secondary" "actions"}[data-ly-recipe="split-hero"]{grid-template-areas:"content" "media" "actions";align-items:var(--ly-split-hero-align,center)}[data-ly-recipe="gallery"],[data-ly-recipe="card-grid"]{display:grid;gap:var(--ly-grid-gap)}[data-ly-recipe="gallery"]{grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-gallery-min)),1fr))}[data-ly-recipe="card-grid"]{grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-card-grid-min)),1fr))}:where( [data-ly-recipe="gallery"],[data-ly-recipe="card-grid"] )[data-ly-responsive="manual"]{grid-template-columns:minmax(0,1fr)}:where( [data-ly-recipe="app-shell"],[data-ly-recipe="dashboard"],[data-ly-recipe="docs"],[data-ly-recipe="list-detail"],[data-ly-recipe="split-hero"],[data-ly-recipe="gallery"],[data-ly-recipe="card-grid"] )>*{min-inline-size:0;min-block-size:0}[data-ly-area="header"]{grid-area:header}[data-ly-area="nav"]{grid-area:nav}[data-ly-area="sidebar"]{grid-area:sidebar}[data-ly-area="main"]{grid-area:main}[data-ly-area="aside"]{grid-area:aside}[data-ly-area="footer"]{grid-area:footer}[data-ly-area="content"]{grid-area:content}[data-ly-area="media"]{grid-area:media}[data-ly-area="actions"]{grid-area:actions}[data-ly-area="primary"]{grid-area:primary}[data-ly-area="secondary"]{grid-area:secondary}@container ly-scope (min-width:42rem){[data-ly-recipe="split-hero"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-split-hero-wide-areas);grid-template-columns:var(--ly-split-hero-wide-columns)}}@container ly-scope (min-width:44rem){[data-ly-recipe="list-detail"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-list-detail-wide-areas);grid-template-columns:var(--ly-list-detail-wide-columns)}}@container ly-scope (min-width:48rem){[data-ly-recipe="docs"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-docs-wide-areas);grid-template-columns:var(--ly-docs-wide-columns)}}@container ly-scope (min-width:52rem){[data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-app-shell-medium-areas);grid-template-columns:var(--ly-app-shell-medium-columns)}[data-ly-recipe="dashboard"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-dashboard-medium-areas);grid-template-columns:var(--ly-dashboard-medium-columns)}}@container ly-scope (min-width:72rem){[data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-app-shell-wide-areas);grid-template-columns:var(--ly-app-shell-wide-columns)}[data-ly-recipe="dashboard"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-dashboard-wide-areas);grid-template-columns:var(--ly-dashboard-wide-columns)}}}@layer ly.utilities{.ly-cols-1{--ly-grid-columns:1}.ly-cols-2{--ly-grid-columns:2}.ly-cols-3{--ly-grid-columns:3}.ly-cols-4{--ly-grid-columns:4}.ly-cols-6{--ly-grid-columns:6}.ly-cols-12{--ly-grid-columns:12}.ly-span-1{grid-column:span 1}.ly-span-2{grid-column:span 2}.ly-span-3{grid-column:span 3}.ly-span-4{grid-column:span 4}.ly-span-full{grid-column:1 / -1}.ly-gap-0{--ly-gap:var(--ly-space-0);--ly-grid-gap:var(--ly-space-0);--ly-stack-gap:var(--ly-space-0);--ly-cluster-gap:var(--ly-space-0)}.ly-gap-2{--ly-gap:var(--ly-space-2);--ly-grid-gap:var(--ly-space-2);--ly-stack-gap:var(--ly-space-2);--ly-cluster-gap:var(--ly-space-2)}.ly-gap-4{--ly-gap:var(--ly-space-4);--ly-grid-gap:var(--ly-space-4);--ly-stack-gap:var(--ly-space-4);--ly-cluster-gap:var(--ly-space-4)}.ly-gap-6{--ly-gap:var(--ly-space-6);--ly-grid-gap:var(--ly-space-6);--ly-stack-gap:var(--ly-space-6);--ly-cluster-gap:var(--ly-space-6)}.ly-gap-8{--ly-gap:var(--ly-space-8);--ly-grid-gap:var(--ly-space-8);--ly-stack-gap:var(--ly-space-8);--ly-cluster-gap:var(--ly-space-8)}.ly-pad-0{padding:var(--ly-space-0)}.ly-pad-2{padding:var(--ly-space-2)}.ly-pad-4{padding:var(--ly-space-4)}.ly-pad-6{padding:var(--ly-space-6)}.ly-pad-8{padding:var(--ly-space-8)}.ly-mx-auto{margin-inline:auto}.ly-w-full{inline-size:100%}.ly-h-full{block-size:100%}.ly-min-h-screen{min-block-size:100vh;min-block-size:100dvh}.ly-overflow-auto{overflow:auto}.ly-overflow-hidden{overflow:hidden}.ly-items-start{align-items:flex-start}.ly-items-center{align-items:center}.ly-items-end{align-items:flex-end}.ly-items-stretch{align-items:stretch}.ly-justify-start{justify-content:flex-start}.ly-justify-center{justify-content:center}.ly-justify-end{justify-content:flex-end}.ly-justify-between{justify-content:space-between}.ly-frame-1x1{--ly-frame-ratio:1 / 1}.ly-frame-3x2{--ly-frame-ratio:3 / 2}.ly-frame-4x3{--ly-frame-ratio:4 / 3}.ly-frame-16x9{--ly-frame-ratio:16 / 9}.ly-frame-21x9{--ly-frame-ratio:21 / 9}.ly-hidden{display:none !important}.ly-visually-hidden{position:absolute !important;inline-size:1px !important;block-size:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0,0,0,0) !important;clip-path:inset(50%) !important;white-space:nowrap !important}}@layer ly.personalities.minimal-saas{:where(.ly-root[data-ly-layout="minimal-saas"]){--ly-personality-wrapper-max:88rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:16rem;--ly-recipe-rail:15rem;--ly-recipe-aside:16rem;--ly-split-primary:1.05fr;--ly-split-secondary:0.95fr}}@layer ly.personalities.bento{:where(.ly-root[data-ly-layout="bento"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:12rem;--ly-card-grid-min:14rem;--ly-gallery-min:11rem;--ly-recipe-rail:14rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header header" "sidebar main main" "aside main main" "footer footer footer";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.3fr) minmax(0,1fr) minmax(0,1fr)}}@layer ly.personalities.maximalist{:where(.ly-root[data-ly-layout="maximalist"]){--ly-personality-wrapper-max:100%;--ly-profile-gap:var(--ly-space-7);--ly-grid-min:10rem;--ly-card-grid-min:13rem;--ly-gallery-min:10rem;--ly-recipe-rail:15rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header header header" "main main sidebar aside" "main main footer footer";--ly-app-shell-wide-columns:minmax(0,1.4fr) minmax(0,0.6fr) minmax(min(100%,var(--ly-recipe-rail)),0.32fr) minmax(min(100%,var(--ly-recipe-aside)),0.38fr)}}@layer ly.personalities.bauhaus{:where(.ly-root[data-ly-layout="bauhaus"]){--ly-personality-wrapper-max:96rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:13rem;--ly-recipe-rail:14rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header header header" "sidebar main main aside" "sidebar footer footer aside";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.24fr) minmax(0,0.7fr) minmax(0,1.3fr) minmax(min(100%,var(--ly-recipe-aside)),0.3fr)}}@layer ly.personalities.tactile{:where(.ly-root[data-ly-layout="tactile"]){--ly-personality-wrapper-max:82rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:15rem;--ly-recipe-rail:17rem;--ly-recipe-aside:20rem;--ly-app-shell-wide-areas:"header header" "sidebar main" "aside main" "footer footer";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.28fr) minmax(0,1fr);--ly-list-detail-wide-columns:minmax(min(100%,var(--ly-pane-size)),0.5fr) minmax(0,0.9fr)}}@layer ly.personalities.neumorphism{:where(.ly-root[data-ly-layout="neumorphism"]){--ly-personality-wrapper-max:84rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:17rem;--ly-recipe-rail:17rem;--ly-recipe-aside:19rem;--ly-app-shell-wide-areas:"header sidebar" "main sidebar" "aside sidebar" "footer sidebar";--ly-app-shell-wide-columns:minmax(0,1fr) minmax(min(100%,var(--ly-recipe-rail)),0.3fr);--ly-list-detail-wide-columns:minmax(0,1.1fr) minmax(min(100%,var(--ly-pane-size)),0.7fr)}}@layer ly.personalities.retrofuturism{:where(.ly-root[data-ly-layout="retrofuturism"]){--ly-personality-wrapper-max:106rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:14rem;--ly-recipe-rail:14rem;--ly-recipe-aside:17rem;--ly-app-shell-wide-areas:"sidebar header aside" "sidebar main aside" "sidebar footer aside";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.24fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.3fr)}}@layer ly.personalities.brutalism{:where(.ly-root[data-ly-layout="brutalism"]){--ly-personality-wrapper-max:100rem;--ly-profile-gap:var(--ly-space-3);--ly-grid-min:14rem;--ly-recipe-rail:16rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header sidebar" "main aside sidebar" "footer footer sidebar";--ly-app-shell-wide-columns:minmax(0,1.3fr) minmax(0,0.7fr) minmax(min(100%,var(--ly-recipe-rail)),0.34fr)}}@layer ly.personalities.cyberpunk{:where(.ly-root[data-ly-layout="cyberpunk"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:12rem;--ly-recipe-rail:13rem;--ly-recipe-aside:19rem;--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "sidebar footer footer";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.2fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.32fr)}}@layer ly.personalities.y2k{:where(.ly-root[data-ly-layout="y2k"]){--ly-personality-wrapper-max:90rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:13rem;--ly-recipe-rail:18rem;--ly-recipe-aside:16rem;--ly-app-shell-wide-areas:"header header sidebar" "aside main sidebar" "footer footer sidebar";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-aside)),0.34fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-rail)),0.28fr)}}@layer ly.personalities.retro-glass{:where(.ly-root[data-ly-layout="retro-glass"]){--ly-personality-wrapper-max:104rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:15rem;--ly-recipe-rail:18rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header sidebar" "main main sidebar" "aside footer footer";--ly-app-shell-wide-columns:minmax(0,1.25fr) minmax(0,0.75fr) minmax(min(100%,var(--ly-recipe-rail)),0.32fr)}}@layer ly.personalities.f-pattern{:where(.ly-root[data-ly-layout="f-pattern"]){--ly-personality-wrapper-max:92rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:14rem;--ly-recipe-rail:16rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "sidebar footer footer";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.26fr) minmax(0,1.35fr) minmax(min(100%,var(--ly-recipe-aside)),0.65fr);--ly-split-hero-wide-columns:minmax(0,1.8fr) minmax(0,0.8fr)}}@layer ly.personalities.z-pattern{:where(.ly-root[data-ly-layout="z-pattern"]){--ly-personality-wrapper-max:108rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:14rem;--ly-recipe-rail:17rem;--ly-recipe-aside:19rem;--ly-app-shell-wide-areas:"header header sidebar" "main aside sidebar" "footer aside sidebar";--ly-app-shell-wide-columns:minmax(0,1.45fr) minmax(min(100%,var(--ly-recipe-aside)),0.55fr) minmax(min(100%,var(--ly-recipe-rail)),0.3fr);--ly-split-hero-wide-columns:minmax(0,1.5fr) minmax(0,0.5fr)}}@layer ly.personalities.split-screen{:where(.ly-root[data-ly-layout="split-screen"]){--ly-personality-wrapper-max:100%;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:20rem;--ly-recipe-rail:24rem;--ly-recipe-aside:24rem;--ly-app-shell-wide-areas:"header header" "main sidebar" "aside sidebar" "footer footer";--ly-app-shell-wide-columns:minmax(0,1fr) minmax(0,1fr);--ly-split-hero-wide-columns:minmax(0,1fr) minmax(0,1fr);--ly-list-detail-wide-columns:minmax(0,1fr) minmax(0,1fr)}}@layer ly.personalities.mondrian{:where(.ly-root[data-ly-layout="mondrian"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-3);--ly-grid-min:11rem;--ly-recipe-rail:13rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "footer footer aside";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.22fr) minmax(0,1.45fr) minmax(min(100%,var(--ly-recipe-aside)),0.55fr)}}@layer ly.personalities.synthwave{:where(.ly-root[data-ly-layout="synthwave"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:14rem;--ly-recipe-rail:16rem;--ly-recipe-aside:20rem;--ly-app-shell-wide-areas:"header header header" "sidebar main aside" "footer footer footer";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.28fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.34fr);--ly-reel-item-min:20rem}} +@layer ly.reset,ly.tokens,ly.wrappers,ly.primitives,ly.recipes,ly.utilities,ly.personalities;@layer ly.tokens{:where(.ly-root){--ly-space-0:0;--ly-space-1:0.25rem;--ly-space-2:0.5rem;--ly-space-3:0.75rem;--ly-space-4:1rem;--ly-space-5:1.5rem;--ly-space-6:2rem;--ly-space-7:3rem;--ly-space-8:4rem;--ly-space-9:6rem;--ly-wrapper-compact:40rem;--ly-wrapper-prose:68ch;--ly-wrapper-content:72rem;--ly-wrapper-wide:112rem;--ly-page-padding-inline:clamp(1rem,4vw,3rem);--ly-safe-area-inline:max( env(safe-area-inset-left,0px),env(safe-area-inset-right,0px) );--ly-safe-area-block-start:env(safe-area-inset-top,0px);--ly-safe-area-block-end:env(safe-area-inset-bottom,0px);--ly-wrapper-gutter:max(var(--ly-page-padding-inline),var(--ly-safe-area-inline));--ly-profile-gap:var(--ly-space-5);--ly-gap:var(--ly-profile-gap);--ly-grid-gap:var(--ly-gap);--ly-stack-gap:var(--ly-space-4);--ly-cluster-gap:var(--ly-space-3);--ly-section-padding-block:clamp(3rem,7vh,6rem);--ly-header-height:4.5rem;--ly-sticky-position:sticky;--ly-cover-min:100vh;--ly-shell-min:100vh;--ly-scroll-max:min(70vh,50rem);--ly-switcher-threshold:42rem;--ly-sidebar-size:18rem;--ly-sidebar-content-min:50%;--ly-grid-columns:12;--ly-grid-min:16rem;--ly-split-min:20rem;--ly-pane-min:18rem;--ly-pane-size:22rem;--ly-media-min:18rem;--ly-media-size:14rem;--ly-reel-item-min:16rem;--ly-reel-item-max:28rem;--ly-frame-ratio:16 / 9;--ly-split-primary:1fr;--ly-split-secondary:1fr;--ly-recipe-rail:18rem;--ly-recipe-aside:22rem;--ly-gallery-min:12rem;--ly-card-grid-min:16rem;--ly-app-shell-base-rows:auto auto minmax(0,1fr) auto auto;--ly-app-shell-medium-areas:"header header" "sidebar main" "aside main" "footer footer";--ly-app-shell-medium-rows:auto auto minmax(0,1fr) auto;--ly-app-shell-medium-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr);--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "sidebar footer footer";--ly-app-shell-wide-rows:auto minmax(0,1fr) auto;--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.35fr);--ly-dashboard-medium-areas:"header header" "nav nav" "main aside" "footer footer";--ly-dashboard-medium-columns:minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.35fr);--ly-dashboard-wide-areas:"nav header header" "nav main aside" "nav footer footer";--ly-dashboard-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.35fr);--ly-docs-wide-areas:"header header" "nav main" "nav aside" "footer footer";--ly-docs-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr);--ly-list-detail-wide-areas:"primary secondary" "actions actions";--ly-list-detail-wide-columns:minmax(min(100%,var(--ly-pane-size)),0.7fr) minmax(0,1fr);--ly-split-hero-wide-areas:"content media" "actions media";--ly-split-hero-wide-columns:minmax(0,var(--ly-split-primary,1fr)) minmax(0,var(--ly-split-secondary,1fr));container-name:ly-scope;container-type:inline-size;min-inline-size:0;min-block-size:0}@supports (height:100dvh){:where(.ly-root){--ly-cover-min:100dvh;--ly-shell-min:100dvh;--ly-scroll-max:min(70dvh,50rem)}}}@layer ly.reset{:where(.ly-root),:where(.ly-root *){box-sizing:border-box}:where(.ly-root){margin:0}:where(.ly-root img),:where(.ly-root svg),:where(.ly-root video),:where(.ly-root canvas),:where(.ly-root iframe){max-inline-size:100%}}@layer ly.tokens{@media (max-height:44rem){:where(.ly-root){--ly-gap:min(var(--ly-profile-gap),var(--ly-space-4));--ly-grid-gap:var(--ly-gap);--ly-stack-gap:min(var(--ly-space-4),3vh);--ly-cluster-gap:min(var(--ly-space-3),2vh);--ly-section-padding-block:clamp(1.5rem,6vh,3rem);--ly-header-height:3.5rem;--ly-scroll-max:min(62vh,34rem)}@supports (height:100dvh){:where(.ly-root){--ly-stack-gap:min(var(--ly-space-4),3dvh);--ly-cluster-gap:min(var(--ly-space-3),2dvh);--ly-section-padding-block:clamp(1.5rem,6dvh,3rem);--ly-scroll-max:min(62dvh,34rem)}}}@media (max-height:30rem){:where(.ly-root){--ly-cover-min:auto;--ly-shell-min:auto;--ly-sticky-position:static;--ly-section-padding-block:var(--ly-space-5);--ly-scroll-max:max(12rem,calc(100vh - 8rem))}@supports (height:100dvh){:where(.ly-root){--ly-scroll-max:max(12rem,calc(100dvh - 8rem))}}}}@layer ly.wrappers{.ly-wrapper{--ly-wrapper-max:var(--ly-personality-wrapper-max,var(--ly-wrapper-content));--ly-wrapper-fluid-gutter:clamp(1rem,3vw,3rem);--ly-wrapper-local-gutter:max( var(--ly-wrapper-fluid-gutter),var(--ly-safe-area-inline) );container-name:ly-scope;container-type:inline-size;inline-size:min(100%,calc(var(--ly-wrapper-max) + (var(--ly-wrapper-local-gutter) * 2)));max-inline-size:100%;margin-inline:auto;padding-inline:var(--ly-wrapper-local-gutter)}@supports (width:1cqi){.ly-wrapper{--ly-wrapper-fluid-gutter:clamp(1rem,3cqi,3rem)}}.ly-wrapper--compact{--ly-wrapper-max:var(--ly-wrapper-compact)}.ly-wrapper--prose{--ly-wrapper-max:var(--ly-wrapper-prose)}.ly-wrapper--content{--ly-wrapper-max:var(--ly-wrapper-content)}.ly-wrapper--wide{--ly-wrapper-max:var(--ly-wrapper-wide)}.ly-wrapper--full{--ly-wrapper-max:100%}.ly-wrapper--breakout{--ly-wrapper-max:100%;--ly-breakout-content-size:min( var(--ly-wrapper-content),calc(100% - (var(--ly-wrapper-local-gutter) * 2)) );--ly-breakout-feature-size:min( var(--ly-wrapper-wide),calc(100% - (var(--ly-wrapper-local-gutter) * 2)) );display:grid;grid-template-columns:[full-start] minmax(min(100%,var(--ly-wrapper-local-gutter)),1fr) [feature-start] minmax( 0,calc((var(--ly-breakout-feature-size) - var(--ly-breakout-content-size)) / 2) ) [content-start] minmax(0,var(--ly-breakout-content-size)) [content-end] minmax( 0,calc((var(--ly-breakout-feature-size) - var(--ly-breakout-content-size)) / 2) ) [feature-end] minmax(min(100%,var(--ly-wrapper-local-gutter)),1fr) [full-end];inline-size:100%;padding-inline:0}.ly-wrapper--breakout>*{grid-column:content;min-inline-size:0;max-inline-size:100%}.ly-wrapper--breakout>:where(.ly-lane--content,[data-ly-lane="content"]){grid-column:content}.ly-wrapper--breakout>:where(.ly-lane--feature,[data-ly-lane="feature"]){grid-column:feature}.ly-wrapper--breakout>:where(.ly-lane--full,[data-ly-lane="full"]){grid-column:full}}@layer ly.primitives{.ly-page{min-block-size:100vh;min-block-size:100dvh;padding-block-start:var(--ly-safe-area-block-start);padding-block-end:var(--ly-safe-area-block-end)}.ly-header,.ly-footer,.ly-main,.ly-section,.ly-surface{min-inline-size:0;min-block-size:0}.ly-header{min-block-size:var(--ly-header-height)}.ly-header--sticky{position:var(--ly-sticky-position,sticky);inset-block-start:0;z-index:var(--ly-z-header,20)}.ly-section{padding-block:var(--ly-section-padding-block)}.ly-section--compact{--ly-section-padding-block:var(--ly-space-7)}.ly-section--flush{--ly-section-padding-block:0}.ly-readable{max-inline-size:var(--ly-wrapper-prose)}.ly-stack{display:flex;flex-direction:column;gap:var(--ly-stack-gap)}.ly-cluster{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ly-cluster-gap)}.ly-center{inline-size:min(100%,var(--ly-center-max,var(--ly-wrapper-content)));margin-inline:auto}.ly-cover{display:flex;flex-direction:column;min-block-size:100vh;min-block-size:var(--ly-cover-min);padding-block:var(--ly-cover-padding,var(--ly-space-5))}.ly-cover>:where(.ly-cover__center,[data-ly-cover-center]){margin-block:auto}.ly-switcher{display:flex;flex-wrap:wrap;gap:var(--ly-gap)}.ly-switcher>*{flex-basis:calc((var(--ly-switcher-threshold) - 100%) * 999);flex-grow:1;min-inline-size:0}.ly-sidebar{display:flex;flex-wrap:wrap;gap:var(--ly-gap)}.ly-sidebar>:where(.ly-sidebar__side,[data-ly-sidebar="side"]){flex-basis:var(--ly-sidebar-size);flex-grow:1;min-inline-size:min(100%,var(--ly-sidebar-size))}.ly-sidebar>:where(.ly-sidebar__content,[data-ly-sidebar="content"]){flex-basis:0;flex-grow:999;min-inline-size:min(100%,var(--ly-sidebar-content-min))}.ly-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-grid-min)),1fr));gap:var(--ly-grid-gap)}.ly-grid--fixed{grid-template-columns:repeat(var(--ly-grid-columns),minmax(0,1fr))}.ly-split{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-split-min)),1fr));gap:var(--ly-gap);align-items:var(--ly-split-align,center)}.ly-panes{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-pane-min)),1fr));gap:var(--ly-gap)}.ly-media{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-media-min)),1fr));gap:var(--ly-gap);align-items:var(--ly-media-align,start)}.ly-media>:where(.ly-media__actions,[data-ly-media="actions"]){min-inline-size:min(100%,var(--ly-media-size))}.ly-grid>*,.ly-split>*,.ly-panes>*,.ly-media>*{min-inline-size:0;min-block-size:0}.ly-reel{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(min(100%,var(--ly-reel-item-min)),var(--ly-reel-item-max));gap:var(--ly-grid-gap);overflow-x:auto;overscroll-behavior-inline:contain;scroll-snap-type:inline proximity}.ly-reel>*{min-inline-size:0;scroll-snap-align:start}.ly-frame{aspect-ratio:var(--ly-frame-ratio);overflow:hidden}.ly-frame>*{inline-size:100%;block-size:100%;object-fit:cover}.ly-scroll{min-block-size:0;max-block-size:var(--ly-scroll-max);overflow-x:clip;overflow-y:auto;overscroll-behavior-block:contain}}@layer ly.recipes{:where( [data-ly-recipe="app-shell"],[data-ly-recipe="dashboard"],[data-ly-recipe="docs"],[data-ly-recipe="list-detail"],[data-ly-recipe="split-hero"],[data-ly-recipe="gallery"],[data-ly-recipe="card-grid"] ){container-name:ly-scope;container-type:inline-size;min-inline-size:0;min-block-size:0}:where( [data-ly-recipe="app-shell"],[data-ly-recipe="dashboard"],[data-ly-recipe="docs"],[data-ly-recipe="list-detail"],[data-ly-recipe="split-hero"] ){display:grid;grid-template-columns:minmax(0,1fr);gap:var(--ly-gap)}[data-ly-recipe="app-shell"]{min-block-size:100vh;min-block-size:var(--ly-shell-min);grid-template-areas:"header" "sidebar" "main" "aside" "footer";grid-template-rows:var(--ly-app-shell-base-rows)}[data-ly-recipe="dashboard"],[data-ly-recipe="docs"]{grid-template-areas:"header" "nav" "main" "aside" "footer"}[data-ly-recipe="list-detail"]{grid-template-areas:"primary" "secondary" "actions"}[data-ly-recipe="split-hero"]{grid-template-areas:"content" "media" "actions";align-items:var(--ly-split-hero-align,center)}[data-ly-recipe="gallery"],[data-ly-recipe="card-grid"]{display:grid;gap:var(--ly-grid-gap)}[data-ly-recipe="gallery"]{grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-gallery-min)),1fr))}[data-ly-recipe="card-grid"]{grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-card-grid-min)),1fr))}:where( [data-ly-recipe="gallery"],[data-ly-recipe="card-grid"] )[data-ly-responsive="manual"]{grid-template-columns:minmax(0,1fr)}:where( [data-ly-recipe="app-shell"],[data-ly-recipe="dashboard"],[data-ly-recipe="docs"],[data-ly-recipe="list-detail"],[data-ly-recipe="split-hero"],[data-ly-recipe="gallery"],[data-ly-recipe="card-grid"] )>*{min-inline-size:0;min-block-size:0}[data-ly-area="header"]{grid-area:header}[data-ly-area="nav"]{grid-area:nav}[data-ly-area="sidebar"]{grid-area:sidebar}[data-ly-area="main"]{grid-area:main}[data-ly-area="aside"]{grid-area:aside}[data-ly-area="footer"]{grid-area:footer}[data-ly-area="content"]{grid-area:content}[data-ly-area="media"]{grid-area:media}[data-ly-area="actions"]{grid-area:actions}[data-ly-area="primary"]{grid-area:primary}[data-ly-area="secondary"]{grid-area:secondary}@container ly-scope (min-width:42rem){[data-ly-recipe="split-hero"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-split-hero-wide-areas);grid-template-columns:var(--ly-split-hero-wide-columns)}}@container ly-scope (min-width:44rem){[data-ly-recipe="list-detail"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-list-detail-wide-areas);grid-template-columns:var(--ly-list-detail-wide-columns)}}@container ly-scope (min-width:48rem){[data-ly-recipe="docs"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-docs-wide-areas);grid-template-columns:var(--ly-docs-wide-columns)}}@container ly-scope (min-width:52rem){[data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-app-shell-medium-areas);grid-template-columns:var(--ly-app-shell-medium-columns);grid-template-rows:var(--ly-app-shell-medium-rows)}[data-ly-recipe="dashboard"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-dashboard-medium-areas);grid-template-columns:var(--ly-dashboard-medium-columns)}}@container ly-scope (min-width:72rem){[data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-app-shell-wide-areas);grid-template-columns:var(--ly-app-shell-wide-columns);grid-template-rows:var(--ly-app-shell-wide-rows)}[data-ly-recipe="dashboard"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-dashboard-wide-areas);grid-template-columns:var(--ly-dashboard-wide-columns)}}}@layer ly.utilities{.ly-cols-1{--ly-grid-columns:1}.ly-cols-2{--ly-grid-columns:2}.ly-cols-3{--ly-grid-columns:3}.ly-cols-4{--ly-grid-columns:4}.ly-cols-6{--ly-grid-columns:6}.ly-cols-12{--ly-grid-columns:12}.ly-span-1{grid-column:span 1}.ly-span-2{grid-column:span 2}.ly-span-3{grid-column:span 3}.ly-span-4{grid-column:span 4}.ly-span-full{grid-column:1 / -1}.ly-gap-0{--ly-gap:var(--ly-space-0);--ly-grid-gap:var(--ly-space-0);--ly-stack-gap:var(--ly-space-0);--ly-cluster-gap:var(--ly-space-0)}.ly-gap-2{--ly-gap:var(--ly-space-2);--ly-grid-gap:var(--ly-space-2);--ly-stack-gap:var(--ly-space-2);--ly-cluster-gap:var(--ly-space-2)}.ly-gap-4{--ly-gap:var(--ly-space-4);--ly-grid-gap:var(--ly-space-4);--ly-stack-gap:var(--ly-space-4);--ly-cluster-gap:var(--ly-space-4)}.ly-gap-6{--ly-gap:var(--ly-space-6);--ly-grid-gap:var(--ly-space-6);--ly-stack-gap:var(--ly-space-6);--ly-cluster-gap:var(--ly-space-6)}.ly-gap-8{--ly-gap:var(--ly-space-8);--ly-grid-gap:var(--ly-space-8);--ly-stack-gap:var(--ly-space-8);--ly-cluster-gap:var(--ly-space-8)}.ly-pad-0{padding:var(--ly-space-0)}.ly-pad-2{padding:var(--ly-space-2)}.ly-pad-4{padding:var(--ly-space-4)}.ly-pad-6{padding:var(--ly-space-6)}.ly-pad-8{padding:var(--ly-space-8)}.ly-mx-auto{margin-inline:auto}.ly-w-full{inline-size:100%}.ly-h-full{block-size:100%}.ly-min-h-screen{min-block-size:100vh;min-block-size:100dvh}.ly-overflow-auto{overflow:auto}.ly-overflow-hidden{overflow:hidden}.ly-items-start{align-items:flex-start}.ly-items-center{align-items:center}.ly-items-end{align-items:flex-end}.ly-items-stretch{align-items:stretch}.ly-justify-start{justify-content:flex-start}.ly-justify-center{justify-content:center}.ly-justify-end{justify-content:flex-end}.ly-justify-between{justify-content:space-between}.ly-frame-1x1{--ly-frame-ratio:1 / 1}.ly-frame-3x2{--ly-frame-ratio:3 / 2}.ly-frame-4x3{--ly-frame-ratio:4 / 3}.ly-frame-16x9{--ly-frame-ratio:16 / 9}.ly-frame-21x9{--ly-frame-ratio:21 / 9}.ly-hidden{display:none !important}.ly-visually-hidden{position:absolute !important;inline-size:1px !important;block-size:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0,0,0,0) !important;clip-path:inset(50%) !important;white-space:nowrap !important}}@layer ly.personalities.minimal-saas{:where(.ly-root[data-ly-layout="minimal-saas"]){--ly-personality-wrapper-max:88rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:16rem;--ly-recipe-rail:15rem;--ly-recipe-aside:16rem;--ly-split-primary:1.05fr;--ly-split-secondary:0.95fr}}@layer ly.personalities.bento{:where(.ly-root[data-ly-layout="bento"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:12rem;--ly-card-grid-min:14rem;--ly-gallery-min:11rem;--ly-recipe-rail:14rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header header" "sidebar main main" "aside main main" "footer footer footer";--ly-app-shell-wide-rows:auto auto minmax(0,1fr) auto;--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.3fr) minmax(0,1fr) minmax(0,1fr)}}@layer ly.personalities.maximalist{:where(.ly-root[data-ly-layout="maximalist"]){--ly-personality-wrapper-max:100%;--ly-profile-gap:var(--ly-space-7);--ly-grid-min:10rem;--ly-card-grid-min:13rem;--ly-gallery-min:10rem;--ly-recipe-rail:15rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header header header" "main main sidebar aside" "main main footer footer";--ly-app-shell-wide-columns:minmax(0,1.4fr) minmax(0,0.6fr) minmax(min(100%,var(--ly-recipe-rail)),0.32fr) minmax(min(100%,var(--ly-recipe-aside)),0.38fr)}}@layer ly.personalities.bauhaus{:where(.ly-root[data-ly-layout="bauhaus"]){--ly-personality-wrapper-max:96rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:13rem;--ly-recipe-rail:14rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header header header" "sidebar main main aside" "sidebar footer footer aside";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.24fr) minmax(0,0.7fr) minmax(0,1.3fr) minmax(min(100%,var(--ly-recipe-aside)),0.3fr)}}@layer ly.personalities.tactile{:where(.ly-root[data-ly-layout="tactile"]){--ly-personality-wrapper-max:82rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:15rem;--ly-recipe-rail:17rem;--ly-recipe-aside:20rem;--ly-app-shell-wide-areas:"header header" "sidebar main" "aside main" "footer footer";--ly-app-shell-wide-rows:auto auto minmax(0,1fr) auto;--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.28fr) minmax(0,1fr);--ly-list-detail-wide-columns:minmax(min(100%,var(--ly-pane-size)),0.5fr) minmax(0,0.9fr)}}@layer ly.personalities.neumorphism{:where(.ly-root[data-ly-layout="neumorphism"]){--ly-personality-wrapper-max:84rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:17rem;--ly-recipe-rail:17rem;--ly-recipe-aside:19rem;--ly-app-shell-wide-areas:"header sidebar" "main sidebar" "aside sidebar" "footer sidebar";--ly-app-shell-wide-rows:auto minmax(0,1fr) auto auto;--ly-app-shell-wide-columns:minmax(0,1fr) minmax(min(100%,var(--ly-recipe-rail)),0.3fr);--ly-list-detail-wide-columns:minmax(0,1.1fr) minmax(min(100%,var(--ly-pane-size)),0.7fr)}}@layer ly.personalities.retrofuturism{:where(.ly-root[data-ly-layout="retrofuturism"]){--ly-personality-wrapper-max:106rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:14rem;--ly-recipe-rail:14rem;--ly-recipe-aside:17rem;--ly-app-shell-wide-areas:"sidebar header aside" "sidebar main aside" "sidebar footer aside";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.24fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.3fr)}}@layer ly.personalities.brutalism{:where(.ly-root[data-ly-layout="brutalism"]){--ly-personality-wrapper-max:100rem;--ly-profile-gap:var(--ly-space-3);--ly-grid-min:14rem;--ly-recipe-rail:16rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header sidebar" "main aside sidebar" "footer footer sidebar";--ly-app-shell-wide-columns:minmax(0,1.3fr) minmax(0,0.7fr) minmax(min(100%,var(--ly-recipe-rail)),0.34fr)}}@layer ly.personalities.cyberpunk{:where(.ly-root[data-ly-layout="cyberpunk"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:12rem;--ly-recipe-rail:13rem;--ly-recipe-aside:19rem;--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "sidebar footer footer";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.2fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.32fr)}}@layer ly.personalities.y2k{:where(.ly-root[data-ly-layout="y2k"]){--ly-personality-wrapper-max:90rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:13rem;--ly-recipe-rail:18rem;--ly-recipe-aside:16rem;--ly-app-shell-wide-areas:"header header sidebar" "aside main sidebar" "footer footer sidebar";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-aside)),0.34fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-rail)),0.28fr)}}@layer ly.personalities.retro-glass{:where(.ly-root[data-ly-layout="retro-glass"]){--ly-personality-wrapper-max:104rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:15rem;--ly-recipe-rail:18rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header sidebar" "main main sidebar" "aside footer footer";--ly-app-shell-wide-columns:minmax(0,1.25fr) minmax(0,0.75fr) minmax(min(100%,var(--ly-recipe-rail)),0.32fr)}}@layer ly.personalities.f-pattern{:where(.ly-root[data-ly-layout="f-pattern"]){--ly-personality-wrapper-max:92rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:14rem;--ly-recipe-rail:16rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "sidebar footer footer";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.26fr) minmax(0,1.35fr) minmax(min(100%,var(--ly-recipe-aside)),0.65fr);--ly-split-hero-wide-columns:minmax(0,1.8fr) minmax(0,0.8fr)}}@layer ly.personalities.z-pattern{:where(.ly-root[data-ly-layout="z-pattern"]){--ly-personality-wrapper-max:108rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:14rem;--ly-recipe-rail:17rem;--ly-recipe-aside:19rem;--ly-app-shell-wide-areas:"header header sidebar" "main aside sidebar" "footer aside sidebar";--ly-app-shell-wide-columns:minmax(0,1.45fr) minmax(min(100%,var(--ly-recipe-aside)),0.55fr) minmax(min(100%,var(--ly-recipe-rail)),0.3fr);--ly-split-hero-wide-columns:minmax(0,1.5fr) minmax(0,0.5fr)}}@layer ly.personalities.split-screen{:where(.ly-root[data-ly-layout="split-screen"]){--ly-personality-wrapper-max:100%;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:20rem;--ly-recipe-rail:24rem;--ly-recipe-aside:24rem;--ly-app-shell-wide-areas:"header header" "main sidebar" "aside sidebar" "footer footer";--ly-app-shell-wide-rows:auto minmax(0,1fr) auto auto;--ly-app-shell-wide-columns:minmax(0,1fr) minmax(0,1fr);--ly-split-hero-wide-columns:minmax(0,1fr) minmax(0,1fr);--ly-list-detail-wide-columns:minmax(0,1fr) minmax(0,1fr)}}@layer ly.personalities.mondrian{:where(.ly-root[data-ly-layout="mondrian"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-3);--ly-grid-min:11rem;--ly-recipe-rail:13rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "footer footer aside";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.22fr) minmax(0,1.45fr) minmax(min(100%,var(--ly-recipe-aside)),0.55fr)}}@layer ly.personalities.synthwave{:where(.ly-root[data-ly-layout="synthwave"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:14rem;--ly-recipe-rail:16rem;--ly-recipe-aside:20rem;--ly-app-shell-wide-areas:"header header header" "sidebar main aside" "footer footer footer";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.28fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.34fr);--ly-reel-item-min:20rem}} diff --git a/dist/personalities/bento.css b/dist/personalities/bento.css index 33d50f1..b09b3d9 100644 --- a/dist/personalities/bento.css +++ b/dist/personalities/bento.css @@ -14,6 +14,7 @@ "sidebar main main" "aside main main" "footer footer footer"; + --ly-app-shell-wide-rows: auto auto minmax(0, 1fr) auto; --ly-app-shell-wide-columns: minmax(min(100%, var(--ly-recipe-rail)), 0.3fr) minmax(0, 1fr) diff --git a/dist/personalities/neumorphism.css b/dist/personalities/neumorphism.css index be83073..48daa23 100644 --- a/dist/personalities/neumorphism.css +++ b/dist/personalities/neumorphism.css @@ -12,6 +12,7 @@ "main sidebar" "aside sidebar" "footer sidebar"; + --ly-app-shell-wide-rows: auto minmax(0, 1fr) auto auto; --ly-app-shell-wide-columns: minmax(0, 1fr) minmax(min(100%, var(--ly-recipe-rail)), 0.3fr); diff --git a/dist/personalities/split-screen.css b/dist/personalities/split-screen.css index 4c95a76..a828162 100644 --- a/dist/personalities/split-screen.css +++ b/dist/personalities/split-screen.css @@ -16,6 +16,7 @@ "main sidebar" "aside sidebar" "footer footer"; + --ly-app-shell-wide-rows: auto minmax(0, 1fr) auto auto; --ly-app-shell-wide-columns: minmax(0, 1fr) minmax(0, 1fr); diff --git a/dist/personalities/tactile.css b/dist/personalities/tactile.css index e3ee930..f82dd90 100644 --- a/dist/personalities/tactile.css +++ b/dist/personalities/tactile.css @@ -12,6 +12,7 @@ "sidebar main" "aside main" "footer footer"; + --ly-app-shell-wide-rows: auto auto minmax(0, 1fr) auto; --ly-app-shell-wide-columns: minmax(min(100%, var(--ly-recipe-rail)), 0.28fr) minmax(0, 1fr); diff --git a/dist/recipes.css b/dist/recipes.css index 05e525c..bd8f16e 100644 --- a/dist/recipes.css +++ b/dist/recipes.css @@ -42,7 +42,7 @@ "main" "aside" "footer"; - grid-template-rows: auto auto minmax(0, 1fr) auto auto; + grid-template-rows: var(--ly-app-shell-base-rows); } [data-ly-recipe="dashboard"], @@ -173,6 +173,7 @@ [data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]) { grid-template-areas: var(--ly-app-shell-medium-areas); grid-template-columns: var(--ly-app-shell-medium-columns); + grid-template-rows: var(--ly-app-shell-medium-rows); } [data-ly-recipe="dashboard"]:not([data-ly-responsive="manual"]) { @@ -185,6 +186,7 @@ [data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]) { grid-template-areas: var(--ly-app-shell-wide-areas); grid-template-columns: var(--ly-app-shell-wide-columns); + grid-template-rows: var(--ly-app-shell-wide-rows); } [data-ly-recipe="dashboard"]:not([data-ly-responsive="manual"]) { diff --git a/docs/superpowers/plans/2026-08-25-layout-style-css-3-0-2-correctness.md b/docs/superpowers/plans/2026-08-25-layout-style-css-3-0-2-correctness.md index 83553fa..9e8e839 100644 --- a/docs/superpowers/plans/2026-08-25-layout-style-css-3-0-2-correctness.md +++ b/docs/superpowers/plans/2026-08-25-layout-style-css-3-0-2-correctness.md @@ -240,13 +240,13 @@ Add the matching row consumer to each automatic query: - [ ] **Step 6: Add four-row personality tokens** -Add this token beside the App Shell wide-area token in Bento, Split Screen, and Tactile: +Add this token beside the App Shell wide-area token in Bento and Tactile: ```css --ly-app-shell-wide-rows: auto auto minmax(0, 1fr) auto; ``` -Add this token in Neumorphism because its `main` occupies the second row: +Add this token in Neumorphism and Split Screen because their `main` occupies the second row: ```css --ly-app-shell-wide-rows: auto minmax(0, 1fr) auto auto; diff --git a/styles/foundation.css b/styles/foundation.css index 9801a80..94929bd 100644 --- a/styles/foundation.css +++ b/styles/foundation.css @@ -64,11 +64,13 @@ --ly-gallery-min: 12rem; --ly-card-grid-min: 16rem; + --ly-app-shell-base-rows: auto auto minmax(0, 1fr) auto auto; --ly-app-shell-medium-areas: "header header" "sidebar main" "aside main" "footer footer"; + --ly-app-shell-medium-rows: auto auto minmax(0, 1fr) auto; --ly-app-shell-medium-columns: minmax(min(100%, var(--ly-recipe-rail)), auto) minmax(0, 1fr); @@ -76,6 +78,7 @@ "sidebar header header" "sidebar main aside" "sidebar footer footer"; + --ly-app-shell-wide-rows: auto minmax(0, 1fr) auto; --ly-app-shell-wide-columns: minmax(min(100%, var(--ly-recipe-rail)), auto) minmax(0, 1fr) diff --git a/styles/personalities/bento.css b/styles/personalities/bento.css index 33d50f1..b09b3d9 100644 --- a/styles/personalities/bento.css +++ b/styles/personalities/bento.css @@ -14,6 +14,7 @@ "sidebar main main" "aside main main" "footer footer footer"; + --ly-app-shell-wide-rows: auto auto minmax(0, 1fr) auto; --ly-app-shell-wide-columns: minmax(min(100%, var(--ly-recipe-rail)), 0.3fr) minmax(0, 1fr) diff --git a/styles/personalities/neumorphism.css b/styles/personalities/neumorphism.css index be83073..48daa23 100644 --- a/styles/personalities/neumorphism.css +++ b/styles/personalities/neumorphism.css @@ -12,6 +12,7 @@ "main sidebar" "aside sidebar" "footer sidebar"; + --ly-app-shell-wide-rows: auto minmax(0, 1fr) auto auto; --ly-app-shell-wide-columns: minmax(0, 1fr) minmax(min(100%, var(--ly-recipe-rail)), 0.3fr); diff --git a/styles/personalities/split-screen.css b/styles/personalities/split-screen.css index 4c95a76..a828162 100644 --- a/styles/personalities/split-screen.css +++ b/styles/personalities/split-screen.css @@ -16,6 +16,7 @@ "main sidebar" "aside sidebar" "footer footer"; + --ly-app-shell-wide-rows: auto minmax(0, 1fr) auto auto; --ly-app-shell-wide-columns: minmax(0, 1fr) minmax(0, 1fr); diff --git a/styles/personalities/tactile.css b/styles/personalities/tactile.css index e3ee930..f82dd90 100644 --- a/styles/personalities/tactile.css +++ b/styles/personalities/tactile.css @@ -12,6 +12,7 @@ "sidebar main" "aside main" "footer footer"; + --ly-app-shell-wide-rows: auto auto minmax(0, 1fr) auto; --ly-app-shell-wide-columns: minmax(min(100%, var(--ly-recipe-rail)), 0.28fr) minmax(0, 1fr); diff --git a/styles/recipes.css b/styles/recipes.css index 05e525c..bd8f16e 100644 --- a/styles/recipes.css +++ b/styles/recipes.css @@ -42,7 +42,7 @@ "main" "aside" "footer"; - grid-template-rows: auto auto minmax(0, 1fr) auto auto; + grid-template-rows: var(--ly-app-shell-base-rows); } [data-ly-recipe="dashboard"], @@ -173,6 +173,7 @@ [data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]) { grid-template-areas: var(--ly-app-shell-medium-areas); grid-template-columns: var(--ly-app-shell-medium-columns); + grid-template-rows: var(--ly-app-shell-medium-rows); } [data-ly-recipe="dashboard"]:not([data-ly-responsive="manual"]) { @@ -185,6 +186,7 @@ [data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]) { grid-template-areas: var(--ly-app-shell-wide-areas); grid-template-columns: var(--ly-app-shell-wide-columns); + grid-template-rows: var(--ly-app-shell-wide-rows); } [data-ly-recipe="dashboard"]:not([data-ly-responsive="manual"]) { diff --git a/test/demo-smoke.test.mjs b/test/demo-smoke.test.mjs index 074e1b4..fa2b950 100644 --- a/test/demo-smoke.test.mjs +++ b/test/demo-smoke.test.mjs @@ -598,6 +598,64 @@ const verifyTopologyEdges = async (page, baseUrl) => { } }; +/** + * Verifies that each automatic App Shell topology exposes one explicit row per + * named area row and gives the primary workspace more height than intrinsic + * header and footer tracks. + * + * @param {import("@playwright/test").Page} page Active browser page. + * @param {string} baseUrl Demo server URL. + * @returns {Promise} + */ +const verifyAppShellRowGeometry = async (page, baseUrl) => { + const cases = [ + { label: "base", width: "51rem", personality: "minimal-saas", rows: 5 }, + { label: "medium", width: "53rem", personality: "minimal-saas", rows: 4 }, + ...personalities.map((personality) => ({ + label: `wide ${personality}`, + width: "73rem", + personality, + rows: ["bento", "neumorphism", "split-screen", "tactile"].includes(personality) + ? 4 + : 3 + })) + ]; + + await page.setViewportSize({ width: 1920, height: 1080 }); + for (const testCase of cases) { + await page.goto( + `${baseUrl}?ecosystem=layout-only&wrapper=full&recipe=app-shell&container=${testCase.width}&height=50rem&personality=${testCase.personality}`, + { waitUntil: "domcontentloaded" } + ); + await page.waitForFunction(() => document.body.dataset.demoReady === "true"); + const geometry = await page.locator('[data-ly-recipe="app-shell"]').evaluate((recipe) => { + const style = getComputedStyle(recipe); + const areaRows = style.gridTemplateAreas.match(/"[^"]+"/g) ?? []; + const main = recipe.querySelector('[data-ly-area="main"]').getBoundingClientRect(); + const header = recipe.querySelector('[data-ly-area="header"]').getBoundingClientRect(); + const footer = recipe.querySelector('[data-ly-area="footer"]').getBoundingClientRect(); + return { + areaRows: areaRows.length, + explicitRows: style.gridTemplateRows.split(/\s+/).filter(Boolean).length, + mainHeight: main.height, + headerHeight: header.height, + footerHeight: footer.height + }; + }); + + assert.equal(geometry.areaRows, testCase.rows, `${testCase.label} area-row count drifted.`); + assert.equal( + geometry.explicitRows, + testCase.rows, + `${testCase.label} explicit rows must match its area rows.` + ); + assert( + geometry.mainHeight > geometry.headerHeight && geometry.mainHeight > geometry.footerHeight, + `${testCase.label} must allocate flexible height to the primary workspace.` + ); + } +}; + const verifyManualAndNearestContainer = async (page, baseUrl) => { await page.setViewportSize({ width: 1920, height: 1080 }); await page.goto( @@ -1102,6 +1160,7 @@ try { await verifySynthwaveVisualRecommendations(page, server.baseUrl); await verifyIdentityAndControls(page, server.baseUrl); await verifyTopologyEdges(page, server.baseUrl); + await verifyAppShellRowGeometry(page, server.baseUrl); await verifyManualAndNearestContainer(page, server.baseUrl); await verifyHeightBehavior(page, server.baseUrl); await verifyDefaultFontHeightTiers(server.baseUrl); diff --git a/test/layout-css-contract.test.mjs b/test/layout-css-contract.test.mjs index 1007323..b56191a 100644 --- a/test/layout-css-contract.test.mjs +++ b/test/layout-css-contract.test.mjs @@ -330,6 +330,24 @@ assert( for (const threshold of ["42rem", "44rem", "48rem", "52rem", "72rem"]) { assert(recipes.includes(`@container ly-scope (min-width: ${threshold})`), `Missing ${threshold} recipe tier`); } +const appShellRowTokens = { + base: "--ly-app-shell-base-rows", + medium: "--ly-app-shell-medium-rows", + wide: "--ly-app-shell-wide-rows" +}; +for (const [topology, token] of Object.entries(appShellRowTokens)) { + assert(foundation.includes(`${token}:`), `App Shell ${topology} rows need a public token.`); + assert( + recipes.includes(`grid-template-rows: var(${token})`), + `App Shell ${topology} topology must consume ${token}.` + ); +} +for (const personality of ["bento", "neumorphism", "split-screen", "tactile"]) { + assert( + readStyle(`personalities/${personality}.css`).includes("--ly-app-shell-wide-rows:"), + `${personality} must describe its four-row wide App Shell.` + ); +} assert( recipes.includes("container-name: ly-scope;") && recipes.includes("container-type: inline-size;"), "Recipe roots must scope nested responsive compositions" From 2226354380c30f359abf83bb7c2de36ca85778d4 Mon Sep 17 00:00:00 2001 From: Foscat Date: Tue, 25 Aug 2026 15:18:11 -0500 Subject: [PATCH 4/7] fix: make section and gutter tokens effective --- dist/foundation.css | 15 +++++++- dist/layout-style-css.css | 28 ++++++++------ dist/layout-style-css.min.css | 2 +- dist/primitives.css | 4 +- dist/wrappers.css | 9 +---- manifest.json | 3 +- styles/foundation.css | 15 +++++++- styles/primitives.css | 4 +- styles/wrappers.css | 9 +---- test/demo-smoke.test.mjs | 63 +++++++++++++++++++++++++++++++ test/layout-css-contract.test.mjs | 16 ++++++-- test/manifest-contract.test.mjs | 3 +- 12 files changed, 128 insertions(+), 43 deletions(-) diff --git a/dist/foundation.css b/dist/foundation.css index 94929bd..368283c 100644 --- a/dist/foundation.css +++ b/dist/foundation.css @@ -22,14 +22,14 @@ --ly-wrapper-prose: 68ch; --ly-wrapper-content: 72rem; --ly-wrapper-wide: 112rem; - --ly-page-padding-inline: clamp(1rem, 4vw, 3rem); + --ly-page-padding-inline: clamp(1rem, 3vw, 3rem); --ly-safe-area-inline: max( env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px) ); --ly-safe-area-block-start: env(safe-area-inset-top, 0px); --ly-safe-area-block-end: env(safe-area-inset-bottom, 0px); - --ly-wrapper-gutter: max(var(--ly-page-padding-inline), var(--ly-safe-area-inline)); + --ly-wrapper-gutter: var(--ly-page-padding-inline); --ly-profile-gap: var(--ly-space-5); --ly-gap: var(--ly-profile-gap); @@ -37,6 +37,7 @@ --ly-stack-gap: var(--ly-space-4); --ly-cluster-gap: var(--ly-space-3); --ly-section-padding-block: clamp(3rem, 7vh, 6rem); + --ly-section-padding-block-compact: clamp(1.5rem, 3.5vh, 3rem); --ly-header-height: 4.5rem; --ly-sticky-position: sticky; --ly-cover-min: 100vh; @@ -128,6 +129,13 @@ min-block-size: 0; } + /* Prefer the nearest layout scope for gutters when container units parse. */ + @supports (width: 1cqi) { + :where(.ly-root) { + --ly-page-padding-inline: clamp(1rem, 3cqi, 3rem); + } + } + /* Enhance viewport-bound tokens only when dynamic viewport units parse. */ @supports (height: 100dvh) { :where(.ly-root) { @@ -165,6 +173,7 @@ --ly-stack-gap: min(var(--ly-space-4), 3vh); --ly-cluster-gap: min(var(--ly-space-3), 2vh); --ly-section-padding-block: clamp(1.5rem, 6vh, 3rem); + --ly-section-padding-block-compact: clamp(0.75rem, 3vh, 1.25rem); --ly-header-height: 3.5rem; --ly-scroll-max: min(62vh, 34rem); } @@ -174,6 +183,7 @@ --ly-stack-gap: min(var(--ly-space-4), 3dvh); --ly-cluster-gap: min(var(--ly-space-3), 2dvh); --ly-section-padding-block: clamp(1.5rem, 6dvh, 3rem); + --ly-section-padding-block-compact: clamp(0.75rem, 3dvh, 1.25rem); --ly-scroll-max: min(62dvh, 34rem); } } @@ -185,6 +195,7 @@ --ly-shell-min: auto; --ly-sticky-position: static; --ly-section-padding-block: var(--ly-space-5); + --ly-section-padding-block-compact: var(--ly-space-3); --ly-scroll-max: max(12rem, calc(100vh - 8rem)); } diff --git a/dist/layout-style-css.css b/dist/layout-style-css.css index 33d90dc..9a33fcf 100644 --- a/dist/layout-style-css.css +++ b/dist/layout-style-css.css @@ -25,14 +25,14 @@ --ly-wrapper-prose: 68ch; --ly-wrapper-content: 72rem; --ly-wrapper-wide: 112rem; - --ly-page-padding-inline: clamp(1rem, 4vw, 3rem); + --ly-page-padding-inline: clamp(1rem, 3vw, 3rem); --ly-safe-area-inline: max( env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px) ); --ly-safe-area-block-start: env(safe-area-inset-top, 0px); --ly-safe-area-block-end: env(safe-area-inset-bottom, 0px); - --ly-wrapper-gutter: max(var(--ly-page-padding-inline), var(--ly-safe-area-inline)); + --ly-wrapper-gutter: var(--ly-page-padding-inline); --ly-profile-gap: var(--ly-space-5); --ly-gap: var(--ly-profile-gap); @@ -40,6 +40,7 @@ --ly-stack-gap: var(--ly-space-4); --ly-cluster-gap: var(--ly-space-3); --ly-section-padding-block: clamp(3rem, 7vh, 6rem); + --ly-section-padding-block-compact: clamp(1.5rem, 3.5vh, 3rem); --ly-header-height: 4.5rem; --ly-sticky-position: sticky; --ly-cover-min: 100vh; @@ -131,6 +132,13 @@ min-block-size: 0; } + /* Prefer the nearest layout scope for gutters when container units parse. */ + @supports (width: 1cqi) { + :where(.ly-root) { + --ly-page-padding-inline: clamp(1rem, 3cqi, 3rem); + } + } + /* Enhance viewport-bound tokens only when dynamic viewport units parse. */ @supports (height: 100dvh) { :where(.ly-root) { @@ -168,6 +176,7 @@ --ly-stack-gap: min(var(--ly-space-4), 3vh); --ly-cluster-gap: min(var(--ly-space-3), 2vh); --ly-section-padding-block: clamp(1.5rem, 6vh, 3rem); + --ly-section-padding-block-compact: clamp(0.75rem, 3vh, 1.25rem); --ly-header-height: 3.5rem; --ly-scroll-max: min(62vh, 34rem); } @@ -177,6 +186,7 @@ --ly-stack-gap: min(var(--ly-space-4), 3dvh); --ly-cluster-gap: min(var(--ly-space-3), 2dvh); --ly-section-padding-block: clamp(1.5rem, 6dvh, 3rem); + --ly-section-padding-block-compact: clamp(0.75rem, 3dvh, 1.25rem); --ly-scroll-max: min(62dvh, 34rem); } } @@ -188,6 +198,7 @@ --ly-shell-min: auto; --ly-sticky-position: static; --ly-section-padding-block: var(--ly-space-5); + --ly-section-padding-block-compact: var(--ly-space-3); --ly-scroll-max: max(12rem, calc(100vh - 8rem)); } @@ -205,7 +216,7 @@ @layer ly.wrappers { .ly-wrapper { --ly-wrapper-max: var(--ly-personality-wrapper-max, var(--ly-wrapper-content)); - --ly-wrapper-fluid-gutter: clamp(1rem, 3vw, 3rem); + --ly-wrapper-fluid-gutter: var(--ly-wrapper-gutter); --ly-wrapper-local-gutter: max( var(--ly-wrapper-fluid-gutter), var(--ly-safe-area-inline) @@ -219,13 +230,6 @@ padding-inline: var(--ly-wrapper-local-gutter); } - /* Container-relative gutters enhance the universally valid viewport fallback. */ - @supports (width: 1cqi) { - .ly-wrapper { - --ly-wrapper-fluid-gutter: clamp(1rem, 3cqi, 3rem); - } - } - .ly-wrapper--compact { --ly-wrapper-max: var(--ly-wrapper-compact); } @@ -328,11 +332,11 @@ } .ly-section--compact { - --ly-section-padding-block: var(--ly-space-7); + padding-block: var(--ly-section-padding-block-compact); } .ly-section--flush { - --ly-section-padding-block: 0; + padding-block: 0; } .ly-readable { diff --git a/dist/layout-style-css.min.css b/dist/layout-style-css.min.css index 7e84d2a..93aee3c 100644 --- a/dist/layout-style-css.min.css +++ b/dist/layout-style-css.min.css @@ -1 +1 @@ -@layer ly.reset,ly.tokens,ly.wrappers,ly.primitives,ly.recipes,ly.utilities,ly.personalities;@layer ly.tokens{:where(.ly-root){--ly-space-0:0;--ly-space-1:0.25rem;--ly-space-2:0.5rem;--ly-space-3:0.75rem;--ly-space-4:1rem;--ly-space-5:1.5rem;--ly-space-6:2rem;--ly-space-7:3rem;--ly-space-8:4rem;--ly-space-9:6rem;--ly-wrapper-compact:40rem;--ly-wrapper-prose:68ch;--ly-wrapper-content:72rem;--ly-wrapper-wide:112rem;--ly-page-padding-inline:clamp(1rem,4vw,3rem);--ly-safe-area-inline:max( env(safe-area-inset-left,0px),env(safe-area-inset-right,0px) );--ly-safe-area-block-start:env(safe-area-inset-top,0px);--ly-safe-area-block-end:env(safe-area-inset-bottom,0px);--ly-wrapper-gutter:max(var(--ly-page-padding-inline),var(--ly-safe-area-inline));--ly-profile-gap:var(--ly-space-5);--ly-gap:var(--ly-profile-gap);--ly-grid-gap:var(--ly-gap);--ly-stack-gap:var(--ly-space-4);--ly-cluster-gap:var(--ly-space-3);--ly-section-padding-block:clamp(3rem,7vh,6rem);--ly-header-height:4.5rem;--ly-sticky-position:sticky;--ly-cover-min:100vh;--ly-shell-min:100vh;--ly-scroll-max:min(70vh,50rem);--ly-switcher-threshold:42rem;--ly-sidebar-size:18rem;--ly-sidebar-content-min:50%;--ly-grid-columns:12;--ly-grid-min:16rem;--ly-split-min:20rem;--ly-pane-min:18rem;--ly-pane-size:22rem;--ly-media-min:18rem;--ly-media-size:14rem;--ly-reel-item-min:16rem;--ly-reel-item-max:28rem;--ly-frame-ratio:16 / 9;--ly-split-primary:1fr;--ly-split-secondary:1fr;--ly-recipe-rail:18rem;--ly-recipe-aside:22rem;--ly-gallery-min:12rem;--ly-card-grid-min:16rem;--ly-app-shell-base-rows:auto auto minmax(0,1fr) auto auto;--ly-app-shell-medium-areas:"header header" "sidebar main" "aside main" "footer footer";--ly-app-shell-medium-rows:auto auto minmax(0,1fr) auto;--ly-app-shell-medium-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr);--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "sidebar footer footer";--ly-app-shell-wide-rows:auto minmax(0,1fr) auto;--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.35fr);--ly-dashboard-medium-areas:"header header" "nav nav" "main aside" "footer footer";--ly-dashboard-medium-columns:minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.35fr);--ly-dashboard-wide-areas:"nav header header" "nav main aside" "nav footer footer";--ly-dashboard-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.35fr);--ly-docs-wide-areas:"header header" "nav main" "nav aside" "footer footer";--ly-docs-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr);--ly-list-detail-wide-areas:"primary secondary" "actions actions";--ly-list-detail-wide-columns:minmax(min(100%,var(--ly-pane-size)),0.7fr) minmax(0,1fr);--ly-split-hero-wide-areas:"content media" "actions media";--ly-split-hero-wide-columns:minmax(0,var(--ly-split-primary,1fr)) minmax(0,var(--ly-split-secondary,1fr));container-name:ly-scope;container-type:inline-size;min-inline-size:0;min-block-size:0}@supports (height:100dvh){:where(.ly-root){--ly-cover-min:100dvh;--ly-shell-min:100dvh;--ly-scroll-max:min(70dvh,50rem)}}}@layer ly.reset{:where(.ly-root),:where(.ly-root *){box-sizing:border-box}:where(.ly-root){margin:0}:where(.ly-root img),:where(.ly-root svg),:where(.ly-root video),:where(.ly-root canvas),:where(.ly-root iframe){max-inline-size:100%}}@layer ly.tokens{@media (max-height:44rem){:where(.ly-root){--ly-gap:min(var(--ly-profile-gap),var(--ly-space-4));--ly-grid-gap:var(--ly-gap);--ly-stack-gap:min(var(--ly-space-4),3vh);--ly-cluster-gap:min(var(--ly-space-3),2vh);--ly-section-padding-block:clamp(1.5rem,6vh,3rem);--ly-header-height:3.5rem;--ly-scroll-max:min(62vh,34rem)}@supports (height:100dvh){:where(.ly-root){--ly-stack-gap:min(var(--ly-space-4),3dvh);--ly-cluster-gap:min(var(--ly-space-3),2dvh);--ly-section-padding-block:clamp(1.5rem,6dvh,3rem);--ly-scroll-max:min(62dvh,34rem)}}}@media (max-height:30rem){:where(.ly-root){--ly-cover-min:auto;--ly-shell-min:auto;--ly-sticky-position:static;--ly-section-padding-block:var(--ly-space-5);--ly-scroll-max:max(12rem,calc(100vh - 8rem))}@supports (height:100dvh){:where(.ly-root){--ly-scroll-max:max(12rem,calc(100dvh - 8rem))}}}}@layer ly.wrappers{.ly-wrapper{--ly-wrapper-max:var(--ly-personality-wrapper-max,var(--ly-wrapper-content));--ly-wrapper-fluid-gutter:clamp(1rem,3vw,3rem);--ly-wrapper-local-gutter:max( var(--ly-wrapper-fluid-gutter),var(--ly-safe-area-inline) );container-name:ly-scope;container-type:inline-size;inline-size:min(100%,calc(var(--ly-wrapper-max) + (var(--ly-wrapper-local-gutter) * 2)));max-inline-size:100%;margin-inline:auto;padding-inline:var(--ly-wrapper-local-gutter)}@supports (width:1cqi){.ly-wrapper{--ly-wrapper-fluid-gutter:clamp(1rem,3cqi,3rem)}}.ly-wrapper--compact{--ly-wrapper-max:var(--ly-wrapper-compact)}.ly-wrapper--prose{--ly-wrapper-max:var(--ly-wrapper-prose)}.ly-wrapper--content{--ly-wrapper-max:var(--ly-wrapper-content)}.ly-wrapper--wide{--ly-wrapper-max:var(--ly-wrapper-wide)}.ly-wrapper--full{--ly-wrapper-max:100%}.ly-wrapper--breakout{--ly-wrapper-max:100%;--ly-breakout-content-size:min( var(--ly-wrapper-content),calc(100% - (var(--ly-wrapper-local-gutter) * 2)) );--ly-breakout-feature-size:min( var(--ly-wrapper-wide),calc(100% - (var(--ly-wrapper-local-gutter) * 2)) );display:grid;grid-template-columns:[full-start] minmax(min(100%,var(--ly-wrapper-local-gutter)),1fr) [feature-start] minmax( 0,calc((var(--ly-breakout-feature-size) - var(--ly-breakout-content-size)) / 2) ) [content-start] minmax(0,var(--ly-breakout-content-size)) [content-end] minmax( 0,calc((var(--ly-breakout-feature-size) - var(--ly-breakout-content-size)) / 2) ) [feature-end] minmax(min(100%,var(--ly-wrapper-local-gutter)),1fr) [full-end];inline-size:100%;padding-inline:0}.ly-wrapper--breakout>*{grid-column:content;min-inline-size:0;max-inline-size:100%}.ly-wrapper--breakout>:where(.ly-lane--content,[data-ly-lane="content"]){grid-column:content}.ly-wrapper--breakout>:where(.ly-lane--feature,[data-ly-lane="feature"]){grid-column:feature}.ly-wrapper--breakout>:where(.ly-lane--full,[data-ly-lane="full"]){grid-column:full}}@layer ly.primitives{.ly-page{min-block-size:100vh;min-block-size:100dvh;padding-block-start:var(--ly-safe-area-block-start);padding-block-end:var(--ly-safe-area-block-end)}.ly-header,.ly-footer,.ly-main,.ly-section,.ly-surface{min-inline-size:0;min-block-size:0}.ly-header{min-block-size:var(--ly-header-height)}.ly-header--sticky{position:var(--ly-sticky-position,sticky);inset-block-start:0;z-index:var(--ly-z-header,20)}.ly-section{padding-block:var(--ly-section-padding-block)}.ly-section--compact{--ly-section-padding-block:var(--ly-space-7)}.ly-section--flush{--ly-section-padding-block:0}.ly-readable{max-inline-size:var(--ly-wrapper-prose)}.ly-stack{display:flex;flex-direction:column;gap:var(--ly-stack-gap)}.ly-cluster{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ly-cluster-gap)}.ly-center{inline-size:min(100%,var(--ly-center-max,var(--ly-wrapper-content)));margin-inline:auto}.ly-cover{display:flex;flex-direction:column;min-block-size:100vh;min-block-size:var(--ly-cover-min);padding-block:var(--ly-cover-padding,var(--ly-space-5))}.ly-cover>:where(.ly-cover__center,[data-ly-cover-center]){margin-block:auto}.ly-switcher{display:flex;flex-wrap:wrap;gap:var(--ly-gap)}.ly-switcher>*{flex-basis:calc((var(--ly-switcher-threshold) - 100%) * 999);flex-grow:1;min-inline-size:0}.ly-sidebar{display:flex;flex-wrap:wrap;gap:var(--ly-gap)}.ly-sidebar>:where(.ly-sidebar__side,[data-ly-sidebar="side"]){flex-basis:var(--ly-sidebar-size);flex-grow:1;min-inline-size:min(100%,var(--ly-sidebar-size))}.ly-sidebar>:where(.ly-sidebar__content,[data-ly-sidebar="content"]){flex-basis:0;flex-grow:999;min-inline-size:min(100%,var(--ly-sidebar-content-min))}.ly-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-grid-min)),1fr));gap:var(--ly-grid-gap)}.ly-grid--fixed{grid-template-columns:repeat(var(--ly-grid-columns),minmax(0,1fr))}.ly-split{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-split-min)),1fr));gap:var(--ly-gap);align-items:var(--ly-split-align,center)}.ly-panes{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-pane-min)),1fr));gap:var(--ly-gap)}.ly-media{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-media-min)),1fr));gap:var(--ly-gap);align-items:var(--ly-media-align,start)}.ly-media>:where(.ly-media__actions,[data-ly-media="actions"]){min-inline-size:min(100%,var(--ly-media-size))}.ly-grid>*,.ly-split>*,.ly-panes>*,.ly-media>*{min-inline-size:0;min-block-size:0}.ly-reel{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(min(100%,var(--ly-reel-item-min)),var(--ly-reel-item-max));gap:var(--ly-grid-gap);overflow-x:auto;overscroll-behavior-inline:contain;scroll-snap-type:inline proximity}.ly-reel>*{min-inline-size:0;scroll-snap-align:start}.ly-frame{aspect-ratio:var(--ly-frame-ratio);overflow:hidden}.ly-frame>*{inline-size:100%;block-size:100%;object-fit:cover}.ly-scroll{min-block-size:0;max-block-size:var(--ly-scroll-max);overflow-x:clip;overflow-y:auto;overscroll-behavior-block:contain}}@layer ly.recipes{:where( [data-ly-recipe="app-shell"],[data-ly-recipe="dashboard"],[data-ly-recipe="docs"],[data-ly-recipe="list-detail"],[data-ly-recipe="split-hero"],[data-ly-recipe="gallery"],[data-ly-recipe="card-grid"] ){container-name:ly-scope;container-type:inline-size;min-inline-size:0;min-block-size:0}:where( [data-ly-recipe="app-shell"],[data-ly-recipe="dashboard"],[data-ly-recipe="docs"],[data-ly-recipe="list-detail"],[data-ly-recipe="split-hero"] ){display:grid;grid-template-columns:minmax(0,1fr);gap:var(--ly-gap)}[data-ly-recipe="app-shell"]{min-block-size:100vh;min-block-size:var(--ly-shell-min);grid-template-areas:"header" "sidebar" "main" "aside" "footer";grid-template-rows:var(--ly-app-shell-base-rows)}[data-ly-recipe="dashboard"],[data-ly-recipe="docs"]{grid-template-areas:"header" "nav" "main" "aside" "footer"}[data-ly-recipe="list-detail"]{grid-template-areas:"primary" "secondary" "actions"}[data-ly-recipe="split-hero"]{grid-template-areas:"content" "media" "actions";align-items:var(--ly-split-hero-align,center)}[data-ly-recipe="gallery"],[data-ly-recipe="card-grid"]{display:grid;gap:var(--ly-grid-gap)}[data-ly-recipe="gallery"]{grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-gallery-min)),1fr))}[data-ly-recipe="card-grid"]{grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-card-grid-min)),1fr))}:where( [data-ly-recipe="gallery"],[data-ly-recipe="card-grid"] )[data-ly-responsive="manual"]{grid-template-columns:minmax(0,1fr)}:where( [data-ly-recipe="app-shell"],[data-ly-recipe="dashboard"],[data-ly-recipe="docs"],[data-ly-recipe="list-detail"],[data-ly-recipe="split-hero"],[data-ly-recipe="gallery"],[data-ly-recipe="card-grid"] )>*{min-inline-size:0;min-block-size:0}[data-ly-area="header"]{grid-area:header}[data-ly-area="nav"]{grid-area:nav}[data-ly-area="sidebar"]{grid-area:sidebar}[data-ly-area="main"]{grid-area:main}[data-ly-area="aside"]{grid-area:aside}[data-ly-area="footer"]{grid-area:footer}[data-ly-area="content"]{grid-area:content}[data-ly-area="media"]{grid-area:media}[data-ly-area="actions"]{grid-area:actions}[data-ly-area="primary"]{grid-area:primary}[data-ly-area="secondary"]{grid-area:secondary}@container ly-scope (min-width:42rem){[data-ly-recipe="split-hero"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-split-hero-wide-areas);grid-template-columns:var(--ly-split-hero-wide-columns)}}@container ly-scope (min-width:44rem){[data-ly-recipe="list-detail"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-list-detail-wide-areas);grid-template-columns:var(--ly-list-detail-wide-columns)}}@container ly-scope (min-width:48rem){[data-ly-recipe="docs"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-docs-wide-areas);grid-template-columns:var(--ly-docs-wide-columns)}}@container ly-scope (min-width:52rem){[data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-app-shell-medium-areas);grid-template-columns:var(--ly-app-shell-medium-columns);grid-template-rows:var(--ly-app-shell-medium-rows)}[data-ly-recipe="dashboard"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-dashboard-medium-areas);grid-template-columns:var(--ly-dashboard-medium-columns)}}@container ly-scope (min-width:72rem){[data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-app-shell-wide-areas);grid-template-columns:var(--ly-app-shell-wide-columns);grid-template-rows:var(--ly-app-shell-wide-rows)}[data-ly-recipe="dashboard"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-dashboard-wide-areas);grid-template-columns:var(--ly-dashboard-wide-columns)}}}@layer ly.utilities{.ly-cols-1{--ly-grid-columns:1}.ly-cols-2{--ly-grid-columns:2}.ly-cols-3{--ly-grid-columns:3}.ly-cols-4{--ly-grid-columns:4}.ly-cols-6{--ly-grid-columns:6}.ly-cols-12{--ly-grid-columns:12}.ly-span-1{grid-column:span 1}.ly-span-2{grid-column:span 2}.ly-span-3{grid-column:span 3}.ly-span-4{grid-column:span 4}.ly-span-full{grid-column:1 / -1}.ly-gap-0{--ly-gap:var(--ly-space-0);--ly-grid-gap:var(--ly-space-0);--ly-stack-gap:var(--ly-space-0);--ly-cluster-gap:var(--ly-space-0)}.ly-gap-2{--ly-gap:var(--ly-space-2);--ly-grid-gap:var(--ly-space-2);--ly-stack-gap:var(--ly-space-2);--ly-cluster-gap:var(--ly-space-2)}.ly-gap-4{--ly-gap:var(--ly-space-4);--ly-grid-gap:var(--ly-space-4);--ly-stack-gap:var(--ly-space-4);--ly-cluster-gap:var(--ly-space-4)}.ly-gap-6{--ly-gap:var(--ly-space-6);--ly-grid-gap:var(--ly-space-6);--ly-stack-gap:var(--ly-space-6);--ly-cluster-gap:var(--ly-space-6)}.ly-gap-8{--ly-gap:var(--ly-space-8);--ly-grid-gap:var(--ly-space-8);--ly-stack-gap:var(--ly-space-8);--ly-cluster-gap:var(--ly-space-8)}.ly-pad-0{padding:var(--ly-space-0)}.ly-pad-2{padding:var(--ly-space-2)}.ly-pad-4{padding:var(--ly-space-4)}.ly-pad-6{padding:var(--ly-space-6)}.ly-pad-8{padding:var(--ly-space-8)}.ly-mx-auto{margin-inline:auto}.ly-w-full{inline-size:100%}.ly-h-full{block-size:100%}.ly-min-h-screen{min-block-size:100vh;min-block-size:100dvh}.ly-overflow-auto{overflow:auto}.ly-overflow-hidden{overflow:hidden}.ly-items-start{align-items:flex-start}.ly-items-center{align-items:center}.ly-items-end{align-items:flex-end}.ly-items-stretch{align-items:stretch}.ly-justify-start{justify-content:flex-start}.ly-justify-center{justify-content:center}.ly-justify-end{justify-content:flex-end}.ly-justify-between{justify-content:space-between}.ly-frame-1x1{--ly-frame-ratio:1 / 1}.ly-frame-3x2{--ly-frame-ratio:3 / 2}.ly-frame-4x3{--ly-frame-ratio:4 / 3}.ly-frame-16x9{--ly-frame-ratio:16 / 9}.ly-frame-21x9{--ly-frame-ratio:21 / 9}.ly-hidden{display:none !important}.ly-visually-hidden{position:absolute !important;inline-size:1px !important;block-size:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0,0,0,0) !important;clip-path:inset(50%) !important;white-space:nowrap !important}}@layer ly.personalities.minimal-saas{:where(.ly-root[data-ly-layout="minimal-saas"]){--ly-personality-wrapper-max:88rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:16rem;--ly-recipe-rail:15rem;--ly-recipe-aside:16rem;--ly-split-primary:1.05fr;--ly-split-secondary:0.95fr}}@layer ly.personalities.bento{:where(.ly-root[data-ly-layout="bento"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:12rem;--ly-card-grid-min:14rem;--ly-gallery-min:11rem;--ly-recipe-rail:14rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header header" "sidebar main main" "aside main main" "footer footer footer";--ly-app-shell-wide-rows:auto auto minmax(0,1fr) auto;--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.3fr) minmax(0,1fr) minmax(0,1fr)}}@layer ly.personalities.maximalist{:where(.ly-root[data-ly-layout="maximalist"]){--ly-personality-wrapper-max:100%;--ly-profile-gap:var(--ly-space-7);--ly-grid-min:10rem;--ly-card-grid-min:13rem;--ly-gallery-min:10rem;--ly-recipe-rail:15rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header header header" "main main sidebar aside" "main main footer footer";--ly-app-shell-wide-columns:minmax(0,1.4fr) minmax(0,0.6fr) minmax(min(100%,var(--ly-recipe-rail)),0.32fr) minmax(min(100%,var(--ly-recipe-aside)),0.38fr)}}@layer ly.personalities.bauhaus{:where(.ly-root[data-ly-layout="bauhaus"]){--ly-personality-wrapper-max:96rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:13rem;--ly-recipe-rail:14rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header header header" "sidebar main main aside" "sidebar footer footer aside";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.24fr) minmax(0,0.7fr) minmax(0,1.3fr) minmax(min(100%,var(--ly-recipe-aside)),0.3fr)}}@layer ly.personalities.tactile{:where(.ly-root[data-ly-layout="tactile"]){--ly-personality-wrapper-max:82rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:15rem;--ly-recipe-rail:17rem;--ly-recipe-aside:20rem;--ly-app-shell-wide-areas:"header header" "sidebar main" "aside main" "footer footer";--ly-app-shell-wide-rows:auto auto minmax(0,1fr) auto;--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.28fr) minmax(0,1fr);--ly-list-detail-wide-columns:minmax(min(100%,var(--ly-pane-size)),0.5fr) minmax(0,0.9fr)}}@layer ly.personalities.neumorphism{:where(.ly-root[data-ly-layout="neumorphism"]){--ly-personality-wrapper-max:84rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:17rem;--ly-recipe-rail:17rem;--ly-recipe-aside:19rem;--ly-app-shell-wide-areas:"header sidebar" "main sidebar" "aside sidebar" "footer sidebar";--ly-app-shell-wide-rows:auto minmax(0,1fr) auto auto;--ly-app-shell-wide-columns:minmax(0,1fr) minmax(min(100%,var(--ly-recipe-rail)),0.3fr);--ly-list-detail-wide-columns:minmax(0,1.1fr) minmax(min(100%,var(--ly-pane-size)),0.7fr)}}@layer ly.personalities.retrofuturism{:where(.ly-root[data-ly-layout="retrofuturism"]){--ly-personality-wrapper-max:106rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:14rem;--ly-recipe-rail:14rem;--ly-recipe-aside:17rem;--ly-app-shell-wide-areas:"sidebar header aside" "sidebar main aside" "sidebar footer aside";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.24fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.3fr)}}@layer ly.personalities.brutalism{:where(.ly-root[data-ly-layout="brutalism"]){--ly-personality-wrapper-max:100rem;--ly-profile-gap:var(--ly-space-3);--ly-grid-min:14rem;--ly-recipe-rail:16rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header sidebar" "main aside sidebar" "footer footer sidebar";--ly-app-shell-wide-columns:minmax(0,1.3fr) minmax(0,0.7fr) minmax(min(100%,var(--ly-recipe-rail)),0.34fr)}}@layer ly.personalities.cyberpunk{:where(.ly-root[data-ly-layout="cyberpunk"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:12rem;--ly-recipe-rail:13rem;--ly-recipe-aside:19rem;--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "sidebar footer footer";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.2fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.32fr)}}@layer ly.personalities.y2k{:where(.ly-root[data-ly-layout="y2k"]){--ly-personality-wrapper-max:90rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:13rem;--ly-recipe-rail:18rem;--ly-recipe-aside:16rem;--ly-app-shell-wide-areas:"header header sidebar" "aside main sidebar" "footer footer sidebar";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-aside)),0.34fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-rail)),0.28fr)}}@layer ly.personalities.retro-glass{:where(.ly-root[data-ly-layout="retro-glass"]){--ly-personality-wrapper-max:104rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:15rem;--ly-recipe-rail:18rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header sidebar" "main main sidebar" "aside footer footer";--ly-app-shell-wide-columns:minmax(0,1.25fr) minmax(0,0.75fr) minmax(min(100%,var(--ly-recipe-rail)),0.32fr)}}@layer ly.personalities.f-pattern{:where(.ly-root[data-ly-layout="f-pattern"]){--ly-personality-wrapper-max:92rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:14rem;--ly-recipe-rail:16rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "sidebar footer footer";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.26fr) minmax(0,1.35fr) minmax(min(100%,var(--ly-recipe-aside)),0.65fr);--ly-split-hero-wide-columns:minmax(0,1.8fr) minmax(0,0.8fr)}}@layer ly.personalities.z-pattern{:where(.ly-root[data-ly-layout="z-pattern"]){--ly-personality-wrapper-max:108rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:14rem;--ly-recipe-rail:17rem;--ly-recipe-aside:19rem;--ly-app-shell-wide-areas:"header header sidebar" "main aside sidebar" "footer aside sidebar";--ly-app-shell-wide-columns:minmax(0,1.45fr) minmax(min(100%,var(--ly-recipe-aside)),0.55fr) minmax(min(100%,var(--ly-recipe-rail)),0.3fr);--ly-split-hero-wide-columns:minmax(0,1.5fr) minmax(0,0.5fr)}}@layer ly.personalities.split-screen{:where(.ly-root[data-ly-layout="split-screen"]){--ly-personality-wrapper-max:100%;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:20rem;--ly-recipe-rail:24rem;--ly-recipe-aside:24rem;--ly-app-shell-wide-areas:"header header" "main sidebar" "aside sidebar" "footer footer";--ly-app-shell-wide-rows:auto minmax(0,1fr) auto auto;--ly-app-shell-wide-columns:minmax(0,1fr) minmax(0,1fr);--ly-split-hero-wide-columns:minmax(0,1fr) minmax(0,1fr);--ly-list-detail-wide-columns:minmax(0,1fr) minmax(0,1fr)}}@layer ly.personalities.mondrian{:where(.ly-root[data-ly-layout="mondrian"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-3);--ly-grid-min:11rem;--ly-recipe-rail:13rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "footer footer aside";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.22fr) minmax(0,1.45fr) minmax(min(100%,var(--ly-recipe-aside)),0.55fr)}}@layer ly.personalities.synthwave{:where(.ly-root[data-ly-layout="synthwave"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:14rem;--ly-recipe-rail:16rem;--ly-recipe-aside:20rem;--ly-app-shell-wide-areas:"header header header" "sidebar main aside" "footer footer footer";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.28fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.34fr);--ly-reel-item-min:20rem}} +@layer ly.reset,ly.tokens,ly.wrappers,ly.primitives,ly.recipes,ly.utilities,ly.personalities;@layer ly.tokens{:where(.ly-root){--ly-space-0:0;--ly-space-1:0.25rem;--ly-space-2:0.5rem;--ly-space-3:0.75rem;--ly-space-4:1rem;--ly-space-5:1.5rem;--ly-space-6:2rem;--ly-space-7:3rem;--ly-space-8:4rem;--ly-space-9:6rem;--ly-wrapper-compact:40rem;--ly-wrapper-prose:68ch;--ly-wrapper-content:72rem;--ly-wrapper-wide:112rem;--ly-page-padding-inline:clamp(1rem,3vw,3rem);--ly-safe-area-inline:max( env(safe-area-inset-left,0px),env(safe-area-inset-right,0px) );--ly-safe-area-block-start:env(safe-area-inset-top,0px);--ly-safe-area-block-end:env(safe-area-inset-bottom,0px);--ly-wrapper-gutter:var(--ly-page-padding-inline);--ly-profile-gap:var(--ly-space-5);--ly-gap:var(--ly-profile-gap);--ly-grid-gap:var(--ly-gap);--ly-stack-gap:var(--ly-space-4);--ly-cluster-gap:var(--ly-space-3);--ly-section-padding-block:clamp(3rem,7vh,6rem);--ly-section-padding-block-compact:clamp(1.5rem,3.5vh,3rem);--ly-header-height:4.5rem;--ly-sticky-position:sticky;--ly-cover-min:100vh;--ly-shell-min:100vh;--ly-scroll-max:min(70vh,50rem);--ly-switcher-threshold:42rem;--ly-sidebar-size:18rem;--ly-sidebar-content-min:50%;--ly-grid-columns:12;--ly-grid-min:16rem;--ly-split-min:20rem;--ly-pane-min:18rem;--ly-pane-size:22rem;--ly-media-min:18rem;--ly-media-size:14rem;--ly-reel-item-min:16rem;--ly-reel-item-max:28rem;--ly-frame-ratio:16 / 9;--ly-split-primary:1fr;--ly-split-secondary:1fr;--ly-recipe-rail:18rem;--ly-recipe-aside:22rem;--ly-gallery-min:12rem;--ly-card-grid-min:16rem;--ly-app-shell-base-rows:auto auto minmax(0,1fr) auto auto;--ly-app-shell-medium-areas:"header header" "sidebar main" "aside main" "footer footer";--ly-app-shell-medium-rows:auto auto minmax(0,1fr) auto;--ly-app-shell-medium-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr);--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "sidebar footer footer";--ly-app-shell-wide-rows:auto minmax(0,1fr) auto;--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.35fr);--ly-dashboard-medium-areas:"header header" "nav nav" "main aside" "footer footer";--ly-dashboard-medium-columns:minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.35fr);--ly-dashboard-wide-areas:"nav header header" "nav main aside" "nav footer footer";--ly-dashboard-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.35fr);--ly-docs-wide-areas:"header header" "nav main" "nav aside" "footer footer";--ly-docs-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),auto) minmax(0,1fr);--ly-list-detail-wide-areas:"primary secondary" "actions actions";--ly-list-detail-wide-columns:minmax(min(100%,var(--ly-pane-size)),0.7fr) minmax(0,1fr);--ly-split-hero-wide-areas:"content media" "actions media";--ly-split-hero-wide-columns:minmax(0,var(--ly-split-primary,1fr)) minmax(0,var(--ly-split-secondary,1fr));container-name:ly-scope;container-type:inline-size;min-inline-size:0;min-block-size:0}@supports (width:1cqi){:where(.ly-root){--ly-page-padding-inline:clamp(1rem,3cqi,3rem)}}@supports (height:100dvh){:where(.ly-root){--ly-cover-min:100dvh;--ly-shell-min:100dvh;--ly-scroll-max:min(70dvh,50rem)}}}@layer ly.reset{:where(.ly-root),:where(.ly-root *){box-sizing:border-box}:where(.ly-root){margin:0}:where(.ly-root img),:where(.ly-root svg),:where(.ly-root video),:where(.ly-root canvas),:where(.ly-root iframe){max-inline-size:100%}}@layer ly.tokens{@media (max-height:44rem){:where(.ly-root){--ly-gap:min(var(--ly-profile-gap),var(--ly-space-4));--ly-grid-gap:var(--ly-gap);--ly-stack-gap:min(var(--ly-space-4),3vh);--ly-cluster-gap:min(var(--ly-space-3),2vh);--ly-section-padding-block:clamp(1.5rem,6vh,3rem);--ly-section-padding-block-compact:clamp(0.75rem,3vh,1.25rem);--ly-header-height:3.5rem;--ly-scroll-max:min(62vh,34rem)}@supports (height:100dvh){:where(.ly-root){--ly-stack-gap:min(var(--ly-space-4),3dvh);--ly-cluster-gap:min(var(--ly-space-3),2dvh);--ly-section-padding-block:clamp(1.5rem,6dvh,3rem);--ly-section-padding-block-compact:clamp(0.75rem,3dvh,1.25rem);--ly-scroll-max:min(62dvh,34rem)}}}@media (max-height:30rem){:where(.ly-root){--ly-cover-min:auto;--ly-shell-min:auto;--ly-sticky-position:static;--ly-section-padding-block:var(--ly-space-5);--ly-section-padding-block-compact:var(--ly-space-3);--ly-scroll-max:max(12rem,calc(100vh - 8rem))}@supports (height:100dvh){:where(.ly-root){--ly-scroll-max:max(12rem,calc(100dvh - 8rem))}}}}@layer ly.wrappers{.ly-wrapper{--ly-wrapper-max:var(--ly-personality-wrapper-max,var(--ly-wrapper-content));--ly-wrapper-fluid-gutter:var(--ly-wrapper-gutter);--ly-wrapper-local-gutter:max( var(--ly-wrapper-fluid-gutter),var(--ly-safe-area-inline) );container-name:ly-scope;container-type:inline-size;inline-size:min(100%,calc(var(--ly-wrapper-max) + (var(--ly-wrapper-local-gutter) * 2)));max-inline-size:100%;margin-inline:auto;padding-inline:var(--ly-wrapper-local-gutter)}.ly-wrapper--compact{--ly-wrapper-max:var(--ly-wrapper-compact)}.ly-wrapper--prose{--ly-wrapper-max:var(--ly-wrapper-prose)}.ly-wrapper--content{--ly-wrapper-max:var(--ly-wrapper-content)}.ly-wrapper--wide{--ly-wrapper-max:var(--ly-wrapper-wide)}.ly-wrapper--full{--ly-wrapper-max:100%}.ly-wrapper--breakout{--ly-wrapper-max:100%;--ly-breakout-content-size:min( var(--ly-wrapper-content),calc(100% - (var(--ly-wrapper-local-gutter) * 2)) );--ly-breakout-feature-size:min( var(--ly-wrapper-wide),calc(100% - (var(--ly-wrapper-local-gutter) * 2)) );display:grid;grid-template-columns:[full-start] minmax(min(100%,var(--ly-wrapper-local-gutter)),1fr) [feature-start] minmax( 0,calc((var(--ly-breakout-feature-size) - var(--ly-breakout-content-size)) / 2) ) [content-start] minmax(0,var(--ly-breakout-content-size)) [content-end] minmax( 0,calc((var(--ly-breakout-feature-size) - var(--ly-breakout-content-size)) / 2) ) [feature-end] minmax(min(100%,var(--ly-wrapper-local-gutter)),1fr) [full-end];inline-size:100%;padding-inline:0}.ly-wrapper--breakout>*{grid-column:content;min-inline-size:0;max-inline-size:100%}.ly-wrapper--breakout>:where(.ly-lane--content,[data-ly-lane="content"]){grid-column:content}.ly-wrapper--breakout>:where(.ly-lane--feature,[data-ly-lane="feature"]){grid-column:feature}.ly-wrapper--breakout>:where(.ly-lane--full,[data-ly-lane="full"]){grid-column:full}}@layer ly.primitives{.ly-page{min-block-size:100vh;min-block-size:100dvh;padding-block-start:var(--ly-safe-area-block-start);padding-block-end:var(--ly-safe-area-block-end)}.ly-header,.ly-footer,.ly-main,.ly-section,.ly-surface{min-inline-size:0;min-block-size:0}.ly-header{min-block-size:var(--ly-header-height)}.ly-header--sticky{position:var(--ly-sticky-position,sticky);inset-block-start:0;z-index:var(--ly-z-header,20)}.ly-section{padding-block:var(--ly-section-padding-block)}.ly-section--compact{padding-block:var(--ly-section-padding-block-compact)}.ly-section--flush{padding-block:0}.ly-readable{max-inline-size:var(--ly-wrapper-prose)}.ly-stack{display:flex;flex-direction:column;gap:var(--ly-stack-gap)}.ly-cluster{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ly-cluster-gap)}.ly-center{inline-size:min(100%,var(--ly-center-max,var(--ly-wrapper-content)));margin-inline:auto}.ly-cover{display:flex;flex-direction:column;min-block-size:100vh;min-block-size:var(--ly-cover-min);padding-block:var(--ly-cover-padding,var(--ly-space-5))}.ly-cover>:where(.ly-cover__center,[data-ly-cover-center]){margin-block:auto}.ly-switcher{display:flex;flex-wrap:wrap;gap:var(--ly-gap)}.ly-switcher>*{flex-basis:calc((var(--ly-switcher-threshold) - 100%) * 999);flex-grow:1;min-inline-size:0}.ly-sidebar{display:flex;flex-wrap:wrap;gap:var(--ly-gap)}.ly-sidebar>:where(.ly-sidebar__side,[data-ly-sidebar="side"]){flex-basis:var(--ly-sidebar-size);flex-grow:1;min-inline-size:min(100%,var(--ly-sidebar-size))}.ly-sidebar>:where(.ly-sidebar__content,[data-ly-sidebar="content"]){flex-basis:0;flex-grow:999;min-inline-size:min(100%,var(--ly-sidebar-content-min))}.ly-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-grid-min)),1fr));gap:var(--ly-grid-gap)}.ly-grid--fixed{grid-template-columns:repeat(var(--ly-grid-columns),minmax(0,1fr))}.ly-split{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-split-min)),1fr));gap:var(--ly-gap);align-items:var(--ly-split-align,center)}.ly-panes{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-pane-min)),1fr));gap:var(--ly-gap)}.ly-media{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-media-min)),1fr));gap:var(--ly-gap);align-items:var(--ly-media-align,start)}.ly-media>:where(.ly-media__actions,[data-ly-media="actions"]){min-inline-size:min(100%,var(--ly-media-size))}.ly-grid>*,.ly-split>*,.ly-panes>*,.ly-media>*{min-inline-size:0;min-block-size:0}.ly-reel{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(min(100%,var(--ly-reel-item-min)),var(--ly-reel-item-max));gap:var(--ly-grid-gap);overflow-x:auto;overscroll-behavior-inline:contain;scroll-snap-type:inline proximity}.ly-reel>*{min-inline-size:0;scroll-snap-align:start}.ly-frame{aspect-ratio:var(--ly-frame-ratio);overflow:hidden}.ly-frame>*{inline-size:100%;block-size:100%;object-fit:cover}.ly-scroll{min-block-size:0;max-block-size:var(--ly-scroll-max);overflow-x:clip;overflow-y:auto;overscroll-behavior-block:contain}}@layer ly.recipes{:where( [data-ly-recipe="app-shell"],[data-ly-recipe="dashboard"],[data-ly-recipe="docs"],[data-ly-recipe="list-detail"],[data-ly-recipe="split-hero"],[data-ly-recipe="gallery"],[data-ly-recipe="card-grid"] ){container-name:ly-scope;container-type:inline-size;min-inline-size:0;min-block-size:0}:where( [data-ly-recipe="app-shell"],[data-ly-recipe="dashboard"],[data-ly-recipe="docs"],[data-ly-recipe="list-detail"],[data-ly-recipe="split-hero"] ){display:grid;grid-template-columns:minmax(0,1fr);gap:var(--ly-gap)}[data-ly-recipe="app-shell"]{min-block-size:100vh;min-block-size:var(--ly-shell-min);grid-template-areas:"header" "sidebar" "main" "aside" "footer";grid-template-rows:var(--ly-app-shell-base-rows)}[data-ly-recipe="dashboard"],[data-ly-recipe="docs"]{grid-template-areas:"header" "nav" "main" "aside" "footer"}[data-ly-recipe="list-detail"]{grid-template-areas:"primary" "secondary" "actions"}[data-ly-recipe="split-hero"]{grid-template-areas:"content" "media" "actions";align-items:var(--ly-split-hero-align,center)}[data-ly-recipe="gallery"],[data-ly-recipe="card-grid"]{display:grid;gap:var(--ly-grid-gap)}[data-ly-recipe="gallery"]{grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-gallery-min)),1fr))}[data-ly-recipe="card-grid"]{grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--ly-card-grid-min)),1fr))}:where( [data-ly-recipe="gallery"],[data-ly-recipe="card-grid"] )[data-ly-responsive="manual"]{grid-template-columns:minmax(0,1fr)}:where( [data-ly-recipe="app-shell"],[data-ly-recipe="dashboard"],[data-ly-recipe="docs"],[data-ly-recipe="list-detail"],[data-ly-recipe="split-hero"],[data-ly-recipe="gallery"],[data-ly-recipe="card-grid"] )>*{min-inline-size:0;min-block-size:0}[data-ly-area="header"]{grid-area:header}[data-ly-area="nav"]{grid-area:nav}[data-ly-area="sidebar"]{grid-area:sidebar}[data-ly-area="main"]{grid-area:main}[data-ly-area="aside"]{grid-area:aside}[data-ly-area="footer"]{grid-area:footer}[data-ly-area="content"]{grid-area:content}[data-ly-area="media"]{grid-area:media}[data-ly-area="actions"]{grid-area:actions}[data-ly-area="primary"]{grid-area:primary}[data-ly-area="secondary"]{grid-area:secondary}@container ly-scope (min-width:42rem){[data-ly-recipe="split-hero"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-split-hero-wide-areas);grid-template-columns:var(--ly-split-hero-wide-columns)}}@container ly-scope (min-width:44rem){[data-ly-recipe="list-detail"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-list-detail-wide-areas);grid-template-columns:var(--ly-list-detail-wide-columns)}}@container ly-scope (min-width:48rem){[data-ly-recipe="docs"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-docs-wide-areas);grid-template-columns:var(--ly-docs-wide-columns)}}@container ly-scope (min-width:52rem){[data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-app-shell-medium-areas);grid-template-columns:var(--ly-app-shell-medium-columns);grid-template-rows:var(--ly-app-shell-medium-rows)}[data-ly-recipe="dashboard"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-dashboard-medium-areas);grid-template-columns:var(--ly-dashboard-medium-columns)}}@container ly-scope (min-width:72rem){[data-ly-recipe="app-shell"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-app-shell-wide-areas);grid-template-columns:var(--ly-app-shell-wide-columns);grid-template-rows:var(--ly-app-shell-wide-rows)}[data-ly-recipe="dashboard"]:not([data-ly-responsive="manual"]){grid-template-areas:var(--ly-dashboard-wide-areas);grid-template-columns:var(--ly-dashboard-wide-columns)}}}@layer ly.utilities{.ly-cols-1{--ly-grid-columns:1}.ly-cols-2{--ly-grid-columns:2}.ly-cols-3{--ly-grid-columns:3}.ly-cols-4{--ly-grid-columns:4}.ly-cols-6{--ly-grid-columns:6}.ly-cols-12{--ly-grid-columns:12}.ly-span-1{grid-column:span 1}.ly-span-2{grid-column:span 2}.ly-span-3{grid-column:span 3}.ly-span-4{grid-column:span 4}.ly-span-full{grid-column:1 / -1}.ly-gap-0{--ly-gap:var(--ly-space-0);--ly-grid-gap:var(--ly-space-0);--ly-stack-gap:var(--ly-space-0);--ly-cluster-gap:var(--ly-space-0)}.ly-gap-2{--ly-gap:var(--ly-space-2);--ly-grid-gap:var(--ly-space-2);--ly-stack-gap:var(--ly-space-2);--ly-cluster-gap:var(--ly-space-2)}.ly-gap-4{--ly-gap:var(--ly-space-4);--ly-grid-gap:var(--ly-space-4);--ly-stack-gap:var(--ly-space-4);--ly-cluster-gap:var(--ly-space-4)}.ly-gap-6{--ly-gap:var(--ly-space-6);--ly-grid-gap:var(--ly-space-6);--ly-stack-gap:var(--ly-space-6);--ly-cluster-gap:var(--ly-space-6)}.ly-gap-8{--ly-gap:var(--ly-space-8);--ly-grid-gap:var(--ly-space-8);--ly-stack-gap:var(--ly-space-8);--ly-cluster-gap:var(--ly-space-8)}.ly-pad-0{padding:var(--ly-space-0)}.ly-pad-2{padding:var(--ly-space-2)}.ly-pad-4{padding:var(--ly-space-4)}.ly-pad-6{padding:var(--ly-space-6)}.ly-pad-8{padding:var(--ly-space-8)}.ly-mx-auto{margin-inline:auto}.ly-w-full{inline-size:100%}.ly-h-full{block-size:100%}.ly-min-h-screen{min-block-size:100vh;min-block-size:100dvh}.ly-overflow-auto{overflow:auto}.ly-overflow-hidden{overflow:hidden}.ly-items-start{align-items:flex-start}.ly-items-center{align-items:center}.ly-items-end{align-items:flex-end}.ly-items-stretch{align-items:stretch}.ly-justify-start{justify-content:flex-start}.ly-justify-center{justify-content:center}.ly-justify-end{justify-content:flex-end}.ly-justify-between{justify-content:space-between}.ly-frame-1x1{--ly-frame-ratio:1 / 1}.ly-frame-3x2{--ly-frame-ratio:3 / 2}.ly-frame-4x3{--ly-frame-ratio:4 / 3}.ly-frame-16x9{--ly-frame-ratio:16 / 9}.ly-frame-21x9{--ly-frame-ratio:21 / 9}.ly-hidden{display:none !important}.ly-visually-hidden{position:absolute !important;inline-size:1px !important;block-size:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0,0,0,0) !important;clip-path:inset(50%) !important;white-space:nowrap !important}}@layer ly.personalities.minimal-saas{:where(.ly-root[data-ly-layout="minimal-saas"]){--ly-personality-wrapper-max:88rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:16rem;--ly-recipe-rail:15rem;--ly-recipe-aside:16rem;--ly-split-primary:1.05fr;--ly-split-secondary:0.95fr}}@layer ly.personalities.bento{:where(.ly-root[data-ly-layout="bento"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:12rem;--ly-card-grid-min:14rem;--ly-gallery-min:11rem;--ly-recipe-rail:14rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header header" "sidebar main main" "aside main main" "footer footer footer";--ly-app-shell-wide-rows:auto auto minmax(0,1fr) auto;--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.3fr) minmax(0,1fr) minmax(0,1fr)}}@layer ly.personalities.maximalist{:where(.ly-root[data-ly-layout="maximalist"]){--ly-personality-wrapper-max:100%;--ly-profile-gap:var(--ly-space-7);--ly-grid-min:10rem;--ly-card-grid-min:13rem;--ly-gallery-min:10rem;--ly-recipe-rail:15rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header header header" "main main sidebar aside" "main main footer footer";--ly-app-shell-wide-columns:minmax(0,1.4fr) minmax(0,0.6fr) minmax(min(100%,var(--ly-recipe-rail)),0.32fr) minmax(min(100%,var(--ly-recipe-aside)),0.38fr)}}@layer ly.personalities.bauhaus{:where(.ly-root[data-ly-layout="bauhaus"]){--ly-personality-wrapper-max:96rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:13rem;--ly-recipe-rail:14rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header header header" "sidebar main main aside" "sidebar footer footer aside";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.24fr) minmax(0,0.7fr) minmax(0,1.3fr) minmax(min(100%,var(--ly-recipe-aside)),0.3fr)}}@layer ly.personalities.tactile{:where(.ly-root[data-ly-layout="tactile"]){--ly-personality-wrapper-max:82rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:15rem;--ly-recipe-rail:17rem;--ly-recipe-aside:20rem;--ly-app-shell-wide-areas:"header header" "sidebar main" "aside main" "footer footer";--ly-app-shell-wide-rows:auto auto minmax(0,1fr) auto;--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.28fr) minmax(0,1fr);--ly-list-detail-wide-columns:minmax(min(100%,var(--ly-pane-size)),0.5fr) minmax(0,0.9fr)}}@layer ly.personalities.neumorphism{:where(.ly-root[data-ly-layout="neumorphism"]){--ly-personality-wrapper-max:84rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:17rem;--ly-recipe-rail:17rem;--ly-recipe-aside:19rem;--ly-app-shell-wide-areas:"header sidebar" "main sidebar" "aside sidebar" "footer sidebar";--ly-app-shell-wide-rows:auto minmax(0,1fr) auto auto;--ly-app-shell-wide-columns:minmax(0,1fr) minmax(min(100%,var(--ly-recipe-rail)),0.3fr);--ly-list-detail-wide-columns:minmax(0,1.1fr) minmax(min(100%,var(--ly-pane-size)),0.7fr)}}@layer ly.personalities.retrofuturism{:where(.ly-root[data-ly-layout="retrofuturism"]){--ly-personality-wrapper-max:106rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:14rem;--ly-recipe-rail:14rem;--ly-recipe-aside:17rem;--ly-app-shell-wide-areas:"sidebar header aside" "sidebar main aside" "sidebar footer aside";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.24fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.3fr)}}@layer ly.personalities.brutalism{:where(.ly-root[data-ly-layout="brutalism"]){--ly-personality-wrapper-max:100rem;--ly-profile-gap:var(--ly-space-3);--ly-grid-min:14rem;--ly-recipe-rail:16rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header sidebar" "main aside sidebar" "footer footer sidebar";--ly-app-shell-wide-columns:minmax(0,1.3fr) minmax(0,0.7fr) minmax(min(100%,var(--ly-recipe-rail)),0.34fr)}}@layer ly.personalities.cyberpunk{:where(.ly-root[data-ly-layout="cyberpunk"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:12rem;--ly-recipe-rail:13rem;--ly-recipe-aside:19rem;--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "sidebar footer footer";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.2fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.32fr)}}@layer ly.personalities.y2k{:where(.ly-root[data-ly-layout="y2k"]){--ly-personality-wrapper-max:90rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:13rem;--ly-recipe-rail:18rem;--ly-recipe-aside:16rem;--ly-app-shell-wide-areas:"header header sidebar" "aside main sidebar" "footer footer sidebar";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-aside)),0.34fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-rail)),0.28fr)}}@layer ly.personalities.retro-glass{:where(.ly-root[data-ly-layout="retro-glass"]){--ly-personality-wrapper-max:104rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:15rem;--ly-recipe-rail:18rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"header header sidebar" "main main sidebar" "aside footer footer";--ly-app-shell-wide-columns:minmax(0,1.25fr) minmax(0,0.75fr) minmax(min(100%,var(--ly-recipe-rail)),0.32fr)}}@layer ly.personalities.f-pattern{:where(.ly-root[data-ly-layout="f-pattern"]){--ly-personality-wrapper-max:92rem;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:14rem;--ly-recipe-rail:16rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "sidebar footer footer";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.26fr) minmax(0,1.35fr) minmax(min(100%,var(--ly-recipe-aside)),0.65fr);--ly-split-hero-wide-columns:minmax(0,1.8fr) minmax(0,0.8fr)}}@layer ly.personalities.z-pattern{:where(.ly-root[data-ly-layout="z-pattern"]){--ly-personality-wrapper-max:108rem;--ly-profile-gap:var(--ly-space-5);--ly-grid-min:14rem;--ly-recipe-rail:17rem;--ly-recipe-aside:19rem;--ly-app-shell-wide-areas:"header header sidebar" "main aside sidebar" "footer aside sidebar";--ly-app-shell-wide-columns:minmax(0,1.45fr) minmax(min(100%,var(--ly-recipe-aside)),0.55fr) minmax(min(100%,var(--ly-recipe-rail)),0.3fr);--ly-split-hero-wide-columns:minmax(0,1.5fr) minmax(0,0.5fr)}}@layer ly.personalities.split-screen{:where(.ly-root[data-ly-layout="split-screen"]){--ly-personality-wrapper-max:100%;--ly-profile-gap:var(--ly-space-4);--ly-grid-min:20rem;--ly-recipe-rail:24rem;--ly-recipe-aside:24rem;--ly-app-shell-wide-areas:"header header" "main sidebar" "aside sidebar" "footer footer";--ly-app-shell-wide-rows:auto minmax(0,1fr) auto auto;--ly-app-shell-wide-columns:minmax(0,1fr) minmax(0,1fr);--ly-split-hero-wide-columns:minmax(0,1fr) minmax(0,1fr);--ly-list-detail-wide-columns:minmax(0,1fr) minmax(0,1fr)}}@layer ly.personalities.mondrian{:where(.ly-root[data-ly-layout="mondrian"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-3);--ly-grid-min:11rem;--ly-recipe-rail:13rem;--ly-recipe-aside:18rem;--ly-app-shell-wide-areas:"sidebar header header" "sidebar main aside" "footer footer aside";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.22fr) minmax(0,1.45fr) minmax(min(100%,var(--ly-recipe-aside)),0.55fr)}}@layer ly.personalities.synthwave{:where(.ly-root[data-ly-layout="synthwave"]){--ly-personality-wrapper-max:112rem;--ly-profile-gap:var(--ly-space-6);--ly-grid-min:14rem;--ly-recipe-rail:16rem;--ly-recipe-aside:20rem;--ly-app-shell-wide-areas:"header header header" "sidebar main aside" "footer footer footer";--ly-app-shell-wide-columns:minmax(min(100%,var(--ly-recipe-rail)),0.28fr) minmax(0,1fr) minmax(min(100%,var(--ly-recipe-aside)),0.34fr);--ly-reel-item-min:20rem}} diff --git a/dist/primitives.css b/dist/primitives.css index 6533b68..dd3262a 100644 --- a/dist/primitives.css +++ b/dist/primitives.css @@ -34,11 +34,11 @@ } .ly-section--compact { - --ly-section-padding-block: var(--ly-space-7); + padding-block: var(--ly-section-padding-block-compact); } .ly-section--flush { - --ly-section-padding-block: 0; + padding-block: 0; } .ly-readable { diff --git a/dist/wrappers.css b/dist/wrappers.css index d9dd8c0..b9b86d3 100644 --- a/dist/wrappers.css +++ b/dist/wrappers.css @@ -5,7 +5,7 @@ @layer ly.wrappers { .ly-wrapper { --ly-wrapper-max: var(--ly-personality-wrapper-max, var(--ly-wrapper-content)); - --ly-wrapper-fluid-gutter: clamp(1rem, 3vw, 3rem); + --ly-wrapper-fluid-gutter: var(--ly-wrapper-gutter); --ly-wrapper-local-gutter: max( var(--ly-wrapper-fluid-gutter), var(--ly-safe-area-inline) @@ -19,13 +19,6 @@ padding-inline: var(--ly-wrapper-local-gutter); } - /* Container-relative gutters enhance the universally valid viewport fallback. */ - @supports (width: 1cqi) { - .ly-wrapper { - --ly-wrapper-fluid-gutter: clamp(1rem, 3cqi, 3rem); - } - } - .ly-wrapper--compact { --ly-wrapper-max: var(--ly-wrapper-compact); } diff --git a/manifest.json b/manifest.json index d10d80d..78b5af4 100644 --- a/manifest.json +++ b/manifest.json @@ -76,7 +76,8 @@ "--ly-page-padding-inline", "--ly-safe-area-inline", "--ly-safe-area-block-start", "--ly-safe-area-block-end", "--ly-wrapper-gutter", "--ly-wrapper-max", "--ly-profile-gap", "--ly-gap", "--ly-grid-gap", "--ly-stack-gap", "--ly-cluster-gap", - "--ly-section-padding-block", "--ly-header-height", "--ly-sticky-position", "--ly-cover-min", + "--ly-section-padding-block", "--ly-section-padding-block-compact", "--ly-header-height", + "--ly-sticky-position", "--ly-cover-min", "--ly-shell-min", "--ly-scroll-max", "--ly-switcher-threshold", "--ly-sidebar-size", "--ly-sidebar-content-min", "--ly-grid-columns", "--ly-grid-min", "--ly-split-min", "--ly-pane-min", "--ly-pane-size", "--ly-media-min", "--ly-media-size", "--ly-reel-item-min", diff --git a/styles/foundation.css b/styles/foundation.css index 94929bd..368283c 100644 --- a/styles/foundation.css +++ b/styles/foundation.css @@ -22,14 +22,14 @@ --ly-wrapper-prose: 68ch; --ly-wrapper-content: 72rem; --ly-wrapper-wide: 112rem; - --ly-page-padding-inline: clamp(1rem, 4vw, 3rem); + --ly-page-padding-inline: clamp(1rem, 3vw, 3rem); --ly-safe-area-inline: max( env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px) ); --ly-safe-area-block-start: env(safe-area-inset-top, 0px); --ly-safe-area-block-end: env(safe-area-inset-bottom, 0px); - --ly-wrapper-gutter: max(var(--ly-page-padding-inline), var(--ly-safe-area-inline)); + --ly-wrapper-gutter: var(--ly-page-padding-inline); --ly-profile-gap: var(--ly-space-5); --ly-gap: var(--ly-profile-gap); @@ -37,6 +37,7 @@ --ly-stack-gap: var(--ly-space-4); --ly-cluster-gap: var(--ly-space-3); --ly-section-padding-block: clamp(3rem, 7vh, 6rem); + --ly-section-padding-block-compact: clamp(1.5rem, 3.5vh, 3rem); --ly-header-height: 4.5rem; --ly-sticky-position: sticky; --ly-cover-min: 100vh; @@ -128,6 +129,13 @@ min-block-size: 0; } + /* Prefer the nearest layout scope for gutters when container units parse. */ + @supports (width: 1cqi) { + :where(.ly-root) { + --ly-page-padding-inline: clamp(1rem, 3cqi, 3rem); + } + } + /* Enhance viewport-bound tokens only when dynamic viewport units parse. */ @supports (height: 100dvh) { :where(.ly-root) { @@ -165,6 +173,7 @@ --ly-stack-gap: min(var(--ly-space-4), 3vh); --ly-cluster-gap: min(var(--ly-space-3), 2vh); --ly-section-padding-block: clamp(1.5rem, 6vh, 3rem); + --ly-section-padding-block-compact: clamp(0.75rem, 3vh, 1.25rem); --ly-header-height: 3.5rem; --ly-scroll-max: min(62vh, 34rem); } @@ -174,6 +183,7 @@ --ly-stack-gap: min(var(--ly-space-4), 3dvh); --ly-cluster-gap: min(var(--ly-space-3), 2dvh); --ly-section-padding-block: clamp(1.5rem, 6dvh, 3rem); + --ly-section-padding-block-compact: clamp(0.75rem, 3dvh, 1.25rem); --ly-scroll-max: min(62dvh, 34rem); } } @@ -185,6 +195,7 @@ --ly-shell-min: auto; --ly-sticky-position: static; --ly-section-padding-block: var(--ly-space-5); + --ly-section-padding-block-compact: var(--ly-space-3); --ly-scroll-max: max(12rem, calc(100vh - 8rem)); } diff --git a/styles/primitives.css b/styles/primitives.css index 6533b68..dd3262a 100644 --- a/styles/primitives.css +++ b/styles/primitives.css @@ -34,11 +34,11 @@ } .ly-section--compact { - --ly-section-padding-block: var(--ly-space-7); + padding-block: var(--ly-section-padding-block-compact); } .ly-section--flush { - --ly-section-padding-block: 0; + padding-block: 0; } .ly-readable { diff --git a/styles/wrappers.css b/styles/wrappers.css index d9dd8c0..b9b86d3 100644 --- a/styles/wrappers.css +++ b/styles/wrappers.css @@ -5,7 +5,7 @@ @layer ly.wrappers { .ly-wrapper { --ly-wrapper-max: var(--ly-personality-wrapper-max, var(--ly-wrapper-content)); - --ly-wrapper-fluid-gutter: clamp(1rem, 3vw, 3rem); + --ly-wrapper-fluid-gutter: var(--ly-wrapper-gutter); --ly-wrapper-local-gutter: max( var(--ly-wrapper-fluid-gutter), var(--ly-safe-area-inline) @@ -19,13 +19,6 @@ padding-inline: var(--ly-wrapper-local-gutter); } - /* Container-relative gutters enhance the universally valid viewport fallback. */ - @supports (width: 1cqi) { - .ly-wrapper { - --ly-wrapper-fluid-gutter: clamp(1rem, 3cqi, 3rem); - } - } - .ly-wrapper--compact { --ly-wrapper-max: var(--ly-wrapper-compact); } diff --git a/test/demo-smoke.test.mjs b/test/demo-smoke.test.mjs index fa2b950..00e5430 100644 --- a/test/demo-smoke.test.mjs +++ b/test/demo-smoke.test.mjs @@ -817,6 +817,68 @@ const verifyHeightBehavior = async (page, baseUrl) => { } }; +/** + * Verifies compact section ordering across viewport-height tiers and proves + * that both public Wrapper gutter tokens control computed padding. + * + * @param {import("@playwright/test").Browser} browser Active browser instance. + * @param {string} baseUrl Demo server URL. + * @returns {Promise} + */ +const verifySectionAndGutterContracts = async (browser, baseUrl) => { + for (const height of [1080, 704, 480]) { + const contractPage = await browser.newPage({ viewport: { width: 1440, height } }); + try { + await contractPage.goto(`${baseUrl}?ecosystem=layout-only`, { + waitUntil: "domcontentloaded" + }); + await contractPage.waitForFunction(() => document.body.dataset.demoReady === "true"); + const result = await contractPage.evaluate(() => { + const root = document.createElement("div"); + root.className = "ly-root"; + root.style.inlineSize = "62.5rem"; + + const normal = document.createElement("section"); + normal.className = "ly-section"; + normal.textContent = "Normal section"; + const compact = document.createElement("section"); + compact.className = "ly-section ly-section--compact"; + compact.textContent = "Compact section"; + const wrapper = document.createElement("div"); + wrapper.className = "ly-wrapper"; + wrapper.textContent = "Wrapper"; + root.append(normal, compact, wrapper); + document.body.append(root); + + const normalPadding = parseFloat(getComputedStyle(normal).paddingBlockStart); + const compactPadding = parseFloat(getComputedStyle(compact).paddingBlockStart); + root.style.setProperty("--ly-page-padding-inline", "22px"); + root.style.removeProperty("--ly-wrapper-gutter"); + const pageTokenPadding = parseFloat(getComputedStyle(wrapper).paddingInlineStart); + root.style.setProperty("--ly-wrapper-gutter", "34px"); + const gutterTokenPadding = parseFloat(getComputedStyle(wrapper).paddingInlineStart); + root.remove(); + + return { + normalPadding, + compactPadding, + pageTokenPadding, + gutterTokenPadding + }; + }); + + assert( + result.compactPadding < result.normalPadding, + `Compact section padding must stay below normal padding at ${height}px: ${JSON.stringify(result)}` + ); + assert.equal(result.pageTokenPadding, 22, "Page padding token must control Wrapper padding."); + assert.equal(result.gutterTokenPadding, 34, "Wrapper gutter token must control padding."); + } finally { + await contractPage.close(); + } + } +}; + const verifyDefaultFontHeightTiers = async (baseUrl) => { if (browserName !== "chromium") return; @@ -1163,6 +1225,7 @@ try { await verifyAppShellRowGeometry(page, server.baseUrl); await verifyManualAndNearestContainer(page, server.baseUrl); await verifyHeightBehavior(page, server.baseUrl); + await verifySectionAndGutterContracts(browser, server.baseUrl); await verifyDefaultFontHeightTiers(server.baseUrl); await verifyDeviceMatrix(page, server.baseUrl); await verifyPersonalityMatrix(page, server.baseUrl); diff --git a/test/layout-css-contract.test.mjs b/test/layout-css-contract.test.mjs index b56191a..fddbaf5 100644 --- a/test/layout-css-contract.test.mjs +++ b/test/layout-css-contract.test.mjs @@ -256,11 +256,19 @@ assert( "Every wrapper must establish the shared responsive scope" ); assert( - /--ly-wrapper-fluid-gutter:\s*clamp\(1rem,\s*3vw,\s*3rem\)/.test(wrappers) && - /@supports\s*\(width:\s*1cqi\)[\s\S]*--ly-wrapper-fluid-gutter:\s*clamp\(1rem,\s*3cqi,\s*3rem\)/.test( - wrappers + /--ly-wrapper-gutter:\s*var\(--ly-page-padding-inline\)/.test(foundation) && + /--ly-wrapper-fluid-gutter:\s*var\(--ly-wrapper-gutter\)/.test(wrappers) && + /padding-inline:\s*var\(--ly-wrapper-local-gutter\)/.test(wrappers), + "Public Wrapper gutter tokens must reach rendered Wrapper padding." +); +assert( + /--ly-section-padding-block-compact:\s*clamp\(1\.5rem,\s*3\.5vh,\s*3rem\)/.test( + foundation + ) && + /\.ly-section--compact\s*\{[^}]*padding-block:\s*var\(--ly-section-padding-block-compact\)/s.test( + primitives ), - "Wrapper gutters must enhance the viewport fallback only when cqi is supported" + "Compact sections need an independent public padding token." ); assert( /\.ly-wrapper--breakout\s*\{[^}]*--ly-wrapper-max:\s*100%/s.test(wrappers), diff --git a/test/manifest-contract.test.mjs b/test/manifest-contract.test.mjs index 9aec7b1..7b9206d 100644 --- a/test/manifest-contract.test.mjs +++ b/test/manifest-contract.test.mjs @@ -52,7 +52,8 @@ const geometryTokens = [ "--ly-page-padding-inline", "--ly-safe-area-inline", "--ly-safe-area-block-start", "--ly-safe-area-block-end", "--ly-wrapper-gutter", "--ly-wrapper-max", "--ly-profile-gap", "--ly-gap", "--ly-grid-gap", "--ly-stack-gap", "--ly-cluster-gap", - "--ly-section-padding-block", "--ly-header-height", "--ly-sticky-position", "--ly-cover-min", + "--ly-section-padding-block", "--ly-section-padding-block-compact", "--ly-header-height", + "--ly-sticky-position", "--ly-cover-min", "--ly-shell-min", "--ly-scroll-max", "--ly-switcher-threshold", "--ly-sidebar-size", "--ly-sidebar-content-min", "--ly-grid-columns", "--ly-grid-min", "--ly-split-min", "--ly-pane-min", "--ly-pane-size", "--ly-media-min", "--ly-media-size", "--ly-reel-item-min", From 6875c5bcd91ad34f1486d596fc081993f4eb3c78 Mon Sep 17 00:00:00 2001 From: Foscat Date: Tue, 25 Aug 2026 15:21:10 -0500 Subject: [PATCH 5/7] chore: prepare layout style 3.0.2 --- .github/workflows/npm-publish.yml | 2 +- CHANGELOG.md | 12 ++++++++++++ README.md | 14 ++++++++++---- demo/index.html | 16 ++++++++-------- demo/sitemap.xml | 2 +- docs/wiki/Getting-Started.md | 4 ++-- docs/wiki/Home.md | 2 +- docs/wiki/Installation-And-CDN.md | 6 +++--- docs/wiki/Layout-Primitives.md | 4 ++++ docs/wiki/Layout-Recipes.md | 2 ++ docs/wiki/Layout-Styles.md | 2 ++ docs/wiki/Release-And-Publishing.md | 10 +++++----- ecosystem-release-fixture.json | 2 +- manifest.json | 2 +- package-lock.json | 10 +++++----- package.json | 4 ++-- test/demo-smoke.test.mjs | 12 ++++++------ test/layout-css-contract.test.mjs | 2 +- test/manifest-contract.test.mjs | 2 +- test/package-contract.test.mjs | 12 ++++++------ test/pages-artifact.test.mjs | 6 +++--- test/release-docs-contract.test.mjs | 18 ++++++++++++------ test/release-preflight.test.mjs | 2 +- 23 files changed, 90 insertions(+), 58 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 2d8d95d..cbb1f42 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: inputs: release_tag: - description: "Release tag to publish, for example v3.0.1" + description: "Release tag to publish, for example v3.0.2" required: true type: string diff --git a/CHANGELOG.md b/CHANGELOG.md index fb5bf96..e195de8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to `layout-style-css` are documented here. This project follows semantic versioning. +## [3.0.2] - 2026-08-25 + +### Fixed + +- Matched App Shell row tracks to base, medium, wide, and personality-specific area topologies so primary content receives flexible height while headers and footers remain intrinsic. +- Separated compact section spacing from normal section spacing and preserved the compact-smaller-than-normal invariant across regular, short, and shallow viewport heights. +- Connected the documented page-padding and Wrapper-gutter tokens to rendered Wrapper padding while retaining safe-area handling. + +### Tests + +- Added static and rendered regression coverage for App Shell row counts, flexible workspace allocation, compact section ordering, public gutter overrides, generated CSS parity, package metadata, and Pages artifacts. + ## [3.0.1] - 2026-08-09 ### Changed diff --git a/README.md b/README.md index 3f53a47..67297f4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Zero-configuration, dependency-free structural CSS that responds to the width and height a layout actually receives. -`layout-style-css@3.0.1` is the current v3 patch release and preserves the clean-break structural contract introduced in 3.0.0. It provides intrinsic wrappers, composition primitives, seven semantic recipes, and sixteen spatial personalities. Layout owns structure; UI Style Kit owns paint; Interactive Surface owns interaction styling. +`layout-style-css@3.0.2` is the current v3 patch release and preserves the clean-break structural contract introduced in 3.0.0. It provides intrinsic wrappers, composition primitives, seven semantic recipes, and sixteen spatial personalities. Layout owns structure; UI Style Kit owns paint; Interactive Surface owns interaction styling. ## Requirements @@ -13,7 +13,7 @@ Zero-configuration, dependency-free structural CSS that responds to the width an ## Install ```bash -npm install layout-style-css@3.0.1 +npm install layout-style-css@3.0.2 ``` Most applications need one import: @@ -68,6 +68,8 @@ Wrappers are optional measure and nesting controls. Every wrapper uses logical p All lanes clamp to the available inline size, including allocations from 320px through ultrawide screens. +`--ly-page-padding-inline` supplies the default `--ly-wrapper-gutter` value. Override `--ly-wrapper-gutter` on `.ly-root` or a narrower scope to control rendered Wrapper padding; safe-area compensation is still applied once at the Wrapper edge. + ## Composition Primitives The core includes: @@ -93,6 +95,8 @@ Recipes use attributes only. The stacked semantic source order is always safe, a | `data-ly-recipe="gallery"` | Intrinsic tracks; no topology breakpoint | | `data-ly-recipe="card-grid"` | Intrinsic tracks; no topology breakpoint | +App Shell base, medium, and wide topologies own matching row definitions. This keeps the primary workspace flexible while header and footer tracks remain intrinsic, including personality-specific four-row topologies. + Canonical regions use `data-ly-area`, including `header`, `sidebar`, `nav`, `main`, `aside`, `footer`, `content`, `media`, `actions`, `primary`, and `secondary`. Recipe class aliases such as `.ly-dashboard` no longer exist. Use: @@ -129,6 +133,8 @@ The system responds to available block size without orientation queries: - At viewport heights of `30rem` or less, recipe-owned sticky behavior becomes normal flow, and cover/shell minimums stop forcing full-height regions. - Safe-area block insets remain available, and required regions are never hidden solely because the viewport is short. +`--ly-section-padding-block` controls normal sections. `--ly-section-padding-block-compact` independently controls `.ly-section--compact`, preserving a smaller rhythm through the regular, short, and shallow height tiers. + Use the public height, gap, measure, ratio, rail, media, card, and grid-minimum custom properties for advanced tuning. Defaults use `100vh` fallbacks followed by `100dvh`. ## Personalities @@ -176,8 +182,8 @@ This order lets UI Style Kit establish paint and theme roles, Interactive Surfac ## CDN ```html - - + + ``` ## Clean-Break Migration diff --git a/demo/index.html b/demo/index.html index cad59f9..a7d3ffc 100644 --- a/demo/index.html +++ b/demo/index.html @@ -6,7 +6,7 @@ - + @@ -47,7 +47,7 @@ - + @@ -71,28 +71,28 @@ - - + + - - + + Skip to the layout lab diff --git a/demo/sitemap.xml b/demo/sitemap.xml index 9be6915..779f098 100644 --- a/demo/sitemap.xml +++ b/demo/sitemap.xml @@ -2,7 +2,7 @@ https://foscat.github.io/Layout-Style-CSS/ - 2026-08-09 + 2026-08-25 weekly 0.8 diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md index a5964e5..7385b18 100644 --- a/docs/wiki/Getting-Started.md +++ b/docs/wiki/Getting-Started.md @@ -1,9 +1,9 @@ # Getting Started -Install `layout-style-css@3.0.1` and import the root bundle: +Install `layout-style-css@3.0.2` and import the root bundle: ```bash -npm install layout-style-css@3.0.1 +npm install layout-style-css@3.0.2 ``` ```js diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index b5af437..8601e07 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -1,6 +1,6 @@ # Layout Style CSS v3 -`layout-style-css@3.0.1` is a dependency-free, CSS-only structural system for layouts that adapt to available width and height. +`layout-style-css@3.0.2` is a dependency-free, CSS-only structural system for layouts that adapt to available width and height. ## Contract diff --git a/docs/wiki/Installation-And-CDN.md b/docs/wiki/Installation-And-CDN.md index ed762e5..c85b186 100644 --- a/docs/wiki/Installation-And-CDN.md +++ b/docs/wiki/Installation-And-CDN.md @@ -9,7 +9,7 @@ ## Package ```bash -npm install layout-style-css@3.0.1 +npm install layout-style-css@3.0.2 ``` ```js @@ -36,11 +36,11 @@ Package and bundler defaults (`main`, `style`, and the root export) use the read ## CDN ```html - + ``` ```html - + ``` The CDN paths include `/dist/` because CDN clients address files in the published tarball, while package import maps use the public exports. diff --git a/docs/wiki/Layout-Primitives.md b/docs/wiki/Layout-Primitives.md index 49edc87..2e328af 100644 --- a/docs/wiki/Layout-Primitives.md +++ b/docs/wiki/Layout-Primitives.md @@ -19,6 +19,8 @@ Wrappers are optional local responsive scopes: Breakout children select clamped lanes with `data-ly-lane="content"`, `data-ly-lane="feature"`, or `data-ly-lane="full"`. +`--ly-page-padding-inline` supplies the default `--ly-wrapper-gutter` value. `--ly-wrapper-gutter` controls rendered Wrapper padding at the scope where it is overridden, while the Wrapper applies safe-area compensation once at its outer edge. + ## Flow - `.ly-stack` creates vertical flow. @@ -49,6 +51,8 @@ Page, cover, and bounded scroll behaviors use `vh` fallbacks followed by dynamic At `44rem` viewport height or less, gaps and scroll maxima tighten. At `30rem` or less, forced cover/shell minimums and recipe-owned sticky positioning are removed. Required regions remain in normal document flow. +`--ly-section-padding-block` controls normal `.ly-section` spacing. `--ly-section-padding-block-compact` controls `.ly-section--compact` independently and remains smaller than the normal value across regular, short, and shallow viewport heights. + ## Public Tuning Advanced consumers can override stable custom properties for: diff --git a/docs/wiki/Layout-Recipes.md b/docs/wiki/Layout-Recipes.md index ef040ed..9152bf2 100644 --- a/docs/wiki/Layout-Recipes.md +++ b/docs/wiki/Layout-Recipes.md @@ -16,6 +16,8 @@ Recipes are attribute-only semantic layouts. The mobile DOM order is authoritati Recipe roots also establish `ly-scope`, so child compositions can respond without extra setup. +App Shell base, medium, and wide topologies own matching row definitions. Header and footer tracks remain intrinsic while the main workspace receives the flexible row, including personality-specific four-row topologies. + ## Areas Canonical `data-ly-area` values are: diff --git a/docs/wiki/Layout-Styles.md b/docs/wiki/Layout-Styles.md index 8def1de..ac2a90b 100644 --- a/docs/wiki/Layout-Styles.md +++ b/docs/wiki/Layout-Styles.md @@ -23,6 +23,8 @@ Every profile feeds the same wrapper, primitive, and recipe engine. Profiles tun A profile must remain visibly distinct through at least two spatial characteristics. Personality files may not declare their own `@container`, viewport, or orientation breakpoint systems. +When a profile supplies an App Shell area template, its matching row definition must contain the same number of rows and keep the main workspace on the flexible track. + ## Ownership Layout owns structure. UI Style Kit owns paint. Interactive Surface owns interaction styling. diff --git a/docs/wiki/Release-And-Publishing.md b/docs/wiki/Release-And-Publishing.md index 14e416a..a8a2521 100644 --- a/docs/wiki/Release-And-Publishing.md +++ b/docs/wiki/Release-And-Publishing.md @@ -1,6 +1,6 @@ # Release And Publishing -This guide describes the release gate for `layout-style-css@3.0.1`. Running verification does not publish, tag, push, create a GitHub Release, or change the npm registry. +This guide describes the release gate for `layout-style-css@3.0.2`. Running verification does not publish, tag, push, create a GitHub Release, or change the npm registry. ## Local Candidate Gate @@ -23,7 +23,7 @@ The package's `prepublishOnly` script runs `npm run release:verify`, so a direct The immutable cross-repository pins require this exact remote sequence: -1. Push a stable UI bootstrap ref containing `bdbb6a7e432f30b92de206cac6a00fe85394190c`. +1. Push a stable UI bootstrap ref containing `33e3c834aec62b85650dd14aea9d7d388abef191`. 2. Push and merge Interactive Surface CSS and Layout Style CSS with merge commits so their reviewed commit SHAs remain reachable. 3. Update and verify the final UI companion pins against those merged companion commits. 4. Push the final UI branch, rerun its ecosystem preflight, and merge UI with a merge commit. @@ -35,8 +35,8 @@ The bootstrap SHA is deliberately stable: companion workflows use it to load the Confirm: -- package version is `3.0.1` -- intended tag would be `v3.0.1` +- package version is `3.0.2` +- intended tag would be `v3.0.2` - runtime and peer dependencies are empty - exact exports and tarball files match the v3 contract - generated CSS matches authored sources @@ -50,7 +50,7 @@ Only after explicit approval: 1. Commit the verified candidate. 2. Push the intended branch. 3. Merge through the repository's normal review policy. -4. Create the exact `v3.0.1` tag from the protected release commit. +4. Create the exact `v3.0.2` tag from the protected release commit. 5. Publish the GitHub Release. 6. Let the protected npm workflow verify the tag and publish with provenance. 7. Confirm the GitHub Release, tag, and npm registry state independently. diff --git a/ecosystem-release-fixture.json b/ecosystem-release-fixture.json index b5efab0..da5aba3 100644 --- a/ecosystem-release-fixture.json +++ b/ecosystem-release-fixture.json @@ -1,4 +1,4 @@ { "repository": "Foscat/ui-style-kit-css", - "revision": "bdbb6a7e432f30b92de206cac6a00fe85394190c" + "revision": "33e3c834aec62b85650dd14aea9d7d388abef191" } diff --git a/manifest.json b/manifest.json index 78b5af4..5141c4e 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "breakingChange": "increment-schemaVersion-before-removing-or-renaming-fields" }, "name": "layout-style-css", - "version": "3.0.1", + "version": "3.0.2", "entrypoints": { "default": "./dist/layout-style-css.css", "minified": "./dist/layout-style-css.min.css", diff --git a/package-lock.json b/package-lock.json index b7a94d6..783746e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "layout-style-css", - "version": "3.0.1", + "version": "3.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "layout-style-css", - "version": "3.0.1", + "version": "3.0.2", "license": "MIT", "devDependencies": { "@playwright/test": "1.61.1", @@ -1069,9 +1069,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.17", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", - "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index 633fa94..0c6ee2f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "layout-style-css", - "version": "3.0.1", + "version": "3.0.2", "description": "Dependency-free structural CSS for responsive wrappers, composition primitives, recipes, utilities, and layout personalities.", "type": "module", "license": "MIT", @@ -64,7 +64,7 @@ "overrides": { "fast-uri": "3.1.5", "js-yaml": "4.3.1", - "nanoid": "3.3.17", + "nanoid": "3.3.18", "postcss": "8.5.23" }, "exports": { diff --git a/test/demo-smoke.test.mjs b/test/demo-smoke.test.mjs index 00e5430..9efe2ce 100644 --- a/test/demo-smoke.test.mjs +++ b/test/demo-smoke.test.mjs @@ -130,7 +130,7 @@ const topologyEdges = [ const assertStaticDemoContract = () => { assert.match(demoHtml, /Layout Style CSS v3/); - assert.match(demoHtml, /content="3\.0\.1"/); + assert.match(demoHtml, /content="3\.0\.2"/); assert.match(demoHtml, /id="deviceSelect"/); assert.match(demoHtml, /id="containerSelect"/); assert.match(demoHtml, /id="heightSelect"/); @@ -138,17 +138,17 @@ const assertStaticDemoContract = () => { assert.match(demoHtml, /id="topologyReadout"/); assert.match( demoHtml, - /href="\.\.\/dist\/layout-style-css\.css\?v=3\.0\.1"/, + /href="\.\.\/dist\/layout-style-css\.css\?v=3\.0\.2"/, "The demo should cache-bust its v3 layout bundle." ); assert.match( demoHtml, - /href="\.\/demo\.css\?v=3\.0\.1"/, + /href="\.\/demo\.css\?v=3\.0\.2"/, "The demo should cache-bust its v3 presentation styles." ); assert.match( demoHtml, - /src="\.\/demo\.js\?v=3\.0\.1"/, + /src="\.\/demo\.js\?v=3\.0\.2"/, "The demo should cache-bust its v3 controller." ); assert.doesNotMatch(demoHtml, /integrations\/ui-style-kit\.css/); @@ -447,7 +447,7 @@ const verifyIdentityAndControls = async (page, baseUrl) => { }; const verifyPersonalityOptionsUsePairingMetadata = async (page, baseUrl) => { - const metadataUrl = new URL("../personalities.json?v=3.0.1", baseUrl).toString(); + const metadataUrl = new URL("../personalities.json?v=3.0.2", baseUrl).toString(); const pairingFixture = { schemaVersion: 1, personalities: [ @@ -539,7 +539,7 @@ const verifySynthwaveVisualRecommendations = async (page, baseUrl) => { }; const verifyPersonalityMetadataFailureRecovery = async (page, baseUrl) => { - const metadataUrl = new URL("../personalities.json?v=3.0.1", baseUrl).toString(); + const metadataUrl = new URL("../personalities.json?v=3.0.2", baseUrl).toString(); const recoveryContext = await page.context().browser().newContext(); const recoveryPage = await recoveryContext.newPage(); diff --git a/test/layout-css-contract.test.mjs b/test/layout-css-contract.test.mjs index fddbaf5..43e3014 100644 --- a/test/layout-css-contract.test.mjs +++ b/test/layout-css-contract.test.mjs @@ -106,7 +106,7 @@ function minifyCss(css) { .trim(); } -assert.equal(packageJson.version, "3.0.1", "The v3 branch must expose version 3.0.1"); +assert.equal(packageJson.version, "3.0.2", "The patch branch must expose version 3.0.2"); assert.equal(packageJson.engines?.node, ">=20", "Development must retain the Node 20 floor"); assert.deepEqual(packageJson.exports, expectedExports, "Package exports must match the clean v3 API"); assert.deepEqual( diff --git a/test/manifest-contract.test.mjs b/test/manifest-contract.test.mjs index 7b9206d..d400266 100644 --- a/test/manifest-contract.test.mjs +++ b/test/manifest-contract.test.mjs @@ -78,7 +78,7 @@ const internalLayoutTokens = [ test("ecosystem manifest publishes the structural API and package export", () => { assert.equal(manifest.schemaVersion, 1); assert.equal(manifest.name, packageJson.name); - assert.equal(manifest.version, "3.0.1"); + assert.equal(manifest.version, "3.0.2"); assert.equal(manifest.version, packageJson.version); assert.equal(manifest.schemaPolicy.compatibility, "additive-within-major"); assert.equal( diff --git a/test/package-contract.test.mjs b/test/package-contract.test.mjs index fa002b2..834eda6 100644 --- a/test/package-contract.test.mjs +++ b/test/package-contract.test.mjs @@ -21,7 +21,7 @@ const ecosystemManifest = JSON.parse( const expectedSecurityOverrides = { "fast-uri": "3.1.5", "js-yaml": "4.3.1", - nanoid: "3.3.17", + nanoid: "3.3.18", postcss: "8.5.23", }; const expectedExports = { @@ -129,11 +129,11 @@ test("package defaults and homepage expose the intended distribution contract", } }); -test("release metadata stays synchronized for the 3.0.1 patch", () => { - assert.equal(manifest.version, "3.0.1"); - assert.equal(packageLock.version, "3.0.1"); - assert.equal(packageLock.packages[""].version, "3.0.1"); - assert.equal(ecosystemManifest.version, "3.0.1"); +test("release metadata stays synchronized for the 3.0.2 patch", () => { + assert.equal(manifest.version, "3.0.2"); + assert.equal(packageLock.version, "3.0.2"); + assert.equal(packageLock.packages[""].version, "3.0.2"); + assert.equal(ecosystemManifest.version, "3.0.2"); assert.equal(Object.keys(manifest.exports).length, 13); }); diff --git a/test/pages-artifact.test.mjs b/test/pages-artifact.test.mjs index ce22d9d..67c1ca0 100644 --- a/test/pages-artifact.test.mjs +++ b/test/pages-artifact.test.mjs @@ -81,7 +81,7 @@ for (const { attribute, path } of [ const contents = readFileSync(join(outputDir, ...path.split("/"))); const fingerprint = createHash("sha256").update(contents).digest("hex").slice(0, 12); assert( - index.includes(`${attribute}="./${path}?v=3.0.1-${fingerprint}"`), + index.includes(`${attribute}="./${path}?v=3.0.2-${fingerprint}"`), `Pages should fingerprint ${path} from its deployed contents.` ); } @@ -101,7 +101,7 @@ assert( "Pages demo should preserve the source demo canonical URL" ); assert( - index.includes('"version": "3.0.1"') && index.includes("Layout Style CSS v3"), + index.includes('"version": "3.0.2"') && index.includes("Layout Style CSS v3"), "Pages metadata should identify the v3 intrinsic responsive lab" ); assert( @@ -124,7 +124,7 @@ assert( sitemap.includes(`${expectedPagesBaseUrl}`), "The sitemap must use the case-sensitive Pages canonical URL." ); -assert(sitemap.includes("2026-08-09"), "Pages sitemap should carry 3.0.1 metadata"); +assert(sitemap.includes("2026-08-25"), "Pages sitemap should carry 3.0.2 metadata"); const pagesWorkflow = readFileSync(pagesWorkflowPath, "utf8"); const pagesPreflightStep = pagesWorkflow.indexOf("- name: Verify Pages configuration"); diff --git a/test/release-docs-contract.test.mjs b/test/release-docs-contract.test.mjs index 6769ed0..dcfe471 100644 --- a/test/release-docs-contract.test.mjs +++ b/test/release-docs-contract.test.mjs @@ -85,7 +85,7 @@ assert( ); for (const requiredText of [ - "3.0.1", + "3.0.2", "Node.js 20", "dependency-free", "zero-configuration", @@ -261,9 +261,15 @@ for (const releaseTopic of [ ]) { assert.match(patchRelease, releaseTopic, `The 3.0.1 changelog must cover ${releaseTopic}.`); } +const currentPatchHeadings = changelog.match(/^## \[3\.0\.2\] - 2026-08-25$/gm) ?? []; +assert.equal( + currentPatchHeadings.length, + 1, + "Changelog needs one dated 3.0.2 section." +); assert( - release.includes("layout-style-css@3.0.1") && - release.includes("v3.0.1") && + release.includes("layout-style-css@3.0.2") && + release.includes("v3.0.2") && release.includes("does not publish"), "Release documentation must distinguish verification from publication." ); @@ -271,9 +277,9 @@ assert.deepEqual( releaseFixture, { repository: "Foscat/ui-style-kit-css", - revision: "bdbb6a7e432f30b92de206cac6a00fe85394190c" + revision: "33e3c834aec62b85650dd14aea9d7d388abef191" }, - "Release automation must pin the reviewed UI Layout 3.0.1 bootstrap commit." + "Release automation must pin the reviewed UI Layout 3.0.2 bootstrap commit." ); assert( release.includes(releaseFixture.revision), @@ -321,7 +327,7 @@ for (const browser of ["chromium", "firefox", "webkit"]) { const publishWorkflow = read(".github", "workflows", "npm-publish.yml"); const demoSmoke = read("test", "demo-smoke.test.mjs"); -assert(publishWorkflow.includes("for example v3.0.1")); +assert(publishWorkflow.includes("for example v3.0.2")); assert(publishWorkflow.includes("playwright install --with-deps chromium firefox webkit")); assert( !/^\s*run:\s+npm\s+run\s+release:verify\s*$/m.test(publishWorkflow) && diff --git a/test/release-preflight.test.mjs b/test/release-preflight.test.mjs index 95f8a83..0849242 100644 --- a/test/release-preflight.test.mjs +++ b/test/release-preflight.test.mjs @@ -11,7 +11,7 @@ const rootDir = path.resolve( "..", ); const reviewedUiFixtureRevision = - "bdbb6a7e432f30b92de206cac6a00fe85394190c"; + "33e3c834aec62b85650dd14aea9d7d388abef191"; let releaseContract; try { releaseContract = await import("../scripts/release-fixture-contract.mjs"); From d410554fe45dc5e60427b065c4b483cc1aab4d55 Mon Sep 17 00:00:00 2001 From: Foscat Date: Tue, 25 Aug 2026 15:40:19 -0500 Subject: [PATCH 6/7] test: make metadata recovery hermetic --- test/demo-smoke.test.mjs | 55 +++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/test/demo-smoke.test.mjs b/test/demo-smoke.test.mjs index 9efe2ce..5981ec4 100644 --- a/test/demo-smoke.test.mjs +++ b/test/demo-smoke.test.mjs @@ -538,30 +538,45 @@ const verifySynthwaveVisualRecommendations = async (page, baseUrl) => { } }; +/** + * Verifies that metadata failure falls back to the packaged personality list + * inside a hermetic browser context with local companion stylesheet fixtures. + * + * @param {import("@playwright/test").Page} page Active demo page. + * @param {string} baseUrl Demo server URL. + * @returns {Promise} + */ const verifyPersonalityMetadataFailureRecovery = async (page, baseUrl) => { const metadataUrl = new URL("../personalities.json?v=3.0.2", baseUrl).toString(); const recoveryContext = await page.context().browser().newContext(); - const recoveryPage = await recoveryContext.newPage(); - - await recoveryPage.route(metadataUrl, (route) => route.fulfill({ status: 503, body: "Unavailable" })); - await recoveryPage.goto(`${baseUrl}?ecosystem=layout-only`, { waitUntil: "domcontentloaded" }); - await recoveryPage.waitForFunction( - () => document.body.dataset.demoReady === "true", - undefined, - { timeout: METADATA_FAILURE_RECOVERY_READINESS_TIMEOUT_MS } - ); + try { + const recoveryPage = await recoveryContext.newPage(); + await installExternalFixtures(recoveryPage); + await recoveryPage.route(metadataUrl, (route) => + route.fulfill({ status: 503, body: "Unavailable" }) + ); + await recoveryPage.goto(`${baseUrl}?ecosystem=layout-only`, { + waitUntil: "domcontentloaded" + }); + await recoveryPage.waitForFunction( + () => document.body.dataset.demoReady === "true", + undefined, + { timeout: METADATA_FAILURE_RECOVERY_READINESS_TIMEOUT_MS } + ); - const recovered = await recoveryPage.evaluate(() => ({ - fallback: window.LAYOUT_STYLE_PERSONALITY_METADATA?.personalities ?? [], - options: [...document.querySelectorAll("#personalitySelect option")].map((option) => option.value), - busy: document.querySelector("#personalitySelect")?.getAttribute("aria-busy"), - status: document.querySelector("#personalityMetadataStatus")?.textContent - })); - - assert.deepEqual(recovered.options, recovered.fallback.map(({ id }) => id)); - assert.equal(recovered.busy, "false"); - assert.match(recovered.status ?? "", /using packaged fallback/i); - await recoveryContext.close(); + const recovered = await recoveryPage.evaluate(() => ({ + fallback: window.LAYOUT_STYLE_PERSONALITY_METADATA?.personalities ?? [], + options: [...document.querySelectorAll("#personalitySelect option")].map((option) => option.value), + busy: document.querySelector("#personalitySelect")?.getAttribute("aria-busy"), + status: document.querySelector("#personalityMetadataStatus")?.textContent + })); + + assert.deepEqual(recovered.options, recovered.fallback.map(({ id }) => id)); + assert.equal(recovered.busy, "false"); + assert.match(recovered.status ?? "", /using packaged fallback/i); + } finally { + await recoveryContext.close(); + } }; const verifyTopologyEdges = async (page, baseUrl) => { From da21f971bfbc7bf3db0fd1cf13d7912680058fb3 Mon Sep 17 00:00:00 2001 From: Foscat Date: Tue, 25 Aug 2026 15:43:33 -0500 Subject: [PATCH 7/7] test: isolate section gutter fixtures --- test/demo-smoke.test.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/demo-smoke.test.mjs b/test/demo-smoke.test.mjs index 5981ec4..b5db163 100644 --- a/test/demo-smoke.test.mjs +++ b/test/demo-smoke.test.mjs @@ -844,6 +844,7 @@ const verifySectionAndGutterContracts = async (browser, baseUrl) => { for (const height of [1080, 704, 480]) { const contractPage = await browser.newPage({ viewport: { width: 1440, height } }); try { + await installExternalFixtures(contractPage); await contractPage.goto(`${baseUrl}?ecosystem=layout-only`, { waitUntil: "domcontentloaded" });