diff --git a/webui/static/video/video-discover.js b/webui/static/video/video-discover.js
index e5ddfff3..53a815b4 100644
--- a/webui/static/video/video-discover.js
+++ b/webui/static/video/video-discover.js
@@ -402,6 +402,7 @@
'';
}).join('');
host.insertAdjacentHTML('afterbegin', html);
+ staggerWithin(host);
hydrateGet(host);
})
.catch(function () { /* personalization is best-effort */ });
@@ -429,6 +430,7 @@
'';
}).join('');
host.insertAdjacentHTML('afterbegin', html);
+ staggerWithin(host);
hydrateGet(host);
})
.catch(function () { /* gaps are best-effort */ });
@@ -453,6 +455,7 @@
'
' + items.map(card).join('') + '
' +
'';
host.insertAdjacentHTML('afterbegin', html);
+ staggerWithin(host);
hydrateGet(host);
})
.catch(function () { /* best-effort */ });
@@ -504,6 +507,22 @@
var h = $('[data-vdsc-hideowned]');
return !!(h && h.checked);
}
+ // Tag each card with its index so the CSS cascade reveals them one-by-one (not all at once).
+ // Capped so a long rail's tail doesn't lag too far behind the head.
+ function stagger(rail) {
+ if (!rail) return;
+ var c = rail.children;
+ for (var i = 0; i < c.length; i++) c[i].style.setProperty('--i', i < 14 ? i : 14);
+ }
+ // Stagger every not-yet-tagged filled rail under a host (used by the prepended
+ // personalized rows, which render their cards inline rather than via fillShelf).
+ function staggerWithin(host) {
+ if (!host) return;
+ var rails = host.querySelectorAll('.vdsc-shelf--in .vdsc-rail');
+ for (var i = 0; i < rails.length; i++) {
+ if (!rails[i].getAttribute('data-stg')) { rails[i].setAttribute('data-stg', '1'); stagger(rails[i]); }
+ }
+ }
function fillShelf(shelf) {
if (!shelf || shelf.getAttribute('data-vdsc-loaded')) return;
shelf.setAttribute('data-vdsc-loaded', '1');
@@ -515,8 +534,8 @@
.then(function (d) {
var items = (d && d.items) || [];
if (!items.length) { shelf.remove(); return; } // drop empty shelves
- if (rail) { rail.innerHTML = items.map(card).join(''); hydrateGet(rail); }
- shelf.classList.add('vdsc-shelf--in'); // reveal
+ if (rail) { rail.innerHTML = items.map(card).join(''); stagger(rail); hydrateGet(rail); }
+ shelf.classList.add('vdsc-shelf--in'); // reveal (cards cascade via --i)
})
.catch(function () { shelf.remove(); });
}
diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css
index e3889fb1..9f5c0c47 100644
--- a/webui/static/video/video-side.css
+++ b/webui/static/video/video-side.css
@@ -2449,15 +2449,19 @@ body[data-side="video"] #soulsync-toggle { display: none; }
.vdsc-ig-empty-ic { font-size: 40px; margin-bottom: 10px; }
.vdsc-ig-empty-sub { font-size: 12.5px; color: rgba(255, 255, 255, 0.38); margin-top: 6px; }
-/* rails: a gentle fade-in when filled + per-title hue on hover */
-.vdsc-shelf--in .vdsc-rail { animation: vdscFade 0.45s ease both; }
-@keyframes vdscFade { from { opacity: 0.35; } to { opacity: 1; } }
+/* rails: cards cascade in (staggered by --i) when the row fills, instead of one block flash.
+ `backwards` fill holds the hidden start-state during the delay, then releases so :hover works. */
+.vdsc-shelf--in .vdsc-rail .vsr-card {
+ animation: vdscCardIn 0.42s cubic-bezier(0.22, 0.61, 0.36, 1) backwards;
+ animation-delay: calc(var(--i, 0) * 40ms);
+}
+@keyframes vdscCardIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
.vdsc-rail .vsr-card, .vdsc-grid .vsr-card { transition: transform 0.2s ease, box-shadow 0.2s ease; }
.vdsc-rail .vsr-card:hover, .vdsc-grid .vsr-card:hover {
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.5), 0 0 26px -6px hsla(var(--vgm-h, 230), 80%, 58%, 0.5); }
@media (prefers-reduced-motion: reduce) {
- .vdsc-shelf--in .vdsc-rail { animation: none; }
+ .vdsc-shelf--in .vdsc-rail .vsr-card { animation: none; }
.vdsc-amb { transition: none; }
}