Compare commits

...

4 Commits

Author SHA1 Message Date
Jonathan Sykes
67fc6f584f Allow swipeout width transition in impeccable checks 2026-10-06 03:06:02 +08:00
Jonathan Sykes
a81f4ffa51 Refine fullscreen control overlay 2026-10-06 02:32:43 +08:00
Jonathan Sykes
1cad18826b Fill viewport in fullscreen playback 2026-10-06 02:32:36 +08:00
Jonathan Sykes
1ba3f8704b Style Framework7 swipe layout 2026-10-06 02:32:36 +08:00
5 changed files with 282 additions and 141 deletions

View File

@@ -12,6 +12,15 @@
], ],
"createdAt": "2026-10-02T23:14:10.804Z", "createdAt": "2026-10-02T23:14:10.804Z",
"reason": "Agent verified all three matching images are hidden dynamic artwork targets (artImg, serviceArt, rvThumb); app.js assigns their src when media metadata is available." "reason": "Agent verified all three matching images are hidden dynamic artwork targets (artImg, serviceArt, rvThumb); app.js assigns their src when media metadata is available."
},
{
"rule": "layout-transition",
"value": "*",
"files": [
"frontend/theme-f7.css"
],
"createdAt": "2026-10-05T18:33:43.625Z",
"reason": "Framework7 swipeout reveal retains its existing width transition for the action area"
} }
] ]
} }

View File

@@ -1,18 +1,67 @@
/* Shared fullscreen surface; inline player styling is unchanged. */ /* Shared fullscreen surface; inline player styling is unchanged. */
.orientation-fullscreen .fullscreen-hud[hidden] { display: none; } .orientation-fullscreen .fullscreen-hud[hidden] { display: none; }
.orientation-fullscreen .fullscreen-hud { position: absolute; z-index: 20; bottom: max(12px, env(safe-area-inset-bottom)); left: max(12px, env(safe-area-inset-left)); right: max(12px, env(safe-area-inset-right)); display: flex; align-items: center; gap: 8px; padding: 8px; border-radius: 12px; background: rgb(0 0 0 / .8); color: white; } .orientation-fullscreen .fullscreen-hud {
.fullscreen-hud button { min-height: 44px; padding: 8px 12px; border: 1px solid rgb(255 255 255 / .25); border-radius: 8px; background: rgb(255 255 255 / .12); color: white; font: inherit; } --hud-glass: color-mix(in srgb, var(--bg-1, #111) 74%, transparent);
.fullscreen-hud button:focus-visible { outline: 2px solid white; outline-offset: 2px; } --hud-ink: var(--text, #fff);
.fullscreen-hud input { min-width: 40px; flex: 1; height: 44px; } --hud-line: color-mix(in srgb, var(--line, #fff) 32%, transparent);
.player-stage.orientation-fullscreen { background: black; } position: absolute; z-index: 20;
.player-stage.orientation-overlay { position: fixed !important; z-index: 10000 !important; inset: 0 !important; width: 100dvw !important; height: 100dvh !important; max-width: none !important; max-height: none !important; aspect-ratio: auto !important; margin: 0 !important; border-radius: 0 !important; } right: max(12px, env(safe-area-inset-right));
.player-stage.orientation-overlay[data-fullscreen-rotation="90"], .player-stage.orientation-overlay[data-fullscreen-rotation="-90"] { inset: auto !important; left: 50% !important; top: 50% !important; width: 100dvh !important; height: 100dvw !important; transform: translate(-50%, -50%) rotate(90deg) !important; } bottom: max(12px, env(safe-area-inset-bottom));
left: max(12px, env(safe-area-inset-left));
display: flex; align-items: center; gap: 6px;
padding: 5px; border: 1px solid var(--hud-line); border-radius: 999px;
background: var(--hud-glass); color: var(--hud-ink);
-webkit-backdrop-filter: saturate(150%) blur(18px);
backdrop-filter: saturate(150%) blur(18px);
box-shadow: 0 8px 28px rgb(0 0 0 / .2);
opacity: 1; transition: opacity .22s ease;
}
.orientation-fullscreen .fullscreen-hud.hud-hidden { opacity: 0; pointer-events: none; }
.fullscreen-hud button {
display: grid; place-items: center; flex: none;
width: 48px; min-width: 48px; height: 48px; min-height: 48px; padding: 0;
border: 1px solid transparent; border-radius: 50%;
background: color-mix(in srgb, var(--hud-ink, #fff) 10%, transparent);
color: var(--hud-ink, #fff); font: inherit; cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.fullscreen-hud button:hover { background: color-mix(in srgb, var(--hud-ink, #fff) 18%, transparent); }
.fullscreen-hud button:active { transform: scale(.94); }
.fullscreen-hud button svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.fullscreen-hud button .fill { fill: currentColor; stroke: none; }
.fullscreen-hud button:focus-visible { outline: 2px solid var(--accent, #fff); outline-offset: 2px; }
.fullscreen-hud input { min-width: 40px; min-height: 44px; height: 44px; flex: 1; margin: 0 4px; accent-color: var(--accent, #fff); touch-action: none; }
.player-stage.orientation-fullscreen { background: #000; }
.player-stage.orientation-overlay {
position: fixed !important; z-index: 10000 !important; inset: 0 !important;
width: 100dvw !important; height: 100dvh !important;
min-width: 0 !important; min-height: 0 !important;
max-width: none !important; max-height: none !important;
aspect-ratio: auto !important; margin: 0 !important; padding: 0 !important;
border: 0 !important; border-radius: 0 !important; box-shadow: none !important;
}
.player-stage.orientation-overlay[data-fullscreen-rotation="90"],
.player-stage.orientation-overlay[data-fullscreen-rotation="-90"] {
inset: auto !important; left: 50% !important; top: 50% !important;
width: 100dvh !important; height: 100dvw !important;
transform: translate(-50%, -50%) rotate(90deg) !important;
}
.player-stage.orientation-overlay[data-fullscreen-rotation="-90"] { transform: translate(-50%, -50%) rotate(-90deg) !important; } .player-stage.orientation-overlay[data-fullscreen-rotation="-90"] { transform: translate(-50%, -50%) rotate(-90deg) !important; }
.orientation-fullscreen video { width: 100% !important; height: 100% !important; object-fit: contain; } .orientation-fullscreen video {
flex: none !important; width: 100% !important; height: 100% !important;
max-width: none !important; max-height: none !important; object-fit: contain;
}
.orientation-fullscreen #stgOverlay { display: none !important; } .orientation-fullscreen #stgOverlay { display: none !important; }
.orientation-overlay[data-fullscreen-rotation="90"] .fullscreen-hud { bottom: max(12px, env(safe-area-inset-left)); left: max(12px, env(safe-area-inset-top)); right: max(12px, env(safe-area-inset-bottom)); } .orientation-overlay[data-fullscreen-rotation="90"] .fullscreen-hud {
.orientation-overlay[data-fullscreen-rotation="-90"] .fullscreen-hud { bottom: max(12px, env(safe-area-inset-right)); left: max(12px, env(safe-area-inset-bottom)); right: max(12px, env(safe-area-inset-top)); } bottom: max(12px, env(safe-area-inset-left)); left: max(12px, env(safe-area-inset-top)); right: max(12px, env(safe-area-inset-bottom));
@media(max-width: 460px) { .fullscreen-hud { flex-wrap: wrap; } .fullscreen-hud input { flex-basis: 100%; order: -1; } .fullscreen-hud button { flex: 1; padding-inline: 6px; font-size: 12px; } } }
/* Raise the existing ancestry instead of moving a playing video in WebKit. */ .orientation-overlay[data-fullscreen-rotation="-90"] .fullscreen-hud {
bottom: max(12px, env(safe-area-inset-right)); left: max(12px, env(safe-area-inset-bottom)); right: max(12px, env(safe-area-inset-top));
}
@media (max-width: 460px) {
.orientation-fullscreen .fullscreen-hud { gap: 2px; padding: 3px; }
.fullscreen-hud button { width: 44px; min-width: 44px; height: 44px; min-height: 44px; }
}
/* Raise existing ancestry instead of moving a playing video in WebKit. */
body.landscape-fs .orientation-ancestor { z-index: 10001 !important; transform: none !important; filter: none !important; overflow: visible !important; } body.landscape-fs .orientation-ancestor { z-index: 10001 !important; transform: none !important; filter: none !important; overflow: visible !important; }
body.landscape-fs { overflow: hidden; } body.landscape-fs { overflow: hidden; }

View File

@@ -13,19 +13,32 @@
} }
function create(options) { function create(options) {
const { stage, settings, persist, toggle, seek, state } = options; const { stage, settings, persist, toggle, seek, state } = options;
let active = false, custom = false, native = false, locked = false, angle = 0, timer; let active = false, custom = false, native = false, locked = false, angle = 0, timer, hideTimer;
const ancestors = []; const ancestors = [];
for (let el = stage.parentElement; el && el !== document.body; el = el.parentElement) ancestors.push(el); for (let el = stage.parentElement; el && el !== document.body; el = el.parentElement) ancestors.push(el);
const hud = document.createElement('div'); hud.className = 'fullscreen-hud'; hud.hidden = true; const hud = document.createElement('div'); hud.className = 'fullscreen-hud'; hud.hidden = true;
hud.innerHTML = '<button type="button" aria-label="Play or pause">Play / pause</button><input type="range" min="0" max="1000" value="0" aria-label="Seek video"><button type="button" class="fullscreen-orientation" aria-label="Change fullscreen orientation"></button><button type="button" aria-label="Exit fullscreen">Exit</button>'; hud.innerHTML = '<button type="button" aria-label="Play or pause" title="Play or pause"><svg class="fill" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></button><input type="range" min="0" max="1000" value="0" aria-label="Seek video"><button type="button" class="fullscreen-orientation" aria-label="Change fullscreen orientation" title="Change fullscreen orientation"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8V4h4M20 16v4h-4M4 4l6 6m4 4 6 6M20 8V4h-4M4 16v4h4M20 4l-6 6m-4 4-6 6"/></svg></button><button type="button" aria-label="Exit fullscreen" title="Exit fullscreen"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 4H4v5m0-5 6 6m5 9h5v-5m0 5-6-6"/></svg></button>';
stage.append(hud); stage.append(hud);
const [play, orientation, exit] = hud.querySelectorAll('button'), range = hud.querySelector('input'); const [play, orientation, exit] = hud.querySelectorAll('button'), range = hud.querySelector('input');
function revealControls() {
if (!active) return;
hud.classList.remove('hud-hidden'); clearTimeout(hideTimer);
hideTimer = setTimeout(() => {
if (!hud.contains(document.activeElement)) hud.classList.add('hud-hidden');
}, 3200);
}
stage.addEventListener('pointerdown', revealControls, { passive: true });
stage.addEventListener('pointermove', event => { if (event.pointerType === 'mouse') revealControls(); }, { passive: true });
hud.addEventListener('focusin', () => { clearTimeout(hideTimer); hud.classList.remove('hud-hidden'); });
hud.addEventListener('focusout', () => setTimeout(revealControls, 0));
hud.addEventListener('click', revealControls);
play.onclick = toggle; exit.onclick = leave; play.onclick = toggle; exit.onclick = leave;
function layout() { function layout() {
if (!active) return; if (!active) return;
angle = custom ? rotation(settings().fullscreenOrientation, innerWidth, innerHeight) : 0; angle = custom ? rotation(settings().fullscreenOrientation, innerWidth, innerHeight) : 0;
stage.dataset.fullscreenRotation = String(angle); stage.dataset.fullscreenRotation = String(angle);
orientation.textContent = `Orientation: ${normalize(settings().fullscreenOrientation)}`; orientation.setAttribute('aria-label', `Change fullscreen orientation (currently ${normalize(settings().fullscreenOrientation)})`);
orientation.title = `Orientation: ${normalize(settings().fullscreenOrientation)}`;
} }
async function orient() { async function orient() {
if (locked) { screen.orientation?.unlock?.(); locked = false; } if (locked) { screen.orientation?.unlock?.(); locked = false; }
@@ -48,9 +61,9 @@
hud.addEventListener('pointerdown', event => event.stopPropagation()); hud.addEventListener('pointerdown', event => event.stopPropagation());
hud.addEventListener('pointerup', event => event.stopPropagation()); hud.addEventListener('pointerup', event => event.stopPropagation());
function cleanup() { function cleanup() {
active = false; custom = false; native = false; angle = 0; clearInterval(timer); active = false; custom = false; native = false; angle = 0; clearInterval(timer); clearTimeout(hideTimer);
ancestors.forEach(el => el.classList.remove('orientation-ancestor')); ancestors.forEach(el => el.classList.remove('orientation-ancestor'));
hud.hidden = true; stage.classList.remove('orientation-overlay', 'orientation-fullscreen'); hud.hidden = true; hud.classList.remove('hud-hidden'); stage.classList.remove('orientation-overlay', 'orientation-fullscreen');
delete stage.dataset.fullscreenRotation; document.body.classList.remove('landscape-fs'); delete stage.dataset.fullscreenRotation; document.body.classList.remove('landscape-fs');
if (locked) { screen.orientation?.unlock?.(); locked = false; } if (locked) { screen.orientation?.unlock?.(); locked = false; }
} }
@@ -60,7 +73,7 @@
} }
async function enter() { async function enter() {
if (active) { leave(); return; } if (active) { leave(); return; }
active = true; ancestors.forEach(el => el.classList.add('orientation-ancestor')); stage.classList.add('orientation-fullscreen'); hud.hidden = false; active = true; ancestors.forEach(el => el.classList.add('orientation-ancestor')); stage.classList.add('orientation-fullscreen'); hud.hidden = false; revealControls();
document.body.classList.add('landscape-fs'); document.body.classList.add('landscape-fs');
try { try {
if (!stage.requestFullscreen) throw new Error('Custom fullscreen'); if (!stage.requestFullscreen) throw new Error('Custom fullscreen');
@@ -70,7 +83,15 @@
if (!active) return; if (!active) return;
await orient(); await orient();
if (!active) return; if (!active) return;
timer = setInterval(() => { const s = state(); range.value = String(s.duration ? s.time / s.duration * 1000 : 0); play.textContent = s.paused ? 'Play' : 'Pause'; }, 250); timer = setInterval(() => {
const s = state();
range.value = String(s.duration ? s.time / s.duration * 1000 : 0);
play.setAttribute('aria-label', s.paused ? 'Play video' : 'Pause video');
play.title = s.paused ? 'Play' : 'Pause';
play.innerHTML = s.paused
? '<svg class="fill" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>'
: '<svg class="fill" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 5h4v14H6zm8 0h4v14h-4z"/></svg>';
}, 250);
} }
document.addEventListener('fullscreenchange', () => { if (active && !document.fullscreenElement && native) cleanup(); }); document.addEventListener('fullscreenchange', () => { if (active && !document.fullscreenElement && native) cleanup(); });
document.addEventListener('keydown', event => { if (active && event.key === 'Escape') leave(); }); document.addEventListener('keydown', event => { if (active && event.key === 'Escape') leave(); });

View File

@@ -3905,6 +3905,35 @@ body.presenter-open { overflow: hidden; }
.player-stage { touch-action: pan-y; } /* page still scrolls; we get double-tap + pinch */ .player-stage { touch-action: pan-y; } /* page still scrolls; we get double-tap + pinch */
:fullscreen .player-stage, .player-stage:fullscreen, :fullscreen .player-stage, .player-stage:fullscreen,
body.landscape-fs .player-stage { touch-action: none; } /* fullscreen (real or the iOS landscape one): swipes too */ body.landscape-fs .player-stage { touch-action: none; } /* fullscreen (real or the iOS landscape one): swipes too */
/* Native fullscreen keeps the ordinary stage's aspect ratio and pane width unless
all sizing constraints are explicitly reset. This also covers every layout's
player because fullscreen is a cross-layout surface. */
:is(.player-stage:fullscreen, .player-stage.orientation-fullscreen.orientation-overlay) {
position: fixed !important;
inset: 0 !important;
width: 100dvw !important;
height: 100dvh !important;
min-width: 0 !important;
min-height: 0 !important;
max-width: none !important;
max-height: none !important;
aspect-ratio: auto !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
overflow: hidden !important;
background: #000 !important;
}
.player-stage.orientation-fullscreen > video {
flex: none !important;
width: 100% !important;
height: 100% !important;
max-width: none !important;
max-height: none !important;
object-fit: contain;
}
#video { transform-origin: center center; transition: transform 0.05s linear; } #video { transform-origin: center center; transition: transform 0.05s linear; }
.gesture-hud { .gesture-hud {
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 6; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 6;

View File

@@ -1,139 +1,172 @@
/* Framework7 Swipe layout (html[data-layout="f7-swipe"]). /* Framework7 Swipe layout: iOS navigation and grouped media lists, built on
Keeps the classic look; only the Framework7 swipeout / sheet mechanics are the app's existing theme tokens. Every rule stays inside this layout. */
styled here, with the app's own tokens instead of Framework7's stylesheet. */ html[data-layout="f7-swipe"] {
--f7-group-bg: color-mix(in srgb, var(--bg-1) 94%, var(--text) 6%);
--f7-hairline: color-mix(in srgb, var(--line) 65%, transparent);
--f7-glass: color-mix(in srgb, var(--bg-1) 82%, transparent);
--f7-radius: 16px;
color-scheme: dark;
}
html[data-layout="f7-swipe"][data-theme="light"] { color-scheme: light; }
html[data-layout="f7-swipe"] body { background: var(--bg); }
/* ---- Swipeout rows ---- */ /* Translucent navigation and iOS-style section titles. */
[data-layout="f7-swipe"] .cards li.f7s-row { html[data-layout="f7-swipe"] .topbar {
list-style: none;
position: relative; position: relative;
overflow: hidden; z-index: 15;
border-radius: 12px; background: var(--f7-glass);
transform-style: preserve-3d; border-bottom: 1px solid var(--f7-hairline);
touch-action: pan-y; -webkit-backdrop-filter: saturate(180%) blur(22px);
backdrop-filter: saturate(180%) blur(22px);
} }
[data-layout="f7-swipe"] .f7s-row > .swipeout-content { html[data-layout="f7-swipe"] .list-header { padding: 18px 20px 10px; }
position: relative; html[data-layout="f7-swipe"] .list-title-row h2 {
z-index: 10; font: 700 32px/1.12 var(--ui, system-ui, sans-serif);
background: var(--bg); letter-spacing: -.035em;
transition: background 0.16s, border-color 0.16s;
} }
[data-layout="f7-swipe"] .f7s-row.swipeout-active > .swipeout-content, html[data-layout="f7-swipe"] .list-sub { color: var(--text-dim); }
[data-layout="f7-swipe"] .f7s-row.swipeout-opened > .swipeout-content { html[data-layout="f7-swipe"] .list-actions button,
background: var(--bg-2); html[data-layout="f7-swipe"] .list-actions .btn {
border-color: var(--line); min-height: 38px; border-radius: 999px;
} }
[data-layout="f7-swipe"] .f7s-row.swipeout-transitioning > .swipeout-content,
[data-layout="f7-swipe"] .f7s-row.swipeout-transitioning .f7s-act { /* Inset grouped rows: one quiet container, hairlines between items. */
transition-duration: 0.3s; html[data-layout="f7-swipe"] .cards {
transition-property: transform, left, width, background-color; gap: 0;
margin: 4px 16px 28px;
padding: 0;
overflow: auto;
border: 1px solid var(--f7-hairline);
border-radius: var(--f7-radius);
background: var(--f7-group-bg);
}
html[data-layout="f7-swipe"] .cards li.f7s-row {
list-style: none; position: relative; overflow: hidden;
transform-style: preserve-3d; touch-action: pan-y;
}
html[data-layout="f7-swipe"] .f7s-row > .swipeout-content {
position: relative; z-index: 10; background: transparent;
transition: background .16s, border-color .16s;
}
html[data-layout="f7-swipe"] .cards .card {
min-height: 88px; padding: 10px 12px; border: 0;
border-radius: 0; border-bottom: 1px solid var(--f7-hairline);
background: transparent; box-shadow: none;
}
html[data-layout="f7-swipe"] .cards .card:last-child { border-bottom: 0; }
html[data-layout="f7-swipe"] .cards .card:hover,
html[data-layout="f7-swipe"] .cards .card.playing { background: color-mix(in srgb, var(--bg-2) 76%, transparent); }
html[data-layout="f7-swipe"] .cards .card.playing::after { display: none; }
html[data-layout="f7-swipe"] .cards .card-menu,
html[data-layout="f7-swipe"] .cards .card-del {
width: 40px; height: 40px; min-width: 40px; border: 0;
border-radius: 50%; background: transparent; color: var(--text-dim);
}
html[data-layout="f7-swipe"] .f7s-row.swipeout-active > .swipeout-content,
html[data-layout="f7-swipe"] .f7s-row.swipeout-opened > .swipeout-content { background: var(--bg-2); }
html[data-layout="f7-swipe"] .f7s-row.swipeout-transitioning > .swipeout-content,
html[data-layout="f7-swipe"] .f7s-row.swipeout-transitioning .f7s-act {
transition-duration: .3s; transition-property: transform, left, width, background-color;
transition-timing-function: var(--ease, ease); transition-timing-function: var(--ease, ease);
} }
[data-layout="f7-swipe"] .f7s-row .swipeout-actions-left, html[data-layout="f7-swipe"] .f7s-row .swipeout-actions-left,
[data-layout="f7-swipe"] .f7s-row .swipeout-actions-right { html[data-layout="f7-swipe"] .f7s-row .swipeout-actions-right {
position: absolute; position: absolute; top: 0; height: 100%; display: flex; align-items: center;
top: 0; gap: 8px; padding: 0 10px; direction: ltr;
height: 100%;
display: flex;
align-items: center;
gap: 8px;
padding: 0 10px;
direction: ltr;
} }
[data-layout="f7-swipe"] .f7s-row .swipeout-actions-left { left: 0; } html[data-layout="f7-swipe"] .f7s-row .swipeout-actions-left { left: 0; }
[data-layout="f7-swipe"] .f7s-row .swipeout-actions-right { right: 0; } html[data-layout="f7-swipe"] .f7s-row .swipeout-actions-right { right: 0; }
[data-layout="f7-swipe"] .f7s-act { html[data-layout="f7-swipe"] .f7s-act {
position: relative; position: relative; left: 0; transform: scale(0); min-height: 44px;
left: 0; border: 0; border-radius: 999px; padding: 10px 16px; font-family: inherit; font-size: 13px; font-weight: 600;
transform: scale(0); color: var(--text); background: var(--bg-3); cursor: pointer;
border: 1px solid var(--line, transparent); white-space: nowrap; text-decoration: none; user-select: none;
border-radius: 999px;
padding: 10px 16px;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--text);
background: var(--bg-3);
cursor: pointer;
white-space: nowrap;
text-decoration: none;
user-select: none;
}
[data-layout="f7-swipe"] .swipeout-actions-left .f7s-act { background: var(--accent); border-color: var(--accent); color: #fff; }
[data-layout="f7-swipe"] .f7s-act.f7s-danger { background: var(--accent-deep); border-color: var(--accent-deep); color: #fff; }
[data-layout="f7-swipe"] .f7s-row.swipeout-overswipe-active .f7s-danger { box-shadow: 0 0 0 3px var(--accent-glow); }
/* Swipe replaces the hover-only row buttons on touch screens. */
@media (hover: none) {
[data-layout="f7-swipe"] .f7s-row .card-del { display: none; }
} }
html[data-layout="f7-swipe"] .swipeout-actions-left .f7s-act { background: var(--accent); color: var(--on-accent, #fff); }
html[data-layout="f7-swipe"] .f7s-act.f7s-danger { background: var(--danger-bg, var(--accent-deep)); color: var(--danger-text, #fff); }
html[data-layout="f7-swipe"] .f7s-row.swipeout-overswipe-active .f7s-danger { box-shadow: 0 0 0 3px var(--accent-glow); }
@media (hover: none) { html[data-layout="f7-swipe"] .f7s-row .card-del { display: none; } }
/* ---- Up next sheet ---- */ /* Compact segmented glass control used by the video/audio choice. */
.sheet-backdrop { html[data-layout="f7-swipe"] .stg-seg {
position: fixed; padding: 3px; gap: 2px; border-radius: 999px;
inset: 0; background: color-mix(in srgb, var(--bg-1) 78%, transparent);
z-index: 11000; border: 1px solid var(--f7-hairline); backdrop-filter: blur(14px);
background: rgba(0, 0, 0, 0.45);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.3s, visibility 0.3s;
} }
.sheet-backdrop.backdrop-in { opacity: 1; visibility: visible; pointer-events: auto; } html[data-layout="f7-swipe"] .stg-seg button { min-height: 36px; border-radius: 999px; }
.f7s-sheet {
--f7-sheet-transition-duration: 300ms; /* iOS tab bar: translucent, with a restrained active tint. */
position: fixed; html[data-layout="f7-swipe"] .bottom-nav {
left: 0; background: var(--f7-glass); border-top: 1px solid var(--f7-hairline);
right: 0; -webkit-backdrop-filter: saturate(180%) blur(24px);
bottom: 0; backdrop-filter: saturate(180%) blur(24px);
z-index: 12500;
display: none;
height: min(85vh, 720px);
overflow: hidden;
box-sizing: border-box;
border-radius: 18px 18px 0 0;
border: 1px solid var(--line, transparent);
border-bottom: 0;
background: var(--bg-1);
color: var(--text);
box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.45);
transform: translate3d(0, 100%, 0);
transition-property: transform;
transition-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
will-change: transform;
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
} }
.f7s-sheet.modal-in, .f7s-sheet.modal-out { transition-duration: var(--f7-sheet-transition-duration); } html[data-layout="f7-swipe"] .bottom-nav-btn { color: var(--text-dim); }
.f7s-sheet.not-animated { transition-duration: 0s; } html[data-layout="f7-swipe"] .bottom-nav-btn.active {
.f7s-sheet.modal-in { display: block; transform: translate3d(0, 0, 0); } color: var(--accent); background: transparent;
.f7s-sheet.modal-in-breakpoint, .f7s-sheet.modal-in-swipe-step {
display: block;
transform: translate3d(0, var(--f7-sheet-breakpoint, var(--f7-sheet-swipe-step, 0)), 0);
} }
.f7s-sheet.modal-out { transform: translate3d(0, 100%, 0); } html[data-layout="f7-swipe"] .bottom-nav-btn.active .bottom-nav-icon { transform: none; }
.f7s-sheet .sheet-modal-inner { height: 100%; display: flex; flex-direction: column; }
.f7s-grab { flex: none; height: 22px; display: grid; place-items: center; cursor: grab; touch-action: none; } /* The mini-player reads as a floating sheet above the tab bar. */
.f7s-grab::before { content: ''; width: 40px; height: 5px; border-radius: 3px; background: var(--text-dim); opacity: 0.6; } html[data-layout="f7-swipe"] .mini-bar {
.f7s-sheet-head { flex: none; padding: 0 18px 10px; font-weight: 700; font-size: 15px; touch-action: none; } left: 10px; right: 10px; bottom: calc(64px + env(safe-area-inset-bottom) + 8px);
.f7s-sheet-list { flex: 1; overflow-y: auto; margin: 0; padding: 0 10px 16px; list-style: none; } overflow: hidden; border: 1px solid var(--f7-hairline); border-radius: 16px;
.f7s-up { display: flex; gap: 12px; align-items: center; padding: 8px; border-radius: 12px; cursor: pointer; } background: var(--f7-glass); box-shadow: 0 8px 28px rgb(0 0 0 / .18);
.f7s-up:active, .f7s-up:hover { background: var(--bg-2); } -webkit-backdrop-filter: saturate(180%) blur(22px);
.f7s-up img { width: 88px; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 8px; flex: none; background: var(--bg-3); } backdrop-filter: saturate(180%) blur(22px);
.f7s-up-info { min-width: 0; } }
.f7s-up-title { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } html[data-layout="f7-swipe"] .mini-bar-inner { border: 0; background: transparent; }
.f7s-up-ch { font-size: 12px; color: var(--text-dim); margin-top: 4px; } html[data-layout="f7-swipe"] .mini-btn { border-radius: 50%; }
.f7s-empty { color: var(--text-dim); padding: 12px 8px; margin: 0; } html[data-layout="f7-swipe"] .mini-progress { height: 2px; }
/* ---- Up next sheet ---- */
html[data-layout="f7-swipe"] .sheet-backdrop {
position: fixed; inset: 0; z-index: 11000; background: rgb(0 0 0 / .42);
opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .3s, visibility .3s;
}
html[data-layout="f7-swipe"] .sheet-backdrop.backdrop-in { opacity: 1; visibility: visible; pointer-events: auto; }
html[data-layout="f7-swipe"] .f7s-sheet {
--f7-sheet-transition-duration: 300ms;
position: fixed; left: 0; right: 0; bottom: 0; z-index: 12500; display: none;
height: min(85dvh, 720px); overflow: hidden; box-sizing: border-box;
border-radius: 18px 18px 0 0; border: 1px solid var(--f7-hairline); border-bottom: 0;
background: var(--bg-1); color: var(--text); box-shadow: 0 -12px 40px rgb(0 0 0 / .28);
transform: translate3d(0, 100%, 0); transition: transform cubic-bezier(.2,.8,.2,1);
padding-bottom: env(safe-area-inset-bottom);
}
html[data-layout="f7-swipe"] .f7s-sheet.modal-in, html[data-layout="f7-swipe"] .f7s-sheet.modal-out { transition-duration: var(--f7-sheet-transition-duration); }
html[data-layout="f7-swipe"] .f7s-sheet.not-animated { transition-duration: 0s; }
html[data-layout="f7-swipe"] .f7s-sheet.modal-in { display: block; transform: translate3d(0,0,0); }
html[data-layout="f7-swipe"] .f7s-sheet.modal-in-breakpoint, html[data-layout="f7-swipe"] .f7s-sheet.modal-in-swipe-step { display: block; transform: translate3d(0,var(--f7-sheet-breakpoint,var(--f7-sheet-swipe-step,0)),0); }
html[data-layout="f7-swipe"] .f7s-sheet.modal-out { transform: translate3d(0,100%,0); }
html[data-layout="f7-swipe"] .f7s-sheet .sheet-modal-inner { height: 100%; display: flex; flex-direction: column; }
html[data-layout="f7-swipe"] .f7s-grab { flex: none; height: 22px; display: grid; place-items: center; cursor: grab; touch-action: none; }
html[data-layout="f7-swipe"] .f7s-grab::before { content: ''; width: 36px; height: 5px; border-radius: 3px; background: var(--text-dim); opacity: .55; }
html[data-layout="f7-swipe"] .f7s-sheet-head { flex: none; padding: 0 18px 10px; font-weight: 700; font-size: 17px; touch-action: none; }
html[data-layout="f7-swipe"] .f7s-sheet-list { flex: 1; overflow-y: auto; margin: 0; padding: 0 12px 16px; list-style: none; }
html[data-layout="f7-swipe"] .f7s-up { display: flex; gap: 12px; align-items: center; padding: 10px 8px; border-bottom: 1px solid var(--f7-hairline); cursor: pointer; }
html[data-layout="f7-swipe"] .f7s-up:active, html[data-layout="f7-swipe"] .f7s-up:hover { background: var(--bg-2); }
html[data-layout="f7-swipe"] .f7s-up img { width: 88px; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 8px; flex: none; background: var(--bg-3); }
html[data-layout="f7-swipe"] .f7s-up-info { min-width: 0; }
html[data-layout="f7-swipe"] .f7s-up-title { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
html[data-layout="f7-swipe"] .f7s-up-ch { font-size: 12px; color: var(--text-dim); margin-top: 4px; }
html[data-layout="f7-swipe"] .f7s-empty { color: var(--text-dim); padding: 12px 8px; margin: 0; }
/* A grab hint on the mini bar: swipe up for Up next, sideways to skip. */ /* A grab hint on the mini bar: swipe up for Up next, sideways to skip. */
[data-layout="f7-swipe"] #miniBar { touch-action: none; } html[data-layout="f7-swipe"] #miniBar { touch-action: none; }
[data-layout="f7-swipe"] #miniBar .mini-bar-inner { position: relative; } html[data-layout="f7-swipe"] #miniBar .mini-bar-inner { position: relative; }
[data-layout="f7-swipe"] #miniBar .mini-bar-inner::before { html[data-layout="f7-swipe"] #miniBar .mini-bar-inner::before {
content: ''; content: ''; position: absolute; top: 3px; left: 50%; width: 32px; height: 4px;
position: absolute; margin-left: -16px; border-radius: 2px; background: var(--text-dim); opacity: .5;
top: 3px; }
left: 50%; @media (max-width: 600px) {
width: 32px; html[data-layout="f7-swipe"] .list-header { padding: 16px 16px 8px; }
height: 4px; html[data-layout="f7-swipe"] .list-title-row h2 { font-size: 30px; }
margin-left: -16px; html[data-layout="f7-swipe"] .cards { margin: 4px 12px 22px; }
border-radius: 2px; }
background: var(--text-dim); @media (max-width: 860px) and (orientation: portrait) {
opacity: 0.5; html[data-layout="f7-swipe"] .body { padding-bottom: calc(64px + env(safe-area-inset-bottom)); box-sizing: border-box; }
html[data-layout="f7-swipe"] .bottom-nav { display: flex; height: calc(64px + env(safe-area-inset-bottom)); }
html[data-layout="f7-swipe"] .bottom-nav-btn { min-height: 64px; }
} }