@refrakt-md/lumina 0.19.0 → 0.20.1

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/base.css CHANGED
@@ -33,5 +33,9 @@
33
33
  @import './styles/dimensions/state.css';
34
34
  @import './styles/dimensions/media.css';
35
35
  @import './styles/dimensions/surfaces.css';
36
+ @import './styles/dimensions/frame.css';
37
+ @import './styles/dimensions/substrate.css';
38
+ @import './styles/dimensions/cover.css';
39
+ @import './styles/dimensions/guest-posture.css';
36
40
  @import './styles/dimensions/checklist.css';
37
41
  @import './styles/dimensions/sequence.css';
@@ -288,6 +288,18 @@
288
288
  "collapse": {
289
289
  "source": "meta",
290
290
  "dataAttribute": "data-collapse"
291
+ },
292
+ "content-place": {
293
+ "source": "meta",
294
+ "dataAttribute": "data-content-place"
295
+ },
296
+ "height": {
297
+ "source": "meta",
298
+ "dataAttribute": "data-height"
299
+ },
300
+ "aspect": {
301
+ "source": "meta",
302
+ "dataAttribute": "data-aspect"
291
303
  }
292
304
  },
293
305
  "elements": {
@@ -305,6 +317,76 @@
305
317
  "inlineStyles": {
306
318
  "valign": {
307
319
  "prop": "--split-valign"
320
+ },
321
+ "aspect": "aspect-ratio"
322
+ },
323
+ "variants": {
324
+ "media-position": {
325
+ "cover": {
326
+ "block": "card",
327
+ "root": ".rf-card",
328
+ "dataRune": "card",
329
+ "childOrder": [
330
+ "media",
331
+ "content",
332
+ "{content}"
333
+ ],
334
+ "modifiers": {
335
+ "media-position": {
336
+ "source": "meta",
337
+ "default": "top",
338
+ "dataAttribute": "data-media-position"
339
+ },
340
+ "media-ratio": {
341
+ "source": "meta",
342
+ "dataAttribute": "data-media-ratio"
343
+ },
344
+ "valign": {
345
+ "source": "meta",
346
+ "dataAttribute": "data-valign"
347
+ },
348
+ "collapse": {
349
+ "source": "meta",
350
+ "dataAttribute": "data-collapse"
351
+ },
352
+ "content-place": {
353
+ "source": "meta",
354
+ "dataAttribute": "data-content-place"
355
+ },
356
+ "height": {
357
+ "source": "meta",
358
+ "dataAttribute": "data-height"
359
+ },
360
+ "aspect": {
361
+ "source": "meta",
362
+ "dataAttribute": "data-aspect"
363
+ }
364
+ },
365
+ "staticModifiers": [
366
+ {
367
+ "name": "cover",
368
+ "selector": ".rf-card--cover"
369
+ }
370
+ ],
371
+ "elements": {
372
+ "content": {
373
+ "tag": "div",
374
+ "selector": ".rf-card__content",
375
+ "source": "layout",
376
+ "children": [
377
+ "eyebrow",
378
+ "body",
379
+ "footer"
380
+ ]
381
+ }
382
+ },
383
+ "inlineStyles": {
384
+ "valign": {
385
+ "prop": "--split-valign"
386
+ },
387
+ "aspect": "aspect-ratio"
388
+ }
389
+ }
308
390
  }
309
391
  }
310
392
  },
@@ -876,44 +958,12 @@
876
958
  "childOrder": [
877
959
  "{content}"
878
960
  ],
879
- "modifiers": {
880
- "shadow": {
881
- "source": "meta",
882
- "default": "none",
883
- "classPattern": ".rf-showcase--{value}",
884
- "dataAttribute": "data-shadow"
885
- },
886
- "bleed": {
887
- "source": "meta",
888
- "default": "none",
889
- "classPattern": ".rf-showcase--{value}",
890
- "dataAttribute": "data-bleed"
891
- },
892
- "aspect": {
893
- "source": "meta",
894
- "dataAttribute": "data-aspect"
895
- },
896
- "offset": {
897
- "source": "meta",
898
- "dataAttribute": "data-offset"
899
- },
900
- "place": {
901
- "source": "meta",
902
- "dataAttribute": "data-place"
903
- }
904
- },
905
961
  "contextModifiers": {
906
962
  "bento-cell": {
907
963
  "suffix": "in-bento-cell",
908
964
  "selector": ".rf-showcase--in-bento-cell"
909
965
  }
910
966
  },
911
- "inlineStyles": {
912
- "offset": {
913
- "prop": "--showcase-offset"
914
- },
915
- "aspect": "--showcase-aspect"
916
- },
917
967
  "childDensity": "compact"
918
968
  },
919
969
  "TabGroup": {
@@ -3266,6 +3316,10 @@
3266
3316
  "collapse": {
3267
3317
  "source": "meta",
3268
3318
  "dataAttribute": "data-collapse"
3319
+ },
3320
+ "content-place": {
3321
+ "source": "meta",
3322
+ "dataAttribute": "data-content-place"
3269
3323
  }
3270
3324
  },
3271
3325
  "elements": {
@@ -3333,6 +3387,144 @@
3333
3387
  "valign": {
3334
3388
  "prop": "--split-valign"
3335
3389
  }
3390
+ },
3391
+ "variants": {
3392
+ "media-position": {
3393
+ "cover": {
3394
+ "block": "recipe",
3395
+ "root": ".rf-recipe",
3396
+ "dataRune": "recipe",
3397
+ "childOrder": [
3398
+ "cover-band",
3399
+ "content",
3400
+ "{content}"
3401
+ ],
3402
+ "modifiers": {
3403
+ "media-position": {
3404
+ "source": "meta",
3405
+ "default": "top",
3406
+ "dataAttribute": "data-media-position"
3407
+ },
3408
+ "prepTime": {
3409
+ "source": "meta",
3410
+ "dataAttribute": "data-prep-time"
3411
+ },
3412
+ "cookTime": {
3413
+ "source": "meta",
3414
+ "dataAttribute": "data-cook-time"
3415
+ },
3416
+ "servings": {
3417
+ "source": "meta",
3418
+ "dataAttribute": "data-servings"
3419
+ },
3420
+ "difficulty": {
3421
+ "source": "meta",
3422
+ "default": "medium",
3423
+ "classPattern": ".rf-recipe--{value}",
3424
+ "dataAttribute": "data-difficulty"
3425
+ },
3426
+ "media-ratio": {
3427
+ "source": "meta",
3428
+ "dataAttribute": "data-media-ratio"
3429
+ },
3430
+ "valign": {
3431
+ "source": "meta",
3432
+ "dataAttribute": "data-valign"
3433
+ },
3434
+ "collapse": {
3435
+ "source": "meta",
3436
+ "dataAttribute": "data-collapse"
3437
+ },
3438
+ "content-place": {
3439
+ "source": "meta",
3440
+ "dataAttribute": "data-content-place"
3441
+ }
3442
+ },
3443
+ "staticModifiers": [
3444
+ {
3445
+ "name": "cover",
3446
+ "selector": ".rf-recipe--cover"
3447
+ }
3448
+ ],
3449
+ "elements": {
3450
+ "metadata": {
3451
+ "tag": "dl",
3452
+ "selector": ".rf-recipe__metadata",
3453
+ "source": "block",
3454
+ "layout": "definition-list",
3455
+ "fields": [
3456
+ "prepTime",
3457
+ "cookTime",
3458
+ "servings",
3459
+ "difficulty"
3460
+ ]
3461
+ },
3462
+ "preamble": {
3463
+ "tag": "header",
3464
+ "selector": ".rf-recipe__preamble",
3465
+ "source": "layout",
3466
+ "children": [
3467
+ "eyebrow",
3468
+ "headline",
3469
+ "blurb"
3470
+ ]
3471
+ },
3472
+ "eyebrow": {
3473
+ "tag": "eyebrow",
3474
+ "selector": ".rf-recipe__eyebrow",
3475
+ "source": "autoLabel"
3476
+ },
3477
+ "headline": {
3478
+ "tag": "headline",
3479
+ "selector": ".rf-recipe__headline",
3480
+ "source": "autoLabel"
3481
+ },
3482
+ "blurb": {
3483
+ "tag": "blurb",
3484
+ "selector": ".rf-recipe__blurb",
3485
+ "source": "autoLabel"
3486
+ },
3487
+ "image": {
3488
+ "tag": "image",
3489
+ "selector": ".rf-recipe__image",
3490
+ "source": "autoLabel"
3491
+ },
3492
+ "media": {
3493
+ "tag": "media",
3494
+ "selector": ".rf-recipe__media",
3495
+ "source": "autoLabel"
3496
+ },
3497
+ "content": {
3498
+ "tag": "div",
3499
+ "selector": ".rf-recipe__content",
3500
+ "source": "layout",
3501
+ "children": [
3502
+ "metadata",
3503
+ "ingredients",
3504
+ "steps",
3505
+ "tips"
3506
+ ]
3507
+ },
3508
+ "cover-band": {
3509
+ "tag": "div",
3510
+ "selector": ".rf-recipe__cover-band",
3511
+ "source": "layout",
3512
+ "children": [
3513
+ "media",
3514
+ "preamble"
3515
+ ],
3516
+ "attrs": {
3517
+ "data-color-scheme": "dark"
3518
+ }
3519
+ }
3520
+ },
3521
+ "inlineStyles": {
3522
+ "valign": {
3523
+ "prop": "--split-valign"
3524
+ }
3525
+ }
3526
+ }
3527
+ }
3336
3528
  }
3337
3529
  },
3338
3530
  "RecipeIngredient": {
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY,EAAE,iBAmL1B,CAAC"}
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY,EAAE,iBAyL1B,CAAC"}
package/dist/tokens.js CHANGED
@@ -93,6 +93,7 @@ export const luminaTokens = {
93
93
  breathe: '8rem',
94
94
  },
95
95
  shadow: {
96
+ none: 'none',
96
97
  xs: '0 1px 2px rgba(0,0,0,0.04)',
97
98
  sm: '0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)',
98
99
  md: '0 4px 12px rgba(0,0,0,0.07), 0 1px 3px rgba(0,0,0,0.04)',
@@ -160,6 +161,8 @@ export const luminaTokens = {
160
161
  // above by the generator.
161
162
  extra: {
162
163
  'rf-color-inline-code-bg': '#2b2b29',
164
+ // SPEC-087 — inset surfaces dip a touch deeper in dark mode.
165
+ 'rf-surface-inset-shift': '0.06',
163
166
  },
164
167
  },
165
168
  },
@@ -175,6 +178,9 @@ export const luminaTokens = {
175
178
  * them here. */
176
179
  extra: {
177
180
  'rf-color-inline-code-bg': '#e6e5e3',
181
+ // SPEC-087 — lightness delta for the derived inset surface (a tint-tracking
182
+ // recessed fill applied at use-site via relative-color). Per-mode tunable.
183
+ 'rf-surface-inset-shift': '0.04',
178
184
  },
179
185
  };
180
186
  //# sourceMappingURL=tokens.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"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"}
package/index.css CHANGED
@@ -34,6 +34,10 @@
34
34
  @import './styles/dimensions/state.css';
35
35
  @import './styles/dimensions/media.css';
36
36
  @import './styles/dimensions/surfaces.css';
37
+ @import './styles/dimensions/frame.css';
38
+ @import './styles/dimensions/substrate.css';
39
+ @import './styles/dimensions/cover.css';
40
+ @import './styles/dimensions/guest-posture.css';
37
41
  @import './styles/dimensions/checklist.css';
38
42
  @import './styles/dimensions/sequence.css';
39
43
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@refrakt-md/lumina",
3
3
  "description": "Lumina theme for refrakt.md — design tokens, CSS, identity transform, and layout configs",
4
- "version": "0.19.0",
4
+ "version": "0.20.1",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -83,9 +83,9 @@
83
83
  "build": "tsc"
84
84
  },
85
85
  "dependencies": {
86
- "@refrakt-md/runes": "0.19.0",
87
- "@refrakt-md/transform": "0.19.0",
88
- "@refrakt-md/types": "0.19.0"
86
+ "@refrakt-md/runes": "0.20.1",
87
+ "@refrakt-md/transform": "0.20.1",
88
+ "@refrakt-md/types": "0.20.1"
89
89
  },
90
90
  "devDependencies": {
91
91
  "postcss": "^8.4.0"
@@ -18,3 +18,12 @@
18
18
  [data-rune][data-inset="tight"] { padding-inline: var(--rf-inset-tight, 1rem); }
19
19
  [data-rune][data-inset="loose"] { padding-inline: var(--rf-inset-loose, 4rem); }
20
20
  [data-rune][data-inset="breathe"] { padding-inline: var(--rf-inset-breathe, 8rem); }
21
+
22
+ /* ── Elevation — drop shadow (box-shadow) ──────────────────────────────
23
+ * Universal `elevation` attribute (SPEC-086). Maps to the shared --rf-shadow-*
24
+ * token scale; `none` explicitly flattens a rune's default shadow. */
25
+
26
+ [data-rune][data-elevation="none"] { box-shadow: var(--rf-shadow-none); }
27
+ [data-rune][data-elevation="sm"] { box-shadow: var(--rf-shadow-sm); }
28
+ [data-rune][data-elevation="md"] { box-shadow: var(--rf-shadow-md); }
29
+ [data-rune][data-elevation="lg"] { box-shadow: var(--rf-shadow-lg); }
@@ -0,0 +1,153 @@
1
+ /* Cover layout (SPEC-089) — `media-position="cover"`.
2
+ *
3
+ * The media well fills the rune interior and the content overlays it (the poster
4
+ * / cover card). The media stays a media guest — the thin-edge frame and
5
+ * `--rf-radius-media` are preserved — with content floated on top via grid
6
+ * stacking (media + content share one grid cell). No overlay primitive in the
7
+ * layout config; the variant supplies the structure (SPEC-091), CSS positions.
8
+ *
9
+ * Height authority: an external grid track (bento) wins; else the media aspect
10
+ * (`frame-aspect`, default portrait); a card height/aspect knob overrides via the
11
+ * cascade. Cover supersedes the media-vs-content split knobs (content-height /
12
+ * media-ratio), which have no meaning when there is no split. */
13
+
14
+ /* ── full scope — the whole content overlays the media well ──────────── */
15
+ [data-media-position="cover"]:not([data-cover-scope="header"]) {
16
+ display: grid;
17
+ grid-template: minmax(0, 1fr) / minmax(0, 1fr);
18
+ aspect-ratio: var(--frame-aspect, var(--cover-aspect, 3 / 4));
19
+ container-type: size;
20
+ overflow: hidden;
21
+ isolation: isolate;
22
+ }
23
+ [data-media-position="cover"]:not([data-cover-scope="header"]) > [data-section="media"],
24
+ [data-media-position="cover"]:not([data-cover-scope="header"]) > [data-name="content"] {
25
+ grid-area: 1 / 1;
26
+ margin: 0;
27
+ min-width: 0;
28
+ }
29
+
30
+ /* ── header scope — only the cover-band overlays; body flows below ────── */
31
+ [data-cover-scope="header"] > [data-name="cover-band"] {
32
+ display: grid;
33
+ grid-template: minmax(0, 1fr) / minmax(0, 1fr);
34
+ aspect-ratio: var(--frame-aspect, 16 / 9);
35
+ border-radius: var(--rf-radius-media);
36
+ overflow: hidden;
37
+ isolation: isolate;
38
+ container-type: size;
39
+ }
40
+ /* On narrow screens a 16/9 band is too short to seat the preamble over the
41
+ * scrim, so default it to a taller (≥1:1) poster shape. An explicit
42
+ * `frame-aspect` still wins. */
43
+ @media (max-width: 40rem) {
44
+ [data-cover-scope="header"] > [data-name="cover-band"] {
45
+ aspect-ratio: var(--frame-aspect, 4 / 5);
46
+ }
47
+ }
48
+ [data-cover-scope="header"] > [data-name="cover-band"] > [data-section="media"],
49
+ [data-cover-scope="header"] > [data-name="cover-band"] > [data-name="preamble"] {
50
+ grid-area: 1 / 1;
51
+ margin: 0;
52
+ }
53
+
54
+ /* ── shared: the media well fills, the overlaid box positions ─────────── */
55
+ [data-media-position="cover"] [data-section="media"] {
56
+ position: relative;
57
+ height: 100%;
58
+ border-radius: var(--rf-radius-media);
59
+ overflow: hidden;
60
+ }
61
+ [data-media-position="cover"] [data-section="media"] > :is(img, video) {
62
+ width: 100%;
63
+ height: 100%;
64
+ object-fit: cover;
65
+ }
66
+ /* The overlaid box (full: content; header: preamble) anchors via content-place. */
67
+ [data-media-position="cover"]:not([data-cover-scope="header"]) > [data-name="content"],
68
+ [data-cover-scope="header"] > [data-name="cover-band"] > [data-name="preamble"] {
69
+ position: relative;
70
+ z-index: 1;
71
+ align-self: var(--cover-place-block, end);
72
+ justify-self: var(--cover-place-inline, stretch);
73
+ padding: var(--rune-padding, var(--rf-spacing-md));
74
+ }
75
+
76
+ /* ── auto / unset placement ───────────────────────────────────────────
77
+ * `auto` is the cover default, so an unset content-place behaves the same as an
78
+ * explicit `auto`. A header band is always a caption strip — the preamble sits at
79
+ * the block-end over the scrim regardless of the band's orientation. A full-scope
80
+ * overlay adapts to the cover region's orientation (portrait → block-end caption;
81
+ * landscape → inline-start side panel). An explicit value (e.g. "center center")
82
+ * sets the `--cover-place-*` vars on the base rule above and does NOT match here,
83
+ * so it pins regardless of orientation. */
84
+ [data-media-position="cover"]:not([data-cover-scope="header"]):is([data-content-place="auto"], :not([data-content-place])) > [data-name="content"],
85
+ [data-cover-scope="header"]:is([data-content-place="auto"], :not([data-content-place])) > [data-name="cover-band"] > [data-name="preamble"] {
86
+ align-self: end;
87
+ justify-self: stretch;
88
+ }
89
+ @container (min-aspect-ratio: 1 / 1) {
90
+ [data-media-position="cover"]:not([data-cover-scope="header"]):is([data-content-place="auto"], :not([data-content-place])) > [data-name="content"] {
91
+ align-self: center;
92
+ justify-self: start;
93
+ }
94
+ }
95
+
96
+ /* ── default cover scrim (SPEC-088 scrim, on the media surface) ────────
97
+ * Overlaying text on an arbitrary image without a scrim is a legibility footgun,
98
+ * so cover applies a default gradient scrim weighted toward the content edge.
99
+ * `scrim="none"` opts out. The overlaid foreground is set light by the engine
100
+ * (data-color-scheme) so text reads against the darkened media. */
101
+ [data-media-position="cover"]:not([data-scrim="none"]) [data-section="media"]::after {
102
+ content: "";
103
+ position: absolute;
104
+ inset: 0;
105
+ pointer-events: none;
106
+ /* `inherit` matches the media zone's `--rf-radius-media`, so a frost scrim's
107
+ * `backdrop-filter` region clips at the same rounded corners — `overflow:
108
+ * hidden` on the media zone doesn't reliably clip backdrop-filter past the
109
+ * rounded edge in WebKit, so the pseudo carries its own radius. (The
110
+ * gradient variant doesn't strictly need this, but it's harmless and keeps
111
+ * the two scrim variants visually congruent.) */
112
+ border-radius: inherit;
113
+ /* The scrim shape is `--cover-scrim-image` (custom property override) when
114
+ * the engine emits one — for `content-place="center …"` it emits a radial
115
+ * ellipse so the dark falls under the centred overlay. For the directional
116
+ * cases (`start`/`end` block-axis) the engine sets `--cover-scrim-dir` and
117
+ * the linear fallback resolves to the right edge. */
118
+ background-image: var(--cover-scrim-image, linear-gradient(var(--cover-scrim-dir, to top), rgb(0 0 0 / 0.55), transparent 62%));
119
+ }
120
+
121
+ /* Frost treatment (`scrim-type="frost"`) — a frosted-glass blur over the media
122
+ * instead of a gradient. `scrim-blur` (named scale) sets the blur radius; the
123
+ * tint follows the cover scheme (a dark scheme → a dark frost for light text).
124
+ * The frost is masked to the content edge (following `--cover-scrim-dir`, default
125
+ * bottom) so it reads as a band behind the text, not a blur over the whole
126
+ * image. `--cover-scrim-mask` is the radial equivalent for centred content. */
127
+ [data-media-position="cover"][data-scrim-blur="none"] { --cover-scrim-blur: 0px; }
128
+ [data-media-position="cover"][data-scrim-blur="sm"] { --cover-scrim-blur: 4px; }
129
+ [data-media-position="cover"][data-scrim-blur="md"] { --cover-scrim-blur: 8px; }
130
+ [data-media-position="cover"][data-scrim-blur="lg"] { --cover-scrim-blur: 16px; }
131
+ [data-media-position="cover"][data-scrim-type="frost"]:not([data-scrim="none"]) [data-section="media"]::after {
132
+ background-image: none;
133
+ background-color: rgb(0 0 0 / 0.18);
134
+ -webkit-backdrop-filter: blur(var(--cover-scrim-blur, 8px));
135
+ backdrop-filter: blur(var(--cover-scrim-blur, 8px));
136
+ -webkit-mask-image: var(--cover-scrim-mask, linear-gradient(var(--cover-scrim-dir, to top), #000 30%, transparent 72%));
137
+ mask-image: var(--cover-scrim-mask, linear-gradient(var(--cover-scrim-dir, to top), #000 30%, transparent 72%));
138
+ }
139
+ [data-media-position="cover"][data-scrim-type="frost"][data-color-scheme="light"]:not([data-scrim="none"]) [data-section="media"]::after {
140
+ background-color: rgb(255 255 255 / 0.25);
141
+ }
142
+
143
+ /* The overlay carries the cover colour-scheme (set by the engine) so its text
144
+ * reads against the darkened media. The standalone `[data-color-scheme]` rule
145
+ * also paints a `background-color`; null it out here so the overlay stays
146
+ * see-through over the media (the scrim, not a solid box, does the legibility). */
147
+ [data-media-position="cover"] [data-name="content"][data-color-scheme],
148
+ [data-cover-scope="header"] > [data-name="cover-band"][data-color-scheme] {
149
+ background-color: transparent;
150
+ /* Muted text (the blurb) is barely legible over a photo, so on the overlay it
151
+ * reads at full foreground strength — the same colour as the headline. */
152
+ --rf-color-muted: var(--rf-color-text);
153
+ }
@@ -0,0 +1,104 @@
1
+ /* Frame chrome (SPEC-086) — media-surface presentation.
2
+ *
3
+ * The engine lands the frame contract on the frame-target element: a rune's
4
+ * `[data-section="media"]` zone (frameTarget: 'media') or the rune's own root
5
+ * (frameTarget: 'self', e.g. figure/showcase). These selectors are intentionally
6
+ * surface-agnostic — they match wherever the chrome is emitted.
7
+ *
8
+ * data-frame — applied preset name (marker)
9
+ * data-frame-shadow — silhouette drop-shadow (none|sm|md|lg)
10
+ * data-displace — edge/corner the guest moves toward
11
+ * --frame-aspect — aspect-ratio of the framed media
12
+ * --frame-offset — displacement distance (resolved spacing token)
13
+ * --frame-oversize — scale factor by which the guest exceeds its slot
14
+ * --frame-anchor — crop focal point (object-position)
15
+ * --frame-place-x / -y — guest-box alignment within its slot
16
+ *
17
+ * Clip is host-owned: a clipping host (card/bento-cell/figure media well) crops a
18
+ * displaced/oversized guest into a *peek*; a breakout host (showcase-self, a
19
+ * section/page) lets it *spill*. `elevation` (box-shadow on the self surface) and
20
+ * `frame-shadow` (drop-shadow silhouette here) never collide — different property,
21
+ * different surface. */
22
+
23
+ /* ── Silhouette drop-shadow ────────────────────────────────────────────
24
+ * Same media-target / self-target split as `frame-displace`. Media-target
25
+ * frames land `data-frame-shadow` on the media zone wrapper, but the shadow
26
+ * should ride the inner guest so it (a) shifts with `frame-displace` and
27
+ * (b) actually casts onto exposed slot interior when the guest is inset
28
+ * from the wrapper. Self-target frames (showcase, figure) keep the filter
29
+ * on the root itself, where it traces the rune's own painted silhouette. */
30
+ [data-frame-shadow="none"] { --frame-shadow: none; }
31
+ [data-frame-shadow="sm"] { --frame-shadow: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.10)); }
32
+ [data-frame-shadow="md"] { --frame-shadow: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.25)); }
33
+ [data-frame-shadow="lg"] { --frame-shadow: drop-shadow(0 12px 40px rgba(0, 0, 0, 0.20)); }
34
+
35
+ /* Self-target — figure, showcase, anything else where the rune root is the
36
+ * frame target. The filter applies to the painted root. */
37
+ [data-frame-shadow]:not([data-section="media"]) {
38
+ filter: var(--frame-shadow);
39
+ }
40
+
41
+ /* Media-target — card, bento-cell, recipe, etc. The filter rides the inner
42
+ * guest so a displaced image's shadow falls into the slot's exposed
43
+ * interior (clipped at the slot's `overflow: hidden` boundary like any
44
+ * other in-slot content). */
45
+ [data-section="media"][data-frame-shadow] > :is(img, video, [data-rune]) {
46
+ filter: var(--frame-shadow);
47
+ }
48
+
49
+ /* ── Aspect ratio ────────────────────────────────────────────────────── */
50
+ [style*="--frame-aspect"] { aspect-ratio: var(--frame-aspect); }
51
+
52
+ /* ── Crop anchor — focal point when the guest is cut ─────────────────── */
53
+ [style*="--frame-anchor"] > :is(img, video) { object-position: var(--frame-anchor); }
54
+
55
+ /* ── Placement of the framed box within its slot ─────────────────────── */
56
+ [style*="--frame-place-x"] { justify-self: var(--frame-place-x, auto); align-self: var(--frame-place-y, auto); }
57
+
58
+ /* ── Oversize — guest exceeds its slot (clipping hosts crop it) ──────── */
59
+ /* `[data-rune]` extends the rule to nested rune guests (codegroup, mockup,
60
+ * chart, etc.), so frame chrome stays guest-agnostic — the same selector
61
+ * pattern the displace rule uses. */
62
+ [style*="--frame-oversize"] > :is(img, video, [data-rune]) { width: calc(100% * var(--frame-oversize, 1)); max-width: none; }
63
+
64
+ /* ── Displacement (peek / spill) — move the guest toward an edge/corner.
65
+ * The host decides whether the guest is cropped or spills; this only moves it.
66
+ * Two cases, distinguished by where the engine landed `data-displace`:
67
+ *
68
+ * 1. Media-target frame (`frameTarget: 'media'`, e.g. card / bento-cell /
69
+ * figure media well). `data-displace` lands on the media zone wrapper
70
+ * (`[data-section="media"]`), but the wrapper itself is the *host* and
71
+ * already has `overflow: hidden`. We translate the inner guest with
72
+ * `transform: translate(...)` so the zone clips the peek at its own edge,
73
+ * not at the outer card/cell edge. The wrapper stays put — the card's
74
+ * layout is unchanged.
75
+ *
76
+ * 2. Self-target frame (`frameTarget: 'self'`, e.g. showcase, where the rune
77
+ * root is the host). `data-displace` lands on the root, and the root's
78
+ * parent (a page section, the page itself) is the clip-or-spill decider.
79
+ * We push the root with negative margins so it can spill past its
80
+ * parent's edge for a true breakout. */
81
+
82
+ /* — Case 1: media-zone displace → translate the inner guest, zone clips. */
83
+ [data-section="media"][data-displace] > :is(img, video, [data-rune]) {
84
+ transform: translate(var(--displace-x, 0), var(--displace-y, 0));
85
+ }
86
+ [data-section="media"][data-displace="top"] { --displace-y: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
87
+ [data-section="media"][data-displace="bottom"] { --displace-y: var(--frame-offset, var(--rf-spacing-lg)); }
88
+ [data-section="media"][data-displace="end"] { --displace-x: var(--frame-offset, var(--rf-spacing-lg)); }
89
+ [data-section="media"][data-displace="top-end"] { --displace-x: var(--frame-offset, var(--rf-spacing-lg)); --displace-y: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
90
+ [data-section="media"][data-displace="bottom-end"] { --displace-x: var(--frame-offset, var(--rf-spacing-lg)); --displace-y: var(--frame-offset, var(--rf-spacing-lg)); }
91
+
92
+ /* — Case 2: self-target (showcase, etc.) → root moves; parent decides. */
93
+ [data-displace]:not([data-section="media"]) { position: relative; z-index: 1; }
94
+ [data-displace="top"]:not([data-section="media"]) { margin-top: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
95
+ [data-displace="bottom"]:not([data-section="media"]) { margin-bottom: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
96
+ [data-displace="both"]:not([data-section="media"]) { margin-block: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
97
+ [data-displace="end"]:not([data-section="media"]) { margin-inline-end: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
98
+ [data-displace="bottom-end"]:not([data-section="media"]) { margin-bottom: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); margin-inline-end: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
99
+ [data-displace="top-end"]:not([data-section="media"]) { margin-top: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); margin-inline-end: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
100
+
101
+ /* Displacement carries across breakpoints. For media-target the guest is
102
+ * clipped by the zone's `overflow: hidden` regardless of width, so the peek
103
+ * reads consistently from desktop to mobile. For self-target (showcase) the
104
+ * negative-margin spill stays on too — that's the intended breakout. */
@@ -0,0 +1,27 @@
1
+ /* Media-guest interaction posture (SPEC-090).
2
+ *
3
+ * A media-slot guest is presentational by default. When its container is itself
4
+ * an interaction target — a `card` / `bento-cell` with a stretched whole-tile
5
+ * `href` link — or the guest is a `cover` backdrop (SPEC-089), the engine marks
6
+ * the media zone `data-guest-posture="presentational"`. That makes the guest
7
+ * non-interactive at the pointer level, so:
8
+ * • a linked tile links reliably — clicks over the media fall through to the
9
+ * stretched link beneath it instead of being eaten by the guest's controls;
10
+ * • a cover backdrop never steals interaction from the overlaid content.
11
+ *
12
+ * The behaviours layer additionally skips JS enhancement for a presentational
13
+ * guest (see @refrakt-md/behaviors), so it renders its static fallback. The
14
+ * posture is scoped to the media zone only — content-overlay controls
15
+ * (body/footer links & buttons) are outside it and stay fully interactive. */
16
+ [data-section="media"][data-guest-posture="presentational"] {
17
+ pointer-events: none;
18
+ }
19
+
20
+ /* Static fallback for demoted tabbed guests (`codegroup`, `tabs`): their tab
21
+ * strip is rendered statically but the behaviour that wires it is skipped, so
22
+ * it would sit there inert. Hide it and let the panels read as plain stacked
23
+ * content — the non-enhanced render is the fallback. (Guests whose chrome is
24
+ * JS-injected — datatable toolbars, map controls — simply never appear.) */
25
+ [data-section="media"][data-guest-posture="presentational"] :is(.rf-tabs__tabs, .rf-codegroup__tabs) {
26
+ display: none;
27
+ }
@@ -0,0 +1,108 @@
1
+ /* Substrate (SPEC-087) — generated surface pattern.
2
+ *
3
+ * The token-driven gradient recipes that realise each pattern ship here in the
4
+ * always-included base layer (not a theme's swappable CSS), so "dots = dots on
5
+ * every theme" is a guarantee: themes retune the `--substrate-*` token hooks
6
+ * (ink colour, cell size), they don't redefine a pattern's geometry. Patterns
7
+ * are pure CSS — no image assets, crisp at any zoom. The engine emits markers
8
+ * only (`data-substrate` + `--substrate-*`); CSS does the drawing.
9
+ *
10
+ * The pattern is a `background-image` over the surface `background-color`; the
11
+ * gradient's `transparent` gaps reveal whatever fill sits beneath (tint/inset).
12
+ * `--substrate-ink` resolves from `--rf-color-border` (tint-bridged), so the
13
+ * pattern recolours with the surface.
14
+ *
15
+ * Runes set their surface with the `background` *shorthand* (e.g.
16
+ * `.rf-card { background: var(--rf-color-surface) }`), which resets
17
+ * `background-image`/`-size`/`-position` and loads *after* this base layer. So
18
+ * the actual `background-*` longhands live here on the higher-specificity
19
+ * `[data-substrate]:not(…)` selector (0,2,0 — beats a rune's `.rf-*`, 0,1,0),
20
+ * and each pattern only feeds custom properties (which never collide with a
21
+ * rune's background). The rune's `background-color` still shows through. */
22
+
23
+ [data-substrate]:not([data-substrate="none"]) {
24
+ --substrate-cell: 16px;
25
+ --substrate-opacity: 0.5;
26
+ /* `--substrate-boost` (per-pattern) and `--substrate-mode-boost` (light/dark)
27
+ * are intentionally NOT set here — only referenced with a `1` fallback — so a
28
+ * lower-specificity pattern rule or an inherited dark-mode rule can drive them
29
+ * without losing to this (0,2,0) base. The product is clamped to 100% by
30
+ * color-mix. */
31
+ --substrate-ink: color-mix(in oklab, var(--rf-color-border) calc(var(--substrate-opacity) * var(--substrate-boost, 1) * var(--substrate-mode-boost, 1) * 100%), transparent);
32
+ background-image: var(--substrate-image);
33
+ background-size: var(--substrate-tile, var(--substrate-cell) var(--substrate-cell));
34
+ background-position: var(--substrate-pos, 0 0);
35
+ background-repeat: repeat;
36
+ }
37
+
38
+ /* dots — one tiled radial-gradient. A small dot reads fainter than a 1px line,
39
+ * so dots get a per-pattern ink boost. */
40
+ [data-substrate="dots"] {
41
+ --substrate-image: radial-gradient(var(--substrate-ink) var(--substrate-dot, 1.5px), transparent 0);
42
+ --substrate-boost: 1.4;
43
+ }
44
+
45
+ /* grid — two tiled linear-gradients */
46
+ [data-substrate="grid"] {
47
+ --substrate-image:
48
+ linear-gradient(var(--substrate-ink) 1px, transparent 1px),
49
+ linear-gradient(90deg, var(--substrate-ink) 1px, transparent 1px);
50
+ }
51
+
52
+ /* lines — diagonal hatching (the repeating gradient owns its own spacing) */
53
+ [data-substrate="lines"] {
54
+ --substrate-image: repeating-linear-gradient(45deg, var(--substrate-ink) 0 1px, transparent 1px var(--substrate-cell));
55
+ --substrate-tile: auto;
56
+ }
57
+
58
+ /* cross — plus marks at the centre of each cell, drawn as two perpendicular
59
+ * thin ellipses (no `mask` — an element-level `mask` would clip the rune's
60
+ * own text content along with the pattern, which is what we don't want). A
61
+ * small plus reads fainter than a 1px line at the same ink opacity, so the
62
+ * pattern carries its own ink boost — same idea as dots.
63
+ *
64
+ * The stop syntax mirrors dots: `ink <r>, transparent 0` produces a sharp-
65
+ * edged shape filled with ink (the explicit length on ink, plus `0` after
66
+ * transparent for a zero-length transition, beats the `ink 100%, transparent
67
+ * 100%` form which some renderers collapse to nothing). Ellipse radii at
68
+ * 1px (rather than sub-pixel 0.5px) so the stroke survives DPR rounding. */
69
+ [data-substrate="cross"] {
70
+ --substrate-image:
71
+ radial-gradient(ellipse 3px 1px at center, var(--substrate-ink) 100%, transparent 0),
72
+ radial-gradient(ellipse 1px 3px at center, var(--substrate-ink) 100%, transparent 0);
73
+ --substrate-pos: center;
74
+ --substrate-boost: 1.6;
75
+ }
76
+
77
+ /* checker — alternating filled cells */
78
+ [data-substrate="checker"] {
79
+ --substrate-image:
80
+ conic-gradient(var(--substrate-ink) 0 25%, transparent 0 50%, var(--substrate-ink) 0 75%, transparent 0);
81
+ }
82
+
83
+ [data-substrate="none"] { background-image: none; }
84
+
85
+ /* substrate-fill="inset" — paint the pattern over the recessed inset fill
86
+ * (SPEC-087 inset surface) instead of the inherited surface colour. */
87
+ [data-substrate][data-substrate-fill="inset"] {
88
+ background-color: oklch(from var(--rf-color-surface) calc(l - var(--rf-surface-inset-shift)) c h);
89
+ }
90
+
91
+ /* Dark mode — the border ink sits close to the dark surface, so patterns need
92
+ * more presence across the board (dots compound their per-pattern boost on top,
93
+ * landing near full strength). Set the multiplier on the dark root so it
94
+ * inherits into every substrate element; mirrors how dark.css triggers dark. */
95
+ [data-theme="dark"],
96
+ [data-color-scheme="dark"] {
97
+ --substrate-mode-boost: 1.6;
98
+ }
99
+ @media (prefers-color-scheme: dark) {
100
+ :root:not([data-theme="light"]) {
101
+ --substrate-mode-boost: 1.6;
102
+ }
103
+ }
104
+ /* A tint-locked light subtree resets to the light intensity (nearest ancestor
105
+ * wins via inheritance), even when nested inside dark mode. */
106
+ [data-color-scheme="light"] {
107
+ --substrate-mode-boost: 1;
108
+ }
@@ -102,3 +102,56 @@
102
102
  border-radius: var(--rf-radius-container);
103
103
  padding: var(--rune-padding, var(--rf-spacing-md));
104
104
  }
105
+
106
+ /* ─── Media slot inherits the surface border ─────────────────────────
107
+ * When a surface-bearing rune wraps a `[data-section="media"]` slot, the slot
108
+ * shares the surface's 1px border so the framed media reads as part of the
109
+ * same surface rather than a separate inner box. Scoped via `:where()` for
110
+ * zero specificity — per-rune CSS can still override if a particular surface
111
+ * wants a different chrome (e.g. character's circular portrait avoids this
112
+ * because it's not a `[data-section="media"]` slot). */
113
+ :where(.rf-card, .rf-bento-cell, .rf-recipe, .rf-realm, .rf-faction, .rf-playlist) [data-section="media"] {
114
+ border: 1px solid var(--rf-color-border);
115
+ }
116
+
117
+ /* ─── Inset surface (SPEC-087) — tint-tracking recessed fill ──────────
118
+ * Derived at use-site via relative-color (lower L, keep C+H) so it recomputes from a tinted
119
+ * `--rf-color-surface` automatically (a static inset-colour token would
120
+ * freeze to the untinted :root). Writes `background` only — never re-bases
121
+ * `--rf-color-surface` — so insets don't compound under nesting; depth is
122
+ * conveyed by border/elevation. `--rf-surface-inset-shift: 0` flushes it. */
123
+
124
+ /* chart / diagram self surface: the standalone "darker surface". */
125
+ .rf-chart,
126
+ .rf-diagram {
127
+ background: oklch(from var(--rf-color-surface) calc(l - var(--rf-surface-inset-shift)) c h);
128
+ }
129
+
130
+ /* …but when they're a media-zone guest the slot already provides the recessed
131
+ * surface (border, radius, inset fill), so the inner chrome would just stack
132
+ * as double-chrome. Drop it; keep padding so axis labels / SVG keep breathing
133
+ * room. */
134
+ [data-section="media"] > :is(.rf-chart, .rf-diagram) {
135
+ background: transparent;
136
+ border: 0;
137
+ }
138
+
139
+ /* Same treatment for `juxtapose` as a media guest — its `__panels` container
140
+ * has its own border + background + rounded corners (juxtapose stands alone
141
+ * outside a card), but inside a card slot they double up with the slot's own
142
+ * chrome. Drop the inner chrome; round to match the slot's media radius so the
143
+ * comparison reads as part of the card surface. The slot's overflow:visible
144
+ * and container-type:normal still come from `split.css` (juxtapose manages
145
+ * its own clip inside `__panels`). */
146
+ [data-section="media"] > .rf-juxtapose .rf-juxtapose__panels {
147
+ background: transparent;
148
+ border: 0;
149
+ border-radius: var(--rf-radius-media);
150
+ }
151
+
152
+ /* Media wells of media-bearing runes: a recessed sub-surface that tracks the
153
+ * (possibly tinted) container colour — invisible under a full-bleed guest,
154
+ * visible in the gaps (transparent, displaced, or absent guest). */
155
+ :is(.rf-card, .rf-bento-cell, .rf-recipe, .rf-realm, .rf-faction, .rf-playlist) [data-section="media"] {
156
+ background: oklch(from var(--rf-color-surface) calc(l - var(--rf-surface-inset-shift)) c h);
157
+ }
@@ -102,10 +102,20 @@
102
102
  [data-section="media"]:is(
103
103
  :has(> [data-rune="preview"]),
104
104
  :has(> [data-rune="juxtapose"]),
105
- :has(> .rf-showcase[data-bleed]:not(.rf-showcase--in-bento-cell))
105
+ :has(> .rf-showcase[data-displace]:not(.rf-showcase--in-bento-cell))
106
106
  ) {
107
107
  overflow: visible;
108
108
  container-type: normal;
109
+ }
110
+ /* `preview` (negative-margin breakout) and a displaced `showcase` (peek
111
+ * spill) want the slot fully de-chromed so the bleed reads as edge-to-edge.
112
+ * Juxtapose just opted out of the clip — it still wants to read as part of
113
+ * the card surface, so it keeps the slot's rounded corners (juxtapose's
114
+ * own `__panels` chrome is dropped instead; see surfaces.css). */
115
+ [data-section="media"]:is(
116
+ :has(> [data-rune="preview"]),
117
+ :has(> .rf-showcase[data-displace]:not(.rf-showcase--in-bento-cell))
118
+ ) {
109
119
  border-radius: 0;
110
120
  }
111
121
  /* …and let those self-managing guests keep their intrinsic width/sizing: the
@@ -114,7 +124,7 @@
114
124
  [data-section="media"]:is(
115
125
  :has(> [data-rune="preview"]),
116
126
  :has(> [data-rune="juxtapose"]),
117
- :has(> .rf-showcase[data-bleed]:not(.rf-showcase--in-bento-cell))
127
+ :has(> .rf-showcase[data-displace]:not(.rf-showcase--in-bento-cell))
118
128
  ) > * {
119
129
  width: auto;
120
130
  max-height: none;
@@ -142,6 +142,11 @@
142
142
  aspect-ratio: var(--bento-media-aspect, 16 / 9);
143
143
  }
144
144
  .rf-bento-cell__media > :is(img, video) { object-position: var(--bento-media-anchor, center); }
145
+ /* SPEC-086 — reconcile `frame` facets with bento's existing media vars rather
146
+ * than duplicating them: frame-aspect/anchor (landed on the media zone by the
147
+ * engine) feed the knobs bento already consumes (incl. the collapse path). */
148
+ .rf-bento-cell__media[style*="--frame-aspect"] { --bento-media-aspect: var(--frame-aspect); }
149
+ .rf-bento-cell__media[style*="--frame-anchor"] { --bento-media-anchor: var(--frame-anchor); }
145
150
  .rf-bento-cell__title {
146
151
  font-size: 1.125rem;
147
152
  font-weight: 600;
@@ -1,11 +1,16 @@
1
1
  /* Background — Directive rune for background images, video, overlays, blur */
2
2
 
3
- /* Background layer — absolute positioned behind content */
3
+ /* Background layer — absolute positioned behind content. `border-radius:
4
+ * inherit` rounds the gradient/image to whatever shape the parent surface has,
5
+ * so a card / bento-cell / any rounded host clips the bg to its own corners
6
+ * without needing `overflow: hidden` (though hosts with thin-edge padding
7
+ * still need it so the bg can't poke past the rounded outer edge). */
4
8
  [data-name="bg"] {
5
9
  position: absolute;
6
10
  inset: 0;
7
11
  z-index: 0;
8
12
  overflow: hidden;
13
+ border-radius: inherit;
9
14
  background-image: var(--bg-image);
10
15
  background-size: var(--bg-fit, cover);
11
16
  background-position: var(--bg-position, center);
@@ -22,8 +27,14 @@
22
27
  [data-name="bg"][data-bg-fixed] {
23
28
  background-attachment: fixed;
24
29
  }
25
- /* Content above background — any sibling of bg layer gets z-index */
26
- .rf-has-bg > :not([data-name="bg"]) {
30
+ /* Content above background — any sibling of the bg layer gets `z-index: 1`
31
+ * so it stacks over the `data-name="bg"` layer (which sits at `z-index: 0`).
32
+ * Keyed off the `data-bg` attribute the engine emits on the bg-owning rune
33
+ * (`engine.ts` → `bgDataAttrs['data-bg'] = ''`), not the per-block BEM
34
+ * modifier class `rf-<block>--has-bg` — the attribute is rune-agnostic so
35
+ * one rule covers card, bento-cell, hero, feature, and anything else that
36
+ * raises a bg layer. */
37
+ [data-bg] > :not([data-name="bg"]) {
27
38
  position: relative;
28
39
  z-index: 1;
29
40
  }
@@ -51,3 +62,27 @@
51
62
  [data-name="bg-overlay"][data-bg-overlay="light"] {
52
63
  background: rgba(255, 255, 255, 0.6);
53
64
  }
65
+
66
+ /* Scrim (SPEC-088) — a structured legibility treatment behind overlaid text.
67
+ * gradient: a directional darken/lighten; frost: a backdrop blur + tint.
68
+ * `tone` picks the colour, `dir` the heavy edge; strength/blur come inline. */
69
+ [data-name="scrim"] {
70
+ position: absolute;
71
+ inset: 0;
72
+ pointer-events: none;
73
+ z-index: 1;
74
+ }
75
+ [data-name="scrim"][data-scrim-tone="dark"] { --scrim-color: 0 0 0; }
76
+ [data-name="scrim"][data-scrim-tone="light"] { --scrim-color: 255 255 255; }
77
+ [data-name="scrim"][data-scrim-dir="top"] { --scrim-grad-dir: to top; }
78
+ [data-name="scrim"][data-scrim-dir="bottom"] { --scrim-grad-dir: to bottom; }
79
+ [data-name="scrim"][data-scrim-dir="left"] { --scrim-grad-dir: to left; }
80
+ [data-name="scrim"][data-scrim-dir="right"] { --scrim-grad-dir: to right; }
81
+ [data-name="scrim"][data-scrim="gradient"] {
82
+ background-image: linear-gradient(var(--scrim-grad-dir, to top), transparent, rgb(var(--scrim-color, 0 0 0) / var(--scrim-strength, 0.55)));
83
+ }
84
+ [data-name="scrim"][data-scrim="frost"] {
85
+ -webkit-backdrop-filter: blur(var(--scrim-blur, 8px));
86
+ backdrop-filter: blur(var(--scrim-blur, 8px));
87
+ background: rgb(var(--scrim-color, 0 0 0) / 0.18);
88
+ }
@@ -27,6 +27,13 @@
27
27
  border-radius: var(--rf-radius-container);
28
28
  border: 1px solid var(--rf-color-border);
29
29
  background: var(--rf-color-surface);
30
+ /* Clip non-media content (substrate patterns, bg gradient/image layers) to
31
+ * the card's rounded outer corners. The displaced-peek case is unaffected:
32
+ * `frame-displace` translates the guest *inside* the media zone, which has
33
+ * its own `overflow: hidden`, so no peek ever crosses this boundary.
34
+ * `box-shadow` from `elevation` paints outside the element box and is
35
+ * unaffected by `overflow`. */
36
+ overflow: hidden;
30
37
  }
31
38
 
32
39
  /* Content zone fills the remaining inset so total text padding still equals
@@ -125,6 +132,19 @@
125
132
  flex: 1;
126
133
  }
127
134
 
135
+ /* Height knob (SPEC-089) — a named intrinsic-height scale for cover / bg-only
136
+ * cards that have no natural content height to fill. `aspect` (→ aspect-ratio,
137
+ * emitted inline by the engine) is the proportional alternative; `height` is the
138
+ * absolute one. Both are authority overrides: they win over the cover variant's
139
+ * default media aspect because they sit on the card root with a real value, and
140
+ * over an external grid track only when the card isn't being stretched by one.
141
+ * For cover cards the grid stack (cover.css) fills this box; for bg-only cards
142
+ * the min-height simply gives the surface a poster shape. */
143
+ .rf-card[data-height="sm"] { min-height: 12rem; }
144
+ .rf-card[data-height="md"] { min-height: 18rem; }
145
+ .rf-card[data-height="lg"] { min-height: 26rem; }
146
+ .rf-card[data-height="xl"] { min-height: 34rem; }
147
+
128
148
  /* Whole-card stretched link — covers the card; real links in body/footer
129
149
  * sit above it via position:relative. */
130
150
  .rf-card__link {
@@ -1,9 +1,23 @@
1
1
  /* Figure */
2
2
  .rf-figure img {
3
+ display: block;
3
4
  max-width: 100%;
4
5
  height: auto;
6
+ margin-inline: auto;
5
7
  border-radius: var(--rf-radius-media);
6
8
  }
9
+
10
+ /* When `frame-aspect` sets an explicit surface shape, the image fills the
11
+ * shape rather than sitting at its natural size with whitespace on the
12
+ * inline-end. `object-fit: cover` crops only if the natural aspect doesn't
13
+ * match — and `frame-anchor` already picks the focal point of that crop
14
+ * (the same contract as a card's media slot). */
15
+ .rf-figure[style*="--frame-aspect"] img {
16
+ width: 100%;
17
+ height: 100%;
18
+ object-fit: cover;
19
+ margin: 0;
20
+ }
7
21
  .rf-figure figcaption {
8
22
  margin-top: 0.625rem;
9
23
  font-size: 0.825rem;
@@ -22,6 +22,13 @@
22
22
  width: 100%;
23
23
  position: relative;
24
24
  background: var(--rf-color-surface, #f8f9fa);
25
+ /* Establish a stacking context so Leaflet's internal pane z-indices
26
+ * (tile-pane 200, overlay-pane 400, marker-pane 600, popup-pane 700,
27
+ * etc.) stay contained instead of escaping past siblings — otherwise
28
+ * a map dropped into a cover card's media slot punches through the
29
+ * cover scrim (z-index auto) and overlaid content (z-index 1).
30
+ * `position: relative` alone is not enough; `isolation: isolate` is. */
31
+ isolation: isolate;
25
32
  }
26
33
 
27
34
  /* Fallback list shown while Leaflet loads */
@@ -80,6 +87,28 @@
80
87
  display: none;
81
88
  }
82
89
 
90
+ /* ── Map as a media guest ──
91
+ * When dropped into a `[data-section="media"]` slot (e.g. a card or
92
+ * bento-cell in cover mode), the map yields its standalone presentation to
93
+ * the slot: no outer margin or padding (map is in the Banner surface group
94
+ * which gives it vertical `--rune-padding`; see `surfaces.css`), no rounding
95
+ * (the slot already clips), and the Leaflet container stretches to fill the
96
+ * slot height instead of capping at the per-size pixel height. The fallback
97
+ * list (rendered before Leaflet hydrates) drops its outer padding too so it
98
+ * doesn't bleed past the slot edge during the brief flash before tiles load. */
99
+ [data-section="media"] > .rf-map {
100
+ margin: 0;
101
+ padding: 0;
102
+ height: 100%;
103
+ border-radius: inherit;
104
+ }
105
+ [data-section="media"] > .rf-map > .rf-map__container {
106
+ height: 100%;
107
+ }
108
+ [data-section="media"] .rf-map__fallback {
109
+ padding: 0.75rem 1rem;
110
+ }
111
+
83
112
  /* Leaflet popup overrides to match theme */
84
113
  .rf-map .leaflet-popup-content-wrapper {
85
114
  border-radius: var(--rf-radius-md, 0.5rem);
@@ -75,7 +75,11 @@
75
75
  border-radius: var(--rf-radius-lg);
76
76
  overflow: hidden;
77
77
  }
78
- .rf-preview__canvas:has(.rf-mockup) {
78
+ /* Centre a standalone mockup whose `data-fit="none"` makes it inline-block
79
+ * margin-inline auto can't centre an inline-block, only its parent's text
80
+ * alignment can. Scoped to a *direct-child* mockup so a card or bento cell
81
+ * that happens to hold a mockup doesn't get its body text centred too. */
82
+ .rf-preview__canvas:has(> .rf-mockup) {
79
83
  text-align: center;
80
84
  }
81
85
  .rf-preview__canvas > *:first-child {
@@ -1,56 +1,16 @@
1
- /* Showcase — Media presentation wrapper with shadows, bleed, and aspect ratio */
1
+ /* Showcase — `frameTarget: 'self'` media wrapper (SPEC-086). Shadow, aspect,
2
+ * displacement, place, and crop anchor are now the shared `frame` chrome
3
+ * (dimensions/frame.css), landing on the showcase root. This file keeps only
4
+ * showcase's structural rules and its distinct value: breakout. */
2
5
  .rf-showcase {
3
6
  position: relative;
4
7
  }
5
8
  .rf-showcase__viewport {
6
9
  position: relative;
7
10
  }
8
- /* Shadows */
9
- .rf-showcase[data-shadow="soft"] {
10
- filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.1));
11
- }
12
- .rf-showcase[data-shadow="hard"] {
13
- filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.25));
14
- }
15
- .rf-showcase[data-shadow="elevated"] {
16
- filter: drop-shadow(0 12px 40px rgba(0, 0, 0, 0.2));
17
- }
18
- /* Bleed displacement via negative margins */
19
- .rf-showcase[data-bleed="top"] {
20
- margin-top: calc(-1 * var(--showcase-offset, 2rem));
21
- position: relative;
22
- z-index: 1;
23
- }
24
- .rf-showcase[data-bleed="bottom"] {
25
- margin-bottom: calc(-1 * var(--showcase-offset, 2rem));
26
- position: relative;
27
- z-index: 1;
28
- }
29
- .rf-showcase[data-bleed="both"] {
30
- margin-top: calc(-1 * var(--showcase-offset, 2rem));
31
- margin-bottom: calc(-1 * var(--showcase-offset, 2rem));
32
- position: relative;
33
- z-index: 1;
34
- }
35
- .rf-showcase[data-bleed="end"] {
36
- margin-inline-end: calc(-1 * var(--showcase-offset, 2rem));
37
- position: relative;
38
- z-index: 1;
39
- }
40
- .rf-showcase[data-bleed="bottom-end"] {
41
- margin-bottom: calc(-1 * var(--showcase-offset, 2rem));
42
- margin-inline-end: calc(-1 * var(--showcase-offset, 2rem));
43
- position: relative;
44
- z-index: 1;
45
- }
46
- .rf-showcase[data-bleed="top-end"] {
47
- margin-top: calc(-1 * var(--showcase-offset, 2rem));
48
- margin-inline-end: calc(-1 * var(--showcase-offset, 2rem));
49
- position: relative;
50
- z-index: 1;
51
- }
52
- /* Parent needs overflow visible for bleed — except inside clipping containers like bento cells */
53
- :has(> .rf-showcase[data-bleed]:not(.rf-showcase--in-bento-cell)) {
11
+ /* Breakout — a displaced showcase spills past a non-clipping ancestor; clipping
12
+ * hosts (e.g. bento cells) keep it cropped (host-owned clip). */
13
+ :has(> .rf-showcase[data-displace]:not(.rf-showcase--in-bento-cell)) {
54
14
  overflow: visible;
55
15
  }
56
16
  /* Bento cell context — showcase fills remaining space */
@@ -58,22 +18,6 @@
58
18
  flex: 1;
59
19
  min-height: 0;
60
20
  }
61
- /* Aspect ratio enforcement */
62
- .rf-showcase[data-aspect] .rf-showcase__viewport {
63
- aspect-ratio: var(--showcase-aspect);
64
- overflow: hidden;
65
- }
66
- .rf-showcase[data-aspect] .rf-showcase__viewport > img,
67
- .rf-showcase[data-aspect] .rf-showcase__viewport > video {
68
- width: 100%;
69
- height: 100%;
70
- object-fit: cover;
71
- }
72
- /* Self-positioning within parent via place attribute */
73
- .rf-showcase[data-place] {
74
- justify-self: var(--place-x, auto);
75
- align-self: var(--place-y, auto);
76
- }
77
21
  /* Spacing — vertical margin around the showcase */
78
22
  .rf-showcase[data-spacing="flush"] { margin-top: 0; margin-bottom: 0; }
79
23
  .rf-showcase[data-spacing="tight"] { margin-top: var(--rf-spacing-section-tight, 1.5rem); margin-bottom: var(--rf-spacing-section-tight, 1.5rem); }
package/tokens/base.css CHANGED
@@ -27,6 +27,9 @@
27
27
  --rf-color-surface-hover: #ecebe8;
28
28
  --rf-color-surface-active: #e2e0dd;
29
29
  --rf-color-surface-raised: #ffffff;
30
+ /* SPEC-087 — lightness delta for the derived inset surface (applied at use-site
31
+ * via relative-color — lower L, keep C+H — so it tracks a tinted --rf-color-surface). */
32
+ --rf-surface-inset-shift: 0.04;
30
33
 
31
34
  /* Semantic — muted earthy band per SPEC-051 */
32
35
  --rf-color-info: #34547a;
@@ -89,6 +92,7 @@
89
92
  --rf-inset-breathe: 8rem;
90
93
 
91
94
  /* Shadows */
95
+ --rf-shadow-none: none;
92
96
  --rf-shadow-xs: 0 1px 2px rgba(0,0,0,0.04);
93
97
  --rf-shadow-sm: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);
94
98
  --rf-shadow-md: 0 4px 12px rgba(0,0,0,0.07), 0 1px 3px rgba(0,0,0,0.04);
package/tokens/dark.css CHANGED
@@ -19,6 +19,7 @@
19
19
  --rf-color-surface-hover: #282825;
20
20
  --rf-color-surface-active: #333330;
21
21
  --rf-color-surface-raised: #272723;
22
+ --rf-surface-inset-shift: 0.06;
22
23
 
23
24
  --rf-color-info: #9bb4c7;
24
25
  --rf-color-info-bg: #1f2530;
@@ -79,6 +80,7 @@
79
80
  --rf-color-surface-hover: #282825;
80
81
  --rf-color-surface-active: #333330;
81
82
  --rf-color-surface-raised: #272723;
83
+ --rf-surface-inset-shift: 0.06;
82
84
 
83
85
  --rf-color-info: #9bb4c7;
84
86
  --rf-color-info-bg: #1f2530;