/* ============================================================
   put.io — components.css
   The component layer of the design system: plain CSS classes over
   the tokens, no framework. Import it after tokens.css.

   Consumers: `@putdotio/design/components`, imported after
   `@putdotio/design/css`. Every value here resolves from a token in the
   DTCG graph under `tokens/` — the classes are one binding of that graph,
   not a second source of truth. Icon glyphs assume Phosphor.

   Port notes: class names are the web contract. SwiftUI / Compose /
   Roku read the same values from the DTCG artifacts and own their
   adapters; appearance (filled / ghost) is a variant axis on one
   Button, never a second component.
   ============================================================ */
/* ===========================================================
   NAMING HAZARD — do not define layout classes in the `ph`/`ph-*`
   namespace. Phosphor's icon font owns it: a rule named `.ph` matches
   every regular-weight <i class="ph ph-x"> on the page and turns the
   glyph into a padded flex box. Page headers are `.pgh`, never `.ph`.
   =========================================================== */
/* ===========================================================
   Button base — production parity with the web app's primary button
   - lg (default): h36 / 1.0px letter-spacing
   - md:           h32 / 0.8px
   - sm:           h28 / 0.6px
   - xs:           h24 / 0.3px / fs-xs
   --tw-fs scales the whole thing for breakpoints (unset = 1). It is a
   consumer hook, deliberately not a token: a breakpoint multiplier
   belongs to the page, not the system.
   Geometry is token-driven — --button-height(-md/-sm/-xs),
   --button-padding-x(-xs), --button-label-size(-xs),
   --button-tracking(-md/-sm/-xs), --button-icon-size, --button-gap —
   so native emitters read the same box. ONE BOX PER SIZE: tiers and
   variants change colour only, never geometry, so two buttons of the
   same size always compare safely side by side.
   =========================================================== */
/* NOTE: uppercase + tracking + sizing live on `.btn` — NOT on bare <button>.
   Plain <button> elements (tabs, menu items, segmented controls) inherit
   the reset below but stay sentence case at the size their component chooses. */
button {
  font-family: var(--font-sans);
  font-weight: var(--fw-medium);
  font-size: calc(var(--button-label-size) * var(--tw-fs, 1));  /* production button = fontSize 0 (0.875rem) */
  /* Real starting bg so transition has something to interpolate
     from — otherwise UA `buttonface` paints silver until hover. */
  background-color: transparent;
  border: 1px solid transparent;
  color: var(--text);
  cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--button-gap);
  white-space: nowrap;
  appearance: none; -webkit-appearance: none;
  transition: background-color var(--dur-fast) var(--ease-out),
              border-color    var(--dur-fast) var(--ease-out),
              color           var(--dur-fast) var(--ease-out);
  user-select: none;
}
button i { font-size: var(--button-icon-size); line-height: 1; display: inline-flex; }

/* Brand button — uppercase + tracking + default sizing live HERE, not on
   bare <button>. Anything that shouts "do a thing" (primary / secondary /
   destructive) is a `.btn-*` variant and inherits these. */
.btn,
.btn-primary, .btn-success, .btn-danger, .btn-info, .btn-invert, .btn-default, .btn-ghost {
  height: calc(var(--button-height) * var(--tw-fs, 1));
  padding: 0 calc(var(--button-padding-x) * var(--tw-fs, 1));
  border-radius: var(--button-radius);
  text-transform: uppercase;
  letter-spacing: var(--button-tracking);
}

/* Sizes — letter-spacing tracks size */
.btn-md { height: calc(var(--button-height-md) * var(--tw-fs, 1)) !important; padding: 0 calc(var(--button-padding-x) * var(--tw-fs, 1)) !important; letter-spacing: var(--button-tracking-md); }
.btn-sm { height: calc(var(--button-height-sm) * var(--tw-fs, 1)) !important; padding: 0 calc(var(--button-padding-x) * var(--tw-fs, 1)) !important; letter-spacing: var(--button-tracking-sm); }
.btn-xs { height: calc(var(--button-height-xs) * var(--tw-fs, 1)) !important; padding: 0 calc(var(--button-padding-x-xs) * var(--tw-fs, 1)) !important; font-size: calc(var(--button-label-size-xs) * var(--tw-fs, 1)) !important; letter-spacing: var(--button-tracking-xs); }

/* Icon-only modifier — square, no text padding, no text transform */
.btn-icon { width: calc(var(--button-height) * var(--tw-fs, 1)) !important; padding: 0 !important; text-transform: none; letter-spacing: normal; }
.btn-icon.btn-md { width: calc(var(--button-height-md) * var(--tw-fs, 1)) !important; }
.btn-icon.btn-sm { width: calc(var(--button-height-sm) * var(--tw-fs, 1)) !important; }

/* Layout-only modifier — full-width CTA */
.btn-block { width: 100%; }

/* ===========================================================
   Quiet icon button — the app's IconButton appearance="hover" (its DEFAULT).
   This is the GHOST appearance applied to icon-only controls: transparent
   at rest, the variant's colour in the glyph, the variant's
   component-bg-hover behind it on hover. The neutral case reads the
   --button-ghost-* tokens — same tier, same values as .btn-ghost.
   This is the treatment for icon controls that live inside a row or a
   toolbar — list actions, close, retry, delete. Use the filled
   `.btn-default.btn-icon` only when the button stands alone as a target
   (appearance="filled"). Sizes come from .btn-sm / .btn-md as usual.
   =========================================================== */
.btn-quiet, .btn-quiet.btn-primary, .btn-quiet.btn-warning, .btn-quiet.btn-success,
.btn-quiet.btn-danger, .btn-quiet.btn-info, .btn-quiet.btn-default {
  background: transparent; border-color: transparent; color: var(--button-ghost-fg);
}
.btn-quiet:hover:not(:disabled), .btn-quiet.btn-default:hover:not(:disabled) { background: var(--button-ghost-bg-hover); border-color: transparent; color: var(--button-ghost-fg-hover); }
.btn-quiet.btn-info { color: var(--text); }
/* --yellow-text-secondary, not --yellow-solid: solid is a fill step, and in
   light mode the brand yellow as a glyph colour on a light surface is
   unreadable. The text step carries a legible value in both modes. */
.btn-quiet.btn-primary, .btn-quiet.btn-warning { color: var(--yellow-text-secondary); }
.btn-quiet.btn-primary:hover:not(:disabled), .btn-quiet.btn-warning:hover:not(:disabled) { background: var(--yellow-component-bg-hover); border-color: transparent; color: var(--yellow-text-secondary); }
.btn-quiet.btn-success { color: var(--green-solid); }
.btn-quiet.btn-success:hover:not(:disabled) { background: var(--green-component-bg-hover); border-color: transparent; color: var(--green-solid); }
.btn-quiet.btn-danger { color: var(--red-solid); }
.btn-quiet.btn-danger:hover:not(:disabled) { background: var(--red-component-bg-hover); border-color: transparent; color: var(--red-solid); }
.btn-quiet:disabled { opacity: 1; color: var(--solid); background: transparent; }

/* Variants — token-driven. Foregrounds read from --*-foreground
   tokens in tokens.css so "what color on top" lives in one place.

   CONTRAST CONTRACT: solid success/danger/info fills are tuned for
   APCA Lc 60+ on white text (WCAG 2.x AA-large, not AA-normal). Mark
   specimens that use them with data-contrast-contract="apca" so an
   automated WCAG audit can tell an intentional APCA target from an
   accidental low-contrast bug. Never use these variants at xs/sm — the
   label drops below the large-text threshold. */
.btn-primary  { background: var(--button-primary-bg); border-color: var(--button-primary-bg); color: var(--button-primary-fg); }
.btn-primary:hover:not(:disabled)  { background: var(--button-primary-bg-hover); border-color: var(--button-primary-bg-hover); }
.btn-success  { background: var(--button-success-bg); border-color: var(--button-success-bg); color: var(--button-success-fg); }
.btn-success:hover:not(:disabled)  { background: var(--button-success-bg-hover); border-color: var(--button-success-bg-hover); }
.btn-danger   { background: var(--button-danger-bg);  border-color: var(--button-danger-bg);  color: var(--button-danger-fg); }
.btn-danger:hover:not(:disabled)   { background: var(--button-danger-bg-hover);  border-color: var(--button-danger-bg-hover); }
.btn-info     { background: var(--button-info-bg);    border-color: var(--button-info-bg);    color: var(--button-info-fg); }
.btn-info:hover:not(:disabled)     { background: var(--button-info-bg-hover);    border-color: var(--button-info-bg-hover); }
.btn-invert   { background: var(--text);          border-color: var(--border);        color: var(--invert-foreground); }
.btn-invert:hover:not(:disabled)   { border-color: var(--border-hover); }
.btn-default  { background: var(--button-default-bg); border-color: var(--border);        color: var(--button-default-fg); }
.btn-default:hover:not(:disabled)  { background: var(--button-default-bg-hover); border-color: var(--border-hover); }

/* Ghost — the third tier: text-only. No fill, no border at rest;
   hover paints the shared quiet fill and lifts the label a step
   (--button-ghost-fg → --button-ghost-fg-hover — the fill is never the
   only cue). For actions quieter than a filled secondary: a header
   "Sign in", toolbar text actions, "Cancel" beside a filled confirm.
   NEVER substitute an outline: a border-only box beside a solid fill
   reads as a different size — see the pairing rule in DESIGN.md.
   Disabled matches the quiet treatment: the colour drops to --solid,
   the box does not fade. */
.btn-ghost    { background: var(--button-ghost-bg); border-color: transparent; color: var(--button-ghost-fg); }
.btn-ghost:hover:not(:disabled)    { background: var(--button-ghost-bg-hover); border-color: transparent; color: var(--button-ghost-fg-hover); }
.btn-ghost:disabled { opacity: 1; color: var(--solid); background: transparent; }

/* States */
/* Disabled buttons freeze their resting fill at reduced opacity and ignore
   hover entirely — hover styles above are gated on :not(:disabled), so there
   is no need to (and we must not) reset background to `inherit` here, which
   would strip a disabled primary/danger button of its color on hover. */
button:disabled { opacity: 0.3; cursor: not-allowed; }
button:focus-visible { outline: none; box-shadow: var(--shadow-focus); }

/* Loading state — hides label, paints a CSS spinner over the button */
button.is-loading { color: transparent !important; position: relative; pointer-events: none; opacity: 0.75; }
button.is-loading::after {
  content: ''; position: absolute; inset: 0; margin: auto;
  width: 14px; height: 14px; border-radius: 50%;
  border: 1.5px solid currentColor; border-top-color: transparent;
  opacity: 0.75;
  animation: spin 0.7s linear infinite;
}
.btn-primary.is-loading::after { color: var(--primary-foreground); }
.btn-success.is-loading::after,
.btn-danger.is-loading::after,
.btn-info.is-loading::after,
.btn-invert.is-loading::after { color: var(--destructive-foreground); }
.btn-default.is-loading::after, .btn-ghost.is-loading::after { color: var(--text); }
/* The spinner borrows currentColor, and .is-loading makes the label colour
   transparent — so a quiet button needs its own spinner colour or the control
   shows nothing at all while it works. */
.btn-quiet.is-loading::after { color: var(--button-ghost-fg-hover); }
.btn-quiet.btn-primary.is-loading::after,
.btn-quiet.btn-warning.is-loading::after { color: var(--yellow-text-secondary); }
.btn-quiet.btn-success.is-loading::after { color: var(--green-solid); }
.btn-quiet.btn-danger.is-loading::after { color: var(--red-solid); }
/* Shared spinner keyframe — consumed by .is-loading and by form/shell
   cards. Defined once here; do not redeclare it per card. */
@keyframes spin { to { transform: rotate(360deg); } }

/* Inputs/textareas/selects inherit nothing from the button reset.
   These are dropped for safety in case they ever do. */
input, textarea, select { text-transform: none; letter-spacing: normal; }


/* ===========================================================
   Specimen header — the yellow-mono index marker that anchors
   each preview card. 11 files used to duplicate this verbatim.
   =========================================================== */
.head {
  display: flex; align-items: baseline; gap: 14px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
}
.head .ix {
  font-family: var(--font-mono); font-size: 11px;
  color: var(--yellow-text-secondary); letter-spacing: 0.04em;
}
.head .t { font-size: 15px; font-weight: 500; color: var(--text); }
.head .n {
  margin-left: auto;
  font-size: 12px; color: var(--text-secondary);
}


/* ===========================================================
   Surface primitives — two roles, two chromes.

     .card  = focal, elevated component surface. Sits ON --bg,
              uses --component-bg so it reads as raised in dark
              and contained in light. Auth card, modal, settings
              card, anything the user's attention should go TO.

     .panel = quiet inline reference container. Sits ON --bg,
              uses --bg-secondary so it stays recessive but
              still bordered. Code blocks, info asides, sidebar
              panels, callouts. Lower visual weight than a card.

   Both use the same hairline border + 8–10 px radius — the
   ONLY thing that differs is the surface fill. Pick the role,
   not the chrome.
   =========================================================== */
.card {
  background: var(--component-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}
.card-md { padding: 24px; }
.card-lg { padding: 32px 32px 28px; }

/* Reads the --panel-* aliases, not the generic surface scale. That is the
   documented contract for panel chrome, and it is the difference between a
   consumer being able to retheme panels and having to override this rule. */
.panel {
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--panel-radius);
  overflow: hidden;
}
.panel-pad     { padding: 14px 16px; }
.panel-pad-lg  { padding: 18px 20px; }

/* ===========================================================
   Field primitives — production parity with app.put.io forms.

   ONE source of truth for input chrome. Local preview files used
   to redefine `.field` three times with different focus rings;
   now they all inherit from here. To override per-screen, change
   geometry (height/padding) — not the surface tokens.

   Surface contract:
     bg            → --field-bg          (= --app-bg)
     border        → --field-border      (= --border)
     border:hover  → --field-border-hover
     focus ring    → --field-ring        (1px border-hover, NOT yellow)
     placeholder   → --field-placeholder
     disabled bg   → --field-bg-disabled

   Invalid: opt in via `aria-invalid="true"` — NO `.has-error`
   class. The DOM contract is the same React state pushes to the
   input. Border + ring flip red; the surface stays --field-bg
   (no red fill) per app.put.io behavior.
   =========================================================== */
.field {
  width: 100%;
  height: 36px;
  padding: 0 12px;
  box-sizing: border-box;
  /* --field-radius, not the generic --radius. Same 6px today, but a consumer
     retheming field chrome must not have to override this rule to move it. */
  border-radius: var(--field-radius);
  border: 1px solid var(--field-border);
  background: var(--field-bg);
  color: var(--field-text);
  font-family: var(--font-sans);
  font-size: var(--fs-base);
  transition:
    border-color var(--dur-fast) var(--ease-out),
    box-shadow   var(--dur-fast) var(--ease-out),
    background   var(--dur-fast) var(--ease-out);
}
.field::placeholder { color: var(--field-placeholder); }
.field:hover { border-color: var(--field-border-hover); }
.field:focus,
.field:focus-visible {
  outline: none;
  border-color: var(--field-border-focus);
  box-shadow: var(--field-ring);
}
.field:disabled,
.field[aria-disabled="true"] {
  background: var(--field-bg-disabled);
  color: var(--text-secondary);
  cursor: not-allowed;
}
/* Invalid — border + text only, no surface fill.
   Step convention: rest = step 7 (--red-border),
   focus/hover = step 8 (--red-border-hover). */
.field[aria-invalid="true"] {
  border-color: var(--red-border);
}
.field[aria-invalid="true"]:hover {
  border-color: var(--red-border-hover);
}
.field[aria-invalid="true"]:focus,
.field[aria-invalid="true"]:focus-visible {
  border-color: var(--red-border-hover);
  box-shadow: 0 0 0 1px var(--red-border-hover);
}
/* Loading / async-validating — suppress focus ring so the spinner
   reads as the dominant signal. */
.field[data-state="loading"]:focus,
.field[data-state="loading"]:focus-visible {
  box-shadow: none;
}
.field.mono { font-family: var(--font-mono); font-size: 13px; }

/* ===========================================================
   Form layout recipes — the rhythm comes from gap, not from
   per-screen margins. Use these instead of stacking ad-hoc
   pixel values on every label/hint/error.
   =========================================================== */
.form-stack {
  display: flex;
  flex-direction: column;
  gap: 14px;       /* between label-group rows (label + field + helper) */
}
.form-stack.tight { gap: 10px; }
.form-stack.loose { gap: 20px; }

.form-group {
  display: flex;
  flex-direction: column;
  gap: 6px;        /* label ↔ field ↔ helper/error */
}
.form-group .label { margin-bottom: 0; }   /* gap owns the rhythm now */
.form-group .hint  { margin-top: 0; }
.form-group > .form-callout.inline { margin-top: 0; }

/* Label-row: label on the left, link on the right (e.g. "Forgot your
   password?"). Sits ABOVE the field, inside a .form-group. */
.form-group .label-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
}
.form-group .label-row .label { margin-bottom: 0; }
.form-group .label-row a {
  font-size: 12px;
  color: var(--text-secondary);
  text-decoration: none;
  white-space: nowrap;
}
.form-group .label-row a:hover { color: var(--text); }

/* ===========================================================
   Form feedback — one public recipe for block and inline status.

   `data-state` is the tone: info (the default), success, or error.
   Block callouts consume the dedicated --alert-* families. `.inline`
   removes their chrome but deliberately keeps the same semantic text
   colour, so a field error does not invent another red.
   =========================================================== */
.form-callout,
.form-callout[data-state="info"] {
  box-sizing: border-box;
  width: 100%;
  display: flex;
  align-items: start;
  gap: 8px;
  padding: 8px 10px;
  border: 1px solid var(--alert-info-border);
  border-radius: var(--alert-radius);
  background: var(--alert-info-bg);
  color: var(--alert-info-body);
  font-size: var(--fs-sm);
  line-height: var(--lh-normal);
}
.form-callout[hidden] { display: none; }
.form-callout > :is(i, svg) {
  flex-shrink: 0;
  color: var(--alert-info-icon);
  font-size: 14px;
  line-height: var(--lh-normal);
}
.form-callout[data-state="success"] {
  border-color: var(--alert-success-border);
  background: var(--alert-success-bg);
  color: var(--alert-success-body);
}
.form-callout[data-state="success"] > :is(i, svg) { color: var(--alert-success-icon); }
.form-callout[data-state="error"] {
  border-color: var(--alert-danger-border);
  background: var(--alert-danger-bg);
  color: var(--alert-danger-body);
}
.form-callout[data-state="error"] > :is(i, svg) { color: var(--alert-danger-icon); }
.form-callout.inline {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  font-size: var(--fs-sm);
}

/* ===========================================================
   One-time password — one real input, six visual slots.

   The transparent input spans the whole recipe so pointer, keyboard,
   paste, password-manager and one-time-code interactions all reach the
   same semantic control. Script only mirrors digits and the active slot;
   invalid styling derives from aria-invalid on the input itself.

   Each three-slot group is a fluid grid. Slots never publish a fixed
   width, which keeps two groups plus their separator inside narrow cards.
   =========================================================== */
.otp {
  position: relative;
  display: flex;
  align-items: center;
  gap: clamp(4px, 2vw, 8px);
  width: 100%;
  min-width: 0;
}
.otp-input {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  cursor: text;
}
.otp-group {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(4px, 2vw, 8px);
  flex: 1 1 0;
  min-width: 0;
}
.otp-slot {
  box-sizing: border-box;
  min-width: 0;
  height: 52px;
  display: grid;
  place-items: center;
  border: 1px solid var(--field-border);
  border-radius: var(--field-radius);
  background: var(--field-bg);
  color: var(--field-text);
  font-family: var(--font-mono);
  font-size: 22px;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  pointer-events: none;
  transition:
    border-color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}
.otp-slot[data-state="filled"] { border-color: var(--field-border-hover); }
.otp:focus-within .otp-slot[data-state="active"] {
  border-color: var(--field-border-focus);
  box-shadow: var(--field-ring);
}
.otp-separator {
  flex: 0 0 auto;
  padding: 0;
  color: var(--text-secondary);
  font-size: 18px;
  line-height: 1;
  pointer-events: none;
}
.otp:has(.otp-input[aria-invalid="true"]) .otp-slot {
  border-color: var(--red-border);
  color: var(--red-text-secondary);
}
.otp:has(.otp-input[aria-invalid="true"]):focus-within .otp-slot[data-state="active"] {
  border-color: var(--red-border-hover);
  box-shadow: 0 0 0 1px var(--red-border-hover);
}
.otp[data-state="success"] .otp-slot {
  border-color: var(--green-border);
  color: var(--green-text-secondary);
}
.otp[data-state="success"]:focus-within .otp-slot[data-state="active"] {
  border-color: var(--green-border-hover);
  box-shadow: 0 0 0 1px var(--green-border-hover);
}
.otp[data-state="verifying"] .otp-input,
.otp[aria-disabled="true"] .otp-input,
.otp-input:disabled { pointer-events: none; }
.otp[data-state="verifying"] .otp-slot,
.otp[data-state="verifying"] .otp-slot[data-state="active"],
.otp[aria-disabled="true"] .otp-slot,
.otp[aria-disabled="true"] .otp-slot[data-state="active"],
.otp:has(.otp-input:disabled) .otp-slot,
.otp:has(.otp-input:disabled) .otp-slot[data-state="active"] {
  border-color: var(--field-border);
  background: var(--field-bg-disabled);
  color: var(--text-secondary);
  box-shadow: none;
  cursor: not-allowed;
}

/* Floating-menu surface — one recipe for dropdowns, context menus,
   submenus, and on-player popovers. Cards keep their item styles local.
   Consumed by web-c09-menu.html and web-p06-player.html — it looks unused
   from this file alone, so do not delete it. */
/* Reads the --menu-* aliases for surface, border, radius and shadow. The
   generic scale is close but not identical, and using it here would silently
   break --menu-* retheming for every consumer of this recipe. */
.menu-pop {
  min-width: 216px;
  background: var(--menu-bg);
  border: 1px solid var(--menu-border);
  border-radius: var(--menu-radius);
  box-shadow: var(--menu-shadow);
  padding: 4px;
  z-index: var(--z-dropdown);
  display: flex; flex-direction: column; gap: 1px;
}
.menu-pop .menu-label {
  padding: 6px 10px 4px;
  font-family: var(--font-mono); font-size: 10.5px;
  letter-spacing: 0.06em; text-transform: uppercase;
  /* --menu-label, not the generic --text-secondary. Same value in both modes
     today, so this is a retheming fix rather than a visual one: the family owns
     menu chrome, and a consumer moving --menu-label must reach this. */
  color: var(--menu-label);
}

/* ===========================================================
   Selection controls — ONE checkbox and ONE switch for the
   whole system.

   State comes from `data-state="checked"`, the same attribute a
   headless primitive already sets, so the DOM contract holds
   whatever drives it. The legacy `.checked` / `.on` class flags
   are still matched for the specimens that predate this, but
   `.on` is exactly the invented flag the state contract tells
   you not to add — prefer the attribute in new markup.

   .checkbox  — div or <span>, data-state="checked" when on
   input[type="checkbox"] — native inputs get the brand accent
   .switch    — div, data-state="checked" when on
   =========================================================== */
.checkbox {
  width: 14px; height: 14px; flex-shrink: 0;
  border: 1.5px solid var(--border-hover);
  border-radius: var(--radius-sm);
  background: transparent;
  transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
.checkbox.checked,
.checkbox[data-state="checked"] {
  background: var(--yellow-solid);
  border-color: var(--yellow-solid);
  position: relative;
}
.checkbox.checked::after,
.checkbox[data-state="checked"]::after {
  content: ''; position: absolute; left: 3px; top: 0;
  width: 5px; height: 9px;
  border: solid var(--primary-foreground); border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
input[type="checkbox"], input[type="radio"] { accent-color: var(--yellow-solid); cursor: pointer; }

.switch {
  width: 44px; height: 24px; flex-shrink: 0;
  border-radius: var(--radius-pill);
  background: var(--component-bg-active);
  position: relative; cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out);
}
.switch::after {
  content: ''; position: absolute; top: 2px; left: 2px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--text);
  transition: left var(--dur-fast) var(--ease-out);
}
.switch.on,
.switch[data-state="checked"] { background: var(--green-solid); }
/* The thumb rides the green selected track, so it takes the token for "what
   colour sits on green" rather than a raw white that no theme can reach. */
.switch.on::after,
.switch[data-state="checked"]::after { left: 22px; background: var(--button-success-fg); }
