isolate video player against opaque-bg regressions (recurring INC class)

Two-layer defense for the recurring "black screen during playback"
bug class (5+ occurrences in 24h per doc 26/28/30):

L1 (prevention): scope every black-bg rule with
:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) so the rules
self-disable while a player is in the DOM. Covers body,
#reactRoot, .skinBody, .backgroundContainer, .mainAnimatedPage,
.mainAnimatedPages, .pageContainer.

L2 (override): when JS-toggled body.arrflix-video-active is set,
high-specificity (0,3,2 + tag) transparent rule wins against any
ancestor opaque-bg rule (including future regressions someone adds
without scoping). Covers all known wrappers, the
videoPlayerContainer + videoPlayerContainer-onTop, #videoOsdPage,
.libraryPage, .htmlVideoPlayer.

L3 (z-index lift): force .htmlVideoPlayer + child <video> to
z-index:9999 + isolation:isolate during playback so it floats above
any opaque ancestor that still leaks through.

Verified in playwright: with arrflix-video-active + .htmlVideoPlayer
mounted, all 7 ancestors return rgba(0,0,0,0). Without — all 7
return rgb(0,0,0). Self-disabling works.

Lesson reinforced (doc 30 roadmap open): add darkPct assertion to
bin/headless-test-v2.py + xmllint CI gate. Five recurrences without
those gates says we keep relearning this. TODO next.
This commit is contained in:
s8n 2026-05-09 22:18:31 +01:00
parent e9d209da73
commit 452ce68d7a
3 changed files with 21 additions and 9 deletions

View file

@ -53,9 +53,13 @@ CSS = (
".arrflix-nav{text-transform:uppercase;letter-spacing:.08em;font-weight:600;padding:0 .9em;color:#fff!important;text-decoration:none;display:inline-flex;align-items:center;height:100%;font-size:.85em;transition:color .18s ease,text-shadow .18s ease,font-weight .18s ease}\n" ".arrflix-nav{text-transform:uppercase;letter-spacing:.08em;font-weight:600;padding:0 .9em;color:#fff!important;text-decoration:none;display:inline-flex;align-items:center;height:100%;font-size:.85em;transition:color .18s ease,text-shadow .18s ease,font-weight .18s ease}\n"
".arrflix-nav:hover{color:#E50914!important}\n" ".arrflix-nav:hover{color:#E50914!important}\n"
".arrflix-nav.active{color:#E50914!important;font-weight:700;text-shadow:0 0 12px rgba(229,9,20,0.55),0 0 24px rgba(229,9,20,0.25)}\n" ".arrflix-nav.active{color:#E50914!important;font-weight:700;text-shadow:0 0 12px rgba(229,9,20,0.55),0 0 24px rgba(229,9,20,0.25)}\n"
"/* Pure-black background: kill the #101010 stripe that bleeds at page bottom (jellyfin-web/themes/dark/theme.css:44-50 .backgroundContainer + html) */\n" "/* Pure-black background — L1: scope rules to non-video pages so they self-disable when player mounts. Skips when .htmlVideoPlayer or #videoOsdPage is in the DOM. Doc 30 lesson — recurring black-screen-over-video bug class. */\n"
"html,body.arrflix-themed,body.arrflix-themed .backgroundContainer,body.arrflix-themed .skinBody,body.arrflix-themed .mainAnimatedPage,body.arrflix-themed .mainAnimatedPages,body.arrflix-themed .pageContainer,body.arrflix-themed #reactRoot{background-color:#000!important}\n" "html,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)),body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .backgroundContainer,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .skinBody,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .mainAnimatedPage,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .mainAnimatedPages,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .pageContainer,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) #reactRoot{background-color:#000!important}\n"
"body.arrflix-themed .backgroundContainer.withBackdrop{background-color:rgba(0,0,0,.86)!important}\n" "body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .backgroundContainer.withBackdrop{background-color:rgba(0,0,0,.86)!important}\n"
"/* Video isolation L2: high-specificity transparent override gated by JS-toggled body.arrflix-video-active. Beats any ancestor opaque-bg rule (specificity 0,3,2 plus tag) by chaining 2 body classes + the wrapper class. */\n"
"body.arrflix-themed.arrflix-video-active,body.arrflix-themed.arrflix-video-active .backgroundContainer,body.arrflix-themed.arrflix-video-active .skinBody,body.arrflix-themed.arrflix-video-active .mainAnimatedPage,body.arrflix-themed.arrflix-video-active .mainAnimatedPages,body.arrflix-themed.arrflix-video-active .pageContainer,body.arrflix-themed.arrflix-video-active #reactRoot,body.arrflix-themed.arrflix-video-active .videoPlayerContainer,body.arrflix-themed.arrflix-video-active .videoPlayerContainer-onTop,body.arrflix-themed.arrflix-video-active #videoOsdPage,body.arrflix-themed.arrflix-video-active #videoOsdPage .pageContainer,body.arrflix-themed.arrflix-video-active #videoOsdPage .mainAnimatedPage,body.arrflix-themed.arrflix-video-active #videoOsdPage .layout-desktop,body.arrflix-themed.arrflix-video-active .libraryPage,body.arrflix-themed.arrflix-video-active .htmlVideoPlayer{background-color:transparent!important;background:transparent!important;background-image:none!important}\n"
"/* Video element on top of all stacking contexts during playback. */\n"
"body.arrflix-themed.arrflix-video-active video.htmlvideoplayer,body.arrflix-themed.arrflix-video-active .htmlVideoPlayer video,body.arrflix-themed.arrflix-video-active .htmlVideoPlayer{z-index:9999!important;isolation:isolate}\n"
"/* Search input: replace stock cyan focus ring (jellyfin-web/themes/dark/theme.css:262-272) with a borderless slab + red underline on focus, true to the Cineplex/Netflix accent */\n" "/* Search input: replace stock cyan focus ring (jellyfin-web/themes/dark/theme.css:262-272) with a borderless slab + red underline on focus, true to the Cineplex/Netflix accent */\n"
"body.arrflix-themed .searchFields .emby-input,body.arrflix-themed input.searchfields-txtSearch,body.arrflix-themed #searchTextInput{background:#141414!important;border:0!important;border-bottom:2px solid transparent!important;border-radius:2px!important;color:#fff!important;transition:border-color .18s ease,box-shadow .18s ease,background-color .18s ease;padding:.55em .8em!important}\n" "body.arrflix-themed .searchFields .emby-input,body.arrflix-themed input.searchfields-txtSearch,body.arrflix-themed #searchTextInput{background:#141414!important;border:0!important;border-bottom:2px solid transparent!important;border-radius:2px!important;color:#fff!important;transition:border-color .18s ease,box-shadow .18s ease,background-color .18s ease;padding:.55em .8em!important}\n"
"body.arrflix-themed .searchFields .emby-input::placeholder,body.arrflix-themed input.searchfields-txtSearch::placeholder,body.arrflix-themed #searchTextInput::placeholder{color:rgba(255,255,255,.4);letter-spacing:.02em}\n" "body.arrflix-themed .searchFields .emby-input::placeholder,body.arrflix-themed input.searchfields-txtSearch::placeholder,body.arrflix-themed #searchTextInput::placeholder{color:rgba(255,255,255,.4);letter-spacing:.02em}\n"

View file

@ -259,9 +259,13 @@ body.arrflix-video-active:not(:has(#loginPage:not(.hide))) .skinHeader,body.arrf
.arrflix-nav{text-transform:uppercase;letter-spacing:.08em;font-weight:600;padding:0 .9em;color:#fff!important;text-decoration:none;display:inline-flex;align-items:center;height:100%;font-size:.85em;transition:color .18s ease,text-shadow .18s ease,font-weight .18s ease} .arrflix-nav{text-transform:uppercase;letter-spacing:.08em;font-weight:600;padding:0 .9em;color:#fff!important;text-decoration:none;display:inline-flex;align-items:center;height:100%;font-size:.85em;transition:color .18s ease,text-shadow .18s ease,font-weight .18s ease}
.arrflix-nav:hover{color:#E50914!important} .arrflix-nav:hover{color:#E50914!important}
.arrflix-nav.active{color:#E50914!important;font-weight:700;text-shadow:0 0 12px rgba(229,9,20,0.55),0 0 24px rgba(229,9,20,0.25)} .arrflix-nav.active{color:#E50914!important;font-weight:700;text-shadow:0 0 12px rgba(229,9,20,0.55),0 0 24px rgba(229,9,20,0.25)}
/* Pure-black background: kill the #101010 stripe that bleeds at page bottom (jellyfin-web/themes/dark/theme.css:44-50 .backgroundContainer + html) */ /* Pure-black background — L1: scope rules to non-video pages so they self-disable when player mounts. Skips when .htmlVideoPlayer or #videoOsdPage is in the DOM. Doc 30 lesson — recurring black-screen-over-video bug class. */
html,body.arrflix-themed,body.arrflix-themed .backgroundContainer,body.arrflix-themed .skinBody,body.arrflix-themed .mainAnimatedPage,body.arrflix-themed .mainAnimatedPages,body.arrflix-themed .pageContainer,body.arrflix-themed #reactRoot{background-color:#000!important} html,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)),body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .backgroundContainer,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .skinBody,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .mainAnimatedPage,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .mainAnimatedPages,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .pageContainer,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) #reactRoot{background-color:#000!important}
body.arrflix-themed .backgroundContainer.withBackdrop{background-color:rgba(0,0,0,.86)!important} body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .backgroundContainer.withBackdrop{background-color:rgba(0,0,0,.86)!important}
/* Video isolation L2: high-specificity transparent override gated by JS-toggled body.arrflix-video-active. Beats any ancestor opaque-bg rule (specificity 0,3,2 plus tag) by chaining 2 body classes + the wrapper class. */
body.arrflix-themed.arrflix-video-active,body.arrflix-themed.arrflix-video-active .backgroundContainer,body.arrflix-themed.arrflix-video-active .skinBody,body.arrflix-themed.arrflix-video-active .mainAnimatedPage,body.arrflix-themed.arrflix-video-active .mainAnimatedPages,body.arrflix-themed.arrflix-video-active .pageContainer,body.arrflix-themed.arrflix-video-active #reactRoot,body.arrflix-themed.arrflix-video-active .videoPlayerContainer,body.arrflix-themed.arrflix-video-active .videoPlayerContainer-onTop,body.arrflix-themed.arrflix-video-active #videoOsdPage,body.arrflix-themed.arrflix-video-active #videoOsdPage .pageContainer,body.arrflix-themed.arrflix-video-active #videoOsdPage .mainAnimatedPage,body.arrflix-themed.arrflix-video-active #videoOsdPage .layout-desktop,body.arrflix-themed.arrflix-video-active .libraryPage,body.arrflix-themed.arrflix-video-active .htmlVideoPlayer{background-color:transparent!important;background:transparent!important;background-image:none!important}
/* Video element on top of all stacking contexts during playback. */
body.arrflix-themed.arrflix-video-active video.htmlvideoplayer,body.arrflix-themed.arrflix-video-active .htmlVideoPlayer video,body.arrflix-themed.arrflix-video-active .htmlVideoPlayer{z-index:9999!important;isolation:isolate}
/* Search input: replace stock cyan focus ring (jellyfin-web/themes/dark/theme.css:262-272) with a borderless slab + red underline on focus, true to the Cineplex/Netflix accent */ /* Search input: replace stock cyan focus ring (jellyfin-web/themes/dark/theme.css:262-272) with a borderless slab + red underline on focus, true to the Cineplex/Netflix accent */
body.arrflix-themed .searchFields .emby-input,body.arrflix-themed input.searchfields-txtSearch,body.arrflix-themed #searchTextInput{background:#141414!important;border:0!important;border-bottom:2px solid transparent!important;border-radius:2px!important;color:#fff!important;transition:border-color .18s ease,box-shadow .18s ease,background-color .18s ease;padding:.55em .8em!important} body.arrflix-themed .searchFields .emby-input,body.arrflix-themed input.searchfields-txtSearch,body.arrflix-themed #searchTextInput{background:#141414!important;border:0!important;border-bottom:2px solid transparent!important;border-radius:2px!important;color:#fff!important;transition:border-color .18s ease,box-shadow .18s ease,background-color .18s ease;padding:.55em .8em!important}
body.arrflix-themed .searchFields .emby-input::placeholder,body.arrflix-themed input.searchfields-txtSearch::placeholder,body.arrflix-themed #searchTextInput::placeholder{color:rgba(255,255,255,.4);letter-spacing:.02em} body.arrflix-themed .searchFields .emby-input::placeholder,body.arrflix-themed input.searchfields-txtSearch::placeholder,body.arrflix-themed #searchTextInput::placeholder{color:rgba(255,255,255,.4);letter-spacing:.02em}

View file

@ -259,9 +259,13 @@ body.arrflix-video-active:not(:has(#loginPage:not(.hide))) .skinHeader,body.arrf
.arrflix-nav{text-transform:uppercase;letter-spacing:.08em;font-weight:600;padding:0 .9em;color:#fff!important;text-decoration:none;display:inline-flex;align-items:center;height:100%;font-size:.85em;transition:color .18s ease,text-shadow .18s ease,font-weight .18s ease} .arrflix-nav{text-transform:uppercase;letter-spacing:.08em;font-weight:600;padding:0 .9em;color:#fff!important;text-decoration:none;display:inline-flex;align-items:center;height:100%;font-size:.85em;transition:color .18s ease,text-shadow .18s ease,font-weight .18s ease}
.arrflix-nav:hover{color:#E50914!important} .arrflix-nav:hover{color:#E50914!important}
.arrflix-nav.active{color:#E50914!important;font-weight:700;text-shadow:0 0 12px rgba(229,9,20,0.55),0 0 24px rgba(229,9,20,0.25)} .arrflix-nav.active{color:#E50914!important;font-weight:700;text-shadow:0 0 12px rgba(229,9,20,0.55),0 0 24px rgba(229,9,20,0.25)}
/* Pure-black background: kill the #101010 stripe that bleeds at page bottom (jellyfin-web/themes/dark/theme.css:44-50 .backgroundContainer + html) */ /* Pure-black background — L1: scope rules to non-video pages so they self-disable when player mounts. Skips when .htmlVideoPlayer or #videoOsdPage is in the DOM. Doc 30 lesson — recurring black-screen-over-video bug class. */
html,body.arrflix-themed,body.arrflix-themed .backgroundContainer,body.arrflix-themed .skinBody,body.arrflix-themed .mainAnimatedPage,body.arrflix-themed .mainAnimatedPages,body.arrflix-themed .pageContainer,body.arrflix-themed #reactRoot{background-color:#000!important} html,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)),body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .backgroundContainer,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .skinBody,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .mainAnimatedPage,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .mainAnimatedPages,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .pageContainer,body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) #reactRoot{background-color:#000!important}
body.arrflix-themed .backgroundContainer.withBackdrop{background-color:rgba(0,0,0,.86)!important} body.arrflix-themed:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) .backgroundContainer.withBackdrop{background-color:rgba(0,0,0,.86)!important}
/* Video isolation L2: high-specificity transparent override gated by JS-toggled body.arrflix-video-active. Beats any ancestor opaque-bg rule (specificity 0,3,2 plus tag) by chaining 2 body classes + the wrapper class. */
body.arrflix-themed.arrflix-video-active,body.arrflix-themed.arrflix-video-active .backgroundContainer,body.arrflix-themed.arrflix-video-active .skinBody,body.arrflix-themed.arrflix-video-active .mainAnimatedPage,body.arrflix-themed.arrflix-video-active .mainAnimatedPages,body.arrflix-themed.arrflix-video-active .pageContainer,body.arrflix-themed.arrflix-video-active #reactRoot,body.arrflix-themed.arrflix-video-active .videoPlayerContainer,body.arrflix-themed.arrflix-video-active .videoPlayerContainer-onTop,body.arrflix-themed.arrflix-video-active #videoOsdPage,body.arrflix-themed.arrflix-video-active #videoOsdPage .pageContainer,body.arrflix-themed.arrflix-video-active #videoOsdPage .mainAnimatedPage,body.arrflix-themed.arrflix-video-active #videoOsdPage .layout-desktop,body.arrflix-themed.arrflix-video-active .libraryPage,body.arrflix-themed.arrflix-video-active .htmlVideoPlayer{background-color:transparent!important;background:transparent!important;background-image:none!important}
/* Video element on top of all stacking contexts during playback. */
body.arrflix-themed.arrflix-video-active video.htmlvideoplayer,body.arrflix-themed.arrflix-video-active .htmlVideoPlayer video,body.arrflix-themed.arrflix-video-active .htmlVideoPlayer{z-index:9999!important;isolation:isolate}
/* Search input: replace stock cyan focus ring (jellyfin-web/themes/dark/theme.css:262-272) with a borderless slab + red underline on focus, true to the Cineplex/Netflix accent */ /* Search input: replace stock cyan focus ring (jellyfin-web/themes/dark/theme.css:262-272) with a borderless slab + red underline on focus, true to the Cineplex/Netflix accent */
body.arrflix-themed .searchFields .emby-input,body.arrflix-themed input.searchfields-txtSearch,body.arrflix-themed #searchTextInput{background:#141414!important;border:0!important;border-bottom:2px solid transparent!important;border-radius:2px!important;color:#fff!important;transition:border-color .18s ease,box-shadow .18s ease,background-color .18s ease;padding:.55em .8em!important} body.arrflix-themed .searchFields .emby-input,body.arrflix-themed input.searchfields-txtSearch,body.arrflix-themed #searchTextInput{background:#141414!important;border:0!important;border-bottom:2px solid transparent!important;border-radius:2px!important;color:#fff!important;transition:border-color .18s ease,box-shadow .18s ease,background-color .18s ease;padding:.55em .8em!important}
body.arrflix-themed .searchFields .emby-input::placeholder,body.arrflix-themed input.searchfields-txtSearch::placeholder,body.arrflix-themed #searchTextInput::placeholder{color:rgba(255,255,255,.4);letter-spacing:.02em} body.arrflix-themed .searchFields .emby-input::placeholder,body.arrflix-themed input.searchfields-txtSearch::placeholder,body.arrflix-themed #searchTextInput::placeholder{color:rgba(255,255,255,.4);letter-spacing:.02em}