<!-- GENERATED by scripts/build-llms.mjs from llms/forms.md — do not edit this file. -->

# `lr-input`

- **Import** `import '@aceshooting/lyra-ui/components/lr-input.js';` (stable tag alias; registers the tag)
- **Class** `LyraInput`, also available unregistered from `@aceshooting/lyra-ui/components/forms/input/input.class.js`
- **Family** `components/forms/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 17 parts, 22 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-input`

A single-line plain-text input primitive, the `lr-*` equivalent of a plain `wa-input`,
form-associated via the same `FormAssociated` mixin as `lr-textarea`. Ships the same opt-in
`label`/`hint`/`errorText` form-control chrome as `lr-textarea`/`lr-select`, and the same
`size` scale as `lr-select`/`lr-combobox`.

`stepUp()` and `stepDown()` use the current `value`, `step`, `min`, and `max` properties in the same
synchronous call, including changes made immediately beforehand. They preserve native step alignment
and bounds, update form submission silently, and remain no-ops before the native input has rendered
or while disabled/readonly. Native `step="any"` remains a non-steppable no-op.

Removing `label`, `hint`, `help-text`, or `error-text` safely omits that content while retaining
native `null` property readback. Explicit empty strings remain empty and later supplied text renders
normally. The same inherited behavior applies to `lr-number-input` and `lr-native-time-input`.

Changing own `disabled` from true to false in the same task that disables an ancestor fieldset keeps
the native editing control effectively disabled. The enabled first-legend exception and explicit
own-disabled state retain their native meaning; validity and form submission follow the effective
disabled state.

Pressing Enter submits the ancestor `<form>` — the implicit submission a native `<input>` performs;
see "Enter-to-submit" below for the exact rules and for which controls deliberately opt out.

Public `--lr-input-*` theme inputs stay undeclared on the host, so an ancestor theme wrapper can
override size, appearance, and pill fallbacks; a value set directly on the element still wins.
When a clear or password action is present, `2xs` through `m` grow only enough to contain its
shared hit target (42px including the row border at the default theme); `l` and `xl` retain their
48px and 56px shared control heights.

**Properties:**

- `type: LyraInputType = 'text'` — `'text' | 'password' | 'email' | 'number' | 'time' | 'search' |
'date' | 'datetime-local' | 'tel' | 'url'`. Unsupported attribute or direct-property strings
  normalize to reflected `text` before native validity and type-dependent chrome are projected
- `size: LyraSize = 'm'` (reflected — see "Shared form vocabulary" below)
- `appearance: 'accent' | 'filled' | 'outlined' | 'filled-outlined' | 'plain' = 'outlined'`
  (reflected) — the shared field-surface vocabulary. `outlined` (the mapped default) draws a border
  without a fill; `filled-outlined` draws both, `filled` drops the border, `plain` drops
  both, and `accent` tints both with the brand color. Each value does nothing but swap
  `--lr-input-fill`/`--lr-input-border-color`, so either can be retuned without a
  `::part(input-wrapper)` rule
- `filled: boolean = false` (reflected) — Shoelace alias for the filled treatment
- `pill: boolean = false` (reflected) — rounds the control row to a full pill by swapping
  `--lr-input-radius` to `--lr-radius-pill`
- `autofocus: boolean = false` — forwarded to the internal native `<input>` rather than left on the
  host, so the browser's own autofocus algorithm targets the real text control (the custom-element
  host is not focusable). Left unset, the native attribute is omitted entirely
- `value: string = ''` (from `FormAssociated`)
- `defaultValue: string = ''` — reset value, backed by the standard `value` content attribute;
  `default-value` is accepted as a Shoelace attribute alias
- `name: string` (from `FormAssociated`) and the plain inherited `id` are both forwarded to the
  internal native `<input>` (previously the internal input carried neither, only a fixed
  `id="input"`), so shadow-DOM-aware password managers that key field detection off the actual
  control's own `name`/`id` — not `autocomplete` alone — recognize `<lr-input>` fields. The
  internal `<label for>` tracks whichever id is in use. Leaving `id` unset on the host keeps the
  internal input at `id="input"`, identical to before
- `placeholder: string = ''`
- `clearable: boolean = false` (reflected) — shows a localized clear action while a `text` or
  `search` input has a value; clearing preserves input focus
- `withClear: boolean = false` (attribute `with-clear`) — Web Awesome's spelling of `clearable`;
  either one shows the same action. Inherited by `lr-number-input` and `lr-native-time-input`,
  where it is inert because their native types render no clear action. The separate segmented
  `lr-time-input` implements its own `with-clear` action
- `readonly: boolean = false` (reflected) — forwarded to the native input and disables clearing
- `label: string = ''`
- `hint: string = ''`
- `helpText: string = ''` (attribute `help-text`) — Shoelace alias for `hint`; `hint` wins when both
  are set. `withLabel`/`withHint` (`with-label`/`with-hint`) provide optional SSR slot-presence hints
- `errorText: string = ''` (attribute `error-text`)
- `accessibleLabel: string | null = null` (attribute `aria-label`)
- `autocomplete: string = ''`
- `title: string = ''` — forwarded to the native input
- `spellcheck: boolean = true` — forwarded to the native input, including `spellcheck="false"`
- `autocapitalize: string = ''` / `autocorrect` (read: `boolean = true`; write:
  `boolean | 'off' | 'on'`; attribute values `on`/`off`)
- `inputMode: string = ''` (attribute `inputmode`) / `enterKeyHint: string = ''` (attribute
  `enterkeyhint`) — `autocapitalize`, `inputMode`, and `enterKeyHint` are forwarded verbatim to the
  native input and an empty string omits them; `autocorrect` is normalized to canonical `on`/`off`
- Lowercase native IDLs `inputmode: string` and `enterkeyhint: string` delegate to the camel-case
  native properties; `autocorrect` reads as boolean while accepting both Web Awesome's boolean
  writes and Shoelace's `'off'`/`'on'` writes

**8.0 migration:** the former camel-case string property `autoCorrect` is not retained as a public
alias. Prefer boolean `autocorrect` writes in new code; migrated Shoelace `'off'`/`'on'` property
writes remain valid and read back as booleans. Markup uses `autocorrect="on"` /
`autocorrect="off"`.

- `min?: number | string` / `max?: number | string` (attributes `min`/`max`) /
  `step?: number | 'any'` (attribute `step`, accepts the native `'any'` value alongside a number)
  — forwarded verbatim to the native
  input and validated by it. Intended for `type="number"`; `step` is equally meaningful on
  `type="time"`. On `lr-input` itself the `min`/`max` _attributes_ are number-converted, so a
  non-numeric bound only survives a direct property assignment; the declared type also admits a
  string so a subclass can narrow the attribute parsing to its own native type's literal form —
  `lr-native-time-input` does exactly that. Inert for the other types
- `minlength?: number` / `maxlength?: number` (attributes `minlength`/`maxlength`) — text-length
  bounds forwarded to the native input and reported as `validity.tooShort`/`validity.tooLong`.
  Apply to the text-bearing types (`text`, `search`, `email`, `password`); the platform ignores
  both on `type="number"`/`type="time"`, and so does this component
- `pattern?: string` (attribute `pattern`) — a regular expression the value must match in full,
  forwarded to the native input and reported as `validity.patternMismatch`. Anchored to the whole
  value by the platform, so no `^`/`$` is needed; an empty value never violates it
- `passwordToggle: boolean = false` (attribute `password-toggle`, reflected — `type="password"`
  only) — renders the built-in show/hide-password button. **Breaking in 8.0.0: this is now opt-in.**
  Before, `type="password"` always rendered the toggle and there was no way to remove it; a consumer
  whose threat model or visual design excludes one had to hide it with CSS. Add `password-toggle`
  to keep the old rendering
- `passwordVisible: boolean = false` (attribute `password-visible` — `type="password"` only) —
  whether the field currently reveals its raw text. Toggled by the built-in button, and also
  settable up front with or without that button being rendered
- `match: string | HTMLElement | null = null` (attribute `match`) — declarative cross-field
  confirmation constraint: a sibling field to compare this one's `value` against, referenced either
  by id (resolved in this element's own root — an idref never crosses a shadow boundary, matching
  every other idref this library resolves) or by a direct element reference (works across shadow
  trees, since no lookup is needed). While set and resolvable, this field additionally fails
  validity — `customError`, with a localized mismatch message — whenever its value differs from the
  referenced element's own `.value`, but only once every other constraint above (`required`,
  `pattern`, length, type-specific format) already reports valid, so an empty required confirm
  field reports `valueMissing`, not a mismatch. Re-validates automatically on either field's own
  edits: this one's through the usual `value` write, and the referenced one's through a listener on
  its `input`/`change` events, so retyping the password half of a confirm pair revalidates the
  confirm field immediately, not only on its own next edit. A `match` that does not resolve to a
  live element (most commonly a dangling id) is inert rather than a permanent block on submission,
  exactly like the platform's own tolerance of an unresolvable `aria-describedby` idref. The
  referenced element only needs a string `.value`, so a native `<input>`/`<textarea>` works the
  same as another `lr-input`; see "A new-password field" below for the password-confirmation shape
- `withoutSpinButtons: boolean = false` (attribute `without-spin-buttons`, reflected —
  `type="number"` only) — suppresses the browser's own increment/decrement spin buttons.
  **Breaking in 8.0.0:** `type="number"` used to hide them unconditionally; left unset, the
  platform's spinners now render exactly as they do on a bare `<input type="number">`.
  `<lr-number-input>` defaults this the other way (`true`), so its rendering is unchanged
- `noSpinButtons: boolean = false` (attribute `no-spin-buttons`) — Shoelace alias for
  `withoutSpinButtons`; either suppresses native number spinners
- `debounce?: number` (attribute `debounce`, ms) — how long to wait after the last keystroke
  before emitting one `lr-input-settled`, while `input`/`lr-input` keep firing per keystroke as
  before. Omitted, `0`, or a non-finite value means no debounce at all: `lr-input-settled` never
  fires. A pending debounce is flushed immediately by `change`/Enter/blur (so a blur never drops
  the last keystroke) and cancelled with no stray settle by disconnection, the built-in clear
  button, and a programmatic `value` write that changes the value. A write of the value already
  held leaves it pending, so a framework binding that writes the just-typed value back on each
  render (the controlled-input pattern) still settles. Shares its `DebounceController` primitive with
  `lr-filter-bar`'s own per-filter `debounce` and with `lr-textarea`'s identical property
- `name`/`disabled`/`required` (from `FormAssociated`)

**Getters/methods:** `input: HTMLInputElement | null` (the internal native `<input>`, for direct DOM
access), `focus(options?: FocusOptions)`, `blur()`, `select()`. Also forwards the full native
selection/editing surface, mirroring `lr-textarea`'s identical passthrough: `selectionStart: number
| null` and `selectionEnd: number | null` (readable/writable; `null` both before the internal input
has rendered and whenever `type` doesn't support selection — only `text`/`search`/`password` do,
matching the native `<input>`'s own contract), `setSelectionRange(start, end, direction?)`
(no-op before render, otherwise throws the same native `InvalidStateError` a native `<input>` would
for an unsupported `type`), and `setRangeText(replacement, start?, end?, selectMode?)` (no-op
before render; syncs `value` afterward without emitting a user event).
The shared form surface also exposes `getForm()`, which returns the browser-resolved owner including
an external form selected by the `form` attribute. `resetValidity()` clears only consumer-supplied
custom validity and recomputes the current native/required constraints; it leaves
`value`/`defaultValue` and prior interaction state unchanged, so an intrinsically invalid input
stays invalid.

Three more native passthroughs:

- `valueAsDate: Date | null` / `valueAsNumber: number` — native getters/setters for date/time and
  numeric input types. Assignment synchronizes `value`, form value, and validity without emitting a
  user edit event; unsupported types retain the native `null`/`NaN` behavior.

- `showPicker(): void` — opens the browser's own picker for the current `type` (the time picker, and
  whatever chooser the platform offers for the other types), delegating to the internal native
  `<input>`. Deliberately failure-tolerant: the platform method throws for environmental reasons a
  component can neither detect up front nor usefully report (no user activation →
  `NotAllowedError`, a cross-origin document → `SecurityError`, a non-mutable control →
  `InvalidStateError`), and engines that predate it don't define it at all. A picker that cannot
  open is a **no-op here rather than an exception** you must wrap every call in. Also a no-op while
  `disabled` or `readonly`.
- `stepUp(steps = 1): void` / `stepDown(steps = 1): void` — increment/decrement by `steps` × the
  effective `step`, through the native `<input>`'s own `stepUp()`/`stepDown()`, so `min`/`max`
  clamping and decimal handling stay the platform's. **Silent, like the native methods**: they
  update `value`, the submitted form value and validity, but emit no `input`/`change`. A
  non-finite `steps` falls back to `1`; `0` is a no-op, as is `step="any"`, as is any `type` the
  platform gives no allowed value step (it throws `InvalidStateError` for those, which is swallowed
  here), and as is `disabled` or `readonly`. `type="number"` and `type="time"` are the two that step
  — on a time field the unit is seconds, matching its `step`.
  `<lr-number-input>`'s stepper buttons build on these and _do_ emit, because a button press is a
  user edit.

**Events:** native-style composed `input` and `change`, plus `lr-input` (`detail: { value }`,
fired on every user-driven edit) and `lr-change` (`detail: { value }`, fired on the native
`change` timing), `blur`/`focus` (re-dispatched bubbling + composed from the internal native input's
own `blur`/`focus`), and
`lr-clear` (no detail, fired after the clear action's `input`/`lr-input`/`change`/`lr-change`
sequence). `lr-invalid` (no detail) fires when a validity check finds the input invalid.
`lr-input-settled` (`detail: { value }`, non-cancelable) fires once, `debounce` ms after the last
keystroke, alongside the per-keystroke `input`/`lr-input` pair; see `debounce` above for the flush
and cancellation rules. Never fires while `debounce` is unset, `0`, or non-finite.

**Slots:** `label`, `hint`/`help-text`, `error`, `start`/`prefix` (aliases before the input),
`end`/`suffix` (aliases after the input and built-in actions), `clear-icon`,
`show-password-icon`, and `hide-password-icon`.

**CSS parts:** `form-control`, `form-control-label`, `label`, `base`/`form-control-input`/
`input-wrapper` (compatibility names on the row wrapping the native input and actions), `input`,
`password-toggle`, `password-toggle-button`
(present only when `type="password"` **and** `password-toggle` is set), `start`, `end`,
`prefix` (alias of `start`), `suffix` (alias of `end`),
`clear-button` (non-empty clearable `text`/`search` inputs only),
`hint`/`form-control-help-text` (compatibility names on the same hint node), and `error`.
Long `start`/`end` adornments shrink and ellipsize inside their flex allocation rather than
widening a narrow field; label, hint, and error text wrap at unbroken boundaries. The
`Narrow RTL (320px)` story exercises both adornments with a clear action and localized long copy.

**The required marker.** `required` with a non-empty `label` paints the library's shared marker on
`[part="form-control-label"]` — the one `::after` rule described under "The required-field marker"
above, and the reference implementation every other labelled control in the library shares, so
`--lr-form-control-required-content`, `--lr-form-control-required-color` and
`--lr-form-control-required-offset` retune or suppress it. With no label text the part is hidden and
no glyph is painted.

**Themeable custom properties:** `--lr-input-padding-block`, `--lr-input-padding-inline`,
`--lr-input-font-size`, `--lr-input-control-min-height` — all four have private defaults that follow
`size` (`2xs`…`xl`), while inherited or direct public values remain authoritative; the same pattern
`lr-select`'s `--lr-select-trigger-padding`/`--lr-select-font-size` use.
`--lr-input-control-height` pins an **exact** outer control-row height (both floors and caps it) —
for example to pixel-match an `<lr-select>` or `<lr-combobox>` in the same toolbar row. It is
undeclared by default, leaving `--lr-input-control-min-height` as a floor only and the row free to
grow. `--lr-input-gap` (default `--lr-space-xs`, the gap inside `[part='input-wrapper']`) is
retunable without a `::part(input-wrapper)` rule and, unlike the four properties above, does not
vary by `size` — the adornment gap a text field wants between an adornment and the caret is looser
than the icon-beside-label gap the ladder is tuned for. `--lr-input-radius` (default
`--lr-form-control-radius`, its corner radius) is retunable the same way but _does_ follow the tier:
the two tightest tiers take a smaller radius, since a 6px corner on a 20px-tall control reads as a
lozenge. `pill` changes its private default to `--lr-radius-pill`; an inherited or direct public
value still wins. `lr-number-input`/`lr-native-time-input` inherit both
unchanged. The separate segmented `lr-time-input` also consumes the documented input theme tokens.

`--lr-input-fill` (default `transparent`) is the control row's background and
`--lr-input-border-color` (default `var(--lr-color-border)`) its border color. `appearance` changes
their private fallback roles rather than the public hooks, and the documented defaults are
`appearance="outlined"`'s values. Ancestor theme wrappers therefore still win. Setting either
directly retunes the surface
without a `::part(input-wrapper)` rule and without leaving the `appearance` vocabulary behind.
`--lr-input-focus-border-color` independently retunes the focused row, and the shared field halo
`--lr-form-control-focus-shadow` (default `none`) adds a `box-shadow` layer while the row holds
focus — one name for every field-shaped control in the library, additive rather than a replacement
for the focused border. Built-in clear/password
actions and `lr-number-input` steppers share `--lr-input-action-color`,
`--lr-input-action-hover-color`, `--lr-input-action-active-color`, and
`--lr-input-action-active-bg`; all fall back to the previous text/surface semantic tokens.
For `type="time"`, the browser-native picker indicator gains disabled-gated hover and focus-visible
affordances through `--lr-input-time-picker-hover-bg`, `--lr-input-time-picker-active-bg`,
`--lr-input-time-picker-focus-bg`, and `--lr-input-time-picker-focus-ring` (falling back to
brand-quiet/brand and the canonical focus-ring token).

### Shared form vocabulary — `size`, `appearance`, `pill`, and custom validity

Four things every form control in this family now spells the same way. They are documented here
because `lr-input` is where a reader meets all four at once; each component's own list restates only
what is specific to it.

- **`size` accepts both spellings of every tier.** The canonical ladder is
  `2xs`/`xs`/`s`/`m`/`l`/`xl`, and `small`/`medium`/`large` — Web Awesome's and Shoelace's names —
  are accepted as exact synonyms for `s`/`m`/`l`. Nothing is normalized away in JS: the shared
  stylesheet matches both spellings in the same selector list, so `size="small"` costs nothing and
  `el.size` reads back whatever you wrote. A migration off either upstream is a tag rename with no
  attribute rewrite. One ladder now drives `lr-button`, `lr-input`, `lr-select`, `lr-combobox`,
  `lr-date-input`, `lr-textarea`, `lr-checkbox`, `lr-radio`, `lr-switch` and `lr-slider`, so
  same-`size` controls line up in a toolbar row by construction. Retune a whole tier from one place
  with `--lr-theme-form-control-height-*` rather than per component. Set
  `--lr-theme-form-control-radius` on the same ancestor to give these controls one shared corner
  radius across every tier; without it, the compact `2xs`/`xs` tiers retain their smaller default
  radius.
- **`appearance` is the fill vocabulary and nothing else.** `accent` (the loud semantic fill),
  `filled` (a quiet tint of the same tone), `outlined` (a border, no fill), `filled-outlined`
  (both) and `plain` (neither). It used to double as a _container_ treatment on other components;
  that meaning moved to `frame` (`card`/`plain`) in 8.0.0, so `appearance` means one thing
  library-wide. `lr-button` adds two tiers of its own on top (`quiet` and `link`). Text fields
  (`lr-input`, `lr-textarea`, and `lr-select`) default to `outlined`; `lr-button` defaults to `accent`.
- **`pill` rounds the control's ends.** Available on `lr-input`, `lr-number-input`, `lr-time-input`,
  `lr-textarea`, `lr-select`, `lr-combobox`, `lr-date-input`, `lr-phone-input`, `lr-token-input`,
  `lr-button` and `lr-radio-button`. In every case it does exactly one thing — re-assign that
  component's own `--lr-*-radius` knob to `--lr-radius-pill` — rather than declaring a radius on a
  part, so the knob stays the single corner-radius override point and a consumer's own value still
  wins over it.
- **`setCustomValidity(message)` and `resetValidity()` are on every form-associated _value_ control
  here** — every one
  that submits something, whether it drives `ElementInternals` through the shared mixin or by hand.
  (`lr-button` is a form-associated submitter, so an ancestor `<fieldset disabled>` and
  `form.elements` reach it, but it carries no value or validity and therefore has no such method;
  `lr-icon-button` is deliberately not form-associated.) It is the standard channel for a rejection
  no client-side constraint can express — a server-side
  "that email is already registered". A non-empty message raises `customError` and becomes
  `validationMessage`, so the control fails `checkValidity()`, blocks submission, and matches
  `:invalid`/`:state(invalid)`.
  `''` clears it and republishes the control's _own_ computed validity rather than forcing it valid:
  a required-and-empty field goes back to `valueMissing`. The message survives every intrinsic
  recomputation and a `form.reset()`, exactly like a native control; `setCustomValidity('')` or
  `resetValidity()` clears it. `resetValidity()` affects only that consumer layer and recomputes the
  current intrinsic constraints: it does not change `value`/`defaultValue`, make the control
  pristine again, or force an intrinsically invalid value valid. The message is used verbatim,
  never localized, because it is caller-supplied content.

### Enter-to-submit

Pressing Enter in a single-line text control submits the ancestor `<form>`, the implicit submission
a native `<input>` performs. The internal input lives in a shadow root and has no form owner of its
own, so the platform can never run it here; the component does, following the platform's own rules
rather than an approximation of them:

- The keystroke must be a **bare** Enter — any of Ctrl/Cmd/Alt/Shift held makes it an application
  shortcut (send-and-keep-open, insert-newline, open-in-new-tab), never a submission.
- An Enter **during IME composition** commits the highlighted candidate; submitting there would
  throw away the word being typed, so it is skipped.
- A keydown already `defaultPrevented` by a listener above stays vetoed.
- The **submitter is resolved, not skipped**: the form's default button is the first enabled submit
  control in `form.elements`, so its `name`/`value` entry and its
  `formaction`/`formmethod`/`formnovalidate` overrides all reach the submission. A native button
  goes through `form.requestSubmit(submitter)`; an `<lr-button type="submit">` is a form-associated
  custom element, which `requestSubmit()` rejects with a `TypeError`, so it is activated through its
  own `click()` — the same path a real click takes.
- A form with **no** submit button submits implicitly only when it holds at most one field that
  blocks implicit submission, matching the platform.
- It runs through `requestSubmit()`, never `submit()`, so the `submit` event fires and interactive
  constraint validation blocks an invalid form exactly as a real submit button would. Each control
  also gates on its own `disabled`/`readonly` first.

**Deliberately not wired everywhere.** Enter means something else in several controls, and implicit
submission must never shadow it: `lr-textarea` and `lr-code-editor` insert a newline, which is the
whole point of a multi-line surface; `lr-select`'s `role="combobox"` trigger opens the listbox (and
then commits the active option), per the ARIA pattern; and `lr-date-picker` selects the focused day
in the calendar grid. The controls that _do_ wire it are `lr-input` (and its `lr-number-input`/
`lr-native-time-input` subclasses), the separate segmented `lr-time-input`, `lr-combobox`,
`lr-date-input`, `lr-phone-input`, `lr-token-input` and `lr-otp-input`.

### Exact-height hatches — the one rule that applies to all of them

Several controls expose the same pair: a per-`size` `*-min-height` **floor**, and an exact
`*-height` **cap**. The family is `--lr-input-control-height`, `--lr-select-trigger-height`,
`--lr-combobox-trigger-height`, `--lr-date-input-control-height`, `--lr-button-height`,
`--lr-known-date-field-height`, and `--lr-chip-height`. Every one of them behaves identically:

- **Each is undeclared by default.** The component reads it only through two `var()` fallbacks —
  `min-block-size: var(--lr-x-height, var(--lr-x-min-height))` and
  `block-size: var(--lr-x-height, auto)` — so leaving it unset is what makes the per-tier floor
  and the content-driven height work at all.
- **Setting one to `auto` is not the same as leaving it unset.** `auto` is a perfectly valid
  _declared_ value, and a declared value wins over the `var()` fallback arm — so `auto` silently
  turns the per-tier `*-min-height` floor into dead code, and nothing anywhere reports it. To
  return a control to default behavior, **remove** the declaration; never neutralize it with
  `auto`.
- Because the component itself never declares them, each can be set inline on the element, from an
  ancestor, or from an outer-tree rule (`lr-input { --lr-input-control-height: 44px }`) — no
  `::part()` rule needed.
- **A dead declaration is invisible in source.** There is no way to tell a live `--lr-*`
  declaration from a shadowed or defeated one without rendering: a test asserting on stylesheet
  text passes either way. Assert the rendered `min-block-size`/`block-size` via
  `getComputedStyle` on the real element instead of reading the custom property back.

**Optional peer deps:** none.

```html
<lr-input type="password" label="Password" password-toggle></lr-input>
<lr-input type="email" label="Email" required></lr-input>
<lr-input size="s" placeholder="Compact"></lr-input>
<lr-input appearance="plain" pill placeholder="Pill, no chrome"></lr-input>
<lr-input
  type="number"
  min="0"
  max="10"
  step="0.5"
  without-spin-buttons
  label="Weight"
></lr-input>
<lr-input type="search" clearable value="workflow" aria-label="Search"
  ><span slot="start">⌕</span></lr-input
>
<lr-input type="time" label="Reminder" id="reminder"></lr-input>
<button type="button" id="open-picker">Pick a time</button>
<script type="module">
  import "@aceshooting/lyra-ui/components/forms/input/input.js";
  const time = document.getElementById("reminder");
  // showPicker() needs user activation, so drive it from a real click.
  document
    .getElementById("open-picker")
    .addEventListener("click", () => time.showPicker());
</script>
```

`password-toggle`, `pill` and `without-spin-buttons` all default to `false`, so the plain
attribute form is enough to turn each on. `autofocus` is likewise `false`-defaulting — none of
these four needs the property form to be reset.

**A new-password field.** There is no dedicated password-purpose preset — a deliberate decision,
not an omission: the only thing such a preset would actually save is `autocomplete`, and that value
has no single correct default for "a password field" (`new-password` on a set/change/reset flow,
`current-password` on a login one, and one is never derivable from the other), so a `purpose`
property would still need a second parameter carrying that same distinction, in exchange for a
non-standard vocabulary a migrating `wa-`/`sl-`/native `<input type="password">` author would have
to learn instead of carrying over unchanged. Compose the existing primitives directly, the same way
a plain native `<input type="password">` does:

```html
<lr-input
  type="password"
  id="new-password"
  label="New password"
  password-toggle
  autocomplete="new-password"
  minlength="12"
  required
></lr-input>
<lr-input
  type="password"
  label="Confirm password"
  password-toggle
  autocomplete="new-password"
  match="new-password"
  required
></lr-input>
```

`autocomplete="new-password"` (rather than the bare `password` token, and never `current-password`
on a set/change/reset flow) is the platform contract that keeps a browser's or password manager's
own generator and save prompt from cross-contaminating a change/reset flow with the account's
existing credential; set it explicitly on every field in the pair, since `autocomplete` has no
purpose-derived default here. `match` (above) is what makes the second field fail validity —
`customError`, with a localized mismatch message — for as long as its value disagrees with the
first field's, referenced here by id.

**Known gotchas:**

- `type="email"`/`type="number"` delegate constraint validation to the internal native `<input>`'s
  own browser-computed `validity` (format/range/step), bridged into this element's own
  `ElementInternals` — not a second hand-rolled regex check. The same bridge carries
  `minlength`/`maxlength`/`pattern`, so `validity` reports the full native set: `valueMissing`,
  `typeMismatch`, `rangeUnderflow`, `rangeOverflow`, `stepMismatch`, `tooShort`, `tooLong`,
  `patternMismatch`, and `badInput`.
- **`tooShort`/`tooLong` also fire for a value assigned from script.** The native flags are raised
  only for a value the _user_ edited, so the component recomputes both from its own `value` and ORs
  them in; `el.value = <over-length>` reports `tooLong` rather than silently submitting. Lengths
  count UTF-16 code units, matching the native control (one emoji counts as two). `patternMismatch`
  needs no such handling — the platform applies `pattern` to script-assigned values already.
  `validationMessage` is the browser's own localized message when the native input flagged the
  value, and the localized `valueInvalid` string when only the script-value check did.
- An empty value is never `tooShort` and never a `patternMismatch` — both native constraints skip
  the empty string, and `required` is what rejects it.
- **The `password-toggle` button is opt-in as of 8.0.0.** A bare `type="password"` now ships no
  toggle at all, and the `password-toggle` part is absent from the shadow tree with it — a
  `::part(password-toggle)` rule, or a test that queries for it, silently matches nothing until the
  attribute is set. The toggle never renders for a non-password `type`, opted in or not.
- **`type="number"` no longer hides the native spin buttons on its own.** Set
  `without-spin-buttons` (or use `<lr-number-input>`, which defaults it to `true` and draws its own
  stepper pair) to get the previous rendering back.
- `showPicker()` swallows every platform failure by design, so it returns without telling you the
  picker didn't open. Don't build a flow that assumes a picker is now on screen.
- `stepUp()`/`stepDown()` are silent — they emit no `input`/`change`. Emit your own, or drive the
  value through a real user affordance, if downstream state depends on those events.
- **A `match` id reference resolves in this element's own root, not the whole document.** Two
  fields inside two different shadow roots (a confirm field composed by one component, the field it
  should match composed by an unrelated one) must pair through a direct element reference
  (`confirm.match = passwordEl`) instead of an id string — exactly like every other idref this
  library resolves, `aria-describedby` included. An id that does not resolve at all (a typo, or an
  element removed later) is inert rather than a permanent block on submission: `match` stops
  applying, it does not fail closed.

**Additional API surface:**

- `click()` — Activates the internal input.
