feat(player): fullscreen "Listening Room" mode
Toggleable theater-scale player view that takes over the viewport and amplifies the existing Studio Reference aesthetic — same fonts, same copper/ink palette, just dialed up for immersive listening. Layout & typography: - Two-column centerfold: massive vinyl stage left (clamp(., 72vh, 720px)), editorial column right with Fraunces italic title at clamp(48px, 6.4vw, 112px), Geist Mono console-style metadata strip, oversized timecodes, full-width amplitude spectrum. - Mobile / portrait flips to vertical theater (vinyl top, masthead+ transport below) at <=900px or any portrait orientation. Ambient bloom: - Duplicate of #album-art rendered behind everything at blur(110px) saturate(1.6) opacity(0.42) — paints the room in the record's color. Slow 28s drift animation. Light-theme variant at lower opacity. - MutationObserver keeps bloom art in sync as tracks change. - Vignette + edge darkening + subtle paper-grain veil frame the stage. Interaction: - Header button (corner-arrows-out icon) toggles; pressing 'F' anywhere outside text inputs also toggles; ESC exits. - Native Fullscreen API requested as best-effort sugar on top of the CSS overlay (works on TV / tablet); CSS overlay alone covers the CSS-only fallback case (iOS Safari, embedded webviews). - fullscreenchange listener mirrors OS-level exit back into the overlay. - Auto-hide chrome + cursor after 2.5s idle, restored on mousemove. - Focus moves to play/pause on enter; restored to invoking element on exit. - Hides mini-player, tab bar, header, folio marks, and other tabs while active. Motion: - 320ms fade-in for the stage, 600ms vinyl rise, 1.4s bloom-in, staggered 80ms ladder for kicker -> title -> byline -> album -> meta -> spectrum -> transport. prefers-reduced-motion disables all. i18n: - player.fullscreen / player.fullscreen.exit / player.fullscreen.exit_short added to en.json and ru.json. Files: index.html (header button + fs-chrome strip + fs-bloom layer), styles.css (~360-line fullscreen block at end, scoped to body.is- fullscreen-player), player.js (toggle + init + idle/key/observer plumbing, ~170 lines), app.js (import + window export + init call). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -8667,3 +8667,461 @@ select option {
|
||||
.mini-album-art { width: 34px !important; height: 34px !important; }
|
||||
.mini-control-btn { width: 36px !important; height: 36px !important; }
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════
|
||||
FULLSCREEN PLAYER — "Listening Room"
|
||||
Activated via body.is-fullscreen-player. Uses position:fixed to
|
||||
take over the viewport (works without native Fullscreen API) and
|
||||
re-projects the existing player markup at theater scale: oversized
|
||||
Fraunces italic title, ambient album-art bloom painting the room,
|
||||
auto-hiding chrome, dramatic vinyl-stage focus.
|
||||
════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Fullscreen-only chrome (top strip with edition mark + exit button)
|
||||
and ambient bloom layer are inert outside fullscreen mode. */
|
||||
.fs-chrome,
|
||||
.fs-bloom {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* While in fullscreen: take over the viewport, hide everything outside
|
||||
the player container, and project the player onto a dark stage. */
|
||||
body.is-fullscreen-player {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Hide the world */
|
||||
body.is-fullscreen-player > .container > header,
|
||||
body.is-fullscreen-player > .container > .tab-bar,
|
||||
body.is-fullscreen-player > .container > .update-banner,
|
||||
body.is-fullscreen-player > .container > .connection-banner,
|
||||
body.is-fullscreen-player > .container > [data-tab-content]:not(#panel-player),
|
||||
body.is-fullscreen-player > .folio,
|
||||
body.is-fullscreen-player > #mini-player {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Promote the player container to fixed-overlay rank */
|
||||
body.is-fullscreen-player .player-container {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9000;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 30% 35%, rgba(224, 128, 56, 0.05) 0%, transparent 55%),
|
||||
radial-gradient(ellipse at center, var(--bg-paper) 0%, var(--bg-deep) 75%);
|
||||
display: grid;
|
||||
place-items: stretch;
|
||||
overflow: hidden;
|
||||
box-shadow: none !important;
|
||||
border: 0 !important;
|
||||
/* Soft entry fade — content layers stagger in via .now-playing animations below */
|
||||
animation: fs-fade-in 320ms var(--ease-out) both;
|
||||
}
|
||||
|
||||
@keyframes fs-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* ─── Ambient bloom: paint the room in the album's color ─────── */
|
||||
body.is-fullscreen-player .fs-bloom {
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: -8%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
animation: fs-bloom-in 1400ms var(--ease-out) 120ms forwards;
|
||||
}
|
||||
|
||||
@keyframes fs-bloom-in {
|
||||
from { opacity: 0; transform: scale(1.08); }
|
||||
to { opacity: 0.42; transform: scale(1); }
|
||||
}
|
||||
|
||||
:root[data-theme="light"] body.is-fullscreen-player .fs-bloom {
|
||||
animation-name: fs-bloom-in-light;
|
||||
}
|
||||
@keyframes fs-bloom-in-light {
|
||||
from { opacity: 0; transform: scale(1.08); }
|
||||
to { opacity: 0.22; transform: scale(1); }
|
||||
}
|
||||
|
||||
body.is-fullscreen-player .fs-bloom #fs-bloom-art {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
filter: blur(110px) saturate(1.6);
|
||||
transform: scale(1.18);
|
||||
animation: fs-bloom-drift 28s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes fs-bloom-drift {
|
||||
from { transform: scale(1.18) translate3d(-1.5%, -1%, 0); }
|
||||
to { transform: scale(1.22) translate3d(2%, 1.5%, 0); }
|
||||
}
|
||||
|
||||
/* Subtle paper-grain veil over the bloom — keeps it from looking flat. */
|
||||
body.is-fullscreen-player .player-container::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 80%, rgba(0, 0, 0, 0.35) 0%, transparent 40%),
|
||||
radial-gradient(circle at 80% 20%, rgba(0, 0, 0, 0.30) 0%, transparent 45%);
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
/* Vignette + top/bottom edge darkening — frames the listening room */
|
||||
body.is-fullscreen-player .player-container::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
background:
|
||||
linear-gradient(to bottom,
|
||||
rgba(0, 0, 0, 0.45) 0%,
|
||||
transparent 12%,
|
||||
transparent 88%,
|
||||
rgba(0, 0, 0, 0.55) 100%),
|
||||
radial-gradient(ellipse at center, transparent 50%, rgba(0, 0, 0, 0.55) 100%);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] body.is-fullscreen-player .player-container::after {
|
||||
background:
|
||||
linear-gradient(to bottom,
|
||||
rgba(0, 0, 0, 0.10) 0%,
|
||||
transparent 14%,
|
||||
transparent 86%,
|
||||
rgba(0, 0, 0, 0.15) 100%),
|
||||
radial-gradient(ellipse at center, transparent 55%, rgba(0, 0, 0, 0.12) 100%);
|
||||
}
|
||||
|
||||
/* ─── Floating chrome (edition mark + exit) — auto-hides on idle ─ */
|
||||
body.is-fullscreen-player .fs-chrome {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
z-index: 50;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 22px 36px;
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
transition: opacity 320ms var(--ease), transform 320ms var(--ease);
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
body.is-fullscreen-player.fs-chrome-hidden .fs-chrome {
|
||||
opacity: 0;
|
||||
transform: translateY(-12px);
|
||||
pointer-events: none;
|
||||
}
|
||||
body.is-fullscreen-player.fs-chrome-hidden { cursor: none; }
|
||||
|
||||
.fs-chrome-mark {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.28em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-mute);
|
||||
}
|
||||
.fs-chrome-edition {
|
||||
color: var(--copper);
|
||||
font-weight: 500;
|
||||
}
|
||||
.fs-chrome-sep { color: var(--ink-faint); }
|
||||
|
||||
.fs-chrome-exit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 14px 8px 12px;
|
||||
background: rgba(14, 13, 11, 0.55);
|
||||
border: 1px solid var(--rule-strong);
|
||||
color: var(--ink-soft);
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
border-radius: 0;
|
||||
backdrop-filter: blur(8px) saturate(1.2);
|
||||
transition: color 160ms var(--ease), border-color 160ms var(--ease), background 160ms var(--ease);
|
||||
}
|
||||
.fs-chrome-exit:hover {
|
||||
color: var(--ink);
|
||||
border-color: var(--copper);
|
||||
background: rgba(14, 13, 11, 0.75);
|
||||
}
|
||||
.fs-chrome-exit svg { color: var(--copper); }
|
||||
.fs-chrome-kbd {
|
||||
font-family: var(--mono);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.16em;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid var(--rule-strong);
|
||||
color: var(--ink-mute);
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
:root[data-theme="light"] .fs-chrome-exit {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* ─── Stage: massive vinyl + masthead at theater scale ─────────── */
|
||||
body.is-fullscreen-player .now-playing,
|
||||
body.is-fullscreen-player .player-layout {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
|
||||
gap: clamp(40px, 6vw, 96px);
|
||||
margin: 0 !important;
|
||||
padding: clamp(80px, 10vh, 140px) clamp(40px, 6vw, 96px) clamp(40px, 6vh, 80px);
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
/* Vinyl stage — pin to comfortable theater size */
|
||||
body.is-fullscreen-player .album-art-container.vinyl-stage {
|
||||
width: min(72vh, 100%);
|
||||
max-width: 720px;
|
||||
margin-inline: auto;
|
||||
aspect-ratio: 1;
|
||||
background:
|
||||
radial-gradient(ellipse at center,
|
||||
rgba(26, 22, 17, 0.85) 0%,
|
||||
transparent 70%);
|
||||
filter: drop-shadow(0 30px 80px rgba(0, 0, 0, 0.55));
|
||||
animation: fs-stage-rise 600ms var(--ease-out) both;
|
||||
}
|
||||
@keyframes fs-stage-rise {
|
||||
from { opacity: 0; transform: scale(0.94) translateY(8px); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
|
||||
:root[data-theme="light"] body.is-fullscreen-player .album-art-container.vinyl-stage {
|
||||
background:
|
||||
radial-gradient(ellipse at center,
|
||||
rgba(255, 255, 255, 0.65) 0%,
|
||||
transparent 75%);
|
||||
filter: drop-shadow(0 30px 60px rgba(0, 0, 0, 0.18));
|
||||
}
|
||||
|
||||
/* ─── Masthead: editorial centerfold ───────────────────────────── */
|
||||
body.is-fullscreen-player .track-masthead {
|
||||
position: relative;
|
||||
max-width: 720px;
|
||||
padding-right: clamp(0px, 2vw, 24px);
|
||||
/* Stagger ladder: each child slides up with a small delay */
|
||||
--fs-stagger: 80ms;
|
||||
}
|
||||
|
||||
body.is-fullscreen-player .track-masthead > * {
|
||||
animation: fs-rise 700ms var(--ease-out) both;
|
||||
}
|
||||
body.is-fullscreen-player .track-masthead > .kicker { animation-delay: calc(var(--fs-stagger) * 1); }
|
||||
body.is-fullscreen-player .track-masthead > .track-title { animation-delay: calc(var(--fs-stagger) * 2); }
|
||||
body.is-fullscreen-player .track-masthead > .track-byline{ animation-delay: calc(var(--fs-stagger) * 3); }
|
||||
body.is-fullscreen-player .track-masthead > .track-album { animation-delay: calc(var(--fs-stagger) * 4); }
|
||||
body.is-fullscreen-player .track-masthead > .meta-grid { animation-delay: calc(var(--fs-stagger) * 5); }
|
||||
body.is-fullscreen-player .track-masthead > .spectrum { animation-delay: calc(var(--fs-stagger) * 6); }
|
||||
body.is-fullscreen-player .track-masthead > .transport { animation-delay: calc(var(--fs-stagger) * 7); }
|
||||
|
||||
@keyframes fs-rise {
|
||||
from { opacity: 0; transform: translateY(14px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Kicker — editorial small-caps with hairline rules */
|
||||
body.is-fullscreen-player .now-playing .kicker {
|
||||
margin-bottom: clamp(18px, 2.4vh, 32px);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.36em;
|
||||
}
|
||||
|
||||
/* The headline — Fraunces italic, oversized, centerfold scale */
|
||||
body.is-fullscreen-player .now-playing .track-title {
|
||||
font-family: var(--serif);
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-size: clamp(48px, 6.4vw, 112px);
|
||||
line-height: 0.95;
|
||||
letter-spacing: -0.015em;
|
||||
margin: 0 0 clamp(14px, 1.8vh, 22px);
|
||||
color: var(--ink);
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
body.is-fullscreen-player .now-playing .track-title em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Artist + album — refined editorial tier */
|
||||
body.is-fullscreen-player .now-playing .track-byline {
|
||||
font-family: var(--sans);
|
||||
font-weight: 500;
|
||||
font-size: clamp(16px, 1.4vw, 22px);
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-soft);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
body.is-fullscreen-player .now-playing .track-album {
|
||||
font-family: var(--serif);
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-size: clamp(15px, 1.2vw, 20px);
|
||||
color: var(--ink-mute);
|
||||
margin-bottom: clamp(20px, 2.4vh, 36px);
|
||||
}
|
||||
|
||||
/* Meta grid — promote to a console-readout strip */
|
||||
body.is-fullscreen-player .now-playing .meta-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: clamp(20px, 2vw, 36px);
|
||||
padding-block: clamp(14px, 1.8vh, 22px);
|
||||
border-top: 1px solid var(--rule);
|
||||
border-bottom: 1px solid var(--rule);
|
||||
margin-bottom: clamp(20px, 2.4vh, 32px);
|
||||
}
|
||||
body.is-fullscreen-player .now-playing .meta-cell .label {
|
||||
font-family: var(--mono);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.32em;
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
body.is-fullscreen-player .now-playing .meta-cell .value {
|
||||
font-family: var(--mono);
|
||||
font-size: clamp(13px, 1vw, 16px);
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* Spectrum — taller, full-width amplitude bars */
|
||||
body.is-fullscreen-player .now-playing .spectrum {
|
||||
height: clamp(60px, 9vh, 120px);
|
||||
margin-bottom: clamp(22px, 2.6vh, 36px);
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
/* Transport — give it room to breathe */
|
||||
body.is-fullscreen-player .now-playing .transport {
|
||||
gap: clamp(20px, 2.6vh, 32px);
|
||||
}
|
||||
|
||||
/* Progress row: huge mono timecodes */
|
||||
body.is-fullscreen-player .now-playing .progress-row {
|
||||
gap: clamp(16px, 1.8vw, 28px);
|
||||
}
|
||||
body.is-fullscreen-player .now-playing .progress-row .timecode,
|
||||
body.is-fullscreen-player .now-playing .timecode {
|
||||
font-family: var(--mono);
|
||||
font-size: clamp(20px, 2.2vw, 32px);
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.04em;
|
||||
min-width: 6ch;
|
||||
}
|
||||
body.is-fullscreen-player .now-playing .progress-track,
|
||||
body.is-fullscreen-player .now-playing .progress-bar {
|
||||
height: 4px;
|
||||
}
|
||||
body.is-fullscreen-player .now-playing .progress-fill {
|
||||
box-shadow: 0 0 18px var(--copper-glow);
|
||||
}
|
||||
|
||||
/* Controls — large, generous spacing */
|
||||
body.is-fullscreen-player .now-playing .controls {
|
||||
gap: clamp(14px, 1.6vw, 22px);
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
body.is-fullscreen-player .now-playing .btn-trans {
|
||||
width: clamp(48px, 4.5vw, 64px) !important;
|
||||
height: clamp(48px, 4.5vw, 64px) !important;
|
||||
}
|
||||
body.is-fullscreen-player .now-playing .btn-trans.primary {
|
||||
width: clamp(64px, 6vw, 88px) !important;
|
||||
height: clamp(64px, 6vw, 88px) !important;
|
||||
}
|
||||
body.is-fullscreen-player .now-playing .btn-trans svg {
|
||||
width: clamp(20px, 1.8vw, 26px);
|
||||
height: clamp(20px, 1.8vw, 26px);
|
||||
}
|
||||
body.is-fullscreen-player .now-playing .btn-trans.primary svg {
|
||||
width: clamp(26px, 2.4vw, 34px);
|
||||
height: clamp(26px, 2.4vw, 34px);
|
||||
}
|
||||
|
||||
/* VU cluster — slightly enlarged, still right-anchored */
|
||||
body.is-fullscreen-player .now-playing .vu-cluster {
|
||||
margin-left: auto;
|
||||
gap: clamp(10px, 1vw, 16px);
|
||||
}
|
||||
body.is-fullscreen-player .now-playing .vu-meter {
|
||||
width: clamp(70px, 6.5vw, 96px);
|
||||
height: clamp(36px, 3.6vh, 50px);
|
||||
}
|
||||
|
||||
/* Header button: active state when fullscreen is on */
|
||||
.header-btn#fullscreenToggle.active {
|
||||
background: var(--bg-card);
|
||||
color: var(--copper);
|
||||
border-color: var(--copper);
|
||||
}
|
||||
|
||||
/* ─── Mobile / portrait variant: vertical theater ──────────────── */
|
||||
@media (max-width: 900px), (orientation: portrait) {
|
||||
body.is-fullscreen-player .now-playing,
|
||||
body.is-fullscreen-player .player-layout {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: minmax(0, 1fr) minmax(0, auto);
|
||||
gap: clamp(20px, 4vh, 40px);
|
||||
padding: clamp(60px, 8vh, 96px) clamp(20px, 5vw, 40px) clamp(24px, 4vh, 48px);
|
||||
align-content: start;
|
||||
}
|
||||
body.is-fullscreen-player .album-art-container.vinyl-stage {
|
||||
width: min(70vw, 56vh);
|
||||
max-width: 480px;
|
||||
}
|
||||
body.is-fullscreen-player .now-playing .track-title {
|
||||
font-size: clamp(36px, 8.5vw, 64px);
|
||||
}
|
||||
body.is-fullscreen-player .now-playing .controls {
|
||||
justify-content: center;
|
||||
}
|
||||
body.is-fullscreen-player .now-playing .vu-cluster {
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
body.is-fullscreen-player .fs-chrome {
|
||||
padding: 14px 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion: kill the entry animations and bloom drift */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body.is-fullscreen-player .player-container,
|
||||
body.is-fullscreen-player .fs-bloom,
|
||||
body.is-fullscreen-player .album-art-container.vinyl-stage,
|
||||
body.is-fullscreen-player .track-masthead > * {
|
||||
animation: none !important;
|
||||
}
|
||||
body.is-fullscreen-player .fs-bloom { opacity: 0.42; }
|
||||
body.is-fullscreen-player .fs-bloom #fs-bloom-art { animation: none !important; }
|
||||
:root[data-theme="light"] body.is-fullscreen-player .fs-bloom { opacity: 0.22; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user