7 docs in /testing/ — institutional memory after 6+ regressions in
24-48h on the v6 theme. Read before any edit.
README.md — index + quickstart
THEMING.md — safe-edit checklist + layer/specificity tables
ERROR-PATTERNS.md — 12 cataloged patterns (Symptom/Cause/Diag/Fix/Prev)
HEADLESS-PROBE.md — 11 playwright recipes (md5 chain, darkPct,
ancestor bg sample, dropdown listItem probe)
ROLLBACK.md — 8 emergency revert recipes (overlay, branding,
encoding, full-from-repo, dev-clone-prod,
git-revert, pw-reset, bind-mount inode-swap)
SMOKE-TEST.md — manual + headless verify checklist
DEPLOY.md — dev → prod promotion workflow with backup +
chown root + restart inode-swap
Empty subdirs: snipUSER-Es/, recipes/, incidents/ (post-mortems land here).
Goal: stop reinventing the same fixes. Catalog every error class,
codify the recovery, build a skills folder for future ARRFLIX work.
7.6 KiB
7.6 KiB
THEMING — how to edit the ARRFLIX theme without breaking it
Short, actionable companion to
docs/31-theme-layer-model-and-edit-guide.md. Read 31 once for the why; come back here for the checklist every edit.
TL;DR — checklist before EVERY theme edit
- Read
docs/31-theme-layer-model-and-edit-guide.md(canonical layer model). - Decide: am I painting an ancestor of
<video>? (see layer table below). - If yes → scope with
body.arrflix-themed:not(.arrflix-video-active)AND add the matching transparent rule underbody.arrflix-themed.arrflix-video-active. - Use the specificity table to predict what wins.
!importantdoes NOT promote specificity. - Edit
bin/inject-middle-theme.py— NEVER hot-patch the deployed overlay. python3 bin/inject-middle-theme.py→scp web-overrides/index.html dev:/opt/docker/jellyfin-dev/web-overrides/→docker restart jellyfin-dev.- Hard-refresh browser (
Ctrl+Shift+R) — bind-mount serves stale otherwise. - Run
testing/SMOKE-TEST.md(login, home, video, OSD). - Green? Promote per
testing/DEPLOY.md. Red?testing/ROLLBACK.md.
The layer model (condensed)
| Layer | Element | bg ownership |
|---|---|---|
| 0 | <html> |
#000 (JS inline-style pinned via setProperty(...,'important')) |
| 1 | <body> |
L1 #000 off-video / L2 transparent on-video |
| 2 | .backgroundContainer / .skinBody / #reactRoot |
follows L1/L2 |
| 3 | .mainAnimatedPages / .pageContainer |
follows L1/L2 |
| 4 | .skinHeader |
#000 off-video, display:none on-video |
| 5 | .videoPlayerContainer (z:1000) → <video.htmlvideoplayer> |
transparent on-video |
| 6 | .osdControls / .videoOsdBottom (z:~1100–1500) |
DO NOT touch — Jellyfin owns |
| 7 | .dialogContainer / .actionSheet (z:~2000+) |
DO NOT touch — Jellyfin owns |
Specificity quick reference
| Selector | (a,b,c) | When to use |
|---|---|---|
body |
(0,0,1) | almost never |
body.arrflix-themed |
(0,1,1) | base theme rule, off/on video both |
body.arrflix-themed:not(.arrflix-video-active) |
(0,2,1) | L1: off-video bg paint |
body.arrflix-themed.arrflix-video-active |
(0,2,1) | L2: on-video transparent |
body.arrflix-themed.arrflix-video-active #videoOsdPage |
(1,2,1) | beats Cineplex #videoOsdPage .pageContainer (1,1,0) |
body.arrflix-video-active:not(:has(#loginPage:not(.hide))) .skinHeader |
(0,4,2) | beats Cineplex display:flex on header |
L1 and L2 tie on (0,2,1). Source order decides — L2 must come AFTER L1 in inject-middle-theme.py. Reordering reopens the black-screen bug.
DO NOT DO
- Set
z-indexon<video>or.videoPlayerContainerabove 1000 → covers OSD scrubber/buttons (image-12 incident). - Add
background-colorrules without:not(.arrflix-video-active)gate → black-screen-over-video. - Hot-patch
/opt/docker/jellyfin/web-overrides/index.htmlin place → repo↔prod drift, INC1 root cause. cpoverlay then skipdocker restart jellyfin→ bind-mount inode swap, container serves stale.- Use
:has(.htmlVideoPlayer)(camelCase) — class is.htmlvideoplayerlowercase. The selector silently never matches. - Drop a
<video>literal intobranding.xml<CustomCss>(even in a comment) without escaping → XML parse fails silently, theme disappears site-wide. Use<video>. - Add
!importanthoping it beats a higher-specificity rule. Among!importantrules, specificity still wins.
When to add to L1/L2 paired rules
If your rule paints background, background-color, or background-image on any of:
body, html, .backgroundContainer, .skinBody, .mainAnimatedPage, .mainAnimatedPages,
.pageContainer, #reactRoot, .videoPlayerContainer, #videoOsdPage, .libraryPage,
video.htmlvideoplayer, .emby-scroller, .backdropContainer
→ Add the selector to BOTH lists in bin/inject-middle-theme.py:
- L1 list — under
/* --- L1: PURE-BLACK BG (off-video only) ------ */, prefixed withbody.arrflix-themed:not(.arrflix-video-active). - L2 list — under the L2 transparent block, prefixed with
body.arrflix-themed.arrflix-video-active, valuebackground:transparent !important.
Always paired. Off-video must stay opaque black; on-video must be transparent so <video> pixels show through.
Safe-edit recipe — "make the search input focus ring red"
# 1. Edit the injector (NOT the deployed overlay)
$EDITOR /tmp/arrflix-recon/bin/inject-middle-theme.py
# Add inside the CSS string, search-input section:
# body.arrflix-themed .searchFields input:focus {
# border-color: #E50914 !important;
# box-shadow: 0 0 0 2px rgba(229,9,20,.35) !important;
# }
# Specificity (0,2,1) — does NOT touch a <video> ancestor → no L1/L2 pairing needed.
# 2. Regenerate the overlay
cd /tmp/arrflix-recon && python3 bin/inject-middle-theme.py
# 3. Sanity: exactly one marker block
grep -c ARRFLIX-MIDDLE-THEME-BEGIN web-overrides/index.html # = 1
# 4. Push to dev only
scp web-overrides/index.html nullstone:/opt/docker/jellyfin-dev/web-overrides/index.html
ssh nullstone 'docker restart jellyfin-dev'
# 5. Verify served
curl -s https://dev.arrflix.s8n.ru/web/index.html | grep -c ARRFLIX-MIDDLE-THEME-BEGIN # = 1
# 6. Hard-refresh browser, run testing/SMOKE-TEST.md, then promote per testing/DEPLOY.md.
How to add a new skin variant
Skin variants are alternative CSS blocks that swap a single visual concern (selector highlight, header logo treatment, etc.) without forking the whole theme.
- Location:
web-overrides/skins/. - Naming:
<concern>-variant-<NN>-<short-slug>.css(e.g.selector-variant-02-red-underline.css). - Format: file-level comment header explaining what concern it replaces, which variant is currently active in
inject-middle-theme.py, and a "drop into the CSS string" instruction. Body is plain CSS scoped underbody.arrflix-themed …. - Activation: copy the rules into the matching section of
bin/inject-middle-theme.py, regen overlay, deploy. Skins are NOT auto-loaded — the file is a parking spot.
Common pitfalls
- camelCase vs lowercase classes —
.htmlVideoPlayerdoes NOT exist; the real class is.htmlvideoplayer. Same trap on.videoOsdBottom(correct) vs.videoosdbottom(wrong). - Cineplex CSS load order —
branding.xml→@import url('/web/cineplex.css')is injected as a<style>AFTER our inline block. On equal specificity Cineplex wins. Bump specificity, do NOT reorder. branding.xmlXML parse —<CustomCss>content must be XML-safe. Escape<>in any CSS comment that mentions HTML tags. Silent failure = whole branding skipped.- iframes / shadow DOM — Jellyfin web does not currently use either. N/A; skip.
backdrop-filter: blur()— only renders if there's content scrolling/painted behind the fixed element. On a pure-black bg the blur is invisible (no pixel diff). Test on a page with a poster backdrop.getComputedStyle(html).backgroundColorreturnsrgba(0,0,0,0)despite stylesheet rules — Chromium root-canvas quirk. We pin<html>via JSstyle.setProperty('background-color','#000','important'). Don't fight it from CSS.
See also
docs/31-theme-layer-model-and-edit-guide.md— canonical layer model and history of past incidents.testing/ERROR-PATTERNS.md— catalog of past mistakes (INC1–INC7, v6-stable, image-12).testing/SMOKE-TEST.md— 4-step manual verify after any theme change.testing/HEADLESS-PROBE.md— Playwright recipes for DOM /darkPct/ OSD-visible assertions.testing/DEPLOY.md/testing/ROLLBACK.md— promote-to-prod and revert procedures.