Accessibility
ChatScroll from @prepared911/ui/chat-scroll.role="log" with aria-live="polite" and aria-relevant="additions", named by label. A message appended to the list is announced once, in order. additions rather than the default additions text keeps a node and its text from matching twice, which some screen readers speak twice. Edits to the text of an existing message are not announced.Scrollable's reverseOrder seats a short thread at the bottom and puts the loading row at the top without reordering the DOM.loadingRow or errorRow renders, and for two animation frames after it goes away, the log is aria-live="off". Assistive technology reads a tree change after the frame that painted it, so the second frame is the grace that keeps the prepend quiet. Remove the row in the same update that adds the messages.Scrollable makes it a focusable region named by label, so the arrow keys, Page Up and Down, Home and End scroll it. The jump control is an ordinary Button after the log in tab order, and it is outside the live region, so it appearing is not announced. The exiting control is inert.tabindex="-1" target) so focus never falls to the page. The list draws the shared focus ring, inset, after a keyboard activation; it appears instantly, with no transition.unreadCount is above zero, jumpUnreadLabel(count) names the control (for example "Jump to latest 3"). Otherwise jumpToLatestLabel names it.Button keeps its system-color edge, and Scrollable keeps the system scrollbar.CONTRACT.md.overlay slot is a sibling of the log, so it is not announced as traffic. The jump control stays above it and out of its way, and the fade is a decorative background. Under forced colors the fade is a system canvas fill.