## What a screen reader says

Every control on the phone is named by what it does, in the app's own
words, and where the desktop already has a word for something it is the
word used here too — Seek, View album artwork, Close artwork viewer. An
icon-only control carries an `aria-label`; one with visible text is named
by the text it shows.

The nav is a tab set: `role="tablist"` on the bar, `role="tab"` and
`aria-selected` on the five destinations, and the count badge round
seventeen added is part of the tab's own name — "You, 3 transfers",
"Community, 2 unread" — rather than a digit read out before the word it
belongs to. The badge itself is `aria-hidden`, because the number is in
the name now.

The two sheets are dialogs: `role="dialog" aria-modal="true"`, the player
sheet named for what is playing, the half sheet reading its own first
title back out of the DOM so that twenty-seven kinds of sheet need no list
of names to keep in step. Opening one puts the focus somewhere sensible —
the player sheet on its Close, the half sheet on its grab handle, which is
a real button now ("Close this sheet") and the first dismissal a screen
reader could ever reach; the scrim behind it never was one.

A sheet that is down is `inert` and `aria-hidden`. Neither sheet is
unmounted — they are translated off the bottom of the screen, and the half
sheet deliberately keeps its last body rendered through the slide down —
so before this the closed sheets' rows, the hidden mini bar and the nav
under an open sheet were all still in the accessibility tree, and TalkBack
could swipe into every one of them.

There is one live region, `.sr-live`, mounted for the life of the app and
clipped to a single pixel, because a region that appears with its words
already inside it is announced reliably by nobody; an announcement here is
a change of text in a node that was always there. That is what carries an
error and a notice, so an Undo offer is actually read out — and the Undo
button itself stays reachable, only the sentence beside it is hidden from
the tree so it is not said twice.

Artwork that opens the viewer is a control, not decoration. Artwork that
is decoration carries `alt=""` and every icon is `aria-hidden`, so the
tree never reads a filename out. The waveform is `role="slider"` with a
value and arrow keys where it can seek, and `aria-hidden` on the mini bar
where it is only a picture of the position. Switches say on or off, choice
chips say pressed, and the two long descriptions and the gap line say
expanded.

The check, so this does not quietly rot: the walk's probe sweeps every
interactive element in the live DOM and counts four things — the ones with
no accessible name (`a11yAnon`, with `a11yWho` naming the first three
offenders so one run tells you what to add), the ones whose name is junk
(`a11yBad`: bare digits, a filename, a lone glyph, an essay), the ones
sitting in a layer that is off-screen but still in the tree (`a11yGhost`)
and the ones that look chosen without saying so (`a11yState`). All four
are asserted at zero on Home, the library, a record, a catalog record, the
artist page, You, the taste page, a half sheet, the player sheet and the
viewer — and the anonymous count on a room and on both shapes of prompt
besides, because a room's message box and the prompt's own field are where
the fields this round finally named actually live: assert it only where
they are not and the round guards everything except its own work.

One known exception, left alone on purpose: the per-row ⋯ is a
`role="button"` inside the row's own `<button>`, and the mini bar's three
transport buttons sit inside its `role="button"` the same way. That is
invalid ARIA, and both ends are named so nothing is anonymous; unpicking
it means splitting `.trk` (one flex row with a whole-row `:active`) and
rewriting the walk's `.trk .r` selectors, for no gain a screen reader
would notice.

