kmcom-nuxt-layers 1.1.9 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/LAYOUT.md ADDED
@@ -0,0 +1,517 @@
1
+ # Layout Layer
2
+
3
+ Swiss Grid System for Nuxt 4 applications. Provides a responsive 6/12/18-column CSS subgrid, page structure components, a mode system, and fluid layout utilities.
4
+
5
+ ---
6
+
7
+ ## Contents
8
+
9
+ - [How it works](#how-it-works)
10
+ - [Playground setup](#playground-setup)
11
+ - [Mode system](#mode-system)
12
+ - [Components](#components)
13
+ - [Composables](#composables)
14
+ - [CSS utilities](#css-utilities)
15
+ - [Z-index system](#z-index-system)
16
+ - [Config reference](#config-reference)
17
+ - [Known pitfalls](#known-pitfalls)
18
+
19
+ ---
20
+
21
+ ## How it works
22
+
23
+ The grid is a single CSS `display: grid` on `<main>` (the `.mastmain` class). Everything else — sections, items — participates via CSS `subgrid`, meaning they inherit the parent's column and row lines rather than defining their own. No JS is involved in the grid itself.
24
+
25
+ ```
26
+ <main class="mastmain"> ← defines the grid (6/12/18 cols, row height)
27
+ <section class="basesection"> ← subgrid: inherits cols + rows, spans 12 rows (= 100vh)
28
+ <div style="grid-column: ..."> ← positioned within the subgrid
29
+ ```
30
+
31
+ `LayoutMain` renders the `<main class="mastmain">`. `LayoutSection` renders `<section class="basesection">`. `LayoutGridItem` handles grid positioning via inline style or presets.
32
+
33
+ ---
34
+
35
+ ## Playground setup
36
+
37
+ The layout layer requires a dedicated Nuxt layout file and page-level layout declaration. Without these, content appears edge-to-edge and the grid CSS never applies.
38
+
39
+ **1. Create `layouts/grid.vue`** in your app:
40
+
41
+ ```vue
42
+ <template>
43
+ <LayoutMain>
44
+ <slot />
45
+ <LayoutGridDebug />
46
+ </LayoutMain>
47
+ </template>
48
+ ```
49
+
50
+ **2. Declare the layout in each page that uses the grid:**
51
+
52
+ ```vue
53
+ <script setup lang="ts">
54
+ definePageMeta({ layout: 'grid' })
55
+ </script>
56
+ ```
57
+
58
+ > `LayoutPage` is a fragment component — it intentionally provides no wrapper element. The `mastmain` grid lives on `LayoutMain` in the layout file, not in `LayoutPage` itself. Do not add a second `LayoutMain` inside `LayoutPage`.
59
+
60
+ **Grid debug overlay** (`Cmd+G` to toggle) is rendered by `LayoutGridDebug` inside the layout file, not per-page.
61
+
62
+ ---
63
+
64
+ ## Mode system
65
+
66
+ The grid has three modes controlled via `app.config.ts`:
67
+
68
+ | Mode | Behaviour |
69
+ |---|---|
70
+ | `'swiss'` | Default. Full Swiss grid with `mastmain`/`basesection` CSS subgrid. |
71
+ | `'fluid'` | Container-query-based auto-fit grid. `basesection` still gets `container-type`. |
72
+ | `'disabled'` | `LayoutMain` renders a plain `<main>` without grid CSS. |
73
+
74
+ Set in your `app.config.ts`:
75
+
76
+ ```ts
77
+ export default defineAppConfig({
78
+ layoutLayer: {
79
+ ui: {
80
+ grid: {
81
+ mode: 'swiss', // 'swiss' | 'fluid' | 'disabled'
82
+ },
83
+ },
84
+ },
85
+ })
86
+ ```
87
+
88
+ The old `enabled: boolean` flag is still supported (mapped to `'disabled'` / `'swiss'`) but `mode` takes precedence.
89
+
90
+ Read in code:
91
+
92
+ ```ts
93
+ const { mode, isEnabled } = useGridConfig()
94
+ // mode.value === 'swiss' | 'fluid' | 'disabled'
95
+ // isEnabled.value === mode !== 'disabled'
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Components
101
+
102
+ ### `LayoutMain`
103
+
104
+ Renders `<main class="mastmain">`. When `mode === 'disabled'` it renders a plain `<main>` with no grid classes.
105
+
106
+ ```vue
107
+ <LayoutMain> <!-- default: <main> -->
108
+ <LayoutMain tag="div"> <!-- custom tag -->
109
+ ```
110
+
111
+ **Props:**
112
+
113
+ | Prop | Type | Default |
114
+ |---|---|---|
115
+ | `tag` | `string` | `'main'` |
116
+
117
+ Place this in your layout file (`layouts/grid.vue`), not in individual pages.
118
+
119
+ ---
120
+
121
+ ### `LayoutContainer`
122
+
123
+ Constrains content width and centres it. Use inside sections or anywhere you need a contained block.
124
+
125
+ ```vue
126
+ <LayoutContainer> <!-- default: wide (90rem) -->
127
+ <LayoutContainer size="content"> <!-- 65ch prose width -->
128
+ <LayoutContainer size="wide"> <!-- 90rem -->
129
+ <LayoutContainer size="fluid"> <!-- 100% of parent -->
130
+ <LayoutContainer size="full"> <!-- 100vw, escapes grid padding -->
131
+ ```
132
+
133
+ **Props:**
134
+
135
+ | Prop | Type | Default |
136
+ |---|---|---|
137
+ | `size` | `'content' \| 'wide' \| 'fluid' \| 'full'` | `'wide'` |
138
+ | `tag` | `string` | `'div'` |
139
+
140
+ The `full` size uses negative `margin-inline` to escape the grid's `--grid-padding`, reaching the true viewport edge.
141
+
142
+ ---
143
+
144
+ ### `LayoutPage`
145
+
146
+ Fragment component (no wrapper element). Handles `useHead()` SEO and optionally renders a `LayoutPageHeader` section.
147
+
148
+ ```vue
149
+ <template>
150
+ <LayoutPage title="About" description="About us">
151
+ <LayoutSection>
152
+ <LayoutGridItem preset="centered">
153
+ <!-- content -->
154
+ </LayoutGridItem>
155
+ </LayoutSection>
156
+ </LayoutPage>
157
+ </template>
158
+ ```
159
+
160
+ **Props:**
161
+
162
+ | Prop | Type | Default |
163
+ |---|---|---|
164
+ | `title` | `string` | — (required) |
165
+ | `description` | `string` | `''` |
166
+ | `showHeader` | `boolean` | `false` |
167
+
168
+ ---
169
+
170
+ ### `LayoutSection`
171
+
172
+ Full-viewport subgrid section. Spans 12 rows (= 100vh). Inherits the parent `.mastmain` grid columns via `subgrid`.
173
+
174
+ ```vue
175
+ <LayoutSection>
176
+ <LayoutSection full-height> <!-- always 100vh, even on mobile -->
177
+ <LayoutSection tag="article">
178
+ ```
179
+
180
+ **Props:**
181
+
182
+ | Prop | Type | Default |
183
+ |---|---|---|
184
+ | `fullHeight` | `boolean` | `false` |
185
+ | `fullWidth` | `boolean` | `false` |
186
+ | `tag` | `string` | `'section'` |
187
+
188
+ ---
189
+
190
+ ### `LayoutGridItem`
191
+
192
+ Positioned child within a subgrid section. Use `preset` for common layouts or set column/row values directly.
193
+
194
+ ```vue
195
+ <LayoutGridItem preset="centered" />
196
+ <LayoutGridItem col-start="3" col-span="12" row-start="2" row-span="8" />
197
+ <!-- Responsive values -->
198
+ <LayoutGridItem :col-span="{ default: 6, md: 10, lg: 12 }" />
199
+ ```
200
+
201
+ **Props:**
202
+
203
+ | Prop | Type | Notes |
204
+ |---|---|---|
205
+ | `preset` | `string` | Named preset from config |
206
+ | `colStart` | `number \| ResponsiveValue` | Grid column start |
207
+ | `colSpan` | `number \| ResponsiveValue` | Column span |
208
+ | `rowStart` | `number \| ResponsiveValue` | Grid row start |
209
+ | `rowSpan` | `number \| ResponsiveValue` | Row span |
210
+ | `layer` | `keyof GridLayers` | Z-index layer: `back`, `mid`, `front`, `top` |
211
+ | `align` | `string` | `align-self` value |
212
+ | `justify` | `string` | `justify-self` value |
213
+ | `bleed` | `boolean` | Negative margin to reach viewport edge |
214
+ | `as` | `string` | Tag override |
215
+
216
+ **Built-in presets:**
217
+
218
+ | Preset | Columns | Rows |
219
+ |---|---|---|
220
+ | `hero` | full width | full height |
221
+ | `centered` | centre 10 cols | rows 2–10 |
222
+ | `fullWidth` | full width | auto |
223
+ | `sidebar` | cols 1–4 | full height |
224
+ | `content` | cols 5–14 | rows 2–10 |
225
+ | `splitLeft` | left half | full |
226
+ | `splitRight` | right half | full |
227
+ | `quarterLeft` | first quarter | full |
228
+ | `threeQuarterRight` | right 3/4 | full |
229
+ | `halfTop` | full width | top half |
230
+ | `halfBottom` | full width | bottom half |
231
+
232
+ ---
233
+
234
+ ### `LayoutSectionHero`
235
+
236
+ Section with `background`, `default`, and `footer` slot layers.
237
+
238
+ ```vue
239
+ <LayoutSectionHero>
240
+ <template #background><img ... /></template>
241
+ <h1>Title</h1>
242
+ <template #footer><p>Subtitle</p></template>
243
+ </LayoutSectionHero>
244
+ ```
245
+
246
+ ---
247
+
248
+ ### `LayoutSectionSplit`
249
+
250
+ Two-column 50/50 layout.
251
+
252
+ ```vue
253
+ <LayoutSectionSplit>
254
+ <template #left>Left content</template>
255
+ <template #right>Right content</template>
256
+ </LayoutSectionSplit>
257
+
258
+ <LayoutSectionSplit reverse /> <!-- right renders first visually -->
259
+ ```
260
+
261
+ ---
262
+
263
+ ### `LayoutSectionGallery`
264
+
265
+ Auto-placing collection grid.
266
+
267
+ ```vue
268
+ <LayoutSectionGallery :items="items" :columns="3" :item-row-span="4">
269
+ <template #item="{ item, index }">
270
+ <img :src="item.src" />
271
+ </template>
272
+ </LayoutSectionGallery>
273
+ ```
274
+
275
+ **Props:** `items` (required), `columns` (2/3/4/6, default 3), `itemRowSpan` (default 4)
276
+
277
+ ---
278
+
279
+ ### `LayoutGridDebug`
280
+
281
+ Column overlay toggled with `Cmd+G`. Place once in your grid layout file, not in pages.
282
+
283
+ ---
284
+
285
+ ## Composables
286
+
287
+ ### `useGridConfig()`
288
+
289
+ ```ts
290
+ const { config, getPreset, isEnabled, mode, layers, useZIndex } = useGridConfig()
291
+ ```
292
+
293
+ | Return | Type | Description |
294
+ |---|---|---|
295
+ | `config` | `Ref<GridConfig>` | Raw config from `app.config` |
296
+ | `isEnabled` | `ComputedRef<boolean>` | `true` when `mode !== 'disabled'` |
297
+ | `mode` | `ComputedRef<GridMode>` | `'swiss' \| 'fluid' \| 'disabled'` |
298
+ | `layers` | `ComputedRef<GridLayers>` | All z-index values |
299
+ | `getPreset(name)` | `GridPresetsItem \| undefined` | Look up a preset by name |
300
+ | `useZIndex(layer)` | `number` | Get a z-index value by layer name |
301
+
302
+ **`useZIndex` example:**
303
+
304
+ ```ts
305
+ const { useZIndex } = useGridConfig()
306
+ const zModal = useZIndex('modal') // 400
307
+ const zHeader = useZIndex('header') // 100
308
+ ```
309
+
310
+ ---
311
+
312
+ ## CSS utilities
313
+
314
+ All grid CSS is in `layers/layout/app/assets/css/layout/`.
315
+
316
+ > **Important:** All classes are plain CSS (not `@utility` directives). Do not convert them to `@utility` — they are loaded via Nuxt's `css: []` array which bypasses the Tailwind CSS 4 pipeline, so `@utility` would produce no output.
317
+
318
+ ### `grids.css` — Core grid classes
319
+
320
+ **`.mastmain`** — Root grid container on `<main>`.
321
+
322
+ ```css
323
+ /* Responsive column counts */
324
+ /* Mobile: 6 columns (< 768px) */
325
+ /* Tablet: 12 columns (≥ 768px) */
326
+ /* Desktop: 18 columns (≥ 1280px) */
327
+ ```
328
+
329
+ CSS custom properties exposed:
330
+
331
+ | Variable | Value | Purpose |
332
+ |---|---|---|
333
+ | `--grid-cols` | 6 / 12 / 18 | Active column count |
334
+ | `--grid-gap` | `clamp(0.75rem, 1.5vw, 1.5rem)` | Gap between columns and rows |
335
+ | `--grid-padding` | `clamp(1rem, 2.5vw, 2rem)` | Outer left/right gutters |
336
+ | `--section-height` | `100vh` | Height of one section |
337
+ | `--grid-row-height` | computed | Height of one grid row |
338
+
339
+ **`.basesection`** — Subgrid section (12 rows = 100vh).
340
+
341
+ **Vertical rhythm utilities** (`.leading-rhythm-*`, `.space-rhythm-*`, `.prose-rhythm`)
342
+
343
+ ### `modes/fluid.css` — Fluid grid classes
344
+
345
+ Container-query-based auto-fit grids for components that should respond to their container width rather than the viewport.
346
+
347
+ ```vue
348
+ <div class="fluid-grid"> <!-- auto-fit, min col 16rem -->
349
+ <div class="fluid-grid-2"> <!-- 2-column at container ≥ 30rem -->
350
+ <div class="fluid-grid-3"> <!-- 3-column at container ≥ 44rem -->
351
+ <div class="fluid-grid-4"> <!-- 4-column at container ≥ 52rem -->
352
+ ```
353
+
354
+ Tune minimum column width per-instance:
355
+
356
+ ```vue
357
+ <div class="fluid-grid" style="--fluid-col-min: 20rem">
358
+ ```
359
+
360
+ **Container size classes** (used by `LayoutContainer`):
361
+
362
+ | Class | Effect |
363
+ |---|---|
364
+ | `.layout-container-content` | `max-width: 65ch`, centred |
365
+ | `.layout-container-wide` | `max-width: 90rem`, centred |
366
+ | `.layout-container-fluid` | `max-width: 100%` |
367
+ | `.layout-container-full` | `width: 100vw`, escapes grid padding |
368
+
369
+ ---
370
+
371
+ ## Z-index system
372
+
373
+ The grid manages z-index through named layers. Use `useZIndex()` or the `layer` prop on `LayoutGridItem` — never raw numbers.
374
+
375
+ | Layer | Value | Use |
376
+ |---|---|---|
377
+ | `back` | 0 | Background elements |
378
+ | `mid` | 10 | Content |
379
+ | `front` | 20 | Floating content |
380
+ | `top` | 30 | Pinned / sticky |
381
+ | `header` | 100 | Site header / nav |
382
+ | `dropdown` | 200 | Dropdown menus, popups |
383
+ | `overlay` | 300 | Drawers, sidebars |
384
+ | `modal` | 400 | Modal dialogs |
385
+ | `toast` | 500 | Toasts, notifications |
386
+
387
+ ```ts
388
+ // In a component
389
+ const { useZIndex } = useGridConfig()
390
+ </script>
391
+ <template>
392
+ <div :style="{ zIndex: useZIndex('modal') }">
393
+ ```
394
+
395
+ ```vue
396
+ <!-- On a grid item -->
397
+ <LayoutGridItem layer="front">
398
+ ```
399
+
400
+ Override values in `app.config.ts`:
401
+
402
+ ```ts
403
+ layoutLayer: {
404
+ ui: {
405
+ grid: {
406
+ layers: {
407
+ toast: 9999, // override individual values
408
+ },
409
+ },
410
+ },
411
+ },
412
+ ```
413
+
414
+ ---
415
+
416
+ ## Config reference
417
+
418
+ Full type from `layers/layout/app/types/layouts.ts`:
419
+
420
+ ```ts
421
+ interface GridConfig {
422
+ mode?: 'swiss' | 'fluid' | 'disabled'
423
+ /** @deprecated use mode: 'disabled' */
424
+ enabled?: boolean
425
+ layers?: Partial<GridLayers>
426
+ presets?: Record<string, GridPresetsItem>
427
+ }
428
+
429
+ interface GridPresetsItem {
430
+ colStart?: number | ResponsiveValue
431
+ colSpan?: number | ResponsiveValue
432
+ rowStart?: number | ResponsiveValue
433
+ rowSpan?: number | ResponsiveValue
434
+ container?: 'content' | 'wide' | 'fluid' | 'full'
435
+ gap?: string
436
+ density?: 'compact' | 'normal' | 'spacious'
437
+ }
438
+ ```
439
+
440
+ App config namespace: `layoutLayer.ui.grid`.
441
+
442
+ **Custom preset example:**
443
+
444
+ ```ts
445
+ export default defineAppConfig({
446
+ layoutLayer: {
447
+ ui: {
448
+ grid: {
449
+ presets: {
450
+ spotlight: {
451
+ colStart: 4,
452
+ colSpan: 12,
453
+ rowStart: 3,
454
+ rowSpan: 6,
455
+ },
456
+ },
457
+ },
458
+ },
459
+ },
460
+ })
461
+ ```
462
+
463
+ ---
464
+
465
+ ## Known pitfalls
466
+
467
+ ### `@utility` doesn't work in layer CSS files
468
+
469
+ `@utility` is a Tailwind CSS 4 directive that only works inside files processed by TW4's own pipeline (files containing `@import "tailwindcss"`). CSS loaded via Nuxt's `css: []` array bypasses this pipeline — `@utility` blocks are silently ignored and the class is never generated.
470
+
471
+ **Rule: always use plain CSS class selectors in layer CSS files.**
472
+
473
+ ```css
474
+ /* Wrong — class will not exist in output */
475
+ @utility mastmain {
476
+ display: grid;
477
+ }
478
+
479
+ /* Correct */
480
+ .mastmain {
481
+ display: grid;
482
+ }
483
+ ```
484
+
485
+ ### `overflow-x: hidden` creates unintended scroll containers
486
+
487
+ The CSS overflow interaction rule: setting `overflow-x: hidden` on any element forces `overflow-y` from `visible` to `auto`, turning that element into a scroll container. When the element is `html` or `body`, `window.scrollY` stays 0 even when content overflows the viewport.
488
+
489
+ **Use `overflow-x: clip` instead.** `clip` cuts off overflow without creating a scroll container and is not subject to the forced `overflow-y` conversion. This is why `.mastmain`, `html`, and `body` all use `overflow-x: clip` in this layer.
490
+
491
+ ```css
492
+ /* Wrong */
493
+ .mastmain { overflow-x: hidden; } /* becomes a scroll container */
494
+
495
+ /* Correct */
496
+ .mastmain { overflow-x: clip; } /* clips without scroll container */
497
+ ```
498
+
499
+ ### Locomotive Scroll intercepts wheel events globally
500
+
501
+ Locomotive Scroll v5 (Lenis-based) with `smoothWheel: true` calls `preventDefault()` on every `wheel` event. If initialised globally in a Nuxt plugin, it intercepts scrolling on every page — even pages where the window has no scroll height, silently swallowing events.
502
+
503
+ **Scope Locomotive Scroll to the specific route** using `addRouteMiddleware`:
504
+
505
+ ```ts
506
+ // plugins/locomotive-scroll.client.ts
507
+ addRouteMiddleware((to, from) => {
508
+ if (to.path === '/the-route') nextTick(init)
509
+ else if (from?.path === '/the-route') destroy()
510
+ })
511
+ ```
512
+
513
+ ### `LayoutPage` is a fragment — it provides no grid wrapper
514
+
515
+ `LayoutPage` renders no element of its own. The `mastmain` grid lives on `LayoutMain` inside a layout file. If you use `<LayoutPage>` on a page that uses the `default` layout (which doesn't include `LayoutMain`), the grid CSS never applies.
516
+
517
+ Always pair `LayoutPage` usage with `definePageMeta({ layout: 'grid' })` and ensure `layouts/grid.vue` exists with `<LayoutMain>`.
@@ -96,8 +96,8 @@ function guard() {
96
96
  const html = document.documentElement
97
97
  const body = document.body
98
98
 
99
- html.style.overflowX = 'hidden'
100
- body.style.overflowX = 'hidden'
99
+ html.style.overflowX = 'clip'
100
+ body.style.overflowX = 'clip'
101
101
  body.style.maxWidth = '100vw'
102
102
 
103
103
  if (!opts.strict) return
@@ -5,13 +5,13 @@
5
5
  * Uses CSS subgrid for precise alignment across layout hierarchy.
6
6
  *
7
7
  * Hierarchy:
8
- * MastMain (mastmain utility)
9
- * └── BaseSection (basesection utility, uses subgrid)
8
+ * MastMain (.mastmain)
9
+ * └── BaseSection (.basesection, uses subgrid)
10
10
  * └── BaseGridItem (positioned via inline grid-column/grid-row)
11
11
  */
12
12
 
13
13
  /**
14
- * mastmain - Root grid container with responsive density
14
+ * .mastmain - Root grid container with responsive density
15
15
  *
16
16
  * Creates responsive grid:
17
17
  * - Mobile: 6 columns (default)
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * Each row height is calculated to fit 12 rows per viewport.
22
22
  */
23
- @utility mastmain {
23
+ .mastmain {
24
24
  --grid-cols: 6;
25
25
  --grid-rows-per-section: 12;
26
26
  --grid-gap: clamp(0.75rem, 1.5vw, 1.5rem);
@@ -36,7 +36,7 @@
36
36
  grid-auto-rows: var(--grid-row-height);
37
37
  gap: var(--grid-gap);
38
38
  padding-inline: var(--grid-padding); /* Outer gutters on left/right */
39
- overflow-x: hidden;
39
+ overflow-x: clip; /* clip without creating a scroll container (overflow-x:hidden would force overflow-y:auto) */
40
40
  width: 100%;
41
41
  max-width: 100%;
42
42
  box-sizing: border-box;
@@ -58,12 +58,12 @@
58
58
  }
59
59
 
60
60
  /**
61
- * basesection - Full-viewport section using subgrid
61
+ * .basesection - Full-viewport section using subgrid
62
62
  *
63
63
  * Inherits parent's grid lines via subgrid.
64
64
  * Spans 12 rows (1 viewport height).
65
65
  */
66
- @utility basesection {
66
+ .basesection {
67
67
  container-type: inline-size;
68
68
  container-name: section;
69
69
  grid-column: 1 / -1;
@@ -100,59 +100,59 @@
100
100
  */
101
101
 
102
102
  /* Line height utilities */
103
- @utility leading-rhythm-4 {
103
+ .leading-rhythm-4 {
104
104
  --rhythm: 0.25rem;
105
105
  line-height: calc(var(--rhythm) * 4); /* 1rem */
106
106
  }
107
107
 
108
- @utility leading-rhythm-5 {
108
+ .leading-rhythm-5 {
109
109
  --rhythm: 0.25rem;
110
110
  line-height: calc(var(--rhythm) * 5); /* 1.25rem */
111
111
  }
112
112
 
113
- @utility leading-rhythm-6 {
113
+ .leading-rhythm-6 {
114
114
  --rhythm: 0.25rem;
115
115
  line-height: calc(var(--rhythm) * 6); /* 1.5rem */
116
116
  }
117
117
 
118
- @utility leading-rhythm-7 {
118
+ .leading-rhythm-7 {
119
119
  --rhythm: 0.25rem;
120
120
  line-height: calc(var(--rhythm) * 7); /* 1.75rem */
121
121
  }
122
122
 
123
- @utility leading-rhythm-8 {
123
+ .leading-rhythm-8 {
124
124
  --rhythm: 0.25rem;
125
125
  line-height: calc(var(--rhythm) * 8); /* 2rem */
126
126
  }
127
127
 
128
128
  /* Vertical spacing utilities */
129
- @utility space-rhythm-1 {
129
+ .space-rhythm-1 {
130
130
  --rhythm: 0.25rem;
131
131
  margin-block: var(--rhythm); /* 0.25rem */
132
132
  }
133
133
 
134
- @utility space-rhythm-2 {
134
+ .space-rhythm-2 {
135
135
  --rhythm: 0.25rem;
136
136
  margin-block: calc(var(--rhythm) * 2); /* 0.5rem */
137
137
  }
138
138
 
139
- @utility space-rhythm-4 {
139
+ .space-rhythm-4 {
140
140
  --rhythm: 0.25rem;
141
141
  margin-block: calc(var(--rhythm) * 4); /* 1rem */
142
142
  }
143
143
 
144
- @utility space-rhythm-6 {
144
+ .space-rhythm-6 {
145
145
  --rhythm: 0.25rem;
146
146
  margin-block: calc(var(--rhythm) * 6); /* 1.5rem */
147
147
  }
148
148
 
149
- @utility space-rhythm-8 {
149
+ .space-rhythm-8 {
150
150
  --rhythm: 0.25rem;
151
151
  margin-block: calc(var(--rhythm) * 8); /* 2rem */
152
152
  }
153
153
 
154
154
  /* Prose container - auto-applies rhythm to children */
155
- @utility prose-rhythm {
155
+ .prose-rhythm {
156
156
  --rhythm: 0.25rem;
157
157
 
158
158
  & > * + * {