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

# `lr-button`

- **Import** `import '@aceshooting/lyra-ui/components/lr-button.js';` (stable tag alias; registers the tag)
- **Class** `LyraButton`, also available unregistered from `@aceshooting/lyra-ui/components/forms/button/button.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** 9 parts, 35 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-button`

A generic action-button primitive. Renders an internal native `<button>`; `type="submit"`/
`type="reset"` are handled by the component itself via its browser-resolved form owner (including
an external owner named by `form`), since a shadow-internal native button doesn't participate in a
light-DOM form's submission on its own. They remain default actions of the composed native
`click`: `preventDefault()` from any listener on that click path vetoes submit/reset before it
runs, while `stopPropagation()` alone does not. Canceling the form's later `submit` or `reset`
event remains an independent veto point.

External descriptions follow the current source element identity when an element with the same ID
replaces it, when a source is removed or reinserted, and when the control reconnects or moves to
another document. Host-root lookup and ordering follow the current `aria-describedby` list,
including unresolved IDs and duplicates. Switching between native button and anchor modes keeps the
relationship on the current action. Existing accessible names and `aria-controls` relationships
retain their separate contracts.

Set `href` to a safe link URL and the root renders as a real `<a part="base" href=…>` instead — a
link styled as a button (e.g. a CTA). Native navigation is then the activation, so the submit/reset
handling and `type` have no effect in that mode. A disabled link button (its own `disabled` or an
ancestor `<fieldset disabled>`) renders the anchor with `aria-disabled="true"` and **no `href`**, so
it is neither focusable nor navigable; it also dims to `--lr-opacity-disabled` with a `not-allowed`
cursor and no hover/press feedback, exactly like the disabled `<button>` path (an `<a>` can never
match `:disabled`, so that arm of the styling keys off `aria-disabled` instead). An
unsafe/unparseable `href` falls back to the native `<button>`.

The label does **not** grow to fill a stretched button. `[part="label"]` shrink-wraps its text and
the whole icon+label pair centres under `--lr-button-justify`, so the icon-to-text distance is
exactly `--lr-button-gap`. Before 16.0.0 the label was `flex: 1 1 auto`, which parked every spare
pixel inside the label box and — because the native `<button>` UA stylesheet centres text, which the
label wrapper inherited — floated the text in the middle of a wide empty row. A `with-caret` button,
and one with an `end`/`suffix` adornment, keep the growing label so that trailing affordance stays
pinned to the trailing content edge. `--lr-button-label-grow: 1` restores the old stretch and `0`
opts a caret/end-adornment row out of pinning. The label is also `text-align: start` now, which
fixes two side effects of the inherited centring: a label narrower than its own text centred the
overflow so the ellipsis appeared at the end while the start of the word was clipped, and the `<a>`
root (which never inherited the centring) disagreed with the `<button>` root across a mode switch.

**Properties:**

- `href?: string` — when set to a safe link URL (`http:`/`https:`/`blob:`/`mailto:`/relative; see
  `safeLinkHref`), the root renders as an `<a href=…>` instead of a `<button>`. Setting `download`
  narrows the allowlist to `http:`/`https:`/`blob:`/relative — `mailto:` names no retrievable bytes,
  so a `mailto:` href paired with `download` falls back to the native `<button>`. Unset (the default)
  renders a plain `<button>`, unchanged. `type` (submit/reset) has no effect while the anchor
  renders. While the button is disabled the anchor is rendered **without** `href` (keeping
  `aria-disabled="true"`), so a disabled link button cannot navigate. An unsafe/unparseable value
  falls back to the native `<button>`
- `target?: string` — native anchor `target`, used only while `href` resolves to a link. Setting it
  (e.g. `'_blank'`) automatically force-adds `noopener noreferrer` to the rendered anchor's
  relationship tokens. Ignored in `<button>` mode
- `rel?: string` — independently settable native relationship tokens with no default. Author tokens
  such as `nofollow`, `me`, `license`, `external`, and `tag` are preserved on same-tab and targeted
  links. `opener` is always stripped, and whenever `target` is set the non-removable
  `noopener noreferrer` floor is merged in. When neither `target` nor author tokens are present the
  anchor omits `rel`
- `download?: string` — native anchor `download` attribute, used only while `href` resolves to a
  link. Presence remains meaningful when the value is empty: `download=""` derives a filename and
  still selects the stricter downloadable-URL policy. Ignored in `<button>` mode
- `variant: 'neutral' | 'brand' | 'success' | 'warning' | 'danger' = 'neutral'` (reflected). Reads
  stay in Lyra's shared vocabulary; migrated Shoelace inputs normalize `default` → `neutral`,
  `primary` → `brand`, and `text` → neutral `appearance="plain"`. The Lyra/Web Awesome default is
  intentionally still `neutral`
- `appearance: 'accent' | 'filled' | 'outlined' | 'filled-outlined' | 'plain' | 'quiet' | 'link' =
'accent'` (reflected) — the library's shared fill vocabulary plus this component's own two extra
  tiers. **Breaking in 8.0.0: the default moved from `'filled'` to `'accent'`**, so a bare
  `<lr-button>` now paints the loud fill it used to need `appearance="accent"` for. The two are no
  longer near-duplicates: `'accent'` takes the active `variant`'s **loud** fill
  (`--lr-button-accent-fill`) with the foreground guaranteed legible on it, while `'filled'` takes
  that variant's **quiet** tint (`--lr-button-fill`) — a secondary-action fill that still reads as a
  fill rather than as the page surface. Before 8.0.0 every chromatic variant's `'filled'` and
  `'accent'` resolved to the same loud token and rendered identically, while `variant="neutral"`'s
  `'filled'` was the page surface, i.e. no fill at all. `'filled-outlined'` is `'filled'`'s fill and
  foreground carrying the outlined tier's border colour, for a filled button that must still read as
  bounded on a same-toned surface. `'quiet'` is a de-emphasized tier:
  transparent background with a bordered, muted-text chrome; its text/border tokens are **not**
  variant-swapped, so `variant` has no effect on it.
  `'link'` is a true inline-link tier:
  zero chrome (no padding, border, border-radius, or `min-block-size` floor), underlined
  (`text-underline-offset: var(--lr-size-0-15rem)`), colored from `--lr-button-accent` (the same token `'plain'`
  uses, so `variant` still selects the link color) and inheriting the surrounding font-size/weight
  — for a text link that flows within a sentence rather than a button-shaped control. Declared
  after the per-`size` rules, so it overrides them whatever `size` is set
- `size: LyraSize = 'm'` (reflected) — the shared control ladder. `'2xs'` is the tightest tier,
  below `'xs'`, for dense chrome; `'m'` is the standard one. Both spellings of every tier are
  accepted — `2xs`/`xs`/`s`/`m`/`l`/`xl` and `small`/`medium`/`large` — and the same ladder drives
  `lr-input`/`lr-select`/`lr-combobox`/`lr-date-input`, so same-`size` controls share density and
  minimum-height tokens rather than separate scales. That is not a blanket pixel-height promise:
  content and nested action hit-target floors can make a composed control such as `lr-date-input`
  taller
- `pill: boolean = false` (reflected) — fully rounded ends. It changes the private radius default
  to `--lr-radius-pill` rather than declaring a radius on `[part="base"]`, so an inherited or
  direct `--lr-button-radius` remains authoritative. `appearance="link"` renders with zero chrome,
  pill or not
- `circle: boolean = false` (reflected) — Shoelace-compatible circular icon-button treatment: a
  square control with the pill radius and compact inline padding. It is additive to, not a rename
  of, `pill`. Circle and automatically detected icon-only buttons retain the shared
  `--lr-icon-button-size` minimum clickable box at every `size`; the tier still scales their glyph
  and chrome. A visually hidden label does not count as content, so an icon plus an `.sr-only` name
  still gets the square, `--lr-icon-button-size`-floored treatment. `<lr-visually-hidden>`,
  `hidden`/`display: none`/`visibility: hidden`, and the standard absolutely-positioned
  `clip-path: inset(50%)` algorithm are all recognised from computed style, so a consumer's own
  utility class works whatever it is called
- `wrap: boolean = false` (reflected) — wraps a long label onto multiple lines instead of
  ellipsis-truncating it to one, the same opt-in `<lr-chip>` ships. Unset, `[part="label"]` keeps
  its single-line, ellipsis-truncated rule exactly
- `outline: boolean = false` (reflected) — Shoelace-compatible outlined treatment. It does not
  overwrite `appearance`, so removing `outline` restores the canonical Lyra appearance
- `withCaret: boolean = false` (attribute `with-caret`, reflected) — renders a decorative trailing
  chevron (`[part="caret"]`, `aria-hidden`) marking the button as a dropdown/menu trigger. It
  carries no accessible name of its own: the button's label already names the action, and the popup
  relationship is expressed by a host `aria-haspopup`/`aria-expanded`, which are forwarded to the
  internal control. Like the label and the two adornment slots it fades to `opacity: 0` while
  `loading`, so the spinner has the button to itself
- `caret: boolean = false` (attribute `caret`, reflected) — Shoelace alias for `withCaret`; either spelling renders the same part
- `withStart: boolean = false` / `withEnd: boolean = false` (attributes `with-start`/`with-end`) —
  Web Awesome SSR presence hints that keep the matching adornment wrapper mounted before slot
  assignment is observable
- `type: 'button' | 'submit' | 'reset' = 'button'`
- `loading: boolean = false` (reflected) — shows an internal spinner and disables the button without
  clearing `disabled`
- `disabled: boolean = false` (reflected)
- `accessibleLabel: string | null = null` (attribute `aria-label`) — accessible name forwarded
  reactively to the internal native button or anchor; changing or removing the attribute after
  mount updates the actual focused control
- `required: boolean = false` (reflected), `validity`, `validationMessage`, and `willValidate` —
  Web Awesome's form-validity surface. A required button needs a non-empty submitter `value`; this
  validation never makes the button a persistent form-data entry. Disabled, loading, and actual
  anchor modes clear effective validity while retaining intrinsic/custom state for restoration
- `customError: string | null` (attribute `custom-error`, reflected) — consumer validation message

**Submitter overrides (`type="submit"` in `<button>` mode).** `name`/`value` plus the five native
`form*` overrides describe the submission this button triggers, not the button itself:

- `formAction?: string` (attribute `formaction`) — overrides the form owner's `action`. Unset leaves
  the form's own action in place; an explicitly present empty string is forwarded and follows
  native resolution against the current document
- `formEnctype?: 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain'`
  (attribute `formenctype`) — overrides the form owner's `enctype`
- `formMethod?: 'get' | 'post' | 'dialog'` (attribute `formmethod`) — overrides the form owner's
  `method`; `'dialog'` closes an ancestor `<dialog>` instead of submitting
- `formNoValidate: boolean = false` (attribute `formnovalidate`) — skips the form owner's
  constraint validation. Without it an invalid form is reported and not submitted, exactly as with
  a native submit button
- `formTarget?: string` (attribute `formtarget`) — overrides the form owner's `target`. Distinct
  from `target`, which is the anchor target used in link mode

All five are `undefined`/`false` by default. When any of them — or `name`/`value` — is set, the
submission runs through a **transient native `<button type="submit">`** inserted directly after the
host, used as `requestSubmit()`'s submitter and removed again in the same synchronous step (in a
`finally`, so a throwing or validation-blocked submission can't leave it behind). That is what makes
the name/value pair reach the submitted `FormData` and the overrides reach the real submission:
`requestSubmit()` only accepts a submitter the form actually owns, and a custom element is never
one. While that stand-in exists it _is_ the form's submitter, so **`SubmitEvent.submitter` is the
transient native button, not this host**. With none of those properties set, submission stays a
plain `requestSubmit()` with a `null` submitter, and all of it is inert in link mode.
For each string override, presence rather than truthiness chooses the transient path and its raw
attribute is copied, so explicit empty values remain distinguishable from absence. Only the
canonical native/upstream spellings are supported; the former hyphenated Lyra aliases were
removed.

Each size tier's `min-block-size` floor is exposed as its own token (see below).

**Getters/methods:** `click()`, `focus(options?)`, and `blur()` — forwarded to the internal base
element (the `<button>`, or the `<a>` in anchor mode); `click()` also runs the component's
submit/reset behavior in `<button>` mode. `getForm()` returns the browser-resolved form owner,
including an external owner selected by the `form` attribute. `checkValidity()`, `reportValidity()`,
and `setCustomValidity(message)` delegate to `ElementInternals`; `resetValidity()` clears only the
consumer error and restores the current `required`/`value` constraint. `formStateRestoreCallback()`
restores `value` for session history/autofill without changing submitter-only form-data semantics.

**Events:** a plain native `click` bubbles and composes through the shadow boundary unmodified
(disabled while `disabled` or `loading`). In button mode, submit/reset runs only after that click's
listener path has accepted the default action; calling `preventDefault()` on the host or an
ancestor therefore vetoes it, while propagation control by itself does not. The internal button's
`focus` and `blur` — which do not
cross the shadow boundary on their own — are re-dispatched from the host as bubbling, composed
events. `lr-invalid` (no
detail, cancelable) fires when a validity check finds the button invalid; `preventDefault()` on it
suppresses the native validation bubble and `reportValidity()`'s focus/scroll.

**Slots:** default (label content), `start` (leading icon/content), `end` (trailing icon/content),
plus Shoelace aliases `prefix` → `start` and `suffix` → `end`.

In a constrained button the default label ellipsizes and each adornment wrapper is capped at 40%
of the control. Fixed icons remain visible while unbroken labels or metadata cannot widen the row.

**CSS parts:** `base` (compatibility name for the internal control; use `button`),
`button` (the internal native `<button>`, or an `<a>` when `href` resolves to a safe link; it is
the same node as `base`), `label`, `start`/`prefix` (the same wrapper), `end`/`suffix` (the same
wrapper), `caret` (the decorative dropdown chevron, present only while either caret spelling is
set), `spinner` (present only while `loading`).

**CSS custom states:** `disabled` (including fieldset-disablement and loading), `icon-button`
(one text-free default-slot element), `link` (safe anchor mode), and `loading`.

**Themeable custom properties.** The colour slots below are re-pointed at the active `variant`'s row
of the library's shared semantic colour grid, so the component carries no `:host([variant='…'])`
block of its own — the ones marked variant-independent are the exceptions:

- `--lr-button-accent` (default `--lr-color-fill-loud`) — text/glyph colour for the chrome-less
  tiers (`outlined`, `plain`, `link`), i.e. the variant's loud fill borrowed as a foreground.
  `variant="neutral"` is the one exception: its loud fill is a mid grey picked to carry _light_
  text, so reusing it as dark-on-surface text would wash out every plain and link button — neutral
  keeps `--lr-color-text` instead.
- `--lr-button-fill` (default `--lr-color-fill-quiet`) and `--lr-button-on-fill` (default
  `--lr-color-on-quiet`) — the `appearance="filled"`/`"filled-outlined"` fill and its
  guaranteed-legible foreground.
- `--lr-button-accent-fill` (default `--lr-color-fill-loud`) and `--lr-button-accent-on-fill`
  (default `--lr-color-on-loud`) — the same pair for `appearance="accent"`, the default tier. The
  accent fill is also that tier's border colour.
- `--lr-button-border` (default `--lr-color-border-normal`) — the border colour, from the active
  variant's row.
- `--lr-button-outlined-border` (default `--lr-color-border-strong`) — the border colour of
  `appearance="outlined"` _and_ `"filled-outlined"`, overriding `--lr-button-border`. Deliberately
  variant-independent.
- `--lr-button-outlined-fill` (default `transparent`) — the `appearance="outlined"` background, also
  variant-independent. Set it to tint an outlined button (a faint surface wash behind the outline)
  without a `::part(base)` rule, and point `--lr-button-hover-base` at the same colour so the hover
  and press states keep moving away from what is actually painted.
- `--lr-button-quiet-text` (default `--lr-color-text-quiet`) and `--lr-button-quiet-border` (default
  `--lr-color-border`) — the `appearance="quiet"` foreground/border pair, variant-independent too.

Hover and press are **colour mixes, not a filter** — `--lr-button-hover-base` (default
`--lr-color-surface`) is the colour both move away from, and each painted tier re-points it at the
fill it actually paints (`--lr-button-fill` for `filled`/`filled-outlined`, `--lr-button-accent-fill`
for `accent`); the chrome-less tiers paint nothing, so they mix from the page surface.
`--lr-button-hover-background` (default `color-mix(in oklab, var(--lr-button-hover-base),
var(--lr-color-mix-partner) var(--lr-color-mix-hover))`) is the hovered background and
`--lr-button-active-background` the same mix at the stronger `--lr-color-mix-active` share, so a
press reads as more than a hover. `appearance="link"` moves its text colour by those two shares
instead of taking a background. `--lr-button-hover-color` and `--lr-button-hover-border` are the
text/border counterparts, letting e.g. `appearance="quiet"` (which has its own resting
`--lr-button-quiet-text`/`-border`) theme its hover state independently. Both are **undeclared by
default**, falling back to whatever colour/border the active `appearance` already paints at rest —
every appearance's current hover paint is unchanged until one is set.
`appearance="link"` ignores `--lr-button-hover-color` (its own hover rule sets a higher-specificity
colour mix) and renders with no border at any state, so `--lr-button-hover-border` has no visible
effect there. **Breaking in 8.0.0:** this replaced `--lr-button-hover-brightness`,
which no longer exists — a `filter: brightness()` multiplies every channel, so it moved a mid-toned
fill but did nothing at all to a pure white or pure black one, and it dimmed the label and icons
along with the box. Retuning `--lr-button-fill` or `--lr-button-accent-fill` now retunes that tier's
hover and press with it.

`--lr-button-width` (default `100%`) is the internal control's inline size, so it follows the host's
own width; override it to `auto` for a compact inline composition. `--lr-button-active-scale`
(default `0.9875`) is the `:active` press-scale, dropped under `prefers-reduced-motion`.
`--lr-button-spinner-duration` (default `var(--lr-transition-ambient)`, i.e. `1.8s ease-in-out`) is
the `loading` spinner's rotation period; that token itself collapses to `0.001ms linear` under
`prefers-reduced-motion`, so the spinner effectively stops.

The per-`size` `min-block-size` floors are `--lr-button-size-2xs`, `--lr-button-size-xs`,
`--lr-button-size-s`, `--lr-button-size-m`, `--lr-button-size-l` and
`--lr-button-size-xl`. Each defaults to the matching tier of the shared form-control ladder
(`--lr-form-control-height-2xs` … `-xl`, i.e. 1.25rem, 1.5rem, 1.875rem, 2.5rem, 3rem, 3.5rem), so a
button shares the same minimum-height ladder as sibling form controls rather than relying on a
second hand-maintained list — which is exactly how the scales drifted apart before 8.0.0. This is
density/floor parity, not guaranteed pixel-height parity: content and nested actions can make a
composed control such as `lr-date-input` taller. Each token is read only by its own tier
(`--lr-button-size-s` also serves `size="small"`, and so on for the other two aliases), and all are
ignored by `appearance="link"`.
Retheming `--lr-theme-form-control-height-*` moves every control on the ladder together.
Circle and automatically detected icon-only buttons add the shared `--lr-icon-button-size` floor
on both axes, so the compact `2xs`/`xs` tiers cannot collapse those standalone targets below 40px.
Ordinary single-line labelled buttons keep the exact ladder heights above unless their content
requires more room.

`--lr-button-gap` (default `--lr-form-control-gap`, the gap between the icon/label and any slotted
content) does not vary by tier. `--lr-button-radius` (default `--lr-form-control-radius`, the corner
radius) _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. Both are inheritable and retunable without a
`::part(base)` rule; `appearance="link"` ignores the radius (it renders with zero), and `pill`
changes its private default to `--lr-radius-pill`. `--lr-button-caret-size` (default
`var(--lr-size-0-75em)`) is the `with-caret`
chevron's font size — declared in `em`, so it tracks every `size` tier through the button's own font
size instead of needing a per-tier value.
`--lr-button-shadow` is **undeclared by default**, so `box-shadow` falls back to `none` —
byte-identical to before this property existed — set it to add a drop shadow (e.g. an
elevated/floating action button) without a `::part(base)` rule. `appearance="link"` always renders
with no shadow regardless of this token — a zero-chrome inline link has no box to elevate.
`--lr-button-justify` (default `center`) is the `justify-content` of the internal button's row. With
the label no longer growing, this is what positions the icon+label pair inside a stretched control:
`flex-start` packs it against the leading edge, `space-between` pushes the adornments apart.
`--lr-button-label-grow` (default `0`) is the `flex-grow` of `[part="label"]`; `1` restores the
pre-16.0.0 behaviour where the label absorbed every spare pixel of a stretched button, and it also
overrides the automatic grow a `with-caret`/`end`-adornment button applies.

**Retuning one `size` tier's geometry, without a `::part(base)` rule.** Four more properties carry
the active tier's geometry. Every `:host([size='…'])` rule changes only private defaults — no
per-tier rule redeclares a public hook or styles `[part='base']` directly. An inherited or direct
public value therefore retunes whatever tier is active (e.g. pinning a `size="s"` button into a compact toolbar row), the same
pattern `lr-input`/`lr-select`/`lr-combobox`/`lr-segmented`/`lr-date-input` follow. Each defaults to
the shared ladder's value for the active tier, which at the default `m` tier resolves to the values
in brackets:

- `--lr-button-padding-block` (default `--lr-form-control-padding-block`; `--lr-space-xs` at `m`)
- `--lr-button-padding-inline` (default `--lr-form-control-padding-inline`; `--lr-space-m` at `m`)
- `--lr-button-font-size` (default `--lr-form-control-font-size`; `--lr-font-size-m` at `m`)
- `--lr-button-min-height` (default `--lr-form-control-height`) — the active tier's `min-block-size`
  floor. Its private default follows that tier's own `--lr-button-size-*` token, and it is used as the
  fallback when `--lr-button-height` is unset.
- `--lr-button-height` — an **exact** height (both floor and cap), for pinning the button to a
  fixed toolbar row. **Undeclared by default**, so the button keeps the active tier's floor and an
  `auto` height; see "exact-height hatches" under `lr-input`.

`appearance="link"` ignores all five: it is declared after the `size` rules and resets padding,
font, and both height properties with literals, so an inline link can never take a button-shaped
box no matter what tier or override is in play.

**Optional peer deps:** none.

```html
<!-- appearance defaults to "accent": the loud fill, for the one primary action in a view. -->
<lr-button variant="brand">Save</lr-button>
<!-- "filled" is the quiet tint of the same tone, for a secondary action beside it. -->
<lr-button variant="brand" appearance="filled">Save a copy</lr-button>
<lr-button appearance="plain" aria-label="Close dialog"
  ><svg slot="start">...</svg></lr-button
>
<p>
  The message failed.
  <lr-button appearance="link" variant="brand">Retry</lr-button>
</p>

<lr-button pill with-caret aria-haspopup="menu" aria-expanded="false"
  >Actions</lr-button
>
<lr-button variant="primary" outline caret
  ><span slot="prefix">★</span>Migrated</lr-button
>
<lr-button circle aria-label="Settings"
  ><svg aria-hidden="true">...</svg></lr-button
>

<form action="/save" method="post">
  <lr-input name="title" label="Title" required></lr-input>
  <lr-button
    type="submit"
    name="intent"
    value="draft"
    formnovalidate
    formaction="/save-draft"
  >
    Save draft
  </lr-button>
  <lr-button type="submit" name="intent" value="publish">Publish</lr-button>
</form>
```

**Known gotchas:**

- `accessibleLabel`/a host `aria-label` is forwarded reactively to the internal button or anchor as
  a literal string (for an icon-only button). Host `aria-describedby` targets in the host's root
  are resolved onto the focused internal control through `ariaDescribedByElements`; external
  `aria-labelledby` is not copied across the shadow boundary.
- Host `aria-haspopup` and `aria-expanded` values are forwarded to the internal semantic control.
  For toggle buttons, host `aria-pressed="true|false|mixed"` reaches the focused native button —
  BUTTONS only. A link button (`href` set) never receives it, because `role="link"` does not support
  `aria-pressed`; remove `href` and the same host attribute starts reaching the `<button>` that
  replaces the anchor. That link carve-out arrived in **16.0.0** — before it, `aria-pressed` was
  forwarded onto the anchor as well, asserting a state `role="link"` does not have. Navigation can
  use the global `aria-current="page|step|location|date|time|true|false"`, which does reach the
  anchor. Both states update reactively, including native `ariaPressed`/`ariaCurrent` property
  assignments, attribute removal and `href` changes. Empty or unsupported tokens are omitted. The native button/link role
  remains unchanged for an enabled control: use pressed state for button toggles and current state
  for navigation links. A DISABLED link button drops `href` (so it genuinely cannot navigate) and
  therefore also drops the anchor's implicit role, so it renders an explicit `role="link"` — without
  it, the forwarded `aria-label`/`aria-haspopup`/`aria-expanded`/`aria-current` would sit on a
  role-less generic element, which is prohibited.
  For host `aria-controls`, targets in the host's own root are resolved through the reflected
  element-reference API so a popup relationship survives the component's shadow boundary; browsers
  with that API expose the relationship through `ariaControlsElements` and intentionally serialize
  the `aria-controls` content attribute as an empty string. Browsers without the API retain the
  forwarded string attribute as a best-effort fallback. A reflected element list and a non-empty
  serialized string cannot coexist; this is what lets either button serve as an `lr-menu` trigger.
- Is form-associated (`static formAssociated = true` + `attachInternals()`), so it participates in
  an ancestor `<form>.elements` the same way `wa-button` does — a sibling text field's own
  Enter-to-submit lookup (which scans `form.elements` for a `type === 'submit'` control) finds it.
- **`SubmitEvent.submitter` is not this element** whenever `name`/`value` or any `form*` override is
  set: it is the transient native `<button>` described above. That native submitter remains
  connected throughout synchronous `requestSubmit()` handling, so a `submit` listener can read
  its `name`, `value` and `form*` fields and construct `FormData` with it. It is removed after
  submission handling returns; its identity is distinct from the `lr-button` host.
- The `form*` overrides and `type` are all inert while `href` renders the anchor — native navigation
  is the activation there, and an anchor has no submit/reset concept.

---
