.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; } /* Two stacked rows, both part of the same top gradient overlay: the action bar (title/members/next/settings/leave) and, below it, the presence avatars - see .presenceOverlay in styles/presence.css. Stacking them in normal flow inside one column means the avatar row always ends up right after the action bar regardless of how tall that bar is (it wraps to two lines on narrow screens), instead of guessing a fixed pixel offset for a second absolutely-positioned block. */ .topOverlay { position: absolute; top: 0; left: 0; right: 0; display: flex; flex-direction: column; gap: 10px; padding: 12px 16px; background: linear-gradient(to bottom, rgba(0,0,0,.7), transparent); z-index: 5; } .actionBar { display: flex; align-items: center; gap: 16px; color: white; font-size: 13px; flex-wrap: wrap; } .actionBar .itemTitle { font-weight: 600; } .actionBar .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); } /* #castBtn while actively casting - see Player.ts's toggleCast. Same "clearly-on" treatment as .avatarActionRequested's orange, not just a background tint, so it reads at a glance whether the TV or this screen is the one actually playing right now. */ .iconBtn[aria-pressed="true"] { color: #ff9800; background: rgba(255, 152, 0, .18); } .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; } .settingsPanel select:disabled { opacity: .5; } /* .hint's own bottom margin (base.css) would stack with the panel's own flex `gap` below it - zeroed here so it's spaced exactly like every other row in the column, not a wider gap after this specific one. */ .burnInHint { margin: 0; max-width: 220px; } .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; } /* One column, bottom-anchored, gap between whichever children are actually visible - same "stack in normal flow instead of guessing a fixed pixel offset per absolutely-positioned block" fix .topOverlay's own actionBar+presenceOverlay grouping already needed (see its own comment, and docs/architecture.md for the exact bug this avoided: a

's default margin silently throwing off a hand-picked bottom offset on the element above it - not repeatable once nothing is positioned independently any more). Centered on purpose - this is exactly where a native subtitle renders too, but both children here are passive text (a transient banner, a static notice), not something a viewer needs to click through subtitle text to reach - unlike .requestToolbar below, which used to live in this same column and was moved out for exactly that reason. */ .bottomOverlay { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; align-items: center; gap: 10px; z-index: 6; max-width: calc(100% - 32px); } .requestBanner { background: rgba(0, 0, 0, .75); color: white; padding: 8px 16px; border-radius: 20px; font-size: 13px; } .followerNotice { margin: 0; background: rgba(0,0,0,.6); padding: 8px 16px; border-radius: 20px; font-size: 13px; color: var(--muted); } /* The non-host's way to ask the host for something - see Player.ts's requestPauseBtn/requestResumeBtn. Pill buttons matching the banner/notice's own dark-overlay look above, not the plain-text .linklike used in the (opaque) top action bar - this sits directly on top of the video image, same as .bottomOverlay. Its own bottom-RIGHT corner, independently of .bottomOverlay - deliberately not centered-bottom any more (see .bottomOverlay's own comment): that's also where a native subtitle renders, and these are real buttons someone needs to read and click, not passive text a subtitle could just as easily sit behind. Stacked as a column (not a row, unlike the old centered layout) so two pill buttons next to each other never has to fight a narrow phone screen for width - each just takes however much it needs, right-aligned under the other. */ .requestToolbar { position: absolute; right: 16px; bottom: 20px; z-index: 6; display: flex; flex-direction: column; align-items: flex-end; gap: 8px; max-width: calc(50% - 16px); } .requestBtn { background: rgba(0, 0, 0, .6); color: white; padding: 8px 14px; border-radius: 20px; font-size: 12.5px; white-space: nowrap; /* keeps the pill shape even in .requestToolbar's own narrow corner on a phone-width screen */ } .requestBtn:hover { background: rgba(0, 0, 0, .8); } /* Shown to EVERY member (including the host and the requester themselves) on the matching person's circle while their pause/resume request is active - see onPauseRequested()/onResumeRequested() in Player.ts. Orange border + a small icon badge, not just a generic pulse, so it reads unambiguously even to someone glancing at the video rather than the banner text. Same treatment for either request - the banner text is what says which one. */ .avatarActionRequested { opacity: 1; border-color: #ff9800; } .avatarRequestBadge { position: absolute; bottom: -4px; right: -4px; width: 16px; height: 16px; border-radius: 50%; background: #ff9800; border: 1px solid rgba(0, 0, 0, .5); display: none; align-items: center; justify-content: center; } .avatarActionRequested .avatarRequestBadge { display: flex; }