diff --git a/media_server/static/css/styles.css b/media_server/static/css/styles.css
index f3ac199..f010c1b 100644
--- a/media_server/static/css/styles.css
+++ b/media_server/static/css/styles.css
@@ -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; }
+}
diff --git a/media_server/static/index.html b/media_server/static/index.html
index a4eb082..05601c4 100644
--- a/media_server/static/index.html
+++ b/media_server/static/index.html
@@ -100,6 +100,10 @@
+