@redseed/redseed-ui-tailwindcss 7.56.0 → 7.58.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
  }
@@ -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 */