* fix(ui): visible keyboard focus everywhere via a global focus-visible fallback
The global stylesheet removed the outline from every input, button,
textarea and `:focus`, so Tab users saw no focus on about 110 raw buttons:
detail actions, season tabs, chips, title results, list rows.
- Remove the outline only under `:focus:not(:focus-visible)` and draw a
fallback ring on every other `:focus-visible` element. Both rules sit
in `:where()`, so any component that styles its own focus still wins.
- One recipe: `focus-ring-declarations` moves to
`libs/ui/styles/_focus-ring.scss` and reads `--app-focus-ring`, declared
per theme (light #1d63e0, dark #8cbaff) with at least 3:1 on every app
surface and selection tint; `m3-theme.spec.ts` measures it. The card
ring, the detail actions, the portal sidebar and both context panels
use the mixin (the panels' selection-blue ring fell to 2.97:1 on the
active item).
- Material Tab stops (buttons, switches, button toggles, checkboxes) take
the ring over their 12% focus state layer; menu items and options keep
Material's highlight.
- Surfaces over video (player controls, vendor chrome, fullscreen panels,
Up Next rail) point the ring at the player's text colour.
- The selected season tab drew its border with `outline`, which outranks
the fallback; it is a spread shadow now.
- Guard: `pnpm run styles:focus-visible:validate` (CI) rejects a global
`outline: none` on an unscoped selector and a missing fallback.
- Electron E2E `keyboard-focus-ring.e2e.ts` tabs through the detail
actions and season tabs, a catalog grid with its refinement chips, the
Sources list and Settings in both themes: one ring per stop, 3:1 where
measurable, none after a click.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): keep the player ring in the fullscreen panel; tighten the focus guard and E2E
Local review round 1 (Codex P2, Greptile 3×P2):
- The fullscreen channel panel carries `dark-theme`, whose context declares
the theme ring again, so its own controls ringed in #8cbaff. Point the
token back at the player's text colour on `.fullscreen-channel-panel
.dark-theme`; the web series-playback E2E checks the close button's ring.
- The guard took a container-scoped rule (`.panel :focus-visible`), a mixin
body or a media query as the global fallback. The fallback is now the
whole selector, outside any at-rule.
- The keyboard-focus E2E now fails a ring that an `overflow: hidden`
ancestor (up to the scroll container) cuts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): show focus where a component removes its outline; catch invisible rings in the guard
GitHub review round on #1866 (Codex P2, Greptile P2):
- A component rule that sets `outline: none` outranks the zero-specificity
fallback. Re-audited every one: two hid a Tab stop with nothing else to
show. The EPG list row (`role="button"`) now draws an inset ring, and the
command palette underlines its search row while the field has focus.
The others already show focus on the field's wrapper, on another
element, or as a highlight inside an arrow-key composite (the "…" menu,
the guide's search listbox); the guidelines now state the rule.
- The guard read only a bare zero or `none` as a removal. It now reads a
zero width, a `none`/`hidden` style or a transparent colour anywhere in
the shorthand or longhands, so `outline: 0 solid transparent` is
reported and `outline: 2px solid transparent` is no fallback.
- The keyboard-focus E2E walks the live EPG list and the command palette
too, and reads an inset or offset shadow line as a ring.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(player): keep --pc-text a literal palette colour
Master's palette spec (#1854) reads `--pc-text` from the controls host as
a literal; this branch had made it `#{palette.$text}`, failing "sets the
timeline label on the dense glass" on the merge ref. The host declares the
literal again, and a spec keeps the palette's `$text` (the focus ring
token for surfaces beside the host) equal to it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* build(epg): declare the ui-styles dependency of the list row's focus ring
The EPG list row now `@use`s `libs/ui/styles/_focus-ring.scss`; Nx cannot
infer a Sass import, so `ui-epg` declares `ui-styles` like the other
consumers of the shared partials.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(tooling): a styleless outline shorthand hides focus too
`outline: 2px` or `outline: red` resets the style to `none`, yet the guard
counted either as a visible fallback. A shorthand without a drawn style
(or a `var()` that may carry one) now counts as removing the outline, as
do `initial` and `unset`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(portals): posters-only cover wall for movie and series grids
Add `Settings.showCoverTitles` (Settings > General, default on). Turning it
off drops the title row under VOD/series covers in catalog, favorites and
recent grids and reveals the title as a bottom-gradient overlay on hover and
keyboard focus, pinned open for items whose cover is missing or failed.
`CoverTitlesService` is the single resolver: the opt-out AND a hover-capable
pointer, so touch-only devices keep their titles. Live channel grids, search
results, "recently added" rails and dashboard rails always keep labels.
Catalog and collection cards become keyboard buttons (role, tabindex,
aria-label, Enter/Space, focus ring) and poster alt text is the title. The
default-on boolean coercion moves into `settings-opt-out.util.ts` because
the settings store reached the max-lines limit.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(portals): keep nested Remove key presses from activating the card
Enter/Space on the content card's nested Remove button bubbled into the
card's own key handlers: Enter opened the item before removing it and
Space opened it while cancelling the removal. Only keys pressed on the
card element itself now activate it. Regression spec added.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(portals): keep cover titles while an in-section search filters the grid
The posters-only wall exempts search results because they are identified
by the name the user typed; the category grid's own in-section filter is
the same case, so `app-grid-list` now keeps the title row while its
`searchTerm` is non-blank. Contract docs updated, regression spec added.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(portals): keep cover titles while the collection tab search is active
The unified favorites/recent tab filters by its own search term, so its
matches are identified by name like every other search result. The tab
now opts its cards out of the posters-only wall while the term is
non-blank. Contract docs updated, regression spec added.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(portals): move the card Remove control out of the button surface
An interactive control nested inside a role="button" is an invalid
accessibility structure. The content card's activation surface is now its
own inner element and the Remove button a sibling positioned over the
poster corner, labelled by its tooltip text. Spec asserts the control is
never a descendant of the button.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(portals): draw the collection card focus ring where it is not clipped
The card's overflow: hidden clipped an outline drawn on the inner
activation surface on every edge, so keyboard users saw no focus
indication. The ring now sits on the outer card via
:has(> .content-card__activation:focus-visible).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(portals): detect any hover-capable pointer for the posters-only wall
`hover` describes only the primary pointer, so a touch-first tablet with
a mouse or hover-capable stylus attached lost the wall. The resolver now
reads `(any-hover: hover)`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>