# Actions and badges — M4 checkpoint

[Live reference](../actions.html) · [Source audit](wizard-actions-audit.md) · [Light comparison](reference/actions-comparison-light-1200.png) · [Dark comparison](reference/actions-comparison-dark-1200.png).

The left column contains actual WSDMM control captures from the audited reference commit; the right column is live KB markup. The reference checkout is still at that commit. The wide source add-row action is cropped at native pixel size, preserving its label/icon scale. Captures are evidence, not an alternative implementation. Default comparisons leave both sides at rest; [light keyboard focus](reference/actions-focus-light.png) and [dark keyboard focus](reference/actions-focus-dark.png) show the right-hand Next action focused separately.

## Shared consumption

```html
<link rel="stylesheet" href="theme.css">
<link rel="stylesheet" href="actions.css">
<button type="button" class="kb-button kb-button--primary">Next</button>
<button type="button" class="kb-button">Back</button>
<span class="kb-badge kb-badge--pill">0 devices</span>
```

```scss
@use 'path/to/kb/scss' as kb;
@include kb.theme();
@include kb.actions();
```

Importing the Sass API alone emits nothing. The optional actions layer emits only its named classes and media rules; theme.css still contains variables/theme scopes only. Use theme overrides for `action-*` and `badge-*` colors. Geometry lives in scss/_action-styles.scss; colors live in scss/_tokens.scss. Build with `npm run build:theme`, `npm run build:actions`, `npm run build:examples`; check with `npm run check:theme` and `npm run check:actions`. Do not edit generated CSS. Consuming pages own Noto Sans loading and native event handlers.

## Icons are optional

Choose icons by use case, not by button variant. Text-only buttons are fully supported and should be used when the label communicates the action clearly. Add an icon when it improves recognition, meaning or direction; omit decorative icons that add no useful information. The wizard captures show its existing choices, not a requirement for every button in the standard.

When an icon is useful, use the documented Lucide size/stroke and spacing. An icon-only action needs an accessible name and a recognizable purpose; a tooltip alone is not its accessible name. Primary, secondary and destructive buttons can all be text-only.

## Roles and source geometry

| Class / role | Geometry | Contract |
|---|---|---|
| `.kb-button` secondary navigation | Mobile 42px, 10px radius, 14px/20px label, 10px/16px padding; ≥640px 50px, 14px radius, 16px/24px label, 12px/24px padding | Native button or navigation link; weight 500, 8px icon gap |
| `--primary` | Navigation geometry | Primary action gradient; use sparingly |
| `--toolbar` | Mobile 34px, 10px radius, 8px/12px padding; desktop 42px, 14px radius, 10px/16px padding | 14px label, 16px icon; mobile text may be visually clipped with `.kb-button-label`, never display:none without an accessible name |
| `--apply` with `--primary` | 51px, 14px radius, 14px/24px padding, 14px/21px label, 20px icon | Distinct source Apply proportions; the reference demonstrates a local loading state only |
| `--add` | 57px, 14px radius, 16px padding, 2px dashed border, 20px icon | Full-width container action, not a default primary button |
| `--icon` | 40×40px, 10px radius, 16px icon | Explicit standard extension with aria-label; title may supplement the name |
| `--danger` | Navigation geometry | Explicit destructive extension, red fill with readable text and a visible contour |
| `.kb-badge` | 12px/16px text, padding 4px/8px, 4px radius | Passive source-inspired type label; no tabindex, pressed state or fake click handler |
| `.kb-badge--count` | 38px, padding 8px/16px, 10px radius, 14px/20px text | Passive count panel |
| `.kb-badge--pill` | 28px, padding 4px/12px, pill radius, 14px/20px text | Passive count/status treatment |
| `.kb-chip` | 14px/20px, padding 6px/12px, pill radius | Explicit selectable-chip extension: native button with aria-pressed and an accessible group name |

Long labels can wrap; content-driven heights can increase under zoom/localization. There is no native readonly button/selectable-chip state. Button invalid styling is not a substitute for associated form errors.

## Button elevation and pressed feedback — accepted R019

Buttons are flat by default. Add `kb-button--elevated` alongside `kb-button--primary` only when subtle elevation helps separate a dominant action from its surface. Elevation is optional; it is not implied by primary, Apply, an icon, or an action being destructive. The example Apply opts in, but this is a use-case choice.

```html
<button type="button" class="kb-button kb-button--primary kb-button--elevated">
  Apply changes
</button>
```

Light rest/hover shadows are 0 1px 2px black/16 and 0 2px 4px black/18. Dark equivalents are 0 2px 4px black/28 and 0 3px 6px black/32. Press removes exterior elevation and uses a soft inset 0 1px 2px black/18, with role-specific pressed fill tokens. The former 2px currentColor inner stroke is removed. Primary, secondary, toolbar, destructive and selected/unselected chip fills retain their semantic roles during press.

Disabled/busy controls become flat immediately, without a lingering shadow transition. Ordinary buttons, chips and badges have no raised rest shadow. Press is temporary; chip selection remains aria-pressed and does not add elevation. Keyboard focus retains its separate explicit outline. No translate/scale or target-size movement is introduced. Forced colors removes shadows while focus remains visible; reduced motion removes transitions.

Consumers can override `button-shadow-rest`, `button-shadow-hover`, `button-shadow-pressed` and the `action-*-pressed` roles through the existing theme mixin. These are button-specific roles, not the later card/overlay elevation system. The [UX decision record](button-ux-review.md) preserves the source and review evidence.

## Explicit source differences and conflicts

- Preserve measured wizard dimensions and 500 weight, rather than the company guide's bold 14.4px button role or the generic CVA button's unrelated dimensions. The old skill's azure action palette is not the actual wizard palette.
- Keep source #004a8f → #005a9c primary gradients, with dark alpha stops. R025 restores the dark primary border to source blue/30 and separates neutral/toolbar/add contours; cyan is reserved for keyboard focus.
- Light toolbar text is #005a9c rather than the source's low-contrast #4a9fd8. R025 restores dark toolbar/secondary fills to source white/10. Hover uses white/15 instead of source white/20 after the latter failed sampled text contrast on this backdrop (4.34:1). White labels and white/5 pressed fills retain readable feedback. Add-row backgrounds are transparent at rest and white/5 dark hover.
- Explicit keyboard focus uses a 2px outline, 3px offset and accepted theme focus color. Held-active actions use a darker role-specific fill and subtle inset contact shadow. This is deliberate standard behavior, not source focus/active parity.
- Disabled variants share one inactive palette rather than inheriting inconsistent source opacity/hover behavior. Native disabled controls cannot activate or receive focus. Disabled navigation links are not supported: use a real button when disabling is meaningful.
- Mobile toolbar labels remain in the accessibility tree, fixing the source's empty mobile Import name. The compact source height is retained; source matching does not establish a universal 44px touch-target policy. Verify target sizing/spacing in the consuming interface.
- Icon/destructive actions, selectable chips and the local loading flow are explicit extensions. Source edit/remove/type-badge markup was inspected, but not live-rendered with a nonempty device list in R014. Do not call these exact source replicas.

## Behavior and accessibility

Use native button type=button for local actions; type=submit only inside an intended form flow. Use an actual href link for navigation (the reference includes a keyboard-tested Field reference link). Icons are Lucide, currentColor, stroke 2, decorative aria-hidden/focusable=false when text or aria-label already names the action. Keep links and action semantics distinct.

Consumers own aria-pressed, disabled, aria-busy and live status. The reference toggles native-chip pressed states with Space/Enter. Its asynchronous example sets disabled and aria-busy, changes the visible label, then restores the action and announces completion. Repeat activation is excluded while busy. No application configuration, uploads, downloads, network writes or backend operations are performed. Review JavaScript is a demonstration, not an actions.css runtime dependency.

Passive badge updates accompany an explicit status message; color alone does not convey the count/type. Neither badge class enters the tab order. Reduced motion removes transitions; forced colors retains system-adjusted text and visible keyboard focus. Selected chips retain a border distinction there.

## Verified and remaining review

[Functional results](reference/actions-verification.json) · [hover/active/pressed observations](reference/actions-state-verification.json) · [sampled contrast](reference/actions-contrast.json).

Both themes at 1200/390px: source-role heights, loaded comparison images, theme/source-image synchronization, keyboard focus, disabled exclusion, chip Space/Enter, local count changes, loading completion/repeat-click exclusion, passive badge tab order, actual Noto Sans glyph rendering, mobile Import accessible name, forced colors/reduced motion and 320px reflow passed. Five Sass contracts and generated CSS checks pass; M3 field output remains unchanged. R019 final role-state samples and browser checks are linked below.

Historical R015 measurements: enabled default text was at least 5.85:1 and button contours at least 3.20:1. R025 supersedes those contour values with source-style softer borders; do not carry that contour claim forward. Pixel samples use the flat top contour and a nearby fill point, with foreground colors resolved by Chromium. Gradients, transparency, anti-aliasing, zoom and alternate backdrops require integration checks. These measurements do not certify all hover/focus/pressed pixels, all chip/backdrop combinations, every browser/OS or all WCAG requirements. M4 is complete after the user-approved button UX refinements in R019; integration-specific limitations remain.

WSDMM Central and SignalView B source remain unchanged. No pushes, application migration, broad palette replacement, card/shadow standard or publication is included.

## R019 checkpoint evidence (historical colors)

[Final behavior checks](reference/actions-final-functional.json) · [Final role-state checks](reference/actions-final-states.json) · [Final text contrast](reference/actions-final-contrast.json). Rest/hover/pressed text was sampled across all nine affected roles, both themes and desktop/mobile. Historical source crops and earlier review-only proposals do not override the shared tokens.

## R025 button color correction

The previous dark black fills, cyan primary contour and white/65 neutral contours departed visibly from the wizard. Current dark roles use white/10 fills, white/30 secondary borders, white/20 toolbar/add borders and source blue/30 primary borders. Light secondary borders use gray-300 and toolbar borders blue-200. Add-row backgrounds are transparent at rest. These are action-specific roles; field borders and passive badge borders remain unchanged. Stronger light toolbar label color remains an explicit contrast refinement. Geometry, optional icons, elevation and native behavior remain unchanged.

Reference captures must wait for theme transitions to settle; the initial icon preview captured partially interpolated light fills against dark labels/backgrounds. [Settled dark example](reference/button-colors-dark.png) · [settled light example](reference/button-colors-light.png). Source 20% white hover failed sampled contrast; the bounded 15% refinement is deliberate, not exact source parity. Soft resting contours are not claimed to independently meet 3:1; verify component identification, focus and contrast in the consuming context.

Current evidence: [108 state/text samples](reference/button-colors-contrast.json), minimum 4.80:1; [role-state behavior](reference/button-colors-states.json); [functional regression](reference/button-colors-functional.json). Both themes/desktop/mobile pass at sampled points. This supersedes earlier color/contour evidence, not the source history.
