// ==UserScript== // @name Plex Pyro Custom UI ๐ฅ // @namespace plex-pyro-custom-ui // @version 2.0.8.1 // @author Pyro // @description Theme Park themes + 3 modern interfaces with their own top bar (Aurora, PlexFlix, Prime) + stable top bar + guaranteed custom logo + flags + Blu-ray/DVD/LaserDisc/4K UHD/WEB badges + studio logos // @match https://app.plex.tv/* // @match http://*/web/* // @match https://*/web/* // @match http://127.0.0.1:32400/* // @match http://localhost:32400/* // @run-at document-idle // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant GM_addValueChangeListener // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @connect en.wikipedia.org // @connect www.wikidata.org // @icon https://watch.plex.tv/icons/favicon.ico // @downloadURL https://raw.githubusercontent.com/PyroDzeus/userscripts/main/plex-pyro-custom-ui.user.js // @updateURL https://raw.githubusercontent.com/PyroDzeus/userscripts/main/plex-pyro-custom-ui.user.js // ==/UserScript== (function () { "use strict"; // The @match lines are broad (Plex can be opened from any address), so make // sure this really is Plex Web before theming anything. const isPlex = /(^|\.)plex\.(tv|direct)$/.test(location.hostname) || location.port === "32400" || /plex/i.test(document.title); if (!isPlex) return; /* ============================================================ THEME SWITCH (CHANGE ONLY THIS) ------------------------------------------------------------ THEME_NAME is only the default until you pick one live with the ๐จ pill (bottom-left), Alt+Shift+T or the Violentmonkey menu. Your pick is remembered. ============================================================ */ const THEME_NAME = "spacegray"; // aquamarine | dark | dracula | hotline | hotpink // organizr | overseerr | spacegray | plex | nord | maroon // default (= stock Plex, no theme.park) const TP_CSS = "https://theme-park.dev/css/base/plex/"; // bg / acc = each theme's --main-bg-color / --accent-color, // only used to paint the picker swatches. const THEMES = { aquamarine: { label: "Aquamarine", url: TP_CSS + "aquamarine.css", acc: "18,175,160", bg: "radial-gradient(ellipse at center, #47918a 0%, #0b3161 100%)" }, hotline: { label: "Hotline", url: TP_CSS + "hotline.css", acc: "249,141,201", bg: "linear-gradient(0deg, rgba(247,101,184,1) 0%, rgb(21,95,165) 100%)" }, hotpink: { label: "Hotpink", url: TP_CSS + "hotpink.css", acc: "251,63,98", bg: "linear-gradient(45deg, #fb3f62 0%, #204c80 37%, #004249 97%)" }, dracula: { label: "Dracula", url: TP_CSS + "dracula.css", acc: "80,250,123", bg: "#282a36" }, dark: { label: "Dark", url: TP_CSS + "dark.css", acc: "170,170,170", bg: "radial-gradient(circle, #3a3a3a, #2d2d2d, #202020, #141414, #000000)" }, organizr: { label: "Organizr", url: TP_CSS + "organizr.css", acc: "44,171,227", bg: "#1f1f1f" }, spacegray: { label: "Space Gray", url: TP_CSS + "space-gray.css", acc: "129,166,183", bg: "radial-gradient(ellipse at center, rgba(87,108,117,1) 0%, rgba(37,50,55,1) 100.2%)" }, overseerr: { label: "Overseerr", url: TP_CSS + "overseerr.css", acc: "167,139,250", bg: "linear-gradient(360deg, hsl(221,39%,11%) 65%, hsl(215,28%,17%) 100%)" }, plex: { label: "Plex", url: TP_CSS + "plex.css", acc: "229,160,13", bg: "radial-gradient(circle farthest-side at 0% 100%, rgb(47,47,47) 0%, rgba(47,47,47,0) 100%)," + "radial-gradient(circle farthest-side at 100% 100%, rgb(63,63,63) 0%, rgba(63,63,63,0) 100%)," + "radial-gradient(circle farthest-side at 100% 0%, rgb(76,76,76) 0%, rgba(76,76,76,0) 100%)," + "radial-gradient(circle farthest-side at 0% 0%, rgb(58,58,58) 0%, rgba(58,58,58,0) 100%), #000" }, nord: { label: "Nord", url: TP_CSS + "nord.css", acc: "121,184,202", bg: "#2e3440" }, maroon: { label: "Maroon", url: TP_CSS + "maroon.css", acc: "162,28,101", bg: "radial-gradient(circle farthest-corner at 48.4% 47.5%, rgba(76,21,51,1) 0%, rgba(34,10,37,1) 90%)" } }; /* ============================================================ CUSTOM LOGO (CHANGE ONLY THE LINK) ============================================================ */ const CUSTOM_LOGO_URL = "https://media.discordapp.net/attachments/1452959865915047973/1455672798583324835/LHF_BLUE.png?ex=69559470&is=695442f0&hm=7b80e04f81c5831665abefd9624157d013b23bddcbec6c678962a0babe829c63&=&format=webp&quality=lossless&width=1578&height=1578"; /* ============================================================ LOGO MODIFIABLE (URL ou fichier local) ============================================================ */ (function () { const LOGO_KEY = "pyroLogoUrl"; const LOGO_WIDTH = "110px"; // l'addon force 40px : ร ajuster selon ton image let logoStyle = null; function applyLogo(url) { if (!logoStyle) { logoStyle = document.createElement("style"); logoStyle.id = "pyro-logo-override"; document.head.appendChild(logoStyle); } logoStyle.textContent = url ? `[class*="NavBar-logoContainer-"]{ background-image:url("${url}") !important; background-size:contain !important; background-position:50% !important; background-repeat:no-repeat !important; width:${LOGO_WIDTH} !important; } [class*="NavBar-logoContainer-"] svg{display:none !important;}` : ""; GM_setValue(LOGO_KEY, url || ""); } function pickFile() { const inp = document.createElement("input"); inp.type = "file"; inp.accept = "image/*"; inp.style.display = "none"; inp.addEventListener("change", () => { const f = inp.files && inp.files[0]; if (f) { const r = new FileReader(); r.onload = () => applyLogo(r.result); // data:image/png;base64,... r.readAsDataURL(f); } inp.remove(); }); document.body.appendChild(inp); inp.click(); } function build() { if (document.getElementById("pyro-logo-bar")) return; const bar = document.createElement("div"); bar.id = "pyro-logo-bar"; // the theme picker pill (further down) slots itself in first: [๐จ theme] [logo URL] [logo ๐] bar.style.cssText = "position:fixed;bottom:14px;left:14px;z-index:999999;display:flex;gap:6px;align-items:center;"; const style = "background:#1c1c1c;color:#eee;border:1px solid #444;border-radius:6px;" + "padding:4px 8px;font:600 12px -apple-system,Helvetica,Arial,sans-serif;" + "cursor:pointer;outline:none;opacity:.3;transition:opacity .15s;" + "box-shadow:0 2px 8px rgba(0,0,0,.4);"; [["logo URL", () => { const url = prompt("URL de l'image (vide = logo du thรจme) :", GM_getValue(LOGO_KEY, CUSTOM_LOGO_URL)); if (url !== null) applyLogo(url.trim()); }], ["logo ๐", pickFile] ].forEach(([label, fn]) => { const b = document.createElement("button"); b.textContent = label; b.style.cssText = style; b.addEventListener("mouseenter", () => (b.style.opacity = "1")); b.addEventListener("mouseleave", () => (b.style.opacity = ".3")); b.addEventListener("click", fn); bar.appendChild(b); }); document.body.appendChild(bar); } applyLogo(GM_getValue(LOGO_KEY, CUSTOM_LOGO_URL)); if (document.body) build(); else window.addEventListener("DOMContentLoaded", build); })(); /* ============================================================ DISC ICONS ============================================================ */ const BLURAY_ICON = "https://upload.wikimedia.org/wikipedia/commons/8/8c/Blu_ray_logo.png"; const DVD_ICON = "https://upload.wikimedia.org/wikipedia/commons/8/85/Dvd_logo.svg"; const LASERDISC_ICON = "https://upload.wikimedia.org/wikipedia/commons/3/3b/LaserDisc_logo.svg"; const WEB_ICON = "https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/Icon_Download_White.svg/500px-Icon_Download_White.svg.png"; // Official Ultra HD Blu-ray logo. It's blue in the source file โ // it gets recolored to white by the .plex-disc-uhd CSS filter below. const UHD_ICON = "https://upload.wikimedia.org/wikipedia/en/thumb/2/21/Ultra_HD_Blu-ray_%28logo%29.svg/512px-Ultra_HD_Blu-ray_%28logo%29.svg.png"; // Fallback if the 512px thumbnail isn't served (used automatically on error) const UHD_ICON_FALLBACK = "https://thumb.wikimedia.org/wikipedia/en/thumb/2/21/Ultra_HD_Blu-ray_%28logo%29.svg/3840px-Ultra_HD_Blu-ray_%28logo%29.svg.png"; /* ============================================================ THEME PARK ADDONS ------------------------------------------------------------ Toggled from the theme picker (both on by default). Your custom logo above always wins over plex-alt-logo: it uses !important, the addon doesn't. ============================================================ */ const ADDONS = [ { id: "sideMenu", label: "Overseerr side menu", hint: "Overseerr-style sidebar", url: "https://theme-park.dev/css/addons/plex/overseerr-side-menu/overseerr-side-menu.css" }, { id: "altLogo", label: "Alternative logo", hint: "theme.park logo slot, under your custom logo", url: "https://theme-park.dev/css/addons/plex/plex-alt-logo/plex-alt-logo.css" } ]; /* ============================================================ CSS INJECTION (safe) ------------------------------------------------------------ Theme + addons are loaded by the THEME PICKER below. ============================================================ */ GM_addStyle(` /* Our injected logo image */ .plex-custom-logo-img { height: 32px !important; width: auto !important; max-width: 180px !important; object-fit: contain !important; display: block !important; opacity: 1 !important; visibility: visible !important; z-index: 99999 !important; } /* Flags */ .plex-audio-flag { margin-left: .35em; display: inline-block; transform: translateY(1px); } /* Disc badges */ .plex-disc-badge { margin-left: .45em; height: 14px; width: auto; vertical-align: middle; opacity: .92; } /* 4K UHD badge โ recolored from blue to white */ .plex-disc-uhd { height: 18px; opacity: .95; filter: brightness(0) invert(1); } /* Added icons: never selectable / copied */ .plex-audio-flag, .plex-disc-badge { user-select: none; -webkit-user-select: none; } /* WEB icon = yellow */ .plex-disc-web { filter: brightness(1.2) sepia(1) saturate(6) hue-rotate(15deg); } `); /* ============================================================ INTERFACE SKINS โ the modern layouts (picked in the panel) ------------------------------------------------------------ theme.park owns the COLOURS; a skin owns the SHAPES and the MOTION: how a poster grows under the cursor, how round the corners are, how the top bar behaves, how the sidebar reads. Three skins share one engine, and differ only by the tokens in SKINS below: ยท Aurora โ calm neutral, 4px tiles, white ring (Disney+) ยท PlexFlix โ near-black, bigger zoom, red accent (Netflix) ยท Prime โ deep navy, very round, blue accent (Prime Video) Their numbers were read out of saved sessions of those sites, so the proportions feel right rather than invented. No logo, font file or stylesheet of theirs is shipped here. With a theme.park theme on, a skin leaves the palette alone and borrows the theme's accent. With "Default" (no theme.park) it also paints its own background, like a standalone script. The hover-zoom fix, in one line: a Plex row clips at its padding box, so symmetric padding plus an equal negative margin gives the poster room to grow without touching `overflow` โ which would have broken the scroll wheel. ============================================================ */ const BLEED = 28; // px of clip-free room added above/below a row const SKINS = { aurora: { bg: "#17171B", nav: "#0b0b0ee6", text: "#dbdcde", textSoft: "#a9aeb5", // Disney+'s loader cyan, sampled from the site itself (their CSS keeps the // brand blue #0072d2 for links, but the interface reads as this cyan). accent: "#61d3e5", accentHover: "color-mix(in srgb, #61d3e5 86%, white)", accentPress: "color-mix(in srgb, #61d3e5 86%, black)", rTile: "4px", rPanel: "12px", rButton: "999px", scale: 1.06, ring: "#61d3e5", ringW: "2px", dur: ".30s", durS: ".20s", ease: "ease-in-out", shadow: "0 16px 26px #000000a8, 0 0 20px #61d3e52e", hairline: "#ffffff1a", font: 'PlexCircular, "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', sidebar: "#00000038", sidebarOpen: "#14141ae6", blur: "28px" }, netflix: { bg: "#141414", nav: "#141414f2", text: "#e5e5e5", textSoft: "#b3b3b3", // #e50914 and #b9090b both come straight from Netflix's own stylesheet. accent: "#e50914", accentHover: "color-mix(in srgb, #e50914 88%, white)", accentPress: "#b9090b", rTile: "4px", rPanel: "4px", rButton: "4px", scale: 1.12, ring: "#e50914", ringW: "2px", dur: ".35s", durS: ".15s", ease: "cubic-bezier(.86,0,.07,1)", shadow: "0 18px 34px #000000d9, 0 0 22px #e5091436", hairline: "#ffffff14", font: '"Netflix Sans", "Helvetica Neue", Helvetica, -apple-system, "Segoe UI", Roboto, Ubuntu, sans-serif', sidebar: "#00000059", sidebarOpen: "#141414f7", blur: "18px" }, prime: { bg: "#00050d", nav: "#00050de6", text: "#c4cacf", textSoft: "#8197a4", // #1a98ff is Prime Video's focus ring, #0070f0 its pressed blue. accent: "#1a98ff", accentHover: "color-mix(in srgb, #1a98ff 88%, white)", accentPress: "#0070f0", rTile: "8px", rPanel: "16px", rButton: "10000px", scale: 1.05, ring: "#1a98ff", ringW: "2px", dur: ".30s", durS: ".20s", ease: "ease-in-out", shadow: "0 18px 30px #000000a6, 0 0 22px #1a98ff33", hairline: "#ffffff1f", font: '"Amazon Ember", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif', sidebar: "#191e2599", sidebarOpen: "#191e25f2", blur: "24px" } }; /** * @param id skin key in SKINS * @param accent colour for rings, markers and tabs * @param ownPalette true = the skin paints the background too * (only when no theme.park sheet is loaded) */ function skinCss(id, accent, ownPalette) { const k = SKINS[id] || SKINS.aurora; const flix = id === "netflix"; const prime = id === "prime"; return ` :root { --ax-bg: ${k.bg}; --ax-accent: ${accent}; --ax-r-tile: ${k.rTile}; --ax-r-panel: ${k.rPanel}; --ax-r-btn: ${k.rButton}; --ax-bleed: ${BLEED}px; --ax-scale: ${k.scale}; --ax-ring-w: ${k.ringW}; --ax-dur: ${k.dur}; --ax-dur-s: ${k.durS}; --ax-ease: ${k.ease}; --ax-ease-out: cubic-bezier(0, 0, .5, 1); } ${ownPalette ? ` /* No theme.park sheet to respect: the skin owns the palette. Plex's gold reaches the page two ways โ through its design tokens, and hard-coded in a few components โ so both are covered here, otherwise the yellow keeps showing up next to the skin's red or blue. */ :root { --color-accent: var(--ax-accent); --color-accent-hover: ${k.accentHover}; --color-accent-press: ${k.accentPress}; --color-background-accent: var(--ax-accent); --color-background-accent-hover: ${k.accentHover}; --color-background-accent-press: ${k.accentPress}; --color-background-accent-subtle: ${k.accent}33; --color-border-accent: var(--ax-accent); --color-brand-accent: var(--ax-accent); --color-text-accent: var(--ax-accent); --color-text-accent-hover: ${k.accentHover}; --color-static-yellow: var(--ax-accent); --color-static-yellow-hover: ${k.accentHover}; --color-primary: var(--ax-accent); --color-primary-hover: ${k.accentHover}; --color-focus: var(--ax-accent); --accent-color: var(--ax-accent); --accent-color-hover: ${k.accentHover}; --brand-color: var(--ax-accent); --color-background-modal: ${k.sidebarOpen}; --color-menu: ${k.sidebarOpen}; --color-overlay-background:${k.sidebarOpen}; } html, body { background-color: var(--ax-bg) !important; } [class*="FullPageBackground-backgroundContainer-"] { background-color: var(--ax-bg) !important; } /* The gold spots Plex paints itself: unwatched dots, progress bars, primary buttons, toggles and the live-activity icon. */ [class*="UnwatchedIndicator"], [class*="UnplayedIndicator"], [class*="PlayedIndicator"] { background-color: var(--ax-accent) !important; } /* Only the filled part, which Plex sizes with an inline width. Painting the track as well is what made every poster look watched right through. */ /* A filled bar is an empty element sized by Plex; the dashboard's session card also has "Progress" in its class names but holds the text, so :empty is what keeps the colour on the bar and off the card. */ [class*="Progress"] > div:empty, [class*="Progress"] > span:empty, [class*="ProgressBar-fill"]:empty, [class*="Progress-fill"]:empty, progress::-webkit-progress-value { background-color: var(--ax-accent) !important; } [class*="Button-primary"], [class*="Button-button-"][class*="primary"], [class*="ButtonGroup-"] [aria-pressed="true"] { background-color: var(--ax-accent) !important; border-color: var(--ax-accent) !important; color: ${flix || prime ? "#fff" : "#101010"} !important; } [class*="Button-primary"]:hover, [class*="Button-button-"][class*="primary"]:hover, .pyro-play:hover { background-color: #fff !important; } input[type="checkbox"], input[type="radio"], input[type="range"], progress { accent-color: var(--ax-accent); } [class*="Toggle-"][class*="checked"], [class*="Switch-"][class*="checked"], [class*="Checkbox-"][class*="checked"] { background-color: var(--ax-accent) !important; } [class*="ActivityIndicator-"], [class*="Spinner-"] { color: var(--ax-accent) !important; } /* Text and icons Plex tints gold through its own class, not a token. */ [class*="-accent-"], [class*="accentColor"] { color: var(--ax-accent) !important; } ` : ""} html, body { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; } [class*="PageHeaderTitle-title-"], [class*="VirtualHubScroller-hubScroller-"] h2, [class*="HubCellTitle-hubCellTitle-"] { font-family: ${k.font}; } /* ---- rows: room to zoom, without touching overflow ---- */ [class*="VirtualHubScroller-hubScroller-"][class*="Scroller-horizontal-"], [class*="HubCell-hubScroller-"][class*="Scroller-horizontal-"] { padding-top: var(--ax-bleed); padding-bottom: var(--ax-bleed); margin-top: calc(-1 * var(--ax-bleed)); margin-bottom: calc(-1 * var(--ax-bleed)); } [class*="HubCell-hubScroller-"][class*="Scroller-horizontal-"] { padding-bottom: calc(var(--ax-bleed) + 20px); margin-bottom: calc(-1 * var(--ax-bleed)); } [class*="PageContent-pageContentScroller-"][class*="Scroller-vertical-"] { scroll-padding-top: var(--ax-bleed); } /* The row now overlaps its header, so keep the header clickable. */ [class*="VirtualHubScroller-hubHeader-"], [class*="HubCellHeader-hubCellHeader-"] { margin-bottom: 12px; position: relative; z-index: 2; } [class*="VirtualHubScroller-hubHeader-"] h2, [class*="HubCellTitle-hubCellTitle-"] { font-size: 17px; font-weight: ${flix ? 500 : 600}; letter-spacing: -.01em; color: ${k.text}; transition: color var(--ax-dur) var(--ax-ease); } [class*="VirtualHubScroller-hub-"]:hover [class*="VirtualHubScroller-hubHeader-"] h2, [class*="VirtualHubScroller-hub-"]:hover [class*="VirtualHubScroller-hubHeader-"] h2 a, [class*="HubCell-hubCell-"]:hover [class*="HubCellTitle-hubCellTitle-"] { color: #fff; } /* Paging arrows appear with the row, as on the streaming apps. */ [class*="VirtualHubScroller-hubActions-"], [class*="HubCell-hubActions-"] { opacity: 0; transition: opacity var(--ax-dur) var(--ax-ease-out); } [class*="VirtualHubScroller-hub-"]:hover [class*="VirtualHubScroller-hubActions-"], [class*="VirtualHubScroller-hub-"]:focus-within [class*="VirtualHubScroller-hubActions-"], [class*="HubCell-hubCell-"]:hover [class*="HubCell-hubActions-"], [class*="HubCell-hubCell-"]:focus-within [class*="HubCell-hubActions-"] { opacity: 1; } [class*="VirtualHubScroller-hubScrollButton-"], [class*="HubCell-hubScrollButton-"] { border-radius: ${flix ? "4px" : "999px"}; ${flix ? "background-color: #000000a6;" : ""} transition: background-color var(--ax-dur-s) var(--ax-ease), transform var(--ax-dur-s) var(--ax-ease); } [class*="VirtualHubScroller-hubScrollButton-"]:hover:not(.isDisabled), [class*="HubCell-hubScrollButton-"]:hover:not(.isDisabled) { background-color: ${flix ? "#000000e6" : "#ffffff1a"}; transform: scale(1.12); } /* ---- poster cards ---- */ [class*="PosterCard-card-"]:not([class*="MetadataPosterCard-card-"]) { border-radius: var(--ax-r-tile); transform: scale(1) translateZ(0); transform-origin: center center; backface-visibility: hidden; box-shadow: inset 0 0 0 1px ${k.hairline}; transition: transform var(--ax-dur) var(--ax-ease), box-shadow var(--ax-dur) var(--ax-ease); } [class*="PosterCard-card-"]:not([class*="MetadataPosterCard-card-"]):hover { transform: scale(var(--ax-scale)) translateZ(0); z-index: 3; box-shadow: inset 0 0 0 1px #ffffff26, ${k.shadow}; } [class*="MetadataSimplePosterCard-image-"], [class*="MetadataDirectoryListItem-image-"] { border-radius: var(--ax-r-tile); } [class*="PosterCard-card-"]:not([class*="MetadataPosterCard-card-"])::after { content: ""; position: absolute; inset: 0; border-radius: inherit; outline: var(--ax-ring-w) solid ${k.ring}; outline-offset: 0; opacity: 0; pointer-events: none; z-index: 2; transition: opacity var(--ax-dur) ease; } [class*="PosterCard-card-"]:not([class*="MetadataPosterCard-card-"]):hover::after, [class*="PosterCard-card-"]:not([class*="MetadataPosterCard-card-"]):focus-within::after { opacity: 1; } /* Plex writes will-change on every cell, which makes it a stacking context: the zoomed poster can only rise above its neighbours if the cell rises. */ [data-testid="cellItem"], [class*="VirtualListScroller-scroller-"] > div > [style*="will-change"] { z-index: 0; } [data-testid="cellItem"]:has(> [class*="PosterCard-card-"]:hover), [data-testid="cellItem"]:has(> [class*="PosterCard-card-"]:focus-within), [style*="will-change"]:has(> [class*="PosterCard-card-"]:hover), [style*="will-change"]:has(> [class*="PosterCard-card-"]:focus-within) { z-index: 30; } [class*="MetadataPosterCardTitle-title-"] { transition: color var(--ax-dur) var(--ax-ease); } [data-testid="cellItem"]:hover [class*="MetadataPosterCardTitle-title-"] { color: #fff; } [data-testid="cellItem"]:hover [class*="MetadataPosterCardTitle-title-"] + [class*="MetadataPosterCardTitle-title-"] { color: ${k.textSoft}; } [class*="MetadataPosterCardTitle-title-"] a:hover, a[class*="MetadataPosterCardTitle-title-"]:hover { text-decoration: none !important; } [class*="MetadataDirectoryListItem-image-"] img { opacity: .82; transition: opacity var(--ax-dur) var(--ax-ease); } [class*="PosterCard-card-"]:hover [class*="MetadataDirectoryListItem-image-"] img, [class*="PosterCard-card-"]:focus-within [class*="MetadataDirectoryListItem-image-"] img { opacity: 1; } [class*="MetadataDirectoryListItem-overlayTitle-"] { text-shadow: 0 2px 10px #000000b3, 0 1px 3px #000000e6; letter-spacing: -.01em; } [class*="MetadataPosterCardBadge-badgeBackground-"] { background-color: #000000a6; -webkit-backdrop-filter: blur(12px) saturate(140%); backdrop-filter: blur(12px) saturate(140%); border-radius: ${prime ? "8px" : flix ? "2px" : "999px"}; box-shadow: inset 0 0 0 1px #ffffff1f; padding: 3px 8px; min-width: 0; } [class*="MetadataPosterCardBadge-topRightBadge-"] { top: 6px; right: 6px; } [class*="MetadataPosterCard-bottomRightBadge-"] { bottom: 6px; right: 6px; } /* ============================================================================ * What tells the three skins apart, beyond the palette * (values read from the saved sessions of each site) * ========================================================================= */ ${id === "aurora" ? ` /* Disney+ CTA: white pill, dark label, hover #e9ebf0. Titles stay readable. */ [class*="Button-primary"], [class*="Button-button-"][class*="primary"], .pyro-play { background-color: #f1f2f4 !important; color: #252526 !important; border-radius: 100px !important; font-weight: 600; letter-spacing: .02em; } [class*="Button-primary"]:hover, [class*="Button-button-"][class*="primary"]:hover, .pyro-play:hover { background-color: #e9ebf0 !important; } [class*="MetadataPosterCardTitle-title-"] { opacity: .88; } [data-testid="cellItem"]:hover [class*="MetadataPosterCardTitle-title-"] { opacity: 1; } ` : ""} ${flix ? ` /* Netflix: white Play button with square corners, grey secondary, and poster titles that only appear when the row is hovered. */ [class*="Button-primary"], [class*="Button-button-"][class*="primary"], .pyro-play { background-color: #fff !important; color: #141414 !important; border-radius: 4px !important; font-weight: 700; } [class*="Button-primary"]:hover, [class*="Button-button-"][class*="primary"]:hover, .pyro-play:hover { background-color: #e6e6e6 !important; } [class*="Button-button-"]:not([class*="primary"]):not([class*="TabButton-"]):not(.pyro-play) { background-color: #6d6d6eb3 !important; color: #fff !important; border-radius: 4px !important; } [class*="MetadataPosterCardTitle-title-"] { opacity: 0; transition: opacity var(--ax-dur-s) var(--ax-ease); } [class*="VirtualHubScroller-hub-"]:hover [class*="MetadataPosterCardTitle-title-"], [class*="HubCell-hubCell-"]:hover [class*="MetadataPosterCardTitle-title-"], [data-testid="cellItem"]:hover [class*="MetadataPosterCardTitle-title-"], [class*="PageContent-pageContentScroller-"]:hover [class*="MetadataPosterCardTitle-title-"] { opacity: 1; } /* Row paging: tall dark panels at the edges rather than small round buttons. */ [class*="VirtualHubScroller-hubScrollButton-"], [class*="HubCell-hubScrollButton-"] { border-radius: 0 !important; width: 48px !important; background-color: #000000a6 !important; } [class*="VirtualHubScroller-hubScrollButton-"]:hover:not(.isDisabled), [class*="HubCell-hubScrollButton-"]:hover:not(.isDisabled) { background-color: #000000d9 !important; transform: none !important; } [class*="IconButton-iconButton-"], [class*="IconButton-button-"] { box-shadow: inset 0 0 0 2px #ffffff8c; } ` : ""} ${prime ? ` /* Prime Video: everything rounder โ pill CTA, 12px chips, circular icon buttons with a thin outline, and a little more air between posters. */ [class*="Button-primary"], [class*="Button-button-"][class*="primary"], .pyro-play { background-color: #f1f1f1 !important; color: #0f171e !important; border-radius: 10px !important; font-weight: 700; padding-left: 18px; padding-right: 18px; } [class*="Button-primary"]:hover, [class*="Button-button-"][class*="primary"]:hover, .pyro-play:hover { background-color: #fff !important; } [class*="Button-button-"]:not([class*="primary"]):not([class*="TabButton-"]):not(.pyro-play) { background-color: #ffffff1a !important; border-radius: 10000px !important; } [class*="IconButton-iconButton-"], [class*="IconButton-button-"] { box-shadow: inset 0 0 0 1px #ffffff40; } [class*="IconButton-iconButton-"]:hover, [class*="IconButton-button-"]:hover { box-shadow: inset 0 0 0 1px var(--ax-accent); } [data-testid="cellItem"] { padding: 0 3px; } [class*="MetadataPosterCardTitle-title-"] { color: ${k.text}; } ` : ""} /* ============================================================================ * THE TOP BAR * Plex keeps its page tabs (Recommended, Library, Collectionsโฆ) on a second * row. The three sites put them in the bar itself, so the script moves them * up and each skin then dresses them its own way โ a white pill on Netflix, * a white rounded rectangle on Prime Video, a dark segmented pill on * Disney+. Proportions below are measured from the three bars. * ========================================================================= */ /* Measured on the three bars, pill by pill, at the centre of each pill (Retina captures, halved): Netflix active pill 80.5 x 40.5, fully round, white veil at 22%, label cap 11.5 โ 14px here, 13.5 of padding either side Prime Video active block 104 x 42, 10px corners, #e7e7e8, text #0f171e Disney+ active pill 108 x 40 inside a 52-tall group with 6 of padding, labels #b6b6b7 The height is set outright rather than left to padding: that was what made the first attempt 18 to 20px too tall. */ #pyro-topbar-tabs { display: flex; align-items: center; min-width: 0; overflow: hidden; ${flix ? "gap: 12px; margin-left: 24px; margin-right: auto;" : prime ? "gap: 8px; margin-left: 24px; margin-right: auto;" : "gap: 0; margin: 0 auto; padding: 6px; border-radius: 999px; background: #26262be6;"} } /* The row Plex hands us brings its own spacing; the skin sets it instead, so the distance between labels matches the site it imitates. */ #pyro-topbar-tabs > * { display: flex !important; align-items: center !important; gap: ${flix ? "12px" : prime ? "8px" : "0px"} !important; margin: 0 !important; padding: 0 !important; border: 0 !important; background: none !important; } #pyro-topbar-tabs [class*="TabButton-button-"], #pyro-topbar-tabs [class*="PageHeaderTabButton-button-"], #pyro-topbar-tabs a, #pyro-topbar-tabs button { display: inline-flex !important; align-items: center !important; justify-content: center !important; box-sizing: border-box !important; font-family: inherit; line-height: 1; ${flix ? "height: 40px; padding: 0 14px; font-size: 14px; font-weight: 500; border-radius: 999px; color: rgba(255,255,255,.72) !important;" : prime ? "height: 42px; padding: 0 16px; font-size: 14px; font-weight: 600; border-radius: 10px; color: #fff !important;" : "height: 40px; padding: 0 20px; font-size: 14px; font-weight: 600; border-radius: 999px; color: #b6b6b7 !important;"} white-space: nowrap; background: none; min-width: 0; transition: background-color var(--ax-dur-s) var(--ax-ease), color var(--ax-dur-s) var(--ax-ease); } #pyro-topbar-tabs [class*="TabButton-button-"]:hover, #pyro-topbar-tabs [class*="PageHeaderTabButton-button-"]:hover { color: #fff !important; background-color: ${flix ? "rgba(255,255,255,.1)" : prime ? "rgba(255,255,255,.12)" : "rgba(255,255,255,.08)"} !important; } /* The selected one: the single biggest difference between the three bars. */ #pyro-topbar-tabs [class*="TabButton-selected-"], #pyro-topbar-tabs [class*="PageHeaderTabButton-isSelected-"] { ${flix ? "background: rgba(255,255,255,.22) !important; color: #fff !important; font-weight: 700;" : prime ? "background: #e7e7e8 !important; color: #0f171e !important; border-radius: 10px !important; font-weight: 700;" : "background: #fff !important; color: #111 !important; font-weight: 700;"} } #pyro-topbar-tabs [class*="TabButton-selected-"] *, #pyro-topbar-tabs [class*="PageHeaderTabButton-isSelected-"] * { color: inherit !important; } /* No underline once the tab is a pill. */ #pyro-topbar-tabs [class*="TabButton-selected-"]::after, #pyro-topbar-tabs [class*="PageHeaderTabButton-isSelected-"]::after { display: none !important; } /* The bar itself: heights measured on the three sites. */ /* Bar heights, measured the same way: Netflix 68, Prime Video 56, Disney+ 62 โ and Disney+ centres its group, hence the gap of 0. */ body.pyro-topbar [class*="NavBar-container-"], body.pyro-topbar [class*="NavBar-navBar-"] { height: ${flix ? "57px" : prime ? "56px" : "62px"}; min-height: 0; padding-top: 0; padding-bottom: 0; align-items: center; padding-left: ${flix ? "4%" : "24px"}; padding-right: ${flix ? "4%" : "24px"}; gap: ${prime ? "12px" : "16px"}; } /* Plex's search field is what pushes the bar past those heights. */ body.pyro-topbar [class*="UniversalSearch-"] input, body.pyro-topbar [class*="UniversalSearch-searchInputContainer-"] { height: ${flix ? "34px" : prime ? "34px" : "36px"}; min-height: 0; } /* The row the tabs came from keeps the library name and the view controls, so it stays โ just thinner, and out of the way. */ body.pyro-topbar [class*="PageHeader-pageHeader-"] { min-height: 0; padding-top: 6px; padding-bottom: 6px; } /* ---- chrome ---- */ [class*="PageHeader-pageHeader-"] { position: relative; z-index: 40; background-color: transparent; transition: background-color var(--ax-dur) var(--ax-ease); } [class*="PageHeader-pageHeader-"]::before { content: ""; position: absolute; inset: 0 0 auto 0; height: ${flix ? "140px" : "230px"}; pointer-events: none; z-index: -1; background: ${flix ? "linear-gradient(180deg, #000000b3 10%, #0000 100%)" : prime ? "linear-gradient(180deg, #00050dd9 0%, #00050d00 100%)" : "linear-gradient(0deg, #0000 0%, #0000000a 14.93%, #00000021 30.32%, #00000040 45.76%, #0006 60.85%, #0000008c 75.19%, #000000b0 88.37%, #000c 100%)"}; opacity: 1; transition: opacity var(--ax-dur) var(--ax-ease); } [class*="PageHeader-pageHeader-"]::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; pointer-events: none; background: linear-gradient(90deg, #fff0, #fff3 20% 80%, #fff0); opacity: 0; transition: opacity var(--ax-dur) var(--ax-ease); } body.pyro-aurora-scrolled [class*="PageHeader-pageHeader-"] { background-color: ${k.nav}; ${flix ? "" : `-webkit-backdrop-filter: blur(${k.blur}) saturate(160%); backdrop-filter: blur(${k.blur}) saturate(160%);`} } body.pyro-aurora-scrolled [class*="PageHeader-pageHeader-"]::before { opacity: 0; } body.pyro-aurora-scrolled [class*="PageHeader-pageHeader-"]::after { opacity: ${flix ? 0 : 1}; } [class*="NavBar-container-"] { background: transparent; border-radius: var(--ax-r-btn); } [class*="TabButton-button-"], [class*="PageHeaderTabButton-button-"] { border-radius: var(--ax-r-btn); transition: color var(--ax-dur-s) var(--ax-ease), background-color var(--ax-dur-s) var(--ax-ease); } [class*="TabButton-button-"]:hover, [class*="PageHeaderTabButton-button-"]:hover { color: #fff !important; background-color: #ffffff0d !important; } [class*="TabButton-selected-"], [class*="PageHeaderTabButton-isSelected-"], [class*="TabButton-selected-"] *, [class*="PageHeaderTabButton-isSelected-"] * { color: var(--ax-accent) !important; } [class*="TabButton-selected-"]::after, [class*="PageHeaderTabButton-isSelected-"]::after { background-color: var(--ax-accent); height: 2px; border-radius: 2px; } /* Selected library in the sidebar: Plex tints the label and the icon too. */ [class*="SourceSidebarLink-isSelected-"] [class*="SourceSidebarLink-title-"], [class*="SourceSidebarLink-isSelected-"] [class*="SourceSidebarLink-iconContainer-"], [class*="SidebarLink-isSelected-"] [class*="title-"], [class*="SidebarLink-isSelected-"] svg { color: var(--ax-accent) !important; } [class*="SourceSidebar-sidebar-"] { background-color: ${k.sidebar}; border-radius: var(--ax-r-panel); box-shadow: inset 0 0 0 1px #ffffff0f; } [class*="SourceSidebar-expandedSidebar-"], [class*="SourceSidebar-openSidebar-"] [class*="SourceSidebar-pane-"] { -webkit-backdrop-filter: blur(${k.blur}) saturate(150%); backdrop-filter: blur(${k.blur}) saturate(150%); } [class*="SourceSidebar-expandedSidebar-"] { border-radius: var(--ax-r-panel); box-shadow: 0 24px 48px #00000080, inset 0 0 0 1px #ffffff14; } [class*="SourceSidebar-footer-"] { background-color: #0000001f; border-radius: 0 0 var(--ax-r-panel) var(--ax-r-panel); } [class*="SourceSidebarLink-sourceLink-"], [class*="SourceSidebarItem-button-"] { border-radius: ${prime ? "12px" : "8px"}; transition: background-color var(--ax-dur-s) var(--ax-ease), color var(--ax-dur-s) var(--ax-ease); } [class*="SourceSidebarLink-sourceLink-"]:hover { background-color: #ffffff0f; } [class*="SourceSidebarLink-isSelected-"]::before, [class*="SidebarLink-isSelected-"]::before { background-color: var(--ax-accent) !important; width: 3px !important; border-radius: 0 3px 3px 0 !important; top: 6px !important; bottom: 6px !important; } [class*="SourceSidebar-toggleMenuIcon-"] { background-color: #ffffff14; transition: background-color var(--ax-dur-s) var(--ax-ease); } [class*="SourceSidebar-toggleMenuIcon-"]:hover { background-color: #ffffff29; } [class*="Button-button-"]:not([class*="TabButton-"]):not([class*="SplitButton-"]) { border-radius: var(--ax-r-btn); transition: transform var(--ax-dur-s) var(--ax-ease), background-color var(--ax-dur-s) var(--ax-ease); } [class*="Button-button-"]:not([class*="TabButton-"]):not([class*="SplitButton-"]):not(.isDisabled):hover { transform: scale(1.04); } [class*="Button-button-"]:not([class*="TabButton-"]):not([class*="SplitButton-"]):active { transform: scale(.98); } [class*="IconButton-iconButton-"], [class*="IconButton-button-"] { border-radius: 999px; transition: background-color var(--ax-dur-s) var(--ax-ease), color var(--ax-dur-s) var(--ax-ease), transform var(--ax-dur-s) var(--ax-ease); } [class*="IconButton-iconButton-"]:hover:not(.isDisabled), [class*="IconButton-button-"]:hover:not(.isDisabled) { color: #fff; transform: scale(1.1); } [class*="UniversalSearch-searchInputContainer-"] input, input[type="search"] { border-radius: ${flix ? "4px" : "999px"} !important; transition: background-color var(--ax-dur-s) var(--ax-ease), box-shadow var(--ax-dur-s) var(--ax-ease); } [class*="UniversalSearch-searchInputContainer-"] input:focus { box-shadow: 0 0 0 2px #ffffff40; } [class*="Menu-menu-"], [class*="ModalContent-"], [class*="Modal-modal-"], [class*="MediaInfo"], [role="menu"], [role="dialog"] { border-radius: var(--ax-r-panel) !important; box-shadow: 0 24px 56px #000000a6, inset 0 0 0 1px #ffffff14; } ${ownPalette ? ` /* The Media Info window keeps Plex's stock grey, which clashes with the skin. */ [class*="ModalContent-"], [class*="Modal-modal-"], [class*="MediaInfo"], [role="dialog"] { background-color: ${k.sidebarOpen} !important; } [class*="ModalHeader-"], [class*="ModalFooter-"] { background-color: transparent !important; } ` : ""} /* The detail page: Plex's own links and expanders are gold by default. */ ${ownPalette ? ` [class*="PrePlayDetails-"] a, [class*="MetadataDetails-"] a, [class*="ExpandableText-"] button, [class*="ExpandableText-"] a, [class*="StreamSelector-"] [class*="selected"], [class*="MediaInfo"] a { color: var(--ax-accent) !important; } ` : ""} [class*="Overlay-overlay-"], [class*="Backdrop-"] { -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); } [class*="Scroller-scroller-"] { scrollbar-width: thin; } [class*="Scroller-scroller-"]::-webkit-scrollbar { width: 10px; height: 10px; } [class*="Scroller-scroller-"]::-webkit-scrollbar-thumb { background-color: #ffffff21; border: 3px solid transparent; background-clip: padding-box; border-radius: 8px; } [class*="Scroller-scroller-"]::-webkit-scrollbar-thumb:hover { background-color: #ffffff3d; } @media (prefers-reduced-motion: reduce) { [class*="PosterCard-card-"], [class*="PosterCard-card-"]::after, [class*="Button-button-"], [class*="PageHeader-pageHeader-"] { transition-duration: .01ms !important; } [class*="PosterCard-card-"]:hover { transform: none !important; } } /* Never touch the player, nor another script's own panels. */ .show-video-player [class*="PageHeader-pageHeader-"], [class*="PlayerContainer-container-"] [class*="PosterCard-card-"], [data-pyro-ignore] [class*="PosterCard-card-"] { transform: none !important; } `; } /* ============================================================ SKINS โ mounting ------------------------------------------------------------ One `; pill = root.querySelector("button"); pillDot = root.querySelector(".dot"); pillLabel = root.querySelector(".label"); pill.addEventListener("click", e => { e.stopPropagation(); togglePanel(); }); refresh(); } // First item of the logo bar when it's there, floating bottom-left otherwise. function ensurePill() { if (!document.body) return; if (!pillHost) buildPill(); const bar = document.getElementById("pyro-logo-bar"); if (bar) { if (pillHost.parentNode !== bar) { bar.prepend(pillHost); pillHost.dataset.mode = "bar"; } } else if (pillHost.parentNode !== document.body) { document.body.appendChild(pillHost); pillHost.dataset.mode = "float"; } } /* ---------- the panel ---------- */ const PANEL_CSS = ` :host { all: initial; } *, *::before, *::after { box-sizing: border-box; } .panel { --acc: 229,160,13; position: fixed; z-index: 2147483000; width: min(440px, calc(100vw - 16px)); max-height: calc(100vh - 32px); overflow: auto; padding: 14px; border-radius: 14px; background: rgba(18,20,26,.94); -webkit-backdrop-filter: blur(14px) saturate(1.2); backdrop-filter: blur(14px) saturate(1.2); color: #e8eaed; font: 13px/1.45 -apple-system, system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; box-shadow: 0 24px 60px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.08); opacity: 0; visibility: hidden; transform: translateY(6px) scale(.985); transform-origin: bottom left; transition: opacity .16s ease, transform .16s ease, visibility 0s linear .16s; } .panel.open { opacity: 1; visibility: visible; transform: none; transition: opacity .16s ease, transform .16s ease; } .head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; } .title { font-weight: 700; font-size: 15px; letter-spacing: .01em; } .title span { color: rgb(var(--acc)); } .sub { color: #9aa0a6; font-size: 12px; } .x { all: unset; cursor: pointer; width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; font-size: 20px; line-height: 1; color: #9aa0a6; } .x:hover { background: rgba(255,255,255,.08); color: #fff; } .x:focus-visible { outline: 2px solid #fff; outline-offset: 1px; } .section { margin: 14px 0 8px; font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: #8b9097; } .section em { font-style: normal; text-transform: none; letter-spacing: 0; font-weight: 400; margin-left: 4px; } .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; } .tile { all: unset; position: relative; height: 56px; border-radius: 6px; cursor: pointer; display: grid; place-items: center; overflow: hidden; background: var(--bg); color: #f2f2f2; font: 400 26px/1 "Brush Script MT", "Segoe Script", "Snell Roundhand", cursive; text-shadow: 0 2px 3px rgba(0,0,0,.65), 0 0 1px rgba(0,0,0,.8); box-shadow: inset 0 0 0 1px rgba(255,255,255,.07); transition: transform .12s ease, box-shadow .12s ease, filter .12s ease; } .tile:hover { transform: translateY(-1px); filter: brightness(1.08); box-shadow: inset 0 0 0 1px rgba(255,255,255,.22); } .tile:focus-visible { outline: 2px solid #fff; outline-offset: 2px; } .tile[aria-pressed="true"] { box-shadow: 0 0 0 2px rgb(var(--tile-acc)), inset 0 0 0 1px rgba(255,255,255,.2); } .tile[aria-pressed="true"]::after { content: "โ"; position: absolute; top: 6px; right: 6px; width: 18px; height: 18px; border-radius: 50%; display: grid; place-items: center; background: rgb(var(--tile-acc)); color: #111; font: 700 11px/1 system-ui, sans-serif; text-shadow: none; } .panel[data-busy] .tile[aria-pressed="true"]::before { content: ""; position: absolute; inset: 0; background: linear-gradient(100deg, transparent 20%, rgba(255,255,255,.2) 50%, transparent 80%); background-size: 200% 100%; animation: shimmer .9s linear infinite; } @keyframes shimmer { from { background-position: 150% 0; } to { background-position: -50% 0; } } .ui-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; } .ui-tile { all: unset; position: relative; cursor: pointer; overflow: hidden; padding: 10px 12px; border-radius: 8px; background: var(--bg); color: #f2f2f2; box-shadow: inset 0 0 0 1px rgba(255,255,255,.07); transition: transform .12s ease, box-shadow .12s ease, filter .12s ease; } .ui-tile:hover { transform: translateY(-1px); filter: brightness(1.1); box-shadow: inset 0 0 0 1px rgba(255,255,255,.22); } .ui-tile:focus-visible { outline: 2px solid #fff; outline-offset: 2px; } .ui-tile b { display: block; font-weight: 600; font-size: 13px; } .ui-tile small { display: block; color: #a9aeb5; font-size: 11px; line-height: 1.35; margin-top: 2px; } .ui-tile[aria-pressed="true"] { box-shadow: 0 0 0 2px rgb(var(--acc)), inset 0 0 0 1px rgba(255,255,255,.2); } .ui-tile[aria-pressed="true"]::after { content: "โ"; position: absolute; top: 8px; right: 8px; width: 18px; height: 18px; border-radius: 50%; display: grid; place-items: center; background: rgb(var(--acc)); color: #111; font: 700 11px/1 system-ui, sans-serif; } .addons { display: grid; gap: 6px; transition: opacity .15s; } .addons[data-off] { opacity: .4; pointer-events: none; } .row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; border-radius: 8px; background: rgba(255,255,255,.04); cursor: pointer; } .row:hover { background: rgba(255,255,255,.07); } .row b { display: block; font-weight: 600; } .row small { display: block; color: #9aa0a6; font-size: 12px; } .switch { appearance: none; -webkit-appearance: none; margin: 0; flex: none; position: relative; cursor: pointer; width: 36px; height: 20px; border-radius: 999px; background: rgba(255,255,255,.18); transition: background .15s ease; } .switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform .15s ease; } .switch:checked { background: rgb(var(--acc)); } .switch:checked::after { transform: translateX(16px); } .switch:focus-visible { outline: 2px solid #fff; outline-offset: 2px; } .status { margin-top: 10px; padding: 8px 10px; border-radius: 8px; background: rgba(255,82,82,.12); color: #ff9a9a; font-size: 12px; } .foot { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; color: #8b9097; font-size: 12px; } .foot a { color: inherit; text-decoration: none; } .foot a:hover { color: #fff; } kbd { font: 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace; padding: 2px 5px; border-radius: 4px; background: rgba(255,255,255,.08); box-shadow: inset 0 -1px 0 rgba(255,255,255,.08); color: #c9ccd1; } @media (prefers-reduced-motion: reduce) { .panel, .tile, .switch, .switch::after, .addons { transition: none; } .panel[data-busy] .tile[aria-pressed="true"]::before { animation: none; } } `; let panelHost = null, panel = null, isOpen = false; function buildPanel() { panelHost = document.createElement("div"); panelHost.id = "pyro-theme-panel"; panelHost.dataset.pyroIgnore = ""; const root = panelHost.attachShadow({ mode: "open" }); const keys = HOTKEY.split("+").map(k => `${k}`).join("+"); root.innerHTML = `