/* Colors pulled live from this Emby server's own active theme (dark), via its CSS custom properties (--theme-primary-color-*, --background-*, etc.) - see docs/architecture.md for how these were read directly off the running server rather than guessed. */ :root { color-scheme: dark; --bg: #151216; /* hsl(285, 10%, 7.84%) - Emby's dark theme background */ --bg-header: #221d24; /* same hue, +5% lightness - Emby's header/footer background */ --panel: #262626; /* Emby's card/button/input background (achromatic, ~15-17% lightness) */ --text: #ffffff; --muted: rgba(255, 255, 255, .6); --accent: #52b54b; /* Emby's theme-primary-color (hsl(116, 41.7%, 50.2%)) */ --accent-bright: #6ccf65; /* Emby's --theme-accent-text-color-darkbg */ --danger: #ff6b6b; } * { box-sizing: border-box; } html, body { margin: 0; height: 100%; } body { background: var(--bg); color: var(--text); font-family: system-ui, -apple-system, "Segoe UI", sans-serif; } #app { min-height: 100%; } /* Login is a small centered form - everything else lives inside the app shell (sidebar + .mainContent), see below. */ .wrap { max-width: 420px; margin: 0 auto; padding: 32px 20px 80px; } h1 { font-size: 22px; margin: 0; } h2 { font-size: 16px; margin: 0 0 12px; } .panel { background: var(--bg-header); border-radius: 12px; padding: 20px; } .hint { color: var(--muted); font-size: 13px; margin: 0 0 16px; } .error { color: var(--danger); font-size: 13px; min-height: 18px; } label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 12px; } input { display: block; width: 100%; margin-top: 4px; padding: 10px; border-radius: 8px; border: 1px solid #3a3a3a; background: var(--panel); color: var(--text); font-size: 14px; } input:focus { outline: none; border-color: var(--accent); } button { padding: 12px 16px; border: none; border-radius: 8px; background: var(--accent); color: white; font-size: 14px; font-weight: 600; cursor: pointer; } button:hover { background: var(--accent-bright); } button[disabled] { opacity: .5; cursor: default; } button.linklike { background: none; color: var(--muted); font-size: 12px; font-weight: 400; text-decoration: underline; padding: 4px 0; } button.linklike:hover { background: none; color: var(--accent-bright); } .code { color: var(--accent-bright); letter-spacing: .1em; } /* ---- App shell: sidebar + routed main content ---- */ .appShell { display: flex; min-height: 100vh; } .sidebar { width: 220px; flex-shrink: 0; background: var(--bg-header); padding: 20px 12px; display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh; overflow-y: auto; } .sidebarBrand { font-size: 18px; font-weight: 700; padding: 0 12px 20px; } .sidebarNav { display: flex; flex-direction: column; gap: 2px; flex: 1; } .navSectionLabel { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .05em; padding: 16px 12px 6px; } .navItem { display: block; padding: 10px 12px; border-radius: 8px; color: var(--text); text-decoration: none; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .navItem:hover { background: rgba(255, 255, 255, .06); } .navItem.active { background: var(--accent); color: white; font-weight: 600; } .sidebarLogout { margin-top: 12px; padding: 8px 12px; } .mainContent { flex: 1; min-width: 0; padding: 24px clamp(16px, 3vw, 48px) 80px; } /* Sidebar becomes a horizontal scrollable topbar on narrow screens - same "scroll instead of wrap" treatment as .tabs below. */ @media (max-width: 720px) { .appShell { flex-direction: column; } .sidebar { width: 100%; height: auto; flex-direction: row; align-items: center; padding: 10px 12px; overflow-x: auto; overflow-y: hidden; } .sidebarBrand { padding: 0 12px 0 0; } .sidebarNav { flex-direction: row; flex: none; } .navSectionLabel { display: none; } .sidebarLogout { margin: 0 0 0 12px; } } .pageHeader { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 20px; } .joinInline { display: flex; gap: 8px; align-items: center; } .joinInline input { width: 140px; margin: 0; } .joinInline button { padding: 10px 14px; } .section { margin-bottom: 32px; } /* ---- Horizontal carousels (home page) ---- */ .carousel { display: flex; gap: 16px; overflow-x: auto; padding-bottom: 10px; } .carousel .tile { flex: 0 0 160px; } .carousel .partyCard { flex: 0 0 280px; } .partyCard { display: flex; align-items: center; gap: 12px; background: var(--bg-header); border-radius: 10px; padding: 10px; } .partyCard img { width: 48px; height: 72px; object-fit: cover; border-radius: 6px; background: var(--panel); flex-shrink: 0; } .partyCard img.noImage { visibility: hidden; } .partyCardInfo { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; } .partyCardInfo strong { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .partyCard button { flex-shrink: 0; padding: 8px 14px; font-size: 13px; } /* Tabs: one per Emby library (library.js) or per season (series.js). */ .tabs { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 8px; margin-bottom: 20px; border-bottom: 1px solid rgba(255,255,255,.1); } .tab { background: none; color: var(--muted); font-weight: 600; padding: 8px 4px; border-radius: 0; white-space: nowrap; border-bottom: 2px solid transparent; } .tab:hover { background: none; color: var(--text); } .tab.active { color: var(--accent-bright); border-bottom-color: var(--accent-bright); } /* ---- Tile grid (library.js) + tiles (shared with the carousels) ---- */ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 20px 16px; } .tile { position: relative; display: flex; flex-direction: column; gap: 6px; /* Without this, a long title (or an image's intrinsic size before it loads) can force this flex/grid item wider than its declared size - the classic "min-width: auto" flexbox/grid pitfall. Every tile must stay exactly the track/flex-basis size regardless of content. */ min-width: 0; } .tile img { width: 100%; aspect-ratio: 2 / 3; object-fit: cover; border-radius: 8px; background: var(--panel); display: block; } .tile img.noImage { visibility: hidden; } .tileLink { position: absolute; inset: 0 0 26px 0; z-index: 1; } .tileLabel { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* Hover-reveal overlay with the two actions, on devices that actually have hover (mouse/trackpad). Touch devices get an always-visible bar instead - see the (hover: none) block below, since :hover is unreachable there. */ .tileOverlay { position: absolute; inset: 0 0 26px 0; z-index: 2; display: flex; flex-direction: column; align-items: stretch; justify-content: center; gap: 8px; padding: 10px; background: rgba(0, 0, 0, .55); border-radius: 8px; opacity: 0; transition: opacity .15s ease; pointer-events: none; } .tile:hover .tileOverlay, .tile:focus-within .tileOverlay { opacity: 1; pointer-events: auto; } .tileBtn { padding: 8px 10px; font-size: 12px; text-align: center; text-decoration: none; } .tileBtnLink { background: var(--panel); color: var(--text); border-radius: 8px; font-weight: 600; } .tileBtnLink:hover { background: #333; } @media (hover: none) { .tileOverlay { opacity: 1; pointer-events: auto; inset: auto 0 26px 0; flex-direction: row; background: linear-gradient(to top, rgba(0,0,0,.75), transparent); padding: 6px; } .tileBtn { font-size: 11px; padding: 6px 4px; } } /* ---- Series page: seasons tabs + episode list ---- */ .episodeList { display: flex; flex-direction: column; gap: 12px; } .episodeRow { display: flex; gap: 14px; align-items: center; background: var(--bg-header); border-radius: 10px; padding: 10px; } .episodeRow img { width: 120px; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 6px; background: var(--panel); flex-shrink: 0; } .episodeRow img.noImage { visibility: hidden; } .episodeInfo { flex: 1; min-width: 0; } .episodeInfo strong { display: block; margin-bottom: 4px; } .episodeInfo p { margin: 0; color: var(--muted); font-size: 13px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } .episodeRow button { flex-shrink: 0; } @media (max-width: 600px) { .episodeRow { flex-wrap: wrap; } .episodeInfo { order: 3; flex-basis: 100%; } } /* ---- Player ---- */ .playerWrap { /* Fixed + inset so it fills the actual viewport regardless of .mainContent's own padding (which used to add to 100vh and push the whole page taller than the screen, causing an outer scrollbar). Also covers the sidebar while playing, like most video players' theater mode - "Quitter" is how you get back to it. */ position: fixed; inset: 0; z-index: 20; background: #000; } .playerWrap video { width: 100%; height: 100%; object-fit: contain; } .overlay { position: absolute; top: 0; left: 0; right: 0; display: flex; align-items: center; gap: 16px; padding: 12px 16px; background: linear-gradient(to bottom, rgba(0,0,0,.7), transparent); color: white; font-size: 13px; flex-wrap: wrap; } .overlay .itemTitle { font-weight: 600; } .overlay .members { color: var(--muted); flex: 1; } /* Settings: a small gear icon in the action bar, not a permanently-visible block of dropdowns - opens a compact popup on click. */ .settingsWrap { position: relative; } .iconBtn { background: none; padding: 4px 8px; font-size: 16px; line-height: 1; border-radius: 6px; } .iconBtn:hover { background: rgba(255, 255, 255, .12); } .settingsPanel { position: absolute; top: 100%; right: 0; margin-top: 6px; display: flex; flex-direction: column; gap: 10px; background: var(--bg-header); padding: 12px; border-radius: 10px; font-size: 12px; min-width: 180px; box-shadow: 0 8px 24px rgba(0, 0, 0, .4); z-index: 5; } .settingsPanel label { color: var(--muted); margin-bottom: 0; display: flex; flex-direction: column; gap: 4px; } .settingsPanel select { background: var(--panel); color: var(--text); border: 1px solid #3a3a3a; border-radius: 6px; padding: 4px 6px; font-size: 12px; } .loadingNotice { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: var(--muted); font-size: 14px; margin: 0; } .playerError { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 360px; text-align: center; background: rgba(0, 0, 0, .75); padding: 20px; border-radius: 10px; } .playerError p { color: var(--danger); font-size: 14px; margin: 0 0 10px; } .followerNotice { position: absolute; bottom: 24px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,.6); padding: 8px 16px; border-radius: 20px; font-size: 13px; color: var(--muted); } /* Presence overlay: one circle per party member, bottom-left so it never fights the top action bar (title/next/settings/leave, which already wraps on narrow screens) or the centered .followerNotice pill. See renderMembers() in player.js. */ .presenceOverlay { position: absolute; bottom: 24px; left: 16px; display: flex; gap: 8px; z-index: 5; } .avatar { width: 36px; height: 36px; flex-shrink: 0; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700; color: white; opacity: .75; /* "overlay en semi transparence" - background color comes from inline style, see colorForName() in player.js */ border: 2px solid rgba(255, 255, 255, .3); user-select: none; } /* Only the viewer's own avatar gets this, and only while not host - see renderMembers(). The host has direct controls, so their own circle is presence-only. */ .avatarClickable { cursor: pointer; transition: opacity .15s, transform .15s; } .avatarClickable:hover { opacity: 1; transform: scale(1.08); } /* Feedback right after clicking your own avatar - stays up as long as the server's own per-member throttle blocks a repeat request anyway (see PAUSE_REQUEST_COOLDOWN_MS in player.js / socketHandler.js). */ .avatarSent { opacity: 1; border-color: var(--accent); } /* Shown to EVERY member (including the host) on the matching person's circle when their "pauseRequested" comes in - see onPauseRequested(). */ .avatarPulse { animation: avatarPulse 1s ease-in-out 2; } @keyframes avatarPulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, .6); opacity: .75; } 50% { box-shadow: 0 0 0 8px rgba(255, 255, 255, 0); opacity: 1; } } .pauseRequestBanner { position: absolute; top: 64px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, .75); color: white; padding: 8px 16px; border-radius: 20px; font-size: 13px; z-index: 6; } .hide { display: none; }