diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml
index cbb1f42..7846905 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.2"
+ description: "Release tag to publish, for example v3.1.0"
required: true
type: string
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e195de8..5b47cde 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,26 @@
All notable changes to `layout-style-css` are documented here. This project follows semantic versioning.
+## [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.
+
## [3.0.2] - 2026-08-25
### Fixed
diff --git a/README.md b/README.md
index 67297f4..cc3f4b7 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.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.
+`layout-style-css@3.1.0` is the current additive v3 release. It provides intrinsic wrappers, composition primitives, seven semantic recipes, explicit local density controls, 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.2
+npm install layout-style-css@3.1.0
```
Most applications need one import:
@@ -44,6 +44,17 @@ Use `.ly-root` as the responsive boundary and keep the mobile DOM order authorit
`.ly-root`, wrappers, and recipe roots establish the named `ly-scope` inline-size container. A recipe works directly in the root; when it is nested in a wrapper, it responds to that nearest wrapper.
+## Density Contexts
+
+Normal density is the zero-configuration default. Use `data-ly-density="compact"`, `data-ly-density="normal"`, or `data-ly-density="spacious"` on `.ly-root` or any nested layout subtree. A nested density explicitly resets inherited gaps and section rhythm, so task controls can remain compact inside a spacious marketing page.
+
+```html
+
+ Dense application controls
+
+
+```
+
## Wrappers
Wrappers are optional measure and nesting controls. Every wrapper uses logical properties, fluid container-relative gutters, safe-area compensation, and shrink-safe sizing.
@@ -54,6 +65,7 @@ Wrappers are optional measure and nesting controls. Every wrapper uses logical p
| `.ly-wrapper--compact` | `40rem` compact measure |
| `.ly-wrapper--prose` | `68ch` reading measure |
| `.ly-wrapper--content` | `72rem` content measure |
+| `.ly-wrapper--workspace` | `96rem` task-oriented application measure |
| `.ly-wrapper--wide` | `112rem` wide measure |
| `.ly-wrapper--full` | Full available inline size |
| `.ly-wrapper--breakout` | Clamped content, feature, and full lanes |
@@ -79,7 +91,9 @@ The core includes:
- `.ly-panes` and `.ly-media`
- `.ly-reel`, `.ly-frame`, and `.ly-scroll`
-Grid and flex primitives wrap intrinsically whenever track wrapping can replace a query. Page, cover, and bounded-scroll sizing use `vh` fallbacks followed by `100dvh`-aware behavior. Only `.ly-reel` deliberately scrolls horizontally; only `.ly-scroll` deliberately creates a bounded vertical scroll region.
+Grid and flex primitives wrap intrinsically whenever track wrapping can replace a query. Base `.ly-scroll` owns overflow but no height cap. Add `.ly-scroll--bounded` for the stable `--ly-scroll-max` cap or `.ly-scroll--viewport` for height-aware `--ly-scroll-viewport-max` behavior. Only `.ly-reel` deliberately scrolls horizontally; only the Scroll family deliberately creates vertical scrolling.
+
+Gap utilities `.ly-gap-0` through `.ly-gap-9` assign `gap` only on the element carrying the class. They do not alter inherited primitive gap variables. Override `--ly-gap`, `--ly-grid-gap`, `--ly-stack-gap`, or `--ly-cluster-gap` when a shared spacing context is intentional.
## Automatic Recipe Engine
@@ -95,6 +109,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 |
+The same ownership is available in `manifest.json` through `thresholds.recipes`, alongside the generic container-width and viewport-height arrays. Release checks verify every mapping against authored queries and require every public geometry token to reach a runtime CSS declaration.
+
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`.
@@ -162,7 +178,7 @@ Each personality is a token/topology profile consumed by the shared recipe engin
| `layout-style-css/personalities.json` | Public layout-to-visual pairing recommendations |
| `layout-style-css/package.json` | Package metadata |
-The cascade order is `ly.reset`, `ly.tokens`, `ly.wrappers`, `ly.primitives`, `ly.recipes`, `ly.utilities`, and `ly.personalities`.
+The cascade order is `ly.reset`, `ly.tokens`, `ly.wrappers`, `ly.primitives`, `ly.recipes`, `ly.utilities`, `ly.personalities`, and `ly.context`. Unlayered application CSS remains stronger than every library layer.
## Ecosystem Imports
@@ -177,18 +193,18 @@ import "layout-style-css";
This order lets UI Style Kit establish paint and theme roles, Interactive Surface add interaction states, and Layout apply structure.
-`data-ly-layout`, `data-ui`, `data-theme`, and `data-mode` are independently selectable. See [Layout Styles](docs/wiki/Layout-Styles.md#visual-pairing-guidance) for the full recommendation matrix; pairings are never dependencies.
+`data-ly-layout`, `data-ly-density`, `data-ui`, `data-theme`, and `data-mode` are independently selectable. See [Layout Styles](docs/wiki/Layout-Styles.md#visual-pairing-guidance) for the full recommendation matrix; pairings are never dependencies.
## CDN
```html
-
-
+
+
```
## Clean-Break Migration
-v3 has no compatibility bundle. It removes `legacy.css`, the deprecated UI Style Kit structural bridge, v1/v2 aliases, responsive `ly-md-*` and `ly-lg-*` utilities, and all visual-order utilities. See [Migrating To 3.0](docs/wiki/Migrating-To-3.0.md) for exact mappings.
+v3 has no compatibility bundle. It removes `legacy.css`, the deprecated UI Style Kit structural bridge, v1/v2 aliases, responsive `ly-md-*` and `ly-lg-*` utilities, and all visual-order utilities. See [Migrating To 3.0](docs/wiki/Migrating-To-3.0.md) for the major-version mappings and [Migrating To 3.1](docs/wiki/Migrating-To-3.1.md) for density, local gap, and Scroll behavior changes.
## Demo And Verification
@@ -214,6 +230,7 @@ npm run release:verify
- [Layout Recipes](docs/wiki/Layout-Recipes.md)
- [Layout Styles](docs/wiki/Layout-Styles.md)
- [Migrating To 3.0](docs/wiki/Migrating-To-3.0.md)
+- [Migrating To 3.1](docs/wiki/Migrating-To-3.1.md)
- [Demo And GitHub Pages](docs/wiki/Demo-And-GitHub-Pages.md)
- [Release And Publishing](docs/wiki/Release-And-Publishing.md)
- [Security And Support](docs/wiki/Security-And-Support.md)
diff --git a/demo/demo.css b/demo/demo.css
index 4170c83..c6a753f 100644
--- a/demo/demo.css
+++ b/demo/demo.css
@@ -45,12 +45,12 @@ body[data-mode="contrast"] {
color-scheme: dark;
}
-body[data-density="compact"] {
+body[data-demo-density="compact"] {
--demo-density-gap: 0.5rem;
--demo-region-padding: 0.75rem;
}
-body[data-density="spacious"] {
+body[data-demo-density="spacious"] {
--demo-density-gap: 1.5rem;
--demo-region-padding: 1.5rem;
}
@@ -367,7 +367,7 @@ body[data-ecosystem="all-three"] .demo-status {
--ly-grid-gap: var(--ly-gap);
--ly-section-padding-block: clamp(1.5rem, 6%, 3rem);
--ly-header-height: 3.5rem;
- --ly-scroll-max: min(62%, 34rem);
+ --ly-scroll-viewport-max: min(62%, 34rem);
}
.demo-preview-root[data-demo-height-tier="shallow"] {
@@ -375,7 +375,7 @@ body[data-ecosystem="all-three"] .demo-status {
--ly-shell-min: auto;
--ly-sticky-position: static;
--ly-section-padding-block: 1.5rem;
- --ly-scroll-max: max(12rem, calc(100% - 8rem));
+ --ly-scroll-viewport-max: max(12rem, calc(100% - 8rem));
}
.demo-preview-root[data-demo-height-tier="shallow"] .demo-recipe {
diff --git a/demo/demo.js b/demo/demo.js
index 4f3d3a4..ca80265 100644
--- a/demo/demo.js
+++ b/demo/demo.js
@@ -161,7 +161,16 @@ const ALLOWLISTS = Object.freeze({
"desktop-landscape",
"desktop-portrait"
]),
- wrapper: Object.freeze(["default", "compact", "prose", "content", "wide", "full", "breakout"]),
+ wrapper: Object.freeze([
+ "default",
+ "compact",
+ "prose",
+ "content",
+ "workspace",
+ "wide",
+ "full",
+ "breakout"
+ ]),
recipe: Object.freeze([
"app-shell",
"dashboard",
@@ -190,7 +199,7 @@ const ALLOWLISTS = Object.freeze({
]),
height: Object.freeze(["auto", "29rem", "31rem", "43rem", "45rem", "50rem"]),
responsive: Object.freeze(["auto", "manual"]),
- density: Object.freeze(["compact", "comfortable", "spacious"]),
+ density: Object.freeze(["compact", "normal", "spacious"]),
ui: Object.freeze(UI_STYLE_KIT_MANIFEST.presets.map((preset) => preset.id)),
theme: Object.freeze(UI_STYLE_KIT_MANIFEST.themes),
mode: Object.freeze(UI_STYLE_KIT_MANIFEST.modes),
@@ -205,7 +214,7 @@ const DEFAULT_STATE = Object.freeze({
container: "auto",
height: "auto",
responsive: "auto",
- density: "comfortable",
+ density: "normal",
ui: "minimal-saas",
theme: "arctic-indigo",
mode: "light",
@@ -248,12 +257,6 @@ const DEVICE_PRESETS = Object.freeze({
"desktop-portrait": Object.freeze({ width: 900, height: 1440 })
});
-const DENSITY_GAPS = Object.freeze({
- compact: "0.5rem",
- comfortable: "1rem",
- spacious: "1.5rem"
-});
-
const UI_CLASS_PREFIXES = Object.freeze(
Object.fromEntries(UI_STYLE_KIT_MANIFEST.presets.map((preset) => [preset.id, preset.prefix]))
);
@@ -326,12 +329,28 @@ let querySyncTimer = null;
let hasSynchronizedQuery = false;
let readoutFrame = null;
+/**
+ * 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;
+}
+
+/**
+ * Reads allowlisted demo state from the current query string.
+ *
+ * @returns {Record} Normalized demo state.
+ */
function readStateFromQuery() {
const query = new URLSearchParams(window.location.search);
return Object.fromEntries(
Object.entries(ALLOWLISTS).map(([key, values]) => {
- const candidate = query.get(key);
+ const candidate = normalizeQueryValue(key, query.get(key));
return [key, candidate !== null && values.includes(candidate) ? candidate : DEFAULT_STATE[key]];
})
);
@@ -392,6 +411,12 @@ function createAction(sequence) {
});
}
+/**
+ * Creates one semantic recipe region and its representative preview content.
+ *
+ * @param {string} area Canonical recipe area name.
+ * @returns {HTMLElement} Populated preview region.
+ */
function createRegion(area) {
const semanticTags = {
header: "header",
@@ -420,7 +445,7 @@ function createRegion(area) {
if (area === "secondary") {
const scroll = createElement("div", {
- className: "ly-scroll demo-list-scroll",
+ className: "ly-scroll ly-scroll--bounded demo-list-scroll",
attributes: { tabindex: "0", "aria-label": "Scrollable detail activity" }
});
@@ -657,7 +682,15 @@ function updatePreviewReadout() {
});
}
-function applyState({ updateQuery = true } = {}) {
+/**
+ * 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}
+ */
+function applyState(options = {}) {
+ const { updateQuery = true } = options;
for (const [key, control] of Object.entries(controls)) {
control.value = state[key];
}
@@ -666,11 +699,10 @@ function applyState({ updateQuery = true } = {}) {
body.dataset.theme = state.theme;
body.dataset.mode = state.mode;
body.dataset.lyLayout = state.personality;
- body.dataset.density = state.density;
+ body.dataset.demoDensity = state.density;
body.dataset.ecosystem = state.ecosystem;
previewRoot.dataset.lyLayout = state.personality;
- previewRoot.style.setProperty("--ly-gap", DENSITY_GAPS[state.density]);
- previewRoot.style.setProperty("--ly-grid-gap", DENSITY_GAPS[state.density]);
+ previewRoot.dataset.lyDensity = state.density;
previewWrapper.className =
state.wrapper === "default" ? "ly-wrapper" : `ly-wrapper ly-wrapper--${state.wrapper}`;
diff --git a/demo/index.html b/demo/index.html
index a7d3ffc..9518cab 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -6,7 +6,7 @@
-
+
@@ -71,15 +71,15 @@
-
-
+
+
-
-
+
+
-
+
Skip to the layout lab
@@ -143,6 +143,7 @@ Layout controls
Compact · 40rem
Prose · 68ch
Content · 72rem
+ Workspace · 96rem
Wide · 112rem
Full width
Breakout lanes
@@ -208,7 +209,7 @@ Layout controls
Density
Compact
- Comfortable
+ Normal
Spacious
@@ -292,7 +293,7 @@ Responsive recipe stage