video Downloads page: actually match the music page (.adl-* layout, full width)

Scrapped the bespoke centered .vdpg- design and reused the music downloads page's
.adl-* classes for real parity: full-width .adl-layout, the segmented .adl-filter-pills,
the title with the accent download glyph, and the compact .adl-row (44px art tile +
.adl-row-info title/meta/error + .adl-row-status dot+label) — driven by the video JS
via data-vdpg-* hooks (no #id clashes with the music page). Per-row cancel reuses the
music hover-reveal .adl-row-cancel; retry mirrors it in accent. Kept the smooth
in-place patching (slim progress line for active rows). Removed the old .vdpg-* block;
balance clean.
This commit is contained in:
BoulderBadgeDad 2026-06-19 15:47:02 -07:00
parent d25be2c3c6
commit 1c3f255a66
3 changed files with 124 additions and 195 deletions

View file

@ -1274,29 +1274,35 @@
</div>
</section>
<!-- Downloads page — every grab from the video side lives here (live status).
Isolated: built by video/video-downloads-page.js, styled by .vdpg-*. -->
Reuses the music downloads page's .adl-* layout/look for parity;
driven by video/video-downloads-page.js via data-vdpg-* hooks. -->
<section class="video-subpage" data-video-subpage="video-downloads" hidden>
<div class="vdpg-page">
<div class="vdpg-head">
<div class="vdpg-head-glow" aria-hidden="true"></div>
<div class="vdpg-head-row">
<div class="vdpg-head-titles">
<h1 class="vdpg-title">Downloads</h1>
<p class="vdpg-sub" data-vdpg-sub>Everything you've grabbed from the video side</p>
<div class="vdpg-wrap">
<div class="adl-layout">
<div class="adl-main">
<div class="adl-container">
<div class="adl-header">
<h2 class="adl-title"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> Downloads</h2>
<div class="adl-controls">
<div class="adl-filter-pills" data-vdpg-pills>
<button class="adl-pill active" type="button" data-vdpg-filter="all">All</button>
<button class="adl-pill" type="button" data-vdpg-filter="active">Active</button>
<button class="adl-pill" type="button" data-vdpg-filter="completed">Completed</button>
<button class="adl-pill" type="button" data-vdpg-filter="failed">Failed</button>
</div>
<div class="adl-controls-right">
<span class="adl-count" data-vdpg-sub></span>
<button class="adl-cancel-all-btn" type="button" data-vdpg-cancel-all style="display:none">Cancel All</button>
<button class="adl-clear-btn" type="button" data-vdpg-clear style="display:none">Clear Finished</button>
</div>
</div>
</div>
<div class="adl-list" data-vdpg-list>
<div class="adl-empty" data-vdpg-empty>No downloads yet. Hit Grab on a search result and it'll show up here.</div>
</div>
</div>
<div class="vdpg-head-actions">
<button class="vdpg-btn vdpg-btn--danger" type="button" data-vdpg-cancel-all hidden>Cancel all</button>
<button class="vdpg-btn" type="button" data-vdpg-clear hidden>Clear finished</button>
</div>
</div>
<div class="vdpg-pills" data-vdpg-pills>
<button class="vdpg-pill-f vdpg-pill-f--on" type="button" data-vdpg-filter="all">All <span class="vdpg-pill-n" data-vdpg-n="all">0</span></button>
<button class="vdpg-pill-f" type="button" data-vdpg-filter="active">Active <span class="vdpg-pill-n" data-vdpg-n="active">0</span></button>
<button class="vdpg-pill-f" type="button" data-vdpg-filter="completed">Completed <span class="vdpg-pill-n" data-vdpg-n="completed">0</span></button>
<button class="vdpg-pill-f" type="button" data-vdpg-filter="failed">Failed <span class="vdpg-pill-n" data-vdpg-n="failed">0</span></button>
</div>
</div>
<div class="vdpg-list" data-vdpg-list></div>
</div>
</section>
<!-- TV-show detail (drill-in from a show card; not a nav page).

View file

@ -1,10 +1,10 @@
/*
* SoulSync Video Downloads page.
*
* Every grab from the video side lands here. Filter tabs (All / Active / Completed /
* Failed), per-row cancel + retry, cancel-all and clear-finished the depth of the
* music downloads page. Cards are created ONCE and patched in place (no innerHTML
* churn) so progress bars glide and nothing re-animates each tick.
* Reuses the music downloads page's .adl-* layout + look (full-width, segmented
* filter pills, compact rows, status dots) for visual parity, driven by video data
* via data-vdpg-* hooks. Filter tabs, per-row cancel + retry, cancel-all, clear.
* Rows are created ONCE and patched in place so progress glides and nothing blinks.
*/
(function () {
'use strict';
@ -14,7 +14,7 @@
var URL_CANCEL = '/api/video/downloads/cancel';
var URL_RETRY = '/api/video/downloads/retry';
var _timer = null, _wired = false, _filter = 'all';
var _cards = {}; // id -> card element (kept across polls for in-place updates)
var _cards = {};
function esc(s) {
return String(s == null ? '' : s)
@ -31,12 +31,13 @@
}
var KIND_ICON = { movie: '🎬', show: '📺', episode: '📺', season: '📺', series: '📺', youtube: '▶️' };
// status -> { label, cls } where cls is the music .adl-row-/.adl-status-dot class
var STATUS = {
downloading: { label: 'Downloading', st: 'dl' },
queued: { label: 'Queued', st: 'q' },
completed: { label: 'Completed', st: 'ok' },
failed: { label: 'Failed', st: 'fail' },
cancelled: { label: 'Cancelled', st: 'cancel' }
downloading: { label: 'Downloading', cls: 'active' },
queued: { label: 'Queued', cls: 'queued' },
completed: { label: 'Completed', cls: 'completed' },
failed: { label: 'Failed', cls: 'failed' },
cancelled: { label: 'Cancelled', cls: 'cancelled' }
};
function isActive(s) { return s === 'downloading' || s === 'queued'; }
function isFail(s) { return s === 'failed' || s === 'cancelled'; }
@ -44,79 +45,79 @@
return _filter === 'all' || (_filter === 'active' && isActive(s)) ||
(_filter === 'completed' && s === 'completed') || (_filter === 'failed' && isFail(s));
}
function fmtSize(bytes) {
var gb = (bytes || 0) / (1024 * 1024 * 1024);
return gb >= 0.1 ? (Math.round(gb * 10) / 10) + ' GB' : Math.round((bytes || 0) / (1024 * 1024)) + ' MB';
}
function ago(ts) {
if (!ts) return '';
var t = Date.parse(String(ts).replace(' ', 'T') + 'Z');
if (isNaN(t)) return '';
var s = Math.max(0, Math.round((Date.now() - t) / 1000));
if (s < 60) return s + 's ago';
if (s < 3600) return Math.round(s / 60) + 'm ago';
if (s < 86400) return Math.round(s / 3600) + 'h ago';
return Math.round(s / 86400) + 'd ago';
}
var X_SVG = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
var R_SVG = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>';
function makeCard(d) {
var el = document.createElement('div');
el.className = 'vdpg-item';
el.className = 'adl-row';
el.setAttribute('data-dl-id', d.id);
el.innerHTML =
'<div class="vdpg-ic" data-f="ic"></div>' +
'<div class="vdpg-body">' +
'<div class="vdpg-row1"><span class="vdpg-name" data-f="name"></span>' +
'<span class="vdpg-pill" data-f="pill"></span></div>' +
'<div class="vdpg-rel" data-f="rel"></div>' +
'<div class="vdpg-bar" data-f="bar"><div class="vdpg-bar-fill" data-f="fill"></div></div>' +
'<div class="vdpg-meta" data-f="meta"></div>' +
'<div class="adl-row-art adl-row-art-empty vdpg-art" data-f="ic"></div>' +
'<div class="adl-row-info">' +
'<div class="adl-row-title" data-f="name"></div>' +
'<div class="adl-row-meta" data-f="meta"></div>' +
'<div class="adl-row-error" data-f="error" style="display:none"></div>' +
'<div class="vdpg-prog" data-f="bar" style="display:none"><div class="vdpg-prog-fill" data-f="fill"></div></div>' +
'</div>' +
'<div class="vdpg-actions" data-f="actions"></div>';
'<div class="adl-row-status" data-f="status"><span class="adl-status-dot" data-f="dot"></span><span data-f="label"></span></div>' +
'<div class="vdpg-rowact" data-f="actions"></div>';
return el;
}
function patchCard(el, d) {
var info = STATUS[d.status] || STATUS.downloading;
var active = isActive(d.status);
var cls = info.cls, active = isActive(d.status);
var pct = Math.max(0, Math.min(100, d.progress || 0));
var q = function (f) { return el.querySelector('[data-f="' + f + '"]'); };
if (el.getAttribute('data-st') !== info.st) el.setAttribute('data-st', info.st);
var want = 'adl-row adl-row-' + cls;
if (el.className !== want) el.className = want;
var ic = q('ic'); var icon = KIND_ICON[(d.kind || '').toLowerCase()] || '🎬';
if (ic.textContent !== icon) ic.textContent = icon;
var name = d.title || d.release_title || 'Download';
var nm = q('name'); if (nm.textContent !== name) nm.textContent = name;
var pill = q('pill');
if (pill.textContent !== info.label) pill.textContent = info.label;
if (pill.getAttribute('data-st') !== info.st) pill.setAttribute('data-st', info.st);
var rel = q('rel');
var relTxt = (d.release_title && d.release_title !== name) ? d.release_title : '';
if (rel.textContent !== relTxt) rel.textContent = relTxt;
// one compact meta line (music-style), context-dependent
var meta;
if (d.status === 'completed' && d.dest_path) meta = '→ ' + d.dest_path;
else {
var bits = [];
if (d.release_title && d.release_title !== name) bits.push(d.release_title);
else bits.push(fmtSize(d.size_bytes));
if (d.username) bits.push('👤 ' + d.username);
if (active) bits.push(Math.round(pct) + '%');
meta = bits.join(' · ');
}
var mt = q('meta');
if (mt.textContent !== meta) mt.textContent = meta;
mt.classList.toggle('vdpg-dest', d.status === 'completed' && !!d.dest_path);
q('bar').style.display = active ? '' : 'none';
var err = q('error');
var errTxt = isFail(d.status) && d.error ? d.error : '';
if (err.textContent !== errTxt) err.textContent = errTxt;
err.style.display = errTxt ? '' : 'none';
var bar = q('bar');
bar.style.display = active ? '' : 'none';
if (active) q('fill').style.width = pct + '%';
var meta = [fmtSize(d.size_bytes)];
if (d.username) meta.push('👤 ' + d.username);
if (active) meta.push(Math.round(pct) + '%');
if (d.status === 'completed' && d.dest_path) meta.push('→ ' + d.dest_path);
else if (isFail(d.status) && d.error) meta.push(d.error);
else if (d.created_at) meta.push(ago(d.created_at));
var html = meta.map(function (m, i) {
var cls = (i === 0) ? 'vdpg-m' : (d.status === 'completed' && /^→/.test(m)) ? 'vdpg-m vdpg-dest'
: (isFail(d.status) && m === d.error) ? 'vdpg-m vdpg-err' : 'vdpg-m';
return '<span class="' + cls + '">' + esc(m) + '</span>';
}).join('');
var mt = q('meta'); if (mt.innerHTML !== html) mt.innerHTML = html;
var st = q('status'); var stWant = 'adl-row-status ' + cls;
if (st.className !== stWant) st.className = stWant;
var dot = q('dot'); var dotWant = 'adl-status-dot ' + cls;
if (dot.className !== dotWant) dot.className = dotWant;
var lab = q('label'); if (lab.textContent !== info.label) lab.textContent = info.label;
var act = q('actions');
var actHTML = active
? '<button class="vdpg-act vdpg-act--cancel" type="button" data-vdpg-cancel="' + d.id + '" title="Cancel"></button>'
? '<button class="adl-row-cancel" type="button" data-vdpg-cancel="' + d.id + '" title="Cancel">' + X_SVG + '</button>'
: isFail(d.status)
? '<button class="vdpg-act vdpg-act--retry" type="button" data-vdpg-retry="' + d.id + '" title="Retry"></button>'
? '<button class="vdpg-row-retry" type="button" data-vdpg-retry="' + d.id + '" title="Retry">' + R_SVG + '</button>'
: '';
if (act.innerHTML !== actHTML) act.innerHTML = actHTML;
}
@ -124,6 +125,7 @@
function render(list) {
var host = document.querySelector('[data-vdpg-list]'); if (!host) return;
list = list || [];
var empty = host.querySelector('[data-vdpg-empty]');
var counts = { all: list.length, active: 0, completed: 0, failed: 0 };
list.forEach(function (d) {
@ -131,63 +133,51 @@
else if (d.status === 'completed') counts.completed++;
else counts.failed++;
});
['all', 'active', 'completed', 'failed'].forEach(function (k) {
var n = document.querySelector('[data-vdpg-n="' + k + '"]'); if (n) n.textContent = counts[k];
});
var cancelAll = document.querySelector('[data-vdpg-cancel-all]'); if (cancelAll) cancelAll.hidden = counts.active === 0;
var clearBtn = document.querySelector('[data-vdpg-clear]'); if (clearBtn) clearBtn.hidden = (counts.completed + counts.failed) === 0;
var cancelAll = document.querySelector('[data-vdpg-cancel-all]'); if (cancelAll) cancelAll.style.display = counts.active ? '' : 'none';
var clearBtn = document.querySelector('[data-vdpg-clear]'); if (clearBtn) clearBtn.style.display = (counts.completed + counts.failed) ? '' : 'none';
var sub = document.querySelector('[data-vdpg-sub]');
if (sub) sub.textContent = list.length ? (counts.active + ' active · ' + counts.completed + ' done · ' + counts.failed + ' failed')
: "Everything you've grabbed from the video side";
if (!list.length) {
_cards = {};
host.innerHTML = '<div class="vdpg-empty"><div class="vdpg-empty-ic">⤓</div>' +
'<div class="vdpg-empty-t">No downloads yet</div>' +
'<div class="vdpg-empty-s">Hit Grab on a search result and it\'ll show up here.</div></div>';
return;
if (sub) {
var parts = [];
if (counts.active) parts.push(counts.active + ' active');
if (counts.completed) parts.push(counts.completed + ' done');
if (counts.failed) parts.push(counts.failed + ' failed');
sub.textContent = parts.join(' · ');
}
var empty = host.querySelector('.vdpg-empty'); if (empty) empty.remove();
var seen = {}, shown = 0;
list.forEach(function (d, i) {
list.forEach(function (d) {
seen[d.id] = true;
var el = _cards[d.id] || (_cards[d.id] = makeCard(d));
patchCard(el, d);
var vis = matches(d.status);
el.style.display = vis ? '' : 'none';
if (vis) shown++;
var atPos = host.children[i];
if (atPos !== el) host.insertBefore(el, atPos || null);
host.appendChild(el); // keep order = server order (active first); no re-anim
});
Object.keys(_cards).forEach(function (id) {
if (!seen[id]) { var el = _cards[id]; if (el && el.parentNode) el.parentNode.removeChild(el); delete _cards[id]; }
if (!seen[id]) { var e = _cards[id]; if (e && e.parentNode) e.parentNode.removeChild(e); delete _cards[id]; }
});
var fe = host.querySelector('.vdpg-filter-empty');
if (shown === 0) {
if (!fe) { fe = document.createElement('div'); fe.className = 'vdpg-filter-empty'; host.appendChild(fe); }
fe.textContent = 'Nothing ' + (_filter === 'all' ? 'here' : _filter) + ' right now.';
} else if (fe) { fe.remove(); }
if (empty) {
host.appendChild(empty); // keep the empty element last
empty.style.display = shown === 0 ? '' : 'none';
empty.textContent = !list.length ? "No downloads yet. Hit Grab on a search result and it'll show up here."
: 'Nothing ' + (_filter === 'all' ? 'here' : _filter) + ' right now.';
}
}
function setFilter(f) {
_filter = f;
Array.prototype.forEach.call(document.querySelectorAll('[data-vdpg-filter]'), function (b) {
b.classList.toggle('vdpg-pill-f--on', b.getAttribute('data-vdpg-filter') === f);
b.classList.toggle('active', b.getAttribute('data-vdpg-filter') === f);
});
getJSON(URL_ACTIVE).then(function (d) { if (d) render(d.downloads || []); });
}
function anyActive() { return !!document.querySelector('.vdpg-item[data-st="dl"], .vdpg-item[data-st="q"]'); }
function anyActive() { return !!document.querySelector('.adl-row.adl-row-active, .adl-row.adl-row-queued'); }
function poll() {
getJSON(URL_ACTIVE).then(function (d) { if (d) render(d.downloads || []); schedule(); });
}
function schedule() {
if (_timer) clearTimeout(_timer);
_timer = setTimeout(poll, anyActive() ? 1500 : 6000);
}
function poll() { getJSON(URL_ACTIVE).then(function (d) { if (d) render(d.downloads || []); schedule(); }); }
function schedule() { if (_timer) clearTimeout(_timer); _timer = setTimeout(poll, anyActive() ? 1500 : 6000); }
function start() { wire(); if (_timer) clearTimeout(_timer); poll(); }
function stop() { if (_timer) { clearTimeout(_timer); _timer = null; } }
@ -212,7 +202,7 @@
var list = document.querySelector('[data-vdpg-list]');
if (list) list.addEventListener('click', function (e) {
var c = e.target.closest('[data-vdpg-cancel]');
if (c) { c.disabled = true; postJSON(URL_CANCEL, { id: +c.getAttribute('data-vdpg-cancel') }).then(function () { poll(); }); return; }
if (c) { c.disabled = true; c.classList.add('adl-row-cancel-pending'); postJSON(URL_CANCEL, { id: +c.getAttribute('data-vdpg-cancel') }).then(function () { poll(); }); return; }
var r = e.target.closest('[data-vdpg-retry]');
if (r) { r.disabled = true; postJSON(URL_RETRY, { id: +r.getAttribute('data-vdpg-retry') }).then(function (res) {
if (res && res.ok) toast('Retrying', 'info'); else toast((res && res.error) || 'Retry failed', 'error'); poll(); }); }

View file

@ -3316,92 +3316,25 @@ body[data-side="video"] #soulsync-toggle { display: none; }
.vdl-src-block .vdl-results { margin: 8px 0 2px; }
@media (prefers-reduced-motion: reduce) { .vdl-res, .vdl-res-spin { animation: none !important; } }
/* ── Downloads page (.vdpg-*) ─────────────────────────────────────────────── */
.vdpg-page { max-width: 1000px; margin: 0 auto; padding: 8px 4px 40px; }
.vdpg-head { position: relative; margin-bottom: 22px; padding: 6px 2px; overflow: hidden; }
.vdpg-head-glow { position: absolute; inset: -40% -10% auto -10%; height: 200px; pointer-events: none;
background: radial-gradient(60% 100% at 30% 0%, rgba(var(--accent-rgb), 0.18), transparent 70%); }
.vdpg-head-row { position: relative; display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; }
.vdpg-title { font-size: 34px; font-weight: 900; letter-spacing: -0.03em; margin: 0; color: #fff; }
.vdpg-sub { margin: 6px 0 0; font-size: 13.5px; font-weight: 600; color: rgba(255, 255, 255, 0.5); }
.vdpg-head-actions { flex-shrink: 0; display: flex; gap: 9px; }
.vdpg-btn { padding: 9px 15px; border-radius: 10px; cursor: pointer; font-size: 12.5px; font-weight: 800;
background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.14); color: rgba(255, 255, 255, 0.85);
transition: all 0.15s ease; }
.vdpg-btn:hover { background: rgba(255, 255, 255, 0.12); border-color: rgba(255, 255, 255, 0.26); color: #fff; }
.vdpg-btn--danger:hover { background: rgba(239, 68, 68, 0.16); border-color: rgba(239, 68, 68, 0.45); color: #fecaca; }
.vdpg-btn[hidden] { display: none; }
/* filter tabs (music-style pills) */
.vdpg-pills { position: relative; display: flex; gap: 8px; margin-top: 18px; flex-wrap: wrap; }
.vdpg-pill-f { display: inline-flex; align-items: center; gap: 7px; padding: 8px 14px; border-radius: 999px; cursor: pointer;
font-size: 12.5px; font-weight: 800; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.6); transition: all 0.15s ease; }
.vdpg-pill-f:hover { color: #fff; border-color: rgba(255, 255, 255, 0.22); }
.vdpg-pill-f--on { background: rgba(var(--accent-rgb), 0.18); border-color: rgba(var(--accent-rgb), 0.5); color: #fff; }
.vdpg-pill-n { font: 800 11px/1 'JetBrains Mono', ui-monospace, monospace; padding: 2px 6px; border-radius: 999px;
background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.75); }
.vdpg-pill-f--on .vdpg-pill-n { background: rgba(var(--accent-rgb), 0.3); color: #fff; }
.vdpg-list { display: flex; flex-direction: column; gap: 11px; }
.vdpg-filter-empty { text-align: center; padding: 40px 20px; font-size: 13.5px; color: rgba(255, 255, 255, 0.4); }
/* per-row actions */
.vdpg-actions { flex-shrink: 0; display: flex; align-items: center; gap: 7px; }
.vdpg-act { width: 34px; height: 34px; border-radius: 10px; cursor: pointer; font-size: 15px; font-weight: 900;
display: grid; place-items: center; background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.14);
color: rgba(255, 255, 255, 0.7); transition: all 0.15s ease; }
.vdpg-act:disabled { opacity: 0.5; cursor: default; }
.vdpg-act--cancel:hover { background: rgba(239, 68, 68, 0.18); border-color: rgba(239, 68, 68, 0.5); color: #fca5a5; }
.vdpg-act--retry:hover { background: rgba(var(--accent-rgb), 0.22); border-color: rgba(var(--accent-rgb), 0.5); color: #fff; transform: rotate(-90deg); }
/* card is created ONCE then patched in place the entrance animation plays a single
time on mount, never on a poll (that was the old 'blink'). */
.vdpg-item { display: flex; gap: 15px; padding: 15px 17px; border-radius: 16px; align-items: center;
background: rgba(255, 255, 255, 0.035); border: 1px solid rgba(255, 255, 255, 0.08); border-left: 3px solid transparent;
animation: vdlRise 0.45s both;
transition: border-color 0.3s ease, background 0.25s ease, transform 0.18s ease; }
.vdpg-item:hover { transform: translateY(-2px); border-color: rgba(255, 255, 255, 0.16); background: rgba(255, 255, 255, 0.055); }
.vdpg-item[data-st="dl"] { border-left-color: rgb(var(--accent-rgb));
background: linear-gradient(100deg, rgba(var(--accent-rgb), 0.06), rgba(255, 255, 255, 0.03) 55%); }
.vdpg-item[data-st="ok"] { border-left-color: #6cd391; }
.vdpg-item[data-st="fail"] { border-left-color: #ef4444; }
.vdpg-item[data-st="cancel"] { border-left-color: rgba(255, 255, 255, 0.35); }
.vdpg-item[data-st="cancel"], .vdpg-item[data-st="fail"] { opacity: 0.82; }
.vdpg-item[data-st="q"] { border-left-color: rgba(255, 255, 255, 0.3); }
.vdpg-ic { flex-shrink: 0; width: 50px; height: 50px; border-radius: 13px; display: grid; place-items: center;
font-size: 24px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.08); }
.vdpg-item[data-st="dl"] .vdpg-ic { background: rgba(var(--accent-rgb), 0.12); border-color: rgba(var(--accent-rgb), 0.3); }
.vdpg-body { flex: 1; min-width: 0; }
.vdpg-row1 { display: flex; align-items: center; gap: 10px; }
.vdpg-name { font-size: 15.5px; font-weight: 800; color: #fff; letter-spacing: -0.01em;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vdpg-pill { flex-shrink: 0; font-size: 10px; font-weight: 900; letter-spacing: 0.04em; text-transform: uppercase;
padding: 3px 9px; border-radius: 999px; background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.6);
transition: background 0.25s ease, color 0.25s ease; }
.vdpg-pill[data-st="dl"] { background: rgba(var(--accent-rgb), 0.18); color: rgb(var(--accent-rgb)); animation: vdpgPulse 1.6s ease-in-out infinite; }
.vdpg-pill[data-st="ok"] { background: rgba(108, 211, 145, 0.18); color: #8fe7af; }
.vdpg-pill[data-st="fail"] { background: rgba(239, 68, 68, 0.18); color: #fca5a5; }
.vdpg-pill[data-st="cancel"] { background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.65); }
.vdpg-rel { font: 500 11px/1.3 'JetBrains Mono', ui-monospace, monospace; color: rgba(255, 255, 255, 0.4);
margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vdpg-rel:empty { display: none; }
.vdpg-bar { margin-top: 10px; height: 7px; border-radius: 999px; background: rgba(255, 255, 255, 0.1); overflow: hidden; }
.vdpg-bar-fill { height: 100%; width: 0; border-radius: 999px; transition: width 1.4s cubic-bezier(0.3, 0.8, 0.3, 1);
background: linear-gradient(90deg, rgba(var(--accent-rgb), 0.7), rgb(var(--accent-rgb))); position: relative; }
.vdpg-bar-fill::after { content: ''; position: absolute; inset: 0; border-radius: 999px;
background: linear-gradient(100deg, transparent 30%, rgba(255, 255, 255, 0.35) 50%, transparent 70%);
animation: vdpgShimmer 1.6s linear infinite; }
.vdpg-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 13px; margin-top: 9px; font-size: 11.5px;
font-weight: 600; color: rgba(255, 255, 255, 0.55); }
.vdpg-m { display: inline-flex; align-items: center; }
.vdpg-dest { color: #8fe7af; font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 11px;
max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vdpg-err { color: #fca5a5; }
.vdpg-empty { text-align: center; padding: 70px 20px; color: rgba(255, 255, 255, 0.45); animation: vdlRise 0.5s both; }
.vdpg-empty-ic { font-size: 46px; opacity: 0.5; }
.vdpg-empty-t { font-size: 17px; font-weight: 800; color: rgba(255, 255, 255, 0.7); margin-top: 12px; }
.vdpg-empty-s { font-size: 13px; margin-top: 6px; }
@keyframes vdpgPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
@keyframes vdpgShimmer { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) {
.vdpg-item, .vdpg-pill[data-st="dl"], .vdpg-bar-fill::after { animation: none !important; }
.vdpg-bar-fill { transition: none; }
}
/* ── Downloads page — reuses the music .adl-* layout; these are the video-only bits ── */
.vdpg-wrap { padding: 4px 0 40px; }
.adl-controls-right { display: flex; align-items: center; gap: 10px; }
.vdpg-art { font-size: 22px; } /* emoji inside the .adl-row-art tile */
.vdpg-rowact { flex-shrink: 0; display: flex; align-items: center; }
/* slim progress line inside the row (music rows have none; video files are big/slow) */
.vdpg-prog { margin-top: 7px; height: 4px; border-radius: 999px; background: rgba(255,255,255,0.08); overflow: hidden; }
.vdpg-prog-fill { height: 100%; width: 0; border-radius: 999px;
background: linear-gradient(90deg, rgba(var(--accent-rgb),0.7), rgb(var(--accent-rgb)));
transition: width 1.4s cubic-bezier(0.3,0.8,0.3,1); }
.adl-row-meta.vdpg-dest { color: #8fe7af; font-family: 'JetBrains Mono', ui-monospace, monospace; }
/* retry button — mirrors .adl-row-cancel's hover-reveal, accent themed */
.vdpg-row-retry { flex-shrink: 0; width: 28px; height: 28px; margin-left: 8px; padding: 0; display: inline-flex;
align-items: center; justify-content: center; border-radius: 50%; cursor: pointer; color: #fff;
background: linear-gradient(135deg, rgba(var(--accent-rgb),0.85), rgba(var(--accent-rgb),0.7));
border: 1px solid rgba(var(--accent-rgb),0.55); box-shadow: 0 0 10px rgba(var(--accent-rgb),0.25);
opacity: 0; pointer-events: none; transform: translateX(8px) scale(0.72);
transition: opacity 0.18s ease, transform 0.22s cubic-bezier(0.34,1.56,0.64,1); }
.adl-row:hover .vdpg-row-retry, .adl-row:focus-within .vdpg-row-retry { opacity: 1; pointer-events: auto; transform: translateX(0) scale(1); }
.vdpg-row-retry:hover { transform: translateX(0) scale(1.12) rotate(-40deg); }
.vdpg-row-retry:disabled { opacity: 0.5 !important; pointer-events: none; }
@media (hover: none) { .vdpg-row-retry { opacity: 1; pointer-events: auto; transform: none; } }