All checks were successful
Publier l'image Docker / publish (push) Successful in 7s
Two rounds of feedback on the previous commit, plus a related feature request: 1. Presence overlay moved from bottom-left to the top of the player. client/js/views/player.js: the top gradient bar is now .topOverlay, a column of two stacked rows - .actionBar (what used to be the whole bar: title/members-text/next/settings/leave) and #presenceOverlay directly below it. Stacking them in normal flow means the avatar row always lands right after the action bar regardless of how tall that bar renders (it wraps on narrow screens), instead of a second absolutely-positioned block at a guessed pixel offset. 2. "Someone requested a pause" is now a specific orange border (.avatarPauseRequested, #ff9800) plus a small pause-icon badge in the circle's corner (.avatarPauseBadge - a plain inline two-bar SVG, not a Unicode glyph, since pause/play characters default to emoji presentation on several platforms and look muddy at 10px) - replacing the previous generic box-shadow pulse. This also absorbed the separate "click feedback" ring that used to live only on the requester's own circle (.avatarSent): since the server already broadcasts "pauseRequested" back to the requester too, one state driven purely by that broadcast is simpler than two overlapping ones. Found while repositioning the banner above .followerNotice: the notice is a <p>, and for an absolutely positioned element "bottom" places the MARGIN edge, so its default ~1em user-agent margin was quietly rendering it 13px higher than its declared bottom:24px ever suggested. Gave it margin:0. 3. Series detail page (client/js/views/series.js): the page now opens with an actual detail header - poster, title + year, overview, "Voir sur Emby" - above the season tabs/episode list, instead of a bare <h1>. The season/episode browsing and per-episode "Lancer une watch party" already worked end to end (library grid -> Series tile -> this route); this pass only made the page look like a detail page. Reused toItemDto's existing fields (GET /api/catalog/item/:id), no backend changes needed. Widened .linklike's CSS selector from `button.linklike` to `.linklike` so the new "Voir sur Emby" <a> picks it up too - app.css has no global `a` reset, so an unclassed anchor would otherwise render with the browser's default blue/underline. Verified with mocked Socket.IO events / API responses against the real running local server (see session history): presence row renders directly under the action bar; a pauseRequested event gives the right circle rgb(255, 152, 0) as its computed border color and display:flex on its badge while leaving every other circle untouched (checked via computed styles, not just class names); rapid repeat clicks on your own circle still collapse to exactly one requestPause emission; banner/followerNotice gap measured 15px after the margin fix, versus 2px before it; the series page's header renders correctly, season-tab switching swaps the episode list, a tvshows library's Series tiles link to /series/:id via both the poster and "Voir les épisodes", and starting an episode's party correctly creates it and navigates to the player. docs/architecture.md has the full writeup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
680 lines
15 KiB
CSS
680 lines
15 KiB
CSS
/* 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; }
|
|
|
|
/* Class-only (not tag-qualified) so an <a class="linklike"> - e.g. the
|
|
"Voir sur Emby" link on the series detail page - gets the same treatment
|
|
as the <button> ones without needing its own duplicate rule; there's no
|
|
global `a` reset in this file, so an unclassed anchor would otherwise
|
|
fall back to the browser's default blue/underline against this dark
|
|
theme. */
|
|
.linklike {
|
|
background: none;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
font-weight: 400;
|
|
text-decoration: underline;
|
|
padding: 4px 0;
|
|
}
|
|
|
|
.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: detail header + seasons tabs + episode list ---- */
|
|
|
|
.seriesHeader {
|
|
display: flex;
|
|
gap: 20px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.seriesPoster {
|
|
width: 160px;
|
|
aspect-ratio: 2 / 3;
|
|
object-fit: cover;
|
|
border-radius: 8px;
|
|
background: var(--panel);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.seriesPoster.noImage { visibility: hidden; }
|
|
|
|
.seriesInfo {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.seriesYear { color: var(--muted); font-weight: 400; }
|
|
|
|
.seriesOverview {
|
|
color: var(--muted);
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
margin: 0;
|
|
max-width: 640px;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.seriesHeader { flex-direction: column; }
|
|
.seriesPoster { width: 120px; }
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* 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. 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); }
|
|
|
|
.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 {
|
|
/* margin:0 overrides the <p>'s default UA margin - for an absolutely
|
|
positioned box, "bottom" places the MARGIN edge, so the default ~1em
|
|
bottom margin was quietly pushing the pill 13px higher than its
|
|
declared bottom:24px ever suggested. Harmless on its own, but it made
|
|
.pauseRequestBanner's bottom offset above it read as barely any gap
|
|
at all once that one was measured against this element's real position. */
|
|
position: absolute;
|
|
bottom: 24px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
margin: 0;
|
|
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. Lives inside .topOverlay,
|
|
right below the action bar - see renderMembers() in player.js. */
|
|
.presenceOverlay {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.avatar {
|
|
position: relative; /* anchors .avatarPauseBadge */
|
|
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);
|
|
}
|
|
|
|
/* Shown to EVERY member (including the host and the requester themselves)
|
|
on the matching person's circle while their "pauseRequested" is active -
|
|
see onPauseRequested() in player.js. Orange border + a small pause-icon
|
|
badge, not just a generic pulse, so it reads unambiguously even to
|
|
someone glancing at the video rather than the banner text. */
|
|
.avatarPauseRequested {
|
|
opacity: 1;
|
|
border-color: #ff9800;
|
|
}
|
|
|
|
.avatarPauseBadge {
|
|
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;
|
|
}
|
|
|
|
.avatarPauseRequested .avatarPauseBadge { display: flex; }
|
|
|
|
.pauseRequestBanner {
|
|
/* Bottom-anchored, clear of the presence row now living up in
|
|
.topOverlay - stacked above .followerNotice (bottom: 24px) rather than
|
|
on top of it, since a non-host sees both at once. */
|
|
position: absolute;
|
|
bottom: 72px;
|
|
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; }
|