@redseed/redseed-ui-tailwindcss 7.57.0 → 7.59.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.
@@ -11,7 +11,7 @@
11
11
  }
12
12
 
13
13
  .rsui-button-card__content {
14
- @apply flex items-center p-2 gap-4;
14
+ @apply flex items-center p-space-xs gap-space-md;
15
15
  }
16
16
 
17
17
  .rsui-button-card__icon {
@@ -27,8 +27,10 @@
27
27
  @apply flex flex-col;
28
28
  }
29
29
 
30
+ /* Brand Header 4, matching .rsui-card-header__title. This set no size at all, so it
31
+ inherited the 16px body and the same title role rendered a step smaller here. */
30
32
  .rsui-button-card__title {
31
- @apply text-text-primary font-semibold line-clamp-1;
33
+ @apply text-xl leading-[1.3] text-text-primary font-semibold line-clamp-1;
32
34
  @apply group-hover/button-card:text-text-primary;
33
35
  }
34
36
 
@@ -120,4 +120,127 @@
120
120
  /* modifier 2xl size with icon */
121
121
  .rsui-button-slot--2xl-icon {
122
122
  @apply p-4.5;
123
- }
123
+ }
124
+ /* ---------------------------------------------------------------------------
125
+ MODIFIER: on-color — a button sitting on a coloured or dark surface.
126
+ ---------------------------------------------------------------------------
127
+ Every variant is drawn for a light ground and has no correct rendering on a
128
+ dark one: secondary's white fill disappears into it, tertiary's ink goes
129
+ near-invisible. This is the inverse ladder, same emphasis order as the light
130
+ one — filled is loudest, then outlined, then text alone.
131
+
132
+ Deliberately NOT named `invert`, which Primary and Danger already use for
133
+ something else: a transparent fill with brand ink, an outline treatment for a
134
+ LIGHT surface. `onColor` matches BodyText and StageDial. The two compose.
135
+
136
+ Each rule below is a COMPOUND selector (two classes), so it beats the
137
+ single-class variant rules whatever order components.css imports them in —
138
+ see the note at the top of that file about import order mattering.
139
+
140
+ Contrast, measured against the four tonal surfaces that exist today
141
+ (Journey Green 800 and Deep Teal 700 from PageHeader, Journey Green 700 from
142
+ MetricCard, and the AI panel's lightest Warm Mauve stop):
143
+
144
+ white label ink 8.31:1 worst needs 4.5:1
145
+ outlined border 0.55 3.71:1 worst needs 3:1 (0.40 measured 2.68 and failed)
146
+ filled default ink 17.07:1 Grey 900 on white
147
+ --------------------------------------------------------------------------- */
148
+ /*
149
+ THE INK for the filled treatment comes from the SURFACE, because the button
150
+ cannot know what it is sitting on: the AI panel wants Warm Mauve 950, a Journey
151
+ Green band wants its own dark green. Same mechanism as --rsui-page-header-* and
152
+ --rsui-metric-card-*.
153
+
154
+ The fallback lives INSIDE the var() call, not as a declaration on this class.
155
+ Declaring `--rsui-button-on-color-ink: <fallback>` here looks equivalent and is
156
+ not: a declaration on the element beats an inherited one, so the surface's value
157
+ would never win and every button would take the fallback. Measured that exact
158
+ bug before fixing it — the ink came back Grey 900 on all three surfaces.
159
+ */
160
+
161
+ /*
162
+ The base slot's focus ring is built for a light ground, and its offset paints
163
+ white — on a dark surface that reads as one thick white band rather than a ring
164
+ with a gap. A plain outline is simpler than fighting the ring utilities, and
165
+ white reads on every tonal surface.
166
+ */
167
+ .rsui-button-slot--on-color:focus-visible {
168
+ @apply ring-0;
169
+
170
+ outline: 2px solid var(--color-text-on-color);
171
+ outline-offset: 2px;
172
+ }
173
+
174
+ /* --- filled: the loudest thing a dark surface can carry ------------------- */
175
+ .rsui-button-slot--on-color.rsui-button-primary {
176
+ background: var(--color-text-on-color);
177
+ border-color: transparent;
178
+ color: var(--rsui-button-on-color-ink, var(--color-text-primary));
179
+ }
180
+
181
+ .rsui-button-slot--on-color.rsui-button-primary:hover {
182
+ background: var(--Colors-Grey-100);
183
+ border-color: transparent;
184
+ color: var(--rsui-button-on-color-ink, var(--color-text-primary));
185
+ }
186
+
187
+ .rsui-button-slot--on-color.rsui-button-primary:active {
188
+ background: var(--Colors-Grey-200);
189
+ border-color: transparent;
190
+ }
191
+
192
+ .rsui-button-slot--on-color.rsui-button-primary:disabled,
193
+ .rsui-button-slot--on-color.rsui-button-primary:disabled:active {
194
+ background: rgba(255, 255, 255, 0.24);
195
+ border-color: transparent;
196
+ color: rgba(255, 255, 255, 0.6);
197
+ }
198
+
199
+ /* --- outlined ------------------------------------------------------------- */
200
+ .rsui-button-slot--on-color.rsui-button-secondary {
201
+ background: transparent;
202
+ /* 0.55, not lower: see the measurements above. */
203
+ border-color: rgba(255, 255, 255, 0.55);
204
+ color: var(--color-text-on-color);
205
+ }
206
+
207
+ .rsui-button-slot--on-color.rsui-button-secondary:hover {
208
+ background: rgba(255, 255, 255, 0.12);
209
+ border-color: var(--color-text-on-color);
210
+ color: var(--color-text-on-color);
211
+ }
212
+
213
+ .rsui-button-slot--on-color.rsui-button-secondary:active {
214
+ background: rgba(255, 255, 255, 0.2);
215
+ }
216
+
217
+ .rsui-button-slot--on-color.rsui-button-secondary:disabled,
218
+ .rsui-button-slot--on-color.rsui-button-secondary:disabled:active {
219
+ background: transparent;
220
+ border-color: rgba(255, 255, 255, 0.2);
221
+ color: rgba(255, 255, 255, 0.45);
222
+ }
223
+
224
+ /* --- text alone ----------------------------------------------------------- */
225
+ .rsui-button-slot--on-color.rsui-button-tertiary {
226
+ background: transparent;
227
+ border-color: transparent;
228
+ color: var(--color-text-on-color);
229
+ }
230
+
231
+ .rsui-button-slot--on-color.rsui-button-tertiary:hover {
232
+ background: rgba(255, 255, 255, 0.12);
233
+ border-color: transparent;
234
+ color: var(--color-text-on-color);
235
+ }
236
+
237
+ .rsui-button-slot--on-color.rsui-button-tertiary:active {
238
+ background: rgba(255, 255, 255, 0.2);
239
+ }
240
+
241
+ .rsui-button-slot--on-color.rsui-button-tertiary:disabled,
242
+ .rsui-button-slot--on-color.rsui-button-tertiary:disabled:active {
243
+ background: transparent;
244
+ border-color: transparent;
245
+ color: rgba(255, 255, 255, 0.45);
246
+ }
@@ -12,6 +12,58 @@
12
12
  @apply border;
13
13
  }
14
14
 
15
+ /* ---------------------------------------------------------------------------
16
+ THE AI FAMILY AT FULL STRENGTH — a bold dark surface, not the pale lilac wash
17
+ that `ai` gives. Every other Card variant is a light tint (brand is Brand-25),
18
+ so this is the first card that has to carry light type, and it is the only one
19
+ that has to adapt anything about its children.
20
+
21
+ THREE stops, and the foot is deliberately not a Warm Mauve value. Two stops one
22
+ ramp-step apart (900 -> 950) gave a lightness travel of 0.0413, which read as
23
+ almost flat. The light end cannot move: at Warm Mauve 800 the muted ink measures
24
+ 4.39:1 and fails. So the travel has to come from the foot, and Grey 950 nearly
25
+ doubles it — 0.0774, 1.9x — while leaving the lightest stop, and therefore every
26
+ contrast figure below, completely unchanged.
27
+
28
+ Warm Mauve 950 stays as a mid stop at 65% so the card reads plum through most of
29
+ its height and only the last third deepens, rather than fading to black throughout.
30
+
31
+ Contrast on the LIGHTEST stop, Warm Mauve 900, which is the worst case and is not
32
+ affected by the foot:
33
+ white title 7.90:1 needs 4.5:1
34
+ Warm Mauve 200 muted 6.05:1 needs 4.5:1
35
+ --------------------------------------------------------------------------- */
36
+ .rsui-card--ai-bold {
37
+ @apply bg-linear-to-br from-background-ai-bold-from via-background-ai-bold-via via-65% to-background-ai-bold-to;
38
+ @apply text-text-on-color;
39
+ }
40
+
41
+ /* Compound, matching how every other variant sets its border. Transparent rather
42
+ than a light rule: a bordered dark card wants its own edge, not a hairline. */
43
+ .rsui-card--bordered.rsui-card--ai-bold {
44
+ @apply border-transparent;
45
+ }
46
+
47
+ /* The hover scrim is a DARK tint at 5% — correct on a light card, wrong here,
48
+ where it would make hover darker instead of lighter. Flipped to white. */
49
+ .rsui-card--hoverable.rsui-card--ai-bold::before {
50
+ @apply bg-background-primary;
51
+ }
52
+
53
+ /* CardHeader sets its own dark ink (text-text-primary on the title,
54
+ text-text-secondary on the subtitle), which disappears on this surface. Reaching
55
+ into another component's classes is not ideal, but an invisible card header is
56
+ worse, and what a slot looks like on this card's own surface is this
57
+ stylesheet's business. Same reach PageHeader's tone makes into MetaInfo. */
58
+ .rsui-card--ai-bold .rsui-card-header__title {
59
+ @apply text-text-on-color;
60
+ }
61
+
62
+ .rsui-card--ai-bold .rsui-card-header__subtitle,
63
+ .rsui-card--ai-bold .rsui-card-header__eyebrow {
64
+ color: var(--Colors-Warm-Mauve-200);
65
+ }
66
+
15
67
  .rsui-card--secondary {
16
68
  @apply bg-background-secondary;
17
69
  }
@@ -94,8 +146,9 @@
94
146
  @apply bg-background-primary-contrast opacity-0 transition-opacity duration-200;
95
147
  }
96
148
 
149
+ /* 8% is Material 3's hover state-layer opacity; this was 5%. */
97
150
  .rsui-card--hoverable:hover::before {
98
- @apply opacity-5;
151
+ @apply opacity-[0.08];
99
152
  }
100
153
 
101
154
  .rsui-card--clickable {
@@ -132,54 +185,129 @@
132
185
  @apply sr-only;
133
186
  }
134
187
 
188
+ /* ── Media height cap ─────────────────────────────────────────────────────────
189
+ The ratio components (Image16By9, Image64by25, Image3By1, Image9By16) each fix an
190
+ aspect-ratio at author time, so the media's height is derived from the card's
191
+ width — while the text block beneath it stays roughly constant. That makes the
192
+ image's share of the card a function of whatever width the card lands on, which
193
+ the author does not control once the card is in a responsive grid. Measured, for
194
+ Image64by25 against a ~96px text block:
195
+
196
+ card width media height share of the card
197
+ 170px (2-col, phone) 66px 41%
198
+ 365px (2-col @800px) 143px 60%
199
+ 1246px (full-width desktop) 487px 84%
200
+
201
+ Same component, 41% to 84%. Image9By16 reaches 2215px tall on that last one.
202
+
203
+ The cap stops the wide end running away without touching the narrow end at all:
204
+ below ~427px of card width a 16:9 image is under 240px and nothing changes.
205
+
206
+ max-h-full on the child is required, not decorative. The ratio box is the child,
207
+ and aspect-ratio yields to max-height — without it the box keeps its full ratio
208
+ height and simply overflows the cap, so the image crops from the bottom instead
209
+ of shrinking. With it the box shrinks and the img's object-cover crops centrally.
210
+
211
+ --rsui-card-image-max-h is the escape hatch: a card that genuinely wants a tall
212
+ portrait image can raise or unset it without an API change. */
135
213
  .rsui-card__image {
136
214
  @apply rounded-t-xl overflow-hidden w-full relative;
137
215
  }
138
216
 
217
+ /* The cap goes on the RATIO BOX, not on the slot above, and it has to be an absolute
218
+ length. A percentage (max-h-full) does not clamp an aspect-ratio box: the box keeps
219
+ its full ratio-derived height and simply overflows the slot, so the image gets
220
+ clipped from the bottom — measured at 495px inside a 240px slot, losing the bottom
221
+ half of the photo. An absolute max-height makes the ratio yield, the box shrinks,
222
+ and .rsui-image's object-cover crops from the centre instead. */
223
+ .rsui-card__image > * {
224
+ /* w-full is load-bearing. With only max-height, aspect-ratio holds the ratio by
225
+ shrinking the WIDTH instead — measured 240x614 inside a 1266px slot, with the
226
+ rest of the card left blank. Pinning the width makes the ratio yield on the
227
+ height axis, which is the axis being capped. */
228
+ @apply w-full;
229
+ max-height: var(--rsui-card-image-max-h, 15rem); /* 240px */
230
+ }
231
+
139
232
  .rsui-card__image__badge-container {
140
- @apply absolute inset-x-0 w-full bottom-0 py-space-lg px-space-xl flex gap-space-xxs;
233
+ @apply absolute inset-x-0 w-full bottom-0 py-space-lg flex gap-space-xs;
141
234
  }
142
235
 
143
236
  .rsui-card__content {
144
- @apply flex-1 flex flex-col relative gap-y-space-sm;
237
+ @apply flex-1 flex flex-col relative gap-y-space-md;
145
238
  }
146
239
 
147
240
  .rsui-card__content--padded {
148
- @apply py-space-lg px-space-xl min-h-16;
241
+ @apply py-space-lg min-h-16;
149
242
  }
150
243
 
151
- .rsui-card__meta {
152
- @apply max-w-full overflow-x-auto grid grid-cols-1 gap-x-space-lg gap-y-space-xs;
153
- }
244
+ /* The space between the header block and the body is an INTERNAL gap, so it takes the
245
+ 16px "between blocks" value rather than the card's 24px padding. The 24px still holds
246
+ where it belongs — the top of the header, the bottom of the body, and both sides — so
247
+ the card is padded 24 all round and its two blocks sit 16 apart inside that.
154
248
 
155
- .rsui-card--2xs .rsui-card__meta {
156
- @apply grid-cols-2;
249
+ It read as too much because the body's own padding-top was doing two jobs at once:
250
+ card padding when nothing sits above it, and a block gap when something does. */
251
+ .rsui-card__header + .rsui-card__content--padded {
252
+ @apply pt-space-md;
157
253
  }
158
254
 
159
- .rsui-card--xs .rsui-card__meta {
160
- @apply grid-cols-2;
255
+ /* A divider is the exception: a rule wants symmetric space around it, and the header
256
+ already pays 24px above, so the body pays 24px below. (0,3,0) against the (0,2,0)
257
+ above, so it wins on specificity rather than on source order. */
258
+ .rsui-card__header:has(.rsui-card-header--divider) + .rsui-card__content--padded {
259
+ @apply pt-space-lg;
161
260
  }
162
261
 
163
- .rsui-card--sm .rsui-card__meta {
164
- @apply grid-cols-2;
165
- }
262
+ /* Fields pack left and wrap, rather than sitting in equal-width grid columns.
263
+ grid-cols-N gave every field the same fraction of the card, so a short value left a
264
+ large hole before the next label and the two stopped reading as related — and below
265
+ 375px there was no multi-column rule at all, so two short fields stacked on a card
266
+ with room for both.
166
267
 
167
- .rsui-card--md .rsui-card__meta {
168
- @apply grid-cols-4;
169
- }
268
+ flex-wrap sizes each field to its content, so they sit 24px apart wherever they fit
269
+ and wrap only when they genuinely run out of room. It is also what
270
+ .rsui-card-horizontal__meta already does, so the same meta content now reads the same
271
+ in either card.
170
272
 
171
- .rsui-card--lg .rsui-card__meta {
172
- @apply grid-cols-6 gap-x-space-xl;
173
- }
273
+ The trade: values no longer line up vertically across wrapped rows. That alignment is
274
+ only worth having when the columns are full, and the complaint here was the opposite
275
+ problem. This replaced eight per-size rules (grid-cols-1/2/4/6/8/10/12) with one.
174
276
 
175
- .rsui-card--xl .rsui-card__meta {
176
- @apply grid-cols-8 gap-x-space-xl;
277
+ 16px between wrapped rows against MetaInfo's 8px label -> value, so a field is grouped
278
+ by having more space around it than inside it. */
279
+ .rsui-card__meta {
280
+ @apply max-w-full flex flex-wrap gap-x-space-lg gap-y-space-md;
177
281
  }
178
282
 
179
- .rsui-card--2xl .rsui-card__meta {
180
- @apply grid-cols-10 gap-x-space-xl;
283
+
284
+ /* ── Padding ───────────────────────────────────────────────────────────────────
285
+ Uniform 24px on all four sides. It replaced a 16/24/32 ramp keyed to the card's
286
+ own measured width, for two reasons:
287
+
288
+ - the brand book assigns 24 to "padding inside tiles and cards" and 32 to
289
+ "between related items", so the family's old flat 32 was borrowing another
290
+ job's value, and a ramp whose ends are 16 and 32 misses the card value twice
291
+ - Material 3 pads card content uniformly. The asymmetric 24 vertical / 32
292
+ horizontal this family carried is what made cards read wider than they are
293
+ tall, and it put the header text and the body on different left edges from
294
+ everything else on the page
295
+
296
+ Flat 24 still fixes the proportional problem the ramp was built for. Two columns
297
+ of a 753px CardGroup land at 365px — narrow even on a wide desktop, and under
298
+ useResponsiveWidth's smallest breakpoint — and 24px is 13% of that card where
299
+ the old 32px was 17.6%.
300
+
301
+ --rsui-card-indent-start lets a variant ADD to the padding rather than override
302
+ it; CheckboxCard sets it to clear its checkbox. The fallback lives inside the
303
+ var() call, never as its own declaration, or it would beat the inherited value.
304
+
305
+ .rsui-card-header and .rsui-card-horizontal__body keep their own px-space-xl at
306
+ (0,1,0) so either still works outside a Card. This rule is (0,2,0). */
307
+ .rsui-card {
308
+ --rsui-card-pad-x: 1.5rem; /* space-lg, 24px */
181
309
  }
182
310
 
183
- .rsui-card--3xl .rsui-card__meta {
184
- @apply grid-cols-12 gap-x-space-xl;
311
+ .rsui-card :is(.rsui-card-header, .rsui-card__content--padded, .rsui-card__image__badge-container, .rsui-card-horizontal__body) {
312
+ padding-inline: calc(var(--rsui-card-pad-x) + var(--rsui-card-indent-start, 0px)) var(--rsui-card-pad-x);
185
313
  }
@@ -1,10 +1,11 @@
1
1
  .rsui-card-header {
2
2
  @apply @container/card-header;
3
- @apply flex flex-col gap-x-space-sm gap-y-space-lg transition-all;
3
+ @apply flex flex-col gap-x-space-md gap-y-space-md transition-all;
4
+ /* px-space-xl is only the fallback for a CardHeader used outside a Card. Inside one
5
+ — which is every usage in this library — the padding rule at the foot of card.css
6
+ overrides it with the card's 24px. Change that rule, not this. */
4
7
  @apply pt-space-lg pb-0 px-space-xl;
5
8
  @apply border-b border-transparent;
6
-
7
- @apply @md/card-header:flex-row @md/card-header:justify-between @md/card-header:items-center;
8
9
  }
9
10
 
10
11
  .rsui-card-header--clickable {
@@ -21,7 +22,7 @@
21
22
  }
22
23
 
23
24
  .rsui-card-header__header {
24
- @apply flex-1 flex justify-between items-start gap-x-space-sm;
25
+ @apply flex-1 flex justify-between items-start gap-x-space-md;
25
26
  }
26
27
 
27
28
  .rsui-card-header__header--single-line {
@@ -29,13 +30,15 @@
29
30
  }
30
31
 
31
32
  /* Centring only holds while the text block is a lone title. `--single-line` means the *title*
32
- is clamped to one line, not that the header is one row — add a subtitle and a badge and the
33
- block is 78px tall, where centring a 48px avatar left it sitting 15px low against the text
34
- it belongs to. Seen in production on the Twinsies meeting cards. Top-align as soon as the
35
- block carries more than the title; a lone title is shorter than the avatar, so centring
36
- stays correct there. */
37
- .rsui-card-header__header--single-line:has(.rsui-card-header__subtitle),
38
- .rsui-card-header__header--single-line:has(.rsui-card-header__badge) {
33
+ is clamped to one line, not that the header is one row — add a subtitle and centring a 48px
34
+ avatar leaves it sitting low against the text it belongs to. Seen in production on the
35
+ Twinsies meeting cards. Top-align as soon as the block carries more than the title; a lone
36
+ title is shorter than the avatar, so centring stays correct there.
37
+
38
+ There used to be a second branch here for :has(.rsui-card-header__badge). The badge is no
39
+ longer inside __header — it sits below the row on the card's own left edge — so that branch
40
+ could never match again and has been dropped rather than left to look load-bearing. */
41
+ .rsui-card-header__header--single-line:has(.rsui-card-header__subtitle) {
39
42
  @apply @md/card-header:items-start;
40
43
  }
41
44
 
@@ -47,19 +50,59 @@
47
50
  @apply items-start;
48
51
  }
49
52
 
53
+ /* An eyebrow above the title: the small label a card uses to say what KIND of
54
+ thing it is before it says which one. There was no type style for this in the
55
+ system, so this introduces one — xs, semibold, uppercase, wide tracking, in the
56
+ secondary ink so it sits under the title rather than competing with it. */
57
+ /* 8px down to the title. The eyebrow labels the card's KIND before its name, so it
58
+ reads as its own line rather than as part of the title block — the brand's smallest
59
+ step, not the 4px a tight pair takes. gap-x-space-xs rather than the raw gap-x-2 it
60
+ arrived with; same 8px, but it now says it is a token. */
61
+ .rsui-card-header__eyebrow {
62
+ @apply flex items-center gap-x-space-xs mb-space-xs;
63
+ @apply text-xs font-semibold uppercase tracking-wider text-text-secondary;
64
+ }
65
+
66
+ /* A badge brings its own case, size and tracking, and BadgeRedSeedAI is the common
67
+ eyebrow — so it must not inherit the uppercase treatment meant for plain text.
68
+ Badge sets no text-transform of its own, so without this the uppercase leaks in. */
69
+ .rsui-card-header__eyebrow .rsui-badge {
70
+ @apply normal-case tracking-normal;
71
+ }
72
+
73
+ /* One gap at every width. The @md override used to drop this to space-xxs (4px),
74
+ which is not on the brand's 8px rhythm — and a title/subtitle pair does not want
75
+ a different relationship on a wide card than on a narrow one. */
76
+ /* No row-gap here on purpose. One gap cannot serve both relationships in this column:
77
+ an eyebrow is a separate label sitting ABOVE the title and wants the brand's 8px,
78
+ while the title and its subtitle are a tight pair and want 4px. The children carry
79
+ their own margins instead, which also means an absent eyebrow or subtitle
80
+ contributes nothing rather than leaving a gap behind. */
50
81
  .rsui-card-header__text {
51
- @apply flex-1 flex flex-col gap-y-space-xs;
52
-
53
- @apply @md/card-header:gap-y-space-xxs;
82
+ @apply flex-1 flex flex-col;
54
83
  }
55
84
 
56
85
  .rsui-card-header__title-container {
57
- @apply flex flex-wrap items-center gap-y-space-xxs gap-x-space-xs;
86
+ @apply flex flex-wrap items-center gap-y-space-xs gap-x-space-xs;
58
87
  }
59
88
 
89
+ /* Header 4 from the brand type scale: Inter Semibold, 20px, 130%. This was 18px,
90
+ which is not a step on the scale at all (16 / 20 / 28 / 40 / 64) and sat only 2px
91
+ above the 16px body, so the hierarchy barely registered.
92
+
93
+ Material 3 would use Title Medium (16sp) for a card headline, but M3 pairs that
94
+ with 14sp body. RedSeed holds a 16px body floor, so a 16px title would collapse
95
+ into the body it sits above — hence the next step up rather than M3's literal size. */
60
96
  .rsui-card-header__title {
61
97
  @apply flex w-full;
62
- @apply text-lg leading-6 font-semibold text-text-primary;
98
+ @apply text-xl leading-[1.3] font-semibold text-text-primary;
99
+ }
100
+
101
+ /* The badge sits below the avatar/title row, on the card's own left edge, level with the
102
+ body beneath it — so it is a child of .rsui-card-header rather than of __text, and picks
103
+ up the card's padding directly. The 16px above it is __card-header's own row gap. */
104
+ .rsui-card-header__badge {
105
+ @apply flex flex-wrap items-center gap-space-xs;
63
106
  }
64
107
 
65
108
  .rsui-card-header__title--single-line {
@@ -68,7 +111,24 @@
68
111
  @apply @md/card-header:line-clamp-1;
69
112
  }
70
113
 
114
+ /* TIGHT PAIR — 4px up to the title, deliberately off the brand's 8px rhythm. The
115
+ canonical statement of why; the other two instances point here.
116
+
117
+ The book's 8px assumes the gap is the only space between two lines of text. It is
118
+ not: line-height contributes leading of its own at both ends. The title is 20/26 and
119
+ this is 14/20, so roughly 6px of half-leading already sits between them before any
120
+ gap is added — 8px renders as about 14px of visible space and the pair reads as two
121
+ separate lines rather than one thing and its qualifier. 4px puts the visible space
122
+ near 7px, measured.
123
+
124
+ Same reason as MetaInfo's label -> value and .rsui-card-horizontal__content's
125
+ title -> description. Three instances is a pattern, not three exceptions: the brand
126
+ scale has no step for a tightly-coupled text pair, raised on #317.
127
+
128
+ Off-scale values elsewhere in the card family are drift and are banned by
129
+ CardSpacing.test.js, which allows only the ones carrying this TIGHT PAIR marker. */
71
130
  .rsui-card-header__subtitle {
131
+ @apply mt-space-xxs;
72
132
  @apply text-sm leading-5 text-text-secondary;
73
133
  }
74
134
 
@@ -79,15 +139,15 @@
79
139
  }
80
140
 
81
141
  .rsui-card-header__toolbar {
82
- @apply flex gap-space-sm justify-end;
142
+ @apply flex gap-space-xs justify-end;
83
143
 
84
144
  @apply @md/card-header:items-center;
85
145
  }
86
146
 
87
147
  .rsui-card-header__actions-desktop {
88
- @apply flex gap-space-sm items-center;
148
+ @apply flex gap-space-xs items-center;
89
149
  }
90
150
 
91
151
  .rsui-card-header__actions-mobile {
92
- @apply flex flex-wrap gap-space-sm items-center;
152
+ @apply flex flex-wrap gap-space-xs items-center;
93
153
  }
@@ -175,7 +175,9 @@
175
175
  .rsui-card-horizontal__body {
176
176
  /* Mobile: body fills full width below the image (self-stretch default).
177
177
  sm+: self-start so body drives the grid row height (not stretches to it). */
178
- @apply flex flex-col gap-y-space-xs py-space-lg px-space-xl min-w-0;
178
+ /* px-space-xl here is the standalone fallback only; the padding rule at the foot of
179
+ card.css overrides it with the card's 24px, since CardHorizontal renders a <Card>. */
180
+ @apply flex flex-col gap-y-space-md py-space-lg px-space-xl min-w-0;
179
181
  }
180
182
 
181
183
  @container card-horizontal (min-width: calc(32rem - 2px)) /* --container-lg (32rem=512px) - 2px border */ {
@@ -184,18 +186,28 @@
184
186
  }
185
187
  }
186
188
 
189
+ /* TIGHT PAIR — 4px, matching the card header's title -> subtitle. See the note above
190
+ .rsui-card-header__text for why a coupled text pair sits off the 8px rhythm: the
191
+ line-height of both lines already contributes leading, so 8px renders as roughly
192
+ 14px of visible space and the two stop reading as one unit. */
187
193
  .rsui-card-horizontal__content {
188
194
  @apply flex flex-col gap-y-space-xxs;
189
195
  }
190
196
 
197
+ /* The same as .rsui-card-header__title — brand Header 4, Inter Semibold 20/26. It was
198
+ text-base with leading-snug (16/22, Header 5), so the same title role rendered a step
199
+ smaller here than in a Card and the two did not read as one family. */
191
200
  .rsui-card-horizontal__title {
192
- @apply text-base font-semibold text-text-primary leading-snug;
201
+ @apply text-xl leading-[1.3] font-semibold text-text-primary;
193
202
  }
194
203
 
195
204
  .rsui-card-horizontal__description {
196
205
  @apply text-sm text-text-secondary;
197
206
  }
198
207
 
208
+ /* Matches .rsui-card__meta: 24px between fields, 16px between wrapped rows. It was
209
+ 12px and 4px, so the same meta content sat on a tighter rhythm here than in a Card
210
+ and the two did not read as the same family. */
199
211
  .rsui-card-horizontal__meta {
200
- @apply flex flex-wrap items-center gap-x-space-sm gap-y-space-xxs;
212
+ @apply flex flex-wrap items-center gap-x-space-lg gap-y-space-md;
201
213
  }
@@ -0,0 +1,65 @@
1
+ /* ---------------------------------------------------------------------------
2
+ Category list
3
+
4
+ A wrapping row of neutral badges naming the buckets a record sits in.
5
+
6
+ Wrapping, not scrolling. The cap is what keeps the row short; a horizontal
7
+ scroller would hide categories behind a gesture for the same reason a table
8
+ never scrolls the page sideways.
9
+
10
+ Imported after badge.css, since the linked variant restyles the badge inside
11
+ its anchor.
12
+ --------------------------------------------------------------------------- */
13
+ /* 16px out to the toggle, 8px between the badges — both on the brand 8px rhythm, and
14
+ the wider gap is what stops "+4 more" reading as one more category. */
15
+ .rsui-category-list {
16
+ @apply flex flex-wrap items-center gap-x-space-md gap-y-space-xs;
17
+ }
18
+
19
+ /* The list resets its own margins and padding, the way breadcrumbs and
20
+ linked_list already do — a `ul` carries a browser indent that would push the
21
+ row out of line with the title above it. */
22
+ .rsui-category-list__items {
23
+ @apply flex flex-wrap items-center gap-x-space-xs gap-y-space-xs;
24
+ @apply list-none m-0 p-0;
25
+ }
26
+
27
+ .rsui-category-list__item {
28
+ @apply flex;
29
+ }
30
+
31
+ /* A linked category has to look like a link. A badge with no pointer and no
32
+ hover reads as a label, which is what most categories are, so the ones that
33
+ genuinely filter a list get the library's always-on underline (the LinkSlot
34
+ convention) plus a hover state on the pill itself. Half-measures are the
35
+ failure here: a pointer alone is not a visible affordance. */
36
+ .rsui-category-list__link {
37
+ @apply block rounded-full no-underline;
38
+ @apply focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-border-primary outline-hidden;
39
+ }
40
+
41
+ .rsui-category-list__link .rsui-badge {
42
+ @apply cursor-pointer underline underline-offset-2 transition-colors;
43
+ }
44
+
45
+ /* One grey step darker, not the usual `hover:bg-background-secondary`. That is the
46
+ convention for a hover on a white surface, and a neutral badge already sits on
47
+ background-secondary — applying it here would be a no-op. */
48
+ .rsui-category-list__link:hover .rsui-badge {
49
+ @apply bg-background-secondary-invert border-border-primary text-text-primary;
50
+ }
51
+
52
+ /* The toggle. Matches the "See all" / view-all treatment already used in
53
+ section headers and footers: semibold on the tertiary-button colour, no
54
+ resting underline, underline on hover only. That pairing reads as an action
55
+ without competing with the badges beside it.
56
+
57
+ Sibling to the list rather than inside it, so it wraps onto its own line at
58
+ phone widths — which is where the cap does most of its work. */
59
+ .rsui-category-list__more {
60
+ @apply inline-flex w-fit items-center whitespace-nowrap;
61
+ @apply text-sm font-semibold no-underline cursor-pointer transition-colors;
62
+ @apply text-button-tertiary hover:text-button-tertiary-hover;
63
+ @apply hover:underline underline-offset-4 decoration-2;
64
+ @apply focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-button-tertiary outline-hidden;
65
+ }
@@ -2,16 +2,15 @@
2
2
  @apply @container/checkbox-card;
3
3
  }
4
4
 
5
- .rsui-checkbox-card .rsui-card-header__actions-mobile {
6
- @apply pl-8;
7
- }
5
+ /* No checkbox indent anywhere in this component — see the note above the content
6
+ rule below. Everything shares the card's own 24px left edge with the checkbox. */
8
7
 
9
8
  .rsui-checkbox-card--disabled {
10
9
  @apply focus-visible:ring-0;
11
10
  }
12
11
 
13
12
  .rsui-checkbox-card__content {
14
- @apply flex items-center p-2 gap-4;
13
+ @apply flex items-center p-space-xs gap-space-md;
15
14
  }
16
15
 
17
16
  .rsui-checkbox-card__icon {
@@ -59,26 +58,40 @@
59
58
  @apply items-center;
60
59
  }
61
60
 
61
+ /* 16px between the checkbox and the title, not 8px. A 24px control next to 20px text
62
+ needs more separation than the raw gap-2 this carried — at 8px the box crowded the
63
+ first letter. Note this is the opposite call to the card header's 48px avatar, which
64
+ wanted 8px: a large avatar reads as attached to its title, a small control reads as
65
+ cramped against it. Both values are on the brand's 8px rhythm. */
62
66
  .rsui-checkbox-card__title-row {
63
- @apply inline-flex items-start gap-2;
67
+ @apply inline-flex items-start gap-space-md;
64
68
  }
65
69
 
66
70
  .rsui-checkbox-card__subtitle {
67
71
  @apply text-sm text-text-quaternary;
68
- @apply pl-8;
69
72
  }
70
73
 
71
74
  .rsui-checkbox-card__badge {
72
- @apply pl-8 flex gap-space-xs;
75
+ @apply flex gap-space-xs;
73
76
  }
74
77
  .rsui-checkbox-card__title-text--strikethrough {
75
78
  @apply line-through;
76
79
  }
77
80
 
78
- /* Align body/meta content with the header title (account for checkbox width + gap) */
79
- .rsui-checkbox-card > .rsui-card__content--padded {
80
- @apply pl-8 @sm/checkbox-card:pl-12;
81
- }
81
+ /* NO checkbox indent. The subtitle, badge, meta and mobile actions all sit on the
82
+ card's own 24px left edge, level with the checkbox — the only thing offset is the
83
+ title text, and that is offset because the checkbox physically precedes it on the
84
+ same line, which reads as deliberate.
85
+
86
+ This component used to indent all four of those by 32px (checkbox 24 + gap 8) so
87
+ they lined up with the title text instead. That left the checkbox alone in a 32px
88
+ gutter with nothing under it, which is what read as messy on a narrow card: three
89
+ elements moved right to serve one, and the empty column below the control was the
90
+ most prominent thing in the layout.
91
+
92
+ Nothing is left to fight the card's padding shorthand here, so no
93
+ --rsui-card-indent-start either. The variable stays in card.css for any variant
94
+ that does need an additive inset. */
82
95
 
83
96
  /* Success state styling */
84
97
  /* Increase specificity so it overrides base .rsui-card styles */
@@ -1,4 +1,18 @@
1
1
  .rsui-meta-info {
2
+ /* TIGHT PAIR — 4px between a label and its value, deliberately off the brand's 8px
3
+ rhythm. See the note above .rsui-card-header__text in card_header.css for the full
4
+ reasoning. Do not "fix" it back to space-xs.
5
+
6
+ The book assigns 8px to a label/value pair, but that assumes 16px body text, where
7
+ 8px is a third of a line box. This component sets 12px with a 16px line box, so 8px
8
+ is HALF a line — measured — and at that size the pair stops reading as one field and
9
+ reads as two loose rows. 4px is a quarter of the line, and against the 16px between
10
+ fields it gives a 1:4 grouping instead of 1:2.
11
+
12
+ The real cause is that the meta is 12px at all, which is below the brand's own 14px
13
+ caption floor. Fix the size and 8px becomes correct; that belongs with the MetaInfo
14
+ size ramp in #316, not here. Until then this is the size the component ships at, and
15
+ the gap has to suit it. */
2
16
  @apply flex flex-col gap-y-1;
3
17
  }
4
18
 
@@ -6,7 +20,7 @@
6
20
  legacy pattern, double-counts against any gap and breaks once the row wraps. Every other
7
21
  component in the library spaces with gap. */
8
22
  .rsui-meta-info__top {
9
- @apply flex gap-x-1 items-center;
23
+ @apply flex gap-x-space-xs items-center;
10
24
  }
11
25
 
12
26
  .rsui-meta-info__label-icon {
@@ -18,7 +32,7 @@
18
32
  eye landed on "Age" instead of on the number. Grey-600 on white measures 7.26:1, so it stays
19
33
  comfortably readable at 12px. */
20
34
  .rsui-meta-info__label {
21
- @apply font-medium text-xs text-text-tertiary;
35
+ @apply font-medium text-text-tertiary;
22
36
  }
23
37
 
24
38
  .rsui-meta-info__help {
@@ -32,25 +46,37 @@
32
46
  /* The value carries the information, so it takes the emphasis the label gave up. */
33
47
  .rsui-meta-info__value {
34
48
  @apply flex items-center gap-x-space-xs;
35
- @apply text-xs font-semibold text-text-primary;
49
+ @apply font-semibold text-text-primary;
36
50
  }
37
51
 
38
- .rsui-meta-info--md .rsui-meta-info__label {
39
- @apply text-sm;
52
+ /* SIZE RAMP. Three steps, matching StageDial. The label and value always share a
53
+ size — they are one field, and a field whose descriptor and content disagree on
54
+ size reads as two things.
55
+
56
+ `sm` is the default, and it is now DECLARED rather than left implicit in the base
57
+ rules. The size used to live on .rsui-meta-info__label and __value directly, so
58
+ the default had no class of its own: it could not be targeted, and the ramp read
59
+ as "md, lg, or nothing".
60
+
61
+ The names sit one step below the scale they produce — sm is text-xs, md is
62
+ text-sm, lg is text-base. That predates this change and is deliberately not
63
+ renamed: md and lg are used in consuming apps, so renaming them is a breaking
64
+ change for a cosmetic gain. */
65
+ .rsui-meta-info--sm .rsui-meta-info__label,
66
+ .rsui-meta-info--sm .rsui-meta-info__value {
67
+ @apply text-xs;
40
68
  }
41
69
 
70
+ .rsui-meta-info--md .rsui-meta-info__label,
42
71
  .rsui-meta-info--md .rsui-meta-info__value {
43
72
  @apply text-sm;
44
73
  }
45
74
 
46
- .rsui-meta-info--lg .rsui-meta-info__label {
47
- @apply text-base;
48
- }
49
-
75
+ .rsui-meta-info--lg .rsui-meta-info__label,
50
76
  .rsui-meta-info--lg .rsui-meta-info__value {
51
77
  @apply text-base;
52
78
  }
53
79
 
54
80
  .rsui-meta-info--inline {
55
- @apply flex-row flex-wrap items-center gap-x-2 gap-y-1;
81
+ @apply flex-row flex-wrap items-center gap-x-space-xs gap-y-space-xs;
56
82
  }
@@ -14,7 +14,7 @@
14
14
  }
15
15
 
16
16
  .rsui-metric-card__heading {
17
- @apply w-full flex items-center gap-space-sm;
17
+ @apply w-full flex items-center gap-space-md;
18
18
  }
19
19
 
20
20
  .rsui-metric-card__icon {
@@ -1,39 +1,86 @@
1
+ /* gap, not space-y. space-* adds margins to children, which is the legacy pattern the
2
+ rest of the library has moved off — see the note in meta_info.css. It double-counts
3
+ against any margin a child sets for itself and it breaks once a row wraps. Both
4
+ happened here: see __title below. */
5
+
6
+ /* A column at every width. This was `flex` — which is row — with `lg:flex-col`, so below
7
+ lg the root laid its children out horizontally.
8
+
9
+ Nothing caught it because below lg there was only ever one visible child to lay out:
10
+ `__top`, since `__meta` is `hidden lg:flex`. A single flex item looks the same in a row
11
+ as in a column. The tell was this rule's own `gap-y-space-md`, which does nothing in a
12
+ row direction unless the row wraps — 16px of dead declaration, written by someone
13
+ thinking "column".
14
+
15
+ Add a second full-width row and the direction stops being invisible: the categories
16
+ landed *beside* the title on a phone with both squeezed to half the screen. Measured.
17
+
18
+ `justify-between` is kept rather than dropped. It is inert in an unconstrained column,
19
+ so it changes nothing here, but a consuming app that constrains the header's height
20
+ would notice it going. From lg up this rule now resolves exactly as it did before. */
1
21
  .rsui-page-header {
2
- @apply w-full flex justify-between space-y-4;
3
- @apply lg:flex-col;
22
+ @apply w-full flex flex-col justify-between gap-y-space-md;
4
23
  }
5
24
 
6
25
  .rsui-page-header__top {
7
- @apply w-full flex flex-col items-start space-y-2 flex-wrap;
8
- @apply lg:flex-row lg:space-y-0 lg:items-center;
26
+ @apply w-full flex flex-col items-start gap-y-space-xs flex-wrap;
27
+ @apply lg:flex-row lg:gap-y-0 lg:items-center;
9
28
  }
10
29
 
30
+ /* 16px avatar -> title, matching the card family's avatar gap, and set once as a gap
31
+ rather than twice as margins.
32
+
33
+ It was space-x-6 here plus mr-3 on the avatar. In Tailwind v4 space-x-* sets
34
+ margin-inline-end on every child but the last, so the avatar's own mr-3 overrode it
35
+ and the rendered gap was 12px, not the 24px this rule reads as. Measured. Neither
36
+ value was reachable by editing the other, which is the whole problem with space-*. */
11
37
  .rsui-page-header__title {
12
- @apply pr-2 flex flex-row items-start space-x-6;
38
+ @apply pr-space-xs flex flex-row items-start gap-x-space-md;
13
39
  }
14
40
 
41
+ /* Material 3 puts a prominent page title in its Headline range and sets all UI type in
42
+ a sans face — so this drops Newsreader and lands on the two RedSeed steps that cover
43
+ that range: Header 3 (Inter Bold 28/120%) below lg, Header 2 (Inter Black 40/115%,
44
+ -1% tracking) from lg up.
45
+
46
+ It was font-redseed-heading — which resolves to Newsreader — at 30 -> 36px. Off-brand
47
+ twice over: the book restricts Newsreader to Header 1 and says not to push it below
48
+ Header 1 sizes, and neither 30 nor 36 is a step on the scale at all.
49
+
50
+ 40px also keeps a page title a clear step above SectionHeader's 28px, which is the
51
+ hierarchy M3 maintains between a top app bar and a heading inside content.
52
+
53
+ Deviation worth naming: M3 sets top-app-bar titles at weight 400 Regular. RedSeed's
54
+ scale specifies Bold for Header 3 and Black for Header 2, and the tokens win here per
55
+ the same rule the card family followed — M3's structure, RedSeed's type.
56
+
57
+ lg:mr-4 removed. __title-text-container already supplies a 24px gap between the title
58
+ and the status slot, so the margin added to it and the rendered gap was 40px — the
59
+ same accidental sum the avatar had. */
15
60
  .rsui-page-header__title h1 {
16
- @apply font-redseed-heading text-3xl lg:text-4xl font-semibold text-text-primary lg:mr-4;
61
+ @apply text-[1.75rem] leading-[1.2] font-bold text-text-primary;
62
+ @apply lg:text-[2.5rem] lg:leading-[1.15] lg:tracking-[-0.01em] lg:font-black;
17
63
  }
18
64
 
19
65
  .rsui-page-header__title-text-container {
20
- @apply flex flex-row space-x-6 items-center;
66
+ @apply flex flex-row gap-x-space-lg items-center;
21
67
  }
22
68
 
23
69
  .rsui-page-header__status {
24
70
  @apply flex-shrink-0;
25
71
  }
26
72
 
73
+ /* Brand body: 16px at 150%. leading-relaxed is 1.625, so this rendered 16/26. */
27
74
  .rsui-page-header__subtitle {
28
- @apply text-base text-text-secondary leading-relaxed;
75
+ @apply text-base leading-normal text-text-secondary;
29
76
  }
30
77
 
31
78
  .rsui-page-header__status-actions {
32
- @apply flex-1 flex space-x-2 items-center justify-between lg:pr-2;
79
+ @apply flex-1 flex gap-x-space-xs items-center justify-between lg:pr-space-xs;
33
80
  }
34
81
 
35
82
  .rsui-page-header__actions {
36
- @apply flex-1 flex space-x-2 items-center justify-end;
83
+ @apply flex-1 flex flex-wrap gap-space-xs items-center justify-end;
37
84
  }
38
85
 
39
86
  .rsui-page-header__meta-action {
@@ -41,7 +88,7 @@
41
88
  }
42
89
 
43
90
  .rsui-page-header__search {
44
- @apply w-full pt-2 lg:pt-0 lg:w-auto;
91
+ @apply w-full pt-space-xs lg:pt-0 lg:w-auto;
45
92
  }
46
93
 
47
94
  .rsui-page-header__meta-modal {
@@ -49,15 +96,15 @@
49
96
  }
50
97
 
51
98
  .rsui-page-header__meta-modal__body {
52
- @apply flex flex-col space-y-4;
99
+ @apply flex flex-col gap-y-space-md;
53
100
  }
54
101
 
55
102
  .rsui-page-header__meta {
56
- @apply hidden lg:flex space-x-8;
103
+ @apply hidden lg:flex gap-x-space-xl;
57
104
  }
58
105
 
59
106
  .rsui-page-header__avatar {
60
- @apply shrink-0 size-12 mr-3;
107
+ @apply shrink-0 size-12;
61
108
  }
62
109
 
63
110
  /* Profile variant — larger avatar, bottom-aligned text, inline subtitle */
@@ -65,16 +112,18 @@
65
112
  @apply lg:items-end;
66
113
  }
67
114
 
115
+ /* The profile avatar is larger, so it takes more room beside the title than the
116
+ default 16px — 24px from lg up, added to the gap rather than replacing it. */
68
117
  .rsui-page-header--profile .rsui-page-header__avatar {
69
- @apply shrink-0 size-auto mr-3 lg:mr-6 lg:pb-4;
118
+ @apply shrink-0 size-auto lg:me-space-xs lg:pb-space-md;
70
119
  }
71
120
 
72
121
  .rsui-page-header--profile .rsui-page-header__status-actions {
73
- @apply lg:self-end lg:pb-6;
122
+ @apply lg:self-end lg:pb-space-lg;
74
123
  }
75
124
 
76
125
  .rsui-page-header--profile .rsui-page-header__subtitle {
77
- @apply flex flex-col gap-1 lg:flex-row lg:items-center lg:gap-4;
126
+ @apply flex flex-col gap-space-xs lg:flex-row lg:items-center lg:gap-space-md;
78
127
  }
79
128
 
80
129
 
@@ -99,7 +148,7 @@
99
148
  /* Fallbacks keep an unrecognised tone legible rather than transparent. */
100
149
  background: var(--rsui-page-header-bg, var(--color-background-secondary));
101
150
 
102
- @apply py-space-lg px-space-xl;
151
+ @apply p-space-lg;
103
152
  }
104
153
 
105
154
  .rsui-page-header--tone-green {
@@ -153,3 +202,30 @@
153
202
  .rsui-page-header--toned .rsui-meta-info__help {
154
203
  color: var(--rsui-page-header-ink-muted);
155
204
  }
205
+
206
+ /* ---------------------------------------------------------------------------
207
+ Categories
208
+
209
+ A wrapping row of neutral badges between the subtitle and the meta, naming
210
+ the buckets the record sits in. See category_list.css for the row itself.
211
+
212
+ Neutral is a rule here rather than a preference: the header already carries
213
+ exactly one coloured badge — the status beside the title — and a second
214
+ colour in this row makes both of them meaningless.
215
+
216
+ One row, not two kinds. Duration, level and domain are facts about the
217
+ record and belong in the meta grid; a bucket the record sits in belongs
218
+ here.
219
+ --------------------------------------------------------------------------- */
220
+ .rsui-page-header__categories {
221
+ @apply flex w-full;
222
+ }
223
+
224
+ /* The tertiary green the toggle borrows disappears on a dark band, so it moves
225
+ onto the tone's own ink. Same reasoning as the MetaInfo overrides above:
226
+ reaching into another component's classes is not ideal, but an invisible
227
+ control is worse, and what a slot looks like on this component's tonal
228
+ surface is this stylesheet's business. */
229
+ .rsui-page-header--toned .rsui-category-list__more {
230
+ color: var(--rsui-page-header-ink);
231
+ }
@@ -1,11 +1,15 @@
1
1
  .rsui-radio-card {
2
2
  @apply relative cursor-pointer transition-all;
3
3
  /* No resting shadow: this was the only card in the family that sat lifted while the
4
- other seven are flat. Padding matches the family's 24/32 rhythm rather than the 12/12
5
- it used to carry alone. ring-offset-2 matches the focus geometry every other card
6
- uses — this ring previously hugged the border with no offset. */
4
+ other seven are flat. ring-offset-2 matches the focus geometry every other card
5
+ uses — this ring previously hugged the border with no offset.
6
+
7
+ Padding is 24/24, the same as every other member — but it has to be written here
8
+ rather than inherited, because RadioCard is the one family member that does NOT
9
+ render a <Card>, so the padding rule in card.css never reaches it. Keep the two in
10
+ step. The 32px sides it used to carry were 20% of its fixed 320px width. */
7
11
  @apply rounded-xl outline-hidden select-none;
8
- @apply flex flex-col gap-3 py-space-lg px-space-xl max-w-80 w-80;
12
+ @apply flex flex-col gap-space-md py-space-lg px-space-lg max-w-80 w-80;
9
13
  @apply bg-background-primary border border-border-primary;
10
14
  @apply focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-border-primary;
11
15
  }
@@ -48,7 +52,7 @@
48
52
  }
49
53
 
50
54
  .rsui-radio-card__title-row {
51
- @apply relative text-base font-medium text-text-primary line-clamp-3 min-h-6 pr-8;
55
+ @apply relative text-xl leading-[1.3] font-medium text-text-primary line-clamp-3 min-h-6 pr-8;
52
56
 
53
57
  @apply after:content-[''] after:absolute after:top-0 after:right-0;
54
58
  @apply after:size-6 after:rounded-full after:transition-all;
@@ -72,8 +76,11 @@
72
76
  @apply after:border-7 after:border-background-primary after:bg-border-primary;
73
77
  }
74
78
 
79
+ /* Brand Header 4, matching .rsui-card-header__title. The row above carries the same
80
+ size deliberately: line-clamp-3 lives there, and it measures against the clamped
81
+ element's own line-height, so a mismatch clips at the wrong place. */
75
82
  .rsui-radio-card__title {
76
- @apply flex-1 text-base font-semibold text-text-primary;
83
+ @apply flex-1 text-xl leading-[1.3] font-semibold text-text-primary;
77
84
  }
78
85
 
79
86
  .rsui-radio-card__content-row {
@@ -85,9 +92,9 @@
85
92
  }
86
93
 
87
94
  .rsui-radio-card__meta-row {
88
- @apply grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-2;
95
+ @apply grid grid-cols-1 sm:grid-cols-2 gap-x-space-xl gap-y-space-xs;
89
96
  }
90
97
 
91
98
  .rsui-radio-card__action-row {
92
- @apply flex flex-wrap gap-2;
99
+ @apply flex flex-wrap gap-space-xs;
93
100
  }
@@ -1,6 +1,7 @@
1
1
  .rsui-section-header {
2
- @apply flex flex-col gap-x-space-sm gap-y-space-lg transition-all;
2
+ @apply flex flex-col gap-x-space-md gap-y-space-md transition-all;
3
3
  @apply md:flex-row md:justify-between md:items-center;
4
+ @apply border-b border-transparent;
4
5
  }
5
6
 
6
7
  /* overflow-hidden guards against a useResponsiveWidth feedback loop (mobile
@@ -11,22 +12,40 @@
11
12
  @apply overflow-hidden;
12
13
  }
13
14
 
15
+ /* The --divider class was being set by SectionHeader.vue with no rule behind it, so
16
+ showDivider did nothing. Implemented the way CardHeader does it — a transparent
17
+ bottom border on the base, coloured here — and the prop's default flips to false in
18
+ the same change, so every consumer's appearance is unchanged (they saw no rule
19
+ before and see none now) while the prop starts working for anyone who opts in.
20
+ Brand-wise a hairline between sections is right; a box around them is not. */
21
+ .rsui-section-header--divider {
22
+ @apply border-border-primary;
23
+ }
24
+
14
25
  .rsui-section-header--clickable {
15
26
  @apply cursor-pointer;
16
27
  }
17
28
 
18
29
  .rsui-section-header__header {
19
- @apply flex-1 min-w-0 flex justify-between items-start gap-x-space-sm;
30
+ @apply flex-1 min-w-0 flex justify-between items-start gap-x-space-md;
20
31
  }
21
32
 
22
33
  .rsui-section-header__icon {
23
34
  @apply flex items-center justify-center;
24
35
  }
25
36
 
37
+ /* No row-gap: one value cannot serve both a title/badge row and a title/subtitle pair.
38
+ The subtitle carries its own margin instead, which also means an absent subtitle
39
+ leaves no gap behind. Same structure as .rsui-card-header__text. */
26
40
  .rsui-section-header__text {
27
- @apply flex-1 flex flex-col gap-y-space-xs;
41
+ @apply flex-1 flex flex-col;
28
42
  }
29
43
 
44
+ /* An optical nudge, not spacing — which is why it stays a raw 4px rather than being
45
+ rounded onto the 8px rhythm. It lines the title's cap height up with the button text
46
+ beside it; the correct value is whatever makes those two agree, and it is not a gap
47
+ between two elements. Same category as the control offsets in checkbox_card and
48
+ card_group, which are also deliberately raw. */
30
49
  .rsui-section-header__text--with-toolbar {
31
50
  @apply pt-1;
32
51
  }
@@ -56,12 +75,35 @@
56
75
  from inside the header would silently override a size prop the consumer passed. The stories
57
76
  demonstrate the `sm` pairing.
58
77
 
59
- TITLE SIZE: text-3xl from lg up. At a flat text-2xl the title box was 32px while even an sm
60
- button is 38px, so the heading never led its own actions. Responsive rather than flat, and
61
- matching how PageHeader scales, because mobile stacks the actions anyway (see
62
- __actions-mobile) and the imbalance only really shows on wide screens. */
78
+ BADGE SIZE: a badge beside the title belongs at `xs`. Measured against the Header 4
79
+ title box (26px): the `md` badge is 34px — 131% of it — and `sm`, which is Badge's
80
+ DEFAULT, is 26px, exactly 100%. Either reads as competing with the heading rather than
81
+ labelling it. `xs` is 18px, 69%. Like ACTION SIZE above this is guidance, not enforced:
82
+ scoping a size from inside the header would silently override a size prop the consumer
83
+ passed, and CSS cannot tell Badge's default `sm` from an explicit one. The stories
84
+ demonstrate the pairing.
85
+
86
+ Worth knowing that `sm` being the default makes `<Badge>` with no size 100% of a
87
+ Header 4 title box anywhere it sits beside one — CardHeader included. That is a
88
+ Badge-level question rather than something to patch per header.
89
+
90
+ TITLE SIZE: brand Header 4 (20/130%) below lg, Header 3 (28/120%) from lg up. It was
91
+ text-2xl -> text-3xl, which is 24 -> 30 and neither is a step on the scale at all
92
+ (16 / 20 / 28 / 40 / 64).
93
+
94
+ Responsive rather than flat is kept deliberately, and so is the reason: mobile stacks
95
+ the actions (see __actions-mobile) so the title does not have to out-weigh them there,
96
+ while on a wide screen it sits beside them and does. Read the note below before
97
+ flattening this.
98
+
99
+ Honest limit: the original concern was that a 32px title box lost to a 38px sm button.
100
+ 28px at 120% is a 34px box, so it is better but not resolved — a title that decisively
101
+ leads an sm button needs Header 2 (40px), which is PageHeader's step and would compete
102
+ with it. The alternative is smaller action buttons. That is a design call, flagged
103
+ rather than taken here. */
63
104
  .rsui-section-header__title-text {
64
- @apply text-2xl lg:text-3xl font-bold line-clamp-2 md:line-clamp-1 text-text-primary;
105
+ @apply text-xl leading-[1.3] font-bold line-clamp-2 md:line-clamp-1 text-text-primary;
106
+ @apply lg:text-[1.75rem] lg:leading-[1.2];
65
107
  /* Reset heading UA margins so an <h1>-<h6> title matches the previous <div> */
66
108
  /* min-w-0 lets the title shrink and truncate first; the badge is shrink-0 so */
67
109
  /* it keeps its size. No flex-1 — a growing title pushes the See-all badge up */
@@ -80,18 +122,23 @@
80
122
  @apply flex items-center shrink-0;
81
123
  }
82
124
 
125
+ /* TIGHT PAIR — 4px up to the title, deliberately off the brand's 8px rhythm. See the
126
+ note above .rsui-card-header__subtitle in card_header.css: line-height already
127
+ contributes half-leading at both ends, so 8px renders as roughly 14px and the pair
128
+ reads as two separate lines rather than one thing and its qualifier. */
83
129
  .rsui-section-header__subtitle {
130
+ @apply mt-space-xxs;
84
131
  @apply text-sm text-text-secondary;
85
132
  }
86
133
 
87
134
  .rsui-section-header__toolbar {
88
- @apply flex gap-space-sm md:items-center justify-end;
135
+ @apply flex gap-space-xs md:items-center justify-end;
89
136
  }
90
137
 
91
138
  .rsui-section-header__actions-desktop {
92
- @apply flex gap-space-sm items-center;
139
+ @apply flex gap-space-xs items-center;
93
140
  }
94
141
 
95
142
  .rsui-section-header__actions-mobile {
96
- @apply flex flex-wrap gap-space-sm items-center justify-start;
143
+ @apply flex flex-wrap gap-space-xs items-center justify-start;
97
144
  }
package/components.css CHANGED
@@ -23,6 +23,7 @@ Please be careful when adding new components and updating the order.
23
23
  @import './components/card.css';
24
24
  @import './components/card_header.css';
25
25
  @import './components/card_horizontal.css';
26
+ @import './components/category_list.css';
26
27
  @import './components/checkbox_card.css';
27
28
  @import './components/column_picker.css';
28
29
  @import './components/comments.css';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redseed/redseed-ui-tailwindcss",
3
- "version": "7.57.0",
3
+ "version": "7.59.0",
4
4
  "description": "RedSeed UI Tailwindcss",
5
5
  "main": "index.js",
6
6
  "style": "index.css",
package/tokens.css CHANGED
@@ -49,6 +49,13 @@
49
49
  --color-background-ai-from: #ebe0fa;
50
50
  --color-background-ai-to: var(--Colors-Background-AI-2); /* v1 defines ai-from only; ai-to kept as warm counterpart */
51
51
  --color-background-ai-contrast: var(--Colors-Warm-Mauve-600);
52
+ /* The AI family at full strength, for a BOLD surface rather than a light tint.
53
+ ai-from/ai-to are a pale lilac wash; these are the dark end of the same ramp,
54
+ for a card that has to carry white type. On-ramp on purpose — the /home/v2
55
+ mock used #56374c/#3b2635, which are not Warm Mauve values. */
56
+ --color-background-ai-bold-from: var(--Colors-Warm-Mauve-900);
57
+ --color-background-ai-bold-via: var(--Colors-Warm-Mauve-950);
58
+ --color-background-ai-bold-to: var(--Colors-Grey-950);
52
59
  --color-background-ai-invert: var(--Colors-Warm-Mauve-200);
53
60
 
54
61
  --color-border-primary: var(--Colors-Grey-400);