/* Persistent bottom overlay for a music listen party - see MusicBar.ts's header comment for why this is a Shell-level component instead of a route (views/player/Player.css's .playerWrap is the video equivalent, and stays a full route takeover on purpose - a shared audio queue has no image to "theater mode" around, and the whole point here is staying able to browse/add tracks while it plays). Sits above .mainContent, at a lower z-index than .playerWrap (20) so a video party's takeover still correctly covers it - moot in practice since joining a video party calls leaveMusicParty() first (see Player.ts), but cheap to get right. The fixed-to-viewport positioning itself lives one level up, on Shell.css's .bottomStack wrapper - not here - so that on a narrow screen the mobile nav trigger right below it in that wrapper always sits flush above this bar's own actual rendered height (its content can wrap taller on a narrow screen), never a hardcoded guess at it. */ .musicBar { position: relative; /* containing block for .musicBarQueue's own position: absolute - see its comment */ background: var(--bg-header); border-top: 1px solid rgba(255, 255, 255, .08); box-shadow: 0 -4px 16px rgba(0, 0, 0, .3); } /* A thin strip along the bar's own top edge, not inside .musicBarRow - the same "compact player" convention Spotify/YouTube Music use for their mini-players. Click/drag to seek (see MusicBar.ts) - the generous ::before hit-area below (not just the 3px visual track) matters most for drag on a touch screen, same trick views/player/Player.css's own .progressBar::before uses. `position: relative` is what gives that absolutely-positioned ::before something to size itself against. `grid-column: 1 / -1` is the fix, not decoration: #trackProgress is .musicBarPanel's first child, and without an explicit span a 3-column grid places it in column 1 alone - squeezing it down to the width of .musicBarPanelTrack's own column and wrapping .musicBarPanelSide onto a second row underneath it, instead of the intended full-width strip with everything else in one row below. Reported directly - never looked obviously wrong at a glance since .musicBarPanelTrack's own content (a small image + short text) left column 1 wide enough to mostly hide the squeeze. Spanning this row is what pushes every sibling after it into row 2, across all 3 columns, with no change needed to them. */ .musicBarProgressTrack { grid-column: 1 / -1; position: relative; height: 3px; background: rgba(255, 255, 255, .12); cursor: pointer; touch-action: none; /* this app's own pointer handlers drive the drag, not the browser's default touch scroll/zoom gestures */ } .musicBarProgressTrack::before { content: ''; position: absolute; top: -8px; bottom: -8px; left: 0; right: 0; } .musicBarProgressFill { height: 100%; width: 0%; background: var(--accent); pointer-events: none; } /* ---- Collapsed mobile preview (see MusicBar.html) - display:none by default since #musicBarPanel below is already always visible on desktop, nothing to collapse into a preview of there. Turned on under the 720px breakpoint further down. ---- */ .mobileMiniBar { display: none; align-items: center; gap: 10px; padding: 8px 12px; position: relative; cursor: pointer; } /* Passive - not draggable, unlike #trackProgress inside the panel below. Glanceable progress while collapsed is enough; dragging is one tap away once expanded, same as most real players' own mini-bar. */ .mobileMiniProgress { position: absolute; top: 0; left: 0; right: 0; height: 2px; background: rgba(255, 255, 255, .12); } .mobileMiniProgressFill { height: 100%; width: 0%; background: var(--accent-bright); } .mobileMiniArt { width: 36px; height: 36px; object-fit: cover; border-radius: 6px; background: var(--panel); flex-shrink: 0; } .mobileMiniArt.noImage { visibility: hidden; } .mobileMiniMeta { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; } .mobileMiniMeta strong { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* ---- The actual controls - always in place on desktop (this IS the bar, same as before); a full-screen "now playing" overlay on mobile, opened via #mobileMiniBar above and closed via #panelCloseBtn - see the 720px breakpoint further down for the fixed-overlay/transform part of this, unchanged here. ---- */ .musicBarPanel { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px; padding: 8px 16px; } .panelCloseBtn { display: none; } .musicBarPanelTrack { display: flex; align-items: center; gap: 12px; min-width: 0; } .musicBarPanelTransport { display: flex; align-items: center; justify-content: center; gap: 16px; } .musicBarPanelSide { display: flex; align-items: center; justify-content: flex-end; gap: 10px; } /* Smaller than the video player's presence row (Player.css/presence.css's 36px default) - this bar is a thin strip, not an overlay on top of a large video frame. */ .musicBarPresence .avatar { width: 26px; height: 26px; font-size: 10px; } .musicBarImage { width: 40px; height: 40px; object-fit: cover; border-radius: 6px; background: var(--panel); flex-shrink: 0; } .musicBarImage.noImage { visibility: hidden; } .musicBarInfo { display: flex; flex-direction: column; gap: 2px; min-width: 0; margin-right: auto; } .musicBarInfo strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; } .musicBarPosition { font-variant-numeric: tabular-nums; white-space: nowrap; } /* The one primary transport action, bigger and filled so it clearly outranks the plain small .iconBtn (skip-vote/queue) either side of it - same "obviously the main button" treatment views/player/Player.ts's own controls already get, instead of blending into a row of equally-weighted buttons. Shared between #playPauseBtn (inside .musicBarPanel) and #miniPlayPauseBtn (the collapsed mobile preview) - same look, same size, both wired to the exact same toggle in MusicBar.ts. */ /* .iconBtn.musicBarPlayBig, not just .musicBarPlayBig: both classes are always present together on these two buttons (see MusicBar.html), so this compound selector's higher specificity is what reliably beats .iconBtn's own background/border-radius regardless of which of the two files esbuild happens to bundle later - two plain single-class selectors at equal specificity would otherwise depend on that bundle order, which isn't something to build a visual guarantee on. */ .iconBtn.musicBarPlayBig { width: 34px; height: 34px; border-radius: 50%; background: var(--text); color: var(--bg); display: flex; align-items: center; justify-content: center; padding: 0; flex-shrink: 0; } .iconBtn.musicBarPlayBig:hover { background: var(--accent-bright); color: #fff; } .musicBarPlayBig svg { width: 14px; height: 14px; display: block; } /* #voteSkipBtn's own vote tally (see MusicBar.ts's updateVoteSkipLabel) - a badge instead of the old "Passer (vote) - X/Y" text label, now that the button itself is icon-only like every other transport control here. Hidden (see MusicBar.html) until there's an actual tally to show - matches every other conditionally-shown badge/pill elsewhere in this app rather than showing "0/1" before anyone's voted. */ #voteSkipBtn { position: relative; } .voteBadge { position: absolute; top: -4px; right: -6px; background: var(--accent); color: #fff; font-size: 9px; font-weight: 700; border-radius: 99px; padding: 1px 4px; min-width: 14px; text-align: center; line-height: 1.4; } /* #castBtn while actively casting - same "clearly-on" treatment as views/player/Player.css's own .iconBtn[aria-pressed="true"] (ID-scoped here rather than a shared .iconBtn rule since #playPauseBtn's own "iconBtn" class in this bar has no matching style at all - see MusicBar.html - and never sets aria-pressed, so this couldn't touch it either way, but there's no shared iconBtn.css to hang a rule off yet). */ #castBtn[aria-pressed="true"] { color: #ff9800; background: rgba(255, 152, 0, .18); } /* Pops up ABOVE the bar (same pattern as Player.css's .settingsPanel above its gear button - position: absolute against .musicBar's own position: relative, bottom: 100% instead of that one's top: 100%/ right: 0, since this floats up from a bottom bar rather than down from a top-corner icon) - collapsed by default, see MusicBar.ts's toggleQueueBtn handler. `position: absolute` (not left in normal flow) is what stops opening this from silently growing .bottomStack's own fixed-to-viewport height and shoving the actual controls upward instead of floating over them - reported directly as the desktop bar looking broken the moment the queue was opened. Spans the full bar width (no max-width) rather than a fixed popup box - a party's queue can run long, and a narrow column wastes the width a desktop viewport already has going spare. display:flex/flex-direction:column here (rather than on .pageHeader/#queueList individually) is what lets #queueList be the only child that scrolls below - see #queueList's own comment. */ .musicBarQueue { position: absolute; left: 0; right: 0; bottom: 100%; max-height: 50vh; margin: 0 16px; padding: 14px 14px 0; background: var(--bg-header); border: 1px solid rgba(255, 255, 255, .08); border-bottom: none; border-radius: 10px 10px 0 0; display: flex; flex-direction: column; } .musicBarQueue .pageHeader { flex-shrink: 0; } /* Always reachable regardless of z-index/positioning below (see MusicBar.ts's own click-outside handler for the other way to close this on desktop) - reported directly that, in mobile's full-screen layout, the *only* previous way to close this (tap the same #toggleQueueBtn again) sat visually underneath the now-open queue panel itself once it covered the screen, making it unreachable. */ .queueCloseBtn { background: none; color: var(--muted); font-size: 12px; font-weight: 400; padding: 2px; flex-shrink: 0; } .queueCloseBtn:hover { background: none; color: var(--text); } /* The only scrolling region inside .musicBarQueue - "File d'attente" above it stays put instead of scrolling away with the tracks. */ #queueList { overflow-y: auto; } @media (max-width: 720px) { .mobileMiniBar { display: flex; } /* Same fixed-overlay/slide-up-from-bottom pattern as Shell.css's .mobileNavSheet - see docs/architecture.md's "Mobile navigation". display stays flex unconditionally (not toggled) so the transform transition below actually animates instead of jumping. */ .musicBarPanel { position: fixed; inset: 0; z-index: 25; display: flex; flex-direction: column; padding: 20px 20px calc(20px + env(safe-area-inset-bottom, 0px)); background: var(--bg); transform: translateY(100%); transition: transform .25s ease; overflow-y: auto; } .musicBarPanel.open { transform: translateY(0); } .panelCloseBtn { display: flex; color: var(--muted); margin-bottom: 24px; } .musicBarPanelTrack { flex-direction: column; text-align: center; margin-bottom: 24px; } .musicBarPanelTrack .musicBarImage { width: min(220px, 60vw); height: min(220px, 60vw); border-radius: 12px; } .musicBarInfo { margin-right: 0; align-items: center; } .musicBarInfo strong { font-size: 19px; white-space: normal; } .musicBarPosition { margin-top: 6px; } .musicBarPanelTransport { gap: 28px; margin-bottom: 28px; } .musicBarPanelTransport .musicBarPlayBig { width: 56px; height: 56px; } .musicBarPanelTransport .musicBarPlayBig svg { width: 22px; height: 22px; } .musicBarPanelSide { margin-top: auto; justify-content: center; gap: 20px; } /* A full-screen overlay of its own here, not desktop's floating popup (there's no "above the bar" to float over once .musicBarPanel is itself a full-screen overlay) - z-index: 26 (above .musicBarPanel's 25) is still what puts it on top, but `position: relative` used to leave it in .musicBar's own normal flow, sized by `max-height: 50vh` inherited from the base rule above and positioned whichever the surrounding layout happened to allow - overlapping the very #toggleQueueBtn used to open it, with no other way to close a `position: relative` sibling that isn't just a small popup here. `inset: 0` + `max-height: none` makes it genuinely full-screen like .musicBarPanel itself, so its own #queueCloseBtn is always reachable regardless of queue length. */ .musicBarQueue { position: fixed; inset: 0; z-index: 26; max-height: none; margin: 0; border-radius: 0; padding: 20px 20px calc(20px + env(safe-area-inset-bottom, 0px)); } } /* One queue entry - a plain flex row (image, then a shrinking/truncating info column, then the duration), NOT views/album's shared .trackRow grid (styles/trackList.css) - that one's 4 fixed columns assume Album.ts's own index/name/duration/button shape, which crushed this list's track names into its 28px index column when reused here. See MusicBar.ts's own comment. */ .queueRow { display: flex; align-items: center; gap: 10px; background: var(--panel); border-radius: 8px; padding: 8px 10px; } .queueRowArt { width: 38px; height: 38px; object-fit: cover; border-radius: 6px; background: var(--bg-header); flex-shrink: 0; } .queueRowArt.noImage { visibility: hidden; } .queueRowInfo { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; } .queueRowName { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* Artist/album/"Ajouté par X" condensed onto one secondary line (see MusicBar.ts) rather than each getting their own column - there's no room for 3+ columns once a name needs to read in full, unlike Album.ts's simpler index/name/duration/button row. */ .queueRowMeta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }