Add the Glass Stage layout theme with overlay controls, 10 s skips, quick chips and stacked tiles

This commit is contained in:
Claude
2026-10-01 21:40:20 +00:00
parent 36fc5ff9e4
commit 19973c75c0
6 changed files with 309 additions and 14 deletions

View File

@@ -46,6 +46,16 @@ JSON shapes mirror the Tauri Rust bridge exactly — don't change one side alone
- `GET /api/p2p/available` + view `{type:'p2p'}` = the "📡 On other devices" list (sidebar, web only):
verified files with ≥1 sharing holder, online first, stale flagged. Rows play, export, or go to a playlist.
## Layout themes + Glass Stage
- Settings → Layout theme sets `html[data-layout]`; rules live in `styles.css` (older ones) and
`frontend/theme-glass.css` (**glass-stage**: frosted cards, coral glow). Glass Stage also changes layout:
overlay on the video (quality chip, Video / Audio-only, −10 / play / +10, shown when paused or after a tap),
deck row prev · −10 · play · +10 · next, volume pod + labelled mode pill, quick-setting chips (`#deckChips`,
refreshed every 1.5 s by the `glassStage` IIFE), four stacked icon tiles and secondary actions as a pill row,
floating mini player. Elements that only this theme shows (`.stg-overlay`, `.skip10`, `.deck-chips`, `.vol-pod`
extras, `.mode-name`) are in the shared markup and hidden by default.
- Now-playing tiles are `[svg.ti][span.tl]`; change state text with `setTile(btn, label)`, never `textContent`.
## Server media cache (`server/media-cache.js`)
- Every played (`/api/streams`, LOW priority, ≤ `MEDIA_AUTO_MAX_SECONDS`, default 3 h) or saved
(`/api/download`, HIGH, ≤ 3 h) video gets ONE copy: `$MEDIA_DIR/<id>.<gen>.mp4`

View File

@@ -364,7 +364,7 @@ const DEFAULT_SETTINGS = {
density: 'comfortable', // 'comfortable' | 'compact'
perfMode: false, // disable heavy visual effects for speed
reduceMotion: false, // disable animations
layout: 'classic', // 'classic'|'sanctuary-stage'|'liturgical'|'living-hymnal'|'broadcaster'|'contemplative'|'bento-hub'
layout: 'classic', // 'classic'|'glass-stage'|'sanctuary-stage'|'liturgical'|'living-hymnal'|'broadcaster'|'contemplative'|'bento-hub'
autoBackupEnabled: false,
autoBackupIntervalDays: 7,
serviceVideo: false, // show the video panel inside service mode (lyric videos)
@@ -1103,6 +1103,8 @@ function syncMuteIcon() {
const waves = lvl === 0 ? '<path d="M16 9l5 6M21 9l-5 6"/>' : lvl === 1 ? '<path d="M15.5 9.5a4 4 0 0 1 0 5"/>' : '<path d="M15.5 9.5a4 4 0 0 1 0 5M18.5 6.5a8 8 0 0 1 0 11"/>';
b.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9.5v5h3.5L12 18.5v-13L7.5 9.5z" class="spk"/>${waves}</svg>`;
b.classList.toggle('muted', lvl === 0);
const pct = $('volPct');
if (pct) pct.textContent = Math.round(v * 100) + '%';
b.title = lvl === 0 ? 'Unmute' : 'Mute';
b.setAttribute('aria-label', b.title);
}
@@ -2411,6 +2413,10 @@ function wirePlayerEvents() {
function updatePlayBtn() {
Remote.changed();
els.playBtn.innerHTML = Player.master.paused ? ICON_PLAY : ICON_PAUSE;
const sp = $('stgPlay');
if (sp) sp.innerHTML = Player.master.paused ? ICON_PLAY : ICON_PAUSE;
const stage = document.querySelector('.player-stage');
if (stage) stage.classList.toggle('is-paused', !!Player.master.paused);
$('miniPlayBtn').textContent = Player.master.paused ? '▶' : '⏸';
const spBtn = $('servicePlayBtn');
if (spBtn) spBtn.textContent = Player.master.paused ? '▶' : '⏸';
@@ -2656,6 +2662,11 @@ function openMoreActions() {
}
// Reflect cache state on the now-playing Save button.
// Tiles are [icon][label]; only the label text changes with state.
function setTile(btn, label) {
const tl = btn.querySelector('.tl');
if (tl) tl.textContent = label; else btn.textContent = label;
}
function updateNowPlayingActions() {
if (!current || !current.meta) return;
const id = current.meta.id;
@@ -2674,20 +2685,20 @@ function updateNowPlayingActions() {
const btn = els.saveBtn;
if (!btn) return;
if (removing.has(id)) {
btn.textContent = '⏳ Removing…';
setTile(btn, 'Removing…');
btn.classList.remove('done');
btn.disabled = true;
} else if (downloading.has(id)) {
btn.textContent = '⏳ Saving…';
setTile(btn, 'Saving…');
btn.classList.remove('done');
btn.disabled = true;
} else if (cachedIds.has(id)) {
btn.textContent = '✓ Saved';
setTile(btn, 'Saved');
btn.classList.add('done');
btn.disabled = false;
btn.title = 'Saved for offline — click to remove from cache';
} else {
btn.textContent = '⬇ Save';
setTile(btn, 'Save');
btn.classList.remove('done');
btn.disabled = false;
btn.title = 'Save this video for offline playback';
@@ -3889,6 +3900,48 @@ const Notes = (() => {
};
})();
// ---- Glass Stage layout: stage overlay, 10 s skips, quick-setting chips ----
(function glassStage() {
const skip = (d) => { try { Player.seek(Math.max(0, Player.master.currentTime + d)); } catch { /* not ready */ } };
const on = (id, fn) => { const el = document.getElementById(id); if (el) el.addEventListener('click', fn); };
on('back10Btn', () => skip(-10)); on('fwd10Btn', () => skip(10));
on('stgBack', () => skip(-10)); on('stgFwd', () => skip(10));
on('stgPlay', () => $('playBtn').click());
const setAudio = (v) => { const t = $('audioOnlyToggle'); if (t && t.checked !== v) { t.checked = v; t.dispatchEvent(new Event('change', { bubbles: true })); } };
on('stgVideo', () => setAudio(false)); on('stgAudio', () => setAudio(true));
const stage = document.querySelector('.player-stage');
let hideT = null;
if (stage) stage.addEventListener('click', (e) => {
if (e.target.closest('.stg-overlay button')) return;
stage.classList.add('stg-show');
clearTimeout(hideT);
hideT = setTimeout(() => stage.classList.remove('stg-show'), 3000);
});
function chips() {
if (document.documentElement.dataset.layout !== 'glass-stage') return;
const box = $('deckChips');
if (!box) return;
const after = (id) => { const t = String(($(id) || {}).title || ''); const m = t.match(/:\s*([^—(]+?)\s*(?:—|\(|$)/); return m ? m[1].trim() : ''; };
const list = [['Speed ' + (($('speedSelect') || {}).value || '1') + '×', true]];
const eq = after('eqBtn'); if (eq) list.push(['EQ: ' + eq, false]);
const tr = after('transitionBtn'); if (tr) list.push([tr === 'Normal' ? 'Gapless: off' : tr, false]);
const pip = $('pipBtn'); if (pip && !pip.classList.contains('hidden')) list.push(['PiP ready', false]);
const key = list.map((x) => x[0]).join('|');
if (box.dataset.key !== key) {
box.dataset.key = key;
box.innerHTML = list.map(([t, a]) => `<button type="button" class="dchip${a ? ' on' : ''}"></button>`).join('');
[...box.children].forEach((b, i) => { b.textContent = list[i][0]; b.onclick = openMoreControls; });
}
const q = $('qualitySelect'), sq = $('stgQuality');
if (sq && q && q.options.length) sq.textContent = q.options[q.selectedIndex] ? q.options[q.selectedIndex].text : 'Auto';
const ao = !!data.settings.audioOnly;
const v = $('stgVideo'), a = $('stgAudio');
if (v) v.classList.toggle('on', !ao); if (a) a.classList.toggle('on', ao);
}
setInterval(chips, 1500);
setTimeout(chips, 400);
})();
// ============================================================================
// Phone remote (server/remote.js relays both ends over /ws/remote)
// ============================================================================
@@ -6931,7 +6984,7 @@ function updateLoopRepeatButtons() {
const mb = $('modeBtn');
if (mb) {
const i = playModeIndex();
mb.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${MODE_ICONS[i]}</svg>`;
mb.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${MODE_ICONS[i]}</svg><span class="mode-name">${PLAY_MODES[i].name}</span>`;
mb.classList.toggle('active', i > 0);
mb.dataset.badge = i === 3 ? '∞' : '';
mb.title = `${PLAY_MODES[i].name} — ${PLAY_MODES[i].sub}. Tap to change.`;
@@ -7939,7 +7992,7 @@ async function renderSettings() {
</label>
<label class="set-row">
<span>Layout theme</span>
${sel('setLayout', data.settings.layout || 'classic', [['classic', 'Classic (default)'], ['sanctuary-stage', 'Sanctuary Stage'], ['liturgical', 'Liturgical Chronicle'], ['living-hymnal', 'Living Hymnal'], ['broadcaster', 'Sunday Broadcaster'], ['contemplative', 'Contemplative'], ['bento-hub', 'Bento Hub']])}
${sel('setLayout', data.settings.layout || 'classic', [['classic', 'Classic (default)'], ['glass-stage', 'Glass Stage'], ['sanctuary-stage', 'Sanctuary Stage'], ['liturgical', 'Liturgical Chronicle'], ['living-hymnal', 'Living Hymnal'], ['broadcaster', 'Sunday Broadcaster'], ['contemplative', 'Contemplative'], ['bento-hub', 'Bento Hub']])}
</label>
<label class="set-row">
<span>Font size</span>

View File

@@ -23,6 +23,7 @@
<link rel="stylesheet" href="styles.css" />
<link rel="stylesheet" href="service-ui.css" />
<link rel="stylesheet" href="refresh.css" />
<link rel="stylesheet" href="theme-glass.css" />
</head>
<body>
<div class="app">
@@ -150,6 +151,20 @@
<div id="bufferSpinner" class="spinner hidden"></div>
<!-- Touch gesture feedback (Gestures in app.js) -->
<div id="gestureHud" class="gesture-hud hidden"></div>
<div id="stgOverlay" class="stg-overlay" aria-hidden="true">
<div class="stg-top">
<span class="stg-chip"><i class="stg-dot"></i><b id="stgQuality">Auto</b></span>
<div class="stg-seg" role="group" aria-label="Video or audio only">
<button id="stgVideo" type="button">Video</button>
<button id="stgAudio" type="button">Audio-only</button>
</div>
</div>
<div class="stg-mid">
<button id="stgBack" class="stg-skip" type="button" aria-label="Back 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12a8 8 0 1 0 3-6.2M4 4v4.5h4.5"/><text x="12" y="15.5" text-anchor="middle" font-size="7.5" font-weight="700" fill="currentColor" stroke="none">10</text></svg></button>
<button id="stgPlay" class="stg-play" type="button" aria-label="Play or pause"><svg viewBox="0 0 24 24" aria-hidden="true" class="fill"><path d="M9 5.5v13l10-6.5z"/></svg></button>
<button id="stgFwd" class="stg-skip" type="button" aria-label="Forward 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 12a8 8 0 1 1-3-6.2M20 4v4.5h-4.5"/><text x="12" y="15.5" text-anchor="middle" font-size="7.5" font-weight="700" fill="currentColor" stroke="none">10</text></svg></button>
</div>
</div>
</div>
<div id="controls" class="controls hidden">
@@ -173,13 +188,18 @@
<button id="waveLoud" class="wave-loud hidden" type="button">⚡ Jump to the loudest part</button>
<div class="deck-main">
<button id="prevBtn" class="ctrl" title="Previous" aria-label="Previous"><svg viewBox="0 0 24 24" aria-hidden="true" class="fill"><rect x="5" y="5" width="2.8" height="14" rx="1.2"/><path d="M19 5.5v13L9.5 12z"/></svg></button>
<button id="back10Btn" class="ctrl skip10" type="button" title="Back 10 seconds" aria-label="Back 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12a8 8 0 1 0 3-6.2M4 4v4.5h4.5"/><text x="12" y="15.5" text-anchor="middle" font-size="7.5" font-weight="700" fill="currentColor" stroke="none">10</text></svg></button>
<button id="playBtn" class="ctrl play" title="Play/Pause" aria-label="Play or pause"><svg viewBox="0 0 24 24" aria-hidden="true" class="fill"><path d="M9 5.5v13l10-6.5z"/></svg></button>
<button id="fwd10Btn" class="ctrl skip10" type="button" title="Forward 10 seconds" aria-label="Forward 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 12a8 8 0 1 1-3-6.2M20 4v4.5h-4.5"/><text x="12" y="15.5" text-anchor="middle" font-size="7.5" font-weight="700" fill="currentColor" stroke="none">10</text></svg></button>
<button id="nextBtn" class="ctrl" title="Next" aria-label="Next"><svg viewBox="0 0 24 24" aria-hidden="true" class="fill"><rect x="16.2" y="5" width="2.8" height="14" rx="1.2"/><path d="M5 5.5v13l9.5-6.5z"/></svg></button>
<div id="dkModeHost" hidden><button id="shuffleBtn" class="ctrl" title="Shuffle upcoming tracks (S)" aria-label="Shuffle"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 3h5v5M4 20 21 3M21 16v5h-5M15 15l6 6M4 4l5 5"/></svg></button><button id="repeatBtn" class="ctrl" title="Repeat list when finished (R)" aria-label="Repeat list"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17 2l4 4-4 4M3 12v-2a4 4 0 0 1 4-4h14M7 22l-4-4 4-4M21 12v2a4 4 0 0 1-4 4H3"/></svg></button><button id="loopBtn" class="ctrl" title="Loop this video (L)"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17 2l4 4-4 4M3 12v-2a4 4 0 0 1 4-4h14M7 22l-4-4 4-4M21 12v2a4 4 0 0 1-4 4H3M11 10.2l1.6-.9v5.4"/></svg></button></div>
</div>
<div class="deck-vol vol">
<div class="vol-pod">
<input id="volume" type="range" min="0" max="1" step="0.01" value="1" aria-label="Volume" />
<button id="muteBtn" class="ctrl" title="Mute" aria-label="Mute"></button>
<span id="volPct" class="vol-pct">100%</span>
</div>
<button id="modeBtn" class="ctrl" type="button" title="Playback mode" aria-label="Playback mode"></button>
</div>
<div class="dk-quick">
@@ -189,6 +209,7 @@
<button id="abClearBtn" class="ctrl ab-ctrl hidden" title="Clear A-B loop">↺</button>
<button id="ctlMoreBtn" class="dk-more" type="button" aria-haspopup="dialog" title="More controls" aria-label="More controls"><span><em class="mc-dots">⋯</em><i class="mc-label"> More<i class="mc-rest"> controls</i></i></span><small>Speed · Quality · A-B · Sleep · EQ</small></button>
</div>
<div id="deckChips" class="deck-chips" aria-label="Quick settings"></div>
<div id="dkMoreHost" hidden>
<button id="rememberPosBtn" class="ctrl" title="Remember playback position for this song (per playlist)">📍</button>
<button id="sleepTimerBtn" class="ctrl" title="Sleep timer">⏱</button>
@@ -225,10 +246,10 @@
<div class="np-avail hidden" id="npAvail" aria-live="polite"></div>
</div>
<div class="np-actions">
<button id="addBtn" class="np-btn" title="Add to the queue or to a playlist">+ Add</button>
<button id="saveBtn" class="np-btn" title="Save this video for offline playback">⬇ Save</button>
<button id="exportBtn" class="np-btn" title="Save this video to your gallery, Files app or Downloads folder — works while streaming too">🖼 Gallery</button>
<button id="serviceBtn" class="np-btn" title="Service mode — set-list view with the screen kept awake">🎤 Service</button>
<button id="addBtn" class="np-btn" title="Add to the queue or to a playlist"><svg class="ti" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h10M4 11h10M4 16h6M17 13v8M13 17h8"/></svg><span class="tl">Add</span></button>
<button id="saveBtn" class="np-btn" title="Save this video for offline playback"><svg class="ti" viewBox="0 0 24 24" aria-hidden="true"><path class="i-save" d="M12 4v11M7 11l5 5 5-5M5 20h14"/><path class="i-done" d="M20 12a8 8 0 1 1-16 0 8 8 0 0 1 16 0zM8.5 12.2l2.4 2.4 4.6-4.8"/></svg><span class="tl">Save</span></button>
<button id="exportBtn" class="np-btn" title="Save this video to your gallery, Files app or Downloads folder — works while streaming too"><svg class="ti" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="3"/><circle cx="9" cy="10.5" r="1.6"/><path d="M4 17l5-4.5 4 3.5 3-2.5 4 3.5"/></svg><span class="tl">Gallery</span></button>
<button id="serviceBtn" class="np-btn" title="Service mode — set-list view with the screen kept awake"><svg class="ti" viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21"/></svg><span class="tl">Service</span></button>
<div id="npMoreHost" class="np-more-host" hidden>
<button id="queueBtn" class="np-btn" title="Add to the temporary queue">+ Queue</button>
<button id="addPlaylistBtn" class="np-btn" title="Add to a playlist">+ Playlist</button>

View File

@@ -534,3 +534,4 @@
border-radius: 16px; color: var(--text); background: var(--bg-3); font-weight: 700; font-size: 12.5px; white-space: nowrap;
}
@media (max-width: 380px) { #nowPlayingMeta .np-actions #addBtn, #nowPlayingMeta .np-actions #serviceBtn { flex-basis: calc(50% - 8px); } }
.player-stage { flex-shrink: 0; }

View File

@@ -58,6 +58,7 @@ const SHELL = [
'/styles.css',
'/service-ui.css',
'/refresh.css',
'/theme-glass.css',
'/flag-ui.js',
'/export.js',
'/async-guard.js',

209
frontend/theme-glass.css Normal file
View File

@@ -0,0 +1,209 @@
/* ============================================================================
* Layout theme: Glass Stage (Settings → Layout theme → Glass Stage)
* html[data-layout="glass-stage"]
*
* Frosted-glass cards over an ambient coral glow. Besides colours it changes the
* layout of the player page:
* · overlay on the video: quality chip, Video / Audio-only switch, −10 / play / +10
* · deck: prev · −10 · play · +10 · next, volume pod + labelled mode pill,
* A-B group, labelled Fullscreen, More, and a row of quick-setting chips
* · now playing: four stacked icon tiles, secondary actions as scrolling pills
* · lyrics: segmented tabs, glowing active line
* · mini player: floating card above the bottom nav
* Everything is scoped, so other themes are untouched.
* ========================================================================== */
/* Elements that only exist for this theme stay hidden elsewhere. */
.stg-overlay, .skip10, .deck-chips, .vol-pct, .mode-name { display: none; }
.np-btn .ti, .np-btn .tl { pointer-events: none; }
.np-btn .ti { width: 20px; height: 20px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.np-btn .i-done { display: none; }
.np-btn.done .i-save { display: none; }
.np-btn.done .i-done { display: inline; }
html[data-layout="glass-stage"] {
--bg: #121317;
--bg-1: #0d0e12;
--bg-2: #1a1b20;
--bg-3: #24252a;
--line: rgba(255, 255, 255, 0.08);
--line-soft: rgba(255, 255, 255, 0.05);
--text: #e3e2e7;
--text-2: #c9c4c2;
--text-dim: #8d8b92;
--accent: #ff553c;
--accent-bright: #ff7865;
--accent-deep: #d63a22;
--accent-glow: rgba(255, 85, 60, 0.5);
--glass: rgba(13, 14, 18, 0.7);
--glass-line: rgba(255, 255, 255, 0.06);
--tile: rgba(255, 255, 255, 0.04);
--tile-hi: rgba(255, 255, 255, 0.09);
--grad: linear-gradient(135deg, #ff553c, #ff7865);
--radius: 24px;
}
html[data-layout="glass-stage"] body::before {
background:
radial-gradient(520px 400px at 50% -6%, rgba(255, 85, 60, 0.14), transparent 70%),
radial-gradient(380px 380px at -8% 42%, rgba(255, 180, 167, 0.07), transparent 70%);
}
/* ---- glass surfaces ---- */
html[data-layout="glass-stage"] #controls,
html[data-layout="glass-stage"] #nowPlayingMeta,
html[data-layout="glass-stage"] .notes-panel,
html[data-layout="glass-stage"] .upnext,
html[data-layout="glass-stage"] .related-panel,
html[data-layout="glass-stage"] .list-pane {
background: var(--glass);
border: 1px solid var(--glass-line);
border-radius: 24px;
-webkit-backdrop-filter: blur(24px) saturate(1.3);
backdrop-filter: blur(24px) saturate(1.3);
box-shadow: 0 16px 40px -18px rgba(0, 0, 0, 0.7);
}
html[data-layout="glass-stage"] #nowPlayingMeta { padding: 16px; margin-top: 16px; gap: 14px; }
html[data-layout="glass-stage"] .topbar { background: rgba(13, 14, 18, 0.8); -webkit-backdrop-filter: blur(20px); backdrop-filter: blur(20px); border-bottom: 1px solid var(--glass-line); }
html[data-layout="glass-stage"] .bottom-nav { background: rgba(13, 14, 18, 0.82); -webkit-backdrop-filter: blur(24px); backdrop-filter: blur(24px); border-top: 1px solid var(--glass-line); }
html[data-layout="glass-stage"] .bottom-nav-btn.active { color: var(--accent); }
/* ---- 1. video stage + overlay ---- */
html[data-layout="glass-stage"] .player-stage { border-radius: 28px; border: 1px solid var(--glass-line); box-shadow: 0 20px 50px rgba(0, 0, 0, 0.8); }
html[data-layout="glass-stage"] .stg-overlay {
display: flex; flex-direction: column; justify-content: space-between;
position: absolute; inset: 0; z-index: 6; padding: 12px; pointer-events: none;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.5), transparent 35%, transparent 65%, rgba(0, 0, 0, 0.55));
opacity: 0; transition: opacity 0.25s;
}
html[data-layout="glass-stage"] .player-stage.is-paused .stg-overlay,
html[data-layout="glass-stage"] .player-stage.stg-show .stg-overlay { opacity: 1; }
html[data-layout="glass-stage"] .player-pane.empty .stg-overlay { display: none; }
html[data-layout="glass-stage"] .stg-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
html[data-layout="glass-stage"] .stg-chip {
display: inline-flex; align-items: center; gap: 8px; height: 28px; padding: 0 12px; border-radius: 99px;
background: rgba(13, 14, 18, 0.8); border: 1px solid rgba(255, 255, 255, 0.1); -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px);
font: 600 10px var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text);
}
html[data-layout="glass-stage"] .stg-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 8px var(--accent); }
html[data-layout="glass-stage"] .stg-seg { display: inline-flex; padding: 2px; border-radius: 99px; background: rgba(13, 14, 18, 0.85); border: 1px solid rgba(255, 255, 255, 0.1); pointer-events: auto; }
html[data-layout="glass-stage"] .stg-seg button { height: 26px; padding: 0 12px; border: 0; border-radius: 99px; background: transparent; color: var(--text-dim); font: 600 11px var(--ui); cursor: pointer; }
html[data-layout="glass-stage"] .stg-seg button.on { background: rgba(255, 255, 255, 0.16); color: var(--text); }
html[data-layout="glass-stage"] .stg-mid { display: flex; align-items: center; justify-content: space-between; padding: 0 22px; }
html[data-layout="glass-stage"] .stg-skip, html[data-layout="glass-stage"] .stg-play { pointer-events: auto; border: 1px solid rgba(255, 255, 255, 0.1); color: #fff; display: grid; place-items: center; cursor: pointer; }
html[data-layout="glass-stage"] .stg-skip { width: 46px; height: 46px; border-radius: 50%; background: rgba(0, 0, 0, 0.45); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px); }
html[data-layout="glass-stage"] .stg-skip svg { width: 26px; height: 26px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
html[data-layout="glass-stage"] .stg-play { width: 62px; height: 62px; border-radius: 50%; border: 0; background: linear-gradient(135deg, #ff553c, #ff8471); box-shadow: 0 0 34px rgba(255, 85, 60, 0.6); }
html[data-layout="glass-stage"] .stg-play svg { width: 32px; height: 32px; fill: currentColor; stroke: none; }
/* ---- 2. deck ---- */
html[data-layout="glass-stage"] #controls { padding: 16px; gap: 14px; }
html[data-layout="glass-stage"] #controls .deck-main { display: flex; justify-content: space-between; gap: 6px; padding: 2px 4px 0; }
html[data-layout="glass-stage"] #controls .deck-main .ctrl { width: 46px; height: 46px; border-radius: 16px; background: var(--tile); border: 1px solid var(--glass-line); }
html[data-layout="glass-stage"] #controls .deck-main .ctrl.skip10 { display: grid; width: 44px; height: 44px; color: var(--text-2); }
html[data-layout="glass-stage"] #controls .deck-main .ctrl.skip10 svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
html[data-layout="glass-stage"] #controls .deck-main .ctrl.play { width: 64px; height: 64px; border-radius: 50%; border: 0; background: var(--grad); box-shadow: 0 12px 30px rgba(255, 85, 60, 0.55), 0 0 20px rgba(255, 85, 60, 0.35); }
html[data-layout="glass-stage"] #controls .deck-main .ctrl.play svg { width: 32px; height: 32px; }
html[data-layout="glass-stage"] #controls .time { font: 600 12px var(--mono); }
html[data-layout="glass-stage"] #controls .deck-vol { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 0; }
html[data-layout="glass-stage"] #controls .vol-pod {
display: flex; align-items: center; gap: 10px; padding: 6px 12px; border-radius: 16px; min-width: 0;
background: var(--tile); border: 1px solid var(--glass-line);
}
html[data-layout="glass-stage"] #controls .vol-pod #muteBtn { order: -1; width: 28px; height: 28px; border-radius: 8px; }
html[data-layout="glass-stage"] #controls .vol-pod input[type="range"] { flex: 1; min-width: 0; height: 4px; }
html[data-layout="glass-stage"] #controls .vol-pct { display: inline; font: 500 10px var(--mono); color: var(--text-dim); }
html[data-layout="glass-stage"] #controls .deck-vol #modeBtn {
width: auto; height: auto; min-height: 40px; padding: 6px 14px; border-radius: 16px; background: var(--tile); border: 1px solid var(--glass-line);
display: flex; flex-direction: row-reverse; align-items: center; justify-content: space-between; gap: 8px; flex: none; order: 2;
}
html[data-layout="glass-stage"] #controls .mode-name { display: inline; font: 600 12px var(--ui); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
html[data-layout="glass-stage"] #controls .deck-vol #modeBtn svg { width: 17px; height: 17px; }
html[data-layout="glass-stage"] #controls .dk-quick { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
html[data-layout="glass-stage"] #controls .dk-quick .ctrl,
html[data-layout="glass-stage"] #controls .dk-more { height: 40px; border-radius: 14px; background: var(--tile); border: 1px solid var(--glass-line); }
html[data-layout="glass-stage"] #controls .dk-quick #fsBtn { font-size: 12px; }
html[data-layout="glass-stage"] #controls .dk-more small { display: none; }
html[data-layout="glass-stage"] #controls .dk-more { flex: 1 1 90px; }
html[data-layout="glass-stage"] #controls .deck-chips { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 2px; scrollbar-width: none; }
html[data-layout="glass-stage"] #controls .deck-chips .dchip {
flex: none; height: 30px; padding: 0 12px; border-radius: 99px; border: 1px solid var(--glass-line); background: rgba(255, 255, 255, 0.03);
color: var(--text-dim); font: 500 11px var(--ui); white-space: nowrap; cursor: pointer;
}
html[data-layout="glass-stage"] #controls .deck-chips .dchip.on { background: rgba(255, 255, 255, 0.09); color: var(--text); font-family: var(--mono); font-weight: 600; }
/* ---- 3. now playing ---- */
html[data-layout="glass-stage"] #nowPlayingMeta .np-title { font-size: clamp(22px, 6vw, 26px); }
html[data-layout="glass-stage"] #npAvail { display: inline-flex; align-items: center; gap: 8px; align-self: flex-start; padding: 4px 12px; border-radius: 99px; background: var(--tile); border: 1px solid var(--glass-line); font-size: 11px; }
html[data-layout="glass-stage"] #nowPlayingMeta .np-actions { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
html[data-layout="glass-stage"] #nowPlayingMeta .np-actions #addBtn,
html[data-layout="glass-stage"] #nowPlayingMeta .np-actions #saveBtn,
html[data-layout="glass-stage"] #nowPlayingMeta .np-actions #exportBtn,
html[data-layout="glass-stage"] #nowPlayingMeta .np-actions #serviceBtn {
order: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; height: auto; padding: 12px 4px;
border-radius: 18px; background: var(--tile); border: 1px solid var(--glass-line); color: var(--text-2); font: 500 11px var(--ui);
}
html[data-layout="glass-stage"] #nowPlayingMeta .np-actions #addBtn:hover,
html[data-layout="glass-stage"] #nowPlayingMeta .np-actions #saveBtn:hover,
html[data-layout="glass-stage"] #nowPlayingMeta .np-actions #exportBtn:hover,
html[data-layout="glass-stage"] #nowPlayingMeta .np-actions #serviceBtn:hover { background: var(--tile-hi); color: #fff; }
html[data-layout="glass-stage"] #nowPlayingMeta .np-actions #saveBtn.done { background: rgba(255, 85, 60, 0.15); border-color: rgba(255, 85, 60, 0.3); color: var(--accent); box-shadow: 0 0 20px rgba(255, 85, 60, 0.15); }
html[data-layout="glass-stage"] #nowPlayingMeta .np-actions #serviceBtn .ti { color: var(--accent-bright); }
/* secondary actions: the hidden host becomes a scrolling pill row; the sheet button goes away */
html[data-layout="glass-stage"] #nowPlayingMeta .np-actions .np-more { display: none; }
html[data-layout="glass-stage"] #nowPlayingMeta .np-actions .np-more-host {
display: flex; grid-column: 1 / -1; gap: 8px; overflow-x: auto; scrollbar-width: none; padding-top: 2px;
}
html[data-layout="glass-stage"] #nowPlayingMeta .np-more-host[hidden] { display: flex; }
html[data-layout="glass-stage"] #nowPlayingMeta .np-more-host #queueBtn,
html[data-layout="glass-stage"] #nowPlayingMeta .np-more-host #addPlaylistBtn { display: none; }
html[data-layout="glass-stage"] #nowPlayingMeta .np-more-host .np-btn {
flex: none; height: 34px; padding: 0 14px; border-radius: 12px; background: var(--tile); border: 1px solid var(--glass-line);
color: var(--text-dim); font: 500 11px var(--ui); white-space: nowrap;
}
html[data-layout="glass-stage"] #nowPlayingMeta .np-more-host .np-btn[hidden] { display: none; }
/* ---- 4. lyrics ---- */
html[data-layout="glass-stage"] .notes-tabs { background: transparent; padding: 14px 16px 6px; gap: 6px; }
html[data-layout="glass-stage"] .notes-tab { border-radius: 14px; padding: 8px 12px; background: transparent; color: var(--text-dim); }
html[data-layout="glass-stage"] .notes-tab.active { background: rgba(255, 255, 255, 0.1); color: var(--accent-bright); box-shadow: none; }
html[data-layout="glass-stage"] #lyricsPane .lyrics-toolbar .np-btn,
html[data-layout="glass-stage"] #lyricsPane .lyr-offset { background: var(--tile); border: 1px solid var(--glass-line); }
html[data-layout="glass-stage"] .lyr-line { opacity: 0.55; }
html[data-layout="glass-stage"] .lyr-line.active {
opacity: 1; position: relative; padding: 14px; border-radius: 18px;
background: linear-gradient(90deg, rgba(255, 85, 60, 0.2), rgba(255, 85, 60, 0.05) 60%, transparent);
border: 1px solid rgba(255, 85, 60, 0.3); box-shadow: 0 8px 24px rgba(255, 85, 60, 0.15); box-shadow: 0 8px 24px rgba(255, 85, 60, 0.15);
}
html[data-layout="glass-stage"] .lyr-line.active .lyr-text { color: #fff; font-family: var(--display); font-weight: 700; font-size: 18px; }
html[data-layout="glass-stage"] .lyr-line.active .lyr-time { color: var(--accent-bright); background: transparent; }
html[data-layout="glass-stage"] .lyr-section { text-align: center; color: var(--accent-bright); }
/* ---- 5. lists ---- */
html[data-layout="glass-stage"] .list-pane { padding: 16px; margin-top: 16px; }
html[data-layout="glass-stage"] .list-header { padding: 4px 0 12px; }
html[data-layout="glass-stage"] .list-header::before { content: "Current list"; font: 600 10px var(--mono); letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent-bright); }
html[data-layout="glass-stage"] #listActions button.la-primary { background: var(--grad); box-shadow: 0 8px 22px rgba(255, 85, 60, 0.4); }
html[data-layout="glass-stage"] #listActions button.la-main { background: var(--tile); border: 1px solid var(--glass-line); }
html[data-layout="glass-stage"] .card { background: var(--tile); border: 1px solid var(--glass-line); border-radius: 18px; }
html[data-layout="glass-stage"] .card.playing, html[data-layout="glass-stage"] .card.now-playing { border-color: rgba(255, 85, 60, 0.4); }
/* ---- 6. mini player: floating card ---- */
@media (max-width: 899px) {
html[data-layout="glass-stage"] .mini-bar { left: 12px; right: 12px; bottom: calc(66px + env(safe-area-inset-bottom)); }
html[data-layout="glass-stage"] .mini-bar-inner {
border-radius: 18px; border: 1px solid rgba(255, 255, 255, 0.08); background: rgba(13, 14, 18, 0.82);
-webkit-backdrop-filter: blur(24px); backdrop-filter: blur(24px); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.75); overflow: hidden;
}
}
/* ordering + pill sizing (beats the id-based order / 50% rules in refresh.css) */
html[data-layout="glass-stage"] #controls .deck-main #prevBtn { order: 1; }
html[data-layout="glass-stage"] #controls .deck-main #back10Btn { order: 2; }
html[data-layout="glass-stage"] #controls .deck-main #playBtn { order: 3; }
html[data-layout="glass-stage"] #controls .deck-main #fwd10Btn { order: 4; }
html[data-layout="glass-stage"] #controls .deck-main #nextBtn { order: 5; }
html[data-layout="glass-stage"] #nowPlayingMeta .np-actions .np-more-host .np-btn { flex: 0 0 auto !important; width: auto !important; min-width: 0 !important; height: 34px !important; }
html[data-layout="glass-stage"] .stg-overlay { justify-content: flex-start; }
html[data-layout="glass-stage"] .stg-mid { margin: auto 0; }