@refrakt-md/lumina 0.14.4 → 0.16.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.
@@ -0,0 +1,166 @@
1
+ /* Collection — list / grid / table / cards of registry entities (SPEC-070).
2
+ *
3
+ * The block is layout-agnostic; the `data-layout` attribute on the wrapper
4
+ * selects the arrangement. Built-in items carry `.rf-collection__card` /
5
+ * `__item`; a body template emits its own markup inside `.rf-collection__item`.
6
+ */
7
+
8
+ .rf-collection {
9
+ margin: var(--rf-spacing-md) 0;
10
+ }
11
+
12
+ /* Preamble — rendered above items only when the query is non-empty, so a
13
+ * heading/intro can live inside the rune and vanish with an empty section. */
14
+ .rf-collection__preamble > :first-child { margin-top: 0; }
15
+
16
+ /* Empty state — shown in place of items when the query yields nothing. */
17
+ .rf-collection__empty {
18
+ color: var(--rf-color-muted);
19
+ font-size: 0.9375em;
20
+ }
21
+
22
+ .rf-collection__items {
23
+ display: flex;
24
+ flex-direction: column;
25
+ gap: var(--rf-spacing-sm);
26
+ }
27
+
28
+ /* grid arranges items into responsive columns; list stays a stacked flow.
29
+ * Item chrome (a card box, bare row, …) comes from the item, not the layout. */
30
+ .rf-collection[data-layout='grid'] .rf-collection__items {
31
+ display: grid;
32
+ grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
33
+ gap: var(--rf-spacing-md);
34
+ align-items: stretch;
35
+ }
36
+
37
+ /* Uniform heights per row: grid stretches each item to the tallest in its row;
38
+ * a body-template item then lets its card fill that stretched height. */
39
+ .rf-collection[data-layout='grid'] .rf-collection__item {
40
+ display: flex;
41
+ }
42
+ .rf-collection[data-layout='grid'] .rf-collection__item > * {
43
+ flex: 1;
44
+ min-width: 0;
45
+ }
46
+
47
+ /* Grouped grid: the grid belongs to each group (its items are the cells), not
48
+ * to __items — whose children are the group blocks (heading groups) or the
49
+ * accordion wrapper. Left on __items, each group becomes a single cell, i.e. a
50
+ * column per group. So when __items holds groups it stacks them, and each
51
+ * group becomes the grid instead, with the group title spanning every column. */
52
+ .rf-collection[data-layout='grid'] .rf-collection__items:has(.rf-collection__group),
53
+ .rf-collection[data-layout='grid'] .rf-collection__items:has(.rf-accordion) {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: var(--rf-spacing-md);
57
+ }
58
+ .rf-collection[data-layout='grid'] .rf-collection__group {
59
+ display: grid;
60
+ grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
61
+ gap: var(--rf-spacing-md);
62
+ align-items: stretch;
63
+ }
64
+ .rf-collection[data-layout='grid'] .rf-collection__group-title {
65
+ grid-column: 1 / -1;
66
+ }
67
+
68
+ .rf-collection__item {
69
+ min-width: 0;
70
+ }
71
+
72
+ .rf-collection__card {
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: var(--rf-spacing-xs);
76
+ padding: var(--rf-spacing-md);
77
+ border: 1px solid var(--rf-color-border);
78
+ border-radius: var(--rf-radius-md);
79
+ background: var(--rf-color-surface);
80
+ }
81
+
82
+ .rf-collection__card:hover {
83
+ background: var(--rf-color-surface-hover);
84
+ }
85
+
86
+ .rf-collection__title {
87
+ font-weight: 600;
88
+ color: var(--rf-color-text);
89
+ text-decoration: none;
90
+ }
91
+
92
+ .rf-collection__title:hover {
93
+ color: var(--rf-color-primary);
94
+ }
95
+
96
+ .rf-collection__field {
97
+ font-size: 0.875em;
98
+ color: var(--rf-color-muted);
99
+ }
100
+
101
+ /* grouping */
102
+ .rf-collection__group {
103
+ margin-top: var(--rf-spacing-md);
104
+ display: flex;
105
+ flex-direction: column;
106
+ /* Tight default for short item rows (the built-in title link, no body). */
107
+ gap: var(--rf-spacing-xs);
108
+ }
109
+ /* Modestly roomier when items carry a body template (block content — a card,
110
+ * callout, list, …), so they don't collide. Body-template items are marked
111
+ * `data-block` by the resolver; the built-in inline title rows are not.
112
+ * Mirrors the rf-relationships rule. */
113
+ .rf-collection__group:has(.rf-collection__item[data-block]) {
114
+ gap: var(--rf-spacing-sm);
115
+ }
116
+
117
+ /* Block-content item — let the body template (card / callout / etc.) fill the
118
+ * container instead of shrink-fitting. No-op in grid mode (items there are
119
+ * already `display: flex` with `flex: 1` on the child). Trim the outer margins
120
+ * of the template's first/last block so the group `gap` controls inter-item
121
+ * spacing — works for any block content, not a wrapper element. */
122
+ .rf-collection__item[data-block] {
123
+ display: block;
124
+ }
125
+ .rf-collection__item[data-block] > :first-child {
126
+ margin-top: 0;
127
+ }
128
+ .rf-collection__item[data-block] > :last-child {
129
+ margin-bottom: 0;
130
+ }
131
+
132
+ .rf-collection__group-title {
133
+ margin: 0 0 var(--rf-spacing-sm);
134
+ font-size: 0.875em;
135
+ text-transform: uppercase;
136
+ letter-spacing: 0.05em;
137
+ color: var(--rf-color-muted);
138
+ }
139
+
140
+ /* table layout */
141
+ .rf-collection__table {
142
+ width: 100%;
143
+ border-collapse: collapse;
144
+ }
145
+
146
+ .rf-collection__table th,
147
+ .rf-collection__table td {
148
+ padding: var(--rf-spacing-sm);
149
+ text-align: left;
150
+ border-bottom: 1px solid var(--rf-color-border);
151
+ }
152
+
153
+ .rf-collection__table th {
154
+ font-size: 0.875em;
155
+ text-transform: uppercase;
156
+ letter-spacing: 0.05em;
157
+ color: var(--rf-color-muted);
158
+ }
159
+
160
+ /* Heading-template cells (`layout="table"` body) wrap their content as
161
+ * markdown paragraphs, which pick up the global `p { margin-bottom: 1rem }`
162
+ * and leave dead space inside each row. Drop it inside the table — table
163
+ * cells don't want paragraph spacing. */
164
+ .rf-collection__table td p {
165
+ margin: 0;
166
+ }
@@ -47,7 +47,12 @@
47
47
  }
48
48
  .rf-conversation-message__body p { margin: 0; }
49
49
  .rf-conversation-message__body p + p { margin-top: 0.5rem; }
50
- .rf-conversation-message__body > span[property],
50
+ /* The speaker name lives inline-bold inside the bubble (the explicit form
51
+ * authors it that way, and the named-speakers form injects it the same way),
52
+ * so the property-carrier span is data only — hide it. Same for the meta
53
+ * tags the engine consumes. `data-field` is the post-engine attribute set by
54
+ * createComponentRenderable. */
55
+ .rf-conversation-message > span[data-field="speaker"],
56
+ .rf-conversation-message > meta[data-field] { display: none; }
57
+ .rf-conversation-message__body > span[data-field],
51
58
  .rf-conversation-message__body > meta { display: none; }
52
- .rf-conversation-message > span[property="speaker"],
53
- .rf-conversation-message > meta[property] { display: none; }
@@ -0,0 +1,316 @@
1
+ /* Drawer — addressable modal panel (SPEC-060).
2
+ *
3
+ * Two visual modes ride on the same `.rf-drawer` element:
4
+ *
5
+ * 1. **No-JS (in-flow)** — `.rf-drawer` not inside a `<dialog>`. Renders
6
+ * as a styled callout block at its authored position, visually
7
+ * distinct so the reader understands "set-aside material". Close
8
+ * button stays `hidden` (the schema marks it that way; behaviors
9
+ * reveals it post-enhancement).
10
+ * 2. **JS (panel)** — `dialog.rf-drawer[open]`. Floats slightly off the
11
+ * viewport edge (margin on all four sides) so it reads as a "card
12
+ * that slid in" rather than a flush slab. Size modifier sets width
13
+ * (right/left) or height (top/bottom). Slide-in animation uses
14
+ * `@starting-style` to interpolate from off-screen on open.
15
+ *
16
+ * Body-scroll lock is applied as a class on `<html>` by the behavior
17
+ * (`html.rf-drawer-open` → `overflow: hidden`) so mobile Safari doesn't
18
+ * scroll the page underneath the open dialog.
19
+ */
20
+
21
+ .rf-drawer {
22
+ --rf-drawer-size-sm: 22rem;
23
+ --rf-drawer-size-md: 36rem;
24
+ --rf-drawer-size-lg: 52rem;
25
+ --rf-drawer-bg: var(--rf-color-surface);
26
+ --rf-drawer-fg: var(--rf-color-text);
27
+ --rf-drawer-border: var(--rf-color-border);
28
+ --rf-drawer-shadow: 0 18px 48px -12px rgb(0 0 0 / 0.35);
29
+ --rf-drawer-gutter: var(--rf-spacing-sm);
30
+ --rf-drawer-radius: var(--rf-radius-md);
31
+ --rf-drawer-anim-duration: 220ms;
32
+ --rf-drawer-anim-ease: cubic-bezier(0.32, 0.72, 0, 1);
33
+ }
34
+
35
+ /* ─── Body-scroll lock while any drawer is open ────────────────── */
36
+
37
+ html.rf-drawer-open,
38
+ html.rf-drawer-open body {
39
+ overflow: hidden;
40
+ }
41
+
42
+ /* ─── No-JS / in-flow mode ─────────────────────────────────────── */
43
+
44
+ section.rf-drawer {
45
+ margin: var(--rf-spacing-lg) 0;
46
+ padding: var(--rf-spacing-md) var(--rf-spacing-lg);
47
+ background: var(--rf-drawer-bg);
48
+ color: var(--rf-drawer-fg);
49
+ border: 1px solid var(--rf-drawer-border);
50
+ border-left: 4px solid var(--rf-color-primary);
51
+ border-radius: var(--rf-drawer-radius);
52
+ }
53
+
54
+ section.rf-drawer .rf-drawer__header {
55
+ display: flex;
56
+ align-items: baseline;
57
+ justify-content: space-between;
58
+ gap: var(--rf-spacing-sm);
59
+ margin-bottom: var(--rf-spacing-sm);
60
+ }
61
+
62
+ section.rf-drawer .rf-drawer__title {
63
+ margin: 0;
64
+ font-size: 1.05rem;
65
+ font-weight: 600;
66
+ color: var(--rf-color-primary);
67
+ letter-spacing: 0.01em;
68
+ }
69
+
70
+ section.rf-drawer .rf-drawer__body > :first-child { margin-top: 0; }
71
+ section.rf-drawer .rf-drawer__body > :last-child { margin-bottom: 0; }
72
+
73
+ /* ─── JS / dialog mode ─────────────────────────────────────────── */
74
+
75
+ dialog.rf-drawer {
76
+ box-sizing: border-box;
77
+ background: var(--rf-drawer-bg);
78
+ color: var(--rf-drawer-fg);
79
+ border: 1px solid var(--rf-drawer-border);
80
+ border-radius: var(--rf-drawer-radius);
81
+ box-shadow: var(--rf-drawer-shadow);
82
+ padding: 0;
83
+ margin: 0;
84
+ overflow: auto;
85
+ max-width: calc(100vw - 2 * var(--rf-drawer-gutter));
86
+ max-height: calc(100vh - 2 * var(--rf-drawer-gutter));
87
+ }
88
+
89
+ dialog.rf-drawer::backdrop {
90
+ background: rgb(0 0 0 / 0.35);
91
+ backdrop-filter: blur(2px);
92
+ }
93
+
94
+ dialog.rf-drawer .rf-drawer__header {
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: space-between;
98
+ gap: var(--rf-spacing-md);
99
+ padding: var(--rf-spacing-md) var(--rf-spacing-lg);
100
+ border-bottom: 1px solid var(--rf-drawer-border);
101
+ position: sticky;
102
+ top: 0;
103
+ background: var(--rf-drawer-bg);
104
+ z-index: 1;
105
+ }dialog.rf-drawer .rf-drawer__title {
106
+ margin: 0;
107
+ font-size: 1.05rem;
108
+ font-weight: 600;
109
+ }
110
+
111
+ dialog.rf-drawer .rf-drawer__close {
112
+ appearance: none;
113
+ background: transparent;
114
+ border: 0;
115
+ cursor: pointer;
116
+ color: var(--rf-color-muted);
117
+ padding: var(--rf-spacing-xs) var(--rf-spacing-sm);
118
+ font-size: 1.25rem;
119
+ line-height: 1;
120
+ border-radius: var(--rf-radius-sm);
121
+ flex-shrink: 0;
122
+ }
123
+ dialog.rf-drawer .rf-drawer__close:hover {
124
+ color: var(--rf-color-text);
125
+ background: var(--rf-color-surface-hover);
126
+ }
127
+
128
+ dialog.rf-drawer .rf-drawer__body {
129
+ padding: var(--rf-spacing-md) var(--rf-spacing-lg);
130
+ }
131
+
132
+ /* Collapse the vertical margins of first / last children inside the body
133
+ * so the drawer's own padding is what spaces content from the chrome —
134
+ * otherwise a `<figure>` or `<p>` with its own top/bottom margin stacks
135
+ * on top of the body's padding and the content looks adrift. A snippet
136
+ * inside a drawer compiles to `body > .rf-codeblock > .rf-snippet > <pre>`,
137
+ * so the margin-zero walk reaches two levels deep to catch the figure. */
138
+ dialog.rf-drawer .rf-drawer__body > :first-child,
139
+ dialog.rf-drawer .rf-drawer__body > :first-child > :first-child {
140
+ margin-top: 0;
141
+ }
142
+ dialog.rf-drawer .rf-drawer__body > :last-child,
143
+ dialog.rf-drawer .rf-drawer__body > :last-child > :last-child {
144
+ margin-bottom: 0;
145
+ }
146
+
147
+ /* ─── Side modifiers ───────────────────────────────────────────────
148
+ *
149
+ * Each side anchors the dialog with `inset`. The slide-in animation is
150
+ * a keyframes animation triggered by `[open]` — picked over
151
+ * `@starting-style` because keyframes have broader browser support
152
+ * (everywhere `<dialog>` works) and a clearer source-of-truth: the
153
+ * starting/ending frames are spelled out, no cascade-interaction
154
+ * footguns. The matching backdrop fade is a separate animation.
155
+ */
156
+
157
+ dialog.rf-drawer[data-side="right"] {
158
+ inset:
159
+ var(--rf-drawer-gutter)
160
+ var(--rf-drawer-gutter)
161
+ var(--rf-drawer-gutter)
162
+ auto;
163
+ height: auto;
164
+ }
165
+ dialog.rf-drawer[data-side="right"][open]:not([data-state="closing"]) {
166
+ animation: rf-drawer-slide-in-right var(--rf-drawer-anim-duration) var(--rf-drawer-anim-ease);
167
+ }
168
+ dialog.rf-drawer[data-side="right"][data-state="closing"] {
169
+ animation: rf-drawer-slide-out-right var(--rf-drawer-anim-duration) var(--rf-drawer-anim-ease) forwards;
170
+ }
171
+ dialog.rf-drawer[data-side="left"] {
172
+ inset:
173
+ var(--rf-drawer-gutter)
174
+ auto
175
+ var(--rf-drawer-gutter)
176
+ var(--rf-drawer-gutter);
177
+ height: auto;
178
+ }
179
+ dialog.rf-drawer[data-side="left"][open]:not([data-state="closing"]) {
180
+ animation: rf-drawer-slide-in-left var(--rf-drawer-anim-duration) var(--rf-drawer-anim-ease);
181
+ }
182
+ dialog.rf-drawer[data-side="left"][data-state="closing"] {
183
+ animation: rf-drawer-slide-out-left var(--rf-drawer-anim-duration) var(--rf-drawer-anim-ease) forwards;
184
+ }
185
+ dialog.rf-drawer[data-side="top"] {
186
+ inset:
187
+ var(--rf-drawer-gutter)
188
+ var(--rf-drawer-gutter)
189
+ auto
190
+ var(--rf-drawer-gutter);
191
+ width: auto;
192
+ }
193
+ dialog.rf-drawer[data-side="top"][open]:not([data-state="closing"]) {
194
+ animation: rf-drawer-slide-in-top var(--rf-drawer-anim-duration) var(--rf-drawer-anim-ease);
195
+ }
196
+ dialog.rf-drawer[data-side="top"][data-state="closing"] {
197
+ animation: rf-drawer-slide-out-top var(--rf-drawer-anim-duration) var(--rf-drawer-anim-ease) forwards;
198
+ }
199
+ dialog.rf-drawer[data-side="bottom"] {
200
+ inset:
201
+ auto
202
+ var(--rf-drawer-gutter)
203
+ var(--rf-drawer-gutter)
204
+ var(--rf-drawer-gutter);
205
+ width: auto;
206
+ }
207
+ dialog.rf-drawer[data-side="bottom"][open]:not([data-state="closing"]) {
208
+ animation: rf-drawer-slide-in-bottom var(--rf-drawer-anim-duration) var(--rf-drawer-anim-ease);
209
+ }
210
+ dialog.rf-drawer[data-side="bottom"][data-state="closing"] {
211
+ animation: rf-drawer-slide-out-bottom var(--rf-drawer-anim-duration) var(--rf-drawer-anim-ease) forwards;
212
+ }
213
+
214
+ dialog.rf-drawer[open]:not([data-state="closing"])::backdrop {
215
+ animation: rf-drawer-backdrop-in var(--rf-drawer-anim-duration) var(--rf-drawer-anim-ease);
216
+ }
217
+ dialog.rf-drawer[data-state="closing"]::backdrop {
218
+ animation: rf-drawer-backdrop-out var(--rf-drawer-anim-duration) var(--rf-drawer-anim-ease) forwards;
219
+ }
220
+
221
+ @keyframes rf-drawer-slide-in-right {
222
+ from { transform: translateX(calc(100% + var(--rf-drawer-gutter))); opacity: 0; }
223
+ to { transform: translateX(0); opacity: 1; }
224
+ }
225
+ @keyframes rf-drawer-slide-in-left {
226
+ from { transform: translateX(calc(-100% - var(--rf-drawer-gutter))); opacity: 0; }
227
+ to { transform: translateX(0); opacity: 1; }
228
+ }
229
+ @keyframes rf-drawer-slide-in-top {
230
+ from { transform: translateY(calc(-100% - var(--rf-drawer-gutter))); opacity: 0; }
231
+ to { transform: translateY(0); opacity: 1; }
232
+ }
233
+ @keyframes rf-drawer-slide-in-bottom {
234
+ from { transform: translateY(calc(100% + var(--rf-drawer-gutter))); opacity: 0; }
235
+ to { transform: translateY(0); opacity: 1; }
236
+ }
237
+ @keyframes rf-drawer-backdrop-in {
238
+ from { background: rgb(0 0 0 / 0); backdrop-filter: blur(0); }
239
+ to { background: rgb(0 0 0 / 0.35); backdrop-filter: blur(2px); }
240
+ }
241
+
242
+ @keyframes rf-drawer-slide-out-right {
243
+ from { transform: translateX(0); opacity: 1; }
244
+ to { transform: translateX(calc(100% + var(--rf-drawer-gutter))); opacity: 0; }
245
+ }
246
+ @keyframes rf-drawer-slide-out-left {
247
+ from { transform: translateX(0); opacity: 1; }
248
+ to { transform: translateX(calc(-100% - var(--rf-drawer-gutter))); opacity: 0; }
249
+ }
250
+ @keyframes rf-drawer-slide-out-top {
251
+ from { transform: translateY(0); opacity: 1; }
252
+ to { transform: translateY(calc(-100% - var(--rf-drawer-gutter))); opacity: 0; }
253
+ }
254
+ @keyframes rf-drawer-slide-out-bottom {
255
+ from { transform: translateY(0); opacity: 1; }
256
+ to { transform: translateY(calc(100% + var(--rf-drawer-gutter))); opacity: 0; }
257
+ }
258
+ @keyframes rf-drawer-backdrop-out {
259
+ from { background: rgb(0 0 0 / 0.35); backdrop-filter: blur(2px); }
260
+ to { background: rgb(0 0 0 / 0); backdrop-filter: blur(0); }
261
+ }
262
+
263
+ @media (prefers-reduced-motion: reduce) {
264
+ dialog.rf-drawer[data-side="right"][open],
265
+ dialog.rf-drawer[data-side="left"][open],
266
+ dialog.rf-drawer[data-side="top"][open],
267
+ dialog.rf-drawer[data-side="bottom"][open],
268
+ dialog.rf-drawer[data-state="closing"],
269
+ dialog.rf-drawer[open]::backdrop,
270
+ dialog.rf-drawer[data-state="closing"]::backdrop {
271
+ animation: none;
272
+ }
273
+ }
274
+
275
+ /* ─── Mobile tightening ────────────────────────────────────────────
276
+ *
277
+ * On narrow viewports the drawer fills the screen (capped by
278
+ * `max-width: 100vw - 2*gutter`), so the desktop-comfortable
279
+ * 1.5rem / 2rem padding starts crowding the content. Halve it
280
+ * roughly to 0.75rem / 1rem so the body has more room without
281
+ * losing the chrome's breathing space.
282
+ */
283
+ @media (max-width: 640px) {
284
+ dialog.rf-drawer .rf-drawer__header,
285
+ dialog.rf-drawer .rf-drawer__body {
286
+ padding: 0.75rem 1rem;
287
+ }
288
+ }
289
+
290
+ /* ─── Size modifiers (only meaningful in dialog mode) ─────────── */
291
+
292
+ dialog.rf-drawer[data-side="right"][data-size="sm"],
293
+ dialog.rf-drawer[data-side="left"][data-size="sm"] {
294
+ width: var(--rf-drawer-size-sm);
295
+ }
296
+ dialog.rf-drawer[data-side="right"][data-size="md"],
297
+ dialog.rf-drawer[data-side="left"][data-size="md"] {
298
+ width: var(--rf-drawer-size-md);
299
+ }
300
+ dialog.rf-drawer[data-side="right"][data-size="lg"],
301
+ dialog.rf-drawer[data-side="left"][data-size="lg"] {
302
+ width: var(--rf-drawer-size-lg);
303
+ }
304
+
305
+ dialog.rf-drawer[data-side="top"][data-size="sm"],
306
+ dialog.rf-drawer[data-side="bottom"][data-size="sm"] {
307
+ height: var(--rf-drawer-size-sm);
308
+ }
309
+ dialog.rf-drawer[data-side="top"][data-size="md"],
310
+ dialog.rf-drawer[data-side="bottom"][data-size="md"] {
311
+ height: var(--rf-drawer-size-md);
312
+ }
313
+ dialog.rf-drawer[data-side="top"][data-size="lg"],
314
+ dialog.rf-drawer[data-side="bottom"][data-size="lg"] {
315
+ height: var(--rf-drawer-size-lg);
316
+ }
@@ -0,0 +1,74 @@
1
+ /* Expand — inline-substitution of a registered entity (SPEC-066).
2
+ *
3
+ * The wrapper is intentionally minimal — the embedded rune (plan rune,
4
+ * character rune, etc.) renders itself standalone-style; expand just
5
+ * provides the wrapper + canonical-link affordance. Themes that want
6
+ * the embedded content visually distinguished from inline-authored
7
+ * content target `.rf-expand .rf-spec`, `.rf-expand h1`, etc.
8
+ *
9
+ * Two visual treatments:
10
+ *
11
+ * 1. **Default (`level=` unset, peer-document mode)** — the embed reads
12
+ * like a quoted document. A subtle border + indent treatment marks
13
+ * it as set-aside material. Heading scale is dialed down slightly
14
+ * so an H1 inside the embed doesn't compete with the host's H1.
15
+ * 2. **Sub-section mode (`level=` set)** — the embed merges into the
16
+ * host outline. The wrapper sheds the quoted treatment so it reads
17
+ * as authored sub-content. Authors opt into this by setting
18
+ * `level=`; CSS targets `.rf-expand:not([data-outline-scope])` to
19
+ * pick the right treatment automatically.
20
+ */
21
+
22
+ .rf-expand {
23
+ margin: var(--rf-spacing-md) 0;
24
+ }
25
+
26
+ /* Peer-document mode — subtle top + bottom rule, no background, no
27
+ * rounded corners. Reads as "set-aside material" without competing
28
+ * visually with the host content. Authors who want a stronger
29
+ * treatment can override `.rf-expand[data-outline-scope]` in their
30
+ * theme. */
31
+ .rf-expand[data-outline-scope] {
32
+ padding: var(--rf-spacing-md) 0;
33
+ border-top: 1px solid var(--rf-color-border);
34
+ border-bottom: 1px solid var(--rf-color-border);
35
+ }
36
+
37
+ /* Tone down embedded headings so the host outline keeps visual primacy.
38
+ * The semantic levels are preserved (H1 stays H1 etc.); only the visual
39
+ * scale is adjusted in the peer-document treatment. Authors using
40
+ * `level=` for sub-section mode get the normal heading scale. */
41
+ .rf-expand[data-outline-scope] :is(h1, h2, h3, h4, h5, h6):first-child {
42
+ margin-top: 0;
43
+ }
44
+ .rf-expand[data-outline-scope] h1 { font-size: 1.5rem; }
45
+ .rf-expand[data-outline-scope] h2 { font-size: 1.25rem; }
46
+ .rf-expand[data-outline-scope] h3 { font-size: 1.1rem; }
47
+
48
+ .rf-expand > :first-child { margin-top: 0; }
49
+ .rf-expand > :last-child { margin-bottom: 0; }
50
+
51
+ .rf-expand__canonical-link {
52
+ display: inline-block;
53
+ margin-top: var(--rf-spacing-sm);
54
+ font-size: 0.875rem;
55
+ color: var(--rf-color-primary);
56
+ text-decoration: none;
57
+ }
58
+ .rf-expand__canonical-link:hover {
59
+ text-decoration: underline;
60
+ }
61
+
62
+ /* Error-state placeholder rendered when resolution fails. The build
63
+ * also surfaces the error through `ctx.error`; this is the user-facing
64
+ * affordance so the failure isn't silent. Keyed off `[data-expand-error]`
65
+ * which the resolver sets on the error wrapper. */
66
+ .rf-expand[data-expand-error] {
67
+ padding: var(--rf-spacing-sm) var(--rf-spacing-md);
68
+ border-left: 3px solid var(--rf-color-danger);
69
+ background: var(--rf-color-danger-bg);
70
+ color: var(--rf-color-danger);
71
+ font-family: var(--rf-font-mono);
72
+ font-size: 0.875rem;
73
+ border-radius: 0 var(--rf-radius-sm) var(--rf-radius-sm) 0;
74
+ }
@@ -67,6 +67,21 @@
67
67
  color: var(--rf-color-muted);
68
68
  }
69
69
 
70
+ /* Items with an inline badge (recognised by the nav schema and tagged with
71
+ * data-name="badge") sit on one row so the badge rides alongside the link
72
+ * instead of wrapping to the next line — the link's `display: block` would
73
+ * otherwise eat the full row. */
74
+ .rf-nav-item:has(> .rf-badge[data-name="badge"]) {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 0.375rem;
78
+ padding: 0;
79
+ }
80
+ .rf-nav-item:has(> .rf-badge[data-name="badge"]) > .rf-nav-item__link {
81
+ flex: 0 1 auto;
82
+ min-width: 0;
83
+ }
84
+
70
85
  /* ─── Collapsible groups (vertical sidebar) ───────────────────────────── */
71
86
 
72
87
  .rf-nav--collapsible .rf-nav-group h2,
@@ -0,0 +1,49 @@
1
+ /* progress — generic completion bar (SPEC-072 / WORK-285).
2
+ *
3
+ * Presentational: the fill width is driven by the `--rf-progress` custom
4
+ * property set on the root from the computed percent. Generalized from the
5
+ * former plan milestone progress bar.
6
+ */
7
+
8
+ .rf-progress {
9
+ display: flex;
10
+ flex-wrap: wrap;
11
+ align-items: baseline;
12
+ gap: var(--rf-spacing-xs) var(--rf-spacing-sm);
13
+ margin: var(--rf-spacing-sm) 0;
14
+ }
15
+
16
+ .rf-progress__label {
17
+ font-weight: 600;
18
+ color: var(--rf-color-text);
19
+ }
20
+
21
+ .rf-progress__value {
22
+ font-weight: 600;
23
+ color: var(--rf-color-text);
24
+ margin-left: auto;
25
+ }
26
+
27
+ .rf-progress__track {
28
+ flex-basis: 100%;
29
+ height: 0.5rem;
30
+ border-radius: var(--rf-radius-pill, 999px);
31
+ background: var(--rf-color-surface-hover);
32
+ overflow: hidden;
33
+ }
34
+
35
+ .rf-progress__fill {
36
+ display: block;
37
+ height: 100%;
38
+ width: var(--rf-progress, 0%);
39
+ border-radius: inherit;
40
+ background: var(--rf-color-primary);
41
+ transition: width 0.3s ease;
42
+ }
43
+
44
+ /* Sentiment variants tint the fill; the neutral default uses the primary.
45
+ * Token names follow the sentiment scheme in dimensions/metadata.css
46
+ * (positive→success, caution→warning, negative→danger). */
47
+ .rf-progress--positive .rf-progress__fill { background: var(--rf-color-success); }
48
+ .rf-progress--caution .rf-progress__fill { background: var(--rf-color-warning); }
49
+ .rf-progress--negative .rf-progress__fill { background: var(--rf-color-danger); }