ARRFLIX/docs/04-theming-and-users.md
s8n 404dc114b4 doc 04 §3e: ElegantFin migration with ARRFLIX recolor
Migrated active CSS theme from Cineplex v1.0.6 to ElegantFin v25.12.31
with Netflix-red #E50914 accent overrides over ElegantFin's default
Jellyseerr-blue/violet palette. ARRFLIX wordmark logo preserved on both
.adminDrawerLogo img and .pageTitleWithLogo selectors (split-rule form).

Eight accent variables overridden at :root: --uiAccentColor, --activeColor
(+Alpha), --osdSeekBarPlayedColor, --checkboxCheckedBgColor,
--highlightOutlineColor, --btnSubmitColor, --btnSubmitBorderColor.

All prior custom blocks preserved verbatim: cast/crew hide, Quick Connect
hide, header-icon hide (§3c), white slider thumbs (§3d), pure-black bg
(§3d), Settings drawer hide, count badge hide, ARRFLIX logo override.
LoginDisclaimer + SplashscreenEnabled untouched.

POST → 204; GET /Branding/Configuration confirms no Cineplex import,
ElegantFin pinned to v25.12.31, all overrides intact. Smoke test on
https://arrflix.s8n.ru/ → HTTP 302 (baseline). No container restart.

Restructured §1: Cineplex content moved to §1 'Previous themes'
subsection (#### Why Cineplex won, #### Tradeoffs, #### What it looked
like, #### Theme history) with the new ElegantFin+recolor stack as
the canonical current theme.

Snapshot tag for rollback: snapshot-2026-05-08-pre-elegantfin
2026-05-08 04:03:32 +01:00

41 KiB
Raw Blame History

04 — Theming and Users

Status: re-themed 2026-05-08 against https://arrflix.s8n.ru (Jellyfin 10.10.3 on nullstone). Active theme: Cineplex v1.0.6 (Netflix-faithful). Replaced ElegantFin v25.12.31 the same day after a Netflix-fidelity-driven survey. Scope: visual theme, server-side branding, multi-user UX prep, SyncPlay, maintenance/revert. LAN-only constraints preserved (no public-facing changes).

Hostname note: this site is being renamed tv.s8n.ruarrflix.s8n.ru in the same session. The Jellyfin API endpoints don't care about hostname — they're served by the same container. All curl examples below are reachable as either https://tv.s8n.ru/... (legacy) or https://arrflix.s8n.ru/... (new), as long as Traefik has a SNI cert for the name. Internal pin: both names should resolve to 192.168.0.100 (see CLAUDE.md memory feedback_s8n_hosts_override.md). If a hostname's DNS or cert isn't up yet, use --resolve tv.s8n.ru:443:192.168.0.100 on curl — that's how this re-theming was applied while arrflix.s8n.ru was still missing a cert.


1. Theme decision: ElegantFin v25.12.31 + ARRFLIX recolor (current)

As of 2026-05-08 (later in the day), the active theme is ElegantFin v25.12.31 with the Netflix-red #E50914 accent recolored over the default Jellyseerr-blue/violet palette and the ARRFLIX wordmark logo preserved. See §3e for the migration details and §1.x ("Previous themes") below for the Cineplex history that preceded it.

Candidates surveyed (2026-05-08)

Theme Type Last commit License Netflix-fidelity JF 10.10.3 compat Verdict
Cineplex (MRunkehl) community CSS, builds on Finity 2025-09-06, tag v1.0.6 none declared 9/10#e50914 accent, Netflix Sans webfont, transform: scale(1.05) card hover, login backdrop, gradient billboard YES — README states "Compatible: v10.10.7 and higher"; renders on 10.10.3 (verified live, no nav breakage) PICKED
JellyFlix (prayag17) community CSS 2023-12-20 none 9/10 — origin of the genre HALTED — repo header: "This skin's development has been halted for sometime." Confirmed broken post-10.11. Risky on 10.10.3 too. rejected
DarkFlix v5.1 (DevilsDesigns) fork of JellyFlix 2024-06 GPL-3.0 8/10 only states 10.8.x; needs 67% browser zoom in users' browsers (non-standard, accessibility issue) rejected
Automationxperts/jellyflix community CSS 2022-11 none 7/10 dead 3.5y, untested on 10.10 rejected
ElegantFin v25.12.31 (lscambo13, previous) community CSS 2026-04-30 GPL-2.0 5/10 — Jellyseerr-style, blue-grey, no Netflix red, no Netflix Sans, no top-10 row excellent (tested 10.11.5) de-themed — wrong aesthetic for this brief
Theme Park (jellyfin pack) multi-app CSS active n/a n/a — no netflix preset for jellyfin (only dracula/nord/hotline/plex) n/a not applicable
zombB / NetfliFin / Finetwo mostly fork-style replacement of jellyfin-web varies varies n/a requires image swap or JS injector DQ — violates "pure CSS, no image swap, no plugins" constraint
Ultrachromic (CTalvio) community CSS "selectively maintained" varies 6/10 — accent-tunable but no Netflix preset unknown not Netflix enough

Previous themes

The two sub-sections below ("Why Cineplex won", "Tradeoffs", "What it looks like", "Theme history") are kept verbatim from when Cineplex was the active theme (earlier on 2026-05-08, before the ElegantFin migration documented in §3e). They remain useful as the reasoning trail for the final brand brief — Netflix-faithful was the goal, Cineplex was the purest expression of that, and the current ElegantFin + recolor stack is a deliberate tradeoff toward "more polished browsing UI" while keeping the Netflix-red accent.

Why Cineplex won (historical)

  1. It is actually Netflix. The CSS literally embeds Netflix Sans (https://assets.nflxext.com/ffe/siteui/fonts/netflix-sans/v3/...) and uses Netflix's exact #E50914 for --accent / --focus-color. Card hover applies transform: scale(1.05), login background gets a radial gradient overlay "to make it look like netflix login" (author's comment). No other live theme matches this fidelity and runs on a maintained codebase.
  2. It targets our Jellyfin series. Header line of cineplex.css reads Compatible: v10.10.7 and higher. We're on 10.10.3 — same minor series, ABI-compatible for selectors. Verified live: page loads, navigation works, no broken layouts.
  3. Single @import line. Zero ops overhead. The CSS imports two transitive deps internally (finity-complete.css for the dark base + jellyfin-icon-metadata for icons) but the user-facing config field has just one line.
  4. Pinned to immutable tag @v1.0.6. jsDelivr serves cache-control: public, max-age=31536000, immutable for tagged commits. We won't get surprised by upstream churn — and if we want updates, a one-line edit to @main opts in.
  5. Companion cineplex.js is purely cosmetic German-locale tweaks (hides "Startseite"/"Favoriten" menu items, swaps a logo). Skipped — we don't run a JS injector plugin (constraint), and our menu labels are English so it'd be a no-op anyway. Theme works fine without it.
  6. Cast/crew hide rule still appended at the bottom of CustomCss, exactly as before.

Tradeoffs (honest list, Cineplex era)

  • License: none. Cineplex doesn't declare one. CSS is generally permissive in practice (you redistribute by @import, not by copying) but if a license argument ever matters for our derivatives, ElegantFin (GPL-2.0) is cleaner.
  • Bus factor: 1. Single author (Maverick Runkehl), 0 stars, last commit Sep 2025. If upstream goes cold we keep working at our pinned tag forever (jsDelivr immutable), but new JF versions might eventually break selectors and we'd need to fork or migrate.
  • Netflix Sans license note. The font files are loaded from Netflix's own CDN, not bundled. If Netflix changes that path or rate- limits non-netflix.com referers, we'd fall back to system-ui (also declared in the stack). Acceptable.
  • Theme footer. Cineplex doesn't add a brand stamp, so users see no "Cineplex" tag — cleaner than ElegantFin's footer label was.

What Cineplex looked like (live, post-apply)

  • Background: #181818 (Finity base) — Netflix-black.
  • Accent: #E50914 (canonical Netflix red) on focus rings, progress bars, primary buttons, hover states.
  • Typeface: Netflix Sans across the whole UI (loaded from Netflix's own CDN — the same fonts netflix.com itself serves). Subtitles also use Netflix Sans Medium.
  • Cards: rounded ~6px, hover scales to 1.05× with subtle shadow lift.
  • Login screen: dark backdrop with radial-gradient overlay — close to netflix.com's sign-in page.
  • No theme-brand footer label any more.

Theme history

Date Theme Version Why changed
2026-05-08 (earlier today) ElegantFin v25.12.31 Initial Jellyfin theming pass. Picked for activity + safety (most actively maintained CSS in the ecosystem).
2026-05-08 (mid-day) Cineplex v1.0.6 Owner asked for the most Netflix-faithful theme available. ElegantFin's Jellyseerr aesthetic (blue-grey, no red) is too far from Netflix; Cineplex is purpose-built for this look and explicitly targets the 10.10 series we're on. JellyFlix (the genre's elder) is halted.
2026-05-08 (later, current) ElegantFin + ARRFLIX recolor v25.12.31 + #E50914 accent overrides Owner liked Cineplex's Netflix accent but preferred ElegantFin's polished browsing UI. Best of both: ElegantFin's layout/typography + ARRFLIX brand red overrides. Snapshot tag for rollback: snapshot-2026-05-08-pre-elegantfin. See §3e.

Rollback paths:

  • To Cineplex (Netflix-faithful): apply snapshots/2026-05-08-pre-elegantfin/branding.json per snapshots/2026-05-08-pre-elegantfin/RESTORE.md.
  • To plain ElegantFin (no recolor): see §6b.
  • To vanilla Jellyfin: see §6b.

2. How it was applied

Branding API (Cineplex, applied 2026-05-08)

TOKEN=*redacted*

cat > /tmp/branding.json <<'EOF'
{
  "LoginDisclaimer": "Welcome to tv.s8n.ru — LAN-only. Be kind, rewind.",
  "CustomCss": "/* Cineplex v1.0.6 — Netflix-faithful theme by MRunkehl, pinned tag (immutable on jsDelivr) */\n/* Compat: Jellyfin 10.10.7+ ; we run 10.10.3 — verified rendering 2026-05-08 */\n@import url(\"https://cdn.jsdelivr.net/gh/MRunkehl/cineplex@v1.0.6/cineplex.css\");\n\n/* Hide Cast & Crew + Guest Stars sections globally (preserved 2026-05-08) */\n#castCollapsible, #guestCastCollapsible { display: none !important; }\n",
  "SplashscreenEnabled": true
}
EOF

# Note: arrflix.s8n.ru didn't have a Traefik SNI cert at apply-time, so
# we sent the request to the legacy SNI tv.s8n.ru and pinned its address
# with --resolve. Either form is fine once both names have certs.
curl -sS --resolve tv.s8n.ru:443:192.168.0.100 \
  -X POST \
  -H "X-Emby-Token: $TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @/tmp/branding.json \
  https://tv.s8n.ru/System/Configuration/branding
# expect: HTTP 204  (got HTTP 204 — applied)

Verification (executed 2026-05-08)

# 1. Admin endpoint — confirms the new CustomCss is stored.
curl -sS --resolve tv.s8n.ru:443:192.168.0.100 \
  -H "X-Emby-Token: $TOKEN" \
  https://tv.s8n.ru/System/Configuration/branding | python3 -m json.tool
# Result: HTTP 200, contains the Cineplex @import + cast/crew hide rule.

# 2. Anonymous endpoint the SPA reads at runtime — confirms what every
#    browser will pull before login.
curl -sS --resolve tv.s8n.ru:443:192.168.0.100 \
  https://tv.s8n.ru/Branding/Configuration | python3 -m json.tool
# Result: HTTP 200, identical CustomCss to admin endpoint. ✓

# 3. The CSS asset itself on jsDelivr (sanity-check the network path).
curl -sSI "https://cdn.jsdelivr.net/gh/MRunkehl/cineplex@v1.0.6/cineplex.css" | head -3
# Result: HTTP/2 200, content-type: text/css,
#         cache-control: public, max-age=31536000, immutable. ✓

# 4. SPA shell still routes (nav not broken).
curl -sSI --resolve tv.s8n.ru:443:192.168.0.100 https://tv.s8n.ru/ | head -1
# Result: HTTP/2 302 → /web/. ✓

/ returns Jellyfin's SPA shell; the theme CSS is fetched at runtime by the JS bundle from /Branding/Configuration, not inlined into index.html. So curl / won't grep-match. The valid JSON at /Branding/Configuration is the API-level confirmation. Final visual check is a hard browser reload (Ctrl-Shift-R) on https://tv.s8n.ru (or https://arrflix.s8n.ru once its cert is up) from the LAN — owner will do this.

Cache clear

Jellyfin web caches aggressively in browsers. After applying:

  • Users: hard reload (Ctrl-Shift-R / Cmd-Shift-R) once.
  • Server: no restart needed; CustomCss change is live on next page load.

3. Server-side branding state (as of 2026-05-08, post-Cineplex)

Field Value
LoginDisclaimer "Welcome to ARRFLIX - Private invite only service"
CustomCss @import of Cineplex v1.0.6 from jsDelivr — pinned tag @v1.0.6 (immutable). Plus cast/crew hide rule and the ARRFLIX logo override (split-rule form, see §3a).
SplashscreenEnabled true

SplashscreenEnabled: true makes Jellyfin auto-pick a backdrop from the library and serve it at /Branding/Splashscreen. The web client doesn't itself surface this; mobile/TV clients do. Harmless to leave on.

3a. ARRFLIX logo override — fix for triple-stacked wordmark (2026-05-08)

Initial override copied Cineplex's three-selector group verbatim and combined content: url(...) with background-image: url(...) on every selector. This rendered the ARRFLIX wordmark up to three times on top of itself in the header (most visible on the login page). Two root causes, verified against the live /jellyfin/jellyfin-web/ bundle:

  1. .imgLogoIcon is a phantom selector. A grep of every *.js, *.html and *.css asset shipped with Jellyfin 10.10.3 returns zero matches. Cineplex's upstream rule (imgLogoIcon { content: url(...) } — note the missing leading dot) is itself a no-op typo. Adding the dot in our override does nothing useful because the class never appears in the DOM; keeping it just bloats the rule.
  2. content: url(...) on a <div> renders the image inside the element. .pageTitleWithLogo is a <div> (set by setDefaultTitle in 73233.*.chunk.js — see c.classList.add("pageTitleWithLogo")). Cineplex deliberately uses background-image: on this div and keeps content: for .adminDrawerLogo img (an <img>, where content: replaces the source). Our override applied both properties to both selectors, so on the header div the logo painted as a replaced element on top of its own background image — instant duplication.

Fix: split the rule by element type, drop the phantom selector entirely.

/* ARRFLIX logo override (replace Cineplex branding) — 2026-05-08
   (fix: split rules, drop phantom .imgLogoIcon) */
.adminDrawerLogo img {
  /* <img> in admin sidebar drawer — content: replaces src */
  content: url("data:image/png;base64,<...ARRFLIX wordmark...>") !important;
}
.pageTitleWithLogo {
  /* <div> masthead on dashboard + login — bg image only, no content: */
  background-image: url("data:image/png;base64,<...ARRFLIX wordmark...>") !important;
}

Verified live (HTTP 204 on POST, then GET /Branding/Configuration): single ARRFLIX wordmark on login, dashboard header, and admin sidebar. Cast/crew hide rule, Cineplex @import, LoginDisclaimer and SplashscreenEnabled all preserved unchanged. The bind-mounted /web/index.html (favicon, <title>ARRFLIX</title>, splashLogo) was not touched — that asset is owned by the index-patcher.

3b. ARRFLIX logo override fix re-applied 2026-05-08 (second time)

Re-curling /System/Configuration/branding after §3a was applied showed the broken three-selector / dual-property rule was back: .adminDrawerLogo img, .imgLogoIcon, .pageTitleWithLogo { content: url(...); background-image: url(...); }. The §3a fix had been silently overwritten by a sibling agent that POSTed a full branding payload (LoginDisclaimer + SplashscreenEnabled + its own stale CustomCss) without first GETting the latest CustomCss. Because the Jellyfin branding endpoint takes a complete object on every POST and has no field-level merge, whichever POST lands last wins — there is no locking, no ETag, no patch semantics. Fix re-applied surgically (split selectors, drop .imgLogoIcon, preserve the data-URL bytes verbatim, preserve the cast/crew hide, Cineplex @import, Quick Connect hide, and both LoginDisclaimer/SplashscreenEnabled values) and re-verified via /Branding/Configuration. Operational rule: any agent or script touching /System/Configuration/branding must (a) GET first, (b) edit only the fields it owns, (c) POST the full object, and the branding-CSS POST must be the last POST in any sequence that touches this endpoint — otherwise a later sibling POST will silently re-stack the logo. If you find yourself about to POST branding for any reason, GET /System/Configuration/branding first and confirm the override block matches the §3a skeleton before sending.

3c. Header icon hide (2026-05-08): keep search, drop SyncPlay/Cast/User-menu

Top-right header had four buttons: SyncPlay (Create Group people-pair), Cast (Chromecast triangle), Search (magnifying glass), User (account menu). Goal: hide everything except Search. Selectors confirmed by grep against the live JF 10.10.3 web bundle (73233.*.chunk.js): headerSyncButton, headerCastButton, headerSearchButton, headerUserButton (also headerAudioPlayerButton for the now-playing badge). Each is a class on the <button> element, not an id, so the hide rules target the class directly. Pure CSS appended to existing CustomCss via the standard GET → edit → POST flow (no container restart, no index.html edit):

/* Hide top-right header icons (keep Search) — 2026-05-08 */
.headerSyncButton { display: none !important; }
.headerCastButton { display: none !important; }
.headerUserButton { display: none !important; }

Rationale per button:

  • headerSyncButton — SyncPlay group creation. Single-user / family box, no co-watching workflow yet (see §5b for the full SyncPlay context).
  • headerCastButton — Chromecast picker. No Cast targets on this LAN, the button just opened an empty device list.
  • headerUserButton — account/sign-out drop-down. Non-admins lose almost every entry in this menu after §4 lockdown anyway, and admins use the Dashboard wrench. Removing the icon eliminates the dead-end click.
  • headerSearchButton — kept. Only navigation affordance left in the top-right. The CSS does not touch .headerSearchButton so its event handler is untouched and the icon still triggers the search drawer.
  • The hamburger drawer toggle on the LEFT (.headerHomeButton / .mainDrawerButton) is also untouched.

Verified post-write via GET /Branding/Configuration that all earlier rules are still present (Cineplex @import, cast/crew hide, ARRFLIX logo override, Quick Connect hide, Settings drawer hide, LoginDisclaimer, SplashscreenEnabled). POST returned 204; same operational rule from §3b applies — this was the last branding POST in the sequence.

3d. Slider thumb white + pure-black background (2026-05-08)

Two small visual nits remained after §3c. (1) The OSD scrubber and volume slider thumbs in the video player were rendered with the Material-UI default primary tint (blue-ish circle), clashing against the otherwise red/white/black Cineplex palette; we want pure white circles so the thumb reads as a neutral "where am I" indicator and not a brand colour. (2) The page surface throughout the SPA was a near-black grey (roughly #181818, inherited from Jellyfin / Cineplex defaults) rather than true #000000; on OLED displays and against the Netflix-style artwork-edge fades this looked dusty. Both fixed with pure CSS appended to CustomCss, no index.html edit, no container restart.

Selectors verified against the live bundle. osdPositionSlider and osdVolumeSlider are visible in playback-video-index-html.*.chunk.js and 90742.*.chunk.js (grep on /jellyfin/jellyfin-web/*.js). The inner .MuiSlider-thumb class is added at runtime by MUI's React component, so it doesn't appear as a literal in the bundle but is the documented MUI public API surface — overriding it directly (rather than the --mui-palette-primary-main CSS variable) keeps the rest of the button/control palette unchanged. Older Jellyfin builds used emby-slider .sliderThumb, included as a belt-and-braces fallback.

/* Tweak: white thumbs (2026-05-08) */
.MuiSlider-thumb,
.osdPositionSlider .MuiSlider-thumb,
.osdVolumeSlider .MuiSlider-thumb,
emby-slider .sliderThumb {
  color: #ffffff !important;
  background-color: #ffffff !important;
  border-color: #ffffff !important;
}
.MuiSlider-thumb:hover,
.MuiSlider-thumb.Mui-focusVisible,
.MuiSlider-thumb:active {
  box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.16) !important;
}

/* Tweak: pure black bg (2026-05-08) */
:root {
  --primary-background-color: #000000 !important;
  --background-color: #000000 !important;
}
html, body, .preload, .skinBody, .mainDrawerHandle {
  background-color: #000000 !important;
}
.skinHeader,
.skinHeader.semiTransparent,
.skinHeader.skinHeader-withBackground,
.mainAnimatedPages,
#reactRoot,
.dashboardDocument {
  background-color: #000000 !important;
}

Rationale notes:

  • We deliberately did not override --mui-palette-primary-main at :root — that variable re-tints buttons, focus rings, and a handful of other accents globally. Scoping to .MuiSlider-thumb keeps the fix surgical.
  • We kept the red track (.MuiSlider-track) and grey rail (.MuiSlider-rail) untouched; only the draggable thumb changed.
  • For the background, both the Jellyfin shell variables (--primary-background-color, --background-color) and the concrete wrapper elements (html, body, .skinHeader, .mainAnimatedPages, #reactRoot, .dashboardDocument) are forced to #000. Belt and braces — different views render through different wrappers and the Cineplex import sometimes redeclares the variables.
  • Screenshots: capture player chrome (scrubber + volume) and home/lib pages on next visual sweep; if poster cards visibly lose contrast against pure black, soften the card surface to #0a0a0a in a follow-up tweak rather than raising the page surface again.

POST returned 204. Verified via GET /Branding/Configuration: both new blocks present, all prior blocks (Cineplex @import, cast/crew hide, ARRFLIX logo override, Quick Connect hide, Settings drawer hide, header icon hide) preserved verbatim. Same race rule applies — this is the last branding POST in the sequence.

3e. ElegantFin migration with ARRFLIX recolor (2026-05-08, current)

Later on 2026-05-08, the active theme was migrated from Cineplex to ElegantFin v25.12.31 while preserving the ARRFLIX brand: Netflix-red #E50914 accent overrides over ElegantFin's default Jellyseerr-blue/ violet palette, plus the existing ARRFLIX wordmark logo. The owner had seen the demo at https://lscambo13.github.io/ElegantFin/, liked ElegantFin's polished browsing UI more than Cineplex's purer Netflix fidelity, and asked for the swap with the brand colour kept intact.

Snapshot tag for rollback (committed and pushed before any change): snapshot-2026-05-08-pre-elegantfin. Captures branding.json, index.html, docker-compose.yml, all per-user displayprefs-*.json, users.json, libraries.json, plus RESTORE.md with three concrete rollback commands. Located at snapshots/2026-05-08-pre-elegantfin/.

ElegantFin tag pinned: v25.12.31 (latest tag at migration time; list resolved via git ls-remote --tags https://github.com/lscambo13/ElegantFin.git). jsDelivr serves tagged refs immutably with year-long cache TTL — same no-surprise-update guarantee we had on cineplex@v1.0.6. To opt into upstream churn, edit the URL to @main; to pin a different tag, edit the version segment.

ElegantFin import:

@import url("https://cdn.jsdelivr.net/gh/lscambo13/ElegantFin@v25.12.31/Theme/ElegantFin-jellyfin-theme-build-latest-minified.css");

Accent variables overridden (ARRFLIX recolor block). ElegantFin declares its accent palette through CSS custom properties at :root. Eight variables were identified by grepping the minified theme for --[a-z]* definitions and inspecting their default values; all eight are remapped to #E50914 (or its rgba() form for alpha variants):

Variable ElegantFin default ARRFLIX value What it controls
--uiAccentColor rgb(117 111 226) (violet) #E50914 Primary UI accent — most surfaces
--activeColor rgb(119,91,244) (violet) #E50914 Active / focused state highlights
--activeColorAlpha rgba(119,91,244,.9) rgba(229, 9, 20, 0.9) Same with alpha — hover overlays
--osdSeekBarPlayedColor var(--textColor) (white) #E50914 Played portion of the video scrubber
--checkboxCheckedBgColor rgb(79,70,229) (indigo) #E50914 Checked checkboxes (settings, lib pickers)
--highlightOutlineColor rgb(37,99,235) (blue) #E50914 Focus / highlight outlines on cards
--btnSubmitColor rgb(61,54,178) (indigo) #E50914 "Submit" button background
--btnSubmitBorderColor rgb(117 111 226) (violet) #E50914 "Submit" button border

Override block:

:root {
  --uiAccentColor: #E50914 !important;
  --activeColor: #E50914 !important;
  --activeColorAlpha: rgba(229, 9, 20, 0.9) !important;
  --osdSeekBarPlayedColor: #E50914 !important;
  --checkboxCheckedBgColor: #E50914 !important;
  --highlightOutlineColor: #E50914 !important;
  --btnSubmitColor: #E50914 !important;
  --btnSubmitBorderColor: #E50914 !important;
}

Variables deliberately NOT changed:

  • --osdSeekBarThumbColor: white — kept the explicit white-thumb rule from §3d (white thumbs read as a neutral position indicator, not as brand colour). The slider-thumb override in this doc's §3d still applies.
  • --drawerColor, --headerColor — kept ElegantFin's translucent blur over its dark-blue surface; these are structural, not accent.
  • --borderColor, --textColor — typography / structure, not accent.

Logo selectors used. ElegantFin does NOT define rules for the two ARRFLIX logo selectors (verified by grepping the minified theme for adminDrawerLogo and pageTitleWithLogo — zero matches), so the same override skeleton from §3a/§3b is re-applied verbatim against the ElegantFin base:

.adminDrawerLogo img {
  /* <img> in admin sidebar drawer — content: replaces src */
  content: url("data:image/png;base64,<...ARRFLIX wordmark...>") !important;
}
.pageTitleWithLogo {
  /* <div> masthead on dashboard + login — bg image only, no content: */
  background-image: url("data:image/png;base64,<...ARRFLIX wordmark...>") !important;
}

The data-URL bytes are byte-for-byte identical to the Cineplex-era override (extracted from the snapshot's branding.json and re-inlined into the new CustomCss payload). Both selectors are still split-rule form (per the §3a/§3b lesson — never combine content: and background-image: on the same selector).

Preserved blocks (every custom rule from the Cineplex era was re-applied on top of ElegantFin):

  • #castCollapsible, #guestCastCollapsible { display: none } — cast/crew sections hidden
  • .btnQuick { display: none } — Quick Connect login button hidden
  • .headerSyncButton, .headerCastButton, .headerUserButton — top-right header icons hidden (§3c)
  • .MuiSlider-thumb + variants — white scrubber/volume thumbs (§3d)
  • :root { --primary-background-color: #000000; --background-color: #000000; } and the wrapper-element rules — pure black bg (§3d)
  • mypreferencesmenu selectors — Settings drawer entry hidden
  • .countIndicator { display: none } — unwatched-episode count badges hidden
  • .adminDrawerLogo img / .pageTitleWithLogo — ARRFLIX wordmark override
  • LoginDisclaimer"Welcome to ARRFLIX - Private invite only service" preserved
  • SplashscreenEnabled: true — preserved

Verification (executed 2026-05-08):

  • POST to /System/Configuration/branding → HTTP 204
  • GET on /Branding/Configuration → no Cineplex @import, ElegantFin @import present and pinned to v25.12.31, ARRFLIX logo data URL intact on both selectors, all preserved blocks intact, all eight accent variable overrides present
  • HEAD on https://arrflix.s8n.ru/ → HTTP 302 (Traefik redirect to web/, baseline behaviour — proxy still serving)

Operational notes:

  • The bind-mounted /web/index.html was NOT touched (sibling work owns that file via the index-patcher). All visual changes ride on CustomCss via the public /Branding/Configuration consumer + the authenticated /System/Configuration/branding writer.
  • No container restart, no docker compose action, no Traefik change.
  • Same race rule from §3b applies — the branding POST in this migration was the last POST in the sequence.

Rollback — see snapshots/2026-05-08-pre-elegantfin/RESTORE.md, or in one shot: git checkout snapshot-2026-05-08-pre-elegantfin -- snapshots/2026-05-08-pre-elegantfin/branding.json then POST it back to /System/Configuration/branding.


4. Multi-user UX prep

4a. Library inventory

Library Type ItemId
Movies movies f137a2dd21bbc1b99aa5c0f6bf02a805
TV Shows tvshows 767bffe4f11c93ef34b805451a696a4e
Playlists playlists 1071671e7bffa0532e930debee501d2e

4b. Existing users

Name UserId Admin?
s8n 2be0f0d3fe3a45dc9298138a15a01925 yes

4c. Creating a new user (UI)

  1. Dashboard → Users → "+ Add User".
  2. Username + initial password. Tick "User can manage server" OFF.
  3. After creation, click the user → tabs:
    • Profile: language, audio default, subtitle default. Set per user; doesn't have to match server defaults.
    • Library Access: untick "Enable access to all libraries", tick only the libraries this user should see.
    • Parental Control: max rating, blocked tags, access schedule.
    • Password: set / reset.

4d. Creating a new user (API) — playbook

Do not run this without explicit user request. Documented for the friend account that will exist later.

TOKEN=*redacted*
TVSHOWS_ID=767bffe4f11c93ef34b805451a696a4e

# 1. Create the user (auth header REQUIRED — admin token).
NEW_USER=$(curl -sS -X POST \
  -H "X-Emby-Token: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"Name":"friend","Password":"<initial-password>"}' \
  https://arrflix.s8n.ru/Users/New)
echo "$NEW_USER" | python3 -m json.tool
NEW_ID=$(echo "$NEW_USER" | python3 -c "import sys,json; print(json.load(sys.stdin)['Id'])")
echo "NEW_ID=$NEW_ID"

# 2. Tighten the policy: TV-only, non-admin, can change own prefs,
#    no content deletion, SyncPlay enabled (so we can co-watch).
cat > /tmp/policy.json <<EOF
{
  "IsAdministrator": false,
  "IsHidden": false,
  "IsDisabled": false,
  "EnableContentDeletion": false,
  "EnableUserPreferenceAccess": true,
  "EnableRemoteAccess": true,
  "EnableSharedDeviceControl": false,
  "EnableLiveTvAccess": false,
  "EnableLiveTvManagement": false,
  "EnableMediaPlayback": true,
  "EnableAudioPlaybackTranscoding": true,
  "EnableVideoPlaybackTranscoding": true,
  "EnablePlaybackRemuxing": true,
  "EnableAllFolders": false,
  "EnabledFolders": ["$TVSHOWS_ID"],
  "EnableAllChannels": false,
  "EnabledChannels": [],
  "EnableAllDevices": true,
  "BlockedTags": [],
  "BlockedMediaFolders": [],
  "MaxParentalRating": null,
  "AccessSchedules": [],
  "SyncPlayAccess": "CreateAndJoinGroups",
  "InvalidLoginAttemptCount": 0,
  "LoginAttemptsBeforeLockout": 5,
  "MaxActiveSessions": 0
}
EOF

curl -sS -X POST \
  -H "X-Emby-Token: $TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @/tmp/policy.json \
  "https://arrflix.s8n.ru/Users/$NEW_ID/Policy"
# expect: HTTP 204

# 3. Verify
curl -sS -H "X-Emby-Token: $TOKEN" \
  "https://arrflix.s8n.ru/Users/$NEW_ID" | python3 -m json.tool

4e. Policy field cheat-sheet

Field What it does Recommended for friend
IsAdministrator Full server admin false
EnableContentDeletion Can delete media items via UI false
EnableUserPreferenceAccess Change own profile/audio/sub prefs true
EnableAllFolders Master switch for library ACL false
EnabledFolders Whitelist of CollectionFolder Ids [TVShows] (only)
BlockedTags Skip items tagged with these optional; e.g. ["adult","unrated"]
MaxParentalRating Hide above this rating null for friend (adult). Set 15 for a kid.
AccessSchedules Day-of-week + time windows [] (no restriction)
SyncPlayAccess CreateAndJoinGroups / JoinGroups / None CreateAndJoinGroups
MaxActiveSessions Concurrent sessions cap; 0 = unlimited 2 if you want to throttle
LoginAttemptsBeforeLockout Brute-force protection 5
EnableLiveTvAccess / Management Live TV / DVR false (we don't run it)

4f. Password reset flow

The friend forgot their password. Two routes:

  • Self-serve (only if SMTP is configured — we don't currently): login page → "Forgot Password". Jellyfin emits a PIN file at /config/data/passwordreset-*.json valid 30 minutes. Without SMTP, the admin reads the PIN out of the container and gives it to the friend.
  • Admin reset (what we'll do):
curl -sS -X POST \
  -H "X-Emby-Token: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"NewPw":"<new-password>","ResetPassword":false}' \
  "https://arrflix.s8n.ru/Users/$USER_ID/Password"

To clear the password entirely (forces friend to set one on next login): same call with "ResetPassword": true and no NewPw.

4g. Quick Connect — disabled (2026-05-08)

Quick Connect is Jellyfin's 6-digit-code device pairing flow. ARRFLIX is private invite-only with a small, known userbase, so we don't need it — and the owner doesn't want the "Use Quick Connect" button cluttering the login page. Disabled at both the server-config and CSS layers:

1. Server-side disable (the canonical fix). In 10.10.3 there is no dedicated POST /QuickConnect/Disable endpoint — the flag lives in system.xml as QuickConnectAvailable and is toggled via the system config API:

# Pull current config, flip the flag, push back.
curl -s -H "Authorization: MediaBrowser Token=\"$TOKEN\"" \
  https://arrflix.s8n.ru/System/Configuration > /tmp/cfg.json
jq '.QuickConnectAvailable = false' /tmp/cfg.json > /tmp/cfg.new.json
curl -s -X POST -H "Authorization: MediaBrowser Token=\"$TOKEN\"" \
     -H "Content-Type: application/json" \
     --data-binary @/tmp/cfg.new.json \
     https://arrflix.s8n.ru/System/Configuration
# expect: HTTP 204

# Verify
curl -s -H "Authorization: MediaBrowser Token=\"$TOKEN\"" \
     https://arrflix.s8n.ru/QuickConnect/Enabled
# expect: false

2. CSS hide as belt-and-braces. The login button in the web bundle has class .btnQuick (verified in session-login-index-html.c73c6453a153f384f752.chunk.js). Even with the server flag off, older builds have been observed to still render the button. Appended to CustomCss:

/* Hide Quick Connect button on login page (server-side disabled too) */
.btnQuick { display: none !important; }

Pushed via POST /System/Configuration/branding (the /Branding/... namespace is read-only — write goes through /System/Configuration/<key>). Cineplex import, cast/crew hide, and ARRFLIX logo override blocks preserved untouched.

Re-enable later (if friend account ever wants it): set QuickConnectAvailable=true via the same endpoint, and remove the .btnQuick rule from CustomCss.

4h. Per-user defaults (profile UI)

Set on each user's profile page (or via /Users/{id}/Configuration API):

  • AudioLanguagePreference: eng
  • SubtitleLanguagePreference: eng
  • SubtitleMode: Smart (only show when audio differs) or Always.
  • PlayDefaultAudioTrack: true.
  • Display language: pick on first login.

5. Watching together / continue-watching

5a. Resume / Next Up / Up Next — how Jellyfin builds them

  • Continue Watching ("Resume" row): items where UserData.PlaybackPositionTicks > 0 and not yet Played: true. Threshold for "watched" is server-side ~90% by default. Per-user.
  • Next Up: for series the user has started, Jellyfin walks the next unwatched episode in season/episode order. Configurable in Dashboard → Display → Next Up (max age, rewatching toggle).
  • Up Next (the in-player auto-advance card): client-side feature in the web/mobile players, fed by the same Next Up logic.

No action needed — these light up automatically once a user has played something. Futurama is loaded, so as soon as anyone plays an episode, the homepage gets populated.

5b. SyncPlay (synchronised group playback)

Server-side: nothing to enable, ships on. Per-user permission lives in Policy.SyncPlayAccess:

Value Meaning
CreateAndJoinGroups Can start a SyncPlay group + invite
JoinGroups Can only join existing groups
None Disabled

Verified current state: s8n.SyncPlayAccess = CreateAndJoinGroups ✓.

How to use:

  1. s8n opens a series episode and starts playing.
  2. Player overlay → top-right people-icon ("SyncPlay") → "Create group".
  3. Friend logs in (any device — same arrflix.s8n.ru), opens the same item or the SyncPlay menu → "Join {s8n}'s group".
  4. Anyone in the group's play/pause/seek is mirrored within ~1 second.
  5. Voice chat is up to you — Jellyfin doesn't bundle one (Matrix room on txt.s8n.ru works fine; or just a phone call).

Caveat: SyncPlay uses WebSockets. Our reverse proxy (traefik) handles WS by default, no changes needed.


6. Maintenance

6a. Updating the theme

We currently pin Cineplex to @v1.0.6 (immutable) — no auto-updates, no surprise breakage. To opt into upstream changes:

# Move from immutable tag to floating @main (pulls future commits;
# jsDelivr cache TTL is up to 7d for floating refs).
curl -sS --resolve tv.s8n.ru:443:192.168.0.100 \
  -X POST -H "X-Emby-Token: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"CustomCss": "@import url(\"https://cdn.jsdelivr.net/gh/MRunkehl/cineplex@main/cineplex.css\");\n#castCollapsible, #guestCastCollapsible { display: none !important; }", "LoginDisclaimer": "Welcome to tv.s8n.ru — LAN-only. Be kind, rewind.", "SplashscreenEnabled": true}' \
  https://tv.s8n.ru/System/Configuration/branding

Or just ask each user to hard-reload — their browser cache is the common bottleneck, not jsDelivr.

When upgrading Jellyfin (e.g. 10.10.3 → 10.10.7+ → 10.11.x), check the Cineplex commits and the README compatibility line. Cineplex's stated floor is 10.10.7, so going forward in the 10.10 series is safe; jumping to 10.11 needs a re-test (selectors changed in some 10.11 release notes). If something regresses, pin back to @v1.0.6.

6b. Reverting to ElegantFin (or vanilla)

Replace the @import line:

# Back to ElegantFin (Jellyseerr-style):
curl -sS --resolve tv.s8n.ru:443:192.168.0.100 \
  -X POST -H "X-Emby-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"CustomCss": "@import url(\"https://cdn.jsdelivr.net/gh/lscambo13/ElegantFin@v25.12.31/Theme/ElegantFin-jellyfin-theme-build-latest-minified.css\");\n#castCollapsible, #guestCastCollapsible { display: none !important; }", "LoginDisclaimer": "Welcome to tv.s8n.ru — LAN-only. Be kind, rewind.", "SplashscreenEnabled": true}' \
  https://tv.s8n.ru/System/Configuration/branding

# To vanilla Jellyfin (clear everything):
curl -sS --resolve tv.s8n.ru:443:192.168.0.100 \
  -X POST -H "X-Emby-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"CustomCss": "", "LoginDisclaimer": "", "SplashscreenEnabled": false}' \
  https://tv.s8n.ru/System/Configuration/branding

Or in the UI: Dashboard → General → edit / clear "Custom CSS code" → Save. Hard-reload browsers afterward.

6c. Pinning a known-good revision

Cineplex is already pinned to @v1.0.6. If a future tag (e.g. v1.0.7) ships and is good, bump the URL. jsDelivr serves @<tag> immutably and forever. Tag list: https://github.com/MRunkehl/cineplex/tags.


7. First-30-minutes UX checklist (new user)

When the friend gets their account, walk them through this once:

  1. Login → see the LAN-only disclaimer; that's the right server.
  2. Profile picture → set one (just helps SyncPlay group UX).
  3. Display preferences (top-right user icon → Display):
    • Theme: keep "Dark" (Cineplex is dark-only — Netflix-black #181818 base; light theme will look half-applied). Don't switch.
    • Landing screen: Home.
  4. Playback preferences:
    • Default audio language: English.
    • Default subtitle language: English.
    • Subtitle mode: Smart (auto-show on foreign audio).
    • "Play next episode automatically": on (this is what enables Up Next).
  5. Quality — first-time playback test on Futurama:
    • Pick S01E01, play. Click the gear → quality. If it stutters on 1080p, drop to 720p; transcoder is CPU-only on nullstone today (GTX 1660 Ti driver still broken — see README.md).
    • Once confirmed playing, that quality is remembered per device.
  6. SyncPlay test: friend in one tab, s8n in another, friend joins s8n's group, confirm play/pause syncs. (Drops the "do you have it running" question forever.)
  7. Mobile/TV: install Jellyfin app, server URL https://arrflix.s8n.ru (must be on LAN or Tailscale), Quick Connect or password.
  8. Bookmarks/RSS: there isn't one — Jellyfin's "Latest" row is the substitute. Friend can favourite shows (heart icon) to pin.

8. Open items / future work

  • Enable Quick Connect when friend account is created (Dashboard → General → Quick Connect).
  • Configure SMTP for self-serve password reset (currently admin-only).
  • Get Traefik to issue a SNI cert for arrflix.s8n.ru so the curl examples don't need --resolve tv.s8n.ru:443:192.168.0.100. Until then, both names point to the same backend on 192.168.0.100 but only tv.s8n.ru has a valid cert.
  • Watch Cineplex commits monthly; if a v1.0.7 lands and looks safe, bump the pin.
  • Add a 2nd library (movies are mounted but the server may have an empty Movies folder — confirm with friend's first ask).
  • After GPU driver fix on nullstone, NVENC transcode → 1080p HEVC will stop being CPU-bound; revisit per-user quality defaults.
  • Sanity-check that Netflix Sans loads on every device — if Netflix's CDN starts blocking foreign referers, swap the @font-face block for a self-hosted copy or fall back to system-ui.