@databricks/design-system 2.0.5 → 2.0.6

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/design-guidance/docs/README.md +40 -0
  3. package/design-guidance/docs/review-contract.md +22 -0
  4. package/design-guidance/docs/tokens.md +35 -0
  5. package/design-guidance/docs/voice.md +40 -0
  6. package/design-guidance/skills/dbui-build-screen.md +24 -0
  7. package/design-guidance/skills/dbui-check-ecosystem.md +21 -0
  8. package/design-guidance/skills/dbui-check-guidelines.md +29 -0
  9. package/design-guidance/skills/dbui-check-principles.md +26 -0
  10. package/design-guidance/skills/dbui-check-voice.md +17 -0
  11. package/design-guidance/skills/dbui-pick-component.md +23 -0
  12. package/design-guidance/skills/dbui-pick-icon.md +23 -0
  13. package/design-guidance/skills/dbui-review.md +30 -0
  14. package/design-guidance/skills/dbui-validate.md +23 -0
  15. package/dist/{RHFControlledTypeaheadComboboxV2-DsH-nIEk.js → RHFControlledTypeaheadComboboxV2-Ct9x3rC1.js} +5 -5
  16. package/dist/{RHFControlledTypeaheadComboboxV2-DsH-nIEk.js.map → RHFControlledTypeaheadComboboxV2-Ct9x3rC1.js.map} +1 -1
  17. package/dist/{WizardStepContentWrapper-Bq1wm0V7.js → WizardStepContentWrapper-6YTjeEaA.js} +477 -450
  18. package/dist/WizardStepContentWrapper-6YTjeEaA.js.map +1 -0
  19. package/dist/icon-metadata.json +2064 -0
  20. package/dist/{index-BRAVZg-S.js → index-DiRpSwH2.js} +239 -38
  21. package/dist/index-DiRpSwH2.js.map +1 -0
  22. package/dist/index-dark.css +458 -190
  23. package/dist/index-dark.mitigated.css +460 -192
  24. package/dist/index.css +1415 -1082
  25. package/dist/index.js +4 -4
  26. package/dist/index.mitigated.css +1417 -1084
  27. package/dist/patterns.js +3 -3
  28. package/dist-types/design-system/Drawer/Drawer.d.ts +3 -1
  29. package/dist-types/design-system/Icon/AntdIcon.d.ts +3 -0
  30. package/dist-types/design-system/Icon/Icon.d.ts +20 -6
  31. package/dist-types/design-system/Icon/NativeIcon.d.ts +3 -0
  32. package/dist-types/design-system/Icon/__generated/icons/LakewatchIcon.d.ts +4 -0
  33. package/dist-types/design-system/Icon/__generated/icons/index.d.ts +1 -0
  34. package/dist-types/design-system/Icon/index.d.ts +1 -0
  35. package/dist-types/design-system/Icon/utils.d.ts +1 -0
  36. package/dist-types/design-system/_shared_/Tabs/styles.d.ts +2 -3
  37. package/dist-types/~patterns/Wizard/WizardModal.d.ts +1 -1
  38. package/package.json +4 -2
  39. package/dist/WizardStepContentWrapper-Bq1wm0V7.js.map +0 -1
  40. package/dist/index-BRAVZg-S.js.map +0 -1
@@ -15872,7 +15872,7 @@ span.du-bois-light-typography-ellipsis-single-line {
15872
15872
 
15873
15873
  --db-surface-selected: var(--db-color-table-background-selected-hover);
15874
15874
  --db-surface-selected-hover: initial; /* TODO */;
15875
- --db-surface-selected-active: initial; /* TODO */;
15875
+ --db-surface-selected-active: var(--db-color-table-background-selected-hover); /* no distinct active value */
15876
15876
 
15877
15877
  /* Surface alpha-blending modifiers */
15878
15878
  --db-surface-backdrop: initial; /* TODO */;
@@ -15898,7 +15898,7 @@ span.du-bois-light-typography-ellipsis-single-line {
15898
15898
  --db-accent-surface-strong: var(--db-color-action-primary-background-hover);
15899
15899
 
15900
15900
  --db-accent-surface-hover: var(--db-color-action-primary-background-hover);
15901
- --db-accent-surface-active: initial; /* TODO */
15901
+ --db-accent-surface-active: var(--db-color-action-primary-background-press); /* no distinct active value */
15902
15902
  --db-accent-surface-selected: var(--db-color-action-primary-background-press);
15903
15903
 
15904
15904
  /* In the current design, the text color for an accent surface does not change with hover, active, etc. */
@@ -15915,7 +15915,7 @@ span.du-bois-light-typography-ellipsis-single-line {
15915
15915
  --db-negative-surface-strong: var(--db-color-action-danger-primary-background-hover);
15916
15916
 
15917
15917
  --db-negative-surface-hover: var(--db-color-action-danger-primary-background-hover);
15918
- --db-negative-surface-active: initial; /* TODO */
15918
+ --db-negative-surface-active: var(--db-color-action-danger-primary-background-press); /* no distinct active value */
15919
15919
  --db-negative-surface-selected: var(--db-color-action-danger-primary-background-press);
15920
15920
 
15921
15921
  --db-negative-text: var(--db-color-text-validation-danger);
@@ -15956,6 +15956,11 @@ span.du-bois-light-typography-ellipsis-single-line {
15956
15956
  --db-positive-border: initial; /* TODO */
15957
15957
  --db-positive-border-hover: initial; /* TODO */
15958
15958
  --db-positive-border-active: initial; /* TODO */
15959
+
15960
+ /* Branded */
15961
+ --db-ai-gradient-start: #4299e0;
15962
+ --db-ai-gradient-mid: #ca42e0;
15963
+ --db-ai-gradient-end: #ff5f46;
15959
15964
  }
15960
15965
 
15961
15966
 
@@ -16036,6 +16041,47 @@ span.du-bois-light-typography-ellipsis-single-line {
16036
16041
  --db-shape-pill: 999px;
16037
16042
  }
16038
16043
 
16044
+ :where(:root), :where(.du-bois-light) {
16045
+ /* Shape */
16046
+ /* Mirrors the ShapeTokens host-override var used by list items; falls back to the theme radius. */
16047
+ --db-list-item-border-radius: var(--list-item-border-radius, 4px);
16048
+
16049
+ /* Color */
16050
+ --db-list-surface: var(--db-surface-base);
16051
+ --db-list-text: var(--db-text-base);
16052
+ --db-list-header-text: var(--db-text-subtle);
16053
+ --db-list-separator: var(--db-border-subtle);
16054
+
16055
+ --db-list-item-surface-hover: var(--db-surface-hover);
16056
+ --db-list-item-surface-active: var(--db-surface-active);
16057
+ --db-list-item-surface-selected: var(--db-accent-surface-subtle);
16058
+ --db-list-item-surface-disabled: var(--db-surface-disabled);
16059
+
16060
+ --db-list-item-text-selected: var(--db-accent-text);
16061
+ --db-list-item-text-disabled: var(--db-text-disabled);
16062
+
16063
+ --db-list-item-danger-text: var(--db-negative-text);
16064
+ --db-list-item-danger-surface-hover: var(--db-negative-surface-subtle);
16065
+
16066
+ /* Density */
16067
+ --db-list-item-padding: var(--db-space-2);
16068
+ --db-list-item-height: var(--db-size-8);
16069
+ }
16070
+
16071
+ :where(:root), :where(.du-bois-light) {
16072
+ /* Shape */
16073
+ /* Mirrors the ShapeTokens host-override var; falls back to the v1 value (borderRadiusSm). */
16074
+ --db-popup-border-radius: var(--popup-border-radius, 4px);
16075
+
16076
+ /* Color */
16077
+ --db-popup-surface: var(--db-surface-base);
16078
+ --db-popup-text: var(--db-text-base);
16079
+ --db-popup-border: var(--db-border-base);
16080
+
16081
+ /* Elevation */
16082
+ --db-popup-elevation: var(--db-elevation-med);
16083
+ }
16084
+
16039
16085
  :where(:root), :where(.du-bois-light) {
16040
16086
  /* Shape */
16041
16087
  --db-selection-toggle-border-radius: 4px;
@@ -16047,15 +16093,15 @@ span.du-bois-light-typography-ellipsis-single-line {
16047
16093
  --db-selection-toggle-border: var(--db-border-strong);
16048
16094
 
16049
16095
  --db-selection-toggle-surface-hover: var(--db-surface-hover);
16050
- --db-selection-toggle-surface-press: var(--db-surface-selected);
16096
+ --db-selection-toggle-surface-active: var(--db-surface-selected-active);
16051
16097
  --db-selection-toggle-surface-checked: var(--db-accent-surface);
16052
16098
  --db-selection-toggle-surface-checked-hover: var(--db-accent-surface-hover);
16053
- --db-selection-toggle-surface-checked-press: var(--db-accent-surface-selected);
16099
+ --db-selection-toggle-surface-checked-active: var(--db-accent-surface-active);
16054
16100
  --db-selection-toggle-surface-disabled: var(--db-surface-disabled);
16055
16101
  --db-selection-toggle-surface-checked-disabled: var(--db-surface-disabled);
16056
16102
 
16057
16103
  --db-selection-toggle-border-hover: var(--db-accent-surface-hover);
16058
- --db-selection-toggle-border-press: var(--db-accent-surface-selected);
16104
+ --db-selection-toggle-border-active: var(--db-accent-surface-active);
16059
16105
  --db-selection-toggle-border-disabled: var(--db-border-base);
16060
16106
 
16061
16107
  --db-selection-toggle-mark-disabled: var(--db-text-disabled);
@@ -16071,1322 +16117,1638 @@ span.du-bois-light-typography-ellipsis-single-line {
16071
16117
  --db-selection-toggle-transition-duration: 120ms;
16072
16118
  }
16073
16119
 
16074
- :where(:root), :where(.du-bois-light) {
16075
- /* Shape */
16076
- --db-checkbox-border-radius: var(--db-selection-toggle-border-radius);
16077
- --db-checkbox-border-width: 1px;
16078
-
16079
- /* Color */
16080
- --db-checkbox-surface: var(--db-selection-toggle-surface);
16081
- --db-checkbox-surface-hover: var(--db-color-action-default-background-hover);
16082
- --db-checkbox-surface-press: var(--db-color-action-default-background-press);
16083
- --db-checkbox-surface-checked: var(--db-selection-toggle-surface-checked);
16084
- --db-checkbox-surface-checked-hover: var(--db-selection-toggle-surface-checked-hover);
16085
- --db-checkbox-surface-checked-press: var(--db-selection-toggle-surface-checked-press);
16086
- --db-checkbox-surface-disabled: var(--db-selection-toggle-surface-disabled);
16087
-
16088
- --db-checkbox-border: var(--db-color-action-default-border-default);
16089
- --db-checkbox-border-hover: var(--db-selection-toggle-border-hover);
16090
- --db-checkbox-border-press: var(--db-selection-toggle-border-press);
16091
- --db-checkbox-focus-ring: var(--db-focus-ring-color);
16092
- --db-checkbox-border-disabled: var(--db-color-action-disabled-border);
16093
16120
 
16094
- --db-checkbox-mark: #ffffff;
16095
- --db-checkbox-mark-disabled: var(--db-selection-toggle-mark-disabled);
16096
- --db-checkbox-text: var(--db-selection-toggle-text);
16097
- --db-checkbox-text-disabled: var(--db-selection-toggle-text-disabled);
16098
16121
 
16099
- --db-checkbox-shadow-checked: var(--db-selection-toggle-shadow-checked);
16122
+ /* Component-tier tokens for the native <Button>, grouped by area (density, shape, typography, color).
16123
+ * Colors alias the action-* system tokens; sizes alias the system density scale.
16124
+ *
16125
+ * Follows the mortar/action.css pattern: one semantic set (surface / border / text, each with
16126
+ * -hover / -active / -disabled) whose defaults are the secondary variant. The variant classes that
16127
+ * re-set these tokens per variant live in NativeButton.css; this file declares only tokens. */
16100
16128
 
16101
- /* Density */
16102
- --db-checkbox-size: var(--db-selection-toggle-size);
16103
- --db-checkbox-label-spacing: var(--db-selection-toggle-label-spacing);
16104
- --db-checkbox-focus-ring-width: var(--db-focus-ring-width);
16105
- --db-checkbox-indeterminate-mark-width: var(--db-size-2); /* 8px */
16106
- --db-checkbox-indeterminate-mark-height: calc(var(--db-size-1) * 0.75); /* 3px */
16129
+ /* Declared under the theme classes too (not just :root) so the var() chain re-resolves inside a
16130
+ * `.du-bois-dark` subtree (or a portal that re-applies the theme class) instead of freezing at the root values. */
16131
+ :where(:root), :where(.du-bois-light) {
16132
+ /* ── Density ──────────────────────────────────────────────────────────── */
16133
+ --db-btn-height: var(--db-size-8);
16134
+ --db-btn-height-small: var(--db-size-6);
16135
+ --db-btn-icon-size: var(--db-size-4);
16136
+ --db-btn-border-width: 1px;
16137
+ --db-btn-padding-vertical: var(--db-space-1);
16138
+ --db-btn-padding-horizontal: var(--db-space-3);
16139
+ --db-btn-padding-horizontal-small: var(--db-space-2);
16140
+ /* Icon/force-icon horizontal padding: half the standard horizontal padding. */
16141
+ --db-btn-padding-horizontal-icon: calc(var(--db-space-3) / 2); /* 6px */
16142
+ --db-btn-gap: var(--db-space-1);
16143
+ --db-btn-end-icon-gap: var(--db-space-2);
16144
+ --db-btn-end-icon-gap-small: var(--db-space-1);
16145
+ /* Width of a content-less button while loading, so the standalone spinner has room. */
16146
+ --db-btn-loading-empty-width: var(--db-size-10);
16107
16147
 
16108
- /* Motion */
16109
- --db-checkbox-transition-duration: var(--db-selection-toggle-transition-duration);
16110
- }
16148
+ /* ── Shape ────────────────────────────────────────────────────────────── */
16149
+ --db-btn-border-radius: var(--db-shape-control);
16111
16150
 
16112
- /* Native-input <Checkbox> styles. Classic CSS only — no native nesting (the
16113
- * `&`-style nesting feature ships in Chrome/Edge 120, Safari 17.2, Firefox 117
16114
- * which leaves a non-trivial slice of our production traffic uncovered today;
16115
- * see PR #1954620 thread). `:has()` lookups are still used to read state from
16116
- * the native <input>; that feature has the support coverage we need.
16117
- *
16118
- * Structure (assumed by the React component):
16119
- *
16120
- * <label class="db-checkbox">
16121
- * <input type="checkbox" class="db-checkbox-input" />
16122
- * <span class="db-checkbox-indicator"> ← border + fill react to state
16123
- * <svg class="db-checkbox-checkmark" .../> ← visible when :checked
16124
- * <span class="db-checkbox-dash" /> ← visible when :indeterminate
16125
- * </span>
16126
- * <span class="db-checkbox-label">{children}</span>
16127
- * </label>
16128
- *
16129
- * State and hiding rules target `.db-checkbox-input` — the checkbox's OWN input —
16130
- * never a bare `input`: consumers may render an inline <input> (e.g. a number
16131
- * field) inside the label children, and a descendant `input` selector would hide
16132
- * / absolutely-position that field and mis-drive the `:has(…)` state rules. */
16151
+ /* ── Typography ───────────────────────────────────────────────────────── */
16152
+ --db-btn-font-size: var(--db-font-size-base);
16153
+ --db-btn-icon-only-font-size: var(--db-font-size-lg); /* 15px */
16154
+ --db-btn-font-weight: var(--db-font-weight-normal);
16155
+ /* Line heights are hard-coded until the typography scale ships computed per-level line-height tokens. */
16156
+ --db-btn-line-height: 1.25rem; /* 20px */
16157
+ --db-btn-line-height-small: 1rem; /* 16px */
16133
16158
 
16134
- .db-checkbox {
16135
- /* `display: flex` (block-level), NOT `inline-flex`: an inline-flex root puts
16136
- * the label inside an inline-line-box whose leading pushes block-level siblings
16137
- * (e.g. `<FormMessage>`) down by a few pixels — making validation messages sit
16138
- * lower under the native checkbox than under the AntD checkbox (which itself is
16139
- * wrapped in a block `<div>`, so its label's inline-line-box is consumed inside
16140
- * that div and never leaks into sibling spacing). `width: fit-content` keeps the
16141
- * label behaving like an inline-flex item inside a flex-row parent (the group,
16142
- * or any consumer's flex container). */
16143
- display: flex;
16144
- align-items: flex-start;
16145
- position: relative;
16146
- cursor: pointer;
16147
- user-select: none;
16148
- /* No `gap` here — spacing between indicator and label, plus the trailing
16149
- * whitespace that gives sibling checkboxes their visual separation, lives
16150
- * on `.db-checkbox-label` as padding-inline. Matches AntD's `.{prefix}-checkbox + span`
16151
- * rule and the prior Emotion-based NativeCheckbox implementation. */
16152
- min-height: var(--db-line-height-base, 20px);
16153
- width: fit-content;
16154
- }
16159
+ /* ── Color: semantic set (defaults = secondary variant) ───────────────── */
16160
+ --db-btn-surface: var(--db-color-action-default-background-default);
16161
+ --db-btn-surface-hover: var(--db-color-action-default-background-hover);
16162
+ --db-btn-surface-active: var(--db-color-action-default-background-press);
16163
+ --db-btn-surface-disabled: transparent;
16164
+ --db-btn-border: var(--db-color-action-default-border-default);
16165
+ --db-btn-border-hover: var(--db-color-action-default-border-hover);
16166
+ --db-btn-border-active: var(--db-color-action-default-border-press);
16167
+ --db-btn-border-disabled: var(--db-color-action-disabled-border);
16168
+ --db-btn-text: var(--db-color-action-default-text-default);
16169
+ --db-btn-text-hover: var(--db-color-action-default-text-hover);
16170
+ --db-btn-text-active: var(--db-color-action-default-text-press);
16171
+ --db-btn-text-disabled: var(--db-color-action-disabled-text);
16155
16172
 
16156
- /* Hover/press live on the root (label), not on the input: the input is
16157
- * visually hidden (opacity:0), so an `input:hover` rule wouldn't be visible
16158
- * anyway. Putting `:hover` on the root also makes the entire label area
16159
- * — not just the indicator square — drive the visual feedback. */
16160
- .db-checkbox:hover:has(.db-checkbox-input:not(:disabled, :checked, :indeterminate)) .db-checkbox-indicator {
16161
- background-color: var(--db-checkbox-surface-hover);
16162
- border-color: var(--db-checkbox-border-hover);
16173
+ /* ── Focus & icon ─────────────────────────────────────────────────────── */
16174
+ --db-btn-focus-ring: var(--db-color-action-default-border-focus);
16175
+ --db-btn-danger-focus-ring: var(--db-color-action-danger-primary-background-default);
16176
+ --db-btn-icon-glyph: var(--db-color-text-secondary);
16163
16177
  }
16164
16178
 
16165
- .db-checkbox:active:has(.db-checkbox-input:not(:disabled, :checked, :indeterminate)) .db-checkbox-indicator {
16166
- background-color: var(--db-checkbox-surface-press);
16167
- border-color: var(--db-checkbox-border-press);
16168
- }
16179
+ /* Native <Button> visual styles. Consumes the component-tier `--db-btn-*` tokens (see tokens.css);
16180
+ * the variant classes below re-set those tokens per variant and the rules that follow paint them.
16181
+ * Shipped via mortar.css. */
16169
16182
 
16170
- /* Selected fill checked and indeterminate share the same surface. */
16171
- .db-checkbox:has(.db-checkbox-input:is(:checked, :indeterminate)) .db-checkbox-indicator {
16172
- background-color: var(--db-checkbox-surface-checked);
16173
- border-color: var(--db-checkbox-surface-checked);
16183
+ /* ── 0. Variant token re-sets ──────────────────────────────────────────────
16184
+ * Each variant class re-sets the semantic tokens declared in tokens.css; combined classes win by
16185
+ * specificity, which beats the `:where(:root)` (specificity 0) defaults regardless of file order. */
16186
+ .db-btn-primary {
16187
+ --db-btn-surface: var(--db-color-action-primary-background-default);
16188
+ --db-btn-surface-hover: var(--db-color-action-primary-background-hover);
16189
+ --db-btn-surface-active: var(--db-color-action-primary-background-press);
16190
+ --db-btn-surface-disabled: var(--db-color-action-disabled-border);
16191
+ --db-btn-border: transparent;
16192
+ --db-btn-border-hover: transparent;
16193
+ --db-btn-border-active: transparent;
16194
+ --db-btn-border-disabled: transparent;
16195
+ --db-btn-text: var(--db-color-action-primary-text-default);
16196
+ --db-btn-text-hover: var(--db-color-action-primary-text-hover);
16197
+ --db-btn-text-active: var(--db-color-action-primary-text-press);
16198
+ --db-btn-text-disabled: var(--db-color-action-primary-text-default);
16174
16199
  }
16175
-
16176
- .db-checkbox:hover:has(.db-checkbox-input:is(:checked, :indeterminate):not(:disabled)) .db-checkbox-indicator {
16177
- background-color: var(--db-checkbox-surface-checked-hover);
16178
- border-color: var(--db-checkbox-surface-checked-hover);
16200
+ .db-btn-tertiary,
16201
+ .db-btn-link {
16202
+ --db-btn-surface: var(--db-color-action-tertiary-background-default);
16203
+ --db-btn-surface-disabled: var(--db-color-action-tertiary-background-default);
16204
+ --db-btn-border: var(--db-color-action-tertiary-background-default);
16205
+ --db-btn-border-hover: transparent;
16206
+ --db-btn-border-active: transparent;
16207
+ --db-btn-border-disabled: transparent;
16208
+ --db-btn-text: var(--db-color-action-tertiary-text-default);
16209
+ --db-btn-text-hover: var(--db-color-action-tertiary-text-hover);
16210
+ --db-btn-text-active: var(--db-color-action-tertiary-text-press);
16211
+ --db-btn-text-disabled: var(--db-color-action-disabled-text);
16179
16212
  }
16180
-
16181
- .db-checkbox:active:has(.db-checkbox-input:is(:checked, :indeterminate):not(:disabled)) .db-checkbox-indicator {
16182
- background-color: var(--db-checkbox-surface-checked-press);
16183
- border-color: var(--db-checkbox-surface-checked-press);
16213
+ .db-btn-tertiary {
16214
+ --db-btn-surface-hover: var(--db-color-action-tertiary-background-hover);
16215
+ --db-btn-surface-active: var(--db-color-action-tertiary-background-press);
16184
16216
  }
16185
-
16186
- /* Mark visibility. */
16187
- .db-checkbox:has(.db-checkbox-input:checked) .db-checkbox-checkmark {
16188
- opacity: 1;
16217
+ /* Link is borderless and text-only, so its hover/press surface stays transparent. */
16218
+ .db-btn-link {
16219
+ --db-btn-surface-hover: transparent;
16220
+ --db-btn-surface-active: transparent;
16189
16221
  }
16190
-
16191
- .db-checkbox:has(.db-checkbox-input:indeterminate) .db-checkbox-dash {
16192
- opacity: 1;
16222
+ /* Every danger button (icon included) disables to the filled treatment. */
16223
+ .db-btn-danger {
16224
+ --db-btn-surface-disabled: var(--db-color-action-disabled-border);
16225
+ --db-btn-border-disabled: transparent;
16226
+ --db-btn-text-disabled: var(--db-color-action-primary-text-default);
16193
16227
  }
16194
-
16195
- /* Focus-visible (keyboard only). */
16196
- .db-checkbox:has(.db-checkbox-input:focus-visible) .db-checkbox-indicator {
16197
- outline: var(--db-checkbox-focus-ring-width) solid var(--db-checkbox-focus-ring);
16198
- outline-offset: 4px;
16228
+ /* Danger, filled (primary): re-set only the surface; text/transparent border come from primary. */
16229
+ .db-btn-primary.db-btn-danger:not(.db-btn-icon-only) {
16230
+ --db-btn-surface: var(--db-color-action-danger-primary-background-default);
16231
+ --db-btn-surface-hover: var(--db-color-action-danger-primary-background-hover);
16232
+ --db-btn-surface-active: var(--db-color-action-danger-primary-background-press);
16199
16233
  }
16200
-
16201
- /* Disabled (covers unchecked, checked, indeterminate). Sourced from
16202
- * `input:disabled` so the standalone-disabled and group-disabled paths both
16203
- * land here without needing a separate root modifier class. */
16204
- .db-checkbox:has(.db-checkbox-input:disabled) {
16205
- cursor: not-allowed;
16234
+ /* Danger, secondary/outlined. */
16235
+ .db-btn-danger:not(.db-btn-primary):not(.db-btn-tertiary):not(.db-btn-link):not(.db-btn-icon-only) {
16236
+ --db-btn-surface: var(--db-color-action-danger-default-background-default);
16237
+ --db-btn-surface-hover: var(--db-color-action-danger-default-background-hover);
16238
+ --db-btn-surface-active: var(--db-color-action-danger-default-background-press);
16239
+ --db-btn-border: var(--db-color-action-danger-default-border-default);
16240
+ --db-btn-border-hover: var(--db-color-action-danger-default-border-hover);
16241
+ --db-btn-border-active: var(--db-color-action-danger-default-border-press);
16242
+ --db-btn-text: var(--db-color-action-danger-default-text-default);
16243
+ --db-btn-text-hover: var(--db-color-action-danger-default-text-hover);
16244
+ --db-btn-text-active: var(--db-color-action-danger-default-text-press);
16206
16245
  }
16207
-
16208
- .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-label {
16209
- color: var(--db-checkbox-text-disabled);
16246
+ /* Danger, tertiary/link: danger text everywhere, danger surface on hover/press; resting/disabled
16247
+ * surface stay the tertiary ghost (override the filled danger-disabled set above). */
16248
+ .db-btn-tertiary.db-btn-danger,
16249
+ .db-btn-link.db-btn-danger {
16250
+ --db-btn-surface-hover: var(--db-color-action-danger-default-background-hover);
16251
+ --db-btn-surface-active: var(--db-color-action-danger-default-background-press);
16252
+ --db-btn-surface-disabled: var(--db-color-action-tertiary-background-default);
16253
+ --db-btn-text: var(--db-color-action-danger-default-text-default);
16254
+ --db-btn-text-hover: var(--db-color-action-danger-default-text-hover);
16255
+ --db-btn-text-active: var(--db-color-action-danger-default-text-press);
16256
+ --db-btn-text-disabled: var(--db-color-action-disabled-text);
16210
16257
  }
16211
-
16212
- .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-indicator {
16213
- background-color: var(--db-checkbox-surface-disabled);
16214
- border-color: var(--db-checkbox-border-disabled);
16215
- box-shadow: none;
16258
+ /* Icon-only renders as the ghost (tertiary) treatment regardless of variant; re-set the semantic set
16259
+ * so the icon-only rules paint from these component tokens. */
16260
+ .db-btn-icon-only {
16261
+ --db-btn-surface: var(--db-color-action-tertiary-background-default);
16262
+ --db-btn-surface-hover: var(--db-color-action-tertiary-background-hover);
16263
+ --db-btn-surface-active: var(--db-color-action-tertiary-background-press);
16264
+ --db-btn-text: var(--db-color-action-tertiary-text-default);
16265
+ --db-btn-text-hover: var(--db-color-action-tertiary-text-hover);
16266
+ --db-btn-text-active: var(--db-color-action-tertiary-text-press);
16216
16267
  }
16217
-
16218
- .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-checkmark {
16219
- color: var(--db-checkbox-mark-disabled);
16268
+ /* Danger icon-only keeps the ghost resting surface (inherited above) but uses danger text and a
16269
+ * danger hover/press surface. */
16270
+ .db-btn-icon-only.db-btn-danger {
16271
+ --db-btn-surface-hover: var(--db-color-action-danger-default-background-hover);
16272
+ --db-btn-surface-active: var(--db-color-action-danger-default-background-press);
16273
+ --db-btn-text: var(--db-color-action-danger-default-text-default);
16274
+ --db-btn-text-hover: var(--db-color-action-danger-default-text-hover);
16275
+ --db-btn-text-active: var(--db-color-action-danger-default-text-press);
16220
16276
  }
16221
16277
 
16222
- .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-dash {
16223
- background-color: var(--db-checkbox-mark-disabled);
16278
+ /* ── 1. Base — paints the default (secondary) variant ────────────────────── */
16279
+ .db-btn {
16280
+ appearance: none;
16281
+ box-sizing: border-box;
16282
+ position: relative; /* anchors the loading spinner */
16283
+ display: inline-flex !important;
16284
+ align-items: center !important;
16285
+ justify-content: center !important;
16286
+ vertical-align: middle;
16287
+ white-space: nowrap;
16288
+ user-select: none;
16289
+ cursor: pointer;
16290
+ height: var(--db-btn-height) !important;
16291
+ font-family: inherit;
16292
+ font-size: var(--db-btn-font-size);
16293
+ font-weight: var(--db-btn-font-weight);
16294
+ line-height: var(--db-btn-line-height) !important;
16295
+ text-decoration: none !important;
16296
+ border: var(--db-btn-border-width) solid var(--db-btn-border);
16297
+ border-color: var(--db-btn-border) !important;
16298
+ border-radius: var(--db-btn-border-radius) !important;
16299
+ box-shadow: none !important;
16300
+ background-color: var(--db-btn-surface) !important;
16301
+ color: var(--db-btn-text) !important;
16224
16302
  }
16225
16303
 
16226
- /* Visually-hidden native input (opacity:0) overlaying the indicator square,
16227
- * kept interactive and in the tab order so `:has()` lookups above can read its
16228
- * state. It stays pointer-clickable — matching AntD's `.ant-checkbox-input`
16229
- * so it is the top-most hit-test target over the box: `getByRole('checkbox').click()`
16230
- * (Playwright, and any coordinate click that lands on the box) resolves to the
16231
- * real control instead of the decorative indicator overlay. Making the input
16232
- * `pointer-events: none` (the prior native-only behavior) is what regressed
16233
- * interaction tests vs. the AntD variant. `cursor: inherit` tracks the label's
16234
- * pointer / disabled `not-allowed` cursor so the box keeps AntD's cursor too. */
16235
- .db-checkbox .db-checkbox-input {
16236
- position: absolute;
16237
- inset: 0;
16238
- inline-size: var(--db-checkbox-size);
16239
- block-size: var(--db-checkbox-size);
16240
- margin: 0;
16241
- opacity: 0;
16242
- cursor: inherit;
16304
+ /* ── 2. Padding ──────────────────────────────────────────────────────────── */
16305
+ .db-btn.db-btn:where(:not(.db-btn-icon-only, .db-btn-force-icon, .db-btn-link)) {
16306
+ padding: var(--db-btn-padding-vertical) var(--db-btn-padding-horizontal) !important;
16243
16307
  }
16244
-
16245
- /* Indicator square with the border/fill that responds to state above.
16246
- * Vertical math: aligns the indicator to the cap-height of the first text
16247
- * line so checkbox + label sit on the same visual baseline even when the
16248
- * label wraps. */
16249
- .db-checkbox .db-checkbox-indicator {
16250
- display: grid;
16251
- place-items: center;
16252
- flex-shrink: 0;
16253
- /* Decorative only — the overlaid input owns hit-testing. Without this, the
16254
- * indicator (and its inherited checkmark/dash children) intercept
16255
- * `getByRole('checkbox').click()`; clicks on any edge sliver the input does
16256
- * not cover fall through to the label, which toggles via label/input
16257
- * association. */
16258
- pointer-events: none;
16259
- inline-size: var(--db-checkbox-size);
16260
- block-size: var(--db-checkbox-size);
16261
- margin-top: calc((var(--db-line-height-base, 20px) - var(--db-checkbox-size)) / 2);
16262
- border: var(--db-checkbox-border-width) solid var(--db-checkbox-border);
16263
- border-radius: var(--db-checkbox-border-radius);
16264
- background-color: var(--db-checkbox-surface);
16265
- box-sizing: border-box;
16266
- transition:
16267
- background-color var(--db-checkbox-transition-duration) ease,
16268
- border-color var(--db-checkbox-transition-duration) ease,
16269
- box-shadow var(--db-checkbox-transition-duration) ease;
16308
+ .db-btn.db-btn-small:where(:not(.db-btn-icon-only, .db-btn-force-icon, .db-btn-link)) {
16309
+ padding: 0 var(--db-btn-padding-horizontal-small) !important;
16270
16310
  }
16271
-
16272
- .db-checkbox .db-checkbox-indicator > .db-checkbox-checkmark {
16273
- grid-area: 1 / 1;
16274
- inline-size: 100%;
16275
- block-size: 100%;
16276
- color: var(--db-checkbox-mark);
16277
- opacity: 0;
16278
- transition: opacity var(--db-checkbox-transition-duration) ease;
16311
+ .db-btn.db-btn-link {
16312
+ padding: 0 !important;
16279
16313
  }
16280
-
16281
- .db-checkbox .db-checkbox-indicator > .db-checkbox-checkmark path {
16282
- fill: none;
16283
- stroke: currentColor;
16284
- stroke-width: 2;
16285
- stroke-linecap: round;
16286
- stroke-linejoin: round;
16314
+ .db-btn.db-btn-force-icon {
16315
+ padding: 0 var(--db-btn-padding-horizontal-icon) !important;
16287
16316
  }
16288
16317
 
16289
- .db-checkbox .db-checkbox-indicator > .db-checkbox-dash {
16290
- grid-area: 1 / 1;
16291
- inline-size: var(--db-checkbox-indeterminate-mark-width);
16292
- block-size: var(--db-checkbox-indeterminate-mark-height);
16293
- /* CSS clamps this to half the 3px height, giving fully rounded end caps so the
16294
- * dash reads as a pill rather than a flat bar. */
16295
- border-radius: 4px;
16296
- background-color: var(--db-checkbox-mark);
16297
- opacity: 0;
16298
- transition: opacity var(--db-checkbox-transition-duration) ease;
16318
+ /* ── 3. Interactive states ───────────────────────────────────────────────── */
16319
+ .db-btn:hover:not(:disabled, .db-btn-loading) {
16320
+ background-color: var(--db-btn-surface-hover) !important;
16321
+ border-color: var(--db-btn-border-hover) !important;
16322
+ color: var(--db-btn-text-hover) !important;
16299
16323
  }
16300
-
16301
- /* Padding-inline replicates AntD's `.{prefix}-checkbox + span { padding: 0 8px }`.
16302
- * `padding-left` provides the visible gap between indicator and label text.
16303
- * `padding-right` extends the checkbox's bounding box so adjacent checkboxes in a
16304
- * flex row sit ~16px apart (8px trailing + 8px group `gap`) — same visual rhythm
16305
- * as the AntD variant. */
16306
- .db-checkbox .db-checkbox-label {
16307
- color: var(--db-checkbox-text);
16308
- padding-inline: var(--db-checkbox-label-spacing);
16309
- word-break: break-word;
16324
+ .db-btn:active:not(:disabled, .db-btn-loading) {
16325
+ background-color: var(--db-btn-surface-active) !important;
16326
+ border-color: var(--db-btn-border-active) !important;
16327
+ color: var(--db-btn-text-active) !important;
16310
16328
  }
16311
16329
 
16312
- /* Form-flow integration: when a checkbox or group sits next to a `<Hint>` or
16313
- * `<FormMessage>` in a form layout, collapse the inherited sibling margin and
16314
- * align the helper text under the checkbox label. Mirrors the AntD-era rules
16315
- * carried in `getWrapperStyle` and `getNativeCheckboxWrapperStyles`.
16316
- *
16317
- * Each helper-text class is listed once per theme (`du-bois-light-*` /
16318
- * `du-bois-dark-*`). The selectors deliberately repeat the root/group class
16319
- * (`.db-checkbox.db-checkbox`) to push specificity to (0,3,0). That
16320
- * overrides `FormMessage`'s own `&&`-doubled `margin-top: 8px` declared with
16321
- * Emotion at runtime — without doubling we'd lose on the specificity tie and
16322
- * the validation message would keep an 8px gap below the checkbox. */
16323
-
16324
- /* Single checkbox followed by Hint and/or FormMessage. Indents the helper text
16325
- * by 24px so it aligns with the start of the label, and removes the default
16326
- * vertical margin Hint/FormMessage carry. */
16327
- .db-checkbox.db-checkbox + .du-bois-light-hint,
16328
- .db-checkbox.db-checkbox + .du-bois-dark-hint,
16329
- .db-checkbox.db-checkbox + .du-bois-light-form-message,
16330
- .db-checkbox.db-checkbox + .du-bois-dark-form-message,
16331
- .db-checkbox.db-checkbox + .du-bois-light-hint + .du-bois-light-form-message,
16332
- .db-checkbox.db-checkbox + .du-bois-dark-hint + .du-bois-dark-form-message {
16333
- padding-left: var(--db-space-6);
16334
- margin-top: 0;
16330
+ /* ── 4. Focus ────────────────────────────────────────────────────────────── */
16331
+ .db-btn:focus-visible {
16332
+ outline: 2px solid var(--db-btn-focus-ring);
16333
+ outline-offset: -2px;
16335
16334
  }
16336
-
16337
- /* Group spacing against sibling form text — 8px gap above the group when it
16338
- * follows a Hint, 8px gap above a FormMessage when it follows the group.
16339
- * Doubled class on the group side beats FormMessage's `&&` for the same reason. */
16340
- .du-bois-light-hint + .db-checkbox-group.db-checkbox-group,
16341
- .du-bois-dark-hint + .db-checkbox-group.db-checkbox-group,
16342
- .db-checkbox-group.db-checkbox-group + .du-bois-light-form-message,
16343
- .db-checkbox-group.db-checkbox-group + .du-bois-dark-form-message {
16344
- margin-top: var(--db-space-2);
16335
+ .db-btn-primary:focus-visible {
16336
+ outline-offset: 1px;
16345
16337
  }
16346
-
16347
- /* CheckboxGroup layout. Vertical delegates to the `.db-stack` primitive; the
16348
- * horizontal default stays bespoke because it must NOT wrap (a long row of
16349
- * checkboxes overflows horizontally rather than reflowing onto multiple lines),
16350
- * which is the one thing `.db-cluster` won't do. Both wire their gap to the
16351
- * checkbox spacing token so a retheme moves with the rest of the component. */
16352
- .db-checkbox-group {
16353
- display: flex;
16338
+ .db-btn-use-focus-pseudo-class:focus {
16339
+ outline: 2px solid var(--db-btn-focus-ring);
16340
+ outline-offset: 1px;
16354
16341
  }
16355
16342
 
16356
- /* Horizontal (default): single non-wrapping row. `:not(.db-stack)` keeps this
16357
- * off the vertical element so it never fights the stack primitive's gap. */
16358
- .db-checkbox-group:not(.db-stack) {
16359
- flex-wrap: nowrap;
16360
- column-gap: var(--db-checkbox-label-spacing);
16361
- row-gap: 0;
16343
+ /* ── 5. Small / block (full-width) / empty ────────────────────────────────── */
16344
+ .db-btn-small {
16345
+ height: var(--db-btn-height-small) !important;
16362
16346
  }
16363
-
16364
- /* Vertical: .db-stack supplies flex-direction + gap; we only retarget the knob. */
16365
- .db-checkbox-group.db-stack {
16366
- --db-stack-gap: var(--db-checkbox-label-spacing);
16347
+ .db-btn-block {
16348
+ width: 100%;
16349
+ }
16350
+ .db-btn-empty {
16351
+ background-color: transparent !important;
16352
+ border-color: transparent !important;
16353
+ }
16354
+ .db-btn-default.db-btn-empty {
16355
+ color: var(--db-btn-icon-glyph) !important;
16356
+ }
16357
+ .db-btn-empty.db-btn-loading {
16358
+ width: var(--db-btn-loading-empty-width);
16367
16359
  }
16368
16360
 
16369
- :where(:root), :where(.du-bois-light) {
16370
- /* Shape */
16371
- --db-radio-border-radius: var(--db-radius-full);
16372
- --db-radio-border-width: 1px;
16373
-
16374
- /* Color */
16375
- --db-radio-surface: var(--db-selection-toggle-surface);
16376
- --db-radio-surface-hover: var(--db-color-action-default-background-hover);
16377
- --db-radio-surface-press: var(--db-color-action-default-background-press);
16378
- --db-radio-surface-checked: var(--db-selection-toggle-surface-checked);
16379
- --db-radio-surface-checked-hover: var(--db-selection-toggle-surface-checked-hover);
16380
- --db-radio-surface-checked-press: var(--db-color-action-default-background-press);
16381
- --db-radio-surface-disabled: var(--db-selection-toggle-surface-disabled);
16382
-
16383
- --db-radio-border: var(--db-color-action-default-border-default);
16384
- --db-radio-border-hover: var(--db-selection-toggle-border-hover);
16385
- --db-radio-border-press: var(--db-selection-toggle-border-press);
16386
- --db-radio-border-focus: var(--db-focus-ring-color);
16387
- --db-radio-border-disabled: var(--db-color-action-disabled-border);
16388
- --db-radio-border-checked-hover: var(--db-selection-toggle-border-press);
16389
- --db-radio-border-checked-press: var(--db-color-action-default-border-press);
16390
- --db-radio-border-unchecked-focus: var(--db-color-action-primary-background-default);
16391
- --db-radio-outline-checked-focus: var(--db-color-action-primary-background-default);
16392
-
16393
- --db-radio-mark: #ffffff;
16394
- --db-radio-mark-disabled: var(--db-selection-toggle-mark-disabled);
16395
- --db-radio-text: var(--db-selection-toggle-text);
16396
- --db-radio-text-disabled: var(--db-selection-toggle-text-disabled);
16397
-
16398
- /* Density */
16399
- --db-radio-size: var(--db-selection-toggle-size);
16400
- --db-radio-label-spacing: var(--db-selection-toggle-label-spacing);
16401
- --db-radio-mark-size: var(--db-size-1);
16361
+ /* ── 6. Tertiary & link ──────────────────────────────────────────────────── */
16362
+ .db-btn-link {
16363
+ height: auto !important;
16364
+ border: none !important;
16365
+ }
16366
+ /* Re-declare the text color for a non-icon-only tertiary/link at two-class weight so a plain
16367
+ * call-site `color` override loses; an icon-only glyph stays at base weight so its override wins. */
16368
+ .db-btn-tertiary:not(.db-btn-icon-only),
16369
+ .db-btn-link:not(.db-btn-icon-only) {
16370
+ color: var(--db-btn-text) !important;
16371
+ }
16372
+ /* Keep the resting border at two-class weight (parity with the shielded border-color). */
16373
+ .db-btn.db-btn-tertiary,
16374
+ .db-btn.db-btn-link {
16375
+ border-color: var(--db-btn-border) !important;
16376
+ }
16402
16377
 
16403
- /* Motion */
16404
- --db-radio-transition-duration: var(--db-selection-toggle-transition-duration);
16378
+ /* ── 7. Danger ───────────────────────────────────────────────────────────── */
16379
+ .db-btn-danger:focus-visible {
16380
+ outline-color: var(--db-btn-danger-focus-ring);
16405
16381
  }
16406
16382
 
16407
- /* Native-input <Radio> styles. Classic CSS only no native nesting (the
16408
- * `&`-style nesting feature ships in Chrome/Edge 120, Safari 17.2, Firefox 117
16409
- * which leaves a non-trivial slice of our production traffic uncovered today;
16410
- * see PR #1954620 thread). `:has()` lookups are still used to read state from
16411
- * the native <input>; that feature has the support coverage we need.
16412
- *
16413
- * Structure (assumed by the React component):
16414
- *
16415
- * <label class="db-radio">
16416
- * <input type="radio" />
16417
- * <span class="db-radio-indicator"> ← circle border + fill react to state
16418
- * <span class="db-radio-dot" /> ← visible when :checked
16419
- * </span>
16420
- * <span class="db-radio-label">{children}</span>
16421
- * </label>
16422
- *
16423
- * The root only ever has one <input> descendant, so a bare `input` selector is
16424
- * safe and clearer than a class. Mirrors NativeCheckbox.css; the differences are
16425
- * the circular indicator (border-radius: full) and the filled dot instead of a
16426
- * checkmark/dash, and there is no indeterminate state.
16383
+ /* ── 8. Icon-only ────────────────────────────────────────────────────────── */
16384
+ .db-btn-icon-only {
16385
+ padding: 0;
16386
+ border: none !important;
16387
+ background-color: var(--db-btn-surface) !important;
16388
+ font-size: var(--db-btn-icon-only-font-size);
16389
+ }
16390
+ /* `:where()` keeps this at (0,1,0) so a call-site `css` width override wins, as it does for `height`. */
16391
+ .db-btn-icon-only:where(:not(.db-btn-small)) {
16392
+ width: var(--db-btn-height) !important;
16393
+ }
16394
+ .db-btn-icon-only:not(.db-btn-link, .db-btn-tertiary, [href], :hover, :active, :disabled) {
16395
+ color: var(--db-btn-icon-glyph) !important;
16396
+ }
16397
+ /* `:where()` on the exclusion holds this at (0,2,0): enough to tint an un-overridden icon-only link
16398
+ * over the base color, while still letting a consumer's own icon-color override win on source order. */
16399
+ .db-btn-icon-only[href]:where(:not(.db-btn-tertiary, .db-btn-link, .db-btn-danger, :disabled)) {
16400
+ color: var(--db-btn-text) !important;
16401
+ }
16402
+ .db-btn-icon-only:where(.db-btn-small) {
16403
+ width: var(--db-btn-height-small);
16404
+ }
16405
+ .db-btn-icon-only.db-btn-small {
16406
+ line-height: var(--db-btn-line-height-small) !important;
16407
+ }
16408
+ .db-btn-icon-only:hover:not(:disabled, .db-btn-loading) {
16409
+ background-color: var(--db-btn-surface-hover) !important;
16410
+ color: var(--db-btn-text-hover) !important;
16411
+ }
16412
+ .db-btn-icon-only:active:not(:disabled, .db-btn-loading) {
16413
+ background-color: var(--db-btn-surface-active) !important;
16414
+ color: var(--db-btn-text-active) !important;
16415
+ }
16416
+ .db-btn-icon-only:enabled:not(.db-btn-primary, .db-btn-tertiary, .db-btn-link, .db-btn-danger, :hover, :active)
16417
+ > .anticon {
16418
+ color: var(--db-btn-icon-glyph);
16419
+ }
16420
+ .db-btn-icon-only.db-btn-danger:not(:hover, :active) > .anticon {
16421
+ color: var(--db-btn-text);
16422
+ }
16423
+ .db-btn-icon-only.db-btn-danger:hover:not(:disabled, .db-btn-loading) {
16424
+ background-color: var(--db-btn-surface-hover) !important;
16425
+ color: var(--db-btn-text-hover) !important;
16426
+ }
16427
+ .db-btn-icon-only.db-btn-danger:active:not(:disabled, .db-btn-loading) {
16428
+ background-color: var(--db-btn-surface-active) !important;
16429
+ color: var(--db-btn-text-active) !important;
16430
+ }
16431
+
16432
+ /* ── 9. Disabled — base rules paint the per-variant `*-disabled` tokens (see tokens.css) ──────── */
16433
+ .db-btn:disabled {
16434
+ cursor: not-allowed;
16435
+ background-color: var(--db-btn-surface-disabled) !important;
16436
+ color: var(--db-btn-text-disabled) !important;
16437
+ }
16438
+ /* Empty disabled buttons keep a transparent border (set elsewhere), so exclude them.
16439
+ * `:where()` on the exclusion holds this at (0,2,0): enough to beat the base border, while still
16440
+ * letting a consumer's own `[disabled]` border override win on source order. */
16441
+ .db-btn:disabled:where(:not(.db-btn-empty)) {
16442
+ border-color: var(--db-btn-border-disabled) !important;
16443
+ }
16444
+ .db-btn-icon-only:disabled {
16445
+ border: none;
16446
+ }
16447
+ .db-btn-icon-only:disabled:hover {
16448
+ background-color: transparent !important;
16449
+ }
16450
+ .db-btn:disabled > .anticon {
16451
+ color: inherit !important;
16452
+ }
16453
+
16454
+ /* ── 10. Icon & content layout ───────────────────────────────────────────── */
16455
+ /* The leading icon renders unwrapped as the button's direct `.anticon` child (see NativeButton.tsx),
16456
+ * so `> .anticon` targets only a real icon glyph — a non-glyph `icon` (e.g. a Spinner) keeps its own
16457
+ * sizing — and out-specifies AntD's runtime-injected global `.anticon` rule. */
16458
+ .db-btn > .anticon,
16459
+ .db-btn-end-icon,
16460
+ .db-btn-end-icon > .anticon {
16461
+ display: inline-flex;
16462
+ align-items: center;
16463
+ font-size: var(--db-btn-icon-size) !important;
16464
+ line-height: 0;
16465
+ }
16466
+ .db-btn-small > .anticon .anticon {
16467
+ line-height: 0;
16468
+ }
16469
+ .db-btn > .db-btn-content > .anticon {
16470
+ display: inline-flex;
16471
+ align-items: center;
16472
+ line-height: 0;
16473
+ }
16474
+ .db-btn-content {
16475
+ display: inline-flex;
16476
+ align-items: center;
16477
+ }
16478
+ .db-btn > .anticon + .db-btn-content {
16479
+ margin-inline-start: var(--db-btn-gap) !important;
16480
+ }
16481
+ .db-btn-end-icon {
16482
+ margin-inline-start: var(--db-btn-end-icon-gap) !important;
16483
+ }
16484
+ .db-btn-small .db-btn-end-icon {
16485
+ margin-inline-start: var(--db-btn-end-icon-gap-small) !important;
16486
+ }
16487
+ .db-btn-default:not(.db-btn-danger, [href], :hover, :active, :disabled) > .anticon {
16488
+ color: var(--db-btn-icon-glyph);
16489
+ }
16490
+ .db-btn-default[href]:not(.db-btn-danger, :hover, :active, :disabled) .db-btn-end-icon {
16491
+ color: var(--db-btn-icon-glyph);
16492
+ }
16493
+ .db-btn-default:not([href], .db-btn-danger, :hover, :active, :disabled)
16494
+ > .db-btn-content
16495
+ > .db-btn-end-icon
16496
+ > .anticon {
16497
+ color: var(--db-btn-icon-glyph);
16498
+ }
16499
+
16500
+ /* ── 11. Loading ─────────────────────────────────────────────────────────── */
16501
+ .db-btn-loading {
16502
+ cursor: default;
16503
+ }
16504
+ .db-btn-force-icon.db-btn-loading:disabled {
16505
+ border-color: transparent !important;
16506
+ }
16507
+ .db-btn-spinner {
16508
+ color: inherit !important;
16509
+ pointer-events: none;
16510
+ }
16511
+ .db-btn-loading-with-icon .db-btn-content {
16512
+ padding-inline-start: calc(var(--db-btn-icon-size) + var(--db-btn-gap));
16513
+ }
16514
+ .db-btn-loading .db-btn-spinner {
16515
+ position: absolute;
16516
+ left: 50%;
16517
+ top: 50%;
16518
+ transform: translate(-50%, -50%);
16519
+ }
16520
+ .db-btn-loading.db-btn-icon-only .db-btn-spinner {
16521
+ position: static;
16522
+ transform: none;
16523
+ }
16524
+
16525
+ /* ── 12. dangerouslySetForceIconStyles ───────────────────────────────────── */
16526
+ .db-btn-force-icon {
16527
+ width: var(--db-btn-height) !important;
16528
+ color: var(--db-btn-icon-glyph) !important;
16529
+ }
16530
+ .db-btn-force-icon.db-btn-small {
16531
+ width: var(--db-btn-height-small) !important;
16532
+ }
16533
+
16534
+
16535
+ :where(:root), :where(.du-bois-light) {
16536
+ /* Shape */
16537
+ --db-checkbox-border-radius: var(--db-selection-toggle-border-radius);
16538
+ --db-checkbox-border-width: 1px;
16539
+
16540
+ /* Color */
16541
+ --db-checkbox-surface: var(--db-selection-toggle-surface);
16542
+ --db-checkbox-surface-hover: var(--db-color-action-default-background-hover);
16543
+ --db-checkbox-surface-active: var(--db-color-action-default-background-press);
16544
+ --db-checkbox-surface-checked: var(--db-selection-toggle-surface-checked);
16545
+ --db-checkbox-surface-checked-hover: var(--db-selection-toggle-surface-checked-hover);
16546
+ --db-checkbox-surface-checked-active: var(--db-selection-toggle-surface-checked-active);
16547
+ --db-checkbox-surface-disabled: var(--db-selection-toggle-surface-disabled);
16548
+
16549
+ --db-checkbox-border: var(--db-color-action-default-border-default);
16550
+ --db-checkbox-border-hover: var(--db-selection-toggle-border-hover);
16551
+ --db-checkbox-border-active: var(--db-selection-toggle-border-active);
16552
+ --db-checkbox-focus-ring: var(--db-focus-ring-color);
16553
+ --db-checkbox-border-disabled: var(--db-color-action-disabled-border);
16554
+
16555
+ --db-checkbox-mark: #ffffff;
16556
+ --db-checkbox-mark-disabled: var(--db-selection-toggle-mark-disabled);
16557
+ --db-checkbox-text: var(--db-selection-toggle-text);
16558
+ --db-checkbox-text-disabled: var(--db-selection-toggle-text-disabled);
16559
+
16560
+ --db-checkbox-shadow-checked: var(--db-selection-toggle-shadow-checked);
16561
+
16562
+ /* Density */
16563
+ --db-checkbox-size: var(--db-selection-toggle-size);
16564
+ --db-checkbox-label-spacing: var(--db-selection-toggle-label-spacing);
16565
+ --db-checkbox-focus-ring-width: var(--db-focus-ring-width);
16566
+ --db-checkbox-indeterminate-mark-width: var(--db-size-2); /* 8px */
16567
+ --db-checkbox-indeterminate-mark-height: calc(var(--db-size-1) * 0.75); /* 3px */
16568
+
16569
+ /* Motion */
16570
+ --db-checkbox-transition-duration: var(--db-selection-toggle-transition-duration);
16571
+ }
16572
+
16573
+ /* Native-input <Checkbox> styles. Classic CSS only — no native nesting (the
16574
+ * `&`-style nesting feature ships in Chrome/Edge 120, Safari 17.2, Firefox 117
16575
+ * which leaves a non-trivial slice of our production traffic uncovered today;
16576
+ * see PR #1954620 thread). `:has()` lookups are still used to read state from
16577
+ * the native <input>; that feature has the support coverage we need.
16427
16578
  *
16428
- * TODO(DUBOIS-452): the indicator/dot state styles below are identical to
16429
- * NativeCheckbox.css — consolidate them into a shared `.db-selection-toggle` class. */
16579
+ * Structure (assumed by the React component):
16580
+ *
16581
+ * <label class="db-checkbox">
16582
+ * <input type="checkbox" class="db-checkbox-input" />
16583
+ * <span class="db-checkbox-indicator"> ← border + fill react to state
16584
+ * <svg class="db-checkbox-checkmark" .../> ← visible when :checked
16585
+ * <span class="db-checkbox-dash" /> ← visible when :indeterminate
16586
+ * </span>
16587
+ * <span class="db-checkbox-label">{children}</span>
16588
+ * </label>
16589
+ *
16590
+ * State and hiding rules target `.db-checkbox-input` — the checkbox's OWN input —
16591
+ * never a bare `input`: consumers may render an inline <input> (e.g. a number
16592
+ * field) inside the label children, and a descendant `input` selector would hide
16593
+ * / absolutely-position that field and mis-drive the `:has(…)` state rules. */
16430
16594
 
16431
- .db-radio {
16432
- /* `display: flex` (block-level), NOT `inline-flex`: see NativeCheckbox.css for
16433
- * the line-box / sibling-spacing rationale. */
16595
+ .db-checkbox {
16596
+ /* `display: flex` (block-level), NOT `inline-flex`: an inline-flex root puts
16597
+ * the label inside an inline-line-box whose leading pushes block-level siblings
16598
+ * (e.g. `<FormMessage>`) down by a few pixels — making validation messages sit
16599
+ * lower under the native checkbox than under the AntD checkbox (which itself is
16600
+ * wrapped in a block `<div>`, so its label's inline-line-box is consumed inside
16601
+ * that div and never leaks into sibling spacing). `width: fit-content` keeps the
16602
+ * label behaving like an inline-flex item inside a flex-row parent (the group,
16603
+ * or any consumer's flex container). */
16434
16604
  display: flex;
16435
16605
  align-items: flex-start;
16436
16606
  position: relative;
16437
16607
  cursor: pointer;
16438
16608
  user-select: none;
16609
+ /* No `gap` here — spacing between indicator and label, plus the trailing
16610
+ * whitespace that gives sibling checkboxes their visual separation, lives
16611
+ * on `.db-checkbox-label` as padding-inline. Matches AntD's `.{prefix}-checkbox + span`
16612
+ * rule and the prior Emotion-based NativeCheckbox implementation. */
16439
16613
  min-height: var(--db-line-height-base, 20px);
16440
16614
  width: fit-content;
16441
16615
  }
16442
16616
 
16443
16617
  /* Hover/press live on the root (label), not on the input: the input is
16444
16618
  * visually hidden (opacity:0), so an `input:hover` rule wouldn't be visible
16445
- * anyway, and the whole label area should drive the feedback. */
16446
- .db-radio:hover:has(input:not(:disabled, :checked)) .db-radio-indicator {
16447
- background-color: var(--db-radio-surface-hover);
16448
- border-color: var(--db-radio-border-hover);
16619
+ * anyway. Putting `:hover` on the root also makes the entire label area
16620
+ * — not just the indicator square — drive the visual feedback. */
16621
+ .db-checkbox:hover:has(.db-checkbox-input:not(:disabled, :checked, :indeterminate)) .db-checkbox-indicator {
16622
+ background-color: var(--db-checkbox-surface-hover);
16623
+ border-color: var(--db-checkbox-border-hover);
16449
16624
  }
16450
16625
 
16451
- .db-radio:active:has(input:not(:disabled, :checked)) .db-radio-indicator {
16452
- background-color: var(--db-radio-surface-press);
16453
- border-color: var(--db-radio-border-press);
16626
+ .db-checkbox:active:has(.db-checkbox-input:not(:disabled, :checked, :indeterminate)) .db-checkbox-indicator {
16627
+ background-color: var(--db-checkbox-surface-active);
16628
+ border-color: var(--db-checkbox-border-active);
16454
16629
  }
16455
16630
 
16456
- /* Selected fill. */
16457
- .db-radio:has(input:checked) .db-radio-indicator {
16458
- background-color: var(--db-radio-surface-checked);
16459
- border-color: var(--db-radio-surface-checked);
16631
+ /* Selected fill — checked and indeterminate share the same surface. */
16632
+ .db-checkbox:has(.db-checkbox-input:is(:checked, :indeterminate)) .db-checkbox-indicator {
16633
+ background-color: var(--db-checkbox-surface-checked);
16634
+ border-color: var(--db-checkbox-surface-checked);
16460
16635
  }
16461
16636
 
16462
- /* AntD getRadioInputStyles checked hover: background = actionPrimaryBackgroundHover,
16463
- * borderColor = actionPrimaryBackgroundPress (a darker ring than the fill). */
16464
- .db-radio:hover:has(input:checked:not(:disabled)) .db-radio-indicator {
16465
- background-color: var(--db-radio-surface-checked-hover);
16466
- border-color: var(--db-radio-border-checked-hover);
16637
+ .db-checkbox:hover:has(.db-checkbox-input:is(:checked, :indeterminate):not(:disabled)) .db-checkbox-indicator {
16638
+ background-color: var(--db-checkbox-surface-checked-hover);
16639
+ border-color: var(--db-checkbox-surface-checked-hover);
16467
16640
  }
16468
16641
 
16469
- /* AntD getRadioInputStyles checked active: background = actionDefaultBackgroundPress,
16470
- * borderColor = actionDefaultBorderPress (two-tone, not flat). */
16471
- .db-radio:active:has(input:checked:not(:disabled)) .db-radio-indicator {
16472
- background-color: var(--db-radio-surface-checked-press);
16473
- border-color: var(--db-radio-border-checked-press);
16642
+ .db-checkbox:active:has(.db-checkbox-input:is(:checked, :indeterminate):not(:disabled)) .db-checkbox-indicator {
16643
+ background-color: var(--db-checkbox-surface-checked-active);
16644
+ border-color: var(--db-checkbox-surface-checked-active);
16474
16645
  }
16475
16646
 
16476
- /* Dot visibility. */
16477
- .db-radio:has(input:checked) .db-radio-dot {
16647
+ /* Mark visibility. */
16648
+ .db-checkbox:has(.db-checkbox-input:checked) .db-checkbox-checkmark {
16478
16649
  opacity: 1;
16479
16650
  }
16480
16651
 
16481
- /* Focus-visible (keyboard only). AntD getRadioInputStyles splits focus by checked
16482
- * state, so we do too.
16483
- *
16484
- * UNCHECKED focus (AntD `&:not(disabled) > input:focus + inner`): only the border
16485
- * color changes to actionPrimaryBackgroundDefault — NO outline ring. */
16486
- .db-radio:has(input:focus-visible:not(:checked)) .db-radio-indicator {
16487
- border-color: var(--db-radio-border-unchecked-focus);
16652
+ .db-checkbox:has(.db-checkbox-input:indeterminate) .db-checkbox-dash {
16653
+ opacity: 1;
16488
16654
  }
16489
16655
 
16490
- /* CHECKED focus-visible (AntD `&.checked &:not(disabled) > input:focus-visible + inner`):
16491
- * background = actionPrimaryBackgroundDefault, borderColor = actionDefaultBorderFocus,
16492
- * outline = 1px solid actionPrimaryBackgroundDefault, outlineOffset = 1. */
16493
- .db-radio:has(input:focus-visible:checked) .db-radio-indicator {
16494
- background-color: var(--db-radio-surface-checked);
16495
- border-color: var(--db-radio-border-focus);
16496
- outline: 1px solid var(--db-radio-outline-checked-focus);
16497
- outline-offset: 1px;
16656
+ /* Focus-visible (keyboard only). */
16657
+ .db-checkbox:has(.db-checkbox-input:focus-visible) .db-checkbox-indicator {
16658
+ outline: var(--db-checkbox-focus-ring-width) solid var(--db-checkbox-focus-ring);
16659
+ outline-offset: 4px;
16498
16660
  }
16499
16661
 
16500
- /* Disabled (covers checked and unchecked). Sourced from `input:disabled` so the
16501
- * standalone-disabled and group-disabled paths both land here. */
16502
- .db-radio:has(input:disabled) {
16662
+ /* Disabled (covers unchecked, checked, indeterminate). Sourced from
16663
+ * `input:disabled` so the standalone-disabled and group-disabled paths both
16664
+ * land here without needing a separate root modifier class. */
16665
+ .db-checkbox:has(.db-checkbox-input:disabled) {
16503
16666
  cursor: not-allowed;
16504
16667
  }
16505
16668
 
16506
- .db-radio:has(input:disabled) .db-radio-label {
16507
- color: var(--db-radio-text-disabled);
16669
+ .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-label {
16670
+ color: var(--db-checkbox-text-disabled);
16508
16671
  }
16509
16672
 
16510
- .db-radio:has(input:disabled) .db-radio-indicator {
16511
- background-color: var(--db-radio-surface-disabled);
16512
- border-color: var(--db-radio-border-disabled);
16673
+ .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-indicator {
16674
+ background-color: var(--db-checkbox-surface-disabled);
16675
+ border-color: var(--db-checkbox-border-disabled);
16513
16676
  box-shadow: none;
16514
16677
  }
16515
16678
 
16516
- .db-radio:has(input:disabled) .db-radio-dot {
16517
- background-color: var(--db-radio-mark-disabled);
16679
+ .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-checkmark {
16680
+ color: var(--db-checkbox-mark-disabled);
16518
16681
  }
16519
16682
 
16520
- /* Visually-hidden native input (opacity:0) overlaying the indicator circle,
16521
- * kept interactive and in the tab order so the `:has()` lookups above can read
16522
- * its state. It stays pointer-clickable — matching AntD's `.ant-radio-input` —
16523
- * so it is the top-most hit-test target over the circle: `getByRole('radio').click()`
16524
- * (Playwright, and any coordinate click that lands on the circle) resolves to the
16683
+ .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-dash {
16684
+ background-color: var(--db-checkbox-mark-disabled);
16685
+ }
16686
+
16687
+ /* Visually-hidden native input (opacity:0) overlaying the indicator square,
16688
+ * kept interactive and in the tab order so `:has()` lookups above can read its
16689
+ * state. It stays pointer-clickable — matching AntD's `.ant-checkbox-input` —
16690
+ * so it is the top-most hit-test target over the box: `getByRole('checkbox').click()`
16691
+ * (Playwright, and any coordinate click that lands on the box) resolves to the
16525
16692
  * real control instead of the decorative indicator overlay. Making the input
16526
16693
  * `pointer-events: none` (the prior native-only behavior) is what regressed
16527
16694
  * interaction tests vs. the AntD variant. `cursor: inherit` tracks the label's
16528
- * pointer / disabled `not-allowed` cursor so the circle keeps AntD's cursor too. */
16529
- .db-radio input {
16695
+ * pointer / disabled `not-allowed` cursor so the box keeps AntD's cursor too. */
16696
+ .db-checkbox .db-checkbox-input {
16530
16697
  position: absolute;
16531
16698
  inset: 0;
16532
- inline-size: var(--db-radio-size);
16533
- block-size: var(--db-radio-size);
16699
+ inline-size: var(--db-checkbox-size);
16700
+ block-size: var(--db-checkbox-size);
16534
16701
  margin: 0;
16535
16702
  opacity: 0;
16536
16703
  cursor: inherit;
16537
16704
  }
16538
16705
 
16539
- /* Indicator — circle with the border/fill that responds to state above.
16540
- * Vertical math aligns the indicator to the cap-height of the first text line
16541
- * so radio + label sit on the same visual baseline even when the label wraps. */
16542
- .db-radio .db-radio-indicator {
16706
+ /* Indicator — square with the border/fill that responds to state above.
16707
+ * Vertical math: aligns the indicator to the cap-height of the first text
16708
+ * line so checkbox + label sit on the same visual baseline even when the
16709
+ * label wraps. */
16710
+ .db-checkbox .db-checkbox-indicator {
16543
16711
  display: grid;
16544
16712
  place-items: center;
16545
16713
  flex-shrink: 0;
16546
16714
  /* Decorative only — the overlaid input owns hit-testing. Without this, the
16547
- * indicator (and its inherited dot child) intercept `getByRole('radio').click()`;
16548
- * clicks on any edge sliver the input does not cover fall through to the label,
16549
- * which toggles via label/input association. */
16715
+ * indicator (and its inherited checkmark/dash children) intercept
16716
+ * `getByRole('checkbox').click()`; clicks on any edge sliver the input does
16717
+ * not cover fall through to the label, which toggles via label/input
16718
+ * association. */
16550
16719
  pointer-events: none;
16551
- inline-size: var(--db-radio-size);
16552
- block-size: var(--db-radio-size);
16553
- margin-top: calc((var(--db-line-height-base, 20px) - var(--db-radio-size)) / 2);
16554
- border: var(--db-radio-border-width) solid var(--db-radio-border);
16555
- border-radius: var(--db-radio-border-radius);
16556
- background-color: var(--db-radio-surface);
16720
+ inline-size: var(--db-checkbox-size);
16721
+ block-size: var(--db-checkbox-size);
16722
+ margin-top: calc((var(--db-line-height-base, 20px) - var(--db-checkbox-size)) / 2);
16723
+ border: var(--db-checkbox-border-width) solid var(--db-checkbox-border);
16724
+ border-radius: var(--db-checkbox-border-radius);
16725
+ background-color: var(--db-checkbox-surface);
16557
16726
  box-sizing: border-box;
16558
16727
  transition:
16559
- background-color var(--db-radio-transition-duration) ease,
16560
- border-color var(--db-radio-transition-duration) ease,
16561
- box-shadow var(--db-radio-transition-duration) ease;
16728
+ background-color var(--db-checkbox-transition-duration) ease,
16729
+ border-color var(--db-checkbox-transition-duration) ease,
16730
+ box-shadow var(--db-checkbox-transition-duration) ease;
16562
16731
  }
16563
16732
 
16564
- .db-radio .db-radio-indicator > .db-radio-dot {
16733
+ .db-checkbox .db-checkbox-indicator > .db-checkbox-checkmark {
16565
16734
  grid-area: 1 / 1;
16566
- inline-size: var(--db-radio-mark-size);
16567
- block-size: var(--db-radio-mark-size);
16568
- border-radius: var(--db-radius-full);
16569
- background-color: var(--db-radio-mark);
16735
+ inline-size: 100%;
16736
+ block-size: 100%;
16737
+ color: var(--db-checkbox-mark);
16570
16738
  opacity: 0;
16571
- transition: opacity var(--db-radio-transition-duration) ease;
16739
+ transition: opacity var(--db-checkbox-transition-duration) ease;
16572
16740
  }
16573
16741
 
16574
- /* Honour the user's reduced-motion preference: drop the state-change
16575
- * transitions on the indicator and dot so checking/hovering is instant. */
16576
- @media (prefers-reduced-motion: reduce) {
16577
- .db-radio .db-radio-indicator,
16578
- .db-radio .db-radio-indicator > .db-radio-dot {
16579
- transition: none;
16580
- }
16742
+ .db-checkbox .db-checkbox-indicator > .db-checkbox-checkmark path {
16743
+ fill: none;
16744
+ stroke: currentColor;
16745
+ stroke-width: 2;
16746
+ stroke-linecap: round;
16747
+ stroke-linejoin: round;
16581
16748
  }
16582
16749
 
16583
- /* Padding-inline replicates the AntD `.{prefix} + span` label gap and the
16584
- * trailing whitespace that separates adjacent radios in a row. */
16585
- .db-radio .db-radio-label {
16586
- color: var(--db-radio-text);
16587
- padding-inline: var(--db-radio-label-spacing);
16588
- word-break: break-word;
16750
+ .db-checkbox .db-checkbox-indicator > .db-checkbox-dash {
16751
+ grid-area: 1 / 1;
16752
+ inline-size: var(--db-checkbox-indeterminate-mark-width);
16753
+ block-size: var(--db-checkbox-indeterminate-mark-height);
16754
+ /* CSS clamps this to half the 3px height, giving fully rounded end caps so the
16755
+ * dash reads as a pill rather than a flat bar. */
16756
+ border-radius: 4px;
16757
+ background-color: var(--db-checkbox-mark);
16758
+ opacity: 0;
16759
+ transition: opacity var(--db-checkbox-transition-duration) ease;
16589
16760
  }
16590
16761
 
16591
- /* Form-flow integration: align helper text under the radio label. The `paddingLeft`
16592
- * indent comes from AntD's getCommonRadioGroupStyles (`& > label + hint`) and applies
16593
- * to BOTH layouts. The doubled root class pushes specificity to (0,3,0) to beat
16594
- * FormMessage's runtime `&&` margin. */
16595
- .db-radio.db-radio + .du-bois-light-hint,
16596
- .db-radio.db-radio + .du-bois-dark-hint,
16597
- .db-radio.db-radio + .du-bois-light-form-message,
16598
- .db-radio.db-radio + .du-bois-dark-form-message,
16599
- .db-radio.db-radio + .du-bois-light-hint + .du-bois-light-form-message,
16600
- .db-radio.db-radio + .du-bois-dark-hint + .du-bois-dark-form-message {
16601
- padding-inline-start: var(--db-space-6);
16602
- margin-top: 0;
16762
+ /* Padding-inline replicates AntD's `.{prefix}-checkbox + span { padding: 0 8px }`.
16763
+ * `padding-left` provides the visible gap between indicator and label text.
16764
+ * `padding-right` extends the checkbox's bounding box so adjacent checkboxes in a
16765
+ * flex row sit ~16px apart (8px trailing + 8px group `gap`) — same visual rhythm
16766
+ * as the AntD variant. */
16767
+ .db-checkbox .db-checkbox-label {
16768
+ color: var(--db-checkbox-text);
16769
+ padding-inline: var(--db-checkbox-label-spacing);
16770
+ word-break: break-word;
16603
16771
  }
16604
16772
 
16605
- /* Vertical hint rhythm (AntD getVerticalRadioGroupStyles, vertical-only): a hint
16606
- * after a radio gets `marginTop: -sm` (cancels the preceding radio's
16607
- * `paddingBottom: sm` so the hint tucks directly under its label) and
16608
- * `marginBottom: sm` (gap before the next radio). The negative top margin relies on
16609
- * the .db-stack gap being 0 (set below). Scoped to .db-stack so it never perturbs
16610
- * the horizontal grid, where hints are positioned by grid-row instead. */
16611
- .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-light-hint,
16612
- .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-dark-hint,
16613
- .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-light-form-message,
16614
- .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-dark-form-message,
16615
- .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-light-hint + .du-bois-light-form-message,
16616
- .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-dark-hint + .du-bois-dark-form-message {
16617
- margin-top: calc(-1 * var(--db-space-2));
16618
- margin-bottom: var(--db-space-2);
16619
- }
16773
+ /* Form-flow integration: when a checkbox or group sits next to a `<Hint>` or
16774
+ * `<FormMessage>` in a form layout, collapse the inherited sibling margin and
16775
+ * align the helper text under the checkbox label. Mirrors the AntD-era rules
16776
+ * carried in `getWrapperStyle` and `getNativeCheckboxWrapperStyles`.
16777
+ *
16778
+ * Each helper-text class is listed once per theme (`du-bois-light-*` /
16779
+ * `du-bois-dark-*`). The selectors deliberately repeat the root/group class
16780
+ * (`.db-checkbox.db-checkbox`) to push specificity to (0,3,0). That
16781
+ * overrides `FormMessage`'s own `&&`-doubled `margin-top: 8px` declared with
16782
+ * Emotion at runtime — without doubling we'd lose on the specificity tie and
16783
+ * the validation message would keep an 8px gap below the checkbox. */
16620
16784
 
16621
- /* AntD `& > label:last-of-type + hint { marginTop: 0 }`: the last radio has no
16622
- * `paddingBottom` to cancel, so its trailing hint keeps its natural top position. */
16623
- .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-light-hint,
16624
- .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-dark-hint,
16625
- .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-light-form-message,
16626
- .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-dark-form-message,
16627
- .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-light-hint + .du-bois-light-form-message,
16628
- .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-dark-hint + .du-bois-dark-form-message {
16785
+ /* Single checkbox followed by Hint and/or FormMessage. Indents the helper text
16786
+ * by 24px so it aligns with the start of the label, and removes the default
16787
+ * vertical margin Hint/FormMessage carry. */
16788
+ .db-checkbox.db-checkbox + .du-bois-light-hint,
16789
+ .db-checkbox.db-checkbox + .du-bois-dark-hint,
16790
+ .db-checkbox.db-checkbox + .du-bois-light-form-message,
16791
+ .db-checkbox.db-checkbox + .du-bois-dark-form-message,
16792
+ .db-checkbox.db-checkbox + .du-bois-light-hint + .du-bois-light-form-message,
16793
+ .db-checkbox.db-checkbox + .du-bois-dark-hint + .du-bois-dark-form-message {
16794
+ padding-left: var(--db-space-6);
16629
16795
  margin-top: 0;
16630
16796
  }
16631
16797
 
16632
- .du-bois-light-hint + .db-radio-group.db-radio-group,
16633
- .du-bois-dark-hint + .db-radio-group.db-radio-group,
16634
- .db-radio-group.db-radio-group + .du-bois-light-form-message,
16635
- .db-radio-group.db-radio-group + .du-bois-dark-form-message {
16798
+ /* Group spacing against sibling form text — 8px gap above the group when it
16799
+ * follows a Label or Hint, 8px gap above a FormMessage when it follows the group.
16800
+ * Doubled class on the group side beats FormMessage's `&&` for the same reason. */
16801
+ .du-bois-light-label + .db-checkbox-group.db-checkbox-group,
16802
+ .du-bois-dark-label + .db-checkbox-group.db-checkbox-group,
16803
+ .du-bois-light-hint + .db-checkbox-group.db-checkbox-group,
16804
+ .du-bois-dark-hint + .db-checkbox-group.db-checkbox-group,
16805
+ .db-checkbox-group.db-checkbox-group + .du-bois-light-form-message,
16806
+ .db-checkbox-group.db-checkbox-group + .du-bois-dark-form-message {
16636
16807
  margin-top: var(--db-space-2);
16637
16808
  }
16638
16809
 
16639
- /* RadioGroup layout. Vertical (the default) delegates flex-direction to the
16640
- * .db-stack primitive; horizontal is a two-row grid (mirroring AntD). */
16641
- .db-radio-group {
16810
+ /* CheckboxGroup layout. Vertical delegates to the `.db-stack` primitive; the
16811
+ * horizontal default stays bespoke because it must NOT wrap (a long row of
16812
+ * checkboxes overflows horizontally rather than reflowing onto multiple lines),
16813
+ * which is the one thing `.db-cluster` won't do. Both wire their gap to the
16814
+ * checkbox spacing token so a retheme moves with the rest of the component. */
16815
+ .db-checkbox-group {
16642
16816
  display: flex;
16643
16817
  }
16644
16818
 
16645
- /* Vertical: .db-stack supplies flex-direction: column. AntD's vertical helper
16646
- * (getVerticalRadioGroupStyles) produces its item rhythm purely from
16647
- * `& > label { paddingBottom: sm }` plus the negative-margin hint trick above — it
16648
- * uses NO flex gap. The negative `marginTop` on hints only works if the stack adds
16649
- * no gap of its own, so we zero out the gap and reproduce the rhythm via
16650
- * padding-bottom on each radio. */
16651
- .db-radio-group.db-stack {
16652
- --db-stack-gap: 0;
16819
+ /* Horizontal (default): single non-wrapping row. `:not(.db-stack)` keeps this
16820
+ * off the vertical element so it never fights the stack primitive's gap. */
16821
+ .db-checkbox-group:not(.db-stack) {
16822
+ flex-wrap: nowrap;
16823
+ column-gap: var(--db-checkbox-label-spacing);
16824
+ row-gap: 0;
16653
16825
  }
16654
16826
 
16655
- /* AntD `& > label { paddingBottom: sm }`. */
16656
- .db-radio-group.db-stack > .db-radio {
16657
- padding-bottom: var(--db-space-2);
16827
+ /* Vertical: .db-stack supplies flex-direction + gap; we only retarget the knob. */
16828
+ .db-checkbox-group.db-stack {
16829
+ --db-stack-gap: var(--db-checkbox-label-spacing);
16658
16830
  }
16659
16831
 
16660
- /* AntD `& > label:last-of-type { paddingBottom: 0 }`. */
16661
- .db-radio-group.db-stack > .db-radio:last-of-type {
16662
- padding-bottom: 0;
16663
- }
16664
16832
 
16665
- /* AntD getVerticalRadioGroupStyles `.${prefix}-radio-tile + .${prefix}-radio-tile {
16666
- * marginTop: md }`: RadioTiles are not matched by the `.db-radio` padding-bottom
16667
- * rhythm above (the tile's own class is du-bois-{light,dark}-radio-tile), so space
16668
- * consecutive tiles explicitly. The .db-stack gap is 0, so this margin is the spacing. */
16669
- .db-radio-group.db-stack > .du-bois-light-radio-tile + .du-bois-light-radio-tile,
16670
- .db-radio-group.db-stack > .du-bois-light-radio-tile + .du-bois-dark-radio-tile,
16671
- .db-radio-group.db-stack > .du-bois-dark-radio-tile + .du-bois-light-radio-tile,
16672
- .db-radio-group.db-stack > .du-bois-dark-radio-tile + .du-bois-dark-radio-tile {
16673
- margin-top: var(--db-space-4);
16674
- }
16675
16833
 
16676
- /* Horizontal: AntD `getHorizontalRadioGroupStyles` is a TWO-ROW grid (NOT a flex
16677
- * row) so each radio sits in the [label] row and its following hint drops into the
16678
- * [hint] row of the same auto-column. `&&` in AntD doubles specificity; the doubled
16679
- * group class here matches that.
16680
- * AntD: `display: grid; gridTemplateRows: [label] auto [hint] auto;
16681
- * gridAutoColumns: max-content; gridColumnGap: md`. */
16682
- .db-radio-group--horizontal.db-radio-group--horizontal {
16683
- display: grid;
16684
- grid-template-rows: [label] auto [hint] auto;
16685
- grid-auto-columns: max-content;
16686
- column-gap: var(--db-space-4);
16687
- }
16834
+ :where(:root), :where(.du-bois-light) {
16835
+ /* Density */
16836
+ --db-drawer-padding-horizontal: var(--db-space-6);
16837
+ --db-drawer-padding-horizontal-small: var(--db-space-4);
16838
+ --db-drawer-content-padding-top: var(--db-space-4);
16839
+ --db-drawer-content-padding-top-small: var(--db-space-2);
16840
+ --db-drawer-footer-padding-top: var(--db-space-4);
16841
+ --db-drawer-footer-padding-bottom: var(--db-space-6);
16842
+ --db-drawer-footer-padding-bottom-small: var(--db-space-4);
16843
+ --db-drawer-title-margin-bottom: var(--db-space-2);
16844
+ --db-drawer-close-button-margin: var(--db-space-1);
16688
16845
 
16689
- /* Equal column widths: AntD `gridAutoColumns: minmax(0, 1fr)`. */
16690
- .db-radio-group--horizontal.db-radio-group--equal-width {
16691
- grid-auto-columns: minmax(0, 1fr);
16846
+ /* Color */
16847
+ --db-drawer-surface: var(--db-surface-base);
16848
+ --db-drawer-text: var(--db-text-base);
16849
+ --db-drawer-border: var(--db-border-base);
16850
+ /* No v2 token holds the drawer scrim yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
16851
+ --db-drawer-overlay: var(--db-color-overlay-overlay);
16692
16852
  }
16693
16853
 
16694
- /* AntD `& > label, & > .${prefix}-radio-tile { gridRow: label / label; marginRight: 0 }`:
16695
- * every radio (and every RadioTile button, whose own class is
16696
- * du-bois-{light,dark}-radio-tile and is NOT matched by `> .db-radio`) lives in the
16697
- * label row. grid-auto-flow defaults to row, so successive items advance the implicit
16698
- * column. */
16699
- .db-radio-group--horizontal > .db-radio,
16700
- .db-radio-group--horizontal > .du-bois-light-radio-tile,
16701
- .db-radio-group--horizontal > .du-bois-dark-radio-tile {
16702
- grid-row: label / label;
16703
- margin-inline-end: 0;
16854
+ :where(:root), :where(.du-bois-light) {
16855
+ /* Elevation */
16856
+ --db-drawer-shadow: var(--db-elevation-high);
16704
16857
  }
16705
16858
 
16706
- /* AntD `& > label + hint { display: inline-block; gridRow: hint / hint }`: a hint
16707
- * following a radio drops into the hint row of the column its radio occupies. The
16708
- * vertical negative-margin rules are scoped to .db-stack, so nothing to reset here. */
16709
- .db-radio-group--horizontal > .du-bois-light-hint,
16710
- .db-radio-group--horizontal > .du-bois-dark-hint {
16711
- display: inline-block;
16712
- grid-row: hint / hint;
16713
- }
16714
16859
 
16715
- /* AntD `:has(> hint) { marginTop: sm }`: when the horizontal group contains a hint,
16716
- * the group itself gets a top margin (room above the two-row grid). */
16717
- .db-radio-group--horizontal:has(> .du-bois-light-hint),
16718
- .db-radio-group--horizontal:has(> .du-bois-dark-hint) {
16719
- margin-top: var(--db-space-2);
16720
- }
16721
16860
 
16722
- /* AntD `&& + form-message { marginTop: sm }` (horizontal): a form-message following
16723
- * the whole group is separated by sm. */
16724
- .db-radio-group--horizontal + .du-bois-light-form-message,
16725
- .db-radio-group--horizontal + .du-bois-dark-form-message {
16726
- margin-top: var(--db-space-2);
16727
- }
16728
16861
 
16729
- :where(:root), :where(.du-bois-light) {
16730
- /* Shape */
16731
- --db-switch-border-radius: var(--db-radius-full);
16732
- --db-switch-border-width: 1px;
16733
16862
 
16734
- /* Color */
16735
- --db-switch-surface: var(--db-selection-toggle-surface);
16736
- --db-switch-surface-hover: var(--db-color-action-default-background-hover);
16737
- --db-switch-surface-press: var(--db-color-action-default-background-press);
16738
- --db-switch-surface-checked: var(--db-selection-toggle-surface-checked);
16739
- --db-switch-surface-checked-hover: var(--db-selection-toggle-surface-checked-hover);
16740
- --db-switch-surface-checked-press: var(--db-selection-toggle-surface-checked-press);
16741
- --db-switch-surface-disabled: var(--db-selection-toggle-surface-disabled);
16742
- --db-switch-surface-checked-disabled: var(--db-color-action-disabled-text);
16743
16863
 
16744
- --db-switch-border: var(--db-color-action-default-border-default);
16745
- --db-switch-border-hover: var(--db-selection-toggle-border-hover);
16746
- --db-switch-border-press: var(--db-selection-toggle-border-press);
16747
- --db-switch-border-disabled: var(--db-color-action-disabled-border);
16748
- --db-switch-border-checked: var(--db-selection-toggle-surface-checked);
16749
- --db-switch-border-checked-hover: var(--db-selection-toggle-surface-checked-hover);
16750
- --db-switch-border-checked-press: var(--db-selection-toggle-surface-checked-press);
16751
- --db-switch-border-focus: var(--db-selection-toggle-surface-checked);
16752
16864
 
16753
- --db-switch-thumb: var(--db-selection-toggle-surface);
16754
16865
 
16755
- --db-switch-text: var(--db-selection-toggle-text);
16756
- --db-switch-text-disabled: var(--db-selection-toggle-text-disabled);
16757
16866
 
16758
- /* Density */
16759
- --db-switch-track-width: calc(var(--db-size-4) * 1.75); /* 28px */
16760
- --db-switch-track-height: var(--db-size-4); /* 16px */
16761
- --db-switch-thumb-size: var(--db-switch-track-height);
16762
- --db-switch-label-spacing: var(--db-selection-toggle-label-spacing);
16763
16867
 
16764
- /* Motion */
16765
- --db-switch-transition-duration: var(--db-selection-toggle-transition-duration);
16868
+ /* Component tokens for the native Icon. */
16869
+ :where(:root),
16870
+ :where(.du-bois-light) {
16871
+ --db-icon-size: var(--db-font-size-xl);
16872
+ --db-icon-color-danger: var(--db-color-text-validation-danger);
16873
+ --db-icon-color-warning: var(--db-color-text-validation-warning);
16874
+ --db-icon-color-success: var(--db-color-text-validation-success);
16875
+ --db-icon-ai-gradient-start: var(--db-ai-gradient-start);
16876
+ --db-icon-ai-gradient-mid: var(--db-ai-gradient-mid);
16877
+ --db-icon-ai-gradient-end: var(--db-ai-gradient-end);
16766
16878
  }
16767
16879
 
16768
- .db-switch {
16769
- /* flex, not inline-flex: an inline-flex root's line-box leading pushes block siblings
16770
- * (e.g. a following FormMessage) down a few px, breaking the form-flow spacing. */
16771
- display: flex;
16772
- align-items: center;
16773
- cursor: pointer;
16774
- user-select: none;
16775
- width: fit-content;
16776
- margin-inline-end: var(--db-switch-label-spacing);
16880
+ /* Native Icon styles. Ships through mortar.css and reads only component-tier --db-icon-* tokens.
16881
+ * Icon size is driven solely by the `--db-icon-size` design token: to resize an icon, set that
16882
+ * token in scope (default 16px in tokens.css) rather than overriding `font-size`. */
16883
+ .db-icon {
16884
+ display: inline-block;
16885
+ color: inherit;
16886
+ font-style: normal;
16887
+ font-size: var(--db-icon-size);
16888
+ line-height: 0;
16889
+ text-align: center;
16890
+ text-transform: none;
16891
+ vertical-align: -0.125em;
16892
+ text-rendering: optimizelegibility;
16893
+ -webkit-font-smoothing: antialiased;
16894
+ -moz-osx-font-smoothing: grayscale;
16777
16895
  }
16778
16896
 
16779
- .db-switch:has(input:disabled) {
16780
- cursor: not-allowed;
16897
+ .db-icon svg {
16898
+ display: inline-block;
16899
+ line-height: 1;
16781
16900
  }
16782
16901
 
16783
- /* The track. `appearance: none` strips the native checkbox rendering so the input itself can be styled. */
16784
- .db-switch input {
16785
- appearance: none;
16786
- position: relative;
16787
- flex-shrink: 0;
16788
- margin: 0;
16902
+ /* TODO: remove once clickable-icon callsites are gone. Kept for backwards compatibility with the
16903
+ * AntD icon, which made icons clickable via onClick; some callsites still rely on it. Actual icons
16904
+ * should never carry a tabindex — a clickable icon belongs inside a button. */
16905
+ .db-icon[tabindex] {
16789
16906
  cursor: pointer;
16790
- inline-size: var(--db-switch-track-width);
16791
- block-size: var(--db-switch-track-height);
16792
- border: var(--db-switch-border-width) solid var(--db-switch-border);
16793
- border-radius: var(--db-switch-border-radius);
16794
- background-color: var(--db-switch-surface);
16795
- box-sizing: border-box;
16796
- transition:
16797
- background-color var(--db-switch-transition-duration) ease,
16798
- border-color var(--db-switch-transition-duration) ease;
16799
- }
16800
-
16801
- /* The thumb. A pseudo-element keeps the input the switch's only element, so nothing can overlay it
16802
- * and intercept clicks. Offset left by the border width to sit flush with the track's outer edge. */
16803
- .db-switch input::before {
16804
- content: '';
16805
- position: absolute;
16806
- top: 50%;
16807
- left: calc(-1 * var(--db-switch-border-width));
16808
- inline-size: var(--db-switch-thumb-size);
16809
- block-size: var(--db-switch-thumb-size);
16810
- border: var(--db-switch-border-width) solid;
16811
- border-color: inherit;
16812
- border-radius: var(--db-switch-border-radius);
16813
- background-color: var(--db-switch-thumb);
16814
- box-sizing: border-box;
16815
- transform: translateY(-50%);
16816
- transition: transform var(--db-switch-transition-duration) ease;
16817
16907
  }
16818
16908
 
16819
- /* `<label for>` propagates `:hover` to the input, so scope hover/press to the control, not `input:hover`. */
16820
- .db-switch:hover input:not(:disabled):not(:checked) {
16821
- background-color: var(--db-switch-surface-hover);
16822
- border-color: var(--db-switch-border-hover);
16909
+ /* Semantic colors set `color` only; the SVG glyph paints its `fill`/`stroke` from `currentColor`,
16910
+ * so setting the text color recolors the icon. */
16911
+ .db-icon-danger {
16912
+ color: var(--db-icon-color-danger);
16823
16913
  }
16824
16914
 
16825
- .db-switch:active input:not(:disabled):not(:checked) {
16826
- background-color: var(--db-switch-surface-press);
16827
- border-color: var(--db-switch-border-press);
16915
+ .db-icon-warning {
16916
+ color: var(--db-icon-color-warning);
16828
16917
  }
16829
16918
 
16830
- .db-switch input:checked {
16831
- background-color: var(--db-switch-surface-checked);
16832
- border-color: var(--db-switch-border-checked);
16919
+ .db-icon-success {
16920
+ color: var(--db-icon-color-success);
16833
16921
  }
16834
16922
 
16835
- .db-switch:hover input:checked:not(:disabled) {
16836
- background-color: var(--db-switch-surface-checked-hover);
16837
- border-color: var(--db-switch-border-checked-hover);
16923
+ .db-icon-ai svg * {
16924
+ fill: var(--db-icon-gradient);
16838
16925
  }
16839
16926
 
16840
- .db-switch:active input:checked:not(:disabled) {
16841
- background-color: var(--db-switch-surface-checked-press);
16842
- border-color: var(--db-switch-border-checked-press);
16927
+ .db-icon-ai-gradient-start {
16928
+ stop-color: var(--db-icon-ai-gradient-start);
16843
16929
  }
16844
16930
 
16845
- .db-switch input:checked::before {
16846
- transform: translateY(-50%) translateX(calc(var(--db-switch-track-width) - var(--db-switch-thumb-size)));
16931
+ .db-icon-ai-gradient-mid {
16932
+ stop-color: var(--db-icon-ai-gradient-mid);
16847
16933
  }
16848
16934
 
16849
- /* On focus, recolor the border to the checked-fill color on top of the outline ring. Matching AntD,
16850
- * the ring sits flush when off and gains a 1px offset when checked.
16851
- * TODO: width/offset are hardcoded 1px to preserve the AntD look; adopt the system 2px focus ring
16852
- * (var(--db-focus-ring-width)) in a separate change. */
16853
- .db-switch input:focus-visible {
16854
- border-color: var(--db-switch-border-focus);
16855
- outline: 1px solid var(--db-focus-ring-color);
16856
- outline-offset: 0;
16935
+ .db-icon-ai-gradient-end {
16936
+ stop-color: var(--db-icon-ai-gradient-end);
16857
16937
  }
16858
16938
 
16859
- .db-switch input:checked:focus-visible {
16860
- outline-offset: 1px;
16939
+ .db-icon-gradient-definitions {
16940
+ position: absolute;
16941
+ width: 1px;
16942
+ height: 1px;
16943
+ padding: 0;
16944
+ margin: -1px;
16945
+ overflow: hidden;
16946
+ clip: rect(0, 0, 0, 0);
16947
+ white-space: nowrap;
16948
+ border: 0;
16861
16949
  }
16862
16950
 
16863
- .db-switch input:disabled {
16864
- cursor: not-allowed;
16865
- background-color: var(--db-switch-surface-disabled);
16866
- border-color: var(--db-switch-border-disabled);
16951
+ .db-icon-spin {
16952
+ display: inline-block;
16953
+ animation: db-icon-loading-circle 1s infinite linear;
16867
16954
  }
16868
16955
 
16869
- /* Disabled + on: more specific than the plain disabled rule, so it wins regardless of source order. */
16870
- .db-switch input:checked:disabled {
16871
- background-color: var(--db-switch-surface-checked-disabled);
16872
- border-color: var(--db-switch-surface-checked-disabled);
16956
+ @keyframes db-icon-loading-circle {
16957
+ 100% {
16958
+ transform: rotate(360deg);
16959
+ }
16873
16960
  }
16874
16961
 
16875
16962
  @media (prefers-reduced-motion: reduce) {
16876
- .db-switch input,
16877
- .db-switch input::before {
16878
- transition: none;
16963
+ .db-icon-spin {
16964
+ animation: none;
16879
16965
  }
16880
16966
  }
16881
16967
 
16882
- .db-switch-wrapper {
16883
- display: flex;
16884
- align-items: center;
16885
- }
16886
16968
 
16887
- .db-switch-wrapper .db-switch-state-message {
16888
- margin-inline-start: auto;
16889
- margin-inline-end: var(--db-switch-label-spacing);
16890
- }
16891
16969
 
16892
- /* `:first-child` scopes this to the label-first layout; the plain layout renders the label last. */
16893
- .db-switch-wrapper > .du-bois-light-label:first-child,
16894
- .db-switch-wrapper > .du-bois-dark-label:first-child {
16895
- margin-inline-end: var(--db-switch-label-spacing);
16896
- }
16897
16970
 
16898
- .db-switch-state-message {
16899
- color: var(--db-switch-text);
16900
- }
16971
+ :where(:root), :where(.du-bois-light) {
16972
+ /* Shape */
16973
+ /* Must not scale with density; no non-scaling system token holds it. TODO(FEINF-6505): adopt a v2 system token when one is added. */
16974
+ --db-navigation-menu-indicator-thickness: 4px;
16975
+ /* No system token holds this control-specific inset. TODO(FEINF-6505): adopt a v2 system token when one is added. */
16976
+ --db-navigation-menu-focus-ring-offset: -1px;
16901
16977
 
16902
- /* Target the <Label>'s class, not a bare `label` — that would also match the .db-switch root <label>. */
16903
- .db-switch-wrapper:has(input:disabled) .du-bois-light-label,
16904
- .db-switch-wrapper:has(input:disabled) .du-bois-dark-label {
16905
- color: var(--db-switch-text-disabled);
16906
- }
16978
+ /* Color */
16979
+ --db-navigation-menu-indicator: var(--db-accent-surface);
16980
+ --db-navigation-menu-focus-ring: var(--db-focus-ring-color);
16981
+ --db-navigation-menu-text: var(--db-text-subtle);
16982
+ --db-navigation-menu-text-selected: var(--db-text-base);
16983
+ /* No v2 semantic for default-action link text yet; bridges on the v1 --db-color-* token. */
16984
+ --db-navigation-menu-text-hover: var(--db-color-action-default-text-hover);
16985
+ --db-navigation-menu-text-active: var(--db-color-action-default-text-press);
16907
16986
 
16908
- /* Indent a following hint/message past the track. The class is doubled to out-specify FormMessage's
16909
- * own margin rule. Each rule's key (rightmost) selector is a single class, not a multi-item :is(),
16910
- * so Chromium can hash-reject it instead of attempting the match against every element on every
16911
- * recalc (the leading/middle :is() groups aren't the key selector, so they're left as-is). */
16912
- :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-light-hint, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-dark-hint, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-dark-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
16913
- + :is(.du-bois-light-hint, .du-bois-dark-hint)
16914
- + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
16915
- + :is(.du-bois-light-hint, .du-bois-dark-hint)
16916
- + .du-bois-dark-form-message {
16917
- padding-inline-start: calc(var(--db-space-2) + var(--db-switch-track-width));
16987
+ /* Density */
16988
+ --db-navigation-menu-item-height: var(--db-size-8);
16989
+ --db-navigation-menu-item-min-width: var(--db-size-6);
16990
+ --db-navigation-menu-link-padding-top: var(--db-space-1);
16991
+ --db-navigation-menu-link-padding-bottom: var(--db-space-2);
16992
+ --db-navigation-menu-focus-ring-width: var(--db-focus-ring-width);
16918
16993
  }
16919
16994
 
16920
- :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-dark-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
16921
- + :is(.du-bois-light-hint, .du-bois-dark-hint)
16922
- + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
16923
- + :is(.du-bois-light-hint, .du-bois-dark-hint)
16924
- + .du-bois-dark-form-message {
16925
- margin-top: 0;
16995
+
16996
+ :where(:root), :where(.du-bois-light) {
16997
+ /* Color */
16998
+ /* No v2 token holds tertiary action-text states yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
16999
+ --db-overflow-trigger-text: var(--db-color-action-tertiary-text-default);
17000
+ --db-overflow-trigger-text-hover: var(--db-color-action-tertiary-text-hover);
17001
+ --db-overflow-trigger-text-press: var(--db-color-action-tertiary-text-press);
17002
+
17003
+ /* Density */
17004
+ --db-overflow-spacing: var(--db-space-2);
17005
+ --db-overflow-menu-spacing: var(--db-space-1);
16926
17006
  }
16927
17007
 
16928
- /* Component-tier tokens for the native <Button>, grouped by area (density, shape, typography, color).
16929
- * Colors alias the action-* system tokens; sizes alias the system density scale.
16930
- *
16931
- * Follows the mortar/action.css pattern: one semantic set (surface / border / text, each with
16932
- * -hover / -press / -disabled) whose defaults are the secondary variant. The variant classes that
16933
- * re-set these tokens per variant live in NativeButton.css; this file declares only tokens. */
16934
17008
 
16935
- /* Declared under the theme classes too (not just :root) so the var() chain re-resolves inside a
16936
- * `.du-bois-dark` subtree (or a portal that re-applies the theme class) instead of freezing at the root values. */
17009
+
17010
+ /* Popover is a neutral-card member of the popup family: surface, text, border, and radius come
17011
+ * from --db-popup-*. Link, focus, and spacing are popover-specific and reference system tokens
17012
+ * directly. The popup elevation scale is light-only, so the dark shadow is re-declared below. */
17013
+
16937
17014
  :where(:root), :where(.du-bois-light) {
16938
- /* ── Density ──────────────────────────────────────────────────────────── */
16939
- --db-btn-height: var(--db-size-8);
16940
- --db-btn-height-small: var(--db-size-6);
16941
- --db-btn-icon-size: var(--db-size-4);
16942
- --db-btn-border-width: 1px;
16943
- --db-btn-padding-vertical: var(--db-space-1);
16944
- --db-btn-padding-horizontal: var(--db-space-3);
16945
- --db-btn-padding-horizontal-small: var(--db-space-2);
16946
- /* Icon/force-icon horizontal padding: half the standard horizontal padding. */
16947
- --db-btn-padding-horizontal-icon: calc(var(--db-space-3) / 2); /* 6px */
16948
- --db-btn-gap: var(--db-space-1);
16949
- --db-btn-end-icon-gap: var(--db-space-2);
16950
- --db-btn-end-icon-gap-small: var(--db-space-1);
16951
- /* Width of a content-less button while loading, so the standalone spinner has room. */
16952
- --db-btn-loading-empty-width: var(--db-size-10);
17015
+ /* Shape */
17016
+ --db-popover-border-radius: var(--db-popup-border-radius);
16953
17017
 
16954
- /* ── Shape ────────────────────────────────────────────────────────────── */
16955
- --db-btn-border-radius: var(--db-shape-control);
17018
+ /* Color */
17019
+ --db-popover-surface: var(--db-popup-surface);
17020
+ --db-popover-text: var(--db-popup-text);
17021
+ --db-popover-border: var(--db-popup-border);
17022
+ /* No v2 token holds the tertiary link text yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17023
+ --db-popover-link-text: var(--db-color-action-tertiary-text-default);
17024
+ --db-popover-link-text-hover: var(--db-color-action-tertiary-text-hover);
17025
+ --db-popover-focus-ring: var(--db-focus-ring-color);
16956
17026
 
16957
- /* ── Typography ───────────────────────────────────────────────────────── */
16958
- --db-btn-font-size: var(--db-font-size-base);
16959
- --db-btn-icon-only-font-size: var(--db-font-size-lg); /* 15px */
16960
- --db-btn-font-weight: var(--db-font-weight-normal);
16961
- /* Line heights are hard-coded until the typography scale ships computed per-level line-height tokens. */
16962
- --db-btn-line-height: 1.25rem; /* 20px */
16963
- --db-btn-line-height-small: 1rem; /* 16px */
17027
+ /* Density */
17028
+ --db-popover-spacing: var(--db-space-2);
17029
+ --db-popover-focus-ring-width: var(--db-focus-ring-width);
16964
17030
 
16965
- /* ── Color: semantic set (defaults = secondary variant) ───────────────── */
16966
- --db-btn-surface: var(--db-color-action-default-background-default);
16967
- --db-btn-surface-hover: var(--db-color-action-default-background-hover);
16968
- --db-btn-surface-press: var(--db-color-action-default-background-press);
16969
- --db-btn-surface-disabled: transparent;
16970
- --db-btn-border: var(--db-color-action-default-border-default);
16971
- --db-btn-border-hover: var(--db-color-action-default-border-hover);
16972
- --db-btn-border-press: var(--db-color-action-default-border-press);
16973
- --db-btn-border-disabled: var(--db-color-action-disabled-border);
16974
- --db-btn-text: var(--db-color-action-default-text-default);
16975
- --db-btn-text-hover: var(--db-color-action-default-text-hover);
16976
- --db-btn-text-press: var(--db-color-action-default-text-press);
16977
- --db-btn-text-disabled: var(--db-color-action-disabled-text);
17031
+ /* Elevation */
17032
+ --db-popover-shadow: var(--db-popup-elevation);
17033
+ }
16978
17034
 
16979
- /* ── Focus & icon ─────────────────────────────────────────────────────── */
16980
- --db-btn-focus-ring: var(--db-color-action-default-border-focus);
16981
- --db-btn-danger-focus-ring: var(--db-color-action-danger-primary-background-default);
16982
- --db-btn-icon-glyph: var(--db-color-text-secondary);
17035
+
17036
+ /* Declared under the theme classes too (not just :root) so the var() chain re-resolves inside a
17037
+ * `.du-bois-dark` subtree instead of freezing at the light values. */
17038
+ :where(:root), :where(.du-bois-light) {
17039
+ /* Shape */
17040
+ --db-progress-border-radius: var(--db-shape-pill);
17041
+
17042
+ /* Color */
17043
+ --db-progress-track: var(--db-color-progress-track);
17044
+ --db-progress-fill: var(--db-color-progress-fill);
17045
+
17046
+ /* Density */
17047
+ --db-progress-size: var(--db-size-2);
17048
+
17049
+ /* Motion */
17050
+ /* No v2 motion token holds this duration. TODO(FEINF-6505): adopt a v2 motion token when one is added. */
17051
+ --db-progress-transition-duration: 300ms;
16983
17052
  }
16984
17053
 
16985
- /* Native <Button> visual styles. Consumes the component-tier `--db-btn-*` tokens (see tokens.css);
16986
- * the variant classes below re-set those tokens per variant and the rules that follow paint them.
16987
- * Shipped via mortar.css. */
17054
+ :where(:root), :where(.du-bois-light) {
17055
+ /* Shape */
17056
+ --db-radio-border-radius: var(--db-radius-full);
17057
+ --db-radio-border-width: 1px;
16988
17058
 
16989
- /* ── 0. Variant token re-sets ──────────────────────────────────────────────
16990
- * Each variant class re-sets the semantic tokens declared in tokens.css; combined classes win by
16991
- * specificity, which beats the `:where(:root)` (specificity 0) defaults regardless of file order. */
16992
- .db-btn-primary {
16993
- --db-btn-surface: var(--db-color-action-primary-background-default);
16994
- --db-btn-surface-hover: var(--db-color-action-primary-background-hover);
16995
- --db-btn-surface-press: var(--db-color-action-primary-background-press);
16996
- --db-btn-surface-disabled: var(--db-color-action-disabled-border);
16997
- --db-btn-border: transparent;
16998
- --db-btn-border-hover: transparent;
16999
- --db-btn-border-press: transparent;
17000
- --db-btn-border-disabled: transparent;
17001
- --db-btn-text: var(--db-color-action-primary-text-default);
17002
- --db-btn-text-hover: var(--db-color-action-primary-text-hover);
17003
- --db-btn-text-press: var(--db-color-action-primary-text-press);
17004
- --db-btn-text-disabled: var(--db-color-action-primary-text-default);
17059
+ /* Color */
17060
+ --db-radio-surface: var(--db-selection-toggle-surface);
17061
+ --db-radio-surface-hover: var(--db-color-action-default-background-hover);
17062
+ --db-radio-surface-active: var(--db-color-action-default-background-press);
17063
+ --db-radio-surface-checked: var(--db-selection-toggle-surface-checked);
17064
+ --db-radio-surface-checked-hover: var(--db-selection-toggle-surface-checked-hover);
17065
+ --db-radio-surface-checked-active: var(--db-color-action-default-background-press);
17066
+ --db-radio-surface-disabled: var(--db-selection-toggle-surface-disabled);
17067
+
17068
+ --db-radio-border: var(--db-color-action-default-border-default);
17069
+ --db-radio-border-hover: var(--db-selection-toggle-border-hover);
17070
+ --db-radio-border-active: var(--db-selection-toggle-border-active);
17071
+ --db-radio-border-focus: var(--db-focus-ring-color);
17072
+ --db-radio-border-disabled: var(--db-color-action-disabled-border);
17073
+ --db-radio-border-checked-hover: var(--db-selection-toggle-border-active);
17074
+ --db-radio-border-checked-active: var(--db-color-action-default-border-press);
17075
+ --db-radio-border-unchecked-focus: var(--db-color-action-primary-background-default);
17076
+ --db-radio-outline-checked-focus: var(--db-color-action-primary-background-default);
17077
+
17078
+ --db-radio-mark: #ffffff;
17079
+ --db-radio-mark-disabled: var(--db-selection-toggle-mark-disabled);
17080
+ --db-radio-text: var(--db-selection-toggle-text);
17081
+ --db-radio-text-disabled: var(--db-selection-toggle-text-disabled);
17082
+
17083
+ /* Density */
17084
+ --db-radio-size: var(--db-selection-toggle-size);
17085
+ --db-radio-label-spacing: var(--db-selection-toggle-label-spacing);
17086
+ --db-radio-mark-size: var(--db-size-1);
17087
+
17088
+ /* Motion */
17089
+ --db-radio-transition-duration: var(--db-selection-toggle-transition-duration);
17005
17090
  }
17006
- .db-btn-tertiary,
17007
- .db-btn-link {
17008
- --db-btn-surface: var(--db-color-action-tertiary-background-default);
17009
- --db-btn-surface-disabled: var(--db-color-action-tertiary-background-default);
17010
- --db-btn-border: var(--db-color-action-tertiary-background-default);
17011
- --db-btn-border-hover: transparent;
17012
- --db-btn-border-press: transparent;
17013
- --db-btn-border-disabled: transparent;
17014
- --db-btn-text: var(--db-color-action-tertiary-text-default);
17015
- --db-btn-text-hover: var(--db-color-action-tertiary-text-hover);
17016
- --db-btn-text-press: var(--db-color-action-tertiary-text-press);
17017
- --db-btn-text-disabled: var(--db-color-action-disabled-text);
17091
+
17092
+ /* Native-input <Radio> styles. Classic CSS only — no native nesting (the
17093
+ * `&`-style nesting feature ships in Chrome/Edge 120, Safari 17.2, Firefox 117
17094
+ * which leaves a non-trivial slice of our production traffic uncovered today;
17095
+ * see PR #1954620 thread). `:has()` lookups are still used to read state from
17096
+ * the native <input>; that feature has the support coverage we need.
17097
+ *
17098
+ * Structure (assumed by the React component):
17099
+ *
17100
+ * <label class="db-radio">
17101
+ * <input type="radio" />
17102
+ * <span class="db-radio-indicator"> ← circle border + fill react to state
17103
+ * <span class="db-radio-dot" /> ← visible when :checked
17104
+ * </span>
17105
+ * <span class="db-radio-label">{children}</span>
17106
+ * </label>
17107
+ *
17108
+ * The root only ever has one <input> descendant, so a bare `input` selector is
17109
+ * safe and clearer than a class. Mirrors NativeCheckbox.css; the differences are
17110
+ * the circular indicator (border-radius: full) and the filled dot instead of a
17111
+ * checkmark/dash, and there is no indeterminate state.
17112
+ *
17113
+ * TODO(DUBOIS-452): the indicator/dot state styles below are identical to
17114
+ * NativeCheckbox.css — consolidate them into a shared `.db-selection-toggle` class. */
17115
+
17116
+ .db-radio {
17117
+ /* `display: flex` (block-level), NOT `inline-flex`: see NativeCheckbox.css for
17118
+ * the line-box / sibling-spacing rationale. */
17119
+ display: flex;
17120
+ align-items: flex-start;
17121
+ position: relative;
17122
+ cursor: pointer;
17123
+ user-select: none;
17124
+ min-height: var(--db-line-height-base, 20px);
17125
+ width: fit-content;
17018
17126
  }
17019
- .db-btn-tertiary {
17020
- --db-btn-surface-hover: var(--db-color-action-tertiary-background-hover);
17021
- --db-btn-surface-press: var(--db-color-action-tertiary-background-press);
17127
+
17128
+ /* Hover/press live on the root (label), not on the input: the input is
17129
+ * visually hidden (opacity:0), so an `input:hover` rule wouldn't be visible
17130
+ * anyway, and the whole label area should drive the feedback. */
17131
+ .db-radio:hover:has(input:not(:disabled, :checked)) .db-radio-indicator {
17132
+ background-color: var(--db-radio-surface-hover);
17133
+ border-color: var(--db-radio-border-hover);
17022
17134
  }
17023
- /* Link is borderless and text-only, so its hover/press surface stays transparent. */
17024
- .db-btn-link {
17025
- --db-btn-surface-hover: transparent;
17026
- --db-btn-surface-press: transparent;
17135
+
17136
+ .db-radio:active:has(input:not(:disabled, :checked)) .db-radio-indicator {
17137
+ background-color: var(--db-radio-surface-active);
17138
+ border-color: var(--db-radio-border-active);
17027
17139
  }
17028
- /* Every danger button (icon included) disables to the filled treatment. */
17029
- .db-btn-danger {
17030
- --db-btn-surface-disabled: var(--db-color-action-disabled-border);
17031
- --db-btn-border-disabled: transparent;
17032
- --db-btn-text-disabled: var(--db-color-action-primary-text-default);
17140
+
17141
+ /* Selected fill. */
17142
+ .db-radio:has(input:checked) .db-radio-indicator {
17143
+ background-color: var(--db-radio-surface-checked);
17144
+ border-color: var(--db-radio-surface-checked);
17033
17145
  }
17034
- /* Danger, filled (primary): re-set only the surface; text/transparent border come from primary. */
17035
- .db-btn-primary.db-btn-danger:not(.db-btn-icon-only) {
17036
- --db-btn-surface: var(--db-color-action-danger-primary-background-default);
17037
- --db-btn-surface-hover: var(--db-color-action-danger-primary-background-hover);
17038
- --db-btn-surface-press: var(--db-color-action-danger-primary-background-press);
17146
+
17147
+ /* AntD getRadioInputStyles checked hover: background = actionPrimaryBackgroundHover,
17148
+ * borderColor = actionPrimaryBackgroundPress (a darker ring than the fill). */
17149
+ .db-radio:hover:has(input:checked:not(:disabled)) .db-radio-indicator {
17150
+ background-color: var(--db-radio-surface-checked-hover);
17151
+ border-color: var(--db-radio-border-checked-hover);
17039
17152
  }
17040
- /* Danger, secondary/outlined. */
17041
- .db-btn-danger:not(.db-btn-primary):not(.db-btn-tertiary):not(.db-btn-link):not(.db-btn-icon-only) {
17042
- --db-btn-surface: var(--db-color-action-danger-default-background-default);
17043
- --db-btn-surface-hover: var(--db-color-action-danger-default-background-hover);
17044
- --db-btn-surface-press: var(--db-color-action-danger-default-background-press);
17045
- --db-btn-border: var(--db-color-action-danger-default-border-default);
17046
- --db-btn-border-hover: var(--db-color-action-danger-default-border-hover);
17047
- --db-btn-border-press: var(--db-color-action-danger-default-border-press);
17048
- --db-btn-text: var(--db-color-action-danger-default-text-default);
17049
- --db-btn-text-hover: var(--db-color-action-danger-default-text-hover);
17050
- --db-btn-text-press: var(--db-color-action-danger-default-text-press);
17153
+
17154
+ /* AntD getRadioInputStyles checked active: background = actionDefaultBackgroundPress,
17155
+ * borderColor = actionDefaultBorderPress (two-tone, not flat). */
17156
+ .db-radio:active:has(input:checked:not(:disabled)) .db-radio-indicator {
17157
+ background-color: var(--db-radio-surface-checked-active);
17158
+ border-color: var(--db-radio-border-checked-active);
17051
17159
  }
17052
- /* Danger, tertiary/link: danger text everywhere, danger surface on hover/press; resting/disabled
17053
- * surface stay the tertiary ghost (override the filled danger-disabled set above). */
17054
- .db-btn-tertiary.db-btn-danger,
17055
- .db-btn-link.db-btn-danger {
17056
- --db-btn-surface-hover: var(--db-color-action-danger-default-background-hover);
17057
- --db-btn-surface-press: var(--db-color-action-danger-default-background-press);
17058
- --db-btn-surface-disabled: var(--db-color-action-tertiary-background-default);
17059
- --db-btn-text: var(--db-color-action-danger-default-text-default);
17060
- --db-btn-text-hover: var(--db-color-action-danger-default-text-hover);
17061
- --db-btn-text-press: var(--db-color-action-danger-default-text-press);
17062
- --db-btn-text-disabled: var(--db-color-action-disabled-text);
17160
+
17161
+ /* Dot visibility. */
17162
+ .db-radio:has(input:checked) .db-radio-dot {
17163
+ opacity: 1;
17063
17164
  }
17064
- /* Icon-only renders as the ghost (tertiary) treatment regardless of variant; re-set the semantic set
17065
- * so the icon-only rules paint from these component tokens. */
17066
- .db-btn-icon-only {
17067
- --db-btn-surface: var(--db-color-action-tertiary-background-default);
17068
- --db-btn-surface-hover: var(--db-color-action-tertiary-background-hover);
17069
- --db-btn-surface-press: var(--db-color-action-tertiary-background-press);
17070
- --db-btn-text: var(--db-color-action-tertiary-text-default);
17071
- --db-btn-text-hover: var(--db-color-action-tertiary-text-hover);
17072
- --db-btn-text-press: var(--db-color-action-tertiary-text-press);
17165
+
17166
+ /* Focus-visible (keyboard only). AntD getRadioInputStyles splits focus by checked
17167
+ * state, so we do too.
17168
+ *
17169
+ * UNCHECKED focus (AntD `&:not(disabled) > input:focus + inner`): only the border
17170
+ * color changes to actionPrimaryBackgroundDefault — NO outline ring. */
17171
+ .db-radio:has(input:focus-visible:not(:checked)) .db-radio-indicator {
17172
+ border-color: var(--db-radio-border-unchecked-focus);
17073
17173
  }
17074
- /* Danger icon-only keeps the ghost resting surface (inherited above) but uses danger text and a
17075
- * danger hover/press surface. */
17076
- .db-btn-icon-only.db-btn-danger {
17077
- --db-btn-surface-hover: var(--db-color-action-danger-default-background-hover);
17078
- --db-btn-surface-press: var(--db-color-action-danger-default-background-press);
17079
- --db-btn-text: var(--db-color-action-danger-default-text-default);
17080
- --db-btn-text-hover: var(--db-color-action-danger-default-text-hover);
17081
- --db-btn-text-press: var(--db-color-action-danger-default-text-press);
17174
+
17175
+ /* CHECKED focus-visible (AntD `&.checked &:not(disabled) > input:focus-visible + inner`):
17176
+ * background = actionPrimaryBackgroundDefault, borderColor = actionDefaultBorderFocus,
17177
+ * outline = 1px solid actionPrimaryBackgroundDefault, outlineOffset = 1. */
17178
+ .db-radio:has(input:focus-visible:checked) .db-radio-indicator {
17179
+ background-color: var(--db-radio-surface-checked);
17180
+ border-color: var(--db-radio-border-focus);
17181
+ outline: 1px solid var(--db-radio-outline-checked-focus);
17182
+ outline-offset: 1px;
17082
17183
  }
17083
17184
 
17084
- /* ── 1. Base paints the default (secondary) variant ────────────────────── */
17085
- .db-btn {
17086
- appearance: none;
17185
+ /* Disabled (covers checked and unchecked). Sourced from `input:disabled` so the
17186
+ * standalone-disabled and group-disabled paths both land here. */
17187
+ .db-radio:has(input:disabled) {
17188
+ cursor: not-allowed;
17189
+ }
17190
+
17191
+ .db-radio:has(input:disabled) .db-radio-label {
17192
+ color: var(--db-radio-text-disabled);
17193
+ }
17194
+
17195
+ .db-radio:has(input:disabled) .db-radio-indicator {
17196
+ background-color: var(--db-radio-surface-disabled);
17197
+ border-color: var(--db-radio-border-disabled);
17198
+ box-shadow: none;
17199
+ }
17200
+
17201
+ .db-radio:has(input:disabled) .db-radio-dot {
17202
+ background-color: var(--db-radio-mark-disabled);
17203
+ }
17204
+
17205
+ /* Visually-hidden native input (opacity:0) overlaying the indicator circle,
17206
+ * kept interactive and in the tab order so the `:has()` lookups above can read
17207
+ * its state. It stays pointer-clickable — matching AntD's `.ant-radio-input` —
17208
+ * so it is the top-most hit-test target over the circle: `getByRole('radio').click()`
17209
+ * (Playwright, and any coordinate click that lands on the circle) resolves to the
17210
+ * real control instead of the decorative indicator overlay. Making the input
17211
+ * `pointer-events: none` (the prior native-only behavior) is what regressed
17212
+ * interaction tests vs. the AntD variant. `cursor: inherit` tracks the label's
17213
+ * pointer / disabled `not-allowed` cursor so the circle keeps AntD's cursor too. */
17214
+ .db-radio input {
17215
+ position: absolute;
17216
+ inset: 0;
17217
+ inline-size: var(--db-radio-size);
17218
+ block-size: var(--db-radio-size);
17219
+ margin: 0;
17220
+ opacity: 0;
17221
+ cursor: inherit;
17222
+ }
17223
+
17224
+ /* Indicator — circle with the border/fill that responds to state above.
17225
+ * Vertical math aligns the indicator to the cap-height of the first text line
17226
+ * so radio + label sit on the same visual baseline even when the label wraps. */
17227
+ .db-radio .db-radio-indicator {
17228
+ display: grid;
17229
+ place-items: center;
17230
+ flex-shrink: 0;
17231
+ /* Decorative only — the overlaid input owns hit-testing. Without this, the
17232
+ * indicator (and its inherited dot child) intercept `getByRole('radio').click()`;
17233
+ * clicks on any edge sliver the input does not cover fall through to the label,
17234
+ * which toggles via label/input association. */
17235
+ pointer-events: none;
17236
+ inline-size: var(--db-radio-size);
17237
+ block-size: var(--db-radio-size);
17238
+ margin-top: calc((var(--db-line-height-base, 20px) - var(--db-radio-size)) / 2);
17239
+ border: var(--db-radio-border-width) solid var(--db-radio-border);
17240
+ border-radius: var(--db-radio-border-radius);
17241
+ background-color: var(--db-radio-surface);
17087
17242
  box-sizing: border-box;
17088
- position: relative; /* anchors the loading spinner */
17089
- display: inline-flex !important;
17090
- align-items: center !important;
17091
- justify-content: center !important;
17092
- vertical-align: middle;
17093
- white-space: nowrap;
17094
- user-select: none;
17095
- cursor: pointer;
17096
- height: var(--db-btn-height) !important;
17097
- font-family: inherit;
17098
- font-size: var(--db-btn-font-size);
17099
- font-weight: var(--db-btn-font-weight);
17100
- line-height: var(--db-btn-line-height) !important;
17101
- text-decoration: none !important;
17102
- border: var(--db-btn-border-width) solid var(--db-btn-border);
17103
- border-color: var(--db-btn-border) !important;
17104
- border-radius: var(--db-btn-border-radius) !important;
17105
- box-shadow: none !important;
17106
- background-color: var(--db-btn-surface) !important;
17107
- color: var(--db-btn-text) !important;
17243
+ transition:
17244
+ background-color var(--db-radio-transition-duration) ease,
17245
+ border-color var(--db-radio-transition-duration) ease,
17246
+ box-shadow var(--db-radio-transition-duration) ease;
17108
17247
  }
17109
17248
 
17110
- /* ── 2. Padding ──────────────────────────────────────────────────────────── */
17111
- .db-btn.db-btn:where(:not(.db-btn-icon-only, .db-btn-force-icon, .db-btn-link)) {
17112
- padding: var(--db-btn-padding-vertical) var(--db-btn-padding-horizontal) !important;
17249
+ .db-radio .db-radio-indicator > .db-radio-dot {
17250
+ grid-area: 1 / 1;
17251
+ inline-size: var(--db-radio-mark-size);
17252
+ block-size: var(--db-radio-mark-size);
17253
+ border-radius: var(--db-radius-full);
17254
+ background-color: var(--db-radio-mark);
17255
+ opacity: 0;
17256
+ transition: opacity var(--db-radio-transition-duration) ease;
17113
17257
  }
17114
- .db-btn.db-btn-small:where(:not(.db-btn-icon-only, .db-btn-force-icon, .db-btn-link)) {
17115
- padding: 0 var(--db-btn-padding-horizontal-small) !important;
17258
+
17259
+ /* Honour the user's reduced-motion preference: drop the state-change
17260
+ * transitions on the indicator and dot so checking/hovering is instant. */
17261
+ @media (prefers-reduced-motion: reduce) {
17262
+ .db-radio .db-radio-indicator,
17263
+ .db-radio .db-radio-indicator > .db-radio-dot {
17264
+ transition: none;
17265
+ }
17266
+ }
17267
+
17268
+ /* Padding-inline replicates the AntD `.{prefix} + span` label gap and the
17269
+ * trailing whitespace that separates adjacent radios in a row. */
17270
+ .db-radio .db-radio-label {
17271
+ color: var(--db-radio-text);
17272
+ padding-inline: var(--db-radio-label-spacing);
17273
+ word-break: break-word;
17274
+ }
17275
+
17276
+ /* Form-flow integration: align helper text under the radio label. The `paddingLeft`
17277
+ * indent comes from AntD's getCommonRadioGroupStyles (`& > label + hint`) and applies
17278
+ * to BOTH layouts. The doubled root class pushes specificity to (0,3,0) to beat
17279
+ * FormMessage's runtime `&&` margin. */
17280
+ .db-radio.db-radio + .du-bois-light-hint,
17281
+ .db-radio.db-radio + .du-bois-dark-hint,
17282
+ .db-radio.db-radio + .du-bois-light-form-message,
17283
+ .db-radio.db-radio + .du-bois-dark-form-message,
17284
+ .db-radio.db-radio + .du-bois-light-hint + .du-bois-light-form-message,
17285
+ .db-radio.db-radio + .du-bois-dark-hint + .du-bois-dark-form-message {
17286
+ padding-inline-start: var(--db-space-6);
17287
+ margin-top: 0;
17288
+ }
17289
+
17290
+ /* Vertical hint rhythm (AntD getVerticalRadioGroupStyles, vertical-only): a hint
17291
+ * after a radio gets `marginTop: -sm` (cancels the preceding radio's
17292
+ * `paddingBottom: sm` so the hint tucks directly under its label) and
17293
+ * `marginBottom: sm` (gap before the next radio). The negative top margin relies on
17294
+ * the .db-stack gap being 0 (set below). Scoped to .db-stack so it never perturbs
17295
+ * the horizontal grid, where hints are positioned by grid-row instead. */
17296
+ .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-light-hint,
17297
+ .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-dark-hint,
17298
+ .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-light-form-message,
17299
+ .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-dark-form-message,
17300
+ .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-light-hint + .du-bois-light-form-message,
17301
+ .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-dark-hint + .du-bois-dark-form-message {
17302
+ margin-top: calc(-1 * var(--db-space-2));
17303
+ margin-bottom: var(--db-space-2);
17304
+ }
17305
+
17306
+ /* AntD `& > label:last-of-type + hint { marginTop: 0 }`: the last radio has no
17307
+ * `paddingBottom` to cancel, so its trailing hint keeps its natural top position. */
17308
+ .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-light-hint,
17309
+ .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-dark-hint,
17310
+ .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-light-form-message,
17311
+ .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-dark-form-message,
17312
+ .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-light-hint + .du-bois-light-form-message,
17313
+ .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-dark-hint + .du-bois-dark-form-message {
17314
+ margin-top: 0;
17315
+ }
17316
+
17317
+ .du-bois-light-label + .db-radio-group.db-radio-group,
17318
+ .du-bois-dark-label + .db-radio-group.db-radio-group,
17319
+ .du-bois-light-hint + .db-radio-group.db-radio-group,
17320
+ .du-bois-dark-hint + .db-radio-group.db-radio-group,
17321
+ .db-radio-group.db-radio-group + .du-bois-light-form-message,
17322
+ .db-radio-group.db-radio-group + .du-bois-dark-form-message {
17323
+ margin-top: var(--db-space-2);
17116
17324
  }
17117
- .db-btn.db-btn-link {
17118
- padding: 0 !important;
17325
+
17326
+ /* RadioGroup layout. Vertical (the default) delegates flex-direction to the
17327
+ * .db-stack primitive; horizontal is a two-row grid (mirroring AntD). */
17328
+ .db-radio-group {
17329
+ display: flex;
17119
17330
  }
17120
- .db-btn.db-btn-force-icon {
17121
- padding: 0 var(--db-btn-padding-horizontal-icon) !important;
17331
+
17332
+ /* Vertical: .db-stack supplies flex-direction: column. AntD's vertical helper
17333
+ * (getVerticalRadioGroupStyles) produces its item rhythm purely from
17334
+ * `& > label { paddingBottom: sm }` plus the negative-margin hint trick above — it
17335
+ * uses NO flex gap. The negative `marginTop` on hints only works if the stack adds
17336
+ * no gap of its own, so we zero out the gap and reproduce the rhythm via
17337
+ * padding-bottom on each radio. */
17338
+ .db-radio-group.db-stack {
17339
+ --db-stack-gap: 0;
17122
17340
  }
17123
17341
 
17124
- /* ── 3. Interactive states ───────────────────────────────────────────────── */
17125
- .db-btn:hover:not(:disabled, .db-btn-loading) {
17126
- background-color: var(--db-btn-surface-hover) !important;
17127
- border-color: var(--db-btn-border-hover) !important;
17128
- color: var(--db-btn-text-hover) !important;
17342
+ /* AntD `& > label { paddingBottom: sm }`. */
17343
+ .db-radio-group.db-stack > .db-radio {
17344
+ padding-bottom: var(--db-space-2);
17129
17345
  }
17130
- .db-btn:active:not(:disabled, .db-btn-loading) {
17131
- background-color: var(--db-btn-surface-press) !important;
17132
- border-color: var(--db-btn-border-press) !important;
17133
- color: var(--db-btn-text-press) !important;
17346
+
17347
+ /* AntD `& > label:last-of-type { paddingBottom: 0 }`. */
17348
+ .db-radio-group.db-stack > .db-radio:last-of-type {
17349
+ padding-bottom: 0;
17134
17350
  }
17135
17351
 
17136
- /* ── 4. Focus ────────────────────────────────────────────────────────────── */
17137
- .db-btn:focus-visible {
17138
- outline: 2px solid var(--db-btn-focus-ring);
17139
- outline-offset: -2px;
17352
+ /* AntD getVerticalRadioGroupStyles `.${prefix}-radio-tile + .${prefix}-radio-tile {
17353
+ * marginTop: md }`: RadioTiles are not matched by the `.db-radio` padding-bottom
17354
+ * rhythm above (the tile's own class is du-bois-{light,dark}-radio-tile), so space
17355
+ * consecutive tiles explicitly. The .db-stack gap is 0, so this margin is the spacing. */
17356
+ .db-radio-group.db-stack > .du-bois-light-radio-tile + .du-bois-light-radio-tile,
17357
+ .db-radio-group.db-stack > .du-bois-light-radio-tile + .du-bois-dark-radio-tile,
17358
+ .db-radio-group.db-stack > .du-bois-dark-radio-tile + .du-bois-light-radio-tile,
17359
+ .db-radio-group.db-stack > .du-bois-dark-radio-tile + .du-bois-dark-radio-tile {
17360
+ margin-top: var(--db-space-4);
17140
17361
  }
17141
- .db-btn-primary:focus-visible {
17142
- outline-offset: 1px;
17362
+
17363
+ /* Horizontal: AntD `getHorizontalRadioGroupStyles` is a TWO-ROW grid (NOT a flex
17364
+ * row) so each radio sits in the [label] row and its following hint drops into the
17365
+ * [hint] row of the same auto-column. `&&` in AntD doubles specificity; the doubled
17366
+ * group class here matches that.
17367
+ * AntD: `display: grid; gridTemplateRows: [label] auto [hint] auto;
17368
+ * gridAutoColumns: max-content; gridColumnGap: md`. */
17369
+ .db-radio-group--horizontal.db-radio-group--horizontal {
17370
+ display: grid;
17371
+ grid-template-rows: [label] auto [hint] auto;
17372
+ grid-auto-columns: max-content;
17373
+ column-gap: var(--db-space-4);
17143
17374
  }
17144
- .db-btn-use-focus-pseudo-class:focus {
17145
- outline: 2px solid var(--db-btn-focus-ring);
17146
- outline-offset: 1px;
17375
+
17376
+ /* Equal column widths: AntD `gridAutoColumns: minmax(0, 1fr)`. */
17377
+ .db-radio-group--horizontal.db-radio-group--equal-width {
17378
+ grid-auto-columns: minmax(0, 1fr);
17147
17379
  }
17148
17380
 
17149
- /* ── 5. Small / full-width / empty ────────────────────────────────────────── */
17150
- .db-btn-small {
17151
- height: var(--db-btn-height-small) !important;
17381
+ /* AntD `& > label, & > .${prefix}-radio-tile { gridRow: label / label; marginRight: 0 }`:
17382
+ * every radio (and every RadioTile button, whose own class is
17383
+ * du-bois-{light,dark}-radio-tile and is NOT matched by `> .db-radio`) lives in the
17384
+ * label row. grid-auto-flow defaults to row, so successive items advance the implicit
17385
+ * column. */
17386
+ .db-radio-group--horizontal > .db-radio,
17387
+ .db-radio-group--horizontal > .du-bois-light-radio-tile,
17388
+ .db-radio-group--horizontal > .du-bois-dark-radio-tile {
17389
+ grid-row: label / label;
17390
+ margin-inline-end: 0;
17152
17391
  }
17153
- .db-btn-full-width {
17154
- width: 100%;
17392
+
17393
+ /* AntD `& > label + hint { display: inline-block; gridRow: hint / hint }`: a hint
17394
+ * following a radio drops into the hint row of the column its radio occupies. The
17395
+ * vertical negative-margin rules are scoped to .db-stack, so nothing to reset here. */
17396
+ .db-radio-group--horizontal > .du-bois-light-hint,
17397
+ .db-radio-group--horizontal > .du-bois-dark-hint {
17398
+ display: inline-block;
17399
+ grid-row: hint / hint;
17155
17400
  }
17156
- .db-btn-empty {
17157
- background-color: transparent !important;
17158
- border-color: transparent !important;
17401
+
17402
+ /* AntD `:has(> hint) { marginTop: sm }`: when the horizontal group contains a hint,
17403
+ * the group itself gets a top margin (room above the two-row grid). */
17404
+ .db-radio-group--horizontal:has(> .du-bois-light-hint),
17405
+ .db-radio-group--horizontal:has(> .du-bois-dark-hint) {
17406
+ margin-top: var(--db-space-2);
17159
17407
  }
17160
- .db-btn-default.db-btn-empty {
17161
- color: var(--db-btn-icon-glyph) !important;
17408
+
17409
+ /* AntD `&& + form-message { marginTop: sm }` (horizontal): a form-message following
17410
+ * the whole group is separated by sm. */
17411
+ .db-radio-group--horizontal + .du-bois-light-form-message,
17412
+ .db-radio-group--horizontal + .du-bois-dark-form-message {
17413
+ margin-top: var(--db-space-2);
17162
17414
  }
17163
- .db-btn-empty.db-btn-loading {
17164
- width: var(--db-btn-loading-empty-width);
17415
+
17416
+
17417
+
17418
+
17419
+
17420
+ :where(:root), :where(.du-bois-light) {
17421
+ /* Shape */
17422
+ --db-slider-border-radius: var(--db-shape-pill);
17423
+
17424
+ /* Color */
17425
+ /* No v2 token holds the track + disabled-range surface yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17426
+ --db-slider-track-surface: #e8ecf0;
17427
+ --db-slider-range-surface-disabled: #e8ecf0;
17428
+ /* No v2 token holds the range fill yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17429
+ --db-slider-range-surface: #2272b4;
17430
+
17431
+ --db-slider-thumb-surface: var(--db-accent-surface);
17432
+ --db-slider-thumb-surface-hover: var(--db-accent-surface-hover);
17433
+ --db-slider-thumb-surface-press: var(--db-accent-surface-active);
17434
+ /* No v2 token holds the disabled thumb yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17435
+ --db-slider-thumb-surface-disabled: #d1d9e1;
17436
+
17437
+ /* Density */
17438
+ --db-slider-size: var(--db-size-5); /* 20px */
17439
+ --db-slider-thumb-size: var(--db-size-5); /* 20px */
17440
+ --db-slider-track-thickness: calc(var(--db-size-1) * 0.75); /* 3px */
17165
17441
  }
17166
17442
 
17167
- /* ── 6. Tertiary & link ──────────────────────────────────────────────────── */
17168
- .db-btn-link {
17169
- height: auto !important;
17170
- border: none !important;
17443
+ :where(:root), :where(.du-bois-light) {
17444
+ /* Shape */
17445
+ /* TODO(FEINF-6505): adopt --db-shape-container-* once a 4px container radius exists; kept on --db-radius-sm to preserve 4px. */
17446
+ --db-snackbar-border-radius: var(--db-radius-sm);
17447
+
17448
+ /* Density */
17449
+ --db-snackbar-spacing-vertical: var(--db-space-3);
17450
+ --db-snackbar-spacing-horizontal: var(--db-space-4);
17451
+ --db-snackbar-close-button-spacing: var(--db-space-2);
17452
+
17453
+ /* Color */
17454
+ /* No v1/v2 system token holds this theme-invariant white. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17455
+ --db-snackbar-text: #ffffff;
17456
+ /* No v1/v2 system token holds these theme-invariant close-icon colors. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17457
+ --db-snackbar-close-icon: #e8ecf0;
17458
+ --db-snackbar-close-icon-hover: #8acaff;
17459
+ --db-snackbar-close-icon-active: #bae1fc;
17171
17460
  }
17172
- /* Re-declare the text color for a non-icon-only tertiary/link at two-class weight so a plain
17173
- * call-site `color` override loses; an icon-only glyph stays at base weight so its override wins. */
17174
- .db-btn-tertiary:not(.db-btn-icon-only),
17175
- .db-btn-link:not(.db-btn-icon-only) {
17176
- color: var(--db-btn-text) !important;
17461
+
17462
+ :where(:root), :where(.du-bois-light) {
17463
+ /* Color */
17464
+ /* No v1/v2 system token holds this light/dark surface pair. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17465
+ --db-snackbar-surface: #11171c;
17466
+
17467
+ /* Elevation */
17468
+ --db-snackbar-shadow: var(--db-elevation-med);
17177
17469
  }
17178
- /* Keep the resting border at two-class weight (parity with the shielded border-color). */
17179
- .db-btn.db-btn-tertiary,
17180
- .db-btn.db-btn-link {
17181
- border-color: var(--db-btn-border) !important;
17470
+
17471
+ /* Spacer's height is a spacing step. The size prop (xs/sm/mid/md/lg) selects one component token,
17472
+ * each aliasing the system space scale — theme-invariant, so a single block (no light/dark split). */
17473
+
17474
+ :where(:root), :where(.du-bois-light) {
17475
+ /* Density */
17476
+ --db-spacer-size-xs: var(--db-space-1); /* 4px */
17477
+ --db-spacer-size-sm: var(--db-space-2); /* 8px */
17478
+ --db-spacer-size-mid: var(--db-space-3); /* 12px */
17479
+ --db-spacer-size-md: var(--db-space-4); /* 16px */
17480
+ --db-spacer-size-lg: var(--db-space-6); /* 24px */
17182
17481
  }
17183
17482
 
17184
- /* ── 7. Danger ───────────────────────────────────────────────────────────── */
17185
- .db-btn-danger:focus-visible {
17186
- outline-color: var(--db-btn-danger-focus-ring);
17483
+
17484
+
17485
+ :where(:root), :where(.du-bois-light) {
17486
+ /* Shape */
17487
+ --db-switch-border-radius: var(--db-radius-full);
17488
+ --db-switch-border-width: 1px;
17489
+
17490
+ /* Color */
17491
+ --db-switch-surface: var(--db-selection-toggle-surface);
17492
+ --db-switch-surface-hover: var(--db-color-action-default-background-hover);
17493
+ --db-switch-surface-active: var(--db-color-action-default-background-press);
17494
+ --db-switch-surface-checked: var(--db-selection-toggle-surface-checked);
17495
+ --db-switch-surface-checked-hover: var(--db-selection-toggle-surface-checked-hover);
17496
+ --db-switch-surface-checked-active: var(--db-selection-toggle-surface-checked-active);
17497
+ --db-switch-surface-disabled: var(--db-selection-toggle-surface-disabled);
17498
+ --db-switch-surface-checked-disabled: var(--db-color-action-disabled-text);
17499
+
17500
+ --db-switch-border: var(--db-color-action-default-border-default);
17501
+ --db-switch-border-hover: var(--db-selection-toggle-border-hover);
17502
+ --db-switch-border-active: var(--db-selection-toggle-border-active);
17503
+ --db-switch-border-disabled: var(--db-color-action-disabled-border);
17504
+ --db-switch-border-checked: var(--db-selection-toggle-surface-checked);
17505
+ --db-switch-border-checked-hover: var(--db-selection-toggle-surface-checked-hover);
17506
+ --db-switch-border-checked-active: var(--db-selection-toggle-surface-checked-active);
17507
+ --db-switch-border-focus: var(--db-selection-toggle-surface-checked);
17508
+
17509
+ --db-switch-thumb: var(--db-selection-toggle-surface);
17510
+
17511
+ --db-switch-text: var(--db-selection-toggle-text);
17512
+ --db-switch-text-disabled: var(--db-selection-toggle-text-disabled);
17513
+
17514
+ /* Density */
17515
+ --db-switch-track-width: calc(var(--db-size-4) * 1.75); /* 28px */
17516
+ --db-switch-track-height: var(--db-size-4); /* 16px */
17517
+ --db-switch-thumb-size: var(--db-switch-track-height);
17518
+ --db-switch-label-spacing: var(--db-selection-toggle-label-spacing);
17519
+
17520
+ /* Motion */
17521
+ --db-switch-transition-duration: var(--db-selection-toggle-transition-duration);
17187
17522
  }
17188
17523
 
17189
- /* ── 8. Icon-only ────────────────────────────────────────────────────────── */
17190
- .db-btn-icon-only {
17191
- padding: 0;
17192
- border: none !important;
17193
- background-color: var(--db-btn-surface) !important;
17194
- font-size: var(--db-btn-icon-only-font-size);
17524
+ .db-switch {
17525
+ /* flex, not inline-flex: an inline-flex root's line-box leading pushes block siblings
17526
+ * (e.g. a following FormMessage) down a few px, breaking the form-flow spacing. */
17527
+ display: flex;
17528
+ align-items: center;
17529
+ cursor: pointer;
17530
+ user-select: none;
17531
+ width: fit-content;
17532
+ margin-inline-end: var(--db-switch-label-spacing);
17195
17533
  }
17196
- /* `:where()` keeps this at (0,1,0) so a call-site `css` width override wins, as it does for `height`. */
17197
- .db-btn-icon-only:where(:not(.db-btn-small)) {
17198
- width: var(--db-btn-height) !important;
17534
+
17535
+ .db-switch:has(input:disabled) {
17536
+ cursor: not-allowed;
17199
17537
  }
17200
- .db-btn-icon-only:not(.db-btn-link, .db-btn-tertiary, [href], :hover, :active, :disabled) {
17201
- color: var(--db-btn-icon-glyph) !important;
17538
+
17539
+ /* The track. `appearance: none` strips the native checkbox rendering so the input itself can be styled. */
17540
+ .db-switch input {
17541
+ appearance: none;
17542
+ position: relative;
17543
+ flex-shrink: 0;
17544
+ margin: 0;
17545
+ cursor: pointer;
17546
+ inline-size: var(--db-switch-track-width);
17547
+ block-size: var(--db-switch-track-height);
17548
+ border: var(--db-switch-border-width) solid var(--db-switch-border);
17549
+ border-radius: var(--db-switch-border-radius);
17550
+ background-color: var(--db-switch-surface);
17551
+ box-sizing: border-box;
17552
+ transition:
17553
+ background-color var(--db-switch-transition-duration) ease,
17554
+ border-color var(--db-switch-transition-duration) ease;
17202
17555
  }
17203
- /* `:where()` on the exclusion holds this at (0,2,0): enough to tint an un-overridden icon-only link
17204
- * over the base color, while still letting a consumer's own icon-color override win on source order. */
17205
- .db-btn-icon-only[href]:where(:not(.db-btn-tertiary, .db-btn-link, .db-btn-danger, :disabled)) {
17206
- color: var(--db-btn-text) !important;
17556
+
17557
+ /* The thumb. A pseudo-element keeps the input the switch's only element, so nothing can overlay it
17558
+ * and intercept clicks. Offset left by the border width to sit flush with the track's outer edge. */
17559
+ .db-switch input::before {
17560
+ content: '';
17561
+ position: absolute;
17562
+ top: 50%;
17563
+ left: calc(-1 * var(--db-switch-border-width));
17564
+ inline-size: var(--db-switch-thumb-size);
17565
+ block-size: var(--db-switch-thumb-size);
17566
+ border: var(--db-switch-border-width) solid;
17567
+ border-color: inherit;
17568
+ border-radius: var(--db-switch-border-radius);
17569
+ background-color: var(--db-switch-thumb);
17570
+ box-sizing: border-box;
17571
+ transform: translateY(-50%);
17572
+ transition: transform var(--db-switch-transition-duration) ease;
17207
17573
  }
17208
- .db-btn-icon-only:where(.db-btn-small) {
17209
- width: var(--db-btn-height-small);
17574
+
17575
+ /* `<label for>` propagates `:hover` to the input, so scope hover/press to the control, not `input:hover`. */
17576
+ .db-switch:hover input:not(:disabled):not(:checked) {
17577
+ background-color: var(--db-switch-surface-hover);
17578
+ border-color: var(--db-switch-border-hover);
17210
17579
  }
17211
- .db-btn-icon-only.db-btn-small {
17212
- line-height: var(--db-btn-line-height-small) !important;
17580
+
17581
+ .db-switch:active input:not(:disabled):not(:checked) {
17582
+ background-color: var(--db-switch-surface-active);
17583
+ border-color: var(--db-switch-border-active);
17213
17584
  }
17214
- .db-btn-icon-only:hover:not(:disabled, .db-btn-loading) {
17215
- background-color: var(--db-btn-surface-hover) !important;
17216
- color: var(--db-btn-text-hover) !important;
17585
+
17586
+ .db-switch input:checked {
17587
+ background-color: var(--db-switch-surface-checked);
17588
+ border-color: var(--db-switch-border-checked);
17217
17589
  }
17218
- .db-btn-icon-only:active:not(:disabled, .db-btn-loading) {
17219
- background-color: var(--db-btn-surface-press) !important;
17220
- color: var(--db-btn-text-press) !important;
17590
+
17591
+ .db-switch:hover input:checked:not(:disabled) {
17592
+ background-color: var(--db-switch-surface-checked-hover);
17593
+ border-color: var(--db-switch-border-checked-hover);
17221
17594
  }
17222
- .db-btn-icon-only:enabled:not(.db-btn-primary, .db-btn-tertiary, .db-btn-link, .db-btn-danger, :hover, :active)
17223
- > .anticon {
17224
- color: var(--db-btn-icon-glyph);
17595
+
17596
+ .db-switch:active input:checked:not(:disabled) {
17597
+ background-color: var(--db-switch-surface-checked-active);
17598
+ border-color: var(--db-switch-border-checked-active);
17225
17599
  }
17226
- .db-btn-icon-only.db-btn-danger:not(:hover, :active) > .anticon {
17227
- color: var(--db-btn-text);
17600
+
17601
+ .db-switch input:checked::before {
17602
+ transform: translateY(-50%) translateX(calc(var(--db-switch-track-width) - var(--db-switch-thumb-size)));
17228
17603
  }
17229
- .db-btn-icon-only.db-btn-danger:hover:not(:disabled, .db-btn-loading) {
17230
- background-color: var(--db-btn-surface-hover) !important;
17231
- color: var(--db-btn-text-hover) !important;
17604
+
17605
+ /* On focus, recolor the border to the checked-fill color on top of the outline ring. Matching AntD,
17606
+ * the ring sits flush when off and gains a 1px offset when checked.
17607
+ * TODO: width/offset are hardcoded 1px to preserve the AntD look; adopt the system 2px focus ring
17608
+ * (var(--db-focus-ring-width)) in a separate change. */
17609
+ .db-switch input:focus-visible {
17610
+ border-color: var(--db-switch-border-focus);
17611
+ outline: 1px solid var(--db-focus-ring-color);
17612
+ outline-offset: 0;
17232
17613
  }
17233
- .db-btn-icon-only.db-btn-danger:active:not(:disabled, .db-btn-loading) {
17234
- background-color: var(--db-btn-surface-press) !important;
17235
- color: var(--db-btn-text-press) !important;
17614
+
17615
+ .db-switch input:checked:focus-visible {
17616
+ outline-offset: 1px;
17236
17617
  }
17237
17618
 
17238
- /* ── 9. Disabled — base rules paint the per-variant `*-disabled` tokens (see tokens.css) ──────── */
17239
- .db-btn:disabled {
17619
+ .db-switch input:disabled {
17240
17620
  cursor: not-allowed;
17241
- background-color: var(--db-btn-surface-disabled) !important;
17242
- color: var(--db-btn-text-disabled) !important;
17243
- }
17244
- /* Empty and loading disabled buttons keep a transparent border (set elsewhere), so exclude them.
17245
- * `:where()` on the exclusion holds this at (0,2,0): enough to beat the base border, while still
17246
- * letting a consumer's own `[disabled]` border override win on source order. */
17247
- .db-btn:disabled:where(:not(.db-btn-empty, .db-btn-loading)) {
17248
- border-color: var(--db-btn-border-disabled) !important;
17249
- }
17250
- .db-btn-icon-only:disabled {
17251
- border: none;
17252
- }
17253
- .db-btn-icon-only:disabled:hover {
17254
- background-color: transparent !important;
17255
- }
17256
- .db-btn:disabled > .anticon {
17257
- color: inherit !important;
17621
+ background-color: var(--db-switch-surface-disabled);
17622
+ border-color: var(--db-switch-border-disabled);
17258
17623
  }
17259
17624
 
17260
- /* ── 10. Icon & content layout ───────────────────────────────────────────── */
17261
- /* The leading icon renders unwrapped as the button's direct `.anticon` child (see NativeButton.tsx),
17262
- * so `> .anticon` targets only a real icon glyph — a non-glyph `icon` (e.g. a Spinner) keeps its own
17263
- * sizing — and out-specifies AntD's runtime-injected global `.anticon` rule. */
17264
- .db-btn > .anticon,
17265
- .db-btn-end-icon,
17266
- .db-btn-end-icon > .anticon {
17267
- display: inline-flex;
17268
- align-items: center;
17269
- font-size: var(--db-btn-icon-size) !important;
17270
- line-height: 0;
17271
- }
17272
- .db-btn-small > .anticon .anticon {
17273
- line-height: 0;
17625
+ /* Disabled + on: more specific than the plain disabled rule, so it wins regardless of source order. */
17626
+ .db-switch input:checked:disabled {
17627
+ background-color: var(--db-switch-surface-checked-disabled);
17628
+ border-color: var(--db-switch-surface-checked-disabled);
17274
17629
  }
17275
- .db-btn > .db-btn-content > .anticon {
17276
- display: inline-flex;
17277
- align-items: center;
17278
- line-height: 0;
17630
+
17631
+ @media (prefers-reduced-motion: reduce) {
17632
+ .db-switch input,
17633
+ .db-switch input::before {
17634
+ transition: none;
17635
+ }
17279
17636
  }
17280
- .db-btn-content {
17281
- display: inline-flex;
17637
+
17638
+ .db-switch-wrapper {
17639
+ display: flex;
17282
17640
  align-items: center;
17283
17641
  }
17284
- .db-btn > .anticon + .db-btn-content {
17285
- margin-inline-start: var(--db-btn-gap) !important;
17286
- }
17287
- .db-btn-end-icon {
17288
- margin-inline-start: var(--db-btn-end-icon-gap) !important;
17289
- }
17290
- .db-btn-small .db-btn-end-icon {
17291
- margin-inline-start: var(--db-btn-end-icon-gap-small) !important;
17292
- }
17293
- .db-btn-default:not(.db-btn-danger, [href], :hover, :active, :disabled) > .anticon {
17294
- color: var(--db-btn-icon-glyph);
17295
- }
17296
- .db-btn-default[href]:not(.db-btn-danger, :hover, :active, :disabled) .db-btn-end-icon {
17297
- color: var(--db-btn-icon-glyph);
17298
- }
17299
- .db-btn-default:not([href], .db-btn-danger, :hover, :active, :disabled)
17300
- > .db-btn-content
17301
- > .db-btn-end-icon
17302
- > .anticon {
17303
- color: var(--db-btn-icon-glyph);
17304
- }
17305
17642
 
17306
- /* ── 11. Loading ─────────────────────────────────────────────────────────── */
17307
- .db-btn-loading {
17308
- cursor: default;
17309
- }
17310
- .db-btn-loading:disabled {
17311
- border-color: transparent !important;
17312
- }
17313
- .db-btn-spinner {
17314
- color: inherit !important;
17315
- pointer-events: none;
17643
+ .db-switch-wrapper .db-switch-state-message {
17644
+ margin-inline-start: auto;
17645
+ margin-inline-end: var(--db-switch-label-spacing);
17316
17646
  }
17317
- .db-btn-loading-with-icon .db-btn-content {
17318
- padding-inline-start: calc(var(--db-btn-icon-size) + var(--db-btn-gap));
17647
+
17648
+ /* `:first-child` scopes this to the label-first layout; the plain layout renders the label last. */
17649
+ .db-switch-wrapper > .du-bois-light-label:first-child,
17650
+ .db-switch-wrapper > .du-bois-dark-label:first-child {
17651
+ margin-inline-end: var(--db-switch-label-spacing);
17319
17652
  }
17320
- .db-btn-loading .db-btn-spinner {
17321
- position: absolute;
17322
- left: 50%;
17323
- top: 50%;
17324
- transform: translate(-50%, -50%);
17653
+
17654
+ .db-switch-state-message {
17655
+ color: var(--db-switch-text);
17325
17656
  }
17326
- .db-btn-loading.db-btn-icon-only .db-btn-spinner {
17327
- position: static;
17328
- transform: none;
17657
+
17658
+ /* Target the <Label>'s class, not a bare `label` — that would also match the .db-switch root <label>. */
17659
+ .db-switch-wrapper:has(input:disabled) .du-bois-light-label,
17660
+ .db-switch-wrapper:has(input:disabled) .du-bois-dark-label {
17661
+ color: var(--db-switch-text-disabled);
17329
17662
  }
17330
17663
 
17331
- /* ── 12. dangerouslySetForceIconStyles ───────────────────────────────────── */
17332
- .db-btn-force-icon {
17333
- width: var(--db-btn-height) !important;
17334
- color: var(--db-btn-icon-glyph) !important;
17664
+ /* Indent a following hint/message past the track. The class is doubled to out-specify FormMessage's
17665
+ * own margin rule. Each rule's key (rightmost) selector is a single class, not a multi-item :is(),
17666
+ * so Chromium can hash-reject it instead of attempting the match against every element on every
17667
+ * recalc (the leading/middle :is() groups aren't the key selector, so they're left as-is). */
17668
+ :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-light-hint, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-dark-hint, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-dark-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
17669
+ + :is(.du-bois-light-hint, .du-bois-dark-hint)
17670
+ + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
17671
+ + :is(.du-bois-light-hint, .du-bois-dark-hint)
17672
+ + .du-bois-dark-form-message {
17673
+ padding-inline-start: calc(var(--db-space-2) + var(--db-switch-track-width));
17335
17674
  }
17336
- .db-btn-force-icon.db-btn-small {
17337
- width: var(--db-btn-height-small) !important;
17675
+
17676
+ :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-dark-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
17677
+ + :is(.du-bois-light-hint, .du-bois-dark-hint)
17678
+ + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
17679
+ + :is(.du-bois-light-hint, .du-bois-dark-hint)
17680
+ + .du-bois-dark-form-message {
17681
+ margin-top: 0;
17338
17682
  }
17339
17683
 
17684
+ /* Component tokens for Tabs (no family). Re-declared under the theme classes, not only :root, so the
17685
+ * var() chain re-resolves inside a .du-bois-dark subtree instead of freezing at the light value. */
17340
17686
  :where(:root), :where(.du-bois-light) {
17341
- /* Shape */
17342
- /* Must not scale with density; no non-scaling system token holds it. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17343
- --db-navigation-menu-indicator-thickness: 4px;
17344
- /* No system token holds this control-specific inset. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17345
- --db-navigation-menu-focus-ring-offset: -1px;
17346
-
17347
- /* Color */
17348
- --db-navigation-menu-indicator: var(--db-accent-surface);
17349
- --db-navigation-menu-focus-ring: var(--db-focus-ring-color);
17350
- --db-navigation-menu-text: var(--db-text-subtle);
17351
- --db-navigation-menu-text-active: var(--db-text-base);
17352
- /* No v2 semantic for default-action link text yet; bridges on the v1 --db-color-* token. */
17353
- --db-navigation-menu-text-hover: var(--db-color-action-default-text-hover);
17354
- --db-navigation-menu-text-press: var(--db-color-action-default-text-press);
17687
+ /* ── Density ──────────────────────────────────────────────────────────── */
17688
+ --db-tabs-height: var(--db-size-8);
17689
+ --db-tabs-trigger-min-width: var(--db-size-6);
17690
+ --db-tabs-trigger-min-width-closable: var(--db-size-10);
17691
+ --db-tabs-list-margin-bottom: var(--db-space-4);
17692
+ --db-tabs-trigger-gap: var(--db-space-4);
17693
+ --db-tabs-trigger-padding-top: var(--db-space-1);
17694
+ --db-tabs-trigger-padding-bottom: var(--db-space-2);
17695
+ --db-tabs-close-icon-gap: var(--db-space-1);
17696
+ --db-tabs-add-button-margin-top: var(--db-space-0-5);
17697
+ /* No 1.5-unit space step exists; synthesize 6px from the nearest. TODO: use a direct step if one lands. */
17698
+ --db-tabs-add-button-margin-bottom: calc(var(--db-space-1) * 1.5);
17355
17699
 
17356
- /* Density */
17357
- --db-navigation-menu-item-height: var(--db-size-8);
17358
- --db-navigation-menu-item-min-width: var(--db-size-6);
17359
- --db-navigation-menu-link-padding-top: var(--db-space-1);
17360
- --db-navigation-menu-link-padding-bottom: var(--db-space-2);
17361
- --db-navigation-menu-focus-ring-width: var(--db-focus-ring-width);
17362
- }
17700
+ /* ── Shape ────────────────────────────────────────────────────────────── */
17701
+ --db-tabs-scrollbar-radius: var(--db-shape-control);
17702
+ --db-tabs-focus-ring-width: var(--db-focus-ring-width);
17363
17703
 
17704
+ /* ── Typography ───────────────────────────────────────────────────────── */
17705
+ --db-tabs-font-size: var(--db-font-size-base);
17706
+ --db-tabs-font-weight: var(--db-font-weight-semibold);
17707
+ /* No v2 token holds the tab line-height yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17708
+ --db-tabs-line-height: 1.25rem;
17709
+
17710
+ /* ── Color ────────────────────────────────────────────────────────────── */
17711
+ /* No v2 token holds the tab text / hover / press / active / disabled colors yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17712
+ --db-tabs-text: var(--db-color-text-secondary);
17713
+ --db-tabs-text-hover: var(--db-color-action-default-text-hover);
17714
+ --db-tabs-text-press: var(--db-color-action-default-text-press);
17715
+ --db-tabs-text-active: var(--db-color-text-primary);
17716
+ --db-tabs-text-disabled: var(--db-color-action-disabled-text);
17717
+ /* No v2 token holds the tab panel content text color yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17718
+ --db-tabs-content-text: var(--db-color-text-primary);
17719
+ /* No v2 token holds the active-tab underline yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17720
+ --db-tabs-indicator: var(--db-color-action-primary-background-default);
17721
+ /* No v2 token holds the list divider yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17722
+ --db-tabs-border: var(--db-color-action-default-border-default);
17723
+ /* No v2 token holds the focus ring color yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17724
+ --db-tabs-focus-ring: var(--db-color-action-default-border-focus);
17725
+ }
17726
+
17727
+ /* No v2 token holds the scrollbar thumb yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17364
17728
  :where(:root), :where(.du-bois-light) {
17365
- /* Shape */
17366
- /* TODO(FEINF-6505): adopt --db-shape-container-* once a 4px container radius exists; kept on --db-radius-sm to preserve 4px. */
17367
- --db-snackbar-border-radius: var(--db-radius-sm);
17729
+ --db-tabs-scrollbar-thumb: rgba(17, 23, 28, 0.2);
17730
+ --db-tabs-scrollbar-thumb-hover: rgba(17, 23, 28, 0.3);
17731
+ }
17368
17732
 
17369
- /* Density */
17370
- --db-snackbar-spacing-vertical: var(--db-space-3);
17371
- --db-snackbar-spacing-horizontal: var(--db-space-4);
17372
- --db-snackbar-close-button-spacing: var(--db-space-2);
17373
17733
 
17374
- /* Color */
17375
- /* No v1/v2 system token holds this theme-invariant white. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17376
- --db-snackbar-text: #ffffff;
17377
- /* No v1/v2 system token holds these theme-invariant close-icon colors. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17378
- --db-snackbar-close-icon: #e8ecf0;
17379
- --db-snackbar-close-icon-hover: #8acaff;
17380
- --db-snackbar-close-icon-press: #bae1fc;
17381
- }
17382
17734
 
17383
17735
  :where(:root), :where(.du-bois-light) {
17736
+ /* Shape */
17737
+ --db-toolbar-border-radius: var(--db-shape-control);
17738
+
17384
17739
  /* Color */
17385
- /* No v1/v2 system token holds this light/dark surface pair. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17386
- --db-snackbar-surface: #11171c;
17740
+ --db-toolbar-surface: var(--db-surface-subtle);
17741
+ --db-toolbar-border: var(--db-border-base);
17742
+ --db-toolbar-separator-surface: var(--db-border-base);
17743
+ --db-toolbar-toggle-item-text: var(--db-text-base);
17744
+ /* No v2 token holds the toggle hover / press text yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17745
+ --db-toolbar-toggle-item-text-hover: var(--db-color-action-default-text-hover);
17746
+ --db-toolbar-toggle-item-text-press: var(--db-color-action-default-text-press);
17747
+ --db-toolbar-shadow: var(--db-elevation-med);
17387
17748
 
17388
- /* Elevation */
17389
- --db-snackbar-shadow: var(--db-elevation-med);
17749
+ /* Density */
17750
+ --db-toolbar-gap: var(--db-space-4);
17751
+ --db-toolbar-spacing: var(--db-space-2);
17390
17752
  }
17391
17753
 
17392
17754
  /* Tooltip renders an inverted surface (dark chip in light mode, light chip in dark mode), so it does
@@ -17413,41 +17775,12 @@ span.du-bois-light-typography-ellipsis-single-line {
17413
17775
  /* v1 blue primitives, held as literals until a v2 link-on-tooltip semantic exists. */
17414
17776
  --db-tooltip-link-text: #4299e0; /* blue500 */
17415
17777
  --db-tooltip-link-text-hover: #8acaff; /* blue400 */
17416
- --db-tooltip-link-text-press: #bae1fc; /* blue300 */
17778
+ --db-tooltip-link-text-active: #bae1fc; /* blue300 */
17417
17779
 
17418
17780
  /* Elevation */
17419
17781
  --db-tooltip-shadow: var(--db-elevation-med);
17420
17782
  }
17421
17783
 
17422
- /* Declared under the theme classes too (not just :root) so the var() chain re-resolves inside a
17423
- * `.du-bois-dark` subtree instead of freezing at the light values. */
17424
- :where(:root), :where(.du-bois-light) {
17425
- /* Shape */
17426
- --db-progress-border-radius: var(--db-shape-pill);
17427
-
17428
- /* Color */
17429
- --db-progress-track: var(--db-color-progress-track);
17430
- --db-progress-fill: var(--db-color-progress-fill);
17431
-
17432
- /* Density */
17433
- --db-progress-size: var(--db-size-2);
17434
-
17435
- /* Motion */
17436
- /* No v2 motion token holds this duration. TODO(FEINF-6505): adopt a v2 motion token when one is added. */
17437
- --db-progress-transition-duration: 300ms;
17438
- }
17439
-
17440
- :where(:root), :where(.du-bois-light) {
17441
- /* Color */
17442
- /* No v2 token holds tertiary action-text states yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17443
- --db-overflow-trigger-text: var(--db-color-action-tertiary-text-default);
17444
- --db-overflow-trigger-text-hover: var(--db-color-action-tertiary-text-hover);
17445
- --db-overflow-trigger-text-press: var(--db-color-action-tertiary-text-press);
17446
-
17447
- /* Density */
17448
- --db-overflow-spacing: var(--db-space-2);
17449
- --db-overflow-menu-spacing: var(--db-space-1);
17450
- }
17451
17784
 
17452
17785
 
17453
17786
  :where(.du-bois-light-btn-icon-only) > :where(.anticon),