A bare <img> sent a Referer that Google's image CDNs refuse and had no retry and no fallback, so a
Home shelf card showed the browser's broken-image mark. ui/RemoteImage gathers what Avatar, the
channel banner and the download rows had each hand-rolled: no Referer, up to three retries past the
browser's negative cache, the caller's fallback (by default the card's empty frame) instead of the
broken mark, the failure remembered by URL. Every picture now goes through it; a lint rule refuses
a bare <img>.
- Download row: answers to its own width (container query); below 40rem
it stacks: picture beside a two-line title, the meta line across the
row (one line, never wraps: zoomed on the iPad its pieces broke apart),
then the source link and the actions on one wrapping line. The paste
field takes a line of its own when the buttons would squeeze it.
- YT playlist row: below 42rem the length rides on the picture and the
title runs two lines over the channel.
- Channels tag row: the suggestions take a line of their own below
40rem (the chip lane was 0 px and the row's menu off the screen).
- Pager: on a phone the arrows drop their words (names kept), and the
size label stays one line.
- Lint: a class split where the interpolation is the variant itself
(${NARROW}:w-24) is refused too; it had shipped with no CSS.
buttonStyles/chipStyles glued the hover guard on at runtime (`${ACT}hover:text-fg`). Tailwind emits CSS
only for a class it finds whole in the source, so all 22 guarded hovers shipped with no rule: no text or
edge colour on hover for ghost/outline/danger buttons, no white layer on the player's control row, no
underline on a link button, no accent hover on token/suggest/ghost chips. The stylesheet had 0 rules
for `not-aria-disabled`.
- `@custom-variant act` (index.css): the same guard as the state layers; `:disabled` never matches an
<a>, so one variant serves buttons and links and the per-element variant maps go.
- ui/tailwindScan.contract.test.ts: every class the primitives can emit stands whole in their source.
- a lint rule refuses a variant right after an interpolation; the `enabled:hover:` rule now points at
`act:hover:`.
- F16: --rating (theme-scoped gold, 4.5:1 on the page and its chip), --rating-media, --live/--live-fg,
--media-chrome replace 14 palette literals; a lint rule refuses palette classes; the theme contract
measures them.
- The Plex player's notices drew over its open settings/tracks popovers: notices z-base, popovers z-menu.
- The player's info overlay rendered theme tokens on its always-dark panel: on a light theme the score
(2.6:1), collection years (3.2:1) and cast initials (1.05:1) were unreadable; onDark variants now.
- The lint-rule contract tests share one ESLint with a one-off warm-up (a cold first lint took 8 s,
past vitest's 5 s).
A FocusSafeButton that holds the focus is disabled by aria-disabled, not the native attribute, so
every hover gated on `:disabled` / `enabled:` alone still fired on it — the glass lift, the primary /
danger / warn dim, and eight `enabled:hover:` chains (saved views, the player's list buttons, the
undo toolbar). Measured on localdev: the Pager's Next, focused at the last page, now keeps its resting
border and shadow under the pointer. The CSS selectors and the chains also exclude
`[aria-disabled="true"]`, and a lint rule refuses a bare `enabled:hover:` from now on.
- `overflow-clip` gets a safelist anchor in index.css (its only occurrence is a `.ts` constant),
and the chip-lane contract keeps it there.
- PlexPlayer's Backspace ownership rule is pinned on all three branches: nothing above, a foreign
layer above (it defers), and its own panel open under a layer (it still closes the panel).
- Removing a playlist item bumps the list's `updated_at`, like the three other item-only writes.
- Seen in a real browser on localdev: an exhausted comfort-shelf tile (an empty pinned playlist) is
focusable, aria-disabled, and its Shift+F10 menu offers Play off and Remove from shelf.