# Frontend accessibility & interaction-model audit — YellowJacket

Scope: `frontend/src/components/**`, `frontend/src/services/keyboard-shortcut-service.ts`,
`frontend/index.html`, `frontend/index.ts`, `frontend/index.css`, `frontend/src/styles/tokens.css.ts`.
Read-only; nothing was changed.

Already confirmed by hand and **not** re-reported: track rows / sidebar `<li>` not focusable,
14 tab stops app-wide, closed queue panel still focusable, global Space/arrow/S/N/P hijack,
`data-shortcut-scope` never set. Adjacent consequences of those are marked *(adjacent)*.

---

## Critical

**1. `frontend/src/components/config-page/config-section.ts:98-104` — the entire Settings page is unreachable by keyboard**
The disclosure header is a bare `<div class="header" @click=${this.toggle}>` with no `<button>`,
no `tabindex`, no `role`, no `aria-expanded`, no `aria-controls`. Sections default to
`expanded = false` (line 84/88), so every setting in the app is behind a control that cannot be
tabbed to or activated.
*Symptom:* a keyboard or screen-reader user can open Settings and see nothing but collapsed
headings they can never expand.
*Fix:* make the header a `<button type="button" aria-expanded=${this.expanded} aria-controls="body">`
and give the body an `id`.

**2. `frontend/src/components/downloads-view/downloads-view.ts:258-271` — tab switching is mouse-only and has no tab semantics**
`<div class="tabs">` containing two `<div class="tab" @click>`; no `role="tablist"`/`role="tab"`,
no `aria-selected`, no `tabindex`, no arrow-key handling, no `aria-controls` on the panel.
*Symptom:* the Downloads tab of the Downloads view can never be reached without a mouse; AT
announces two unlabelled generic containers.
*Fix:* `role="tablist"` on the wrapper, `<button role="tab" aria-selected=... aria-controls=...>`
per tab with roving tabindex.

**3. `frontend/src/components/track-list/track-list.ts:1967`, `frontend/src/components/queue-panel/queue-panel.ts:1543`, `frontend/src/components/cover-grid/cover-grid.ts` — context menus have no menu semantics, no focus, no keyboard**
`<div class="context-menu-panel">` holds `wa-dropdown-item`s inside a raw `<wa-popup>`. The items do
carry `role="menuitem"` (Web Awesome sets it — verified in
`node_modules/@awesome.me/webawesome/dist/chunks/chunk.MCDD6PFW.js`), but the container has no
`role="menu"`, so the menuitems are orphaned. Because they are in a bare `wa-popup` rather than a
`wa-dropdown`, nothing moves focus into the menu, nothing handles Up/Down/Escape, and nothing
restores focus on close. The menu only opens on `contextmenu` (mouse right-click); there is no
Shift+F10 / Menu-key path.
*Symptom:* Play, Add to Queue, Play Next, Add to Playlist, Favourite and Track Details are
completely unavailable without a mouse — this is the only path to most of those actions.
*Fix:* wrap in `role="menu"`, open on `keydown` Shift+F10/ContextMenu, focus the first item, handle
Arrow/Escape/Tab, restore focus to the originating row on close.

**4. `frontend/src/components/autotag-view/autotag-view.ts:2824-2950` — four hand-rolled modal dialogs with no dialog semantics, no focus trap, no focus restore**
`renderPasteDialog` (2824), `renderWarningDialog` (2856), `renderLeaveDialog` (2891),
`renderSearchDialog` (2922) each render `<div class="dialog-overlay"><div class="dialog">` with no
`role="dialog"`, no `aria-modal="true"`, no `aria-labelledby` pointing at the `<h3>`, and no focus
management. Only the paste and search dialogs set `autofocus`; the Warning and Leave dialogs — the
two that gate an **irreversible on-disk metadata rewrite** — leave focus wherever it was.
*Symptom:* a screen-reader user is never told a dialog opened, can Tab straight out of it into the
page behind, and can confirm "this rewrites audio files" without ever hearing the warning.
*Fix:* use `<wa-dialog>` (which already does `showModal()` + activeElement restore — see
`chunk.ZUIYLL2X.js`), or add role/aria-modal/labelledby + a Tab trap + focus save/restore.

**5. `frontend/src/components/autotag-view/autotag-view.ts:1706-1746` — bare single-letter shortcuts on `document`, including a destructive one, with an incomplete guard**
`A` = Apply (rewrites tags on every track on disk, explicitly "not automatically reversible" per the
warning copy at 2866-2872), `S` = Skip, `L` = Leave as-is, `U`/`F` = dialogs. The suppression check
at 1707-1712 only tests `tagName === 'INPUT' | 'TEXTAREA' | isContentEditable`. Events originating
inside a Web Awesome control's shadow DOM are retargeted to the host (`WA-SELECT`, `WA-INPUT`,
`YJ-COMBOBOX`), so the guard passes and `A` fires while the user is typing. Buttons, checkboxes and
`<select>` are likewise unguarded — pressing `S` on a focused `<select>` triggers Skip *and* jumps
the option list.
*Symptom:* typing an artist name into a Web Awesome field, or type-ahead on a select, silently
rewrites metadata on an entire album.
*Fix:* reuse `isTextInputFocused` from `keyboard-shortcut-service.ts` (which resolves through shadow
roots via `getDeepActiveElement`) and require a confirm/modifier for `A`.

**6. `frontend/src/components/search-bar/search-bar.ts:166-174` and `frontend/src/components/explore-view/explore-view.ts:1317-1323` — clear buttons have no accessible name at all**
Both are `<button class="clear-button">` containing only `<wa-icon name="xmark">`. No `aria-label`,
no `title`, no text. (A systematic scan of every `<button>` in `components/**` found these two as the
only truly unnamed controls; the rest have text or at least a `title` fallback.)
*Symptom:* announced as "button" with no name; unusable via voice control.
*Fix:* `aria-label="Clear search"`.

**7. `frontend/src/components/top-results-row/top-results-row.ts:267` — result cards are click-only divs**
`<div class="card" @click=${() => this.handleClick(r)}>` — the only `role`/`tabindex`/`keydown`-free
card renderer in the codebase (every other card view added at least `role="button" tabindex="0"`).
*Symptom:* the top-results row on the Explore page cannot be activated by keyboard.
*Fix:* `role="button" tabindex="0"` + Enter/Space handler, matching `home-view.ts:305-309`.

**8. `frontend/index.html:34` + `frontend/index.ts:263-275` — queue toggle has no state, and the closed panel is not inert** *(adjacent)*
The button carries `aria-label="Toggle queue"` but never `aria-expanded` or `aria-controls`. The
toggle just adds/removes the `open` attribute; the closed state is purely
`:host { width: 0; overflow: hidden }` (`queue-panel.ts:214-217`), which hides nothing from the
accessibility tree.
*Symptom:* the button never reports open/closed, and a screen-reader's virtual cursor walks the
entire queue (title, artist, remove button for every track) while the panel is visually closed.
This is the same root cause as the already-confirmed "closed queue panel is still focusable".
*Fix:* set `aria-expanded`/`aria-controls` on the button and `inert` (or `aria-hidden="true"` plus
`visibility: hidden`) on the panel when closed.

---

## Major

**9. `frontend/src/components/track-list/track-list.ts:1906-1926` — column headers are not headers and never expose sort state**
`<div class="header-row">` with `<div class="header-cell" @click>` per column. No `role="grid"`/
`row`/`columnheader`, no `aria-sort`, no `tabindex`, no keydown. The sort direction is conveyed only
by a `▲`/`▼` glyph in a `<span class="sort-arrow">` at 10px (`track-list.ts:900-901`).
*Symptom:* AT cannot tell which column the list is sorted by or in which direction, and clicking a
header to sort is mouse-only. (There is a redundant keyboard-reachable sort dropdown at 1806-1841,
so this is not a total loss of function.)
*Fix:* `role="columnheader" aria-sort=${'ascending'|'descending'|'none'}` on each header cell and
make it a `<button>`.

**10. `frontend/src/components/track-list/track-list.ts:1746-1755` — the per-row favourite toggle is an unlabelled, unfocusable div**
`<div class=${classMap({'fav-icon': true, favorited: isFav})}>` with an inline `<svg>` and
`cursor: pointer` (`track-list.ts:1034-1043`); the click is delegated off the virtualizer. No
`role`, no `tabindex`, no accessible name, no `aria-pressed`.
*Symptom:* favouriting a track from the list is mouse-only, and the current favourite state of every
row is invisible to AT (heart/star fill is a shape-and-colour change with no text equivalent).
*Fix:* `<button role="switch" aria-checked=${isFav} aria-label="Favourite ${track.TrackName}">`.

**11. `frontend/src/components/queue-panel/queue-panel.ts:1417` + `cover-grid.ts:1798`, `album-dropdown.ts:385`, `app-sidebar.ts:222-232` — drag-and-drop has no keyboard equivalent anywhere**
Queue reordering (`draggable="true"` on `.track-item`, drop index computed from cursor Y at
`queue-panel.ts:1093-1140`), album→queue/playlist drag, expanded-album track drag, and drop-on-nav-item
are all pointer-only. There is no Alt+Up/Down reorder, no "move to…" command, and no `aria-grabbed`/
`aria-dropeffect` substitute.
*Symptom:* queue order can never be changed without a mouse. Combined with finding 3 (the context
menu is mouse-only too), there is **no** keyboard path to add a track to the queue or a playlist.
*Fix:* add Alt+ArrowUp/Down reorder on the focused queue item, and expose the drag targets as
context-menu commands once the menu is keyboard-reachable.

**12. No `aria-live` region anywhere for async status — scan/job progress, toasts, search results, now-playing**
A repo-wide grep finds exactly one live region: `catalog-scope-notice.ts:110` (`role="status"`), and
even that is conditionally rendered *with* its content already present, which most ATs do not
announce. Specific gaps:
- `frontend/src/components/config-page/config-page.ts:2137-2139` — `<div class="toast">` with no
  `role="status"`/`aria-live`; it is the only feedback that a setting saved or failed, and it
  auto-dismisses after a timer (1174-1176).
- `frontend/src/components/jobs/job-indicator.ts:359-370` — the trigger label swings between
  "Scanning Music", "3 background jobs" and "Finished" with no live region.
- `frontend/src/components/now-playing/now-playing.ts:340-357` — track title/artist change on every
  auto-advance with no announcement.
- `frontend/src/components/explore-view/explore-view.ts:1270-1278` — "Searching…" and the error
  block are silent.
- `frontend/src/components/track-list/track-list.ts:1901`, `1930-1933` — "Loading tracks…" /
  "No tracks match your search." with no `aria-live` and no `aria-busy` on the list.
*Symptom:* a screen-reader user gets no feedback that a scan started or finished, that a setting
saved, that a search returned nothing, or that the track changed.
*Fix:* one `<div role="status" aria-live="polite" class="sr-only">` per surface, populated after the
region already exists in the DOM.

**13. `frontend/src/components/artists-view/artists-view.ts:1059-1063` and `frontend/src/components/genres-view/genres-view.ts:947-951` — `aria-selected` on `role="button"` is invalid and dropped**
Both cards render `role="button" aria-selected="${isSelected}"`. `aria-selected` is only valid on
`gridcell`, `option`, `row`, `tab` and `treeitem`; on `button` it is ignored outright. These grids
are genuinely multi-select (ctrl/shift-click via `SelectionController`).
*Symptom:* selection state — the thing the whole ctrl/shift interaction exists to produce — is
invisible to AT; visually it is a background-colour change only.
*Fix:* `role="listbox" aria-multiselectable="true"` on the grid, `role="option" aria-selected` on
the cards.

**14. `frontend/src/components/combobox/combobox.ts:288-303` — combobox has no `aria-controls` / `aria-activedescendant`**
`role="combobox" aria-expanded aria-autocomplete="list"` on the input, `role="listbox"` on the `<ul>`,
`role="option"` on the `<li>`s — but no `id` on the listbox, no `aria-controls`, no
`aria-activedescendant`, and no `id` on the options. `aria-selected` is used to mean "highlighted"
(302), not "chosen".
*Symptom:* arrowing through suggestions moves the visual highlight but announces nothing; the user
hears only their own typing.
*Fix:* give the listbox and each option an `id`, add `aria-controls` and
`aria-activedescendant=${optionId(highlightedIndex)}`.

**15. `frontend/src/components/now-playing/now-playing.ts:203-212, 391-408` — marquee text auto-scrolls with no reduced-motion guard and no pause**
`transition: transform var(--scroll-duration, 5s) linear` re-armed in a loop by
`onScrollCycleEnd`; when `scrollMode === 'always'` (persisted in localStorage, line 388-395) the
title and artist scroll continuously for as long as the track plays. Only four files in the repo
have a `prefers-reduced-motion` guard (`job-indicator.ts:126`, `job-row.ts:154`,
`autotag-view.ts:471,599`) and this is not one of them.
*Symptom:* WCAG 2.2.2 — moving content longer than 5s with no mechanism to pause it, and a
vestibular-trigger risk with no reduced-motion opt-out.
*Fix:* `@media (prefers-reduced-motion: reduce) { .scroll-content { transition: none } }` and treat
`always` as `never` under that query.

**16. `frontend/src/components/config-page/config-page.ts:2091-2131` — the "Remove Library" confirmation is not a dialog**
`<div class="cancel-dialog-overlay">` / `<div class="cancel-dialog">` with a
`<div class="cancel-dialog-title">` — no `role="dialog"`, no `aria-modal`, no `aria-labelledby`, no
focus move, no focus trap, no Escape handler, no focus restore. This gates deleting tracks,
playlists and queue entries.
*Symptom:* the destructive confirmation is never announced and can be Tab-escaped.
*Fix:* same as finding 4 — `wa-dialog`, or role + trap + restore.

**17. `frontend/src/components/jobs/job-indicator.ts:378` — `role="dialog"` on an unmanaged popover**
The panel declares `role="dialog"` (and the trigger `aria-haspopup="dialog"`, line 362) but nothing
moves focus into it, traps Tab, handles Escape, or restores focus. It is a non-modal popover, not a
dialog.
*Symptom:* AT announces a dialog that never receives focus and cannot be dismissed by keyboard;
tabbing past the trigger lands in the page behind while the panel is open.
*Fix:* drop `role="dialog"` (use `role="group" aria-label="Background jobs"` and
`aria-haspopup="true"`), or implement real dialog behaviour.

**18. `frontend/src/components/explore-view/explore-view.ts:1289-1305` — search-mode "tabs" convey the active mode by colour class only**
`<button class="search-mode-tab ${this.searchMode === 'catalog' ? 'active' : ''}">` — no
`role="tab"`/`aria-selected`, no `aria-pressed`, no text or icon difference between active and
inactive.
*Symptom:* the user cannot tell whether they are searching the catalog or lyrics.
*Fix:* `aria-pressed=${this.searchMode === 'catalog'}` (or a proper tablist).

---

## Minor

**19. `frontend/src/styles/tokens.css.ts:18-22` — the entire type scale is hardcoded px**
`--yj-text-xs: 11px` … `--yj-text-xl: 18px`, consumed by essentially every component. Combined with
~50 further literal `font-size: Npx` declarations (e.g. `job-indicator.ts:138` at **9px**,
`explore-view.ts:518` at 10px, `track-list.ts:901` at 10px, and inline
`style="font-size: 12px"` at `queue-panel.ts:1518` and `playlist-view.ts:1865`).
*Symptom:* text-only resize (WCAG 1.4.4) does nothing — a user who raises their OS/browser font size
sees no change. 9-11px body text is below any reasonable floor to begin with.
*Fix:* express the scale in `rem` so it tracks the root font size.

**20. `frontend/src/components/track-list/track-list.ts:972-985` and `frontend/src/components/queue-panel/queue-panel.ts:164-166` — fixed row heights with `contain: strict`**
`.track-row { height: 33px; contain: strict }` and the matching virtualizer `_itemSize`
(`track-list.ts:222`, `queue-panel.ts:165`, 49px). `contain: strict` clips overflow rather than
growing the row.
*Symptom:* any increase in text size (finding 19, or a user stylesheet) clips row text mid-glyph
instead of reflowing; the virtualizer's scroll math also desynchronises.
*Fix:* out of scope for a quick change, but at minimum document that the type scale and `_itemSize`
are coupled.

**21. `frontend/index.css:12-20` — the app shell is `height: 100vh; overflow: hidden`**
`body { height: 100vh; grid-template: "top-bar top-bar" 4em ... "bottom-bar bottom-bar" 4em; overflow: hidden }`.
*Symptom:* at high zoom the 4em bars grow while the viewport does not, and anything that no longer
fits is clipped with no scrollbar — WCAG 1.4.10 Reflow. The bottom bar's
`grid-template-columns: var(--now-playing-width, 200px) 1fr auto` keeps a fixed 200px column while
its text scales.
*Fix:* allow the shell to scroll (`min-height: 100vh` + `overflow: auto`) below a breakpoint.

**22. `frontend/src/components/track-list/track-list.ts:1000-1017` — "now playing" and "selected" rows are colour-only**
`.track-row.active { background-color: var(--yj-accent-bg); color: var(--yj-accent) }` and
`.track-row.selected { background-color: var(--yj-selection-bg) }`; the row markup
(`track-list.ts:1736-1745`) carries no `aria-current`, `aria-selected` or non-colour marker.
*Symptom:* WCAG 1.4.1 — a colour-blind user cannot distinguish the playing row, and AT has no signal
at all. Same pattern in `queue-panel.ts:1406-1409`.
*Fix:* add a ▶ marker (or the existing play icon) to the active row and `aria-current="true"` once
rows carry `role="row"`.

**23. `frontend/src/components/jobs/job-indicator.ts:150-156, 369` — the failure indicator is a bare 6px red dot**
`<span class="alert-dot">` with `background: #ff6b6b` and no text, `aria-label` or `title`; the
trigger's own name (`title="Background jobs"`, 363) does not change when it appears.
*Symptom:* "a background job failed" is communicated by colour alone and not at all to AT.
*Fix:* `<span class="alert-dot" role="img" aria-label="A background job failed"></span>`.

**24. Ellipsis truncation without `title` in the highest-density lists**
`text-overflow: ellipsis` appears in 40+ places. `cover-grid.ts:1821,1832` and `home-view.ts:308`
do add `title`; these do not:
- `frontend/src/components/queue-panel/queue-panel.ts:389,401` (`.track-title`, `.track-artist`)
  vs. the markup at 1422-1428 — no `title`.
- `frontend/src/components/track-info/track-info.ts:92,100` vs. markup at 118-126.
- `frontend/src/components/track-list/track-list.ts:1018-1022` (`.cell`) vs. `1782-1788`.
- `frontend/src/components/playlist-view/playlist-view.ts:355,360`.
*Symptom:* long titles are clipped with no way to read the full value — acute in the queue panel,
whose width is user-resizable down to `MIN_WIDTH`.
*Fix:* `title=${value}` on the truncating element.

**25. `frontend/src/components/jobs/job-row.ts:270-272` — progress bar has no accessible name**
`<wa-progress-bar value=...>`; Web Awesome renders `role="progressbar"` + `aria-valuenow`
(`chunk.WDFK5BNW.js:42,47`) but no label is supplied.
*Symptom:* announced as an unnamed "progress bar, 45%" with no indication of what is progressing.
*Fix:* `aria-label=${job.title}` (or WA's `label` attribute).

**26. `frontend/src/components/search-bar/search-bar.ts:157-163` and `explore-view.ts:1308-1314` — search inputs are labelled by placeholder only**
No `aria-label`, no `<label>`, no `role="searchbox"`, no `aria-describedby` pointing at the result
count.
*Fix:* `aria-label="Search library"` / `"Search catalog"`.

**27. `frontend/src/components/sidebar/app-sidebar.ts:202-241` — nav list has no landmark or item role** *(adjacent)*
`<ul>` of `<li>` with `aria-current` (219) but no `role`, so `aria-current` sits on a
non-interactive item and the whole thing is not inside a `<nav>` (`frontend/index.html:22` is a
plain `<div class="sidebar">`).
*Fix:* `<nav aria-label="Main">` in `index.html` and make each item a `<button>`/`<a>` — which also
resolves the already-confirmed focusability gap.

**28. Mouse-only resize handles with no keyboard equivalent**
`app-sidebar.ts:200`, `queue-panel.ts:1447`, `now-playing.ts:377`, and the track-list column
resizers at `track-list.ts:1945-1953` are all `@mousedown`-only `<div>`s with no `role="separator"`,
`tabindex` or arrow-key handling.
*Symptom:* panel and column widths cannot be adjusted without a mouse. Low impact (cosmetic
preference), but the pattern repeats four times.

---

## Polish

**29. `frontend/index.html:14-16` — heading hierarchy skips h1 → h3**
`<h1 class="title">` immediately followed by `<h3 class="subtitle">`, styled at `0.8em`
(`index.css:52-55`) — using a heading level for type size.
*Fix:* make the subtitle a `<p>`.

**30. `frontend/index.html` — no skip link**
`<main id="main-content">` exists (line 26) but nothing links to it, so keyboard users traverse the
top bar and sidebar on every navigation.
*Fix:* add a visually-hidden `<a href="#main-content">Skip to content</a>` as the first body child.

**31. `frontend/src/components/cover-grid/cover-grid.ts:509` — `<img>` with no `alt`**
The only `alt`-less `<img>` in the codebase (every other one is either descriptive or correctly
`alt=""`).
*Fix:* `alt=""` if decorative.

**32. `frontend/src/components/queue-panel/queue-panel.ts:1431-1437` — per-row remove button is named by `title` only, and the name is not unique**
`title="Remove from queue"` on every row provides an accname fallback, but it never identifies
*which* track and is invisible to touch users.
*Fix:* `aria-label="Remove ${track.title} from queue"`.

**33. `frontend/src/components/cover-grid/cover-grid.ts:1793-1797` — every album card is `tabindex="0"`** *(adjacent)*
`role="button" tabindex="0"` on each virtualised card means the tab sequence length equals the number
of rendered cards, with no roving tabindex. This is the opposite failure mode to the confirmed
"only 14 tab stops" finding and will surface as soon as the other views are made focusable.
*Fix:* roving tabindex (one `tabindex="0"`, the rest `-1`) once the grid gets `role="listbox"` per
finding 13.

**34. `frontend/src/components/track-list/track-list.ts:900-901` — 10px sort arrow**
`font-size: 10px; /* intentionally sub-token: tiny sort indicator */` — the comment acknowledges it.
Combined with finding 9 (no `aria-sort`), the sort direction is a 10px glyph or nothing.

---

## What is already correct

- **`frontend/src/components/audio-player/controls/player-controls.ts:121-148`** — every transport
  button has an `aria-label`, shuffle and repeat carry `aria-pressed`, and repeat's three-state mode
  is spelled into the label (`Repeat: one`) rather than left to the CSS class. This is the model the
  rest of the app should follow.
- **`frontend/src/components/audio-player/seekbar/seek-bar.ts:160-168`** and
  **`volume-control.ts:198`** — `wa-slider` with `aria-label` and a `valueFormatter`, so the seek
  position is announced as `3:42` rather than `222`.
- **All five `wa-dialog` usages are genuinely modal and restore focus** — `track-details.ts:735`,
  `duplicate-tracks-dialog.ts:278`, `download-picker.ts:180`, `phantom-resolver.ts:927`,
  `first-run-wizard.ts:170`. Web Awesome's dialog uses native `showModal()`, `lockBodyScrolling` and
  `activeElement` restore (`chunk.ZUIYLL2X.js`), and every one of them passes a `label`. The
  hand-rolled dialogs in findings 4 and 16 are the outliers, and both have a working component to
  migrate to.
- **`frontend/src/components/explore-artist-details/explore-artist-details.ts:2152, 2178, 2201, 2327, 2457`**
  — every disclosure toggle is a real `<button>` with `aria-expanded`, and the CSS keys off the
  attribute (`:465, :520, :680`) rather than a duplicate class. This is exactly the pattern
  `config-section.ts` (finding 1) is missing.
- **`keyboard-shortcut-service.ts:73-83, 106-121`** — `getDeepActiveElement` correctly walks the
  shadow-root chain and `isTextInputFocused` covers `contentEditable` and the empty-`type` input
  case. The suppression logic is sound; the problems the parent already found are in *what* it does
  with the result, not in the resolution itself. Finding 5 is the autotag view failing to reuse it.
- **`library-status-indicator.ts:186-196`** — status is conveyed by three distinct icons *and* a
  full sentence in both `title` and `aria-label`, and `handleKeydown` (175-180) stops Enter/Space
  from double-firing on the wrapping card. Correct on every axis.

---

## Residual risks / not covered

- Colour-contrast ratios were not measured (no rendering); the token palette
  (`--yj-text-tertiary: #888` on `--yj-bg-surface: #212529` ≈ 4.1:1) is borderline for the 11-12px
  text it is most often paired with, but that needs a real measurement.
- `templ`-rendered HTMX fragments in `backend/config/` were out of scope and are not audited.
- WebKit2GTK-specific behaviour (whether Ctrl+= page zoom is even reachable in the Wails shell, and
  how Orca traverses lit-virtualizer's windowed DOM) can only be confirmed on a running app.
