* 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>
* fix(playback): show the Up next card near the real end of an episode
The card appeared a fixed 8 minutes before the end: most of a short
episode, and far into the story of a long one, with no way to hide it.
- Adaptive lead: 4% of the episode, clamped to 40 s … 3 min.
- A closing-credits chapter in the last third, when timeline segments
carry one, brings the card forward to its start (capped at 5 min).
- Close button and Escape dismiss the card for the current next episode.
- After 10 s (not while hovered) the card collapses into a one-line pill.
- Seconds countdown in the last minute.
- New playback setting "Up next card" (default on) turns it off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): address Up next card review feedback
- Offer the Up next card setting for Embedded MPV only under the
frame-copy engine; native view never mounts the shared controls.
- Hovering pauses the collapse delay instead of restarting it.
- Document that the card stays visible when the controls auto-hide.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): restart the Up next collapse delay for a new episode
- A card that stays mounted while its next episode changes gives the new
item the full delay instead of the previous item's leftover.
- The setting description no longer promises credit-based timing: no
current series path supplies chapters yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Series playing in fullscreen get the same slide-in side panel the live channel list has, with season tabs and the episode list: rest the mouse on the left edge, click it, or press C; pick an episode and it plays inline without leaving fullscreen.
- Panel contract: `FullscreenChannelPanelHost` gains optional `panelSearchEnabled` and `panelKind`; the template context gains `open`. Pointer/keyboard rules and the four live providers are unchanged.
- Series host: `PortalInlinePlayerComponent` provides the token through `createEpisodePanelHost()` and stamps `app-fullscreen-episode-panel` (SeasonTabsComponent over rows with TMDB still or numeral tile, label, runtime, clamped overview, progress, watched check, now-playing marker; playing row centred on open). Episode clicks reuse the Up Next rail's inline path; season tab clicks reach the hosts' `onSeasonSelected` (Xtream TMDB season enrichment, Stalker lazy VOD load with a Retry row after a failed request).
- Gates: `Settings.fullscreenChannelPanel` (label now covers both lists in all locales), episode content only, native-view Embedded MPV withheld by the view, external players excluded.
- Inline-series e2e moved to `xtream-series-playback.e2e.ts` with shared Xtream helpers in a fixture; adds a fullscreen episode switch through the panel.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>