@flytedan/flytebot-design-system 0.12.9 → 0.12.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flytedan/flytebot-design-system",
3
- "version": "0.12.9",
3
+ "version": "0.12.11",
4
4
  "description": "flytedesk Design System — shared component library, tokens, and business-logic kits for flytedesk apps.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -176,13 +176,23 @@
176
176
  background: var(--surface);
177
177
  box-shadow: var(--e-1);
178
178
  }
179
- /* Tone on a quiet (ghost/outline) icon button: the glyph carries it, on hover too. */
180
- .fd-btn-icon.is-ok:not(.fd-btn-icon-solid),
181
- .fd-btn-icon.is-ok:not(.fd-btn-icon-solid):hover {
179
+ /* Tone on a quiet (ghost/outline) icon button: the glyph carries it, on hover too.
180
+ MP-114 follow-up: also excludes .fd-btn-icon-soft, which was never meant to match
181
+ here (it isn't ghost or outline — it has its own real fill, see .fd-btn-icon-soft
182
+ below) but did, at (0,3,0) beating the soft rule's own un-toned (0,1,0)/toned
183
+ (0,2,0) color rules at rest. That stomped --btn-soft-ok-ink/--btn-soft-danger-ink
184
+ (chosen to clear 4.5:1 against the soft fill) with --ok-text/--danger-text (chosen
185
+ to clear 4.5:1 against a plain card instead) — a real fill behind the wrong ink,
186
+ e2e-measured at 2.04:1. The soft rule's own `:hover`/`:active` restatements already
187
+ won their own specificity fight against this same rule's `:hover` (both (0,3,0),
188
+ soft declared later so it wins the tie) — only the REST state, where soft never
189
+ restated color at all, actually lost. */
190
+ .fd-btn-icon.is-ok:not(.fd-btn-icon-solid):not(.fd-btn-icon-soft),
191
+ .fd-btn-icon.is-ok:not(.fd-btn-icon-solid):not(.fd-btn-icon-soft):hover {
182
192
  color: var(--ok-text);
183
193
  }
184
- .fd-btn-icon.is-danger:not(.fd-btn-icon-solid),
185
- .fd-btn-icon.is-danger:not(.fd-btn-icon-solid):hover {
194
+ .fd-btn-icon.is-danger:not(.fd-btn-icon-solid):not(.fd-btn-icon-soft),
195
+ .fd-btn-icon.is-danger:not(.fd-btn-icon-solid):not(.fd-btn-icon-soft):hover {
186
196
  color: var(--danger-text);
187
197
  }
188
198
  /* Solid: a real fill in the tone's colour. The fills and inks are the --btn-solid-*
@@ -231,6 +241,41 @@
231
241
  .fd-btn-icon-solid:focus-visible {
232
242
  box-shadow: var(--focus-ring);
233
243
  }
244
+ /* Soft: a lighter fill than solid, with a visibly darker border drawn around it (MP-52's
245
+ "lighter fill, darker border" row action). Same --btn-soft-* tokens (tokens.css), same
246
+ per-theme measurement discipline as solid, and the same pressed-selector-specificity
247
+ fix baked in from the start (see the comment on .fd-btn-icon-solid:active above). */
248
+ .fd-btn-icon-soft {
249
+ background: var(--btn-soft-bg);
250
+ color: var(--btn-soft-ink);
251
+ border-color: var(--btn-soft-border);
252
+ box-shadow: var(--e-1);
253
+ }
254
+ .fd-btn-icon-soft.is-ok {
255
+ --btn-soft-bg: var(--btn-soft-ok-bg);
256
+ --btn-soft-border: var(--btn-soft-ok-border);
257
+ --btn-soft-ink: var(--btn-soft-ok-ink);
258
+ }
259
+ .fd-btn-icon-soft.is-danger {
260
+ --btn-soft-bg: var(--btn-soft-danger-bg);
261
+ --btn-soft-border: var(--btn-soft-danger-border);
262
+ --btn-soft-ink: var(--btn-soft-danger-ink);
263
+ }
264
+ .fd-btn-icon-soft:hover,
265
+ .fd-btn-icon-soft.is-ok:hover,
266
+ .fd-btn-icon-soft.is-danger:hover {
267
+ filter: brightness(1.08);
268
+ color: var(--btn-soft-ink);
269
+ }
270
+ .fd-btn-icon-soft:active,
271
+ .fd-btn-icon-soft.is-ok:active,
272
+ .fd-btn-icon-soft.is-danger:active {
273
+ filter: brightness(0.92);
274
+ box-shadow: none;
275
+ }
276
+ .fd-btn-icon-soft:focus-visible {
277
+ box-shadow: var(--focus-ring);
278
+ }
234
279
  .fd-btn-icon-sm {
235
280
  width: var(--control-h-sm);
236
281
  height: var(--control-h-sm);
@@ -4054,8 +4099,20 @@
4054
4099
  itself uses (--r-md here rather than Card's --r-lg — a compact list row reads
4055
4100
  better with the tighter radius; --e-1/--e-2 are the same elevation tokens). */
4056
4101
  .fd-erow {
4102
+ /* display/align-items/gap/padding live here, not on the component's inline style
4103
+ (where they used to be, MP-114 follow-ups) — an inline style always wins over a
4104
+ stylesheet rule, which would make it impossible for the narrow-container rule
4105
+ below to ever switch this row from a flex row to a grid. gap/padding/border-width
4106
+ read the shared --erow-* custom properties (declared once on .fd-tlist above)
4107
+ rather than carrying their own literals, which is also what lets
4108
+ .fd-tlist-footer's indent stay correct by construction instead of by two
4109
+ numbers happening to agree. */
4110
+ display: flex;
4111
+ align-items: center;
4112
+ gap: var(--erow-gap);
4113
+ padding: 0 var(--erow-pad-x);
4057
4114
  background: var(--surface-2);
4058
- border: 1px solid var(--erow-border);
4115
+ border: var(--erow-border-w) solid var(--erow-border);
4059
4116
  border-radius: var(--r-md);
4060
4117
  box-shadow: var(--e-1);
4061
4118
  transition:
@@ -4126,6 +4183,43 @@
4126
4183
  overflow: hidden;
4127
4184
  text-overflow: ellipsis;
4128
4185
  }
4186
+ /* The title-icon Tooltip trigger (MP-52): icon and title share one hoverable/focusable
4187
+ region rather than two, so hovering either opens the same explanation. Carries the same
4188
+ flex floor .fd-erow-title has on its own (see the comment above it) so wrapping the
4189
+ title in a button never changes how the titleline shares space with `meta`. Reset to a
4190
+ plain inline button — no border/background/font change — because visually this must
4191
+ read exactly like a title with an icon in front of it, not a control. */
4192
+ .fd-erow-title-icon {
4193
+ display: inline-flex;
4194
+ align-items: center;
4195
+ gap: 4px;
4196
+ flex: 1 1 auto;
4197
+ min-width: 45%;
4198
+ border: 0;
4199
+ background: none;
4200
+ padding: 0;
4201
+ margin: 0;
4202
+ font: inherit;
4203
+ color: inherit;
4204
+ text-align: left;
4205
+ cursor: pointer;
4206
+ }
4207
+ .fd-erow-title-icon .fd-erow-title {
4208
+ min-width: 0;
4209
+ }
4210
+ .fd-erow-title-icon > .ph {
4211
+ flex: none;
4212
+ font-size: 14px;
4213
+ color: var(--text-muted);
4214
+ }
4215
+ .fd-erow-title-icon.is-danger > .ph {
4216
+ color: var(--danger-solid);
4217
+ }
4218
+ .fd-erow-title-icon:focus-visible {
4219
+ outline: none;
4220
+ box-shadow: var(--focus-ring);
4221
+ border-radius: var(--r-sm);
4222
+ }
4129
4223
  .fd-erow-strip {
4130
4224
  display: flex;
4131
4225
  align-items: center;
@@ -4227,6 +4321,12 @@ button.fd-erow-metric:focus-visible {
4227
4321
  display: inline-flex;
4228
4322
  align-items: center;
4229
4323
  }
4324
+ /* Size lives here, not on the component's inline style (MP-114 third follow-up,
4325
+ same reasoning as .fd-erow's own padding above): .fd-tlist-footer's indent reads
4326
+ it back as --erow-grip-size. */
4327
+ .fd-erow-grip {
4328
+ font-size: var(--erow-grip-size);
4329
+ }
4230
4330
  .fd-erow[draggable="true"]:active {
4231
4331
  cursor: grabbing;
4232
4332
  }
@@ -4256,49 +4356,182 @@ button.fd-erow-metric:focus-visible {
4256
4356
  screen's. */
4257
4357
  .fd-tlist {
4258
4358
  container-type: inline-size;
4359
+ /* A row's own geometry, declared ONCE here rather than as literals wherever
4360
+ something needs to line up with it (MP-114, third follow-up: the totals
4361
+ footer's indent restated these as bare numbers in two places while the real
4362
+ values lived as inline styles on EntityRow.tsx's row and its drag grip — a
4363
+ change to either drifted the footer's indent silently, which is exactly what
4364
+ the second follow-up's 24px misalignment was). `.fd-erow` and `.fd-erow-grip`
4365
+ read these instead of carrying their own inline padding/font-size, and
4366
+ `.fd-tlist-footer`'s two indents (below) are `calc()` of the same four
4367
+ properties — a row and the totals line can only drift apart now if one of
4368
+ THESE four values changes, and both readers change with it. Declared on
4369
+ `.fd-tlist` (the one ancestor common to both a row and the footer, which are
4370
+ otherwise unrelated siblings under `.fd-tlist-side`) rather than on `.fd-erow`
4371
+ itself, so the footer — which is never a descendant of any `.fd-erow` — can
4372
+ still read them by ordinary custom-property inheritance. */
4373
+ --erow-border-w: 1px;
4374
+ --erow-pad-x: 12px;
4375
+ --erow-grip-size: 14px;
4376
+ --erow-gap: 10px;
4259
4377
  }
4260
4378
  .fd-tlist-panes {
4261
4379
  display: flex;
4262
4380
  gap: 16px;
4263
4381
  align-items: flex-start;
4264
4382
  }
4383
+ /* TransferList's per-side slots. The footer is where a running totals line goes;
4384
+ it sits on the panel, not on a card, and is separated by a rule rather than a
4385
+ gap so it reads as a summary OF the list rather than another row IN it.
4386
+ Declared here, BEFORE the narrow @container block below, deliberately: CSS
4387
+ cascade order for two rules of equal specificity is source order regardless of
4388
+ which one sits inside an at-rule, so a base rule declared AFTER a narrow
4389
+ override for the same property would silently win at every width, override
4390
+ included — this is what actually happened to this rule's own `padding-left`
4391
+ for two follow-ups running (the override was textually correct and had a
4392
+ passing stylesheet-contract test, but the base rule used to be declared further
4393
+ down the file, after the @container block, and always won the tie). */
4394
+ .fd-tlist-header {
4395
+ display: flex;
4396
+ align-items: center;
4397
+ gap: 8px;
4398
+ min-width: 0;
4399
+ }
4400
+ .fd-tlist-footer {
4401
+ display: flex;
4402
+ align-items: center;
4403
+ gap: 10px;
4404
+ min-width: 0;
4405
+ padding-top: 8px;
4406
+ /* Indented to the x a row's own body starts at, so a totals strip in here lands
4407
+ under the columns it totals rather than a card's width to the left of them: the
4408
+ card's border, the row's own padding, the drag grip and its gap — every row in
4409
+ a TransferList is a drag source, so it is the same on all of them. Written as
4410
+ calc() of the shared --erow-* custom properties (.fd-tlist's own comment)
4411
+ rather than as separate literals that merely happened to equal the same
4412
+ numbers EntityRow.tsx's row/grip carried inline — a change to any one of those
4413
+ four properties now moves this indent with it instead of silently drifting out
4414
+ of alignment, which is exactly how this went wrong at the narrow breakpoint
4415
+ (see that rule's own comment). */
4416
+ padding-left: calc(var(--erow-border-w) + var(--erow-pad-x) + var(--erow-grip-size) + var(--erow-gap));
4417
+ border-top: 1px solid var(--border);
4418
+ font-size: var(--overline-size);
4419
+ color: var(--text-2);
4420
+ }
4265
4421
  @container (max-width: 600px) {
4266
4422
  .fd-tlist-panes {
4267
4423
  flex-direction: column;
4268
4424
  align-items: stretch;
4269
4425
  }
4270
- /* MP-114 (follow-up the `width: auto` version of this rule regressed the
4271
- alignment it was supposed to preserve): below this width EntityRow's metric strip
4272
- stops having room to keep every column at the desktop's fixed --erow-metric-w (set
4273
- so rows line up with a multi-digit totals footer, see EntityRowMetrics/.fd-erow-cell
4274
- above) even each cell's own un-wrapped figure (`.fd-erow-metric`'s
4275
- `white-space: nowrap` makes its min-content equal its full width) no longer fits
4276
- together in the strip, and `.fd-erow-metrics`'s `overflow: hidden` was clipping the
4277
- ones that didn't.
4278
- The first fix let `.fd-erow-cell` go `width: auto` but "auto" means "size to your
4279
- own content", and a row's own figures and TransferList's footer totals almost never
4280
- have the same digit counts, so the row's strip and the totals strip (the SAME
4281
- component, at the SAME nominal column widths) drifted apart the moment either one's
4282
- content did audience-builder.spec.ts caught this: a row 14px narrower than the
4283
- totals line under it. A fixed-width cell has to stay fixed-width EVERYWHERE for
4284
- "the same column lands at the same x on every row and on the totals line" (see
4285
- .fd-erow-metrics's own comment above) to mean anything, including here. So instead:
4286
- `flex-shrink: 0` keeps every cell at its exact declared width even when the strip
4287
- has less room than the three columns need, which the row and the footer both still
4288
- agree on regardless of what either one's figures say — and `.fd-erow-metrics`
4289
- scrolls that overflow horizontally instead of clipping it, the same "wide content
4290
- gets its own scroll container, nothing else has to give" rule DataTable's
4291
- `.fd-table-wrap` already follows (and the layout auditor already treats an
4292
- `overflow: auto` ancestor as fine by design). A phone-width row keeps every figure
4293
- reachable, one swipe away, rather than losing it or drifting out of line with the
4294
- total it belongs under. */
4426
+ /* MP-114 (second follow-up). The first follow-up kept every `.fd-erow-cell` at its
4427
+ exact fixed `--erow-metric-w` down here (never `width: auto` see the alignment
4428
+ argument in EntityRowMetrics/.fd-erow-metrics's own comments above, which this
4429
+ still honours) and let `.fd-erow-metrics` scroll instead of clip when the strip ran
4430
+ out of room. That satisfied the alignment contract, but media-planner's
4431
+ audience-list-affordances.spec.ts (375px, touch) caught what it traded away: a
4432
+ scrollable strip still HIDES figures behind a scroll a touch user has no reason to
4433
+ discover, which the spec's `stripClipsNothing` check treats as no better than
4434
+ clipping at phone width every figure has to be actually, fully visible.
4435
+
4436
+ The real room was never in the strip itself — it was in the REST of the row.
4437
+ EntityRow is `title (+ titleIcon/meta)` above `metrics (+ accessory)` beside
4438
+ `action`, with a drag grip down the side: at desktop width that two-line-plus-rail
4439
+ shape is what lets metrics and a school's name and its preference-meter accessory
4440
+ and its +/- action all fit on a compact row. At a stacked phone pane that shape is
4441
+ the problem cramming metrics onto the SAME line as an accessory (media-planner's
4442
+ TransferList rows carry a ScoreMeter accessory) leaves the strip a fraction of the
4443
+ row's own width even though the row has plenty of width overall.
4444
+
4445
+ So below this width the row becomes a two-row GRID instead: name (+ its icon/meta)
4446
+ with the accessory and the +/- action all on row one, and the metric strip ALONE
4447
+ spanning the full row width on row two — the same width `.fd-tlist-footer`'s totals
4448
+ line gets (see below), which is what keeps the two aligned. `.fd-erow-body` and
4449
+ `.fd-erow-strip` contribute no box of their own any more here (`display: contents`
4450
+ unwraps them, MDN's own documented way to let a grid reach through a wrapper
4451
+ without changing the component's DOM/JSX) — every element that actually needs a
4452
+ grid position (title line, grip, accessory, metrics, action) becomes a direct
4453
+ grid child by that unwrapping alone. `grid-template-areas` uses one row with no
4454
+ "strip" cell in it for a plain row that has neither metrics nor an accessory (an
4455
+ unfilled `auto` grid row is 0-height), so a name-only row still stays one line
4456
+ tall exactly as `.fd-erow-body`'s own base-rule comment describes.
4457
+
4458
+ Even spanning the full row, three columns this wide (media-planner: 96/82/92px)
4459
+ can still be tight once the row also loses width to its drag grip and a 44px
4460
+ touch-target action button — so `--erow-metric-w` itself also shrinks by a flat,
4461
+ modest 15% here, identically for a row's own cells and the totals footer's (both
4462
+ read the SAME custom property, so both shrink the same and stay aligned). 15% is a
4463
+ deliberate buffer, not a measured minimum: it keeps real figures (media-planner's
4464
+ own test data reaches "958,000") comfortably clear of `.fd-erow-metric-value`'s
4465
+ own `text-overflow: ellipsis` floor rather than sizing to the exact edge of it. */
4466
+ .fd-erow {
4467
+ display: grid;
4468
+ grid-template-columns: auto minmax(0, 1fr) auto auto;
4469
+ grid-template-areas:
4470
+ "grip title accessory action"
4471
+ "strip strip strip strip";
4472
+ /* Row-gap is a new number (there was no vertical gap to preserve — the old
4473
+ layout never had two grid rows), but the column-gap is the same --erow-gap
4474
+ the wide flex row uses, not a second literal 10px that could drift from it. */
4475
+ gap: 4px var(--erow-gap);
4476
+ }
4477
+ .fd-erow-body,
4478
+ .fd-erow-strip {
4479
+ display: contents;
4480
+ }
4481
+ .fd-erow-grip {
4482
+ grid-area: grip;
4483
+ }
4484
+ .fd-erow-titleline {
4485
+ grid-area: title;
4486
+ }
4487
+ .fd-erow-accessory {
4488
+ grid-area: accessory;
4489
+ }
4490
+ .fd-erow-action {
4491
+ grid-area: action;
4492
+ }
4295
4493
  .fd-erow-cell {
4296
4494
  flex-shrink: 0;
4495
+ width: calc(var(--erow-metric-w) * 0.85);
4297
4496
  }
4298
4497
  .fd-erow-metrics {
4498
+ /* Row context (.fd-erow is the grid above): claims the whole second grid row.
4499
+ Footer context (.fd-tlist-footer is a plain flex row, see below): `grid-area`
4500
+ is a no-op there since its parent isn't a grid, and `flex: 1 1 100%` is what
4501
+ does the equivalent job — combined with that flex row's own `flex-wrap: wrap`,
4502
+ claiming the full line pushes the "Totals · N schools" caption after it onto
4503
+ a second line, the same shape the row now has. Only one of the two rules ever
4504
+ actually applies in a given parent, so both can live on this one selector. */
4505
+ grid-area: strip;
4506
+ flex: 1 1 100%;
4507
+ /* Belt-and-suspenders, not the primary mechanism any more: if some OTHER
4508
+ consumer's columns still don't fit even the whole row's width after the 15%
4509
+ reduction above, this scrolls that overflow rather than clipping it (the same
4510
+ "wide content gets its own scroll container" rule DataTable's `.fd-table-wrap`
4511
+ follows, and the layout auditor already treats an `overflow: auto` ancestor as
4512
+ fine by design) — never reached by media-planner's own audience columns, which
4513
+ the full-width row and the 15% reduction together already fit. */
4299
4514
  overflow-x: auto;
4300
4515
  overflow-y: hidden;
4301
4516
  }
4517
+ .fd-tlist-footer {
4518
+ flex-wrap: wrap;
4519
+ /* The wide-width padding-left (see the base rule's own comment) indents the
4520
+ totals strip to where a row's metric strip used to start: AFTER the card border,
4521
+ the row's own padding, the drag grip and its gap. Down here the row's strip no
4522
+ longer starts there — it's `.fd-erow`'s own full-width second grid row (see
4523
+ `.fd-erow`'s own comment above), which starts at the row's own left padding
4524
+ edge, before the grip rather than after it. audience-builder.spec.ts caught the
4525
+ mismatch this left behind: a row's strip and the totals strip 24px apart at the
4526
+ SAME breakpoint that made the grid switch active in the first place (any
4527
+ TransferList pane under 600px, which an ordinary 1280px desktop window reaches
4528
+ easily — see that rule's own comment). The totals strip needs the SAME new
4529
+ origin: just the card's border and the row's own padding, with the grip/gap
4530
+ terms dropped since they no longer apply to either strip — both terms of the
4531
+ SAME --erow-* properties (.fd-tlist's own comment) the base rule below uses,
4532
+ not separately-guessed numbers, so the two can't drift apart again. */
4533
+ padding-left: calc(var(--erow-border-w) + var(--erow-pad-x));
4534
+ }
4302
4535
  }
4303
4536
  .fd-tlist-side {
4304
4537
  flex: 1;
@@ -4346,32 +4579,6 @@ button.fd-erow-metric:focus-visible {
4346
4579
  box-shadow: var(--e-2);
4347
4580
  color: var(--brand);
4348
4581
  }
4349
- /* TransferList's per-side slots. The footer is where a running totals line goes;
4350
- it sits on the panel, not on a card, and is separated by a rule rather than a
4351
- gap so it reads as a summary OF the list rather than another row IN it. */
4352
- .fd-tlist-header {
4353
- display: flex;
4354
- align-items: center;
4355
- gap: 8px;
4356
- min-width: 0;
4357
- }
4358
- .fd-tlist-footer {
4359
- display: flex;
4360
- align-items: center;
4361
- gap: 10px;
4362
- min-width: 0;
4363
- padding-top: 8px;
4364
- /* Indented to the x a row's own body starts at, so a totals strip in here lands
4365
- under the columns it totals rather than a card's width to the left of them.
4366
- The sum is the card's border and padding-left plus the drag grip and its gap
4367
- — every row in a TransferList is a drag source, so it is the same on all of
4368
- them. Spelled out rather than collapsed to 37px so it stays checkable against
4369
- .fd-erow if that padding ever moves. */
4370
- padding-left: calc(1px + 12px + 14px + 10px);
4371
- border-top: 1px solid var(--border);
4372
- font-size: var(--overline-size);
4373
- color: var(--text-2);
4374
- }
4375
4582
 
4376
4583
  /* --- ScoreMeter — a small categorical 0-100 bar, optionally a trigger ------ */
4377
4584
  .fd-scoremeter {
package/styles/tokens.css CHANGED
@@ -100,6 +100,20 @@
100
100
  --btn-solid-ok-bg:#1E7A4C;--btn-solid-ok-bg-hover:#18663F;--btn-solid-ok-bg-active:#145A38;--btn-solid-ok-ink:#FFFFFF;
101
101
  --btn-solid-danger-bg:#B93A31;--btn-solid-danger-bg-hover:#9E2F27;--btn-solid-danger-bg-active:#862720;--btn-solid-danger-ink:#FFFFFF;
102
102
 
103
+ /* Soft icon button (IconButton variant="soft") — MP-52's "lighter fill, darker border"
104
+ row action. A lighter, less saturated fill than --btn-solid-* with a visibly darker
105
+ border drawn around it, so the button still reads as a button rather than a solid
106
+ colour chip. Measured exactly like --btn-solid-* above, against the same two grounds
107
+ (--surface-2 #F7F8FA and --erow-danger-fill #FCE9E6): white ink on the fill is 4.53:1
108
+ or better and the fill itself is 3.87:1 or better against either ground. Going any
109
+ lighter breaks one of those two floors — a fill light enough to read as "soft" in the
110
+ usual badge sense (e.g. --ok-fill/--danger-fill) is only ~1.05:1 from --erow-danger-fill
111
+ and fails outright; this is the lightest fill that still clears the bar next to a red
112
+ row. The border reuses the darker --btn-solid-*-bg-active token, 1.8-2:1 off the new
113
+ fill and 7:1+ off both grounds, so it reads even where hue alone would not. */
114
+ --btn-soft-ok-bg:#557F6A;--btn-soft-ok-border:#145A38;--btn-soft-ok-ink:#FFFFFF;
115
+ --btn-soft-danger-bg:#C84E43;--btn-soft-danger-border:#862720;--btn-soft-danger-ink:#FFFFFF;
116
+
103
117
  --ok-fill:#DFF3E8;--ok-border:#A8DDC2;--ok-text:#0E5233;--ok-solid:#34A26B;--ok-emph:#57C68C;
104
118
  --warn-fill:#FDF1DA;--warn-border:#F0D49A;--warn-text:#6B4A05;--warn-solid:#C8890B;
105
119
  --danger-fill:#FCE7E5;--danger-border:#F2BDB8;--danger-text:#7A241D;--danger-solid:#D0473D;
@@ -256,6 +270,19 @@
256
270
  --btn-solid-ok-bg:#3FB37A;--btn-solid-ok-bg-hover:#57C68C;--btn-solid-ok-bg-active:#35A26D;--btn-solid-ok-ink:#0B2A1B;
257
271
  --btn-solid-danger-bg:#E8776D;--btn-solid-danger-bg-hover:#F08A80;--btn-solid-danger-bg-active:#D9685E;--btn-solid-danger-ink:#2A0B08;
258
272
 
273
+ /* Soft icon button — see the :root block above. On dark ground the direction flips the
274
+ same way --btn-solid-* already does: a lighter fill cannot separate from a near-black
275
+ card by luminance, so the soft fill goes DARKER and saturated (not lighter, the way
276
+ light theme reads "lighter") with a light ink and a lighter border drawn around it —
277
+ the same "still a button, one step down from --btn-solid-*" relationship, expressed in
278
+ whichever direction actually clears the floor on this ground. Measured against
279
+ --surface-2 (#1B1F27) and --erow-danger-fill (#331813): white ink is 5.4:1 or better,
280
+ the fill is 3.0:1 or better against either ground, and the border (the theme's own
281
+ --btn-solid-*-bg, already proven legible here) is 1.9:1+ off the fill and 5.7:1+ off
282
+ both grounds. */
283
+ --btn-soft-ok-bg:#007A3D;--btn-soft-ok-border:#3FB37A;--btn-soft-ok-ink:#FFFFFF;
284
+ --btn-soft-danger-bg:#D41200;--btn-soft-danger-border:#E8776D;--btn-soft-danger-ink:#FFFFFF;
285
+
259
286
  /* chat kit — see the :root block above */
260
287
  --chat-code-bg:#12151B;
261
288
  --code-kw:#B3A6F0;--code-str:#E9C377;--code-num:#EFA8BE;--code-com:#6C7686;