@nomercy-entertainment/nomercy-video-player 2.1.2 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.1.3] — 2026-08-30
6
+
7
+ ### Fixed
8
+
9
+ - The playlist menu opened at the frame's full width on every title. The seasons and episode panes each carried a minimum that together reached the 52rem cap, and a card's thumbnail is a percentage of that pane, so the episode art drew larger than any other image in the player. The content minimum is 42rem now and the thumbnail takes 32% up to 176px.
10
+
5
11
  ## [2.1.2] — 2026-08-29
6
12
 
7
13
  ### Fixed
@@ -170,7 +170,7 @@ export class DesktopUiPlugin extends Plugin {
170
170
  this.player.activityTracking(false);
171
171
  this.lifecycle.addCleanup(() => this.player.activityTracking(true));
172
172
  this.initState();
173
- this.appendInlineStyles("/* -----------------------------------------------------------------------------\n * Copyright (c) NoMercy Entertainment\n *\n * Licensed under the Apache License, Version 2.0. See LICENSE for details.\n *\n * SPDX-License-Identifier: Apache-2.0\n * ----------------------------------------------------------------------------- */\n\n.nomercyplayer {\n\tcontainer-type: inline-size;\n\tposition: relative;\n\tdisplay: block;\n\toverflow: hidden;\n\tbackground-color: #000;\n\n\tvideo {\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tobject-fit: contain;\n\t\tz-index: 0;\n\t\toutline: 0;\n\t}\n\n\t.overlay {\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\tz-index: 20;\n\t\tfont-family: system-ui, sans-serif;\n\t\tcolor: #fff;\n\t\tpointer-events: none;\n\t}\n\n\t/* Disabling the plugin hides its whole overlay (a peer owns the screen). The\n\t* scoped .overlay selector outranks the UA `[hidden]` rule, so pin it here. */\n\t.overlay[hidden] {\n\t\tdisplay: none;\n\t}\n\n\t/* ── Top bar ──────────────────────────────────────────────────────── */\n\t.top-bar {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tpadding: 16px 16px 48px 16px;\n\t\tdisplay: flex;\n\t\talign-items: flex-start;\n\t\tjustify-content: space-between;\n\t\tbackground: linear-gradient(to bottom, rgba(0, 0, 0, 0.85), rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0));\n\t\tpointer-events: none;\n\t\ttransition:\n\t\t\topacity 0.25s ease,\n\t\t\ttransform 0.25s ease;\n\t}\n\n\t/* The user-agent `[hidden] { display: none }` rule loses to the\n * .top-bar-right specificity below, so setting `right.hidden = true`\n * from the plugin (hideTitle option) silently fails. Explicit override\n * at matching specificity. Applies to both columns so future hide-side\n * toggles work without surprises. */\n\t.top-bar-left[hidden],\n\t.top-bar-right[hidden] {\n\t\tdisplay: none;\n\t}\n\n\t.top-bar-left {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tpointer-events: auto;\n\t}\n\n\t.top-bar-right {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 0.5rem;\n\t\talign-items: flex-end;\n\t\ttext-align: right;\n\t\tmax-width: 60%;\n\t}\n\n\t.show-info {\n\t\tfont-size: 0.88rem;\n\t\tfont-weight: 600;\n\t\tcolor: rgba(255, 255, 255, 0.75);\n\t\tmargin-bottom: 2px;\n\t}\n\n\t.title {\n\t\tfont-size: 1.05rem;\n\t\tfont-weight: 700;\n\t\ttext-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);\n\t}\n\n\t.back-btn,\n\t.cast-btn,\n\t.close-btn {\n\t\tpointer-events: auto;\n\t\tbackground: rgba(0, 0, 0, 0.35);\n\t\tborder: none;\n\t\tcolor: #fff;\n\t\twidth: 40px;\n\t\theight: 40px;\n\t\tborder-radius: 50%;\n\t\tcursor: pointer;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\ttransition: background 0.18s ease;\n\t\tmargin-right: 8px;\n\t}\n\n\t.back-btn:hover,\n\t.cast-btn:hover,\n\t.close-btn:hover {\n\t\tbackground: color-mix(in srgb, #fff 10%, rgba(0, 0, 0, 0.35));\n\t}\n\n\t.back-btn[hidden],\n\t.cast-btn[hidden],\n\t.close-btn[hidden] {\n\t\tdisplay: none !important;\n\t}\n\n\t/* ── Center play button ──────────────────────────────────────────── */\n\t.center {\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tpointer-events: none;\n\t}\n\n\t.center-btn {\n\t\tpointer-events: auto;\n\t\tbackground: rgba(0, 0, 0, 0.45);\n\t\tborder: none;\n\t\tcolor: #fff;\n\t\twidth: 80px;\n\t\theight: 80px;\n\t\tborder-radius: 50%;\n\t\tcursor: pointer;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tbackdrop-filter: blur(2px);\n\t\ttransition:\n\t\t\tbackground 0.18s ease,\n\t\t\ttransform 0.18s ease,\n\t\t\topacity 0.18s ease;\n\t}\n\n\t.center-btn:hover {\n\t\tbackground: color-mix(in srgb, #fff 10%, rgba(0, 0, 0, 0.45));\n\t\ttransform: scale(1.08);\n\t}\n\n\t/* Center button is a one-shot affordance — visible until the user clicks\n * it (or any touch zone triggers play). Once dismissed it stays hidden\n * and the touch zones own play/pause. */\n\t.center.dismissed .center-btn {\n\t\topacity: 0;\n\t\tpointer-events: none;\n\t}\n\n\t/* Buffering ring — sits inside the 80px center-btn with a small inset,\n * centered on the same point so it reads as a halo around the play icon.\n * Centering uses calc() offsets (not a translate) so the SVG's rotation\n * animation doesn't fight the spinner box's own transform. */\n\t.spinner {\n\t\tposition: absolute;\n\t\ttop: calc(50% - 36px);\n\t\tleft: calc(50% - 36px);\n\t\twidth: 72px;\n\t\theight: 72px;\n\t\tdisplay: none;\n\t\tpointer-events: none;\n\t}\n\n\t.spinner svg {\n\t\tdisplay: block;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\ttransform-origin: center;\n\t\tanimation: nm-spin 0.9s linear infinite;\n\t}\n\n\t/* Two sources, on purpose. `buffering` is core's — its rules table raises it\n\ton `waiting`/`stalled` and drops it on `canplay`. `busy` is this plugin's own\n\tload feedback, and it lives on `.center` so the two never overwrite each\n\tother on the same element. The spinner is the union of both. */\n\t&.buffering .spinner,\n\t.center.busy .spinner {\n\t\tdisplay: block;\n\t}\n\n\t/* ── Center status / toast line ──────────────────────────────────── */\n\t.player-message {\n\t\tposition: absolute;\n\t\ttop: 48px;\n\t\tleft: 50%;\n\t\ttransform: translateX(-50%);\n\t\tmax-width: 80%;\n\t\tpadding: 8px 16px;\n\t\tbackground: rgba(20, 22, 30, 0.85);\n\t\tcolor: #fff;\n\t\tfont-size: 0.875rem;\n\t\tborder-radius: 8px;\n\t\topacity: 0;\n\t\tpointer-events: none;\n\t\ttransition: opacity 0.2s ease;\n\t\tz-index: 95;\n\t}\n\n\t.player-message.visible {\n\t\topacity: 1;\n\t}\n\n\t/* ── Bottom bar (v1: bottom-bar > bottom-bar-shadow + top-row + bottom-row) ── */\n\t.bottom-bar {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 8px;\n\t\talign-items: center;\n\t\tmargin-top: auto;\n\t\twidth: 100%;\n\t\tz-index: 10;\n\t\tpadding-bottom: 8px;\n\t\tpointer-events: none;\n\t\ttransition:\n\t\t\topacity 0.25s ease,\n\t\t\ttransform 0.25s ease;\n\t}\n\n\t.bottom-bar > * {\n\t\tpointer-events: auto;\n\t}\n\n\t.bottom-bar-shadow {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tbottom: 0;\n\t\theight: calc(100% + 24px);\n\t\tpointer-events: none;\n\t\tbackground: linear-gradient(to top, rgba(0, 0, 0, 0.85), rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0));\n\t\tz-index: 0;\n\t}\n\n\t.top-row {\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\tgap: 8px;\n\t\talign-items: center;\n\t\theight: 8px;\n\t\tpadding: 0 24px;\n\t\twidth: 100%;\n\t\tbox-sizing: border-box;\n\t\tmargin-top: 16px;\n\t\tz-index: 1;\n\t}\n\n\t.bottom-row {\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tgap: 2px;\n\t\theight: 40px;\n\t\tpadding: 4px 16px;\n\t\twidth: 100%;\n\t\tbox-sizing: border-box;\n\t\tz-index: 1;\n\t}\n\n\t/* ── slider-bar tree (v1) ────────────────────────────────────────── */\n\t.slider-bar {\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\theight: 8px;\n\t\twidth: 100%;\n\t\tbackground: rgba(255, 255, 255, 0.2);\n\t\tborder-radius: 9999px;\n\t\tpointer-events: auto;\n\t\tcursor: pointer;\n\t\ttransition: height 140ms ease-out;\n\t}\n\n\t/* The strip a pointer can hit, which is not the strip that is drawn.\n\t*\n\t* The bar draws 8px and used to be targetable in exactly those 8 — measured\n\t* row by row with elementFromPoint on the running player: 4 rows above the\n\t* centre, 3 below, and the rows either side handed to the touch-zone\n\t* overlay behind it. The native chrome gives the same bar a 32dp strip and\n\t* centres the drawn bar inside it, which is the height a pointer is aimed\n\t* with rather than the height a bar is read at.\n\t*\n\t* Insets rather than a centred fixed height, because the space above the bar\n\t* and the space below it are not the same size. 16 up is `.top-row`'s whole\n\t* `margin-top` and stops on `.bottom-bar`'s own top edge; 12 down is the\n\t* 8px gap between the rows and stops on `.bottom-row`'s. Between them the\n\t* strip is 32 rows, the height the native chrome gives the same bar, and it\n\t* takes not one row from anything — the play button measures 40 of 40\n\t* before and after.\n\t*\n\t* A centred 32px box was tried first and came back 28: it reached four rows\n\t* INTO `.bottom-row`, whose box spans the full width, so the controls took\n\t* them back everywhere rather than only over a button. Raising\n\t* `.bottom-row`'s z-index to defend them changed nothing for the same\n\t* reason.\n\t*\n\t* z-index 0 so every layer above it inside the bar — buffer at 1, the\n\t* chapter bar at 5, the nipple and the preview pop — keeps its own clicks. */\n\t.slider-bar::before {\n\t\tcontent: '';\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t\ttop: -16px;\n\t\tbottom: -12px;\n\t\tz-index: 0;\n\t}\n\n\t/* Pointer devices grow when hovering anywhere in the top-row strip\n\t* (broader hit area than the bar itself) or while actively scrubbing. */\n\t@media (hover: hover) {\n\t\t.top-row:hover .slider-bar,\n\t\t.slider-bar.slider-scrubbing {\n\t\t\theight: 12px;\n\t\t}\n\t}\n\n\t.slider-bar.has-chapters {\n\t\tbackground: transparent;\n\t}\n\n\t.slider-buffer {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\ttop: 0;\n\t\theight: 100%;\n\t\twidth: 0;\n\t\tbackground: rgba(255, 255, 255, 0.4);\n\t\tborder-radius: 9999px;\n\t\toverflow: hidden;\n\t\tpointer-events: none;\n\t\tz-index: 1;\n\t}\n\n\t.slider-hover {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\ttop: 0;\n\t\theight: 100%;\n\t\twidth: 0;\n\t\tbackground: rgba(255, 255, 255, 0.3);\n\t\tborder-radius: 9999px;\n\t\toverflow: hidden;\n\t\tpointer-events: none;\n\t\tz-index: 0;\n\t}\n\n\t.slider-progress {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\ttop: 0;\n\t\theight: 100%;\n\t\twidth: 0;\n\t\tbackground: #fff;\n\t\tborder-radius: 9999px;\n\t\toverflow: hidden;\n\t\tpointer-events: none;\n\t\tz-index: 10;\n\t}\n\n\t.slider-bar.has-chapters .slider-progress {\n\t\tdisplay: none;\n\t}\n\n\t.slider-bar.has-chapters .slider-hover {\n\t\tdisplay: none;\n\t}\n\n\t.slider-bar.has-chapters .slider-buffer {\n\t\tdisplay: none;\n\t}\n\n\t.chapter-bar {\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tborder-radius: 9999px;\n\t\toverflow: visible;\n\t\tpointer-events: none;\n\t\tz-index: 5;\n\t}\n\n\t.chapter-marker {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\theight: 100%;\n\t\tmin-width: 2px;\n\t\toverflow: hidden;\n\t\tborder-radius: 2px;\n\t}\n\n\t.chapter-marker-bg {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tbackground: rgba(255, 255, 255, 0.2);\n\t\tborder-radius: 2px;\n\t\tz-index: 0;\n\t}\n\n\t.chapter-marker-buffer {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tbackground: rgba(255, 255, 255, 0.4);\n\t\ttransform-origin: left;\n\t\ttransform: scaleX(0);\n\t\tborder-radius: 2px;\n\t\tz-index: 5;\n\t\tpointer-events: none;\n\t}\n\n\t.chapter-marker-hover {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tbackground: rgba(229, 231, 235, 1);\n\t\ttransform-origin: left;\n\t\ttransform: scaleX(0);\n\t\tborder-radius: 2px;\n\t\tz-index: 10;\n\t\tpointer-events: none;\n\t}\n\n\t.chapter-marker-progress {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tbackground: #fff;\n\t\ttransform-origin: left;\n\t\ttransform: scaleX(0);\n\t\tborder-radius: 2px;\n\t\tz-index: 20;\n\t\tpointer-events: none;\n\t}\n\n\t.slider-nipple {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\twidth: 16px;\n\t\theight: 16px;\n\t\tbackground: #fff;\n\t\tborder-radius: 9999px;\n\t\ttransform: translate(-50%, -25%);\n\t\tdisplay: none;\n\t\tz-index: 30;\n\t\tpointer-events: none;\n\t}\n\n\t.slider-bar:hover .slider-nipple,\n\t.slider-bar.slider-scrubbing .slider-nipple {\n\t\tdisplay: block;\n\t\twidth: 24px;\n\t\theight: 24px;\n\t\ttransform: translate(-50%, -25%);\n\t}\n\n\t.slider-pop {\n\t\tposition: absolute;\n\t\tbottom: 24px;\n\t\tleft: 0;\n\t\ttransform: translateX(-50%);\n\t\tbackground: rgba(20, 20, 25, 0.95);\n\t\tborder-radius: 6px;\n\t\toverflow: hidden;\n\t\tpadding-bottom: 4px;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 4px;\n\t\ttext-align: center;\n\t\tpointer-events: none;\n\t\tz-index: 30;\n\t\topacity: var(--visibility, 0);\n\t\ttransition: opacity 0.12s ease;\n\t\tfont-weight: 600;\n\t\tmin-width: 60px;\n\t}\n\n\t.slider-pop-image {\n\t\twidth: 0;\n\t\theight: 0;\n\t\tbackground-repeat: no-repeat;\n\t\tbackground-size: auto auto;\n\t\tmargin: 0 auto;\n\t}\n\n\t.slider-pop-image[style*='background-image'] {\n\t\tmargin: 6px 6px 0;\n\t\tborder-radius: 3px;\n\t\tbox-sizing: content-box;\n\t}\n\n\t.slider-pop-text {\n\t\tfont-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n\t\tfont-size: 0.78rem;\n\t\tpadding: 0 8px;\n\t\tcolor: #fff;\n\t}\n\n\t.chapter-text {\n\t\tfont-size: 0.72rem;\n\t\tcolor: rgba(255, 255, 255, 0.75);\n\t\tpadding: 0 8px;\n\t}\n\n\t.chapter-text:empty {\n\t\tdisplay: none;\n\t}\n\n\t/* ── Round 40x40 createUiButton equivalent ───────────────────────── */\n\t.btn {\n\t\tcursor: pointer;\n\t\tfill: #fff;\n\t\tcolor: #fff;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\theight: 40px;\n\t\twidth: 40px;\n\t\tmin-width: 40px;\n\t\tpadding: 8px;\n\t\tborder-radius: 9999px;\n\t\tbackground: transparent;\n\t\tborder: 2px solid transparent;\n\t\tpointer-events: auto;\n\t\tposition: relative;\n\t\ttransition: transform 0.15s ease;\n\t}\n\n\t.btn:hover svg {\n\t\ttransform: scale(1.1);\n\t}\n\n\t.btn:focus-visible {\n\t\toutline: 2px solid rgba(255, 255, 255, 0.5);\n\t\toutline-offset: -2px;\n\t}\n\n\t.btn[hidden] {\n\t\tdisplay: none !important;\n\t}\n\n\t.btn[disabled] {\n\t\t/* Stay laid out so the bottom-row arrangement doesn't shift, but\n * make it clear the action isn't available right now. */\n\t\topacity: 0.3;\n\t\tcursor: not-allowed;\n\t\tpointer-events: none;\n\t}\n\n\t.btn[disabled]:hover svg {\n\t\ttransform: none;\n\t}\n\n\t.btn svg {\n\t\ttransition: transform 0.18s ease;\n\t\tpointer-events: none;\n\t}\n\n\t/* Icon holder — keeps SVG swaps from wiping the sibling .tooltip span. */\n\t.btn-icon {\n\t\tdisplay: inline-flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tpointer-events: none;\n\t}\n\n\t/* Filled / outlined path swap — every bottom-bar icon inverts on hover.\n * Hide the hover-variant path globally so menu/non-button icons stay clean;\n * .btn:hover opts in to the swap for the bottom-bar buttons. */\n\t.icon-hover {\n\t\tdisplay: none;\n\t}\n\n\t.btn:hover .icon-normal,\n\t.btn.is-active .icon-normal {\n\t\tdisplay: none;\n\t}\n\n\t.btn:hover .icon-hover,\n\t.btn.is-active .icon-hover {\n\t\tdisplay: inline;\n\t}\n\n\t/* ── Volume container ────────────────────────────────────────────── */\n\t.volume-container {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\toverflow: visible;\n\t\tpointer-events: auto;\n\t}\n\n\t.volume-slider {\n\t\tappearance: none;\n\t\twidth: 0;\n\t\topacity: 0;\n\t\theight: 8px;\n\t\tbackground: linear-gradient(\n\t\t\tto right,\n\t\t\t#fff 0%,\n\t\t\t#fff var(--vol-pct, 100%),\n\t\t\trgba(255, 255, 255, 0.3) var(--vol-pct, 100%)\n\t\t);\n\t\tborder-radius: 9999px;\n\t\tcursor: pointer;\n\t\talign-self: center;\n\t\ttransition:\n\t\t\twidth 0.3s ease,\n\t\t\topacity 0.3s ease,\n\t\t\tmargin 0.3s ease,\n\t\t\theight 80ms ease-out;\n\t}\n\n\t.volume-container:hover .volume-slider,\n\t.volume-container:focus-within .volume-slider {\n\t\twidth: 80px;\n\t\topacity: 1;\n\t\tmargin: 0 8px;\n\t}\n\n\t.volume-slider::-webkit-slider-thumb {\n\t\tappearance: none;\n\t\twidth: 12px;\n\t\theight: 12px;\n\t\t/* Chromium: appearance:none thumbs center on track; nudge down slightly */\n\t\tmargin-top: 2px;\n\t\tbackground: #fff;\n\t\tborder-radius: 50%;\n\t}\n\n\t/* ── Vertical volume slider (popup above mute button) ───────────── */\n\t.volume-container {\n\t\tposition: relative;\n\t}\n\n\t/* Hide the horizontal slider when vertical mode is active. */\n\t.volume-container-vertical .volume-slider {\n\t\tdisplay: none;\n\t}\n\n\t.volume-slider-vertical {\n\t\tdisplay: none;\n\t\tposition: absolute;\n\t\tbottom: calc(100% + 8px);\n\t\tleft: 50%;\n\t\ttransform: translateX(-50%);\n\t\tbackground: rgba(20, 22, 30, 0.92);\n\t\tborder-radius: 8px;\n\t\tpadding: 12px 10px;\n\t\tz-index: 90;\n\t\tbackdrop-filter: blur(4px);\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t}\n\n\t.volume-slider-vertical.volume-slider-vertical-open {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tgap: 8px;\n\t}\n\n\t.vol-popup-mute {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tmin-width: 44px;\n\t\tmin-height: 44px;\n\t\tpadding: 0;\n\t\tbackground: transparent;\n\t\tborder: none;\n\t\tcolor: rgba(255, 255, 255, 0.8);\n\t\tborder-radius: 6px;\n\t\tcursor: pointer;\n\t}\n\n\t.vol-popup-mute:hover {\n\t\tcolor: #fff;\n\t\tbackground: rgba(255, 255, 255, 0.1);\n\t}\n\n\t.volume-slider-vertical-input {\n\t\tappearance: none;\n\t\twriting-mode: vertical-lr;\n\t\tdirection: rtl;\n\t\twidth: 4px;\n\t\theight: 80px;\n\t\tbackground: linear-gradient(\n\t\t\tto top,\n\t\t\t#fff 0%,\n\t\t\t#fff var(--vol-pct, 100%),\n\t\t\trgba(255, 255, 255, 0.3) var(--vol-pct, 100%)\n\t\t);\n\t\tborder-radius: 9999px;\n\t\tcursor: pointer;\n\t}\n\n\t.volume-slider-vertical-input::-webkit-slider-thumb {\n\t\tappearance: none;\n\t\twidth: 12px;\n\t\theight: 12px;\n\t\t/* Chromium centers appearance:none thumbs — see horizontal slider. */\n\t\tbackground: #fff;\n\t\tborder-radius: 50%;\n\t}\n\n\t/* ── Time labels ─────────────────────────────────────────────────── */\n\t.time {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tpointer-events: none;\n\t\tuser-select: none;\n\t\tfont-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n\t\tfont-size: 0.82rem;\n\t\tcolor: #ddd;\n\t}\n\n\t.current-time {\n\t\tmargin-left: 8px;\n\t}\n\n\t.remaining-time {\n\t\tmargin-right: 8px;\n\t\tpointer-events: auto;\n\t\tcursor: pointer;\n\t\tborder-radius: 4px;\n\t\tpadding: 0 4px;\n\t\ttransition: background 0.15s ease;\n\t}\n\n\t.remaining-time:hover {\n\t\tbackground: rgba(255, 255, 255, 0.1);\n\t}\n\n\t.divider {\n\t\tdisplay: flex;\n\t\tflex: 1;\n\t\tmin-width: 16px;\n\t}\n\n\t/* ── Menu frame (v1) ─────────────────────────────────────────────── */\n\t.menu-frame-dialog {\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\tbackground: transparent;\n\t\tborder: none;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tcolor: inherit;\n\t\tpointer-events: none;\n\t\tz-index: 50;\n\t}\n\n\t.menu-frame-dialog[open] {\n\t\tpointer-events: auto;\n\t}\n\n\t.menu-wrapper {\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\tpointer-events: none;\n\t\tcolor: #fff;\n\t}\n\n\t.menu-frame {\n\t\tposition: absolute;\n\t\ttop: 16px;\n\t\tright: 16px;\n\t\tbottom: 52px;\n\t\tdisplay: none;\n\t\tflex-direction: column;\n\t\theight: auto;\n\t\tmax-height: calc(100% - 2rem);\n\t\tmax-width: min(52rem, calc(100% - 2rem));\n\t\twidth: min-content;\n\t\toverflow: hidden;\n\t\tborder-radius: 8px;\n\t\tz-index: 50;\n\t}\n\n\t.menu-frame.open {\n\t\tdisplay: flex;\n\t}\n\n\t.menu-content {\n\t\tdisplay: flex;\n\t\tflex-direction: row;\n\t\tjustify-content: flex-end;\n\t\theight: 100%;\n\t\twidth: 100%;\n\t\tmargin-top: auto;\n\t\toverflow: hidden;\n\t}\n\n\t.main-menu {\n\t\tbackground: rgba(20, 20, 25, 0.95);\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 4px;\n\t\theight: auto;\n\t\tmax-height: 60vh;\n\t\tmax-height: 60dvh;\n\t\tmin-width: 16rem;\n\t\tmargin-top: auto;\n\t\toverflow: hidden;\n\t\tborder-radius: 8px;\n\t\tpointer-events: auto;\n\t}\n\n\t.menu-content.sub-menu-open .main-menu {\n\t\tdisplay: none;\n\t}\n\n\t.sub-menu {\n\t\tbackground: rgba(20, 20, 25, 0.95);\n\t\tdisplay: none;\n\t\tflex-direction: column;\n\t\tgap: 4px;\n\t\theight: auto;\n\t\tmax-height: 100%;\n\t\tmin-width: 16rem;\n\t\tmargin-top: auto;\n\t\toverflow: hidden;\n\t\tborder-radius: 8px;\n\t\tpointer-events: auto;\n\t\twidth: 100%;\n\t}\n\n\t.menu-content.sub-menu-open .sub-menu {\n\t\tdisplay: flex;\n\t}\n\n\t.menu-header {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tmin-height: 2.5rem;\n\t\tcolor: #fff;\n\t\tborder-bottom: 1px solid rgba(209, 213, 219, 0.2);\n\t\twidth: 100%;\n\t\tpadding: 6px 6px 6px 6px;\n\t\tbox-sizing: border-box;\n\t\tgap: 4px;\n\t}\n\n\t.menu-header .menu-button-text {\n\t\tfont-weight: 600;\n\t\tflex: 1;\n\t}\n\n\t.menu-header-close,\n\t.menu-header-back {\n\t\tbackground: transparent;\n\t\tborder: none;\n\t\tcolor: #fff;\n\t\tcursor: pointer;\n\t\twidth: 32px;\n\t\theight: 32px;\n\t\tflex: 0 0 32px;\n\t\tdisplay: inline-flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tborder-radius: 6px;\n\t}\n\n\t.menu-header-close {\n\t\tmargin-left: auto;\n\t}\n\n\t.menu-header-back:hover,\n\t.menu-header-close:hover {\n\t\tbackground: rgba(255, 255, 255, 0.08);\n\t}\n\n\t.menu-header > .menu-button-text:first-child {\n\t\tpadding-left: 16px;\n\t}\n\n\t.language-button {\n\t\twidth: 100%;\n\t\tbox-sizing: border-box;\n\t\theight: 32px;\n\t\tpadding: 4px 8px;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tborder-radius: 4px;\n\t\tbackground: transparent;\n\t\tborder: 1px solid transparent;\n\t\tcolor: #fff;\n\t\tcursor: pointer;\n\t\ttransition:\n\t\t\tbackground 0.18s ease,\n\t\t\toutline-color 0.18s ease;\n\t\ttext-align: left;\n\t\twhite-space: nowrap;\n\t}\n\n\t.language-button:hover {\n\t\tbackground: rgba(115, 115, 115, 0.5);\n\t}\n\n\t.language-button:focus-visible {\n\t\toutline: 2px solid #fff;\n\t\toutline-offset: -2px;\n\t}\n\n\t.language-button.is-active {\n\t\tbackground: rgba(255, 255, 255, 0.2);\n\t}\n\n\t.language-button .menu-button-text {\n\t\tcolor: #fff;\n\t\tcursor: pointer;\n\t\tfont-weight: 600;\n\t\tpadding-left: 8px;\n\t\tdisplay: flex;\n\t\tgap: 8px;\n\t\tline-height: normal;\n\t}\n\n\t.language-button .menu-button-icon-left {\n\t\twidth: 20px;\n\t\theight: 20px;\n\t\tflex: 0 0 20px;\n\t\tdisplay: inline-flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t}\n\n\t.language-button .menu-button-chevron {\n\t\twidth: 20px;\n\t\theight: 20px;\n\t\tflex: 0 0 20px;\n\t\tdisplay: inline-flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t}\n\n\t.language-button:not(:has(.menu-button-subtext)) .menu-button-chevron {\n\t\tmargin-left: auto;\n\t}\n\n\t.language-button .menu-button-check {\n\t\tmargin-left: auto;\n\t\twidth: 20px;\n\t\theight: 20px;\n\t\tflex: 0 0 20px;\n\t\tdisplay: none;\n\t\tcolor: #fff;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t}\n\n\t.language-button.is-active .menu-button-check {\n\t\tdisplay: inline-flex;\n\t}\n\n\t.language-button .menu-button-subtext {\n\t\t/* `margin-left` was declared twice — 8px, then auto — and the second\n\t\t* won, so the separation the first was asking for never existed. It is\n\t\t* invisible until a label is long enough to fill the row, and then\n\t\t* `auto` has no space left to give and the value sits against the last\n\t\t* letter: \"Transparantie achtergrondvak0%\".\n\t\t*\n\t\t* The push and the minimum gap are two different jobs, so they are two\n\t\t* different properties now. */\n\t\tmargin-left: auto;\n\t\tpadding-left: 8px;\n\t\tcolor: rgba(255, 255, 255, 0.6);\n\t\tfont-size: 10px;\n\t\tfont-weight: 600;\n\t\tpadding-right: 8px;\n\t\twhite-space: nowrap;\n\t\tline-height: 100%;\n\t}\n\n\t/* Consumer-supplied action rows (e.g. \"Search subtitles online…\") append\n\tafter a pane's own choices — a top divider sets them apart visually. */\n\t.language-button.menu-action-row {\n\t\tmargin-top: 4px;\n\t\tborder-top: 1px solid rgba(255, 255, 255, 0.12);\n\t}\n\n\t.sub-menu-content {\n\t\tdisplay: none;\n\t\tflex-direction: column;\n\t\tmax-height: 60vh;\n\t\tmax-height: 60dvh;\n\t\twidth: 100%;\n\t\toverflow-y: auto;\n\t\toverflow-x: hidden;\n\t}\n\n\t.sub-menu-content.is-open {\n\t\tdisplay: flex;\n\t}\n\n\t.scroll-container {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 4px;\n\t\toverflow-x: hidden;\n\t\toverflow-y: auto;\n\t\t/* One inset, declared once, on all four sides.\n\t\t*\n\t\t* This was `8px 0 8px 8px` — no right padding at all — with\n\t\t* `scrollbar-gutter: stable` standing in for it on that side. A gutter\n\t\t* is not a padding: its width is the browser's scrollbar width, which\n\t\t* differs by engine, platform and overlay-scrollbar setting, so the\n\t\t* right inset never equalled the left.\n\t\t*\n\t\t* Nor is the answer `both-edges`: a gutter is reserved IN ADDITION to\n\t\t* the padding, so 8px of padding became an 18px inset on both sides —\n\t\t* symmetric and twice as deep as anything else in the menu. The inset\n\t\t* is 8, and the scrollbar overlays it rather than reserving beside it. */\n\t\tpadding: 8px;\n\t\ttransition: all 0.3s ease;\n\t\twidth: 100%;\n\t\tmax-height: 50vh;\n\t\tmax-height: 50dvh;\n\t\tscrollbar-width: thin;\n\t\tscrollbar-color: rgba(255, 255, 255, 0.3) transparent;\n\t}\n\n\t/* ── Playlist sub-menu (two-pane, mirrors v1's createEpisodeMenu) ──────\n * playlist-menu (flex-row, gap:0)\n * ├─ sub-menu-content.seasons-pane (1/3 width, border-right)\n * │ ├─ menu-header (back / \"Seasons\" / close placeholder)\n * │ └─ scroll-container (season buttons; empty for movies)\n * └─ episode-menu (much wider — 63rem cap)\n * ├─ menu-header-main (close × only)\n * └─ scroll-container (rich-card episodes)\n *\n * playlist-menu-button (each card)\n * ├─ episode-menu-button-left\n * │ ├─ episode-menu-button-image\n * │ ├─ episode-menu-button-shadow\n * │ └─ episode-menu-progress-container\n * │ ├─ episode-menu-progress-box (episode label + duration)\n * │ └─ slider-container > progress-bar\n * └─ playlist-card-right (title + overview)\n * ──────────────────────────────────────────────────────────────────── */\n\t#playlist-menu.playlist-menu {\n\t\tdisplay: none;\n\t\tflex-direction: column;\n\t\tgap: 0;\n\t\twidth: 100%;\n\t\tmax-height: 60vh;\n\t\tmax-height: 60dvh;\n\t}\n\n\t#playlist-menu.playlist-menu.is-open {\n\t\tdisplay: flex;\n\t}\n\n\t.playlist-cols {\n\t\tdisplay: flex;\n\t\tflex-direction: row;\n\t\tflex: 1;\n\t\tmin-height: 0;\n\t\toverflow: hidden;\n\t}\n\n\t.seasons-pane {\n\t\tflex: 1;\n\t\twidth: 100%;\n\t\tmin-width: 16rem;\n\t\tborder-right: 2px solid rgba(107, 114, 128, 0.2);\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\toverflow: hidden;\n\t\tmin-height: 0;\n\t}\n\n\t.playlist-flat .seasons-pane {\n\t\tdisplay: none;\n\t}\n\n\t.episode-menu {\n\t\tflex: 1;\n\t\twidth: 100%;\n\t\tmin-width: 36rem;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\toverflow: hidden;\n\t\tmin-height: 0;\n\t}\n\n\t.episode-menu .scroll-container {\n\t\tmax-height: none;\n\t\tflex: 1;\n\t}\n\n\t/* ── Portrait playlist: full-viewport overlay ──────────────────────\n * In portrait the menu-frame expands to fill the available viewport\n * (inset 0 with bottom clearance for the control bar). The playlist\n * menu fills that frame vertically. Seasons and episodes stack into a\n * single scroll column. Touch targets on each card stay ≥ 44px. */\n\t&[data-orientation='portrait'] {\n\t\t/* Portrait full-viewport ONLY applies when the PLAYLIST sub-menu is\n * the active one (.is-open). Subtitle / audio / quality / speed / aspect\n * sub-menus keep their default popover sizing — they were fine.\n * Menu overlays the bottom bar too (no 56px reserve) for full real estate. */\n\t\t.menu-frame:has(.playlist-menu.is-open) {\n\t\t\ttop: 0;\n\t\t\tright: 0;\n\t\t\tleft: 0;\n\t\t\tbottom: 0;\n\t\t\tmax-width: 100%;\n\t\t\twidth: 100%;\n\t\t\tmax-height: 100%;\n\t\t\theight: 100%;\n\t\t\tborder-radius: 0;\n\t\t}\n\n\t\t.playlist-menu.is-open {\n\t\t\twidth: 100%;\n\t\t\tmax-height: 100%;\n\t\t\tborder-radius: 0;\n\t\t}\n\n\t\t#playlist-menu.playlist-menu {\n\t\t\tmax-height: 100%;\n\t\t\theight: 100%;\n\t\t}\n\n\t\t.playlist-cols {\n\t\t\tflex-direction: column;\n\t\t\toverflow-y: auto;\n\t\t\tflex: 1;\n\t\t\tmin-height: 0;\n\t\t}\n\n\t\t.seasons-pane {\n\t\t\tmin-width: 0;\n\t\t\tborder-right: none;\n\t\t\tborder-bottom: 2px solid rgba(107, 114, 128, 0.2);\n\t\t\tmin-height: 0;\n\t\t\theight: auto;\n\t\t\toverflow: visible;\n\t\t\twidth: 100%;\n\t\t}\n\n\t\t.episode-menu {\n\t\t\tmin-width: 0;\n\t\t\tmin-height: 0;\n\t\t\tflex: 1;\n\t\t\toverflow-y: auto;\n\t\t\twidth: 100%;\n\t\t}\n\n\t\t.episode-menu .scroll-container {\n\t\t\tmax-height: none;\n\t\t\tflex: 1;\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tgap: 6px;\n\t\t\t/* Inherited, not restated. This pane carried its own symmetric\n\t\t\t* padding because the base rule had none on the right — so one pane\n\t\t\t* looked correct and every other one did not. The base is symmetric\n\t\t\t* now and a second number here would be the drift starting again. */\n\t\t}\n\n\t\t.playlist-menu-button {\n\t\t\tpadding: 8px;\n\t\t\tgap: 10px;\n\t\t\talign-items: flex-start;\n\t\t}\n\n\t\t.episode-menu-button-left {\n\t\t\twidth: 38%;\n\t\t\tflex-basis: 38%;\n\t\t\tmin-width: 0;\n\t\t\tmax-width: 180px;\n\t\t\talign-self: flex-start;\n\t\t\taspect-ratio: 16 / 9;\n\t\t}\n\n\t\t.playlist-menu-button-overview {\n\t\t\t-webkit-line-clamp: 3;\n\t\t\tline-clamp: 3;\n\t\t}\n\n\t\t.playlist-menu-button-title {\n\t\t\tfont-size: 0.95rem;\n\t\t\tmargin-bottom: 4px;\n\t\t}\n\t}\n\n\t.playlist-menu-button {\n\t\tdisplay: flex;\n\t\tbox-sizing: border-box;\n\t\tgap: 8px;\n\t\tpadding: 8px;\n\t\tborder-radius: 8px;\n\t\tbackground: transparent;\n\t\tborder: 1px solid transparent;\n\t\tcolor: #fff;\n\t\tcursor: pointer;\n\t\ttransition: background 0.18s ease;\n\t\ttext-align: left;\n\t\twidth: 100%;\n\t}\n\n\t.playlist-menu-button:hover {\n\t\tbackground: rgba(115, 115, 115, 0.2);\n\t}\n\n\t.playlist-menu-button:focus-visible {\n\t\toutline: 2px solid #fff;\n\t\toutline-offset: -2px;\n\t}\n\n\t.playlist-menu-button.is-active {\n\t\tbackground: rgba(255, 255, 255, 0.1);\n\t}\n\n\t.playlist-menu-button.is-active::before {\n\t\tcontent: '';\n\t\tposition: absolute;\n\t}\n\n\t.episode-menu-button-left {\n\t\tposition: relative;\n\t\theight: auto;\n\t\toverflow: hidden;\n\t\tborder-radius: 6px;\n\t\talign-self: center;\n\t\twidth: 37.5%;\n\t\tflex: 0 0 37.5%;\n\t\taspect-ratio: 16 / 9;\n\t\tbackground: rgba(255, 255, 255, 0.05);\n\t}\n\n\t.episode-menu-button-image {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tobject-fit: cover;\n\t\taspect-ratio: 16 / 9;\n\t}\n\n\t.episode-menu-button-shadow {\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\tbackground: linear-gradient(\n\t\t\t0deg,\n\t\t\trgba(0, 0, 0, 0.85) 0%,\n\t\t\trgba(0, 0, 0, 0.7) 25%,\n\t\t\trgba(0, 0, 0, 0) 50%,\n\t\t\trgba(0, 0, 0, 0) 100%\n\t\t);\n\t\tbox-shadow:\n\t\t\tinset 0 1px 0 rgba(255, 255, 255, 0.24),\n\t\t\tinset 0 -1px 0 rgba(0, 0, 0, 0.24),\n\t\t\tinset 0 -2px 0 rgba(0, 0, 0, 0.24);\n\t\tpointer-events: none;\n\t}\n\n\t.episode-menu-progress-container {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tpadding: 0 12px;\n\t}\n\n\t.episode-menu-progress-box {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t\theight: auto;\n\t\tmargin-bottom: 4px;\n\t\tpadding: 0 4px;\n\t}\n\n\t.progress-item-text,\n\t.progress-duration {\n\t\tfont-size: 0.7rem;\n\t\tcolor: rgba(255, 255, 255, 0.85);\n\t}\n\n\t.slider-container {\n\t\tbackground: rgba(107, 114, 128, 0.8);\n\t\theight: 4px;\n\t\tmargin: 0 4px 8px;\n\t\toverflow: hidden;\n\t\tborder-radius: 4px;\n\t\tdisplay: none;\n\t}\n\n\t.slider-container.has-progress {\n\t\tdisplay: flex;\n\t}\n\n\t.slider-container .progress-bar {\n\t\tbackground: #fff;\n\t\theight: 100%;\n\t\twidth: 0;\n\t\tborder-radius: 4px;\n\t}\n\n\t.playlist-card-right {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 4px;\n\t\tpointer-events: none;\n\t\ttext-align: left;\n\t\twidth: 75%;\n\t\tflex: 1;\n\t\tmin-width: 0;\n\t}\n\n\t.playlist-menu-button-title {\n\t\tfont-weight: 700;\n\t\tcolor: #fff;\n\t\tline-height: 1.25;\n\t\tdisplay: -webkit-box;\n\t\t-webkit-line-clamp: 2;\n\t\tline-clamp: 2;\n\t\t-webkit-box-orient: vertical;\n\t\toverflow: hidden;\n\t}\n\n\t.playlist-menu-button-overview {\n\t\tfont-size: 0.7rem;\n\t\tline-height: 1rem;\n\t\tcolor: rgba(255, 255, 255, 0.8);\n\t\tdisplay: -webkit-box;\n\t\t-webkit-line-clamp: 4;\n\t\tline-clamp: 4;\n\t\t-webkit-box-orient: vertical;\n\t\toverflow: hidden;\n\t}\n\n\t.playlist-menu-button-watched {\n\t\tfont-size: 0.65rem;\n\t\tcolor: rgba(255, 255, 255, 0.55);\n\t\tfont-variant-numeric: tabular-nums;\n\t\twhite-space: nowrap;\n\t\toverflow: hidden;\n\t\ttext-overflow: ellipsis;\n\t}\n\n\t/* ── Active / inactive overlay states ─────────────────────────────── */\n\t&.active .top-bar,\n\t&.active .bottom-bar {\n\t\topacity: 1;\n\t\ttransform: translateY(0);\n\t}\n\n\t&.inactive .top-bar {\n\t\topacity: 0;\n\t\ttransform: translateY(-100%);\n\t\tpointer-events: none;\n\t}\n\n\t&.inactive .bottom-bar {\n\t\topacity: 0;\n\t\ttransform: translateY(100%);\n\t\tpointer-events: none;\n\t}\n\n\t/* ── Button tooltips ──────────────────────────────────────────────── */\n\t.tooltip {\n\t\tposition: absolute;\n\t\tbottom: calc(100% + 8px);\n\t\tleft: 50%;\n\t\ttransform: translateX(-50%);\n\t\tbackground: rgba(20, 22, 30, 0.92);\n\t\tcolor: #fff;\n\t\tfont-size: 0.75rem;\n\t\tline-height: 2;\n\t\tpadding: 8px 16px;\n\t\tborder-radius: 8px;\n\t\twhite-space: nowrap;\n\t\tpointer-events: none;\n\t\tz-index: 100;\n\t\topacity: 0;\n\t\ttransition: opacity 0.15s ease;\n\t\tbackdrop-filter: blur(4px);\n\t}\n\n\t.tooltip.tooltip-visible {\n\t\topacity: 1;\n\t}\n\n\t.tooltip::after {\n\t\tcontent: '';\n\t\tposition: absolute;\n\t\ttop: 100%;\n\t\tleft: var(--arrow-x, 50%);\n\t\ttransform: translateX(-50%);\n\t\tborder: 5px solid transparent;\n\t\tborder-top-color: rgba(20, 22, 30, 0.92);\n\t}\n\n\t/* ── Keyboard shortcuts overlay ───────────────────────────────────── */\n\t#nmplayer-keybinds-dialog {\n\t\tscrollbar-width: thin;\n\t\tscrollbar-color: rgba(255, 255, 255, 0.2) transparent;\n\t\toverflow-y: auto;\n\t}\n\n\t/* ── Portrait orientation ─────────────────────────────────────────\n * Which buttons portrait hides is `DesktopUiOptions.portraitHidden`,\n * applied in JS by `applyAllVisibilityRules`. This used to be duplicated\n * here as `display: none !important` per id — \"additive redundancy\" for\n * the non-JS-resize path — but an !important rule the option can't reach\n * silently wins over it, so a consumer overriding the set got nothing.\n * One source of truth; JS owns it. */\n\n\t/* ── No-hover touch devices — suppress volume slider ──────────────\n * On (hover: none) devices the slider never expands and must be\n * hidden entirely. The volume button remains as a mute-toggle only. */\n\t&[data-no-hover] {\n\t\t.volume-slider {\n\t\t\tdisplay: none !important;\n\t\t}\n\n\t\t.volume-container:hover .volume-slider,\n\t\t.volume-container:focus-within .volume-slider {\n\t\t\tdisplay: none !important;\n\t\t}\n\t}\n}\n\n/* ── Container-query responsive layout (belt-and-suspenders alongside JS) ── */\n\n/* xl / lg: full layout — no overrides needed. */\n\n/* md (≤ 720 px): tighten padding, smaller time labels. */\n@container (max-width: 720px) {\n\t.nomercyplayer {\n\t\t.bottom-row {\n\t\t\tpadding: 4px 8px;\n\t\t\tgap: 0;\n\t\t}\n\n\t\t.top-row {\n\t\t\tpadding: 0 12px;\n\t\t}\n\n\t\t.time {\n\t\t\tfont-size: 0.75rem;\n\t\t}\n\n\t\t.volume-container:hover .volume-slider,\n\t\t.volume-container:focus-within .volume-slider {\n\t\t\twidth: 60px;\n\t\t}\n\t}\n}\n\n/* sm (≤ 480 px): drop the remaining-time label, compress volume.\n * `current-time` survives: \"where am I\" is the one readout a viewer still needs\n * at this width, and the scrubber alone can't answer it. `remaining-time` goes\n * because the pair together costs ~100px the row doesn't have. */\n@container (max-width: 480px) {\n\t.nomercyplayer {\n\t\t.remaining-time {\n\t\t\tdisplay: none;\n\t\t}\n\n\t\t.volume-container:hover .volume-slider,\n\t\t.volume-container:focus-within .volume-slider {\n\t\t\twidth: 48px;\n\t\t}\n\n\t\t.top-row {\n\t\t\tpadding: 0 8px;\n\t\t}\n\n\t\t.bottom-row {\n\t\t\tpadding: 4px 4px;\n\t\t}\n\n\t\t.title {\n\t\t\tfont-size: 0.9rem;\n\t\t}\n\t}\n}\n\n/* xs (≤ 360 px): minimal layout — hide top bar text, minimal slider. */\n@container (max-width: 360px) {\n\t.nomercyplayer {\n\t\t.show-info {\n\t\t\tdisplay: none;\n\t\t}\n\n\t\t.title {\n\t\t\tfont-size: 0.8rem;\n\t\t}\n\n\t\t.top-row {\n\t\t\tpadding: 0 4px;\n\t\t}\n\n\t\t.bottom-row {\n\t\t\tgap: 0;\n\t\t\tpadding: 2px 2px;\n\t\t}\n\n\t\t.volume-container:hover .volume-slider,\n\t\t.volume-container:focus-within .volume-slider {\n\t\t\twidth: 32px;\n\t\t}\n\t}\n}\n\n@keyframes nm-spin {\n\tto {\n\t\ttransform: rotate(360deg);\n\t}\n}\n\n/* ── Touch-device hover suppression ──────────────────────────────────────\n * On (hover: none) devices the :hover state sticks AFTER a tap until the\n * user touches elsewhere. Override every hover-affecting selector so taps\n * don't leave buttons looking \"permanently hovered\". Active menu-open\n * markers (.is-open / .is-active) are explicitly preserved. */\n@media (hover: none) {\n\t.nomercyplayer .back-btn:hover,\n\t.nomercyplayer .cast-btn:hover,\n\t.nomercyplayer .close-btn:hover,\n\t.nomercyplayer .center-btn:hover,\n\t.nomercyplayer .menu-header-back:hover,\n\t.nomercyplayer .menu-header-close:hover,\n\t.nomercyplayer .btn:hover,\n\t.nomercyplayer .remaining-time:hover {\n\t\tbackground: transparent !important;\n\t}\n\n\t.nomercyplayer .language-button:hover:not(.is-active) {\n\t\tbackground: transparent !important;\n\t}\n\n\t.nomercyplayer .playlist-menu-button:hover:not(.is-active) {\n\t\tbackground: transparent !important;\n\t}\n\n\t/* Bottom-bar icon swap (.icon-normal / .icon-hover) — keep the normal\n * variant visible on touch even after tap. The hover variant is the\n * \"active feedback\" only meant for real hover. */\n\t.nomercyplayer .btn:hover .icon-normal {\n\t\tdisplay: inline-flex !important;\n\t}\n\t.nomercyplayer .btn:hover .icon-hover {\n\t\tdisplay: none !important;\n\t}\n}\n\n/* ── Keyboard shortcuts overlay ──────────────────────────────────────────── */\n.nomercyplayer {\n\t.keybinds-dialog {\n\t\tdisplay: none;\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\tz-index: 200;\n\t\tbackground: rgba(0, 0, 0, 0.85);\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tpointer-events: auto;\n\n\t\t&.keybinds-dialog-visible {\n\t\t\tdisplay: flex;\n\t\t}\n\n\t\t.keybinds-card {\n\t\t\tbackground: rgba(20, 20, 25, 0.8);\n\t\t\tborder-radius: 14px;\n\t\t\tpadding: 24px 28px;\n\t\t\tposition: relative;\n\t\t\toverflow: hidden;\n\t\t}\n\n\t\t.keybinds-heading {\n\t\t\tmargin: 0 0 14px 0;\n\t\t\tfont-size: 19px;\n\t\t\tfont-weight: 600;\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.keybinds-grid {\n\t\t\tdisplay: grid;\n\t\t\tgrid-template-columns: repeat(3, 1fr);\n\t\t\tgap: 28px 60px;\n\t\t}\n\n\t\t.keybinds-column {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tgap: 14px;\n\t\t}\n\n\t\t.keybinds-group-title {\n\t\t\tmargin: 0 0 3px 0;\n\t\t\tfont-size: 14px;\n\t\t\tfont-weight: 600;\n\t\t\tcolor: rgba(255, 255, 255, 0.5);\n\t\t\ttext-transform: uppercase;\n\t\t\tletter-spacing: 0.05em;\n\t\t}\n\n\t\t.keybinds-row {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: row-reverse;\n\t\t\talign-items: center;\n\t\t\tjustify-content: space-between;\n\t\t\tpadding: 3px 0;\n\t\t\tgap: 14px;\n\t\t}\n\n\t\t.keybinds-keys {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tgap: 4px;\n\t\t\twhite-space: nowrap;\n\t\t}\n\n\t\t.keybinds-plus {\n\t\t\tfont-size: 12px;\n\t\t\tcolor: rgba(255, 255, 255, 0.4);\n\t\t}\n\n\t\tkbd.keybinds-key {\n\t\t\tbackground: rgba(255, 255, 255, 0.12);\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.2);\n\t\t\tborder-radius: 5px;\n\t\t\tpadding: 2px 7px;\n\t\t\tfont-size: 13px;\n\t\t\tfont-family: monospace;\n\t\t\twhite-space: nowrap;\n\t\t}\n\n\t\t.keybinds-label {\n\t\t\tfont-size: 14px;\n\t\t\tcolor: rgba(255, 255, 255, 0.85);\n\t\t\ttext-align: left;\n\t\t}\n\n\t\t.keybinds-decoration {\n\t\t\tposition: absolute;\n\t\t\tbottom: 48px;\n\t\t\tright: -114px;\n\t\t\tpointer-events: none;\n\t\t\ttransform: rotate(-8deg);\n\t\t\topacity: 0.04;\n\t\t}\n\n\t\t.keybinds-hint {\n\t\t\tmargin: 12px 0 0 0;\n\t\t\tfont-size: 13px;\n\t\t\tcolor: rgba(255, 255, 255, 0.35);\n\t\t\ttext-align: center;\n\t\t}\n\t}\n}\n", 'desktop-ui-styles');
173
+ this.appendInlineStyles("/* -----------------------------------------------------------------------------\n * Copyright (c) NoMercy Entertainment\n *\n * Licensed under the Apache License, Version 2.0. See LICENSE for details.\n *\n * SPDX-License-Identifier: Apache-2.0\n * ----------------------------------------------------------------------------- */\n\n.nomercyplayer {\n\tcontainer-type: inline-size;\n\tposition: relative;\n\tdisplay: block;\n\toverflow: hidden;\n\tbackground-color: #000;\n\n\tvideo {\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tobject-fit: contain;\n\t\tz-index: 0;\n\t\toutline: 0;\n\t}\n\n\t.overlay {\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\tz-index: 20;\n\t\tfont-family: system-ui, sans-serif;\n\t\tcolor: #fff;\n\t\tpointer-events: none;\n\t}\n\n\t/* Disabling the plugin hides its whole overlay (a peer owns the screen). The\n\t* scoped .overlay selector outranks the UA `[hidden]` rule, so pin it here. */\n\t.overlay[hidden] {\n\t\tdisplay: none;\n\t}\n\n\t/* ── Top bar ──────────────────────────────────────────────────────── */\n\t.top-bar {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tpadding: 16px 16px 48px 16px;\n\t\tdisplay: flex;\n\t\talign-items: flex-start;\n\t\tjustify-content: space-between;\n\t\tbackground: linear-gradient(to bottom, rgba(0, 0, 0, 0.85), rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0));\n\t\tpointer-events: none;\n\t\ttransition:\n\t\t\topacity 0.25s ease,\n\t\t\ttransform 0.25s ease;\n\t}\n\n\t/* The user-agent `[hidden] { display: none }` rule loses to the\n * .top-bar-right specificity below, so setting `right.hidden = true`\n * from the plugin (hideTitle option) silently fails. Explicit override\n * at matching specificity. Applies to both columns so future hide-side\n * toggles work without surprises. */\n\t.top-bar-left[hidden],\n\t.top-bar-right[hidden] {\n\t\tdisplay: none;\n\t}\n\n\t.top-bar-left {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tpointer-events: auto;\n\t}\n\n\t.top-bar-right {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 0.5rem;\n\t\talign-items: flex-end;\n\t\ttext-align: right;\n\t\tmax-width: 60%;\n\t}\n\n\t.show-info {\n\t\tfont-size: 0.88rem;\n\t\tfont-weight: 600;\n\t\tcolor: rgba(255, 255, 255, 0.75);\n\t\tmargin-bottom: 2px;\n\t}\n\n\t.title {\n\t\tfont-size: 1.05rem;\n\t\tfont-weight: 700;\n\t\ttext-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);\n\t}\n\n\t.back-btn,\n\t.cast-btn,\n\t.close-btn {\n\t\tpointer-events: auto;\n\t\tbackground: rgba(0, 0, 0, 0.35);\n\t\tborder: none;\n\t\tcolor: #fff;\n\t\twidth: 40px;\n\t\theight: 40px;\n\t\tborder-radius: 50%;\n\t\tcursor: pointer;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\ttransition: background 0.18s ease;\n\t\tmargin-right: 8px;\n\t}\n\n\t.back-btn:hover,\n\t.cast-btn:hover,\n\t.close-btn:hover {\n\t\tbackground: color-mix(in srgb, #fff 10%, rgba(0, 0, 0, 0.35));\n\t}\n\n\t.back-btn[hidden],\n\t.cast-btn[hidden],\n\t.close-btn[hidden] {\n\t\tdisplay: none !important;\n\t}\n\n\t/* ── Center play button ──────────────────────────────────────────── */\n\t.center {\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tpointer-events: none;\n\t}\n\n\t.center-btn {\n\t\tpointer-events: auto;\n\t\tbackground: rgba(0, 0, 0, 0.45);\n\t\tborder: none;\n\t\tcolor: #fff;\n\t\twidth: 80px;\n\t\theight: 80px;\n\t\tborder-radius: 50%;\n\t\tcursor: pointer;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tbackdrop-filter: blur(2px);\n\t\ttransition:\n\t\t\tbackground 0.18s ease,\n\t\t\ttransform 0.18s ease,\n\t\t\topacity 0.18s ease;\n\t}\n\n\t.center-btn:hover {\n\t\tbackground: color-mix(in srgb, #fff 10%, rgba(0, 0, 0, 0.45));\n\t\ttransform: scale(1.08);\n\t}\n\n\t/* Center button is a one-shot affordance — visible until the user clicks\n * it (or any touch zone triggers play). Once dismissed it stays hidden\n * and the touch zones own play/pause. */\n\t.center.dismissed .center-btn {\n\t\topacity: 0;\n\t\tpointer-events: none;\n\t}\n\n\t/* Buffering ring — sits inside the 80px center-btn with a small inset,\n * centered on the same point so it reads as a halo around the play icon.\n * Centering uses calc() offsets (not a translate) so the SVG's rotation\n * animation doesn't fight the spinner box's own transform. */\n\t.spinner {\n\t\tposition: absolute;\n\t\ttop: calc(50% - 36px);\n\t\tleft: calc(50% - 36px);\n\t\twidth: 72px;\n\t\theight: 72px;\n\t\tdisplay: none;\n\t\tpointer-events: none;\n\t}\n\n\t.spinner svg {\n\t\tdisplay: block;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\ttransform-origin: center;\n\t\tanimation: nm-spin 0.9s linear infinite;\n\t}\n\n\t/* Two sources, on purpose. `buffering` is core's — its rules table raises it\n\ton `waiting`/`stalled` and drops it on `canplay`. `busy` is this plugin's own\n\tload feedback, and it lives on `.center` so the two never overwrite each\n\tother on the same element. The spinner is the union of both. */\n\t&.buffering .spinner,\n\t.center.busy .spinner {\n\t\tdisplay: block;\n\t}\n\n\t/* ── Center status / toast line ──────────────────────────────────── */\n\t.player-message {\n\t\tposition: absolute;\n\t\ttop: 48px;\n\t\tleft: 50%;\n\t\ttransform: translateX(-50%);\n\t\tmax-width: 80%;\n\t\tpadding: 8px 16px;\n\t\tbackground: rgba(20, 22, 30, 0.85);\n\t\tcolor: #fff;\n\t\tfont-size: 0.875rem;\n\t\tborder-radius: 8px;\n\t\topacity: 0;\n\t\tpointer-events: none;\n\t\ttransition: opacity 0.2s ease;\n\t\tz-index: 95;\n\t}\n\n\t.player-message.visible {\n\t\topacity: 1;\n\t}\n\n\t/* ── Bottom bar (v1: bottom-bar > bottom-bar-shadow + top-row + bottom-row) ── */\n\t.bottom-bar {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 8px;\n\t\talign-items: center;\n\t\tmargin-top: auto;\n\t\twidth: 100%;\n\t\tz-index: 10;\n\t\tpadding-bottom: 8px;\n\t\tpointer-events: none;\n\t\ttransition:\n\t\t\topacity 0.25s ease,\n\t\t\ttransform 0.25s ease;\n\t}\n\n\t.bottom-bar > * {\n\t\tpointer-events: auto;\n\t}\n\n\t.bottom-bar-shadow {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tbottom: 0;\n\t\theight: calc(100% + 24px);\n\t\tpointer-events: none;\n\t\tbackground: linear-gradient(to top, rgba(0, 0, 0, 0.85), rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0));\n\t\tz-index: 0;\n\t}\n\n\t.top-row {\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\tgap: 8px;\n\t\talign-items: center;\n\t\theight: 8px;\n\t\tpadding: 0 24px;\n\t\twidth: 100%;\n\t\tbox-sizing: border-box;\n\t\tmargin-top: 16px;\n\t\tz-index: 1;\n\t}\n\n\t.bottom-row {\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tgap: 2px;\n\t\theight: 40px;\n\t\tpadding: 4px 16px;\n\t\twidth: 100%;\n\t\tbox-sizing: border-box;\n\t\tz-index: 1;\n\t}\n\n\t/* ── slider-bar tree (v1) ────────────────────────────────────────── */\n\t.slider-bar {\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\theight: 8px;\n\t\twidth: 100%;\n\t\tbackground: rgba(255, 255, 255, 0.2);\n\t\tborder-radius: 9999px;\n\t\tpointer-events: auto;\n\t\tcursor: pointer;\n\t\ttransition: height 140ms ease-out;\n\t}\n\n\t/* The strip a pointer can hit, which is not the strip that is drawn.\n\t*\n\t* The bar draws 8px and used to be targetable in exactly those 8 — measured\n\t* row by row with elementFromPoint on the running player: 4 rows above the\n\t* centre, 3 below, and the rows either side handed to the touch-zone\n\t* overlay behind it. The native chrome gives the same bar a 32dp strip and\n\t* centres the drawn bar inside it, which is the height a pointer is aimed\n\t* with rather than the height a bar is read at.\n\t*\n\t* Insets rather than a centred fixed height, because the space above the bar\n\t* and the space below it are not the same size. 16 up is `.top-row`'s whole\n\t* `margin-top` and stops on `.bottom-bar`'s own top edge; 12 down is the\n\t* 8px gap between the rows and stops on `.bottom-row`'s. Between them the\n\t* strip is 32 rows, the height the native chrome gives the same bar, and it\n\t* takes not one row from anything — the play button measures 40 of 40\n\t* before and after.\n\t*\n\t* A centred 32px box was tried first and came back 28: it reached four rows\n\t* INTO `.bottom-row`, whose box spans the full width, so the controls took\n\t* them back everywhere rather than only over a button. Raising\n\t* `.bottom-row`'s z-index to defend them changed nothing for the same\n\t* reason.\n\t*\n\t* z-index 0 so every layer above it inside the bar — buffer at 1, the\n\t* chapter bar at 5, the nipple and the preview pop — keeps its own clicks. */\n\t.slider-bar::before {\n\t\tcontent: '';\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t\ttop: -16px;\n\t\tbottom: -12px;\n\t\tz-index: 0;\n\t}\n\n\t/* Pointer devices grow when hovering anywhere in the top-row strip\n\t* (broader hit area than the bar itself) or while actively scrubbing. */\n\t@media (hover: hover) {\n\t\t.top-row:hover .slider-bar,\n\t\t.slider-bar.slider-scrubbing {\n\t\t\theight: 12px;\n\t\t}\n\t}\n\n\t.slider-bar.has-chapters {\n\t\tbackground: transparent;\n\t}\n\n\t.slider-buffer {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\ttop: 0;\n\t\theight: 100%;\n\t\twidth: 0;\n\t\tbackground: rgba(255, 255, 255, 0.4);\n\t\tborder-radius: 9999px;\n\t\toverflow: hidden;\n\t\tpointer-events: none;\n\t\tz-index: 1;\n\t}\n\n\t.slider-hover {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\ttop: 0;\n\t\theight: 100%;\n\t\twidth: 0;\n\t\tbackground: rgba(255, 255, 255, 0.3);\n\t\tborder-radius: 9999px;\n\t\toverflow: hidden;\n\t\tpointer-events: none;\n\t\tz-index: 0;\n\t}\n\n\t.slider-progress {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\ttop: 0;\n\t\theight: 100%;\n\t\twidth: 0;\n\t\tbackground: #fff;\n\t\tborder-radius: 9999px;\n\t\toverflow: hidden;\n\t\tpointer-events: none;\n\t\tz-index: 10;\n\t}\n\n\t.slider-bar.has-chapters .slider-progress {\n\t\tdisplay: none;\n\t}\n\n\t.slider-bar.has-chapters .slider-hover {\n\t\tdisplay: none;\n\t}\n\n\t.slider-bar.has-chapters .slider-buffer {\n\t\tdisplay: none;\n\t}\n\n\t.chapter-bar {\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tborder-radius: 9999px;\n\t\toverflow: visible;\n\t\tpointer-events: none;\n\t\tz-index: 5;\n\t}\n\n\t.chapter-marker {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\theight: 100%;\n\t\tmin-width: 2px;\n\t\toverflow: hidden;\n\t\tborder-radius: 2px;\n\t}\n\n\t.chapter-marker-bg {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tbackground: rgba(255, 255, 255, 0.2);\n\t\tborder-radius: 2px;\n\t\tz-index: 0;\n\t}\n\n\t.chapter-marker-buffer {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tbackground: rgba(255, 255, 255, 0.4);\n\t\ttransform-origin: left;\n\t\ttransform: scaleX(0);\n\t\tborder-radius: 2px;\n\t\tz-index: 5;\n\t\tpointer-events: none;\n\t}\n\n\t.chapter-marker-hover {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tbackground: rgba(229, 231, 235, 1);\n\t\ttransform-origin: left;\n\t\ttransform: scaleX(0);\n\t\tborder-radius: 2px;\n\t\tz-index: 10;\n\t\tpointer-events: none;\n\t}\n\n\t.chapter-marker-progress {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tbackground: #fff;\n\t\ttransform-origin: left;\n\t\ttransform: scaleX(0);\n\t\tborder-radius: 2px;\n\t\tz-index: 20;\n\t\tpointer-events: none;\n\t}\n\n\t.slider-nipple {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\twidth: 16px;\n\t\theight: 16px;\n\t\tbackground: #fff;\n\t\tborder-radius: 9999px;\n\t\ttransform: translate(-50%, -25%);\n\t\tdisplay: none;\n\t\tz-index: 30;\n\t\tpointer-events: none;\n\t}\n\n\t.slider-bar:hover .slider-nipple,\n\t.slider-bar.slider-scrubbing .slider-nipple {\n\t\tdisplay: block;\n\t\twidth: 24px;\n\t\theight: 24px;\n\t\ttransform: translate(-50%, -25%);\n\t}\n\n\t.slider-pop {\n\t\tposition: absolute;\n\t\tbottom: 24px;\n\t\tleft: 0;\n\t\ttransform: translateX(-50%);\n\t\tbackground: rgba(20, 20, 25, 0.95);\n\t\tborder-radius: 6px;\n\t\toverflow: hidden;\n\t\tpadding-bottom: 4px;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 4px;\n\t\ttext-align: center;\n\t\tpointer-events: none;\n\t\tz-index: 30;\n\t\topacity: var(--visibility, 0);\n\t\ttransition: opacity 0.12s ease;\n\t\tfont-weight: 600;\n\t\tmin-width: 60px;\n\t}\n\n\t.slider-pop-image {\n\t\twidth: 0;\n\t\theight: 0;\n\t\tbackground-repeat: no-repeat;\n\t\tbackground-size: auto auto;\n\t\tmargin: 0 auto;\n\t}\n\n\t.slider-pop-image[style*='background-image'] {\n\t\tmargin: 6px 6px 0;\n\t\tborder-radius: 3px;\n\t\tbox-sizing: content-box;\n\t}\n\n\t.slider-pop-text {\n\t\tfont-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n\t\tfont-size: 0.78rem;\n\t\tpadding: 0 8px;\n\t\tcolor: #fff;\n\t}\n\n\t.chapter-text {\n\t\tfont-size: 0.72rem;\n\t\tcolor: rgba(255, 255, 255, 0.75);\n\t\tpadding: 0 8px;\n\t}\n\n\t.chapter-text:empty {\n\t\tdisplay: none;\n\t}\n\n\t/* ── Round 40x40 createUiButton equivalent ───────────────────────── */\n\t.btn {\n\t\tcursor: pointer;\n\t\tfill: #fff;\n\t\tcolor: #fff;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\theight: 40px;\n\t\twidth: 40px;\n\t\tmin-width: 40px;\n\t\tpadding: 8px;\n\t\tborder-radius: 9999px;\n\t\tbackground: transparent;\n\t\tborder: 2px solid transparent;\n\t\tpointer-events: auto;\n\t\tposition: relative;\n\t\ttransition: transform 0.15s ease;\n\t}\n\n\t.btn:hover svg {\n\t\ttransform: scale(1.1);\n\t}\n\n\t.btn:focus-visible {\n\t\toutline: 2px solid rgba(255, 255, 255, 0.5);\n\t\toutline-offset: -2px;\n\t}\n\n\t.btn[hidden] {\n\t\tdisplay: none !important;\n\t}\n\n\t.btn[disabled] {\n\t\t/* Stay laid out so the bottom-row arrangement doesn't shift, but\n * make it clear the action isn't available right now. */\n\t\topacity: 0.3;\n\t\tcursor: not-allowed;\n\t\tpointer-events: none;\n\t}\n\n\t.btn[disabled]:hover svg {\n\t\ttransform: none;\n\t}\n\n\t.btn svg {\n\t\ttransition: transform 0.18s ease;\n\t\tpointer-events: none;\n\t}\n\n\t/* Icon holder — keeps SVG swaps from wiping the sibling .tooltip span. */\n\t.btn-icon {\n\t\tdisplay: inline-flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tpointer-events: none;\n\t}\n\n\t/* Filled / outlined path swap — every bottom-bar icon inverts on hover.\n * Hide the hover-variant path globally so menu/non-button icons stay clean;\n * .btn:hover opts in to the swap for the bottom-bar buttons. */\n\t.icon-hover {\n\t\tdisplay: none;\n\t}\n\n\t.btn:hover .icon-normal,\n\t.btn.is-active .icon-normal {\n\t\tdisplay: none;\n\t}\n\n\t.btn:hover .icon-hover,\n\t.btn.is-active .icon-hover {\n\t\tdisplay: inline;\n\t}\n\n\t/* ── Volume container ────────────────────────────────────────────── */\n\t.volume-container {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\toverflow: visible;\n\t\tpointer-events: auto;\n\t}\n\n\t.volume-slider {\n\t\tappearance: none;\n\t\twidth: 0;\n\t\topacity: 0;\n\t\theight: 8px;\n\t\tbackground: linear-gradient(\n\t\t\tto right,\n\t\t\t#fff 0%,\n\t\t\t#fff var(--vol-pct, 100%),\n\t\t\trgba(255, 255, 255, 0.3) var(--vol-pct, 100%)\n\t\t);\n\t\tborder-radius: 9999px;\n\t\tcursor: pointer;\n\t\talign-self: center;\n\t\ttransition:\n\t\t\twidth 0.3s ease,\n\t\t\topacity 0.3s ease,\n\t\t\tmargin 0.3s ease,\n\t\t\theight 80ms ease-out;\n\t}\n\n\t.volume-container:hover .volume-slider,\n\t.volume-container:focus-within .volume-slider {\n\t\twidth: 80px;\n\t\topacity: 1;\n\t\tmargin: 0 8px;\n\t}\n\n\t.volume-slider::-webkit-slider-thumb {\n\t\tappearance: none;\n\t\twidth: 12px;\n\t\theight: 12px;\n\t\t/* Chromium: appearance:none thumbs center on track; nudge down slightly */\n\t\tmargin-top: 2px;\n\t\tbackground: #fff;\n\t\tborder-radius: 50%;\n\t}\n\n\t/* ── Vertical volume slider (popup above mute button) ───────────── */\n\t.volume-container {\n\t\tposition: relative;\n\t}\n\n\t/* Hide the horizontal slider when vertical mode is active. */\n\t.volume-container-vertical .volume-slider {\n\t\tdisplay: none;\n\t}\n\n\t.volume-slider-vertical {\n\t\tdisplay: none;\n\t\tposition: absolute;\n\t\tbottom: calc(100% + 8px);\n\t\tleft: 50%;\n\t\ttransform: translateX(-50%);\n\t\tbackground: rgba(20, 22, 30, 0.92);\n\t\tborder-radius: 8px;\n\t\tpadding: 12px 10px;\n\t\tz-index: 90;\n\t\tbackdrop-filter: blur(4px);\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t}\n\n\t.volume-slider-vertical.volume-slider-vertical-open {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tgap: 8px;\n\t}\n\n\t.vol-popup-mute {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tmin-width: 44px;\n\t\tmin-height: 44px;\n\t\tpadding: 0;\n\t\tbackground: transparent;\n\t\tborder: none;\n\t\tcolor: rgba(255, 255, 255, 0.8);\n\t\tborder-radius: 6px;\n\t\tcursor: pointer;\n\t}\n\n\t.vol-popup-mute:hover {\n\t\tcolor: #fff;\n\t\tbackground: rgba(255, 255, 255, 0.1);\n\t}\n\n\t.volume-slider-vertical-input {\n\t\tappearance: none;\n\t\twriting-mode: vertical-lr;\n\t\tdirection: rtl;\n\t\twidth: 4px;\n\t\theight: 80px;\n\t\tbackground: linear-gradient(\n\t\t\tto top,\n\t\t\t#fff 0%,\n\t\t\t#fff var(--vol-pct, 100%),\n\t\t\trgba(255, 255, 255, 0.3) var(--vol-pct, 100%)\n\t\t);\n\t\tborder-radius: 9999px;\n\t\tcursor: pointer;\n\t}\n\n\t.volume-slider-vertical-input::-webkit-slider-thumb {\n\t\tappearance: none;\n\t\twidth: 12px;\n\t\theight: 12px;\n\t\t/* Chromium centers appearance:none thumbs — see horizontal slider. */\n\t\tbackground: #fff;\n\t\tborder-radius: 50%;\n\t}\n\n\t/* ── Time labels ─────────────────────────────────────────────────── */\n\t.time {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tpointer-events: none;\n\t\tuser-select: none;\n\t\tfont-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n\t\tfont-size: 0.82rem;\n\t\tcolor: #ddd;\n\t}\n\n\t.current-time {\n\t\tmargin-left: 8px;\n\t}\n\n\t.remaining-time {\n\t\tmargin-right: 8px;\n\t\tpointer-events: auto;\n\t\tcursor: pointer;\n\t\tborder-radius: 4px;\n\t\tpadding: 0 4px;\n\t\ttransition: background 0.15s ease;\n\t}\n\n\t.remaining-time:hover {\n\t\tbackground: rgba(255, 255, 255, 0.1);\n\t}\n\n\t.divider {\n\t\tdisplay: flex;\n\t\tflex: 1;\n\t\tmin-width: 16px;\n\t}\n\n\t/* ── Menu frame (v1) ─────────────────────────────────────────────── */\n\t.menu-frame-dialog {\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\tbackground: transparent;\n\t\tborder: none;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tcolor: inherit;\n\t\tpointer-events: none;\n\t\tz-index: 50;\n\t}\n\n\t.menu-frame-dialog[open] {\n\t\tpointer-events: auto;\n\t}\n\n\t.menu-wrapper {\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\tpointer-events: none;\n\t\tcolor: #fff;\n\t}\n\n\t.menu-frame {\n\t\tposition: absolute;\n\t\ttop: 16px;\n\t\tright: 16px;\n\t\tbottom: 52px;\n\t\tdisplay: none;\n\t\tflex-direction: column;\n\t\theight: auto;\n\t\tmax-height: calc(100% - 2rem);\n\t\tmax-width: min(52rem, calc(100% - 2rem));\n\t\twidth: min-content;\n\t\toverflow: hidden;\n\t\tborder-radius: 8px;\n\t\tz-index: 50;\n\t}\n\n\t.menu-frame.open {\n\t\tdisplay: flex;\n\t}\n\n\t.menu-content {\n\t\tdisplay: flex;\n\t\tflex-direction: row;\n\t\tjustify-content: flex-end;\n\t\theight: 100%;\n\t\twidth: 100%;\n\t\tmargin-top: auto;\n\t\toverflow: hidden;\n\t}\n\n\t.main-menu {\n\t\tbackground: rgba(20, 20, 25, 0.95);\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 4px;\n\t\theight: auto;\n\t\tmax-height: 60vh;\n\t\tmax-height: 60dvh;\n\t\tmin-width: 16rem;\n\t\tmargin-top: auto;\n\t\toverflow: hidden;\n\t\tborder-radius: 8px;\n\t\tpointer-events: auto;\n\t}\n\n\t.menu-content.sub-menu-open .main-menu {\n\t\tdisplay: none;\n\t}\n\n\t.sub-menu {\n\t\tbackground: rgba(20, 20, 25, 0.95);\n\t\tdisplay: none;\n\t\tflex-direction: column;\n\t\tgap: 4px;\n\t\theight: auto;\n\t\tmax-height: 100%;\n\t\tmin-width: 16rem;\n\t\tmargin-top: auto;\n\t\toverflow: hidden;\n\t\tborder-radius: 8px;\n\t\tpointer-events: auto;\n\t\twidth: 100%;\n\t}\n\n\t.menu-content.sub-menu-open .sub-menu {\n\t\tdisplay: flex;\n\t}\n\n\t.menu-header {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tmin-height: 2.5rem;\n\t\tcolor: #fff;\n\t\tborder-bottom: 1px solid rgba(209, 213, 219, 0.2);\n\t\twidth: 100%;\n\t\tpadding: 6px 6px 6px 6px;\n\t\tbox-sizing: border-box;\n\t\tgap: 4px;\n\t}\n\n\t.menu-header .menu-button-text {\n\t\tfont-weight: 600;\n\t\tflex: 1;\n\t}\n\n\t.menu-header-close,\n\t.menu-header-back {\n\t\tbackground: transparent;\n\t\tborder: none;\n\t\tcolor: #fff;\n\t\tcursor: pointer;\n\t\twidth: 32px;\n\t\theight: 32px;\n\t\tflex: 0 0 32px;\n\t\tdisplay: inline-flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tborder-radius: 6px;\n\t}\n\n\t.menu-header-close {\n\t\tmargin-left: auto;\n\t}\n\n\t.menu-header-back:hover,\n\t.menu-header-close:hover {\n\t\tbackground: rgba(255, 255, 255, 0.08);\n\t}\n\n\t.menu-header > .menu-button-text:first-child {\n\t\tpadding-left: 16px;\n\t}\n\n\t.language-button {\n\t\twidth: 100%;\n\t\tbox-sizing: border-box;\n\t\theight: 32px;\n\t\tpadding: 4px 8px;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tborder-radius: 4px;\n\t\tbackground: transparent;\n\t\tborder: 1px solid transparent;\n\t\tcolor: #fff;\n\t\tcursor: pointer;\n\t\ttransition:\n\t\t\tbackground 0.18s ease,\n\t\t\toutline-color 0.18s ease;\n\t\ttext-align: left;\n\t\twhite-space: nowrap;\n\t}\n\n\t.language-button:hover {\n\t\tbackground: rgba(115, 115, 115, 0.5);\n\t}\n\n\t.language-button:focus-visible {\n\t\toutline: 2px solid #fff;\n\t\toutline-offset: -2px;\n\t}\n\n\t.language-button.is-active {\n\t\tbackground: rgba(255, 255, 255, 0.2);\n\t}\n\n\t.language-button .menu-button-text {\n\t\tcolor: #fff;\n\t\tcursor: pointer;\n\t\tfont-weight: 600;\n\t\tpadding-left: 8px;\n\t\tdisplay: flex;\n\t\tgap: 8px;\n\t\tline-height: normal;\n\t}\n\n\t.language-button .menu-button-icon-left {\n\t\twidth: 20px;\n\t\theight: 20px;\n\t\tflex: 0 0 20px;\n\t\tdisplay: inline-flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t}\n\n\t.language-button .menu-button-chevron {\n\t\twidth: 20px;\n\t\theight: 20px;\n\t\tflex: 0 0 20px;\n\t\tdisplay: inline-flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t}\n\n\t.language-button:not(:has(.menu-button-subtext)) .menu-button-chevron {\n\t\tmargin-left: auto;\n\t}\n\n\t.language-button .menu-button-check {\n\t\tmargin-left: auto;\n\t\twidth: 20px;\n\t\theight: 20px;\n\t\tflex: 0 0 20px;\n\t\tdisplay: none;\n\t\tcolor: #fff;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t}\n\n\t.language-button.is-active .menu-button-check {\n\t\tdisplay: inline-flex;\n\t}\n\n\t.language-button .menu-button-subtext {\n\t\t/* `margin-left` was declared twice — 8px, then auto — and the second\n\t\t* won, so the separation the first was asking for never existed. It is\n\t\t* invisible until a label is long enough to fill the row, and then\n\t\t* `auto` has no space left to give and the value sits against the last\n\t\t* letter: \"Transparantie achtergrondvak0%\".\n\t\t*\n\t\t* The push and the minimum gap are two different jobs, so they are two\n\t\t* different properties now. */\n\t\tmargin-left: auto;\n\t\tpadding-left: 8px;\n\t\tcolor: rgba(255, 255, 255, 0.6);\n\t\tfont-size: 10px;\n\t\tfont-weight: 600;\n\t\tpadding-right: 8px;\n\t\twhite-space: nowrap;\n\t\tline-height: 100%;\n\t}\n\n\t/* Consumer-supplied action rows (e.g. \"Search subtitles online…\") append\n\tafter a pane's own choices — a top divider sets them apart visually. */\n\t.language-button.menu-action-row {\n\t\tmargin-top: 4px;\n\t\tborder-top: 1px solid rgba(255, 255, 255, 0.12);\n\t}\n\n\t.sub-menu-content {\n\t\tdisplay: none;\n\t\tflex-direction: column;\n\t\tmax-height: 60vh;\n\t\tmax-height: 60dvh;\n\t\twidth: 100%;\n\t\toverflow-y: auto;\n\t\toverflow-x: hidden;\n\t}\n\n\t.sub-menu-content.is-open {\n\t\tdisplay: flex;\n\t}\n\n\t.scroll-container {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 4px;\n\t\toverflow-x: hidden;\n\t\toverflow-y: auto;\n\t\t/* One inset, declared once, on all four sides.\n\t\t*\n\t\t* This was `8px 0 8px 8px` — no right padding at all — with\n\t\t* `scrollbar-gutter: stable` standing in for it on that side. A gutter\n\t\t* is not a padding: its width is the browser's scrollbar width, which\n\t\t* differs by engine, platform and overlay-scrollbar setting, so the\n\t\t* right inset never equalled the left.\n\t\t*\n\t\t* Nor is the answer `both-edges`: a gutter is reserved IN ADDITION to\n\t\t* the padding, so 8px of padding became an 18px inset on both sides —\n\t\t* symmetric and twice as deep as anything else in the menu. The inset\n\t\t* is 8, and the scrollbar overlays it rather than reserving beside it. */\n\t\tpadding: 8px;\n\t\ttransition: all 0.3s ease;\n\t\twidth: 100%;\n\t\tmax-height: 50vh;\n\t\tmax-height: 50dvh;\n\t\tscrollbar-width: thin;\n\t\tscrollbar-color: rgba(255, 255, 255, 0.3) transparent;\n\t}\n\n\t/* ── Playlist sub-menu (two-pane, mirrors v1's createEpisodeMenu) ──────\n * playlist-menu (flex-row, gap:0)\n * ├─ sub-menu-content.seasons-pane (1/3 width, border-right)\n * │ ├─ menu-header (back / \"Seasons\" / close placeholder)\n * │ └─ scroll-container (season buttons; empty for movies)\n * └─ episode-menu (much wider — 63rem cap)\n * ├─ menu-header-main (close × only)\n * └─ scroll-container (rich-card episodes)\n *\n * playlist-menu-button (each card)\n * ├─ episode-menu-button-left\n * │ ├─ episode-menu-button-image\n * │ ├─ episode-menu-button-shadow\n * │ └─ episode-menu-progress-container\n * │ ├─ episode-menu-progress-box (episode label + duration)\n * │ └─ slider-container > progress-bar\n * └─ playlist-card-right (title + overview)\n * ──────────────────────────────────────────────────────────────────── */\n\t#playlist-menu.playlist-menu {\n\t\tdisplay: none;\n\t\tflex-direction: column;\n\t\tgap: 0;\n\t\twidth: 100%;\n\t\tmax-height: 60vh;\n\t\tmax-height: 60dvh;\n\t}\n\n\t#playlist-menu.playlist-menu.is-open {\n\t\tdisplay: flex;\n\t}\n\n\t.playlist-cols {\n\t\tdisplay: flex;\n\t\tflex-direction: row;\n\t\tflex: 1;\n\t\tmin-height: 0;\n\t\toverflow: hidden;\n\t}\n\n\t.seasons-pane {\n\t\tflex: 1;\n\t\twidth: 100%;\n\t\tmin-width: 13rem;\n\t\tborder-right: 2px solid rgba(107, 114, 128, 0.2);\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\toverflow: hidden;\n\t\tmin-height: 0;\n\t}\n\n\t.playlist-flat .seasons-pane {\n\t\tdisplay: none;\n\t}\n\n\t/* 29rem, not 36: the two panes' minimums together decided the frame's real\n\t\t* width, so the playlist pinned itself to the 52rem cap and its cards drew\n\t\t* a thumbnail wider than the poster art anywhere else in the player. */\n\t.episode-menu {\n\t\tflex: 1;\n\t\twidth: 100%;\n\t\tmin-width: 29rem;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\toverflow: hidden;\n\t\tmin-height: 0;\n\t}\n\n\t.episode-menu .scroll-container {\n\t\tmax-height: none;\n\t\tflex: 1;\n\t}\n\n\t/* ── Portrait playlist: full-viewport overlay ──────────────────────\n * In portrait the menu-frame expands to fill the available viewport\n * (inset 0 with bottom clearance for the control bar). The playlist\n * menu fills that frame vertically. Seasons and episodes stack into a\n * single scroll column. Touch targets on each card stay ≥ 44px. */\n\t&[data-orientation='portrait'] {\n\t\t/* Portrait full-viewport ONLY applies when the PLAYLIST sub-menu is\n * the active one (.is-open). Subtitle / audio / quality / speed / aspect\n * sub-menus keep their default popover sizing — they were fine.\n * Menu overlays the bottom bar too (no 56px reserve) for full real estate. */\n\t\t.menu-frame:has(.playlist-menu.is-open) {\n\t\t\ttop: 0;\n\t\t\tright: 0;\n\t\t\tleft: 0;\n\t\t\tbottom: 0;\n\t\t\tmax-width: 100%;\n\t\t\twidth: 100%;\n\t\t\tmax-height: 100%;\n\t\t\theight: 100%;\n\t\t\tborder-radius: 0;\n\t\t}\n\n\t\t.playlist-menu.is-open {\n\t\t\twidth: 100%;\n\t\t\tmax-height: 100%;\n\t\t\tborder-radius: 0;\n\t\t}\n\n\t\t#playlist-menu.playlist-menu {\n\t\t\tmax-height: 100%;\n\t\t\theight: 100%;\n\t\t}\n\n\t\t.playlist-cols {\n\t\t\tflex-direction: column;\n\t\t\toverflow-y: auto;\n\t\t\tflex: 1;\n\t\t\tmin-height: 0;\n\t\t}\n\n\t\t.seasons-pane {\n\t\t\tmin-width: 0;\n\t\t\tborder-right: none;\n\t\t\tborder-bottom: 2px solid rgba(107, 114, 128, 0.2);\n\t\t\tmin-height: 0;\n\t\t\theight: auto;\n\t\t\toverflow: visible;\n\t\t\twidth: 100%;\n\t\t}\n\n\t\t.episode-menu {\n\t\t\tmin-width: 0;\n\t\t\tmin-height: 0;\n\t\t\tflex: 1;\n\t\t\toverflow-y: auto;\n\t\t\twidth: 100%;\n\t\t}\n\n\t\t.episode-menu .scroll-container {\n\t\t\tmax-height: none;\n\t\t\tflex: 1;\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tgap: 6px;\n\t\t\t/* Inherited, not restated. This pane carried its own symmetric\n\t\t\t* padding because the base rule had none on the right — so one pane\n\t\t\t* looked correct and every other one did not. The base is symmetric\n\t\t\t* now and a second number here would be the drift starting again. */\n\t\t}\n\n\t\t.playlist-menu-button {\n\t\t\tpadding: 8px;\n\t\t\tgap: 10px;\n\t\t\talign-items: flex-start;\n\t\t}\n\n\t\t.episode-menu-button-left {\n\t\t\twidth: 38%;\n\t\t\tflex-basis: 38%;\n\t\t\tmin-width: 0;\n\t\t\tmax-width: 180px;\n\t\t\talign-self: flex-start;\n\t\t\taspect-ratio: 16 / 9;\n\t\t}\n\n\t\t.playlist-menu-button-overview {\n\t\t\t-webkit-line-clamp: 3;\n\t\t\tline-clamp: 3;\n\t\t}\n\n\t\t.playlist-menu-button-title {\n\t\t\tfont-size: 0.95rem;\n\t\t\tmargin-bottom: 4px;\n\t\t}\n\t}\n\n\t.playlist-menu-button {\n\t\tdisplay: flex;\n\t\tbox-sizing: border-box;\n\t\tgap: 8px;\n\t\tpadding: 8px;\n\t\tborder-radius: 8px;\n\t\tbackground: transparent;\n\t\tborder: 1px solid transparent;\n\t\tcolor: #fff;\n\t\tcursor: pointer;\n\t\ttransition: background 0.18s ease;\n\t\ttext-align: left;\n\t\twidth: 100%;\n\t}\n\n\t.playlist-menu-button:hover {\n\t\tbackground: rgba(115, 115, 115, 0.2);\n\t}\n\n\t.playlist-menu-button:focus-visible {\n\t\toutline: 2px solid #fff;\n\t\toutline-offset: -2px;\n\t}\n\n\t.playlist-menu-button.is-active {\n\t\tbackground: rgba(255, 255, 255, 0.1);\n\t}\n\n\t.playlist-menu-button.is-active::before {\n\t\tcontent: '';\n\t\tposition: absolute;\n\t}\n\n\t.episode-menu-button-left {\n\t\tposition: relative;\n\t\theight: auto;\n\t\toverflow: hidden;\n\t\tborder-radius: 6px;\n\t\talign-self: center;\n\t\twidth: 32%;\n\t\tflex: 0 0 32%;\n\t\tmax-width: 176px;\n\t\taspect-ratio: 16 / 9;\n\t\tbackground: rgba(255, 255, 255, 0.05);\n\t}\n\n\t.episode-menu-button-image {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tobject-fit: cover;\n\t\taspect-ratio: 16 / 9;\n\t}\n\n\t.episode-menu-button-shadow {\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\tbackground: linear-gradient(\n\t\t\t0deg,\n\t\t\trgba(0, 0, 0, 0.85) 0%,\n\t\t\trgba(0, 0, 0, 0.7) 25%,\n\t\t\trgba(0, 0, 0, 0) 50%,\n\t\t\trgba(0, 0, 0, 0) 100%\n\t\t);\n\t\tbox-shadow:\n\t\t\tinset 0 1px 0 rgba(255, 255, 255, 0.24),\n\t\t\tinset 0 -1px 0 rgba(0, 0, 0, 0.24),\n\t\t\tinset 0 -2px 0 rgba(0, 0, 0, 0.24);\n\t\tpointer-events: none;\n\t}\n\n\t.episode-menu-progress-container {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tpadding: 0 12px;\n\t}\n\n\t.episode-menu-progress-box {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t\theight: auto;\n\t\tmargin-bottom: 4px;\n\t\tpadding: 0 4px;\n\t}\n\n\t.progress-item-text,\n\t.progress-duration {\n\t\tfont-size: 0.7rem;\n\t\tcolor: rgba(255, 255, 255, 0.85);\n\t}\n\n\t.slider-container {\n\t\tbackground: rgba(107, 114, 128, 0.8);\n\t\theight: 4px;\n\t\tmargin: 0 4px 8px;\n\t\toverflow: hidden;\n\t\tborder-radius: 4px;\n\t\tdisplay: none;\n\t}\n\n\t.slider-container.has-progress {\n\t\tdisplay: flex;\n\t}\n\n\t.slider-container .progress-bar {\n\t\tbackground: #fff;\n\t\theight: 100%;\n\t\twidth: 0;\n\t\tborder-radius: 4px;\n\t}\n\n\t.playlist-card-right {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 4px;\n\t\tpointer-events: none;\n\t\ttext-align: left;\n\t\twidth: 75%;\n\t\tflex: 1;\n\t\tmin-width: 0;\n\t}\n\n\t.playlist-menu-button-title {\n\t\tfont-weight: 700;\n\t\tcolor: #fff;\n\t\tline-height: 1.25;\n\t\tdisplay: -webkit-box;\n\t\t-webkit-line-clamp: 2;\n\t\tline-clamp: 2;\n\t\t-webkit-box-orient: vertical;\n\t\toverflow: hidden;\n\t}\n\n\t.playlist-menu-button-overview {\n\t\tfont-size: 0.7rem;\n\t\tline-height: 1rem;\n\t\tcolor: rgba(255, 255, 255, 0.8);\n\t\tdisplay: -webkit-box;\n\t\t-webkit-line-clamp: 4;\n\t\tline-clamp: 4;\n\t\t-webkit-box-orient: vertical;\n\t\toverflow: hidden;\n\t}\n\n\t.playlist-menu-button-watched {\n\t\tfont-size: 0.65rem;\n\t\tcolor: rgba(255, 255, 255, 0.55);\n\t\tfont-variant-numeric: tabular-nums;\n\t\twhite-space: nowrap;\n\t\toverflow: hidden;\n\t\ttext-overflow: ellipsis;\n\t}\n\n\t/* ── Active / inactive overlay states ─────────────────────────────── */\n\t&.active .top-bar,\n\t&.active .bottom-bar {\n\t\topacity: 1;\n\t\ttransform: translateY(0);\n\t}\n\n\t&.inactive .top-bar {\n\t\topacity: 0;\n\t\ttransform: translateY(-100%);\n\t\tpointer-events: none;\n\t}\n\n\t&.inactive .bottom-bar {\n\t\topacity: 0;\n\t\ttransform: translateY(100%);\n\t\tpointer-events: none;\n\t}\n\n\t/* ── Button tooltips ──────────────────────────────────────────────── */\n\t.tooltip {\n\t\tposition: absolute;\n\t\tbottom: calc(100% + 8px);\n\t\tleft: 50%;\n\t\ttransform: translateX(-50%);\n\t\tbackground: rgba(20, 22, 30, 0.92);\n\t\tcolor: #fff;\n\t\tfont-size: 0.75rem;\n\t\tline-height: 2;\n\t\tpadding: 8px 16px;\n\t\tborder-radius: 8px;\n\t\twhite-space: nowrap;\n\t\tpointer-events: none;\n\t\tz-index: 100;\n\t\topacity: 0;\n\t\ttransition: opacity 0.15s ease;\n\t\tbackdrop-filter: blur(4px);\n\t}\n\n\t.tooltip.tooltip-visible {\n\t\topacity: 1;\n\t}\n\n\t.tooltip::after {\n\t\tcontent: '';\n\t\tposition: absolute;\n\t\ttop: 100%;\n\t\tleft: var(--arrow-x, 50%);\n\t\ttransform: translateX(-50%);\n\t\tborder: 5px solid transparent;\n\t\tborder-top-color: rgba(20, 22, 30, 0.92);\n\t}\n\n\t/* ── Keyboard shortcuts overlay ───────────────────────────────────── */\n\t#nmplayer-keybinds-dialog {\n\t\tscrollbar-width: thin;\n\t\tscrollbar-color: rgba(255, 255, 255, 0.2) transparent;\n\t\toverflow-y: auto;\n\t}\n\n\t/* ── Portrait orientation ─────────────────────────────────────────\n * Which buttons portrait hides is `DesktopUiOptions.portraitHidden`,\n * applied in JS by `applyAllVisibilityRules`. This used to be duplicated\n * here as `display: none !important` per id — \"additive redundancy\" for\n * the non-JS-resize path — but an !important rule the option can't reach\n * silently wins over it, so a consumer overriding the set got nothing.\n * One source of truth; JS owns it. */\n\n\t/* ── No-hover touch devices — suppress volume slider ──────────────\n * On (hover: none) devices the slider never expands and must be\n * hidden entirely. The volume button remains as a mute-toggle only. */\n\t&[data-no-hover] {\n\t\t.volume-slider {\n\t\t\tdisplay: none !important;\n\t\t}\n\n\t\t.volume-container:hover .volume-slider,\n\t\t.volume-container:focus-within .volume-slider {\n\t\t\tdisplay: none !important;\n\t\t}\n\t}\n}\n\n/* ── Container-query responsive layout (belt-and-suspenders alongside JS) ── */\n\n/* xl / lg: full layout — no overrides needed. */\n\n/* md (≤ 720 px): tighten padding, smaller time labels. */\n@container (max-width: 720px) {\n\t.nomercyplayer {\n\t\t.bottom-row {\n\t\t\tpadding: 4px 8px;\n\t\t\tgap: 0;\n\t\t}\n\n\t\t.top-row {\n\t\t\tpadding: 0 12px;\n\t\t}\n\n\t\t.time {\n\t\t\tfont-size: 0.75rem;\n\t\t}\n\n\t\t.volume-container:hover .volume-slider,\n\t\t.volume-container:focus-within .volume-slider {\n\t\t\twidth: 60px;\n\t\t}\n\t}\n}\n\n/* sm (≤ 480 px): drop the remaining-time label, compress volume.\n * `current-time` survives: \"where am I\" is the one readout a viewer still needs\n * at this width, and the scrubber alone can't answer it. `remaining-time` goes\n * because the pair together costs ~100px the row doesn't have. */\n@container (max-width: 480px) {\n\t.nomercyplayer {\n\t\t.remaining-time {\n\t\t\tdisplay: none;\n\t\t}\n\n\t\t.volume-container:hover .volume-slider,\n\t\t.volume-container:focus-within .volume-slider {\n\t\t\twidth: 48px;\n\t\t}\n\n\t\t.top-row {\n\t\t\tpadding: 0 8px;\n\t\t}\n\n\t\t.bottom-row {\n\t\t\tpadding: 4px 4px;\n\t\t}\n\n\t\t.title {\n\t\t\tfont-size: 0.9rem;\n\t\t}\n\t}\n}\n\n/* xs (≤ 360 px): minimal layout — hide top bar text, minimal slider. */\n@container (max-width: 360px) {\n\t.nomercyplayer {\n\t\t.show-info {\n\t\t\tdisplay: none;\n\t\t}\n\n\t\t.title {\n\t\t\tfont-size: 0.8rem;\n\t\t}\n\n\t\t.top-row {\n\t\t\tpadding: 0 4px;\n\t\t}\n\n\t\t.bottom-row {\n\t\t\tgap: 0;\n\t\t\tpadding: 2px 2px;\n\t\t}\n\n\t\t.volume-container:hover .volume-slider,\n\t\t.volume-container:focus-within .volume-slider {\n\t\t\twidth: 32px;\n\t\t}\n\t}\n}\n\n@keyframes nm-spin {\n\tto {\n\t\ttransform: rotate(360deg);\n\t}\n}\n\n/* ── Touch-device hover suppression ──────────────────────────────────────\n * On (hover: none) devices the :hover state sticks AFTER a tap until the\n * user touches elsewhere. Override every hover-affecting selector so taps\n * don't leave buttons looking \"permanently hovered\". Active menu-open\n * markers (.is-open / .is-active) are explicitly preserved. */\n@media (hover: none) {\n\t.nomercyplayer .back-btn:hover,\n\t.nomercyplayer .cast-btn:hover,\n\t.nomercyplayer .close-btn:hover,\n\t.nomercyplayer .center-btn:hover,\n\t.nomercyplayer .menu-header-back:hover,\n\t.nomercyplayer .menu-header-close:hover,\n\t.nomercyplayer .btn:hover,\n\t.nomercyplayer .remaining-time:hover {\n\t\tbackground: transparent !important;\n\t}\n\n\t.nomercyplayer .language-button:hover:not(.is-active) {\n\t\tbackground: transparent !important;\n\t}\n\n\t.nomercyplayer .playlist-menu-button:hover:not(.is-active) {\n\t\tbackground: transparent !important;\n\t}\n\n\t/* Bottom-bar icon swap (.icon-normal / .icon-hover) — keep the normal\n * variant visible on touch even after tap. The hover variant is the\n * \"active feedback\" only meant for real hover. */\n\t.nomercyplayer .btn:hover .icon-normal {\n\t\tdisplay: inline-flex !important;\n\t}\n\t.nomercyplayer .btn:hover .icon-hover {\n\t\tdisplay: none !important;\n\t}\n}\n\n/* ── Keyboard shortcuts overlay ──────────────────────────────────────────── */\n.nomercyplayer {\n\t.keybinds-dialog {\n\t\tdisplay: none;\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\tz-index: 200;\n\t\tbackground: rgba(0, 0, 0, 0.85);\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tpointer-events: auto;\n\n\t\t&.keybinds-dialog-visible {\n\t\t\tdisplay: flex;\n\t\t}\n\n\t\t.keybinds-card {\n\t\t\tbackground: rgba(20, 20, 25, 0.8);\n\t\t\tborder-radius: 14px;\n\t\t\tpadding: 24px 28px;\n\t\t\tposition: relative;\n\t\t\toverflow: hidden;\n\t\t}\n\n\t\t.keybinds-heading {\n\t\t\tmargin: 0 0 14px 0;\n\t\t\tfont-size: 19px;\n\t\t\tfont-weight: 600;\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.keybinds-grid {\n\t\t\tdisplay: grid;\n\t\t\tgrid-template-columns: repeat(3, 1fr);\n\t\t\tgap: 28px 60px;\n\t\t}\n\n\t\t.keybinds-column {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tgap: 14px;\n\t\t}\n\n\t\t.keybinds-group-title {\n\t\t\tmargin: 0 0 3px 0;\n\t\t\tfont-size: 14px;\n\t\t\tfont-weight: 600;\n\t\t\tcolor: rgba(255, 255, 255, 0.5);\n\t\t\ttext-transform: uppercase;\n\t\t\tletter-spacing: 0.05em;\n\t\t}\n\n\t\t.keybinds-row {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: row-reverse;\n\t\t\talign-items: center;\n\t\t\tjustify-content: space-between;\n\t\t\tpadding: 3px 0;\n\t\t\tgap: 14px;\n\t\t}\n\n\t\t.keybinds-keys {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tgap: 4px;\n\t\t\twhite-space: nowrap;\n\t\t}\n\n\t\t.keybinds-plus {\n\t\t\tfont-size: 12px;\n\t\t\tcolor: rgba(255, 255, 255, 0.4);\n\t\t}\n\n\t\tkbd.keybinds-key {\n\t\t\tbackground: rgba(255, 255, 255, 0.12);\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.2);\n\t\t\tborder-radius: 5px;\n\t\t\tpadding: 2px 7px;\n\t\t\tfont-size: 13px;\n\t\t\tfont-family: monospace;\n\t\t\twhite-space: nowrap;\n\t\t}\n\n\t\t.keybinds-label {\n\t\t\tfont-size: 14px;\n\t\t\tcolor: rgba(255, 255, 255, 0.85);\n\t\t\ttext-align: left;\n\t\t}\n\n\t\t.keybinds-decoration {\n\t\t\tposition: absolute;\n\t\t\tbottom: 48px;\n\t\t\tright: -114px;\n\t\t\tpointer-events: none;\n\t\t\ttransform: rotate(-8deg);\n\t\t\topacity: 0.04;\n\t\t}\n\n\t\t.keybinds-hint {\n\t\t\tmargin: 12px 0 0 0;\n\t\t\tfont-size: 13px;\n\t\t\tcolor: rgba(255, 255, 255, 0.35);\n\t\t\ttext-align: center;\n\t\t}\n\t}\n}\n", 'desktop-ui-styles');
174
174
  this.buildDom();
175
175
  this.wireTooltips();
176
176
  this.wireEvents();
@@ -1092,7 +1092,7 @@
1092
1092
  .seasons-pane {
1093
1093
  flex: 1;
1094
1094
  width: 100%;
1095
- min-width: 16rem;
1095
+ min-width: 13rem;
1096
1096
  border-right: 2px solid rgba(107, 114, 128, 0.2);
1097
1097
  display: flex;
1098
1098
  flex-direction: column;
@@ -1104,10 +1104,13 @@
1104
1104
  display: none;
1105
1105
  }
1106
1106
 
1107
+ /* 29rem, not 36: the two panes' minimums together decided the frame's real
1108
+ * width, so the playlist pinned itself to the 52rem cap and its cards drew
1109
+ * a thumbnail wider than the poster art anywhere else in the player. */
1107
1110
  .episode-menu {
1108
1111
  flex: 1;
1109
1112
  width: 100%;
1110
- min-width: 36rem;
1113
+ min-width: 29rem;
1111
1114
  display: flex;
1112
1115
  flex-direction: column;
1113
1116
  overflow: hidden;
@@ -1254,8 +1257,9 @@
1254
1257
  overflow: hidden;
1255
1258
  border-radius: 6px;
1256
1259
  align-self: center;
1257
- width: 37.5%;
1258
- flex: 0 0 37.5%;
1260
+ width: 32%;
1261
+ flex: 0 0 32%;
1262
+ max-width: 176px;
1259
1263
  aspect-ratio: 16 / 9;
1260
1264
  background: rgba(255, 255, 255, 0.05);
1261
1265
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nomercy-entertainment/nomercy-video-player",
3
3
  "type": "module",
4
- "version": "2.1.2",
4
+ "version": "2.1.3",
5
5
  "description": "Headless, event-driven HTML5 video player with HLS streaming, subtitle rendering (VTT/ASS/SSA), and a plugin system. No UI — you build your own.",
6
6
  "author": {
7
7
  "name": "NoMercy Entertainment",