Two regressions from the recent token sweep, both producing low-contrast
results in dark mode.
## DS::Toggle off-track
PR #1843 (DS::Toggle a11y + token swaps) replaced the raw
`bg-gray-100 theme-dark:bg-gray-700` off-track with `bg-surface-inset`
for semantic alignment. `bg-surface-inset` resolves to gray-800 in
dark mode, but the toggle typically sits inside `bg-container`
(gray-900). The contrast ratio dropped from ~2.45:1 (gray-700 vs
gray-900) to ~1.5:1 (gray-800 vs gray-900) — visibly worse than the
pre-#1843 baseline and below WCAG 1.4.11 (3:1 for UI components).
Most visible inside the transaction-edit modal SETTINGS section
(`Exclude`, `One-time Expense`) where the off-state switches nearly
vanished into the modal chrome.
Introduce `--color-toggle-track` (light: gray-100, dark: gray-700) and
swap `bg-surface-inset` → `bg-toggle-track` in DS::Toggle. Restores the
pre-#1843 off-track contrast while keeping a semantic token (instead
of the raw palette references the migration was trying to remove).
## border-destructive subtle borders
PR #1849 (single-color tokens to @theme) flagged that
`border-destructive/N` rendered the wrong shade (the `@utility
border-destructive` block defined red-500 light, while
`--color-destructive` in `@theme` is red-600 — `/N` resolves from
@theme), and swapped a couple of callsites to solid `border-destructive`.
Solid renders red-500/red-400 at full saturation in both modes, which
reads as a loud error border on contexts that were meant to be subtle
(left-rule on the provider-sync "view error details" pane, error-message
box in SimpleFIN settings, alert-component border, provider connection
error rows).
Two callsites (`DS::Alert`, settings/providers/_connection_row) still
carried the broken `border-destructive/20` / `/25` modifier — same
off-shade footgun #1849 was meant to retire.
Introduce `--color-destructive-subtle` (light: red-200, dark: red-800)
and swap the four subtle-by-intent callsites to `border-destructive-subtle`:
- app/components/DS/alert.rb (destructive variant)
- app/views/settings/providers/_connection_row.html.erb (err status)
- app/components/provider_sync_summary.html.erb (error-details left rule)
- app/views/simplefin_items/edit.html.erb (error-message box)
The handful of intentionally-loud `border-destructive` callsites
(split-transaction over-allocation, blank-name account labels, etc.)
keep the solid token.
Regenerated `_generated.css` via `npm run tokens:build`.
* fix(design-system): DS::Toggle a11y + token swaps
Closes#1746. Four fixes on the toggle primitive (visual switch
backed by a sr-only checkbox).
1. **Focus ring (WCAG 2.4.7)** — the `<input>` is `sr-only`, so the
browser-default focus ring lands on an invisible 0px element.
The label (the track) had no focus styling, meaning the
component had **no visible focus indicator at all**. Add
`peer-focus-visible:ring-2 ring-offset-2 ring-gray-900` with
`theme-dark:peer-focus-visible:ring-white` so the ring appears
on the visible track when the underlying checkbox receives
keyboard focus.
2. **Role semantics** — visual is a switch, but the element was
announced as "checkbox, checked" because the native input is
a checkbox. Add `role="switch"` so AT users hear "switch, on"
/ "switch, off". `aria-checked` is inherited from the
checkbox's checked state, no manual wiring needed.
3. **Token swaps** — replace raw palette references with semantic
tokens:
- Track `bg-gray-100 theme-dark:bg-gray-700` → `bg-surface-inset`
- Checked `peer-checked:bg-green-600` → `peer-checked:bg-success`
Picks up the contrast bump from #1735 automatically.
4. **Motion safety (WCAG 2.3.3)** — gate the bg color +
thumb-translate transitions behind `motion-safe:`. Reduced-motion
users see an instant state snap; everyone else gets the
existing 300ms ease.
API unchanged. Existing 8 callsites (settings/preferences,
settings/appearances, account_sharings, budgets/edit,
recurring_transactions, styled_form_builder bridge) work without
changes.
* fix(review): use alpha tokens for Toggle focus ring
Swap raw palette (ring-gray-900 / theme-dark:ring-white) on the
DS::Toggle focus ring to ring-alpha-black-300 / ring-alpha-white-300
to match the focus-ring token pattern already used by
form-field, provider_card, and shared/_badge.
Closes AI review feedback on #1843.