@flytedan/flytebot-design-system 0.12.10 → 0.12.12

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.10",
3
+ "version": "0.12.12",
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-*
@@ -234,7 +244,22 @@
234
244
  /* Soft: a lighter fill than solid, with a visibly darker border drawn around it (MP-52's
235
245
  "lighter fill, darker border" row action). Same --btn-soft-* tokens (tokens.css), same
236
246
  per-theme measurement discipline as solid, and the same pressed-selector-specificity
237
- fix baked in from the start (see the comment on .fd-btn-icon-solid:active above). */
247
+ fix baked in from the start (see the comment on .fd-btn-icon-solid:active above).
248
+
249
+ MP-114 hover/active: the base `.fd-btn-icon:hover`/`:active` rules above (plain
250
+ `.fd-btn-icon`, no variant) set `background` too, at the same (0,2,0) specificity as
251
+ `.fd-btn-icon-soft:hover`/`:active` — a tie the cascade breaks by source order, so
252
+ whichever rule sets `background` LAST wins. The old hover/active rules here only set
253
+ `filter` and `color`, never `background`, so they never entered that tie for the
254
+ `background` property at all: the base rule's `background: var(--surface-3)` /
255
+ `var(--border)` was the only declaration left standing, and the soft fill vanished
256
+ under a plain neutral surface colour while the ink stayed tuned for the soft fill —
257
+ e2e-measured at 1.11:1. `filter: brightness()` made it worse in a way no contrast
258
+ check (this file's or a screen reader user's own eye) could catch: a filter changes
259
+ the pixels without ever touching `background-color`, so it cannot be reasoned about
260
+ from the declared colours at all. Fixed on both counts: every state below restates a
261
+ real `background` from a real token, so it wins its own tie the same way solid's
262
+ already does, and there is no filter left to hide behind. */
238
263
  .fd-btn-icon-soft {
239
264
  background: var(--btn-soft-bg);
240
265
  color: var(--btn-soft-ink);
@@ -243,26 +268,36 @@
243
268
  }
244
269
  .fd-btn-icon-soft.is-ok {
245
270
  --btn-soft-bg: var(--btn-soft-ok-bg);
271
+ --btn-soft-bg-hover: var(--btn-soft-ok-bg-hover);
272
+ --btn-soft-bg-active: var(--btn-soft-ok-bg-active);
246
273
  --btn-soft-border: var(--btn-soft-ok-border);
247
274
  --btn-soft-ink: var(--btn-soft-ok-ink);
248
275
  }
249
276
  .fd-btn-icon-soft.is-danger {
250
277
  --btn-soft-bg: var(--btn-soft-danger-bg);
278
+ --btn-soft-bg-hover: var(--btn-soft-danger-bg-hover);
279
+ --btn-soft-bg-active: var(--btn-soft-danger-bg-active);
251
280
  --btn-soft-border: var(--btn-soft-danger-border);
252
281
  --btn-soft-ink: var(--btn-soft-danger-ink);
253
282
  }
254
283
  .fd-btn-icon-soft:hover,
255
284
  .fd-btn-icon-soft.is-ok:hover,
256
285
  .fd-btn-icon-soft.is-danger:hover {
257
- filter: brightness(1.08);
286
+ background: var(--btn-soft-bg-hover);
258
287
  color: var(--btn-soft-ink);
259
288
  }
289
+ /* Pressed restates every selector hover uses, same reasoning as
290
+ .fd-btn-icon-solid:active above. */
260
291
  .fd-btn-icon-soft:active,
261
292
  .fd-btn-icon-soft.is-ok:active,
262
293
  .fd-btn-icon-soft.is-danger:active {
263
- filter: brightness(0.92);
294
+ background: var(--btn-soft-bg-active);
295
+ color: var(--btn-soft-ink);
264
296
  box-shadow: none;
265
297
  }
298
+ /* Restated after :active, same reasoning as .fd-btn-icon-solid:focus-visible above: a
299
+ pressed-while-focused button must keep the ring, not the pressed rule's `box-shadow:
300
+ none`. */
266
301
  .fd-btn-icon-soft:focus-visible {
267
302
  box-shadow: var(--focus-ring);
268
303
  }
@@ -4089,15 +4124,20 @@
4089
4124
  itself uses (--r-md here rather than Card's --r-lg — a compact list row reads
4090
4125
  better with the tighter radius; --e-1/--e-2 are the same elevation tokens). */
4091
4126
  .fd-erow {
4092
- /* display/align-items/gap live here, not on the component's inline style (where
4093
- they used to be, MP-114 follow-up) — an inline style always wins over a
4127
+ /* display/align-items/gap/padding live here, not on the component's inline style
4128
+ (where they used to be, MP-114 follow-ups) — an inline style always wins over a
4094
4129
  stylesheet rule, which would make it impossible for the narrow-container rule
4095
- below to ever switch this row from a flex row to a grid. */
4130
+ below to ever switch this row from a flex row to a grid. gap/padding/border-width
4131
+ read the shared --erow-* custom properties (declared once on .fd-tlist above)
4132
+ rather than carrying their own literals, which is also what lets
4133
+ .fd-tlist-footer's indent stay correct by construction instead of by two
4134
+ numbers happening to agree. */
4096
4135
  display: flex;
4097
4136
  align-items: center;
4098
- gap: 10px;
4137
+ gap: var(--erow-gap);
4138
+ padding: 0 var(--erow-pad-x);
4099
4139
  background: var(--surface-2);
4100
- border: 1px solid var(--erow-border);
4140
+ border: var(--erow-border-w) solid var(--erow-border);
4101
4141
  border-radius: var(--r-md);
4102
4142
  box-shadow: var(--e-1);
4103
4143
  transition:
@@ -4306,6 +4346,12 @@ button.fd-erow-metric:focus-visible {
4306
4346
  display: inline-flex;
4307
4347
  align-items: center;
4308
4348
  }
4349
+ /* Size lives here, not on the component's inline style (MP-114 third follow-up,
4350
+ same reasoning as .fd-erow's own padding above): .fd-tlist-footer's indent reads
4351
+ it back as --erow-grip-size. */
4352
+ .fd-erow-grip {
4353
+ font-size: var(--erow-grip-size);
4354
+ }
4309
4355
  .fd-erow[draggable="true"]:active {
4310
4356
  cursor: grabbing;
4311
4357
  }
@@ -4335,12 +4381,68 @@ button.fd-erow-metric:focus-visible {
4335
4381
  screen's. */
4336
4382
  .fd-tlist {
4337
4383
  container-type: inline-size;
4384
+ /* A row's own geometry, declared ONCE here rather than as literals wherever
4385
+ something needs to line up with it (MP-114, third follow-up: the totals
4386
+ footer's indent restated these as bare numbers in two places while the real
4387
+ values lived as inline styles on EntityRow.tsx's row and its drag grip — a
4388
+ change to either drifted the footer's indent silently, which is exactly what
4389
+ the second follow-up's 24px misalignment was). `.fd-erow` and `.fd-erow-grip`
4390
+ read these instead of carrying their own inline padding/font-size, and
4391
+ `.fd-tlist-footer`'s two indents (below) are `calc()` of the same four
4392
+ properties — a row and the totals line can only drift apart now if one of
4393
+ THESE four values changes, and both readers change with it. Declared on
4394
+ `.fd-tlist` (the one ancestor common to both a row and the footer, which are
4395
+ otherwise unrelated siblings under `.fd-tlist-side`) rather than on `.fd-erow`
4396
+ itself, so the footer — which is never a descendant of any `.fd-erow` — can
4397
+ still read them by ordinary custom-property inheritance. */
4398
+ --erow-border-w: 1px;
4399
+ --erow-pad-x: 12px;
4400
+ --erow-grip-size: 14px;
4401
+ --erow-gap: 10px;
4338
4402
  }
4339
4403
  .fd-tlist-panes {
4340
4404
  display: flex;
4341
4405
  gap: 16px;
4342
4406
  align-items: flex-start;
4343
4407
  }
4408
+ /* TransferList's per-side slots. The footer is where a running totals line goes;
4409
+ it sits on the panel, not on a card, and is separated by a rule rather than a
4410
+ gap so it reads as a summary OF the list rather than another row IN it.
4411
+ Declared here, BEFORE the narrow @container block below, deliberately: CSS
4412
+ cascade order for two rules of equal specificity is source order regardless of
4413
+ which one sits inside an at-rule, so a base rule declared AFTER a narrow
4414
+ override for the same property would silently win at every width, override
4415
+ included — this is what actually happened to this rule's own `padding-left`
4416
+ for two follow-ups running (the override was textually correct and had a
4417
+ passing stylesheet-contract test, but the base rule used to be declared further
4418
+ down the file, after the @container block, and always won the tie). */
4419
+ .fd-tlist-header {
4420
+ display: flex;
4421
+ align-items: center;
4422
+ gap: 8px;
4423
+ min-width: 0;
4424
+ }
4425
+ .fd-tlist-footer {
4426
+ display: flex;
4427
+ align-items: center;
4428
+ gap: 10px;
4429
+ min-width: 0;
4430
+ padding-top: 8px;
4431
+ /* Indented to the x a row's own body starts at, so a totals strip in here lands
4432
+ under the columns it totals rather than a card's width to the left of them: the
4433
+ card's border, the row's own padding, the drag grip and its gap — every row in
4434
+ a TransferList is a drag source, so it is the same on all of them. Written as
4435
+ calc() of the shared --erow-* custom properties (.fd-tlist's own comment)
4436
+ rather than as separate literals that merely happened to equal the same
4437
+ numbers EntityRow.tsx's row/grip carried inline — a change to any one of those
4438
+ four properties now moves this indent with it instead of silently drifting out
4439
+ of alignment, which is exactly how this went wrong at the narrow breakpoint
4440
+ (see that rule's own comment). */
4441
+ padding-left: calc(var(--erow-border-w) + var(--erow-pad-x) + var(--erow-grip-size) + var(--erow-gap));
4442
+ border-top: 1px solid var(--border);
4443
+ font-size: var(--overline-size);
4444
+ color: var(--text-2);
4445
+ }
4344
4446
  @container (max-width: 600px) {
4345
4447
  .fd-tlist-panes {
4346
4448
  flex-direction: column;
@@ -4392,7 +4494,10 @@ button.fd-erow-metric:focus-visible {
4392
4494
  grid-template-areas:
4393
4495
  "grip title accessory action"
4394
4496
  "strip strip strip strip";
4395
- gap: 4px 10px;
4497
+ /* Row-gap is a new number (there was no vertical gap to preserve — the old
4498
+ layout never had two grid rows), but the column-gap is the same --erow-gap
4499
+ the wide flex row uses, not a second literal 10px that could drift from it. */
4500
+ gap: 4px var(--erow-gap);
4396
4501
  }
4397
4502
  .fd-erow-body,
4398
4503
  .fd-erow-strip {
@@ -4436,6 +4541,21 @@ button.fd-erow-metric:focus-visible {
4436
4541
  }
4437
4542
  .fd-tlist-footer {
4438
4543
  flex-wrap: wrap;
4544
+ /* The wide-width padding-left (see the base rule's own comment) indents the
4545
+ totals strip to where a row's metric strip used to start: AFTER the card border,
4546
+ the row's own padding, the drag grip and its gap. Down here the row's strip no
4547
+ longer starts there — it's `.fd-erow`'s own full-width second grid row (see
4548
+ `.fd-erow`'s own comment above), which starts at the row's own left padding
4549
+ edge, before the grip rather than after it. audience-builder.spec.ts caught the
4550
+ mismatch this left behind: a row's strip and the totals strip 24px apart at the
4551
+ SAME breakpoint that made the grid switch active in the first place (any
4552
+ TransferList pane under 600px, which an ordinary 1280px desktop window reaches
4553
+ easily — see that rule's own comment). The totals strip needs the SAME new
4554
+ origin: just the card's border and the row's own padding, with the grip/gap
4555
+ terms dropped since they no longer apply to either strip — both terms of the
4556
+ SAME --erow-* properties (.fd-tlist's own comment) the base rule below uses,
4557
+ not separately-guessed numbers, so the two can't drift apart again. */
4558
+ padding-left: calc(var(--erow-border-w) + var(--erow-pad-x));
4439
4559
  }
4440
4560
  }
4441
4561
  .fd-tlist-side {
@@ -4484,32 +4604,6 @@ button.fd-erow-metric:focus-visible {
4484
4604
  box-shadow: var(--e-2);
4485
4605
  color: var(--brand);
4486
4606
  }
4487
- /* TransferList's per-side slots. The footer is where a running totals line goes;
4488
- it sits on the panel, not on a card, and is separated by a rule rather than a
4489
- gap so it reads as a summary OF the list rather than another row IN it. */
4490
- .fd-tlist-header {
4491
- display: flex;
4492
- align-items: center;
4493
- gap: 8px;
4494
- min-width: 0;
4495
- }
4496
- .fd-tlist-footer {
4497
- display: flex;
4498
- align-items: center;
4499
- gap: 10px;
4500
- min-width: 0;
4501
- padding-top: 8px;
4502
- /* Indented to the x a row's own body starts at, so a totals strip in here lands
4503
- under the columns it totals rather than a card's width to the left of them.
4504
- The sum is the card's border and padding-left plus the drag grip and its gap
4505
- — every row in a TransferList is a drag source, so it is the same on all of
4506
- them. Spelled out rather than collapsed to 37px so it stays checkable against
4507
- .fd-erow if that padding ever moves. */
4508
- padding-left: calc(1px + 12px + 14px + 10px);
4509
- border-top: 1px solid var(--border);
4510
- font-size: var(--overline-size);
4511
- color: var(--text-2);
4512
- }
4513
4607
 
4514
4608
  /* --- ScoreMeter — a small categorical 0-100 bar, optionally a trigger ------ */
4515
4609
  .fd-scoremeter {
@@ -6560,3 +6654,191 @@ button.fd-erow-metric:focus-visible {
6560
6654
  .fd-spinner{animation-duration:1.6s}
6561
6655
  .fd-arrived{animation:none}
6562
6656
  }
6657
+
6658
+ /* FileViewer (MP-130): a preview pane plus an optional host-supplied side panel — the
6659
+ companion notes/metadata surface the app composes in, generic here as `sidePanel`.
6660
+ Two regions, one scroll region each (R-8): `.fd-fv-body` never scrolls, each pane owns
6661
+ its own overflow. Side by side while there's room; below a narrow width of the
6662
+ component's OWN box (a container query, not a media query — the same reasoning as
6663
+ `.fd-tlist`'s above, since a FileViewer opened full-page and one opened in a Drawer see
6664
+ completely different widths at the same viewport) the side panel becomes a tab next to
6665
+ Preview and exactly one pane is visible at a time. */
6666
+ .fd-fv {
6667
+ container-type: inline-size;
6668
+ display: flex;
6669
+ flex-direction: column;
6670
+ height: 100%;
6671
+ min-height: 0;
6672
+ background: var(--surface);
6673
+ color: var(--text);
6674
+ }
6675
+ .fd-fv-head {
6676
+ display: flex;
6677
+ align-items: center;
6678
+ gap: 10px;
6679
+ padding: 12px 16px;
6680
+ border-bottom: 1px solid var(--border);
6681
+ flex: none;
6682
+ }
6683
+ .fd-fv-head-icon {
6684
+ display: grid;
6685
+ place-items: center;
6686
+ width: 32px;
6687
+ height: 32px;
6688
+ border-radius: var(--r-md);
6689
+ background: var(--surface-2);
6690
+ color: var(--text-2);
6691
+ font-size: 17px;
6692
+ flex: none;
6693
+ }
6694
+ .fd-fv-head-titles {
6695
+ display: flex;
6696
+ flex-direction: column;
6697
+ min-width: 0;
6698
+ flex: 1 1 auto;
6699
+ }
6700
+ .fd-fv-head-title {
6701
+ font-weight: 600;
6702
+ overflow: hidden;
6703
+ text-overflow: ellipsis;
6704
+ white-space: nowrap;
6705
+ }
6706
+ .fd-fv-head-meta {
6707
+ font-size: var(--body-sm-size);
6708
+ color: var(--text-2);
6709
+ }
6710
+ .fd-fv-head-actions {
6711
+ display: flex;
6712
+ align-items: center;
6713
+ gap: 8px;
6714
+ flex: none;
6715
+ }
6716
+ .fd-fv-download-error {
6717
+ margin: 10px 16px 0;
6718
+ padding: 8px 12px;
6719
+ border-radius: var(--r-md);
6720
+ background: var(--danger-fill);
6721
+ border: 1px solid var(--danger-border);
6722
+ color: var(--danger-text);
6723
+ font-size: var(--body-sm-size);
6724
+ }
6725
+ /* Only ever shown below the narrow container-query breakpoint (see the block at the
6726
+ bottom of this section) — at full width both panes are already visible, so a tab
6727
+ bar choosing between them would be a control with nothing to do. */
6728
+ .fd-fv-tabs {
6729
+ display: none;
6730
+ flex: none;
6731
+ padding: 0 16px;
6732
+ border-bottom: 1px solid var(--border);
6733
+ }
6734
+ .fd-fv-body {
6735
+ display: flex;
6736
+ flex: 1 1 auto;
6737
+ min-height: 0;
6738
+ overflow: hidden;
6739
+ }
6740
+ .fd-fv-preview {
6741
+ flex: 1 1 auto;
6742
+ min-width: 0;
6743
+ min-height: 0;
6744
+ overflow: auto;
6745
+ padding: 16px;
6746
+ }
6747
+ .fd-fv-side {
6748
+ flex: 0 0 320px;
6749
+ min-width: 0;
6750
+ min-height: 0;
6751
+ overflow: auto;
6752
+ padding: 16px;
6753
+ border-left: 1px solid var(--border);
6754
+ background: var(--surface-2);
6755
+ }
6756
+ .fd-fv-image {
6757
+ display: block;
6758
+ max-width: 100%;
6759
+ height: auto;
6760
+ margin: 0 auto;
6761
+ border-radius: var(--r-md);
6762
+ }
6763
+ .fd-fv-pdf {
6764
+ display: block;
6765
+ width: 100%;
6766
+ height: 100%;
6767
+ min-height: 480px;
6768
+ border: 0;
6769
+ border-radius: var(--r-md);
6770
+ background: var(--surface-2);
6771
+ }
6772
+ .fd-fv-fallback {
6773
+ display: flex;
6774
+ flex-direction: column;
6775
+ align-items: center;
6776
+ justify-content: center;
6777
+ gap: 8px;
6778
+ height: 100%;
6779
+ min-height: 220px;
6780
+ text-align: center;
6781
+ color: var(--text-2);
6782
+ }
6783
+ .fd-fv-fallback-icon {
6784
+ display: grid;
6785
+ place-items: center;
6786
+ width: 56px;
6787
+ height: 56px;
6788
+ border-radius: var(--r-full);
6789
+ background: var(--surface-2);
6790
+ font-size: 26px;
6791
+ }
6792
+ .fd-fv-fallback-title {
6793
+ font-weight: 600;
6794
+ color: var(--text);
6795
+ }
6796
+ .fd-fv-fallback-meta {
6797
+ font-size: var(--body-sm-size);
6798
+ }
6799
+ .fd-fv-csv-table {
6800
+ width: 100%;
6801
+ border-collapse: collapse;
6802
+ font-size: var(--body-sm-size);
6803
+ }
6804
+ .fd-fv-csv-table th,
6805
+ .fd-fv-csv-table td {
6806
+ padding: 6px 10px;
6807
+ border-bottom: 1px solid var(--border);
6808
+ text-align: left;
6809
+ white-space: nowrap;
6810
+ }
6811
+ .fd-fv-csv-table th {
6812
+ color: var(--text-2);
6813
+ font-weight: 600;
6814
+ background: var(--surface-2);
6815
+ position: sticky;
6816
+ top: 0;
6817
+ }
6818
+ .fd-fv-csv-note {
6819
+ margin-top: 10px;
6820
+ }
6821
+ /* Below 640px of `.fd-fv`'s OWN width (not the viewport's — see the block comment
6822
+ above) there is no room for a preview beside a side panel: the tab bar takes over
6823
+ choosing which one is visible, and the two panes stack full-width instead of sitting
6824
+ side by side. Each pane keeps its own `overflow: auto` from the base rules above, so
6825
+ R-8 (one scroll region per pane) holds at this width too — there is just one pane on
6826
+ screen at a time instead of two. */
6827
+ @container (max-width: 640px) {
6828
+ .fd-fv-tabs {
6829
+ display: flex;
6830
+ }
6831
+ .fd-fv-body {
6832
+ flex-direction: column;
6833
+ }
6834
+ .fd-fv-side {
6835
+ border-left: 0;
6836
+ border-top: 1px solid var(--border);
6837
+ }
6838
+ .fd-fv.is-tab-preview .fd-fv-side {
6839
+ display: none;
6840
+ }
6841
+ .fd-fv.is-tab-side .fd-fv-preview {
6842
+ display: none;
6843
+ }
6844
+ }
package/styles/tokens.css CHANGED
@@ -110,9 +110,18 @@
110
110
  usual badge sense (e.g. --ok-fill/--danger-fill) is only ~1.05:1 from --erow-danger-fill
111
111
  and fails outright; this is the lightest fill that still clears the bar next to a red
112
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;
113
+ fill and 7:1+ off both grounds, so it reads even where hue alone would not.
114
+
115
+ MP-114 hover/active: darkened the same way --btn-solid-* darkens for its own
116
+ hover/active (light theme gets darker as you press deeper). -bg-hover is 8% darker
117
+ than -bg, -bg-active 16% darker; white ink stays 5.19:1+ / 5.97:1+ and the fill stays
118
+ 4.45:1+ against --erow-danger-fill (the tighter of the two grounds) at every step.
119
+ These are real fills, not a `filter: brightness()` restatement — a filter changes
120
+ what the eye sees without changing `background-color`, so a contrast check (and a
121
+ person's actual reading of the button) that goes by the declared colour never sees
122
+ it move. */
123
+ --btn-soft-ok-bg:#557F6A;--btn-soft-ok-bg-hover:#4E7562;--btn-soft-ok-bg-active:#476B59;--btn-soft-ok-border:#145A38;--btn-soft-ok-ink:#FFFFFF;
124
+ --btn-soft-danger-bg:#C84E43;--btn-soft-danger-bg-hover:#B8483E;--btn-soft-danger-bg-active:#A84238;--btn-soft-danger-border:#862720;--btn-soft-danger-ink:#FFFFFF;
116
125
 
117
126
  --ok-fill:#DFF3E8;--ok-border:#A8DDC2;--ok-text:#0E5233;--ok-solid:#34A26B;--ok-emph:#57C68C;
118
127
  --warn-fill:#FDF1DA;--warn-border:#F0D49A;--warn-text:#6B4A05;--warn-solid:#C8890B;
@@ -279,9 +288,16 @@
279
288
  --surface-2 (#1B1F27) and --erow-danger-fill (#331813): white ink is 5.4:1 or better,
280
289
  the fill is 3.0:1 or better against either ground, and the border (the theme's own
281
290
  --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;
291
+ both grounds.
292
+
293
+ MP-114 hover/active: rest is already only ~3.0:1 off --surface-2, the floor, so
294
+ darkening further (the light-theme direction) would drop the fill below 3:1 — both
295
+ states have to LIGHTEN here instead, same direction as --btn-solid-* on this ground.
296
+ -bg-hover is +6% toward white, -bg-active +9%/+12% (ok/danger): white ink stays
297
+ 4.61:1+ and the fill stays 3.30:1+ against both --surface-2 and --erow-danger-fill at
298
+ every step. Real fills, not `filter: brightness()` — see the :root comment above. */
299
+ --btn-soft-ok-bg:#007A3D;--btn-soft-ok-bg-hover:#0F8249;--btn-soft-ok-bg-active:#17864E;--btn-soft-ok-border:#3FB37A;--btn-soft-ok-ink:#FFFFFF;
300
+ --btn-soft-danger-bg:#D41200;--btn-soft-danger-bg-hover:#D7200F;--btn-soft-danger-bg-active:#D92E1F;--btn-soft-danger-border:#E8776D;--btn-soft-danger-ink:#FFFFFF;
285
301
 
286
302
  /* chat kit — see the :root block above */
287
303
  --chat-code-bg:#12151B;