# 11 — NeutralFin Render Audit

Status: **read-only audit**, executed 2026-05-08 against
`https://arrflix.s8n.ru` (Jellyfin 10.10.3 on nullstone). Owner reported
the live render "doesn't look as good as it should" relative to the
NeutralFin demo screenshots. Scope: identify why the current `CustomCss`
+ inline critical-path `<style>` block fail to deliver the polished
NeutralFin aesthetic. **No fixes applied. No state mutated.**

> **Headline finding (must read first).** The audit was commissioned
> against the **live NeutralFin render**, but at audit time
> `/Branding/Configuration` returned a `CustomCss` whose `@import` is
> `MRunkehl/cineplex@v1.0.6` and whose accompanying personal-tweak
> blocks reference Cineplex / Netflix-red, **not** NeutralFin. A single
> earlier curl in the audit session momentarily showed a NeutralFin
> import block, then three follow-up cache-busted curls reverted to
> Cineplex. This matches the §3b race rule in `04-theming-and-users.md`:
> the branding endpoint takes a complete object on every POST; whichever
> POST lands last wins. **A NeutralFin payload was applied and then
> overwritten by a sibling Cineplex POST.** The render the owner is
> seeing is therefore not NeutralFin — it is Cineplex with a stale set
> of personal tweaks layered on top, plus a critical-path `<style>` in
> `index.html` that pre-paints the page in Netflix red. That mismatch
> alone is the single highest-impact root cause; everything else below
> is secondary.

---

## 1. Visual contract — what NeutralFin should look like

Sourced from <https://github.com/KartoffelChipss/NeutralFin> README and
the upstream minified CSS at
`https://cdn.jsdelivr.net/gh/KartoffelChipss/NeutralFin@1.3.0/theme/neutralfin-minified.css`.

| Aspect | NeutralFin contract |
|---|---|
| Tagline | *"a sleek black and grey color scheme for a more neutral and modern look"* |
| Lineage | Built on ElegantFin (GPL-2.0). Bundles Jellyfin Lucide icons for the modern icon set. |
| Page background | **Gradient** between `--darkerGradientPoint #131313` and `--lighterGradientPoint #1e1e1e` (0deg). NOT pure `#000`. |
| Card background | `--cardBackgroundGradient` (same two-stop dark gradient). NOT pure `#000`. |
| Header / drawer surface | `--headerColor rgba(40,40,40,0.5)` and `--drawerColor rgba(40,40,40,0.9)` — translucent over a blurred backdrop. |
| Accent (UI) | `--uiAccentColor rgb(130,130,130)` — **mid-grey, not coloured**. |
| Active / focus tint | `--activeColor rgb(100,100,100)` — slightly darker grey. |
| Borders | `--borderColor rgb(71,71,71)` (mid), `--darkerBorderColor rgb(51,51,51)`, `--lighterBorderColor rgba(255,255,255,0.2)` — subtle hierarchy across cards/sections. |
| Selector bg | `--selectorBackgroundColor rgb(60,60,60)`. |
| Text | `--textColor rgb(209,213,219)` (off-white, not pure white). `--dimTextColor rgb(156,163,175)`. |
| Play button | `--btnMiniPlayColor rgb(41,154,93)` — the only saturated colour, used on play CTAs. |
| Delete button | `--btnDeleteColor rgb(169,29,29)` — saturated red, but ONLY for destructive confirms. |
| Recommended pairing | Owner enables **backdrops** in Jellyfin (`Display → Show backdrops`). The translucent header/drawer relies on having something to blur. |
| Minimum JF | Not stated. Demos shown on JF 10.11+. We're on 10.10.3 — selectors should largely match (Lucide icon refs may degrade gracefully). |

**Net visual impression:** subdued monochrome, soft gradients, mid-grey
accents, restrained borders, off-white text. The whole thing is a
*texture* of dark greys, not a flat black.

NeutralFin defines **none** of `--primary-background-color`,
`--background-color`, `--background-color-alpha`,
`--card-background-color`, or `--mui-palette-primary-main`. Those are
Jellyfin's own variables; NeutralFin lets Jellyfin's defaults pass
through and skins via its own `--darkerGradientPoint` /
`--lighterGradientPoint` / `--headerColor` / `--drawerColor` set.

---

## 2. Live state at audit time

**`/Branding/Configuration` (anon)** and
**`/System/Configuration/branding` (authed)** both return identical
payload, 25 225 chars of `CustomCss`. Theme banner comments name
**Cineplex v1.0.6**. Sole `@import` is
`https://cdn.jsdelivr.net/gh/MRunkehl/cineplex@v1.0.6/cineplex.css`.

`!important` count in CustomCss: **17**.

`#E50914` occurrences in CustomCss: **0**.

Inline critical-path `<style>` block in
`/jellyfin/jellyfin-web/index.html` (bind-mounted from
`web-overrides/index.html`, 93 lines total): forces `#000000` on
shell wrappers AND `#E50914` on `.raised, .button-submit,
.emby-button[type=submit], button[type=submit]`. **1 occurrence of
`#E50914`**, **8 occurrences of `ARRFLIX`** (title, shim, comments).

ARRFLIX wordmark PNG embedded in CustomCss: **235 × 85 px**,
aspect ratio **2.765**.

---

## 3. Drift table — every rule in current CustomCss + index.html

For each block, classify as KEEP (compatible with NeutralFin),
DROP (legacy / harmful), or MODIFY (needs adjustment for NeutralFin).
Assumes the owner's intent was to be on NeutralFin.

| # | Source | Block | Classification | Reason |
|---|---|---|---|---|
| 1 | CustomCss | `@import cineplex@v1.0.6` | **DROP** | Wrong theme entirely. Owner wants NeutralFin. Replace with `@import KartoffelChipss/NeutralFin@1.3.0/theme/neutralfin-minified.css`. |
| 2 | CustomCss | `#castCollapsible, #guestCastCollapsible { display:none }` | **KEEP** | Personal preference, theme-agnostic. NeutralFin doesn't redefine these. |
| 3 | CustomCss | `.adminDrawerLogo img { content: url(<ARRFLIX 235×85 PNG>) }` | **KEEP** | NeutralFin defines no rule for this selector (verified). Override stands. Split-rule form (per §3a) preserved. |
| 4 | CustomCss | `.pageTitleWithLogo { background-image: url(<same PNG>) }` | **KEEP** | Same; NeutralFin doesn't touch this selector. |
| 5 | CustomCss | `.btnQuick { display:none }` | **KEEP** | Server-side disable in §4g still in effect. CSS belt-and-braces is fine on any theme. |
| 6 | CustomCss | `.headerSyncButton/.headerCastButton/.headerUserButton { display:none }` | **KEEP** | NeutralFin sets `width/height/border` on `.headerUserButton` — those rules become moot under `display:none`, no conflict. |
| 7 | CustomCss | `.MuiSlider-thumb { color/bg/border:#fff }` (+ hover halo) | **KEEP**, but reconsider | NeutralFin doesn't theme MUI sliders. White thumbs work, but they're a Cineplex-era decision when the rest of the chrome was Netflix-red/white/black. Against a monochrome grey theme, mid-grey thumbs would read more native. Low priority. |
| 8 | CustomCss | `:root { --primary-background-color:#000 !important; --background-color:#000 !important }` | **DROP** | **High-impact harm.** NeutralFin's whole aesthetic depends on the page background showing the gradient between `#131313` and `#1e1e1e`. Forcing `#000` flattens that gradient to a single pure black, killing the depth NeutralFin was designed to deliver. Owner literally cannot see NeutralFin's intent while these vars are clamped. |
| 9 | CustomCss | `html, body, .preload, .skinBody, .mainDrawerHandle { background-color:#000 !important }` | **DROP** | Same — clobbers NeutralFin's gradient surface. NeutralFin paints page bg via the gradient applied at body / wrapper level; this rule forces solid black underneath. |
| 10 | CustomCss | `.skinHeader/.skinHeader.semiTransparent/.skinHeader-withBackground/.mainAnimatedPages/#reactRoot/.dashboardDocument { background:#000 !important }` | **DROP** | Same. Notably `.skinHeader.semiTransparent` is the surface NeutralFin's `--headerColor rgba(40,40,40,0.5)` translucency renders OVER. Forcing `#000` underneath defeats the blur/translucency effect — the header becomes a flat black bar instead of a glassy panel. |
| 11 | CustomCss | `mypreferencesmenu` :has() block | **KEEP** | Personal tweak, theme-agnostic. JF 10.10.3 supports `:has()` in modern browsers; if a user is on Firefox <121 they'll see the link, but no harm to NeutralFin. |
| 12 | CustomCss | `.countIndicator { display:none }` | **KEEP**, but note | NeutralFin sets `background:#1f50bd; border:var(--defaultLighterBorder)` on this selector. Hiding it is fine and is what owner asked for; the NeutralFin styling becomes irrelevant under `display:none`. |
| 13 | index.html `<style>` | `:root { --primary-background-color:#000; --background-color:#000 }` (no `!important`) | **MODIFY (DROP the var lines)** | Same harm as row 8 but in a sneakier place: it's pre-bundle, paints before CustomCss arrives, then CustomCss row 8 keeps it pinned post-bundle. For NeutralFin to look right, both need to go. |
| 14 | index.html `<style>` | `html, body, .preload, .skinBody, .skinHeader, #reactRoot, .mainAnimatedPages { background:#000 !important; color:#fff !important }` | **MODIFY** | Drop `.skinHeader` from the selector list (so NeutralFin's translucent header isn't pre-painted black) and consider dropping the wrapper bg overrides entirely. The `color:#fff` is also more saturated than NeutralFin's off-white `rgb(209,213,219)` — fine for pre-bundle anti-flash but needs to NOT be `!important` post-bundle. The `!important` here outranks NeutralFin's inherited text colour. |
| 15 | index.html `<style>` | `.raised, .button-submit, .emby-button[type=submit], button[type=submit] { background:#E50914 !important; color:#fff !important }` | **DROP** | **Critical.** NeutralFin is monochrome — the play CTA is green (`--btnMiniPlayColor`), submits use grey accent, only `--btnDeleteColor` is red and only on destructive confirms. This block paints **every submit button Netflix-red**, including login → Sign In, settings → Save, library → Add. Owner did not ask for that on NeutralFin. This is the most jarring single visual conflict. |
| 16 | index.html `<script>` | `nukeSettings()` MutationObserver + `setInterval(...,1000)` | **KEEP** | Targets `mypreferencesmenu` only; doesn't mutate styles or layout. Does fire on every DOM mutation (could be tens per second on rich pages) but the work is one querySelectorAll scoped to a narrow attribute selector. No measurable layout thrash on a non-loaded page; on heavy lists it's the sort of thing to profile but not a "looks bad" cause. |
| 17 | index.html `<script>` | `lockTitle/lockFavicon` head observer + interval | **KEEP** | Cosmetic, unrelated to render quality. |

---

## 4. Variable conflict report

| NeutralFin variable | Default | Overridden by us? | Effect |
|---|---|---|---|
| `--darkerGradientPoint` | `#131313` | no | Gradient bottom intact … but masked by row 9 `body{bg:#000}` |
| `--lighterGradientPoint` | `#1e1e1e` | no | Gradient top intact … masked same way |
| `--headerColor` | `rgba(40,40,40,0.5)` | no | Translucent header colour intact … but row 10 paints `.skinHeader{bg:#000}` underneath, so the alpha composes against pure black instead of the gradient. Header reads flatter than NeutralFin intends. |
| `--drawerColor` | `rgba(40,40,40,0.9)` | no | OK — drawer bg unaffected by the wrapper-element rules. |
| `--borderColor` | `rgb(71,71,71)` | no | OK |
| `--uiAccentColor` | `rgb(130,130,130)` | no in CustomCss; **YES** indirectly via index.html row 15 (every submit button forced red) | Submit buttons should be grey-accented; instead they are `#E50914`. |
| `--activeColor` | `rgb(100,100,100)` | no | OK |
| `--textColor` | `rgb(209,213,219)` | partially — index.html row 14 sets `color:#fff !important` on body | Text is full white instead of the off-white NeutralFin uses. Subtle but cumulative. |
| `--btnMiniPlayColor` | `rgb(41,154,93)` | no | Play CTA still green, OK. |
| `--btnDeleteColor` | `rgb(169,29,29)` | no | Delete confirms still red, OK. |
| Jellyfin `--primary-background-color` | (Jellyfin default `#101010`-ish) | **YES** — row 8 + row 13 → `#000` | NeutralFin doesn't override this var; NeutralFin paints the gradient directly on `body`. Forcing `--primary-background-color:#000` doesn't break NeutralFin's body gradient (NeutralFin doesn't read this var) BUT the `body{bg:#000 !important}` rule that lives next to it DOES, because it sets the body bg directly and beats NeutralFin's lower-specificity body rule. |
| Jellyfin `--background-color` | Jellyfin default | **YES** — row 8 + row 13 → `#000` | Same — variable override harmless on its own; the wrapper rule next door is the real damage. |
| `--mui-palette-primary-main` | (MUI default) | no | OK; sliders/checkboxes keep MUI palette. |

---

## 5. Logo aspect ratio

ARRFLIX wordmark PNG: **235 × 85 px**, aspect **2.765 : 1**.

NeutralFin (and Cineplex) target three logo containers:

- `.adminDrawerLogo img` — admin sidebar drawer. Inherits sidebar
  width (~240 px on desktop). 235 × 85 fits naturally; replaces
  `<img>` source via `content:`. **Match: YES.**
- `.pageTitleWithLogo` — masthead `<div>` on dashboard / login pages.
  In NeutralFin this `<div>` is sized by `var(--appBarHeight) 5em`
  (header height) and the `background-image` is laid out with
  `background-size: contain` (NeutralFin / ElegantFin convention).
  At 5 em ≈ 80 px header height a 235 × 85 image will render at
  ~221 × 80 — fits the header band cleanly. **Match: YES**, no
  squish, no clip.
- `.detailLogo` — clear-logo on item detail pages (movies / shows).
  NeutralFin sizes this at `width:40%; height:25vh; background-position:bottom`
  with `background-size:contain` — designed for tall, near-square
  clear logos. A 2.765:1 wordmark will render small (height-limited
  by the 25vh box only at very narrow viewports; at 1080p it's
  width-limited at 40% = 768 px and height settles at ~278 px, well
  under 25vh = 270 px). Acceptable, no distortion. **Match: YES.**

**Verdict: Logo aspect ratio is fine. Not a render-quality root
cause.** A 235 × 85 wordmark is on the wide end of typical Jellyfin
custom logos but fits every container cleanly because both NeutralFin
and Cineplex use `background-size: contain` on the masthead.

---

## 6. Recommended fix list (impact-ranked, top = biggest visual win)

> **Read-only audit. None of these have been applied.** Owner sign-off
> required before any branding POST.

1. **Apply NeutralFin (currently NOT applied).** Replace the
   `@import` line in CustomCss to point at
   `https://cdn.jsdelivr.net/gh/KartoffelChipss/NeutralFin@1.3.0/theme/neutralfin-minified.css`.
   (Verify the live `Branding/Configuration` reflects this *after* the
   POST, and that no sibling agent is racing the endpoint — see §3b
   operational rule. Make this POST the LAST POST in the sequence.)
2. **Drop the pure-black background overrides** in CustomCss
   (drift-table rows 8, 9, 10). NeutralFin's whole texture is the
   `#131313 → #1e1e1e` gradient; clamping it to `#000` flattens it
   and is the single biggest cause of the "not as good as it should"
   feel.
3. **Drop `#E50914` from the index.html critical-path `<style>`**
   (drift-table row 15). On NeutralFin, every submit button suddenly
   being Netflix-red is the single most jarring visual conflict.
   Also drop `.skinHeader` from the wrapper bg list (row 14) and
   the `--primary-background-color/--background-color #000`
   declarations (row 13). What stays in the critical-path `<style>`
   should be: `html, body { background:#0e0e0e }` (close enough to
   NeutralFin's gradient midpoint to avoid pre-bundle flash without
   clamping the gradient post-bundle) and `color:#d1d5db` (the
   off-white NeutralFin uses) — both WITHOUT `!important` so the
   theme can take over once it loads.
4. **Reconsider the white slider thumbs** (row 7) once #1–3 land. If
   the owner still finds them too "Netflix" against a grey theme,
   change to `currentColor` or `var(--uiAccentColor)`. Low priority,
   purely taste.
5. **Audit the `!important` count** post-fix. Currently 17; once the
   black-bg wrapper rules drop, the count falls to ~10, all of which
   are legitimate (display:none overrides, logo content: replacements,
   slider thumb forces). NeutralFin's hover/focus states will then
   fire correctly because no `!important` rule is masking them.

---

## 7. Rollback note

If owner says "revert everything I had before the audit-driven fixes":

```bash
git checkout snapshot-2026-05-08-pre-elegantfin -- \
  snapshots/2026-05-08-pre-elegantfin/branding.json
# then POST that file's contents to /System/Configuration/branding
# (full restore command in snapshots/2026-05-08-pre-elegantfin/RESTORE.md)
```

That snapshot captures the **Cineplex era** state — the CustomCss in it
is the same Cineplex import that's live RIGHT NOW (modulo personal-tweak
appendices that were added after the snapshot). It does NOT contain a
NeutralFin import, because NeutralFin was never persisted long enough
to enter the canonical history; the §3e ElegantFin migration block in
`04-theming-and-users.md` documents an *intended* state that the owner
had asked for but which a sibling Cineplex POST has since silently
reverted.

For a clean reset to vanilla Jellyfin (no theme at all) before
re-trying NeutralFin:

```bash
curl -sS -X POST -H "X-Emby-Token: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"CustomCss":"","LoginDisclaimer":"Welcome to ARRFLIX - Private invite only service","SplashscreenEnabled":true}' \
  https://arrflix.s8n.ru/System/Configuration/branding
```

---

## 8. What was NOT touched during this audit

- No POST to `/System/Configuration/branding`.
- No edit to `web-overrides/index.html` or the bind-mounted
  `/jellyfin/jellyfin-web/index.html`.
- No `docker compose` action, no container restart.
- No git commit on `snapshots/`, no tag movement.
- Read-only over SSH; only `docker exec jellyfin sh -c '...'` shell
  invocations, all bounded to `wc -l` / `head` / `grep -c`.

---

## 9. Sign-off

- **Auditor:** s8n (audit pass, 2026-05-08)
- **Live theme at audit time:** Cineplex v1.0.6 (despite doc 04 §3e
  claiming ElegantFin + ARRFLIX recolor; despite owner believing the
  state is NeutralFin)
- **Doc 04 §3e accuracy:** stale — needs an §3f addendum after fixes
  documenting the NeutralFin migration and the race-loss that hid it.
- **Next step:** owner reviews this doc, decides whether to apply the
  fix list in §6. No work to be done on the live server until that
  review.
