/* ═══════════════════════════════════════════════════════════════════════
   form-fields.jsx — Phase 2 dedup target 10 canonicals.

   Family of related primitives that share a token set but each have their
   own ergonomics, accessibility model, and keyboard behavior.

   Components:
     <TextField>     — single-line input (text/email/tel/url/number/password/search)
     <Textarea>      — multi-line input
     <Select>        — native <select> styled to match TextField
     <Checkbox>      — boolean, staged choice
     <Switch>        — boolean, immediate state change
     <RadioGroup>    — compact list-style choice (Yes/No/Unsure)
                       (high-decision branching uses TileSelect target 3)
     <FileDrop>      — drag-and-drop file upload zone

   Hard rules baked in:
     - Native <input>/<textarea>/<select> underneath all visual styling
     - Visible label required (srOnly modifier hides visually but keeps
       in accessible name)
     - Focus styling via :focus-visible on real elements (not React state) —
       avoids re-renders that would cross-contaminate global THEME when
       dark + light variants render side-by-side on the design canvas
     - Error state communicates via BOTH color AND text AND icon
     - Disabled state uses opacity AND cursor AND bg shift
     - Material Symbols Rounded for any icon slot
     - Placeholder is NEVER a substitute for label
     - Distinct from Btn geometry: form fields are rounded-rectangles
       (8-12px), not pills. Reads as "editable surface" not "action."
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Inject one-time field CSS into the document ───────────────────────── */
/* Focus + floating-label styling lives in CSS, not React state. Means the
   components are pure functions of props — they don't re-render on focus,
   which keeps their captured THEME values stable when multiple instances
   render in different modes side-by-side.
 *
 * Floating-label pattern (Material outlined + notched-border):
 *   - Input has placeholder=" " (single space) so :placeholder-shown
 *     correctly distinguishes "empty input" from "user has typed."
 *   - Label is absolutely positioned, centered vertically at rest.
 *   - On :focus or :not(:placeholder-shown), label lifts to top: 0 with
 *     a SOLID pill background that masks the border underneath — gives
 *     the notched-outline look from the login page.
 *   - Pill bg is mode-aware via CSS custom properties on the wrapper.
 */

(function ensureFieldCss() {
  if (document.getElementById('ds-field-css')) return;
  const css = `
    /* Wrapper provides positioning context + CSS vars for label pill bg. */
    .ds-fld {
      position: relative;
      width: 100%;
    }

    /* The actual input/textarea/select. */
    .ds-fld__input {
      width: 100%;
      box-sizing: border-box;
      font-family: inherit;
      outline: none;
      transition: border-color 200ms ease, background 200ms ease, box-shadow 200ms ease;
    }
    .ds-fld__input:focus-visible {
      border-color: var(--ds-fld-focus, #4E6DA8);
      box-shadow: 0 0 0 3px var(--ds-fld-focus-ring, rgba(78,109,168,0.18));
    }
    .ds-fld__input[aria-invalid="true"] {
      border-color: #C24A38 !important;
    }
    .ds-fld__input[aria-invalid="true"]:focus-visible {
      box-shadow: 0 0 0 3px rgba(194,74,56,0.22) !important;
    }
    .ds-fld__input[disabled], .ds-fld__input[readonly] {
      cursor: not-allowed;
    }

    /* The floating label. */
    .ds-fld__label {
      position: absolute;
      top: 50%;
      /* At REST: clear the leading icon if present (rest var is set per-field).
         At FLOAT: snap back to the standard 14px so labels align across the
         whole form regardless of which fields have icons. */
      left: var(--ds-fld-label-left-rest, 14px);
      transform: translateY(-50%);
      font-size: var(--ds-fld-label-rest-size, 13px);
      color: var(--ds-fld-label-rest, rgba(242,244,248,0.55));
      pointer-events: none;
      background: transparent;
      padding: 0 4px;
      letter-spacing: 0;
      text-transform: none;
      font-weight: 400;
      line-height: 1;
      transition: top 200ms ease, left 200ms ease, font-size 200ms ease, color 200ms ease, letter-spacing 200ms ease, padding 200ms ease;
    }

    /* Floating state — focused or filled. Label lifts to the border with
       a pill bg that notches the outline. translateY(-50%) inherited from
       base rule so the pill centers ON the top border (notched-outline). */
    .ds-fld__input:focus + .ds-fld__label,
    .ds-fld__input:not(:placeholder-shown) + .ds-fld__label,
    /* Select is special — it always has a value, so always treat as floating */
    .ds-fld--select .ds-fld__label {
      top: 0;
      left: 14px;
      font-size: 9.5px;
      letter-spacing: 1.3px;
      color: var(--ds-fld-label-pill-fg, #4E6DA8);
      background: var(--ds-fld-label-pill-bg, #212A32);
      border-radius: 999px;
      text-transform: uppercase;
      font-weight: 700;
      padding: 2px 8px;
      line-height: 1;
    }
    .ds-fld__input[aria-invalid="true"]:focus + .ds-fld__label,
    .ds-fld__input[aria-invalid="true"]:not(:placeholder-shown) + .ds-fld__label,
    .ds-fld--select.ds-fld--error .ds-fld__label {
      color: #C24A38;
    }

    /* Required asterisk. */
    .ds-fld__required {
      color: #C24A38;
      margin-left: 4px;
    }

    /* Helper + error text below the field. */
    .ds-fld__helper {
      font-size: 11.5px;
      line-height: 1.4;
      margin-top: 11px;
      color: var(--ds-fld-helper, rgba(0,0,0,0.55));
    }
    .ds-fld__error {
      font-size: 11.5px;
      line-height: 1.4;
      margin-top: 11px;
      color: #C24A38;
      font-weight: 500;
      display: flex;
      align-items: center;
      gap: 6px;
    }

    /* Trailing chevron for select. */
    .ds-fld__chevron {
      position: absolute;
      right: 12px;
      top: 50%;
      transform: translateY(-50%);
      pointer-events: none;
      color: var(--ds-fld-chevron, rgba(0,0,0,0.45));
      font-size: 20px;
    }

    /* Leading icon inside input. */
    .ds-fld__icon-leading {
      position: absolute;
      left: 12px;
      top: 50%;
      transform: translateY(-50%);
      pointer-events: none;
      color: var(--ds-fld-icon, rgba(0,0,0,0.45));
      font-size: 18px;
    }
    /* Text adornment (prefix/suffix) — for currency, units, percent, etc.
       Sits at the same positions as the icon slots but renders as text. */
    .ds-fld__adornment {
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      pointer-events: none;
      color: var(--ds-fld-adornment, rgba(0,0,0,0.55));
      font-size: 13px;
      font-weight: 500;
      letter-spacing: 0.3px;
    }
    .ds-fld__adornment--leading { left: 14px; }
    .ds-fld__adornment--trailing { right: 14px; }

    /* Textarea-specific: at REST the label sits inside the textarea near the
       top (textareas don't vertically-center text). At FLOAT it lifts to the
       border with translateY(-50%) so the pill centers ON the top edge —
       same notched-outline behavior as text inputs. */
    .ds-fld--textarea .ds-fld__label {
      top: 18px;
      transform: none;
    }
    .ds-fld--textarea .ds-fld__input:focus + .ds-fld__label,
    .ds-fld--textarea .ds-fld__input:not(:placeholder-shown) + .ds-fld__label {
      top: 0;
      transform: translateY(-50%);
    }
  `;
  const style = document.createElement('style');
  style.id = 'ds-field-css';
  style.textContent = css;
  document.head.appendChild(style);
})();

/* ── Shared field-shell tokens ─────────────────────────────────────────── */

const FIELD_SIZE = {
  sm: { height: 36, padX: 12, radius: 8, fontSize: 12.5, borderWidth: 1   },
  md: { height: 44, padX: 14, radius: 8, fontSize: 13.5, borderWidth: 1   },
  lg: { height: 52, padX: 16, radius: 10, fontSize: 14,  borderWidth: 1.5 },
};

function buildFieldVars({ isDark }) {
  // Mode-aware CSS variables — every value sized for WCAG AA with margin.
  //
  // Contrast targets:
  //   - Body text on surface:  7:1+ (AAA)
  //   - Small text on surface: 4.5:1 (AA), aiming 6:1+
  //   - UI element borders:    3:1 (AA), aiming 4:1+
  //
  // Light helper @ 0.72 ink → ~7:1 on paper.
  // Dark helper @ 0.72 white → ~9:1 on navy.
  // Border @ 0.40 → ~3.3:1 dark, ~3.4:1 light. Comfortable AA pass for UI.
  const periwinkle    = isDark ? '#9FB3FF' : '#4E6DA8';
  const periwinkleRing = isDark
    ? 'rgba(159,179,255,0.22)'
    : 'rgba(78,109,168,0.20)';
  // Floating-label pill — solid opaque background matching the EFFECTIVE
  // input background (page bg + the input's semi-transparent overlay), so
  // the floated label visually reads as a continuation of the input rather
  // than a separate chip. Calculated from buildInputStyle's baseBg overlay:
  //   dark:  #1A232C + rgba(255,255,255,0.03) -> #212A32
  //   light: #E8E2D2 + rgba(255,255,255,0.50) -> #F3F0E8
  // Transparent variants let the border line bleed through the label text;
  // these solids mask the border cleanly while keeping pill = input visually.
  const labelPillBg   = isDark ? '#212A32'  : '#F3F0E8';
  const labelPillFg   = isDark ? '#D7DEE2'  : '#2C4470';
  // Borders, helper, label-rest, chevron all bumped from 0.18/0.55 to
  // 0.40/0.72/0.65 to pass WCAG AA for both UI elements (3:1) and small
  // text (4.5:1) in both modes with comfortable margin.
  const borderRest    = isDark ? 'rgba(255,255,255,0.40)' : 'rgba(20,30,50,0.40)';
  const labelRest     = isDark ? 'rgba(242,244,248,0.72)' : 'rgba(20,30,50,0.72)';
  const helperColor   = isDark ? 'rgba(242,244,248,0.72)' : 'rgba(20,30,50,0.72)';
  const chevronColor  = isDark ? 'rgba(242,244,248,0.65)' : 'rgba(20,30,50,0.65)';
  // Adornment text (prefix/suffix) — slightly dimmer than input text but
  // strong enough to read as "label/unit" not "placeholder."
  const adornmentColor = isDark ? 'rgba(242,244,248,0.78)' : 'rgba(20,30,50,0.78)';

  return {
    '--ds-fld-focus': periwinkle,
    '--ds-fld-focus-ring': periwinkleRing,
    '--ds-fld-border': borderRest,
    '--ds-fld-label-rest': labelRest,
    '--ds-fld-label-pill-bg': labelPillBg,
    '--ds-fld-label-pill-fg': labelPillFg,
    '--ds-fld-helper': helperColor,
    '--ds-fld-chevron': chevronColor,
    '--ds-fld-icon': chevronColor,
    '--ds-fld-adornment': adornmentColor,
  };
}

function buildInputStyle({ size = 'md', variant = 'bordered', disabled = false, isDark, hasLeadingIcon = false, hasTrailingIcon = false, hasLeadingAdornment = false, hasTrailingAdornment = false }) {
  const s = FIELD_SIZE[size] || FIELD_SIZE.md;
  if (variant === 'transparent') {
    return {
      width: '100%', boxSizing: 'border-box',
      padding: 0,
      background: 'transparent',
      border: 'none',
      color: THEME.text,
      fontSize: s.fontSize, lineHeight: 1.5,
      opacity: disabled ? 0.45 : 1,
      cursor: disabled ? 'not-allowed' : 'text',
    };
  }
  const baseBg = isDark ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.50)';
  const disabledBg = isDark ? 'rgba(255,255,255,0.012)' : 'rgba(255,255,255,0.30)';
  // AA-bumped border (was 0.18 → 0.40 for 3:1 UI contrast).
  const borderColor = isDark ? 'rgba(255,255,255,0.40)' : 'rgba(20,30,50,0.40)';
  const padLeft = hasLeadingIcon || hasLeadingAdornment ? (s.padX + 22) : s.padX;
  const padRight = hasTrailingIcon || hasTrailingAdornment ? (s.padX + 22) : s.padX;
  return {
    width: '100%',
    boxSizing: 'border-box',
    height: s.height,
    padding: `0 ${padRight}px 0 ${padLeft}px`,
    borderRadius: s.radius,
    background: disabled ? disabledBg : baseBg,
    border: `${s.borderWidth}px solid ${borderColor}`,
    color: THEME.text,
    fontSize: s.fontSize,
    letterSpacing: 0.3,
    opacity: disabled ? 0.55 : 1,
    cursor: disabled ? 'not-allowed' : 'text',
  };
}

function buildTextareaStyle({ size = 'md', variant = 'bordered', disabled = false, isDark, rows = 4 }) {
  const s = FIELD_SIZE[size] || FIELD_SIZE.md;
  if (variant === 'transparent') {
    return {
      width: '100%', boxSizing: 'border-box',
      padding: 0,
      background: 'transparent',
      border: 'none',
      color: THEME.text,
      fontSize: s.fontSize, lineHeight: 1.55,
      resize: 'vertical',
      opacity: disabled ? 0.45 : 1,
    };
  }
  const baseBg = isDark ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.50)';
  const disabledBg = isDark ? 'rgba(255,255,255,0.012)' : 'rgba(255,255,255,0.30)';
  // AA-bumped border (was 0.18 → 0.40 for 3:1 UI contrast).
  const borderColor = isDark ? 'rgba(255,255,255,0.40)' : 'rgba(20,30,50,0.40)';
  return {
    width: '100%',
    boxSizing: 'border-box',
    padding: `14px ${s.padX}px 12px`,
    borderRadius: s.radius,
    background: disabled ? disabledBg : baseBg,
    border: `${s.borderWidth}px solid ${borderColor}`,
    color: THEME.text,
    fontSize: s.fontSize,
    lineHeight: 1.55,
    letterSpacing: 0.2,
    resize: 'vertical',
    minHeight: 24 * rows + 28,
    opacity: disabled ? 0.55 : 1,
  };
}

/* ── FieldShell: helper + error below the field (label is FLOATING, lives
   inside the wrapper next to the input, not above it). ─────────────────── */

const FieldShell = ({ helperText, errorText, children }) => {
  const hasError = Boolean(errorText);
  // Color the helper from THEME directly. The --ds-fld-helper var is set on the
  // input wrapper (inside children), not here, so the helper would otherwise
  // fall back to dark rgba(0,0,0,0.55) and vanish in dark mode.
  const isDark = THEME.mode === 'dark';
  const helperColor = isDark ? 'rgba(242,244,248,0.78)' : 'rgba(20,30,50,0.72)';
  return (
    <div style={{ width: '100%' }}>
      {children}
      {hasError ? (
        <div className="ds-fld__error" role="alert">
          <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 14 }}>error</span>
          {errorText}
        </div>
      ) : (helperText && (
        <div className="ds-fld__helper" style={{ color: helperColor }}>{helperText}</div>
      ))}
    </div>
  );
};

/* ── TextField ─────────────────────────────────────────────────────────── */
/* Material outlined floating-label pattern (matches login gate).
 * The label sits inside the field at rest, lifts to the border when the
 * input is focused or filled. CSS-driven via :focus + :not(:placeholder-shown)
 * — that's why we always set placeholder=" " (single space) on the input.
 * The user-supplied "placeholder" prop maps to the label, NOT the HTML
 * placeholder attribute. (Apple HIG / Material both recommend this.)
 */

const TextField = ({
  id, type = 'text', value, defaultValue, onChange,
  label, helperText, errorText, required, disabled, readOnly,
  size = 'md', variant = 'bordered',
  iconLeading, iconTrailing,
  prefix, suffix, // text adornments (e.g., "$", "%", "kg") — for currency, units
  placeholder: _ignoredPlaceholder, // floating label IS the placeholder; no separate placeholder prop
  ...rest
}) => {
  const isDark = THEME.mode === 'dark';
  const fieldId = id || React.useId();
  const wrapperVars = buildFieldVars({ isDark });
  const inputStyle = buildInputStyle({
    size, variant, disabled, isDark,
    hasLeadingIcon: Boolean(iconLeading),
    hasTrailingIcon: Boolean(iconTrailing),
    hasLeadingAdornment: Boolean(prefix),
    hasTrailingAdornment: Boolean(suffix),
  });
  // When an icon OR text adornment sits at the leading edge, push the label
  // right past it AT REST only. At floating state the label is on the top
  // border (above the input area, doesn't collide) so the CSS snaps it back
  // to the standard 14px left for alignment across all fields in the form.
  if (iconLeading || prefix) {
    wrapperVars['--ds-fld-label-left-rest'] = (FIELD_SIZE[size].padX + 22) + 'px';
  }

  return (
    <FieldShell helperText={helperText} errorText={errorText}>
      <div className={`ds-fld${variant === 'transparent' ? ' ds-fld--transparent' : ''}`} style={wrapperVars}>
        {iconLeading && (
          <span className="ds-fld__icon-leading material-symbols-rounded" aria-hidden="true">{iconLeading}</span>
        )}
        {prefix && (
          <span className="ds-fld__adornment ds-fld__adornment--leading" aria-hidden="true">{prefix}</span>
        )}
        <input
          className="ds-fld__input"
          id={fieldId}
          type={type}
          placeholder=" "
          {...(value !== undefined ? { value, onChange } : { defaultValue })}
          required={required}
          disabled={disabled}
          readOnly={readOnly}
          aria-invalid={Boolean(errorText)}
          style={inputStyle}
          {...rest}
        />
        {label && variant !== 'transparent' && (
          <label className="ds-fld__label" htmlFor={fieldId}>
            {label}
            {required && <span aria-hidden="true" className="ds-fld__required">*</span>}
          </label>
        )}
        {suffix && (
          <span className="ds-fld__adornment ds-fld__adornment--trailing" aria-hidden="true">{suffix}</span>
        )}
        {iconTrailing && (
          <span className="ds-fld__chevron material-symbols-rounded" aria-hidden="true" style={{ right: 12 }}>{iconTrailing}</span>
        )}
      </div>
    </FieldShell>
  );
};

/* ── Textarea ──────────────────────────────────────────────────────────── */

const Textarea = ({
  id, value, defaultValue, onChange,
  label, helperText, errorText, required, disabled, readOnly,
  size = 'md', variant = 'bordered',
  rows = 4, resize = 'vertical',
  placeholder: _ignoredPlaceholder, // floating label IS the placeholder
  ...rest
}) => {
  const isDark = THEME.mode === 'dark';
  const fieldId = id || React.useId();
  const wrapperVars = buildFieldVars({ isDark });
  const taStyle = buildTextareaStyle({ size, variant, disabled, isDark, rows });

  return (
    <FieldShell helperText={helperText} errorText={errorText}>
      <div className={`ds-fld ds-fld--textarea${variant === 'transparent' ? ' ds-fld--transparent' : ''}`} style={wrapperVars}>
        <textarea
          className="ds-fld__input"
          id={fieldId}
          placeholder=" "
          {...(value !== undefined ? { value, onChange } : { defaultValue })}
          required={required}
          disabled={disabled}
          readOnly={readOnly}
          rows={rows}
          aria-invalid={Boolean(errorText)}
          style={taStyle}
          {...rest}
        />
        {label && variant !== 'transparent' && (
          <label className="ds-fld__label" htmlFor={fieldId}>
            {label}
            {required && <span aria-hidden="true" className="ds-fld__required">*</span>}
          </label>
        )}
      </div>
    </FieldShell>
  );
};

/* ── Select (custom — NOT native) ──────────────────────────────────────── */
/* Custom button-trigger + flyout menu so the expanded dropdown matches the
 * design system instead of using the browser's system picker. The trigger
 * looks identical to a TextField (floating label, AA-strong border, chevron).
 * The menu is a styled glass card with hover/keyboard-focused/selected
 * states. A hidden <input type="hidden"> mirrors the value for form posting.
 *
 * Keyboard:
 *   - Tab to focus, Enter/Space/ArrowDown opens
 *   - Open: ArrowUp/Down navigates, Enter selects, Esc closes
 * ARIA: button[aria-haspopup="listbox"][aria-expanded] + role="listbox" +
 * role="option"[aria-selected].
 */

const Select = ({
  id, name, value, defaultValue, onChange,
  label, helperText, errorText, required, disabled,
  size = 'md', variant = 'bordered',
  options = [], placeholder,
  // searchable -> combobox: the trigger becomes a typeable text input that
  // filters the option list as you type. Identical flyout styling; scales to
  // ~50 options (think 50 states). Plain (non-searchable) Select keeps the
  // button trigger + listbox behavior below.
  searchable = false,
  // Text shown in the list when a search yields nothing.
  noResultsText = 'No matches',
  // Optional mode prop — when set, re-applies setThemeMode at top of every
  // render. Needed when Select renders alongside other instances in the
  // OPPOSITE mode (e.g. dark+light side-by-side on the design canvas) AND
  // user interactions trigger re-renders. Production usage typically omits.
  mode,
}) => {
  if (mode && typeof setThemeMode === 'function') setThemeMode(mode);
  const isDark = THEME.mode === 'dark';
  const fieldId = id || React.useId();
  const listboxId = fieldId + '-listbox';
  const wrapperVars = buildFieldVars({ isDark });

  const [isOpen, setIsOpen] = React.useState(false);
  const [internalValue, setInternalValue] = React.useState(defaultValue !== undefined ? defaultValue : '');
  const [focusIdx, setFocusIdx] = React.useState(-1);
  // Combobox-only: the current text typed into the input.
  const [query, setQuery] = React.useState('');
  const currentValue = value !== undefined ? value : internalValue;

  const optionsNorm = options.map(o => typeof o === 'string' ? { value: o, label: o } : o);
  const selectedOpt = optionsNorm.find(o => o.value === currentValue);
  const triggerLabel = selectedOpt ? selectedOpt.label : '';

  // Combobox filter: case-insensitive substring on label. When closed, or no
  // query, the full list shows. The plain Select always uses the full list.
  const filtered = (searchable && isOpen && query.trim())
    ? optionsNorm.filter(o => o.label.toLowerCase().includes(query.trim().toLowerCase()))
    : optionsNorm;

  const triggerRef = React.useRef(null);
  const listRef = React.useRef(null);

  React.useEffect(() => {
    if (!isOpen) return;
    const onClick = (e) => {
      if (triggerRef.current && triggerRef.current.contains(e.target)) return;
      if (listRef.current && listRef.current.contains(e.target)) return;
      closeMenu();
    };
    document.addEventListener('mousedown', onClick);
    return () => document.removeEventListener('mousedown', onClick);
  }, [isOpen]);

  function closeMenu() {
    setIsOpen(false);
    // Combobox: drop any stray query, restore the selected label.
    if (searchable) setQuery(triggerLabel);
  }

  const choose = (opt) => {
    if (opt.disabled) return;
    if (value === undefined) setInternalValue(opt.value);
    if (onChange) onChange(opt.value);
    setIsOpen(false);
    if (searchable) setQuery(opt.label);
    if (triggerRef.current) triggerRef.current.focus();
  };

  // Plain (button) trigger keyboard.
  const onTriggerKey = (e) => {
    if (disabled) return;
    if (!isOpen) {
      if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
        e.preventDefault();
        setIsOpen(true);
        const idx = optionsNorm.findIndex(o => o.value === currentValue);
        setFocusIdx(idx >= 0 ? idx : 0);
      }
    } else {
      if (e.key === 'Escape') {
        e.preventDefault();
        closeMenu();
      } else if (e.key === 'ArrowDown') {
        e.preventDefault();
        setFocusIdx(i => Math.min(i + 1, optionsNorm.length - 1));
      } else if (e.key === 'ArrowUp') {
        e.preventDefault();
        setFocusIdx(i => Math.max(i - 1, 0));
      } else if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        if (focusIdx >= 0) choose(optionsNorm[focusIdx]);
      } else if (e.key === 'Home') {
        e.preventDefault();
        setFocusIdx(0);
      } else if (e.key === 'End') {
        e.preventDefault();
        setFocusIdx(optionsNorm.length - 1);
      }
    }
  };

  // Combobox (typeable input) keyboard. Operates over the FILTERED list.
  const onComboKey = (e) => {
    if (disabled) return;
    if (!isOpen) {
      if (e.key === 'ArrowDown' || e.key === 'Enter') {
        e.preventDefault();
        setIsOpen(true);
        const idx = filtered.findIndex(o => o.value === currentValue);
        setFocusIdx(idx >= 0 ? idx : 0);
      }
      return;
    }
    if (e.key === 'Escape') {
      e.preventDefault();
      closeMenu();
    } else if (e.key === 'ArrowDown') {
      e.preventDefault();
      setFocusIdx(i => Math.min(i + 1, filtered.length - 1));
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      setFocusIdx(i => Math.max(i - 1, 0));
    } else if (e.key === 'Enter') {
      e.preventDefault();
      if (focusIdx >= 0 && filtered[focusIdx]) choose(filtered[focusIdx]);
    } else if (e.key === 'Home') {
      e.preventDefault();
      setFocusIdx(0);
    } else if (e.key === 'End') {
      e.preventDefault();
      setFocusIdx(filtered.length - 1);
    }
  };

  const onComboChange = (e) => {
    setQuery(e.target.value);
    if (!isOpen) setIsOpen(true);
    setFocusIdx(0); // first match becomes active for Enter-to-pick
  };

  // Trigger surface — same shape as a TextField. Use buildInputStyle for the
  // surface, override display so triggerLabel sits on the left.
  const baseInput = buildInputStyle({ size, variant, disabled, isDark, hasTrailingIcon: true });
  const triggerStyle = {
    ...baseInput,
    display: 'flex', alignItems: 'center',
    textAlign: 'left',
    cursor: disabled ? 'not-allowed' : 'pointer',
    color: triggerLabel ? THEME.text : 'transparent', // hidden when empty so floating label sits clean
    fontFamily: 'inherit',
  };
  // Combobox input shares the exact field surface; its text is always visible.
  const comboInputStyle = {
    ...baseInput,
    cursor: disabled ? 'not-allowed' : 'text',
    color: THEME.text,
    fontFamily: 'inherit',
  };

  const periwinkle = isDark ? '#9FB3FF' : '#4E6DA8';
  // Float the label whenever there's a value, the menu is open, or (combobox)
  // the user has typed something.
  const labelFloated = Boolean(triggerLabel) || isOpen || (searchable && Boolean(query));
  const activeId = (isOpen && focusIdx >= 0 && filtered[focusIdx]) ? (listboxId + '-opt-' + focusIdx) : undefined;

  return (
    <FieldShell helperText={helperText} errorText={errorText}>
      <div className={`ds-fld ds-fld--select${labelFloated ? '' : ' ds-fld--select-empty'}${errorText ? ' ds-fld--error' : ''}`} style={{ ...wrapperVars, position: 'relative' }}>
        {/* Hidden form field for native form posting. */}
        <input type="hidden" name={name} value={currentValue || ''} />
        {/* Use a real input (display:none) just to satisfy the
            :not(:placeholder-shown) selector when we want the label floated.
            The placeholder=" " trick keeps the label rest by default; we
            promote to floating when triggerLabel is non-empty by setting
            value to a non-empty string. Simpler: drive label-float purely
            via the ds-fld--select class which always floats — that's
            why we use ds-fld--select-empty modifier above for the empty
            state if we want different behavior. For now keep always-floating
            since that matches the existing pattern. */}
        {searchable ? (
          /* Combobox: a real text input that filters the list as you type. */
          <input
            ref={triggerRef}
            id={fieldId}
            type="text"
            role="combobox"
            autoComplete="off"
            spellCheck={false}
            aria-haspopup="listbox"
            aria-expanded={isOpen}
            aria-controls={listboxId}
            aria-autocomplete="list"
            aria-activedescendant={activeId}
            aria-required={required || undefined}
            aria-invalid={Boolean(errorText)}
            aria-labelledby={fieldId + '-label'}
            disabled={disabled}
            value={isOpen ? query : triggerLabel}
            placeholder=" "
            onChange={onComboChange}
            onClick={() => { if (!disabled && !isOpen) { setIsOpen(true); setQuery(''); } }}
            onKeyDown={onComboKey}
            style={comboInputStyle}
          />
        ) : (
          <button
            ref={triggerRef}
            id={fieldId}
            type="button"
            role="combobox"
            aria-haspopup="listbox"
            aria-expanded={isOpen}
            aria-controls={listboxId}
            aria-required={required || undefined}
            aria-invalid={Boolean(errorText)}
            aria-labelledby={fieldId + '-label'}
            disabled={disabled}
            onClick={() => !disabled && setIsOpen(o => !o)}
            onKeyDown={onTriggerKey}
            style={triggerStyle}
          >
            {triggerLabel || (placeholder ? <span style={{ color: 'var(--ds-fld-label-rest)' }}>{placeholder}</span> : ' ')}
          </button>
        )}
        {label && (
          <label id={fieldId + '-label'} className="ds-fld__label" htmlFor={fieldId}>
            {label}
            {required && <span aria-hidden="true" className="ds-fld__required">*</span>}
          </label>
        )}
        <span className="ds-fld__chevron material-symbols-rounded" aria-hidden="true" style={{ transition: 'transform 200ms ease', transform: isOpen ? 'translateY(-50%) rotate(180deg)' : 'translateY(-50%)' }}>{searchable ? 'search' : 'unfold_more'}</span>

        {isOpen && (
          <div
            ref={listRef}
            id={listboxId}
            role="listbox"
            aria-labelledby={fieldId + '-label'}
            tabIndex={-1}
            style={{
              position: 'absolute', top: 'calc(100% + 6px)', left: 0, right: 0,
              maxHeight: 280, overflowY: 'auto',
              background: isDark ? 'rgba(31,40,49,0.96)' : 'rgba(252,248,238,0.96)',
              backdropFilter: 'blur(28px) saturate(1.6)',
              WebkitBackdropFilter: 'blur(28px) saturate(1.6)',
              border: `1px solid ${isDark ? 'rgba(255,255,255,0.16)' : 'rgba(20,30,50,0.12)'}`,
              borderRadius: 10,
              boxShadow: isDark
                ? '0 12px 32px rgba(0,0,0,0.55), 0 4px 8px rgba(0,0,0,0.30)'
                : '0 12px 32px rgba(20,30,50,0.18), 0 4px 8px rgba(20,30,50,0.10)',
              padding: 6,
              zIndex: 50,
            }}
          >
            {filtered.length === 0 && (
              <div style={{ padding: '10px 12px', fontSize: 13, color: isDark ? 'rgba(242,244,248,0.55)' : 'rgba(20,30,50,0.55)' }}>
                {noResultsText}
              </div>
            )}
            {filtered.map((opt, idx) => {
              const isSelected = opt.value === currentValue;
              const isFocused = idx === focusIdx;
              return (
                <div
                  key={opt.value}
                  id={listboxId + '-opt-' + idx}
                  role="option"
                  aria-selected={isSelected}
                  aria-disabled={opt.disabled || undefined}
                  onMouseEnter={() => setFocusIdx(idx)}
                  onClick={() => choose(opt)}
                  style={{
                    display: 'flex', alignItems: 'center', gap: 10,
                    padding: '10px 12px', borderRadius: 7,
                    fontSize: 13.5, color: opt.disabled ? (isDark ? 'rgba(242,244,248,0.40)' : 'rgba(20,30,50,0.40)') : THEME.text,
                    background: isFocused
                      ? (isDark ? 'rgba(159,179,255,0.16)' : 'rgba(78,109,168,0.12)')
                      : 'transparent',
                    cursor: opt.disabled ? 'not-allowed' : 'pointer',
                    transition: 'background 0.10s ease',
                  }}
                >
                  <span style={{ flex: 1 }}>{searchable && query.trim() ? highlightMatch(opt.label, query.trim(), periwinkle) : opt.label}</span>
                  {isSelected && (
                    <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 18, color: periwinkle, fontVariationSettings: "'FILL' 1, 'wght' 600" }}>check</span>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </div>
    </FieldShell>
  );
};

/* Highlight the matched substring in a combobox option label. Returns a React
 * fragment with the match wrapped in a periwinkle-weighted span. */
function highlightMatch(label, q, accent) {
  const lower = label.toLowerCase();
  const idx = lower.indexOf(q.toLowerCase());
  if (idx < 0) return label;
  const before = label.slice(0, idx);
  const match = label.slice(idx, idx + q.length);
  const after = label.slice(idx + q.length);
  return (
    <span>
      {before}
      <span style={{ color: accent, fontWeight: 700 }}>{match}</span>
      {after}
    </span>
  );
}

/* ── Popover surface helper (shared by DatePicker + searchable Select) ──── */
/* Returns the inline style for an elevated glass popover that mirrors the
 * canonical Modal (radius 18, periwinkle-tinted border, lift shadow) AND the
 * Select flyout (blur 28px saturate 1.6). Date calendar uses radius 16 (the
 * 14-16 band the brief calls for); the listbox flyout keeps its own radius.
 * Animation honors prefers-reduced-motion via the ds-pop-anim CSS class. */
function buildPopoverStyle({ isDark, radius = 16 }) {
  return {
    background: isDark
      ? 'linear-gradient(180deg, rgba(36,46,68,0.97), rgba(26,34,52,0.97))'
      : 'linear-gradient(180deg, rgba(255,253,247,0.98), rgba(248,243,231,0.98))',
    backdropFilter: 'blur(28px) saturate(1.6)',
    WebkitBackdropFilter: 'blur(28px) saturate(1.6)',
    border: `1px solid ${isDark ? 'rgba(159,179,255,0.18)' : 'rgba(105,134,191,0.22)'}`,
    borderRadius: radius,
    boxShadow: isDark
      ? '0 28px 70px rgba(0,0,0,0.60), 0 4px 12px rgba(0,0,0,0.35), inset 0 1px 0 rgba(159,179,255,0.10)'
      : '0 28px 70px rgba(20,30,50,0.22), 0 4px 12px rgba(20,30,50,0.10)',
  };
}

/* One-time popover animation CSS — modalLift, gated by prefers-reduced-motion. */
(function ensurePopoverCss() {
  if (document.getElementById('ds-pop-css')) return;
  const css = `
    @keyframes dsPopLift {
      from { opacity: 0; transform: translateY(8px) scale(0.98); }
      to   { opacity: 1; transform: translateY(0) scale(1); }
    }
    .ds-pop-anim { animation: dsPopLift 0.28s cubic-bezier(0.22, 1, 0.36, 1); transform-origin: top left; }
    @media (prefers-reduced-motion: reduce) {
      .ds-pop-anim { animation: none; }
    }
  `;
  const style = document.createElement('style');
  style.id = 'ds-pop-css';
  style.textContent = css;
  document.head.appendChild(style);
})();

/* ── DatePicker (custom — replaces native <input type=date>) ───────────── */
/* Native date inputs cannot be themed (the calendar popup is browser chrome
 * and the indicator icon is black-on-dark). This renders the SAME floating-
 * label field shell as TextField (with a leading "event" icon in the theme
 * color) as the trigger, and a custom calendar popover styled like our Modal.
 *
 * Trigger: button shaped exactly like a TextField (buildInputStyle). Shows the
 * formatted selected date ("Jun 7, 2026") or stays empty so the floating label
 * sits at rest.
 *
 * Popover: role="dialog", glass/elevated surface (buildPopoverStyle), month +
 * year header, weekday row, 6x7 day grid. Selected day = periwinkle fill;
 * today = subtle ring (not the loud native blue+orange).
 *
 * Keyboard (a11y): trigger Enter/Space/ArrowDown opens. In the grid: arrows
 * move by day/week, Home/End jump to week edges, PageUp/PageDown change month,
 * Enter selects, Esc closes. Click-outside closes. Focus returns to trigger on
 * close. Honors prefers-reduced-motion via .ds-pop-anim.
 *
 * Month navigation respects the no-arrows-in-buttons rule: the month+year is a
 * clickable label (toggles a year/month grid). Directional month-stepping is a
 * DOCUMENTED exception (conventional calendar nav, not a CTA) — logged in
 * _bmad/memory/_shared/DESIGN-CORRECTIONS.md — and uses chevron icons.
 */

const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const MONTHS_SHORT = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const WEEKDAYS = ['Su','Mo','Tu','We','Th','Fr','Sa'];

function fmtDate(d) {
  if (!d) return '';
  return `${MONTHS_SHORT[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
}
function sameDay(a, b) {
  return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
}
function isoDate(d) {
  if (!d) return '';
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${d.getFullYear()}-${m}-${day}`;
}

const DatePicker = ({
  id, name, value, defaultValue, onChange,
  label, helperText, errorText, required, disabled,
  size = 'md', variant = 'bordered',
  placeholder,
  // Optional mode prop — see Select. Re-applies setThemeMode when this instance
  // renders alongside an opposite-mode instance (design canvas) and a re-render
  // is triggered by interaction. Production usage typically omits.
  mode,
}) => {
  if (mode && typeof setThemeMode === 'function') setThemeMode(mode);
  const isDark = THEME.mode === 'dark';
  const fieldId = id || React.useId();
  const dialogId = fieldId + '-dialog';
  const wrapperVars = buildFieldVars({ isDark });
  // Leading icon at rest → push the rest label past the icon (same as TextField).
  wrapperVars['--ds-fld-label-left-rest'] = (FIELD_SIZE[size].padX + 22) + 'px';

  const parseInit = (v) => {
    if (!v) return null;
    if (v instanceof Date) return v;
    // Parse YYYY-MM-DD as a LOCAL date (avoid UTC shift from new Date(str)).
    const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(v));
    if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
    const d = new Date(v);
    return isNaN(d) ? null : d;
  };

  const [isOpen, setIsOpen] = React.useState(false);
  const [internal, setInternal] = React.useState(() => parseInit(defaultValue));
  const selected = value !== undefined ? parseInit(value) : internal;

  const today = new Date();
  const [viewMonth, setViewMonth] = React.useState(() => {
    const base = selected || today;
    return new Date(base.getFullYear(), base.getMonth(), 1);
  });
  // The day the keyboard "cursor" is on while the grid is open.
  const [activeDay, setActiveDay] = React.useState(() => selected || today);
  // Popover view: 'days' = the calendar grid (default), 'years' = a fast year
  // picker opened by clicking the year in the header. activeYear is the year
  // the keyboard cursor sits on inside the year grid.
  const [picker, setPicker] = React.useState('days');
  const [activeYear, setActiveYear] = React.useState(() => (selected || today).getFullYear());

  const triggerRef = React.useRef(null);
  const dialogRef = React.useRef(null);
  const gridRef = React.useRef(null);
  const yearGridRef = React.useRef(null);

  // Year grid: a 12-year block (4 rows x 3 cols) containing the active year.
  // Recomputed from activeYear so paging up/down moves to the prev/next block.
  const YEAR_COLS = 3;
  const YEAR_COUNT = 12;
  const yearBlockStart = Math.floor(activeYear / YEAR_COUNT) * YEAR_COUNT;
  const yearCells = Array.from({ length: YEAR_COUNT }, (_, i) => yearBlockStart + i);

  const periwinkle = isDark ? '#9FB3FF' : '#4E6DA8';
  const periwinkleDeep = isDark ? '#6986BF' : '#4E6DA8';

  // Click-outside + Esc closes.
  React.useEffect(() => {
    if (!isOpen) return;
    const onClick = (e) => {
      if (triggerRef.current && triggerRef.current.contains(e.target)) return;
      if (dialogRef.current && dialogRef.current.contains(e.target)) return;
      close();
    };
    document.addEventListener('mousedown', onClick);
    return () => document.removeEventListener('mousedown', onClick);
  }, [isOpen]);

  // When the grid opens, move DOM focus to the active day so arrow keys work
  // and screen readers announce it. Respect reduced motion = no smooth scroll.
  React.useEffect(() => {
    if (!isOpen || picker !== 'days' || !gridRef.current) return;
    const el = gridRef.current.querySelector('[data-active="true"]');
    if (el) el.focus();
  }, [isOpen, picker]);

  // When the year picker opens, move focus to the active year cell so the
  // arrow keys drive it and SR announces the current year.
  React.useEffect(() => {
    if (!isOpen || picker !== 'years' || !yearGridRef.current) return;
    const el = yearGridRef.current.querySelector('[data-active="true"]');
    if (el) el.focus();
  }, [isOpen, picker]);

  function open() {
    const base = selected || today;
    setViewMonth(new Date(base.getFullYear(), base.getMonth(), 1));
    setActiveDay(base);
    setActiveYear(base.getFullYear());
    setPicker('days');
    setIsOpen(true);
  }
  function close() {
    setIsOpen(false);
    setPicker('days');
    if (triggerRef.current) triggerRef.current.focus();
  }
  function pick(d) {
    if (value === undefined) setInternal(d);
    if (onChange) onChange(isoDate(d), d);
    close();
  }
  function stepMonth(delta) {
    setViewMonth(m => new Date(m.getFullYear(), m.getMonth() + delta, 1));
  }

  // ── Year picker ────────────────────────────────────────────────────────
  // Open the compact year grid from the header. Seed the cursor on the year
  // currently shown so arrows start from a sensible place.
  function openYearPicker() {
    setActiveYear(viewMonth.getFullYear());
    setPicker('years');
  }
  // Pick a year: jump the calendar to that year (keep the same month + the
  // keyboard day cursor's day-of-month, clamped) and return to the day grid.
  function chooseYear(y) {
    const month = viewMonth.getMonth();
    setViewMonth(new Date(y, month, 1));
    setActiveDay(prev => {
      const dim = new Date(y, month + 1, 0).getDate(); // days in target month
      return new Date(y, month, Math.min(prev.getDate(), dim));
    });
    setPicker('days');
  }

  // Keyboard nav inside the year grid. Arrows move by 1 year (L/R) or one row
  // = 3 years (U/D); Home/End jump to block edges; PageUp/PageDown page the
  // 12-year block; Enter picks; Esc returns to the day grid.
  const onYearGridKey = (e) => {
    let y = activeYear;
    let handled = true;
    switch (e.key) {
      case 'ArrowLeft':  y -= 1; break;
      case 'ArrowRight': y += 1; break;
      case 'ArrowUp':    y -= YEAR_COLS; break;
      case 'ArrowDown':  y += YEAR_COLS; break;
      case 'Home':       y = yearBlockStart; break;
      case 'End':        y = yearBlockStart + YEAR_COUNT - 1; break;
      case 'PageUp':     y -= YEAR_COUNT; break;
      case 'PageDown':   y += YEAR_COUNT; break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        chooseYear(activeYear);
        return;
      case 'Escape':
        e.preventDefault();
        setPicker('days');
        return;
      default:
        handled = false;
    }
    if (!handled) return;
    e.preventDefault();
    setActiveYear(y);
    requestAnimationFrame(() => {
      if (yearGridRef.current) {
        const el = yearGridRef.current.querySelector('[data-active="true"]');
        if (el) el.focus();
      }
    });
  };

  const onTriggerKey = (e) => {
    if (disabled) return;
    if (!isOpen && (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown')) {
      e.preventDefault();
      open();
    }
  };

  // Keyboard nav inside the day grid.
  const onGridKey = (e) => {
    let next = new Date(activeDay);
    let handled = true;
    switch (e.key) {
      case 'ArrowLeft':  next.setDate(next.getDate() - 1); break;
      case 'ArrowRight': next.setDate(next.getDate() + 1); break;
      case 'ArrowUp':    next.setDate(next.getDate() - 7); break;
      case 'ArrowDown':  next.setDate(next.getDate() + 7); break;
      case 'Home':       next.setDate(next.getDate() - next.getDay()); break;
      case 'End':        next.setDate(next.getDate() + (6 - next.getDay())); break;
      case 'PageUp':     next = new Date(next.getFullYear(), next.getMonth() - 1, next.getDate()); break;
      case 'PageDown':   next = new Date(next.getFullYear(), next.getMonth() + 1, next.getDate()); break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        pick(activeDay);
        return;
      case 'Escape':
        e.preventDefault();
        close();
        return;
      default:
        handled = false;
    }
    if (!handled) return;
    e.preventDefault();
    setActiveDay(next);
    if (next.getMonth() !== viewMonth.getMonth() || next.getFullYear() !== viewMonth.getFullYear()) {
      setViewMonth(new Date(next.getFullYear(), next.getMonth(), 1));
    }
    // Move DOM focus to the newly active cell after the re-render.
    requestAnimationFrame(() => {
      if (gridRef.current) {
        const el = gridRef.current.querySelector('[data-active="true"]');
        if (el) el.focus();
      }
    });
  };

  // Build the 6x7 day matrix for viewMonth (leading/trailing days from
  // adjacent months, dimmed and non-selectable focus targets).
  const firstOfMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth(), 1);
  const startOffset = firstOfMonth.getDay();
  const gridStart = new Date(firstOfMonth);
  gridStart.setDate(1 - startOffset);
  const cells = [];
  for (let i = 0; i < 42; i++) {
    const d = new Date(gridStart);
    d.setDate(gridStart.getDate() + i);
    cells.push(d);
  }

  const baseInput = buildInputStyle({ size, variant, disabled, isDark, hasLeadingIcon: true, hasTrailingIcon: true });
  const triggerStyle = {
    ...baseInput,
    display: 'flex', alignItems: 'center', textAlign: 'left',
    cursor: disabled ? 'not-allowed' : 'pointer',
    color: selected ? THEME.text : 'transparent',
    fontFamily: 'inherit',
  };

  const navBtnStyle = {
    width: 30, height: 30, borderRadius: 9,
    display: 'flex', alignItems: 'center', justifyContent: 'center',
    background: 'transparent',
    border: `1px solid ${isDark ? 'rgba(255,255,255,0.12)' : 'rgba(42,54,64,0.16)'}`,
    color: isDark ? 'rgba(242,244,248,0.78)' : 'rgba(26,35,44,0.78)',
    cursor: 'pointer', fontFamily: 'inherit',
    transition: 'background 0.15s ease, color 0.15s ease',
  };

  return (
    <FieldShell helperText={helperText} errorText={errorText}>
      <div className={`ds-fld ds-fld--select${selected || isOpen ? '' : ' ds-fld--select-empty'}${errorText ? ' ds-fld--error' : ''}`} style={{ ...wrapperVars, position: 'relative' }}>
        {/* Hidden form field — posts ISO date. */}
        <input type="hidden" name={name} value={selected ? isoDate(selected) : ''} />
        <span className="ds-fld__icon-leading material-symbols-rounded" aria-hidden="true" style={{ color: periwinkle }}>event</span>
        <button
          ref={triggerRef}
          id={fieldId}
          type="button"
          aria-haspopup="dialog"
          aria-expanded={isOpen}
          aria-controls={isOpen ? dialogId : undefined}
          aria-required={required || undefined}
          aria-invalid={Boolean(errorText)}
          aria-labelledby={fieldId + '-label'}
          disabled={disabled}
          onClick={() => !disabled && (isOpen ? close() : open())}
          onKeyDown={onTriggerKey}
          style={triggerStyle}
        >
          {selected ? fmtDate(selected) : (placeholder ? <span style={{ color: 'var(--ds-fld-label-rest)' }}>{placeholder}</span> : ' ')}
        </button>
        {label && (
          <label id={fieldId + '-label'} className="ds-fld__label" htmlFor={fieldId}>
            {label}
            {required && <span aria-hidden="true" className="ds-fld__required">*</span>}
          </label>
        )}

        {isOpen && (
          <div
            ref={dialogRef}
            id={dialogId}
            role="dialog"
            aria-modal="false"
            aria-label={picker === 'years'
              ? `Choose year, ${yearBlockStart} to ${yearBlockStart + YEAR_COUNT - 1}`
              : `Choose ${label || 'date'}, ${MONTHS[viewMonth.getMonth()]} ${viewMonth.getFullYear()}`}
            className="ds-pop-anim"
            style={{
              ...buildPopoverStyle({ isDark, radius: 16 }),
              position: 'absolute', top: 'calc(100% + 8px)', left: 0,
              width: 296, padding: 16, zIndex: 60,
            }}
          >
            {/* Header. In DAY view: chevron prev · clickable month+year heading
                (the YEAR is a button that opens the fast year picker) · chevron
                next. In YEAR view: the heading shows the visible year range and
                the chevrons page the 12-year block. Chevrons are the documented
                calendar-nav exception; the heading is a real button, not a CTA. */}
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
              <button
                type="button"
                aria-label={picker === 'years' ? 'Previous years' : 'Previous month'}
                onClick={() => picker === 'years' ? setActiveYear(y => y - YEAR_COUNT) : stepMonth(-1)}
                style={navBtnStyle}
              >
                <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 18 }}>chevron_left</span>
              </button>
              {picker === 'years' ? (
                <button
                  type="button"
                  aria-label="Back to days"
                  onClick={() => setPicker('days')}
                  style={{
                    background: 'transparent', border: 'none', cursor: 'pointer',
                    fontFamily: "'Playfair Display', Georgia, serif",
                    fontSize: 16, fontWeight: 500, letterSpacing: '-0.2px',
                    color: THEME.text, padding: '2px 8px', borderRadius: 8,
                  }}
                >
                  {yearBlockStart} – {yearBlockStart + YEAR_COUNT - 1}
                </button>
              ) : (
                <div
                  aria-live="polite"
                  style={{
                    fontFamily: "'Playfair Display', Georgia, serif",
                    fontSize: 16, fontWeight: 500, letterSpacing: '-0.2px',
                    color: THEME.text, display: 'inline-flex', alignItems: 'baseline', gap: 6,
                  }}
                >
                  <span>{MONTHS[viewMonth.getMonth()]}</span>
                  <button
                    type="button"
                    aria-label={`Change year, currently ${viewMonth.getFullYear()}`}
                    title="Switch year"
                    onClick={openYearPicker}
                    onMouseEnter={(e) => { e.currentTarget.style.background = isDark ? 'rgba(159,179,255,0.16)' : 'rgba(78,109,168,0.12)'; }}
                    onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
                    style={{
                      background: 'transparent', border: 'none', cursor: 'pointer',
                      fontFamily: "'Playfair Display', Georgia, serif",
                      fontSize: 16, fontWeight: 600, letterSpacing: '-0.2px',
                      color: periwinkle, padding: '2px 7px', borderRadius: 7,
                      transition: 'background 0.15s ease',
                    }}
                  >
                    {viewMonth.getFullYear()}
                  </button>
                </div>
              )}
              <button
                type="button"
                aria-label={picker === 'years' ? 'Next years' : 'Next month'}
                onClick={() => picker === 'years' ? setActiveYear(y => y + YEAR_COUNT) : stepMonth(1)}
                style={navBtnStyle}
              >
                <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 18 }}>chevron_right</span>
              </button>
            </div>

            {/* ── YEAR PICKER (fast year switching) ──────────────────────── */}
            {picker === 'years' && (
              <div
                ref={yearGridRef}
                role="grid"
                aria-label="Choose year"
                onKeyDown={onYearGridKey}
                style={{ display: 'grid', gridTemplateColumns: `repeat(${YEAR_COLS}, 1fr)`, gap: 6, padding: '2px 0' }}
              >
                {yearCells.map((y) => {
                  const isSelYear = selected && selected.getFullYear() === y;
                  const isCurYear = y === viewMonth.getFullYear();
                  const isTodayYear = y === today.getFullYear();
                  const isActive = y === activeYear;
                  const fillSel = isSelYear || (!selected && isCurYear);
                  return (
                    <button
                      key={y}
                      type="button"
                      role="gridcell"
                      aria-selected={fillSel}
                      aria-current={isTodayYear ? 'date' : undefined}
                      data-active={isActive}
                      tabIndex={isActive ? 0 : -1}
                      onClick={() => chooseYear(y)}
                      onFocus={() => setActiveYear(y)}
                      style={{
                        height: 46, width: '100%',
                        display: 'flex', alignItems: 'center', justifyContent: 'center',
                        borderRadius: 9, fontSize: 13.5, fontFamily: 'inherit',
                        fontWeight: fillSel ? 700 : (isTodayYear ? 600 : 400),
                        cursor: 'pointer',
                        border: isTodayYear && !fillSel
                          ? `1px solid ${isDark ? 'rgba(159,179,255,0.55)' : 'rgba(78,109,168,0.55)'}`
                          : '1px solid transparent',
                        background: fillSel ? periwinkleDeep : 'transparent',
                        color: fillSel ? '#FFFFFF' : THEME.text,
                        transition: 'background 0.12s ease, color 0.12s ease',
                        outlineOffset: 2,
                      }}
                      onMouseEnter={(e) => { if (!fillSel) e.currentTarget.style.background = isDark ? 'rgba(159,179,255,0.14)' : 'rgba(78,109,168,0.12)'; }}
                      onMouseLeave={(e) => { if (!fillSel) e.currentTarget.style.background = 'transparent'; }}
                    >
                      {y}
                    </button>
                  );
                })}
              </div>
            )}

            {/* Weekday row. */}
            {picker === 'days' && (
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', marginBottom: 4 }} aria-hidden="true">
              {WEEKDAYS.map(w => (
                <div key={w} style={{
                  textAlign: 'center', fontSize: 10.5, fontWeight: 700, letterSpacing: '0.4px',
                  textTransform: 'uppercase',
                  color: isDark ? 'rgba(242,244,248,0.50)' : 'rgba(26,35,44,0.52)',
                  padding: '4px 0',
                }}>{w}</div>
              ))}
            </div>
            )}

            {/* Day grid. */}
            {picker === 'days' && (
            <div
              ref={gridRef}
              role="grid"
              aria-label={`${MONTHS[viewMonth.getMonth()]} ${viewMonth.getFullYear()}`}
              onKeyDown={onGridKey}
              style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 2 }}
            >
              {cells.map((d) => {
                const inMonth = d.getMonth() === viewMonth.getMonth();
                const isSel = sameDay(d, selected);
                const isToday = sameDay(d, today);
                const isActive = sameDay(d, activeDay);
                return (
                  <button
                    key={isoDate(d)}
                    type="button"
                    role="gridcell"
                    aria-selected={isSel}
                    aria-current={isToday ? 'date' : undefined}
                    aria-label={`${MONTHS[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`}
                    data-active={isActive}
                    tabIndex={isActive ? 0 : -1}
                    onClick={() => pick(d)}
                    onFocus={() => setActiveDay(d)}
                    style={{
                      height: 34, width: '100%',
                      display: 'flex', alignItems: 'center', justifyContent: 'center',
                      borderRadius: 9,
                      fontSize: 13, fontFamily: 'inherit',
                      fontWeight: isSel ? 700 : (isToday ? 600 : 400),
                      cursor: 'pointer',
                      border: isToday && !isSel
                        ? `1px solid ${isDark ? 'rgba(159,179,255,0.55)' : 'rgba(78,109,168,0.55)'}`
                        : '1px solid transparent',
                      background: isSel ? periwinkleDeep : 'transparent',
                      color: isSel
                        ? '#FFFFFF'
                        : inMonth
                          ? THEME.text
                          : (isDark ? 'rgba(242,244,248,0.30)' : 'rgba(26,35,44,0.32)'),
                      transition: 'background 0.12s ease, color 0.12s ease',
                      outlineOffset: 2,
                    }}
                    onMouseEnter={(e) => { if (!isSel) e.currentTarget.style.background = isDark ? 'rgba(159,179,255,0.14)' : 'rgba(78,109,168,0.12)'; }}
                    onMouseLeave={(e) => { if (!isSel) e.currentTarget.style.background = 'transparent'; }}
                  >
                    {d.getDate()}
                  </button>
                );
              })}
            </div>
            )}

            {/* Footer: Today shortcut. No arrow, label <= 4 words. */}
            <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 12 }}>
              <button
                type="button"
                onClick={() => { setViewMonth(new Date(today.getFullYear(), today.getMonth(), 1)); setActiveDay(today); pick(today); }}
                style={{
                  background: 'transparent', border: 'none', cursor: 'pointer',
                  fontFamily: 'inherit', fontSize: 12.5, fontWeight: 600,
                  color: periwinkle, padding: '4px 6px', borderRadius: 7,
                }}
              >
                Today
              </button>
            </div>
          </div>
        )}
      </div>
    </FieldShell>
  );
};

/* ── Checkbox ──────────────────────────────────────────────────────────── */

const Checkbox = ({
  id, checked, defaultChecked, onChange, indeterminate,
  label, helperText, errorText, disabled, required,
  ...rest
}) => {
  const isDark = THEME.mode === 'dark';
  const inputRef = React.useRef(null);
  const fieldId = id || React.useId();
  const periwinkle = isDark ? '#9FB3FF' : '#4E6DA8';
  const danger = '#C24A38';

  React.useEffect(() => {
    if (inputRef.current) inputRef.current.indeterminate = Boolean(indeterminate);
  }, [indeterminate]);

  const isChecked = checked || indeterminate;
  // AA-strong border (3:1 UI contrast). THEME.line is 0.08-0.10 opacity which
  // fails 3:1 hard. Using 0.40 dark / 0.40 light matches the form-field
  // border tokens.
  const checkboxBorder = isDark ? 'rgba(255,255,255,0.40)' : 'rgba(20,30,50,0.40)';

  return (
    <div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, opacity: disabled ? 0.45 : 1 }}>
      <label htmlFor={fieldId} style={{ position: 'relative', display: 'inline-flex', alignItems: 'center', cursor: disabled ? 'not-allowed' : 'pointer', flexShrink: 0, marginTop: 1 }}>
        <input
          ref={inputRef}
          id={fieldId}
          type="checkbox"
          {...(checked !== undefined ? { checked, onChange } : { defaultChecked })}
          disabled={disabled}
          required={required}
          aria-invalid={Boolean(errorText)}
          style={{ position: 'absolute', opacity: 0, width: 24, height: 24, margin: 0, cursor: disabled ? 'not-allowed' : 'pointer' }}
          {...rest}
        />
        <span aria-hidden="true" style={{
          width: 20, height: 20, borderRadius: 4,
          background: isChecked
            ? periwinkle
            : (isDark ? 'rgba(255,255,255,0.025)' : 'rgba(255,255,255,0.65)'),
          border: `1.5px solid ${isChecked ? periwinkle : (errorText ? danger : checkboxBorder)}`,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          transition: 'background 0.15s ease, border-color 0.15s ease',
        }}>
          {isChecked && (
            <span className="material-symbols-rounded" style={{
              fontSize: 16, color: '#D7DEE2', fontVariationSettings: "'FILL' 1, 'wght' 600",
            }}>{indeterminate ? 'remove' : 'check'}</span>
          )}
        </span>
      </label>
      {label && (
        <label htmlFor={fieldId} style={{
          fontSize: 13, color: THEME.text, lineHeight: 1.5,
          cursor: disabled ? 'not-allowed' : 'pointer',
        }}>
          {label}
          {required && <span aria-hidden="true" style={{ color: danger, marginLeft: 4 }}>*</span>}
          {helperText && !errorText && (
            <div style={{ fontSize: 11.5, color: THEME.textDim, marginTop: 2, fontWeight: 400 }}>{helperText}</div>
          )}
          {errorText && (
            <div role="alert" style={{ fontSize: 11.5, color: danger, marginTop: 2, fontWeight: 500 }}>{errorText}</div>
          )}
        </label>
      )}
    </div>
  );
};

/* ── Switch ────────────────────────────────────────────────────────────── */

const Switch = ({
  id, checked, defaultChecked, onChange,
  label, helperText, disabled, srOnlyLabel,
  ...rest
}) => {
  const isDark = THEME.mode === 'dark';
  const fieldId = id || React.useId();
  // Track on/off internally when used uncontrolled (defaultChecked) so the
  // track visibly flips. Controlled use (checked prop) still wins.
  const isControlled = checked !== undefined;
  const [internalOn, setInternalOn] = React.useState(Boolean(defaultChecked));
  const isOn = isControlled ? checked : internalOn;
  const handleChange = (e) => {
    if (!isControlled) setInternalOn(e.target.checked);
    if (onChange) onChange(e);
  };
  // On = green track (success/active); off = neutral track.
  const greenOn = isDark ? '#56C28A' : '#2E875A';
  const trackOff = isDark ? 'rgba(255,255,255,0.10)' : 'rgba(20,30,50,0.20)';

  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 12, opacity: disabled ? 0.45 : 1 }}>
      <label htmlFor={fieldId} style={{ position: 'relative', display: 'inline-flex', cursor: disabled ? 'not-allowed' : 'pointer', flexShrink: 0 }}>
        <input
          id={fieldId}
          type="checkbox"
          role="switch"
          checked={isOn}
          onChange={handleChange}
          disabled={disabled}
          style={{ position: 'absolute', opacity: 0, width: 44, height: 20, margin: 0, cursor: disabled ? 'not-allowed' : 'pointer' }}
          {...rest}
        />
        <span aria-hidden="true" style={{
          width: 44, height: 20, borderRadius: 999,
          background: isOn ? greenOn : trackOff,
          border: `1px solid ${isOn ? greenOn : THEME.line}`,
          position: 'relative',
          transition: 'background 0.18s ease',
        }}>
          <span style={{
            position: 'absolute', top: 3, left: isOn ? 26 : 3,
            width: 14, height: 14, borderRadius: '50%',
            background: '#FFFFFF',
            boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
            transition: 'left 0.18s cubic-bezier(0.22, 1, 0.36, 1)',
          }}/>
        </span>
      </label>
      {label && !srOnlyLabel && (
        <label htmlFor={fieldId} style={{ fontSize: 13, color: THEME.text, lineHeight: 1.5, cursor: disabled ? 'not-allowed' : 'pointer' }}>
          {label}
          {helperText && (
            <div style={{ fontSize: 11.5, color: THEME.textDim, marginTop: 2 }}>{helperText}</div>
          )}
        </label>
      )}
    </div>
  );
};

/* ── RadioGroup + Radio ────────────────────────────────────────────────── */

const RadioGroup = ({
  name, value, onChange,
  label, helperText, errorText, required,
  options = [],
  orientation = 'vertical',
  disabled,
}) => {
  const isDark = THEME.mode === 'dark';
  const groupId = React.useId();
  const periwinkle = isDark ? '#9FB3FF' : '#4E6DA8';
  const danger = '#C24A38';
  // AA-strong border (3:1 UI contrast). Same token as form-field border.
  const radioBorder = isDark ? 'rgba(255,255,255,0.40)' : 'rgba(20,30,50,0.40)';

  return (
    <fieldset style={{ border: 0, padding: 0, margin: 0, width: '100%', display: 'flex', flexDirection: 'column', gap: 8 }}>
      {label && (
        <legend style={{
          fontSize: 11, color: THEME.textHeader, letterSpacing: 1,
          textTransform: 'uppercase', fontWeight: 600, padding: 0, marginBottom: 4,
        }}>
          {label}
          {required && <span aria-hidden="true" style={{ color: danger, marginLeft: 4 }}>*</span>}
        </legend>
      )}
      <div style={{ display: 'flex', flexDirection: orientation === 'horizontal' ? 'row' : 'column', gap: orientation === 'horizontal' ? 18 : 8, flexWrap: 'wrap' }}>
        {options.map((opt, idx) => {
          const optValue = typeof opt === 'string' ? opt : opt.value;
          const optLabel = typeof opt === 'string' ? opt : opt.label;
          const optDisabled = disabled || (typeof opt === 'object' && opt.disabled);
          const checked = value === optValue;
          const radioId = `${groupId}-${idx}`;
          return (
            <label key={optValue} htmlFor={radioId} style={{
              display: 'flex', alignItems: 'center', gap: 10,
              cursor: optDisabled ? 'not-allowed' : 'pointer',
              opacity: optDisabled ? 0.45 : 1,
              fontSize: 13, color: THEME.text,
            }}>
              <span style={{ position: 'relative', display: 'inline-flex' }}>
                <input
                  type="radio"
                  id={radioId}
                  name={name || groupId}
                  value={optValue}
                  checked={checked}
                  onChange={onChange ? () => onChange(optValue) : undefined}
                  disabled={optDisabled}
                  style={{ position: 'absolute', opacity: 0, width: 20, height: 20, margin: 0, cursor: optDisabled ? 'not-allowed' : 'pointer' }}
                />
                <span aria-hidden="true" style={{
                  width: 20, height: 20, borderRadius: '50%',
                  background: isDark ? 'rgba(255,255,255,0.025)' : 'rgba(255,255,255,0.65)',
                  border: `1.5px solid ${checked ? periwinkle : (errorText ? danger : radioBorder)}`,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  transition: 'border-color 0.15s ease',
                }}>
                  {checked && (
                    <span style={{ width: 10, height: 10, borderRadius: '50%', background: periwinkle }}/>
                  )}
                </span>
              </span>
              {optLabel}
            </label>
          );
        })}
      </div>
      {errorText ? (
        <div role="alert" style={{ fontSize: 11.5, fontWeight: 500, color: danger }}>{errorText}</div>
      ) : (helperText && (
        <div style={{ fontSize: 11.5, color: THEME.textDim }}>{helperText}</div>
      ))}
    </fieldset>
  );
};

/* ── FileDrop ──────────────────────────────────────────────────────────── */

const FileDrop = ({
  id, onFiles, accept, multiple,
  label, helperText, errorText, required, disabled,
  size = 'md',
  // For static/preview rendering, force a "drag-active" or "with-files" appearance.
  previewState,
}) => {
  const isDark = THEME.mode === 'dark';
  const fieldId = id || React.useId();
  const periwinkle = isDark ? '#9FB3FF' : '#4E6DA8';
  const danger = '#C24A38';

  // For demo rendering we don't track real drag/file state to avoid React re-renders
  // in side-by-side dark/light layouts. Production callers pass onFiles + handle real state.
  const dragActive = previewState === 'drag';
  const hasFiles   = previewState === 'files' ? 2 : 0;

  const borderColor = errorText ? danger : (dragActive ? periwinkle : THEME.line);

  return (
    <FieldShell label={label} helperText={helperText} errorText={errorText} required={required} htmlFor={fieldId}>
      <label
        htmlFor={fieldId}
        style={{
          display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8,
          padding: '24px 16px', borderRadius: 12,
          background: isDark
            ? (dragActive ? 'rgba(159,179,255,0.06)' : 'rgba(255,255,255,0.025)')
            : (dragActive ? 'rgba(105,134,191,0.08)' : 'rgba(255,255,255,0.65)'),
          border: `1.5px dashed ${borderColor}`,
          cursor: disabled ? 'not-allowed' : 'pointer',
          opacity: disabled ? 0.45 : 1,
          textAlign: 'center',
          transition: 'background 0.15s ease, border-color 0.15s ease',
        }}
      >
        <span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 32, color: dragActive ? periwinkle : THEME.textMute }}>
          {hasFiles ? 'task' : 'upload_file'}
        </span>
        <div style={{ fontSize: 13, color: THEME.text, fontWeight: 500 }}>
          {hasFiles
            ? `${hasFiles} files selected`
            : (dragActive ? 'Drop to upload' : 'Drop a file or click to browse')}
        </div>
        {!hasFiles && (
          <div style={{ fontSize: 11.5, color: THEME.textDim }}>
            {accept ? `Accepts: ${accept}` : 'Any file type'}
            {multiple && ' · multiple'}
          </div>
        )}
        <input
          id={fieldId}
          type="file"
          accept={accept}
          multiple={multiple}
          disabled={disabled}
          required={required}
          onChange={(e) => onFiles && e.target.files.length && onFiles(Array.from(e.target.files))}
          aria-invalid={Boolean(errorText)}
          style={{ position: 'absolute', opacity: 0, width: 1, height: 1, overflow: 'hidden' }}
        />
      </label>
    </FieldShell>
  );
};

/* ── Window exports for canvas + future migration ──────────────────────── */

window.TextField  = TextField;
window.Textarea   = Textarea;
window.Select     = Select;
window.DatePicker = DatePicker;
window.Checkbox   = Checkbox;
window.Switch     = Switch;
window.RadioGroup = RadioGroup;
window.FileDrop   = FileDrop;
