@layer components {
/* Canonical text-input field. Label-above-input, optional required indicator,
   optional helper or error message. Shared by `input`, `textarea`,
   `select`, `checkbox`, `radio`, and `switch` components. Upload variants
   TBD in a follow-up. */

.psw-field { display: flex; flex-direction: column; gap: var(--space-1); }

/* Header row for fields that need metadata next to the label (currently
   used by the textarea partial for its character counter). Flex row with
   the label on the left and the metadata anchored on the right. Without
   this header, fields render just the bare <label>. */
.psw-field__header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
}

/* Character counter — muted by default, turns red when the --over modifier is
   added (Alpine toggles it when count > max). Uses tabular-nums so digits
   don't shift width as the count grows. */
.psw-field__counter {
    font-size: var(--text-paragraph-sm);
    color: var(--color-content-medium);
    flex-shrink: 0;
    font-variant-numeric: tabular-nums;
}
.psw-field__counter--over {
    color: var(--color-danger-400);
}

.psw-field__label {
    font-size: var(--text-label-md);
    line-height: var(--text-label-md--line-height);
    font-weight: var(--font-weight-bold);
    color: var(--color-gray-700);
}
/* Red asterisk that follows the label when required=True. The asterisk is
   aria-hidden in the partial — the <input required> attribute already conveys
   the constraint to assistive tech, so the visual marker is purely decorative.
   No spacing between label and asterisk — the partial emits them adjacent
   so the label text doesn't have trailing whitespace (which would otherwise
   show up when selecting the label or read out by some AT). */
.psw-field__label-required {
    color: var(--color-danger-400);
}
/* Optional descriptive line between the label and the input. Visually
   subordinate to the label above, distinct from validation/error messages. */
.psw-field__subtitle {
    font-size: var(--text-paragraph-sm);
    font-weight: var(--font-weight-regular);
    color: var(--color-content-medium);
    /* Parent flex gap is 4px; this adds 8px so subtitle -> input totals 12px. */
    margin: 0 0 var(--space-2);
}

/* Wrapper around the <input>. Hosts the left-side icon (optional) and the
   always-present clear button (visibility toggled by --has-value). Carries
   top-only spacing when there is no subtitle. The wrapper has no bottom
   margin so error messages and following content own their own spacing.
   position:relative
   so the icon + clear button can absolutely-position inside. */
.psw-field__input-wrap {
    position: relative;
    /* 8px margin + 4px parent flex gap = 12px label -> input. */
    margin: var(--space-2) 0 0;
}
.psw-field__subtitle + .psw-field__input-wrap { margin-top: 0; }

/* Left-side icon — 20px square (WidgetIcon md), vertically centered. Sits
   12px in from the left edge of the input. Color is gray-700 to match the
   input text color rather than the theme accent — the icon is informational
   (e.g. `$` for currency), not interactive. */
.psw-field__input-icon {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    display: inline-flex;
    pointer-events: none;                          /* clicks fall through to the input */
    color: var(--color-gray-700);
}
/* .psw-icon's own rule sets color: var(--theme-primary, …), which would
   override our gray on the SVG itself. Force inheritance instead, mirroring
   the same workaround psw-btn uses (.psw-btn .psw-icon { color: currentColor }). */
.psw-field__input-icon .psw-icon { color: currentColor; }
.psw-field__input-icon--left { left: 12px; }

/* Clear button — themed color, always in DOM, hidden until the input has
   a value (psw-field.js toggles --has-value on every input event; the
   server pre-applies it when value=... is non-empty on first paint).
   Disabled/readonly inputs never get a clear button — :has() suppresses
   it without needing a second class. visibility:hidden (not display:none)
   preserves layout space so the input's right padding is consistent
   regardless of value state. */
.psw-field__input-clear {
    position: absolute;
    top: 50%;
    right: 8px;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    background: transparent;
    border: none;
    border-radius: var(--radius-max);                        /* circular hover/focus pill — Figma node 2347-21033 */
    color: var(--theme-primary, var(--theme-primary-default));      /* themed; SVG inherits via currentColor */
    cursor: pointer;
    visibility: hidden;
    transition: background-color 120ms ease;
}
.psw-field__input-wrap--has-value .psw-field__input-clear { visibility: visible; }
.psw-field__input-wrap:has(input:disabled) .psw-field__input-clear,
.psw-field__input-wrap:has(input[readonly]) .psw-field__input-clear { visibility: hidden; }
/* Hover only on hover-capable pointers — on touch, :hover sticks to the
   button after a tap and reads as a stuck state. :active still paints the
   pressed feedback while the finger is down. */
@media (hover: hover) {
    .psw-field__input-clear:hover { background: var(--color-gray-100); }
}
.psw-field__input-clear:active { background: var(--color-gray-100); }
.psw-field__input-clear:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--theme-primary, var(--theme-primary-default));
}
/* When a chevron is present in the same input-wrap (select fields),
   the clear button shifts LEFT so it sits beside the chevron instead
   of overlapping it. 12 (chevron edge) + 20 (chevron) + 4 (gap) = 36. */
.psw-field__input-wrap:has(.psw-field__input-chevron) .psw-field__input-clear {
    right: 36px;
}

/* Chevron-down indicator on the right edge of select fields. Always
   visible — when the field carries a value the clear button surfaces
   ALONGSIDE the chevron (to the chevron's left), not in place of it.
   Decorative (pointer-events: none) so clicks fall through to the
   input and trigger the dropdown-open behavior wired by psw-field.js.
   Rotates 180° when the menu is open (driven by aria-expanded on the
   sibling combobox input — a CSS-only state derived from the wiring
   psw-field.js already sets). */
.psw-field__input-chevron {
    position: absolute;
    top: 50%;
    right: 12px;
    transform: translateY(-50%);
    display: inline-flex;
    pointer-events: none;
    color: var(--color-gray-700);
    transition: transform 150ms ease;
}
.psw-field__input-chevron .psw-icon { color: currentColor; }
.psw-field__input-wrap:has([aria-expanded="true"]) .psw-field__input-chevron {
    transform: translateY(-50%) rotate(180deg);
}

/* Input text is body content, not a label, matching the existing React field
   reference. */
.psw-field__input {
    font-family: var(--font-sans);
    font-size: var(--text-paragraph-md);
    line-height: var(--text-paragraph-md--line-height);
    color: var(--color-gray-700);
    background: var(--color-white);
    /* 11px vertical + 24px line-height + 2×1px border = 48px tall; 16px sides. */
    padding: 11px 16px;
    /* Gray 500 — the same stroke the checkbox and radio carry. */
    border: 1px solid var(--color-gray-500);
    border-radius: var(--radius-md);
    width: 100%;
    transition: border-color 120ms ease, box-shadow 120ms ease;
}
.psw-field__input::placeholder { color: var(--color-gray-400); }

/* Padding modifiers — make room for the inline icon and the clear button.
   --with-clear is always applied (clear button always in DOM, even when
   hidden) so input width doesn't jitter on value change. --with-icon-left
   only applied when an icon is rendered.
     12px (input padding-left) + 20px (icon md) + 8px (gap)  = 40px
     28px (clear button) + 8px (right offset) + 4px (gap)    = 40px */
.psw-field__input--with-icon-left { padding-left: 40px; }
.psw-field__input--with-clear     { padding-right: 40px; }
/* Wider right padding because the select renders BOTH the chevron and
   (when --has-value) the clear button side by side. Math: chevron at
   right:12 (20px wide) + 4px gap + clear button (28px hit area) + 8px
   text buffer = 72px. Defined after --with-clear so the cascade picks
   this when both classes are present on the same input (select uses
   both). */
.psw-field__input--with-chevron   { padding-right: 72px; }
/* Suppress the browser-default "x" clear button on <input type="search">.
   WebKit/Blink only — Firefox does not expose this pseudo-element. */
.psw-field__input[type="search"]::-webkit-search-cancel-button {
    -webkit-appearance: none;
    appearance: none;
}
/* Hover = 2px, focus = 3px ring. Implemented as box-shadow rather than
   `outline` because browsers interpolate outline-width in integer steps
   (causes visible jitter); box-shadow blur/spread animates smoothly.
   border-color collapses to transparent on interaction so the gray rest-
   state border doesn't show alongside the colored ring. Both rules use
   `:not(:disabled)` so they have equal specificity — source order then
   wins, putting :focus after :hover so focus takes over on click. */
.psw-field__input:not(:disabled):hover {
    border-color: transparent;
    box-shadow: 0 0 0 2px var(--theme-primary, var(--theme-primary-default));
}
.psw-field__input:not(:disabled):focus {
    outline: none;
    border-color: transparent;
    box-shadow: 0 0 0 3px var(--theme-primary, var(--theme-primary-default));
}
.psw-field__input:disabled {
    background: var(--color-white);
    border-color: var(--color-border-disabled);
    color: var(--color-content-disabled);       /* Gray 400 — disabled field text */
    cursor: not-allowed;
}

/* Textarea overrides on the shared input class. Taller line-height for
   multiline readability, vertical-only resize handle, and a minimum
   height so empty textareas still feel like a paragraph slot rather
   than a single-line input. */
.psw-field__input--textarea {
    line-height: 1.5;
    resize: vertical;
    min-height: 96px;
    /* 8px + 4px parent flex gap = 12px label -> textarea, matching
       .psw-field__input-wrap (textareas render without that wrapper). */
    margin: var(--space-2) 0 0;
}
.psw-field__subtitle + .psw-field__input--textarea { margin-top: 0; }

/* Disabled wrapper — mute label + subtitle so the whole field block reads
   as disabled, not just the input. The input's own :disabled rule handles
   its background/color/cursor; this picks up the surrounding text. */
.psw-field--disabled .psw-field__label,
.psw-field--disabled .psw-field__subtitle,
.psw-field--disabled .psw-field__choice-label,
.psw-field--disabled .psw-field__input-icon,
.psw-field--disabled .psw-field__input-chevron {
    color: var(--color-content-disabled);
}

/* The message slot is exclusively an error message — it only renders when
   the developer passes `error="..."`. So it defaults to red; no neutral
   "hint" state exists. */
.psw-field__message {
    font-size: var(--text-label-sm);
    color: var(--color-danger-400);
    margin: var(--space-2) 0 0;
}

/* Validation-message variant. Pre-rendered when the dev supplies
   `validation_message=`, but hidden until the field contains a
   :user-invalid input the user has left — i.e. they finished editing and
   native HTML5 constraint validation has failed. Inline alternative to the
   browser's popover. */
.psw-field__message--invalid {
    display: none;
}
.psw-field:has(.psw-field__input:user-invalid:not(:focus)) > .psw-field__message--invalid {
    display: block;
}

/* Error state — red border, and hover/focus ring turns red too so the
   error treatment is preserved across interaction states. */
.psw-field--error .psw-field__input {
    border-color: var(--color-danger-400);
}
.psw-field--error .psw-field__input:not(:disabled):hover {
    border-color: transparent;
    box-shadow: 0 0 0 2px var(--color-danger-400);
}
.psw-field--error .psw-field__input:not(:disabled):focus {
    border-color: transparent;
    box-shadow: 0 0 0 3px var(--color-danger-400);
}

/* Native HTML5 validation. :user-invalid fires only AFTER the user has
   interacted with the field (no flash-of-invalid on initial render — that's
   why we don't use plain :invalid). Composes with .psw-field--error: a server-
   rendered error and a failed HTML5 constraint reach the same visual state via
   independent paths. Browser support: Chrome 119+, Safari 16.5+, Firefox 88+.

   :not(:focus) holds the error back while the field is focused — the user is
   still editing, and a half-typed email or a field they just cleared isn't a
   mistake yet. The error lands on blur. .psw-field--error above is deliberately
   NOT gated this way: a server-rejected value is wrong whether or not the
   cursor is in it. */
.psw-field__input:user-invalid:not(:focus) {
    border-color: var(--color-danger-400);
}
.psw-field__input:user-invalid:not(:disabled):not(:focus):hover {
    border-color: transparent;
    box-shadow: 0 0 0 2px var(--color-danger-400);
}

/* Small-size modifier. Used by `select` (sm 36px tall) and available to
   any field that needs a denser footprint. md is the implicit default —
   no .psw-field--md selector is needed since the base rules already
   describe it. Affects label + input typography; subtitle is already
   Paragraph/Small in both variants per Figma. */
.psw-field--sm .psw-field__label {
    font-size: var(--text-label-sm);
    line-height: var(--text-label-sm--line-height);
}
.psw-field--sm .psw-field__input {
    font-size: var(--text-paragraph-sm);
    line-height: var(--text-paragraph-sm--line-height);
    /* 7px vertical + 20px line-height + 2×1px border = 36px tall; 16px sides. */
    padding: 7px 16px;
}
.psw-field--sm .psw-field__input--with-chevron {
    padding-right: 72px;
}

/* Native mobile select. Desktop gets the searchable custom combobox; mobile
   gets the platform picker (iOS bottom sheet / Android native UI). JS moves
   the submitted `name` between native and custom controls to avoid duplicate
   form values. */
.psw-select__native {
    display: none;
    appearance: auto;
    -webkit-appearance: menulist;
}

@media (hover: none), (pointer: coarse), (max-width: 768px) {
    .psw-field[data-psw-select] .psw-field__input-wrap {
        display: none;
    }
    .psw-field[data-psw-select] .psw-select__native {
        display: block;
        margin: var(--space-2) 0 0;
    }
    .psw-field__subtitle + .psw-select__native { margin-top: 0; }
}

/* ============================================================
   Select dropdown menu (.psw-select__menu)
   Lives inside .psw-field__input-wrap so it can position: absolute
   against the wrap and overlay subsequent content instead of pushing
   the page down. The wrap already carries position: relative for the
   chevron/clear icons; the menu reuses that anchor.
   ============================================================ */
.psw-select__menu {
    position: absolute;
    top: calc(100% + 4px);                    /* 4px gap below the input */
    left: 0;
    right: 0;
    z-index: 10;                              /* over downstream form fields */
    list-style: none;
    margin: 0;
    padding: var(--space-2);                  /* Figma padding-small (8px) */
    background: var(--color-white);
    border-radius: var(--radius-md);
    box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);    /* Figma drop-shadow */
    max-height: 240px;
    overflow-y: auto;
}
.psw-select__menu[hidden] { display: none; }

/* Option row. Horizontal flex: [media?] [text-stack] [check].
   Padding 12px / 16px and gap 16px come straight from Figma. */
.psw-select__option {
    display: flex;
    align-items: center;
    gap: var(--space-4);                      /* 16px between media / text / check */
    padding: var(--space-3) var(--space-4);   /* 12px vertical, 16px horizontal */
    border-radius: var(--radius-md);
    color: var(--color-gray-700);
    cursor: pointer;
}
.psw-select__option:hover,
.psw-select__option--active,
.psw-select__option[aria-selected="true"] {
    /* Figma uses rgba(15,15,15,0.04) — gray-700 at 4% alpha. Same
       treatment for hover, keyboard-active, and selected; the
       checkmark icon is what differentiates the selected state. */
    background: rgba(15, 15, 15, 0.04);
}
/* Hidden when the typed filter excludes this option — JS toggles
   the class on every input event. */
.psw-select__option--hidden { display: none; }

/* Leading media slot — either an icon (WidgetIcon SVG) or an
   image (URL). Both occupy the same square footprint so swapping
   one for the other doesn't reflow the row. md = 20px, sm = 16px. */
.psw-select__option-icon,
.psw-select__option-image {
    flex-shrink: 0;
    width: 20px;
    height: 20px;
}
.psw-select__option-icon {
    display: inline-flex;
    color: var(--color-gray-700);
}
.psw-select__option-icon .psw-icon { color: currentColor; }
.psw-select__option-image {
    object-fit: cover;
    border-radius: var(--radius-sm);
}

/* Text column — label on top, optional subtitle beneath. Vertical
   2px gap matches Figma's `spacing/vertical/between-text-xsm`.
   min-width:0 + flex:1 so a long label can ellipsize cleanly
   inside the constrained menu width. */
.psw-select__option-text {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.psw-select__option-label {
    font-size: var(--text-paragraph-md);
    line-height: var(--text-paragraph-md--line-height);
    color: var(--color-gray-700);
}
.psw-select__option-subtitle {
    font-size: var(--text-paragraph-sm);
    line-height: var(--text-paragraph-sm--line-height);
    color: var(--color-content-medium);
}

/* Trailing checkmark — themed primary color, always reserved in
   the layout so option rows stay aligned regardless of which one is
   selected. Visible only when the option carries aria-selected. */
.psw-select__option-check {
    flex-shrink: 0;
    width: 20px;
    height: 20px;
    display: inline-flex;
    visibility: hidden;
    color: var(--theme-primary, var(--theme-primary-default));
}
.psw-select__option-check .psw-icon { color: currentColor; }
.psw-select__option[aria-selected="true"] .psw-select__option-check { visibility: visible; }

/* Small-size dropdown. Icons / image / check drop to 16px, label
   one step smaller, subtitle one step smaller again. Matches the
   field's sm chrome — 14px input text, 12px helper text. */
.psw-field--sm .psw-select__option-icon,
.psw-field--sm .psw-select__option-image,
.psw-field--sm .psw-select__option-check {
    width: 16px;
    height: 16px;
}
.psw-field--sm .psw-select__option-label {
    font-size: var(--text-paragraph-sm);
    line-height: var(--text-paragraph-sm--line-height);
}
.psw-field--sm .psw-select__option-subtitle {
    font-size: var(--text-paragraph-xs);
    line-height: var(--text-paragraph-xs--line-height);
}

/* ──────────────────────────────────────────────────────────────────────────
   Checkbox variant. Native <input type="checkbox"> with appearance: none so
   the box, border, focus ring, and check icon are all controlled here. The
   wrapping <label> is the click target — clicking anywhere in the row
   toggles the input via native HTML semantics.

   States:
     Unselected         — white fill, gray-500 (#757575) 2px border.
     Hover (unselected) — white fill, theme-primary (Cobalt 500 default) 2px border.
     Selected           — theme-primary fill + border, white check icon centered.
     Disabled unchecked — white fill, border-disabled border.
     Disabled checked   — theme fill + theme border at 50% opacity (no grey stroke).
   ────────────────────────────────────────────────────────────────────────── */

/* Clickable row holding the box + label text. align-items:flex-start so a
   wrapped multi-line label keeps the box pinned to the first line. */
.psw-field__choice-row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    cursor: pointer;
}
/* Disabled field — the parent <label> still wraps the input, so the row
   inherits cursor:pointer above. Override here (and the input's own
   :disabled rule sets the cursor on the box) so neither surface invites
   a click. */
.psw-field--disabled .psw-field__choice-row {
    cursor: not-allowed;
}

.psw-field__checkbox-input {
    appearance: none;
    -webkit-appearance: none;
    flex-shrink: 0;
    width: 20px;
    height: 20px;
    margin: 0;
    border: 2px solid var(--color-gray-500);
    border-radius: var(--radius-sm);
    background-color: var(--color-white);
    background-repeat: no-repeat;
    background-position: center;
    background-size: 12px 12px;
    cursor: pointer;
    transition: background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
}
/* Size variants (Figma): sm 16/check 10, md 20/check 12 (default), lg
   24/check 16. Scale the box AND the check icon (background-size) together. */
.psw-field--checkbox-sm .psw-field__checkbox-input {
    width: 16px;
    height: 16px;
    border-width: var(--border-width-sm, 1px);
    background-size: 10px 10px;
}
.psw-field--checkbox-lg .psw-field__checkbox-input {
    width: 24px;
    height: 24px;
    background-size: 16px 16px;
}
.psw-field--checkbox-sm .psw-field__choice-label {
    font-size: var(--text-paragraph-sm);
    line-height: var(--text-paragraph-sm--line-height);
}
.psw-field__checkbox-input:not(:disabled):hover {
    border-color: var(--theme-primary, var(--theme-primary-default));
}
/* :focus-visible (not :focus) so click-focus on mouse-down doesn't paint a
   ring around every just-toggled box — only keyboard / programmatic focus
   triggers it. Figma: 2px black ring with a 2px gap (the white surface shows
   through the inner shadow). Shared with radio + switch. */
.psw-field__checkbox-input:not(:disabled):focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--color-white), 0 0 0 4px var(--color-gray-700);
}
.psw-field__checkbox-input:checked {
    background-color: var(--theme-primary, var(--theme-primary-default));
    border-color: var(--theme-primary, var(--theme-primary-default));
    /* Inlined: a separately fetched asset carries no ?ver= and caches for 30d, so an
       edited glyph would not reach warm browsers. */
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3e%3cpath d='M3 8.5L6.5 12L13 5' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3e%3c/svg%3e");
}
.psw-field__checkbox-input:disabled {
    cursor: not-allowed;
    border-color: var(--color-border-disabled);
}
/* Disabled + checked: keep the theme-primary fill at full saturation and
   dim the whole input via opacity. Earlier attempts mixed the theme color
   toward white (~35%) which desaturated cobalt / peacock / grape themes
   into nearly-grey shades; opacity preserves the theme character while
   still reading as inactive. The shared :checked rule above already
   wires up the background fill + check icon. */
.psw-field__checkbox-input:disabled:checked {
    opacity: 0.5;
    /* Keep the theme border (matches the fill) so disabled-selected shows no
       grey stroke — only the whole box dims via opacity. */
    border-color: var(--theme-primary, var(--theme-primary-default));
}

/* Paragraph-weight label text — visually subordinate to the box, which
   carries the field's primary visual weight. Distinct from .psw-field__label
   (input/textarea) which is bold. */
.psw-field__choice-label {
    font-size: var(--text-paragraph-md);
    line-height: var(--text-paragraph-md--line-height);
    color: var(--color-gray-700);
}

/* Subtitle and message slots align under the label text, not the box, so
   the row reads as a coherent block. Default = 24px box + var(--space-2) gap
   (the radio box size); checkbox overrides below track its per-size box. */
.psw-field__choice-subtitle,
.psw-field__choice-message {
    margin-left: calc(24px + var(--space-2));
}
/* Checkbox box is 20px (md) by default, 16px (sm) / 24px (lg) — align the
   subtitle/message under the label accordingly. */
.psw-field--checkbox .psw-field__choice-subtitle,
.psw-field--checkbox .psw-field__choice-message {
    margin-left: calc(20px + var(--space-2));
}
.psw-field--checkbox-sm .psw-field__choice-subtitle,
.psw-field--checkbox-sm .psw-field__choice-message {
    margin-left: calc(16px + var(--space-2));
}
.psw-field--checkbox-lg .psw-field__choice-subtitle,
.psw-field--checkbox-lg .psw-field__choice-message {
    margin-left: calc(24px + var(--space-2));
}

/* Error state — red border + matching hover/focus ring. Mirrors the
   .psw-field--error .psw-field__input rules above. */
.psw-field--error .psw-field__checkbox-input {
    border-color: var(--color-danger-400);
}
.psw-field--error .psw-field__checkbox-input:not(:disabled):hover {
    border-color: var(--color-danger-400);
}
.psw-field--error .psw-field__checkbox-input:not(:disabled):focus-visible {
    box-shadow: 0 0 0 2px var(--color-white), 0 0 0 4px var(--color-danger-400);
}

/* Native :user-invalid (required + unchecked + interacted) reaches the same
   visual state as server-rendered error, via an independent path. */
.psw-field__checkbox-input:user-invalid {
    border-color: var(--color-danger-400);
}
.psw-field__checkbox-input:user-invalid:not(:disabled):hover {
    border-color: var(--color-danger-400);
}
.psw-field__checkbox-input:user-invalid:not(:disabled):focus-visible {
    box-shadow: 0 0 0 2px var(--color-white), 0 0 0 4px var(--color-danger-400);
}
/* The general-sibling :user-invalid rule above (line ~137) targets
   .psw-field__message--invalid as a direct sibling of the input. For
   choice-row variants (checkbox / radio / switch) the message lives
   outside the <label>, so hop out through it via :has. Covers all
   three because no other element inside a choice-row can be
   :user-invalid. */
.psw-field__choice-row:has(:user-invalid) ~ .psw-field__message--invalid {
    display: block;
}

/* ──────────────────────────────────────────────────────────────────────────
   Radio variant. Native <input type="radio"> with appearance: none so the
   circle, border, focus ring, and inner dot are all controlled here.
   Shares .psw-field__choice-{row,label,subtitle,message} with checkbox
   (clickable row + paragraph-weight label + aligned subtitle/message
   slot). The radio-specific input itself is round, has no background-
   color change on selected, and uses a radial-gradient inner dot.

   States:
     Unselected         — white fill, gray-500 (#757575) 2px border.
     Hover (unselected) — white fill, info-400 2px border.
     Selected           — white fill, info-400 2px border, 12px solid
                          info-400 dot centered.
     Disabled           — white fill, border-disabled border.
   ────────────────────────────────────────────────────────────────────────── */

.psw-field__radio-input {
    appearance: none;
    -webkit-appearance: none;
    flex-shrink: 0;
    width: 24px;
    height: 24px;
    margin: 0;
    border: 2px solid var(--color-gray-500);
    border-radius: 50%;
    background-color: var(--color-white);
    cursor: pointer;
    transition: border-color 120ms ease, box-shadow 120ms ease;
}
.psw-field__radio-input:not(:disabled):hover {
    border-color: var(--theme-primary, var(--theme-primary-default));
}
.psw-field__radio-input:not(:disabled):focus-visible {
    outline: none;
    /* Figma: 2px black focus ring with a 2px gap. Shared with checkbox + switch. */
    box-shadow: 0 0 0 2px var(--color-white), 0 0 0 4px var(--color-gray-700);
}
/* Selected = blue border + 12px solid blue dot. Render the dot via a
   radial-gradient so we don't need a separate SVG asset. The 5.5/6.5 split
   antialiases the edge — sharper than `0 6px, transparent 6px` (which can
   show a hard pixel-step in some browsers). */
.psw-field__radio-input:checked {
    border-color: var(--theme-primary, var(--theme-primary-default));
    background-image: radial-gradient(circle,
        var(--theme-primary, var(--theme-primary-default)) 5.5px,
        transparent 6.5px);
}
.psw-field__radio-input:disabled {
    cursor: not-allowed;
    border-color: var(--color-border-disabled);
}
/* Disabled + checked: dim to 50% but keep the theme-primary border so the
   dot reads as a muted active selection, not as disabled-gray. The :checked
   rule above already sets the radial-gradient dot. Mirrors the checkbox
   treatment. */
.psw-field__radio-input:disabled:checked {
    border-color: var(--theme-primary, var(--theme-primary-default));
    opacity: 0.5;
}

/* Error state — red border + matching hover/focus ring. */
.psw-field--error .psw-field__radio-input {
    border-color: var(--color-danger-400);
}
.psw-field--error .psw-field__radio-input:not(:disabled):hover {
    border-color: var(--color-danger-400);
}
.psw-field--error .psw-field__radio-input:not(:disabled):focus-visible {
    box-shadow: 0 0 0 2px var(--color-white), 0 0 0 4px var(--color-danger-400);
}
.psw-field--error .psw-field__radio-input:checked {
    border-color: var(--color-danger-400);
    background-image: radial-gradient(circle,
        var(--color-danger-400) 5.5px,
        transparent 6.5px);
}

/* Native :user-invalid (required + group-unselected + interacted) reaches
   the same red treatment via an independent path. */
.psw-field__radio-input:user-invalid {
    border-color: var(--color-danger-400);
}
.psw-field__radio-input:user-invalid:not(:disabled):hover {
    border-color: var(--color-danger-400);
}
.psw-field__radio-input:user-invalid:not(:disabled):focus-visible {
    box-shadow: 0 0 0 2px var(--color-white), 0 0 0 4px var(--color-danger-400);
}

/* ──────────────────────────────────────────────────────────────────────────
   Switch variant. A pill-shaped track with a sliding thumb, backed by a
   visually-hidden native <input type="checkbox"> so form submission, keyboard
   toggle (Space), and AT semantics remain native. Sibling <span> .__track
   renders the visible track + thumb (the thumb is the track's ::before
   pseudo-element).

   States:
     Unselected         — track gray-400 (#afafaf), thumb left.
     Selected           — track theme-primary (Cobalt 500 default), thumb right.
     Hover (unselected) — track theme-primary, thumb still left (preview of intent
                          to enable; deliberate design call from the Figma).
     Disabled           — track border-disabled (#f0f0f0), thumb left.
   ────────────────────────────────────────────────────────────────────────── */

/* The native input is visually hidden via the .u-sr-only utility (applied
   in switch.html alongside this class) — display:none would also hide it
   from AT and break focus. The class .psw-field__switch-input is kept as
   an identifier so the sibling-state selectors below can target it. */
.psw-field__switch-input { /* SR-only via .u-sr-only — see switch.html */ }

/* Track + thumb. Track is 56×32 pill, thumb is a 24px circle positioned
   via the ::before pseudo on the track <span>. `transition: left` on the
   pseudo gives the smooth slide; track background-color transitions
   independently for the color change. */
.psw-field__switch-track {
    flex-shrink: 0;
    position: relative;
    display: inline-block;
    width: 56px;
    height: 32px;
    /* Off-state contrast vs page background. gray-400 (#afafaf) on white was
       ~2.6:1, below WCAG AA's 3:1 floor for UI components. gray-500 (#757575)
       is ~4.6:1 and matches the unselected border color of checkbox / radio,
       keeping the family visually consistent. */
    background-color: var(--color-gray-500);
    border-radius: var(--radius-max);
    cursor: pointer;
    transition: background-color 120ms ease, box-shadow 120ms ease;
}
.psw-field__switch-track::before {
    content: '';
    position: absolute;
    top: 4px;
    left: 4px;
    width: 24px;
    height: 24px;
    background-color: var(--color-white);
    border-radius: 50%;
    transition: left 200ms ease;
}

/* Checked = track info-400, thumb slides right.
   left: 56 - 24 - 4 = 28px so the thumb keeps its 4px gutter on the right. */
.psw-field__switch-input:checked + .psw-field__switch-track {
    background-color: var(--theme-primary, var(--theme-primary-default));
}
.psw-field__switch-input:checked + .psw-field__switch-track::before {
    left: 28px;
}

/* Hover (unselected): track previews the selected color while the thumb
   stays put — a "click here to enable" hint. Matches Figma's "Hover
   (Unselected)" cell. The :not(:checked) ensures hovering an already-on
   switch doesn't change anything visually. */
.psw-field__switch-input:not(:disabled):not(:checked):hover + .psw-field__switch-track,
.psw-field__choice-row:hover .psw-field__switch-input:not(:disabled):not(:checked) + .psw-field__switch-track {
    background-color: var(--theme-primary, var(--theme-primary-default));
}

/* Hover (selected): darken the on-state track with a 10% black overlay
   (Figma-added). The thumb is a separate ::before element, so it stays white. */
.psw-field__switch-input:not(:disabled):checked:hover + .psw-field__switch-track,
.psw-field__choice-row:hover .psw-field__switch-input:not(:disabled):checked + .psw-field__switch-track {
    background-image: linear-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.1));
}

/* Keyboard focus ring on the track (since the input itself is SR-only).
   Figma: 2px black ring with a 2px gap, shared with checkbox + radio. */
.psw-field__switch-input:not(:disabled):focus-visible + .psw-field__switch-track {
    box-shadow: 0 0 0 2px var(--color-white), 0 0 0 4px var(--color-gray-700);
}

/* Disabled: track muted to bg-disabled, cursor changes, no hover preview. */
.psw-field__switch-input:disabled + .psw-field__switch-track {
    background-color: var(--color-border-disabled);
    cursor: not-allowed;
}

/* Subtitle / message slots align under the label text (right of the
   56px track + space-2 gap), not under the track itself. */
.psw-field__switch-subtitle,
.psw-field__switch-message {
    margin-left: calc(56px + var(--space-2));
}

/* Error state — red track + matching focus ring. */
.psw-field--error .psw-field__switch-input + .psw-field__switch-track {
    background-color: var(--color-danger-400);
}
.psw-field--error .psw-field__switch-input:not(:disabled):focus-visible + .psw-field__switch-track {
    box-shadow: 0 0 0 2px var(--color-white), 0 0 0 4px var(--color-danger-400);
}

/* Switch deliberately omits :user-invalid track-color rules (unlike checkbox
   and radio above). A switch is a toggle, not an agreement — turning a
   `required` switch from on to off mid-form qualifies as "significant
   interaction" under the HTML5 :user-invalid spec and would otherwise paint
   the track red on every toggle-off, which reads as a UI bug. Server-
   rendered `error=` still surfaces via .psw-field--error, and the browser's
   submit-time popover still fires from the native `required` attribute. */
}
