ARRFLIX/docs/11-neutralfin-audit.md
s8n 667694adbf strip: remove Claude attribution from ROADMAP + audit docs
ROADMAP owner column 's8n' (was 'claude'). Audit-run-by lines in
docs/{11,14,16} reattributed to s8n. Removed CLAUDE.md memory ref
from docs/04 hosts-pin note.
2026-05-08 16:44:49 +01:00

261 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 #13 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.