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

# `lr-multi-split`

- **Import** `import '@aceshooting/lyra-ui/components/lr-multi-split.js';` (stable tag alias; registers the tag)
- **Class** `LyraMultiSplit`, also available unregistered from `@aceshooting/lyra-ui/components/layout/multi-split/multi-split.class.js`
- **Family** `components/layout/` — see `llms/index.md` for its siblings
- **Status** `stable` since `9.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** 3 parts, 8 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-multi-split`

Resizable panels for dashboard layouts. Direct **light-DOM children are the panels**; a divider is
auto-inserted between each adjacent pair. Panels participating in persistence carry a unique,
nonempty, whitespace-stable `panel-id`; this business identity stays independent from the platform
`id` attribute and is never rewritten.

Feasible `minPx` floors also constrain flex shrinking after the divider target gutters take their
allocation: a 600px split with a 40px divider can render panels of 300px and 260px. If the floors
cannot fit, the remaining panel space is shared proportionally instead of overflowing. The budget
follows allocation and the actual divider geometry, including font-relative tokens resolved in the
divider's own font context. Panel font sizes do not change that gutter budget; stored percentages,
initialization precedence, and resize event values retain their existing meaning.

Granular import: `@aceshooting/lyra-ui/components/layout/multi-split/multi-split.js`.
The Lyra-original v9 identity migration is mechanical: `lr-split` → `lr-multi-split`,
`LyraSplit` → `LyraMultiSplit`, generic container authoring types → the corresponding
`LyraMultiSplit*` names, identity-specific `lr-split-*` events → `lr-multi-split-*`, and
`--lr-split-*` hooks/storage keys → `--lr-multi-split-*`/`lr-multi-split:*`. The separate mirrored
`lr-split-panel`, its `LyraSplitPanel` class, `SnapFunction`, and `SNAP_NONE` are unchanged.

With a definite block size, each direct panel owns a native `overflow: auto` scroll surface.
Unconstrained panels have a zero block minimum, so long content stays inside the split rather than
escaping into following content. Set `overflow` directly on an individual panel when its content
needs a different scrolling surface.

**Properties:**

- `sizes: number[] = []` (attribute: false — percentages per panel, auto-computed equally if
  omitted/mismatched)
- `defaultSizes: (number | string)[] = []` (attribute: false) — initialization-only fallback: a
  valid restored `storageKey` layout wins first; otherwise a valid `defaultSizes` wins over equal
  distribution. Initialization occurs on the first update so framework property bindings committed
  after connection in the same turn — including `defaultSizes` and `storageKey` — are honored
  before the layout becomes live. Later reassignment never overwrites live drag/persisted state —
  set it once, at mount. Each entry is either a plain **number** (percent-of-container, validated unchanged: a
  pure-number array that does not sum to ~100, e.g. `[30, 60]`, is still rejected and falls through
  to the equal split) or a CSS **length string** (`'200px'`, `'20%'`, `'3rem'`). When at least one
  entry is a length string, every entry is resolved through the public contextual
  `resolveCssLength()` utility (`%` against the measured container, `rem`/`em` against the
  owner-document/host, and `vw`/`vh` against the owner window), then normalized to percentages
  before the same validation applies — so `['200px', '300px']` on a 500px container initializes to
  `[40, 60]`.
- `min: number = 10` (min % per panel)
- `orientation: 'horizontal'|'vertical' = 'horizontal'` (reflected) — the axis used at/above
  `orientationBreakpoint` (or always, when that's unset).
- `orientationBreakpoint?: number | string` (attribute `orientation-breakpoint`) — opt-in inline-size
  breakpoint measured on `[part='base']`; unset (the default) means no behavior change at all, and
  no `ResizeObserver` is armed. Below it, `narrowOrientation` becomes the effective axis instead of
  `orientation`. Accepts a bare pixel number (`900`, `orientation-breakpoint="900"`) or a CSS length
  string: `'900px'`, `'56.25rem'`, `'3em'`. Under the default `orientationBreakpointBasis='container'`,
  `rem` resolves against the document root's **computed** font size (the rule a `@container` query
  follows) and `em` against this element's own computed font size. The length is **re-resolved on
  every measurement**, never cached at first render, so a root font-size change moves the crossing
  width with no invalidation step on the consumer's side. Anything that isn't a resolvable length
  behaves exactly as unset (no observation, no `data-effective-orientation`): `''`, `'auto'`,
  garbage, a non-finite number, and deliberately `%`, `vw`/`vh` and `calc()` — a viewport-relative
  threshold would mix reference boxes against a measurement of the element's own allocation. For a
  genuinely viewport-relative breakpoint set `orientationBreakpointBasis='viewport'` instead.
- `orientationBreakpointBasis: 'container'|'viewport' = 'container'` (reflected, attribute
  `orientation-breakpoint-basis`) — which box `orientationBreakpoint` is compared against. Unset,
  behavior is identical to before this property existed. `'container'` measures this component's own
  `[part='base']` via `ResizeObserver` and compares strictly `<`. `'viewport'` evaluates
  `matchMedia('(max-width: <breakpoint>)')` and arms no `ResizeObserver` for the orientation feature
  at all, so the shared observer stays armed only for `collapse`; its comparison is inclusive (`<=`),
  because that is what native `max-width` means — deliberate, so the crossing point matches a CSS
  `@media` rule authored with the same length exactly.
  **`rem`/`em` mean different things under the two bases, and this is the reason `'viewport'` exists.**
  Inside a media query, relative units resolve against the browser's _initial_ font size and ignore
  any `html { font-size }` override; under `'container'` they resolve against live computed font
  sizes. The two agree unless an app re-points the root font size. So when the breakpoint must stay
  in lockstep with a CSS `@media` rule, `'viewport'` is the exact match — the browser evaluates the
  same query, live, across browser zoom and user font-size preferences, with no px re-derivation.
- `narrowOrientation: 'horizontal'|'vertical' = 'vertical'` (reflected, attribute `narrow-orientation`)
- `effectiveOrientation: 'horizontal'|'vertical'` (readonly getter) — the live resize/layout axis
  actually in effect; identical to `orientation` whenever `orientationBreakpoint` is unset or
  doesn't resolve to a length. Also reflected as `data-effective-orientation` (only present while
  `orientationBreakpoint` resolves to a usable length).
- `storageKey?: string` (attribute `storage-key`) — persists a versioned list of `{ panelId, size }`
  records to `localStorage` under `` `lr-multi-split:${key}:panels` ``. Every direct panel must have
  a unique, nonempty, whitespace-stable `panel-id`; a missing, surrounding-whitespace, or duplicate identity fails persistence closed without
  disabling the live split. Restores and same-instance membership reconciliation follow `panelId`,
  so a reorder/replacement never transfers a saved size to a different business panel.
- `panelConstraints: (LyraMultiSplitPanelConstraint | null)[] = []` (attribute: false) — `LyraMultiSplitPanelConstraint { minPx?:
number; maxPx?: number; minPercent?: number; maxPercent?: number }`, index-aligned with `sizes`; a
  `null`/missing entry (or an omitted bound within an entry) leaves that side of that panel purely
  percent-based (the existing `min`-only behavior). Combining a px and a percent bound on the same
  side resolves to the stricter one (larger for min, smaller for max) via a native CSS `max()`/`min()`
  in the rendered `clamp()` flex-basis. `sizes`, the `lr-resize` payload, and localStorage persistence
  stay percent-based regardless — only the effective drag/keyboard clamp bounds (and the rendered
  `flex-basis`, via a native CSS `clamp()` across container resizes) change for a constrained panel.
  Pixel minimums also account for live divider dimensions. Bounds on neighboring panels remain
  active during collapse transitions, including when constraints are updated from an
  `lr-multi-split-collapse-change` handler.
- `collapse: 'start'|'end'|'none' = 'none'` (reflected) — opt-in responsive collapse for one panel:
  `'start'`/`'end'` is a _logical_ position (RTL-aware, matching CSS logical properties — the panel at
  the document's visual leading/trailing edge, not a raw array index). `lr-multi-split` only owns the
  width-collapse mechanics/state signaling below; it renders no icon-only UI itself — the collapsing
  panel's own slotted content is expected to adapt itself (e.g. via its own `@container` query reading
  the panel's clamped width or `data-collapse-state`, see below).
- `railWidth: string = '3.5rem'` (attribute `rail-width`) — the fixed CSS length the collapsing panel
  clamps to in `'rail'` state.
- `railBreakpoint: number | string = 640` (attribute `rail-breakpoint`) — below this width, the
  collapsing panel switches from its normal drag-resizable percent width to the fixed `railWidth`.
  Accepts a bare pixel number (`640`, `rail-breakpoint="640"` — the original form) or a CSS length
  string: `'640px'`, `'68.75rem'`, `'3em'`. Under the default `collapseBreakpointBasis='container'`
  it is compared against this component's own measured `[part='base']` inline size (a
  `ResizeObserver`, active only while `collapse !== 'none'`), and `rem` resolves against the
  document root's **computed** font size while `em` resolves against this element's own; the length
  is re-resolved on every measurement, never cached. Anything the grammar rejects — `''`, `'auto'`,
  garbage, a non-finite number, and deliberately `%`, `vw`/`vh`, `calc()` and `var()` — falls back
  to the `640` default rather than switching the feature off (unlike `orientationBreakpoint`, this
  breakpoint has a documented default to fall back to). A negative length is floored at `0`, i.e.
  never crossed. Must stay above `floatBreakpoint` — an inverted pair is sanitized by raising this
  one to match, collapsing the `'rail'` band away rather than reporting a wide container as
  collapsed.
- `floatBreakpoint: number | string = 400` (attribute `float-breakpoint`) — below this narrower
  width, the collapsing panel instead becomes an absolutely-positioned overlay ("floating card") on
  top of the other pane(s), removed from the normal flex flow; the sibling(s) take the full width.
  Same accepted forms, basis, and sanitization as `railBreakpoint`; an unparseable value falls back
  to the `400` default.
- `collapseBreakpointBasis: 'container'|'viewport' = 'container'` (reflected, attribute
  `collapse-breakpoint-basis`) — which box `railBreakpoint`/`floatBreakpoint` are measured against.
  Unset, behavior is identical to before this property existed. `'container'` observes this
  component's own `[part='base']` inline size via `ResizeObserver` and compares strictly `<`;
  `'viewport'` evaluates `matchMedia('(max-width: <breakpoint>)')` for each of the two thresholds,
  which is inclusive (`<=`) — native `max-width` semantics, deliberately, so the crossing point
  matches a CSS `@media` rule authored with the same length exactly. Use `'viewport'` to collapse in
  step with a page-level responsive layout (a shell whose own `@media` rules restack at the same
  width) rather than with this split's own allocation; it is also what lets the browser resolve a
  `rem` breakpoint with real `@media` semantics (against the _initial_ font size, ignoring an
  `html { font-size }` override). Both bands are classified from both queries together on every
  change, so a fast resize crossing both thresholds at once still lands on one correct state and
  fires `lr-multi-split-collapse-change` once; under `'viewport'` the first paint is already correct — no
  `ResizeObserver` round-trip — and that initial state is not announced as a transition.
- `collapseState: 'wide'|'rail'|'floating'` (reflected, attribute `collapse-state`) — a public
  accessor with force/auto semantics mirroring `<lr-app-rail>`'s `mode`: normally derived
  automatically from the measured container width, but assigning it a concrete value pins it there
  (stopping automatic breakpoint tracking) — useful for a consumer-driven toggle (e.g. a button that
  forces `'floating'` regardless of width). Assigning the write-only `'auto'` sentinel releases the
  pin and immediately re-derives the state from the current measured width; the getter never returns
  `'auto'`. This is an **effective** state: while `collapse='none'` or fewer than two panels exist,
  the getter/reflected attribute stays `'wide'` and forced rail/floating intent produces no event,
  marker, backdrop, focus trap, or scroll lock. Enabling an eligible pane can make retained forced
  intent effective; disabling/removing it transitions back to `'wide'`, closes `open`, releases
  overlay ownership, and restores focus.
- `open: boolean = false` (reflected) — whether the `'floating'` collapse state's drawer is shown.
  While `collapseState` is `'floating'` and `open` is `false` (the default), the collapsing panel
  renders nothing (`hidden`, out of the accessibility tree) instead of the always-visible overlay
  card this state rendered before `open` existed — not just visually hidden; this holds even
  against an author `display` rule targeting the panel directly. Setting `open = true` reveals it as a
  focus-trapped floating panel with a `[part="backdrop"]` scrim; Escape or a backdrop click set
  proposes a cancelable close before changing `open`. While open, the floating panel is the modal root and every sibling pane
  behind it is inert. Leaving `'floating'` while `open` is still `true` also closes it, the same
  way `<lr-app-rail>` closes its mobile overlay when leaving `'mobile'` while open.
- `releasePinOnBreakpoint: boolean = false` (attribute `release-pin-on-breakpoint`, reflected) — opts a
  pinned `collapseState` in to releasing itself when the layout it was made for is gone: either the
  measured collapse band changes to a different one than the pin was made in, or
  `effectiveOrientation` crosses `orientationBreakpoint`. The pin is dropped exactly as if `'auto'`
  had been assigned and the state re-derives from the current measurement, firing
  `lr-multi-split-collapse-change` when that is a real transition. Re-measuring the same band never
  releases a pin, so ordinary resizing inside one band leaves it alone. Left unset (the default), a
  pin survives every band and orientation change until a consumer writes `'auto'` — the pre-existing
  behavior.

**Methods:** `expandPane()`, `collapsePane()` and `togglePane()` drive the collapse feature
semantically, each picking the mechanism the pane's *current band* provides rather than its pinned
state: inside the `floatBreakpoint` band that is the overlay drawer (`open`), above it it is the
`collapseState` pin (`'wide'` expanded, `'rail'` collapsed). `togglePane()` reads the pane's current
presentation — `'wide'` counts as expanded, `'rail'` as collapsed, `'floating'` as expanded exactly
while `open` — so toggling a pane pinned to `'rail'` after the container narrowed into the floating
band opens the drawer instead of doing nothing visible. All three are no-ops while `collapse='none'`
or fewer than two panels exist, none creates a pin the band already produces, and a pin one of them
cancels is released rather than replaced, so an expand/collapse cycle leaves automatic breakpoint
tracking as it found it. They emit no `lr-toggle`, matching the existing rule that a direct `open`
write does not. They are named `…Pane()` rather than `expand()`/`collapse()`/`toggle()` because
`collapse` is already the pane-selection property, following `<lr-page>`'s `showNavigation()` trio.
The component still renders no trigger of its own — wire these to your own control.

`collapse`'s three resulting states — `'wide'` (default, today's plain layout) / `'rail'` / `'floating'`
— are exposed as: a `data-collapse-state` attribute on both the host and the collapsing panel element
itself (absent for `'wide'`/`collapse="none"`); and the `lr-multi-split-collapse-change` event below. The
divider adjacent to the collapsed panel is drag/keyboard-disabled (`aria-disabled="true"`) while
collapsed. `collapse="none"` (the default) is byte-for-byte identical to pre-collapse-feature behavior.

`dividerLabel?: (index: number, panelCount: number) => string` (attribute: false) customizes the
localized accessible label generated for each auto-inserted divider.

**Events:** `lr-resize-request` (cancelable; `detail: { sizes }` is the proposed constrained size
array from a divider drag or keyboard step. Call `preventDefault()` to leave `sizes` and its
persisted layout unchanged. It is not emitted when a consumer assigns `sizes` directly or a
keyboard/pointer proposal clamps to the already-current sizes),
`lr-resize` (non-cancelable; the same `detail: { sizes }`, emitted after an accepted drag movement
or keyboard step commits. A genuine pointer gesture has one terminal persistence write on
`pointerup`; no-move, fully clamped, vetoed, canceled, and lost-capture gestures have none. Pointer
release emits no additional event; direct `sizes` assignments stay silent),
`lr-multi-split-collapse-change` (`detail: { state: 'wide'|'rail'|'floating' }`, fired only
on a real `collapse`-state transition, never on every resize/render. It fires *after* the collapsing
panel is decorated for the new state — its `data-collapse-state` marker, the closed drawer's `hidden`
flag and its owned inline sizing are all applied first — so a listener can read the panel
synchronously inside its own handler instead of deferring past `updateComplete`. Focus is also moved
out of a pane the new state hides (`'floating'` while closed) or clamps (`'rail'`) before the event
fires, landing on the first surviving pane that can take it, otherwise on the split's own divider;
focus anywhere other than the collapsing pane is untouched),
`lr-toggle` (`detail: LyraMultiSplitToggleDetail = { open: boolean }`) — Escape/backdrop close
proposals are cancelable and fire before `open` changes; preventing the event or making a synchronous
reentrant mutation aborts the proposal. A forced close when a responsive collapse transition leaves
`floating` fires noncancelably after `open` is false. Direct `open` writes and no-op dismissals are silent,
`lr-multi-split-constraints-invalid` (`detail: LyraMultiSplitConstraintIssueDetail`, fired once when the configured
panel minimums/maximums cannot fit the track; the infeasible set is rejected for interaction and a
normalized percent minimum is used instead), `lr-multi-split-orientation-change` (`detail: { orientation }`,
fired only when an enabled `orientationBreakpoint` actually changes `effectiveOrientation`)

**Slots:** default (each direct child element is one panel; set a unique `panel-id` on every child
when `storage-key` is used).

**CSS parts:** `base` (`position: relative`, so the `'floating'` state can anchor to it), `divider`
(carries `aria-disabled="true"` and is drag/keyboard-inert while its adjacent panel is collapsed),
`backdrop` (the `'floating'` drawer's scrim — only rendered while `collapseState === 'floating'` and
`open`)

**Themeable custom properties:** `--lr-multi-split-overlay-color` (default `var(--lr-color-overlay)`) —
the `'floating'` drawer's `[part='backdrop']` scrim; scoped to `[part='base']`, not the viewport.
`--lr-multi-split-divider-target-size` (default
`max(var(--lr-icon-button-size), var(--lr-size-3px))`) — the real flex track/gutter reserved for the
divider along the resize axis. The visual rule is painted in its center; no pseudo-element
extends into either adjacent panel, so slotted controls retain pointer ownership up to their edge.
Set it on an ancestor to retune a split subtree or directly on one component; either public value
remains authoritative.
`--lr-multi-split-divider-thickness` (default `var(--lr-size-3px)`) sets the painted hairline's own
width, independent of `--lr-multi-split-divider-target-size` above — retuning one never changes the
other, so the WCAG 2.5.8 pointer target can never be shrunk by a thinner or thicker visual line.
`--lr-multi-split-divider-color` (default `var(--lr-color-border)`),
`--lr-multi-split-divider-hover-color` (default `var(--lr-color-brand)`), and
`--lr-multi-split-divider-active-color` (default
`color-mix(in oklab, var(--lr-color-brand), var(--lr-color-mix-partner) var(--lr-color-mix-active))`)
theme the divider hairline's resting/hover/pressed color; the active color is only reachable while a
resize gesture is pressed (pointer capture holds `:active` for the whole gesture).
`--lr-multi-split-floating-panel-inline-size` overrides the `'floating'` collapse state's overlay
card `inline-size`, which otherwise mirrors its own live `sizes[i]` percent (i.e. what it renders
at in the `'wide'` state, so un-floating never jumps). Unset, the rendered geometry is unchanged;
set (e.g. on an ancestor), it wins over that percent at ordinary specificity, with no `!important`
needed against the inline style the component rewrites on every render.
`--lr-multi-split-floating-panel-inset` (default `0`) sets the `'floating'` drawer's distance from
`[part="base"]`'s edges, applied to both block insets and to whichever logical inline edge
`collapse` anchors the drawer to, so one declaration insets all three anchored edges; the free inline
edge stays governed by the panel's own width. Unset, the drawer is flush with its container exactly
as before.
Otherwise shared tokens only.

**Optional peer deps:** none.

```html
<lr-multi-split storage-key="dashboard-main" min="15">
  <div panel-id="navigation">Panel A</div>
  <div panel-id="content">Panel B</div>
  <div panel-id="inspector">Panel C</div>
</lr-multi-split>
```

Keyboard: focus a divider (`Tab`), then `ArrowRight`/`ArrowLeft` (horizontal) or
`ArrowDown`/`ArrowUp` (vertical) to resize by a fixed 2% step — RTL-aware for horizontal layouts
(under `direction: rtl`, the forward/backward keys and drag-delta sign both swap so they still track
the visually-adjacent panel). Home and End jump directly to that divider's current achievable
minimum and maximum, including per-panel px/percent constraints.

**Known gotchas:**

- Panel membership tracks the complete ordered direct-child identity sequence, not just its count.
  Same-count `replaceWith()` and DOM reorder immediately reconcile visual order with DOM/AX order
  while preserving sizes by `panel-id`; count changes retain existing panels' relative proportions
  and allocate an equal share to each new identity. Without a complete unique `panel-id` set, the
  live layout retains the positional fallback but persistence is deliberately disabled. Every temporary
  inline layout/collapse declaration, `hidden`, and `data-collapse-state` value is
  snapshot/adopt/restore-owned, so late author writes survive removal, disconnect/adoption, reuse,
  and reconnect without stale multi-split state.
- divider `aria-valuemin`/`aria-valuemax` are computed per adjacent pair from the same resolved
  `panelConstraints` bounds used by pointer and keyboard resizing, rather than a blanket
  `100 - min`. They therefore remain accurate for 3+-panel layouts and for px/percent constraints.
  Each divider also has its own `aria-label` ("Resize divider between panel N and panel N+1")
  distinguishing it from any other divider in a multi-divider layout.
- infeasible aggregate constraints (for example, three panels with `min=40`) are reported through
  `lr-multi-split-constraints-invalid`; interaction rejects that set and uses a normalized percent minimum
  with aggregate slack, so the divider remains operable instead of silently freezing.
- concurrent drags are tracked per `pointerId` (not a single scalar), so a multi-touch drag on two
  different dividers moves both independently instead of the second pointer clobbering the first's
  drag state; `pointercancel`/`lostpointercapture` (not just `pointerup`) both end a drag.
- `localStorage.getItem`/`setItem` calls are now both wrapped in their own `try`/`catch` (in addition
  to the `JSON.parse` result already being caught), so a blocked or quota-exceeded store fails
  silently instead of throwing from inside a `pointerup`/`keydown` handler. A malformed/duplicate
  identity record or layout whose sizes are already below the current `min` floor is rejected rather
  than restored.
- Pointer-drag lifecycle (pointer capture, window-listener cleanup on both drag-end and
  `disconnectedCallback`) is solid and safe to rely on.
- `orientationBreakpoint` shares its `[part='base']` `ResizeObserver` with `collapse` (one observer,
  not two) — arming logic covers either feature being opted into independently.
- **Switching a basis moves the crossing point by exactly 1px.** Container basis compares strictly
  `<` against a measured width; viewport basis asks `matchMedia('(max-width: …)')`, which is
  inclusive (`<=`). So at a breakpoint of `640`, a container-basis split is still `'wide'` at 640px
  while a viewport-basis one has already collapsed. This is deliberate on both sides: `<` is the
  right comparison for "how much room do I actually have", and `<=` is what a CSS `@media` rule with
  the same length does, which is the whole point of the viewport basis.
- `collapseBreakpointBasis='viewport'` does **not** drop the `ResizeObserver` the way the
  orientation feature's viewport basis does. The measured width it feeds is still read by a
  container-basis `orientationBreakpoint` and by the `collapseState = 'auto'` release path, which
  re-derives from the current measured width. Collapse's basis changes only _which values_ the
  classification consults, never whether the split measures itself.
- `railBreakpoint`/`floatBreakpoint` are typed `number | string`. Authored as attributes they read
  back as **strings** (`el.railBreakpoint === '640'`, not `640`) — the same value, a different type.
  Compare with `Number(el.railBreakpoint)` rather than `===`, or assign the property directly when a
  numeric identity matters.
- **Picking a basis.** `orientationBreakpointBasis='container'` (the default) observes the
  component's own allocated inline size, so it fits a component that is the sole flex/grid item in
  the container being measured. It does **not** fit a component sitting beside a fixed-width sibling
  in a row that stacks via a CSS `@media` rule: while the row is a row, this element's width shrinks
  with the viewport; the instant the row stacks (a pure-CSS event no component can observe) it jumps
  to the _full_ row width — wider than it was just before the transition. Because the measured width
  is not monotonic across that transition, no single container threshold both stays wide while the
  row is a row and goes narrow exactly when it stacks. A fixed-width sibling is worse still: its own
  width never changes with the viewport at all, so no container breakpoint on it can react to the
  stacking.
  That layout is what `orientationBreakpointBasis='viewport'` is for. Give every sibling the same
  `orientation-breakpoint` and `orientation-breakpoint-basis='viewport'` and they flip together, in
  lockstep with the CSS rule that stacks the row:
  ```html
  <lr-stepper
    orientation-breakpoint="56.25rem"
    orientation-breakpoint-basis="viewport"
  ></lr-stepper>
  <lr-multi-split
    orientation-breakpoint="56.25rem"
    orientation-breakpoint-basis="viewport"
  ></lr-multi-split>
  <style>
    @media (max-width: 56.25rem) {
      .shell {
        flex-direction: column;
      }
    }
  </style>
  ```
  A consumer-side `matchMedia()` controller driving the `orientation` attribute directly is still
  supported and still correct — it is simply no longer required for this case.

---
