video: universal Download modal (movie/show/youtube) — visual scaffolding

Adds a 'Download' action to the get-modal that closes it and opens a new universal
Download modal (VideoDownload.open) shared by movies, shows and YouTube:

  - Quality TARGET shown as chips, read live from the profile you configured
    (cutoff / codec / HDR / rejects / size for p2p; resolution/codec/container/
    60fps/HDR for YouTube).
  - Owned copies get a REAL verdict: 'In your library · 720p · BluRay · X265' +
    a 'Meets your target' / 'Eligible for upgrade' badge with reasons, via
    /downloads/evaluate (the quality_eval seam).
  - Per-source rows (soulseek/torrent/usenet from the download config, or yt-dlp
    for YouTube) each with a Search button + a 'Search all'.

Per the ask, the searches are STUBS — clicking flips the row to 'coming soon'; no
backend yet (that's the engine phase). Beautiful .vdl-* styling mirrors the get-modal
vibe (per-title hue glow). New JS file wired into index.html. Pure front-end.
This commit is contained in:
BoulderBadgeDad 2026-06-19 11:06:11 -07:00
parent e880be9910
commit 1dc727f0bb
4 changed files with 363 additions and 0 deletions

View file

@ -10210,6 +10210,8 @@
<script src="{{ url_for('static', filename='video/video-watchlist-btn.js', v=static_v) }}"></script>
<!-- Video "get" button + detail/download modal (shared; movies + ended shows) -->
<script src="{{ url_for('static', filename='video/video-get-modal.js', v=static_v) }}"></script>
<!-- Universal Download modal (movie / show / youtube): per-source search + quality verdict -->
<script src="{{ url_for('static', filename='video/video-download-modal.js', v=static_v) }}"></script>
<!-- Video library page (isolated; lists movies/shows + scan trigger) -->
<script src="{{ url_for('static', filename='video/video-library.js', v=static_v) }}"></script>
<!-- Video detail page (isolated; show drill-in: hero + season/episode tree) -->

View file

@ -0,0 +1,253 @@
/*
* SoulSync universal Download modal (movie / TV show / YouTube).
*
* Opened from the get-modal's "Download" button (and, later, the YouTube card).
* v1 is VISUAL scaffolding for the direct-download flow: it shows the quality
* TARGET (read from the Settings Downloads profile you configured), judges any
* copy you ALREADY own against that target (real via /downloads/evaluate), and
* lists each attached download source with a per-source "Search" affordance. The
* searches themselves are stubs no backend wiring yet (that's the engine phase).
*
* VideoDownload.open({ kind, id, source, title, thumb }). Self-contained.
*/
(function () {
'use strict';
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function toast(msg, type) { if (typeof showToast === 'function') showToast(msg, type); }
function hueOf(s) { var h = 0, t = String(s || ''); for (var i = 0; i < t.length; i++) h = (h * 31 + t.charCodeAt(i)) >>> 0; return h % 360; }
function resLabel(res) {
if (!res) return '';
res = String(res).toLowerCase();
if (res.indexOf('2160') > -1 || res === '4k') return '4K';
if (res.indexOf('1080') > -1) return '1080p';
if (res.indexOf('720') > -1) return '720p';
if (res.indexOf('480') > -1 || res.indexOf('576') > -1) return 'SD';
return res.toUpperCase();
}
var CUT_LABEL = { '2160p': '4K', '1080p': '1080p', '720p': '720p', '480p': 'SD' };
var SRC_META = {
soulseek: { name: 'Soulseek', emoji: '🎵' },
torrent: { name: 'Torrent', emoji: '🧲' },
usenet: { name: 'Usenet', emoji: '📰' },
youtube: { name: 'YouTube', emoji: '▶' }
};
function getJSON(url) {
return fetch(url, { headers: { Accept: 'application/json' } })
.then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; });
}
function postJSON(url, body) {
return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(body) }).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; });
}
var modalEl = null, keyHandler = null;
function closeModal() {
if (!modalEl) return;
modalEl.classList.remove('vdl-open');
document.body.style.removeProperty('overflow');
if (keyHandler) { document.removeEventListener('keydown', keyHandler); keyHandler = null; }
var el = modalEl; modalEl = null;
setTimeout(function () { if (el && el.parentNode) el.parentNode.removeChild(el); }, 220);
}
function shell(o) {
return '<div class="vdl-modal" role="dialog" aria-modal="true">' +
'<button class="vdl-close" type="button" data-vdl-close aria-label="Close">&times;</button>' +
'<div class="vdl-hero" data-vdl-hero>' +
'<div class="vdl-hero-scrim"></div>' +
'<img class="vdl-poster" data-vdl-poster alt="" hidden>' +
'<div class="vdl-hero-content">' +
'<div class="vdl-eyebrow">⤓ Download</div>' +
'<h2 class="vdl-title" data-vdl-title>' + esc(o.title || 'Loading…') + '</h2>' +
'<div class="vdl-meta" data-vdl-meta></div>' +
'</div>' +
'</div>' +
'<div class="vdl-body">' +
'<div class="vdl-section">' +
'<div class="vdl-sec-label">Quality target</div>' +
'<div class="vdl-chips" data-vdl-target><span class="vdl-chip vdl-chip--ghost">Loading…</span></div>' +
'</div>' +
'<div class="vdl-owned" data-vdl-owned hidden></div>' +
'<div class="vdl-section">' +
'<div class="vdl-sec-head">' +
'<div class="vdl-sec-label">Sources</div>' +
'<button class="vdl-search-all" type="button" data-vdl-search-all>⌕ Search all</button>' +
'</div>' +
'<div class="vdl-sources" data-vdl-sources><div class="vdl-src-empty">Loading sources…</div></div>' +
'<div class="vdl-foot-note">Automatic searching arrives with the download engine — this is the layout it\'ll drive.</div>' +
'</div>' +
'</div>' +
'</div>';
}
function open(o) {
if (!o) return;
closeModal();
var ov = document.createElement('div');
ov.className = 'vdl-overlay';
ov.style.setProperty('--vdl-h', hueOf(o.title || ''));
ov.innerHTML = shell(o);
document.body.appendChild(ov);
document.body.style.overflow = 'hidden';
modalEl = ov;
requestAnimationFrame(function () { ov.classList.add('vdl-open'); });
ov.addEventListener('click', function (e) {
if (e.target === ov || e.target.closest('[data-vdl-close]')) { closeModal(); return; }
var sb = e.target.closest('[data-vdl-search]');
if (sb) { stubSearch(sb.getAttribute('data-vdl-search')); return; }
if (e.target.closest('[data-vdl-search-all]')) { stubSearch('*'); return; }
});
keyHandler = function (e) { if (e.key === 'Escape') closeModal(); };
document.addEventListener('keydown', keyHandler);
load(o);
}
function load(o) {
var isYt = o.kind === 'youtube';
if (isYt) {
fillYt(o);
renderSources(['youtube']);
} else {
var url = (o.source === 'tmdb')
? '/api/video/tmdb/' + o.kind + '/' + o.id
: '/api/video/detail/' + o.kind + '/' + o.id;
getJSON(url).then(function (d) { if (modalEl && d) fillDetail(d, o); });
getJSON('/api/video/downloads/config').then(function (c) { if (modalEl) renderSources(sourcesFromConfig(c)); });
}
var qurl = isYt ? '/api/video/downloads/youtube-quality' : '/api/video/downloads/quality';
getJSON(qurl).then(function (p) { if (modalEl && p) renderTarget(p, isYt); });
}
function sourcesFromConfig(c) {
c = c || {};
if (c.download_mode === 'hybrid' && Array.isArray(c.hybrid_order) && c.hybrid_order.length) return c.hybrid_order;
if (c.download_mode) return [c.download_mode];
return ['soulseek'];
}
function fillDetail(d, o) {
var q = function (s) { return modalEl.querySelector(s); };
if (d.title) { var t = q('[data-vdl-title]'); if (t) t.textContent = d.title; modalEl.style.setProperty('--vdl-h', hueOf(d.title)); }
var hero = q('[data-vdl-hero]');
var bg = (o.source !== 'tmdb' && d.has_backdrop)
? '/api/video/backdrop/' + o.kind + '/' + o.id + '?w=1280'
: (d.backdrop_url || d.backdrop || '');
if (hero && bg) hero.style.backgroundImage = "url('" + bg + "')";
var poster = q('[data-vdl-poster]');
var pUrl = (o.source !== 'tmdb' && d.has_poster)
? '/api/video/poster/' + o.kind + '/' + o.id
: (d.poster_url || d.poster || '');
if (poster && pUrl) {
poster.onload = function () { if (hero) hero.classList.add('vdl-has-poster'); poster.hidden = false; };
poster.onerror = function () { poster.hidden = true; };
poster.src = pUrl;
}
var meta = [];
if (d.year) meta.push(d.year);
if (d.runtime_minutes) meta.push(d.runtime_minutes + ' min');
meta.push(o.kind === 'show' ? 'TV series' : 'Movie');
var mt = q('[data-vdl-meta]'); if (mt) mt.textContent = meta.filter(Boolean).join(' · ');
if (o.kind === 'movie' && d.owned && d.file) renderOwned(d.file);
}
function fillYt(o) {
var t = modalEl.querySelector('[data-vdl-title]'); if (t && o.title) t.textContent = o.title;
var hero = modalEl.querySelector('[data-vdl-hero]');
if (hero && o.thumb) hero.style.backgroundImage = "url('" + o.thumb + "')";
var mt = modalEl.querySelector('[data-vdl-meta]'); if (mt) mt.textContent = 'YouTube';
}
function chip(text, mod) { return '<span class="vdl-chip' + (mod ? ' vdl-chip--' + mod : '') + '">' + esc(text) + '</span>'; }
function renderTarget(p, isYt) {
var box = modalEl.querySelector('[data-vdl-target]'); if (!box) return;
var chips = [];
if (isYt) {
chips.push(chip('Up to ' + (p.max_resolution === 'best' ? 'best' : (p.max_resolution || '1080p'))));
if (p.video_codec && p.video_codec !== 'any') chips.push(chip('Prefer ' + p.video_codec.toUpperCase()));
if (p.container) chips.push(chip(p.container.toUpperCase()));
if (p.prefer_60fps) chips.push(chip('60fps'));
chips.push(chip(p.allow_hdr ? 'HDR ok' : 'SDR'));
} else {
chips.push(chip(p.cutoff_resolution ? 'Stop at ' + (CUT_LABEL[p.cutoff_resolution] || p.cutoff_resolution) : 'Always upgrade'));
if (p.prefer_codec && p.prefer_codec !== 'any') chips.push(chip('Prefer ' + (p.prefer_codec === 'hevc' ? 'HEVC' : p.prefer_codec.toUpperCase())));
if (p.prefer_hdr === 'prefer') chips.push(chip('Prefer HDR'));
else if (p.prefer_hdr === 'require') chips.push(chip('HDR required', 'req'));
if (Array.isArray(p.rejects) && p.rejects.length) chips.push(chip('Reject ' + p.rejects.join(', '), 'rej'));
if (p.max_movie_gb) chips.push(chip('Movie ≤ ' + p.max_movie_gb + ' GB'));
if (p.max_episode_gb) chips.push(chip('Episode ≤ ' + p.max_episode_gb + ' GB'));
}
box.innerHTML = chips.join('');
}
// Owned copy → "In your library · 720p · BluRay · X265" + a verdict against the
// quality target (real: /downloads/evaluate). meets → reassuring; else upgrade.
function renderOwned(file) {
var box = modalEl.querySelector('[data-vdl-owned]'); if (!box) return;
var bits = [resLabel(file.resolution), file.release_source, (file.video_codec || '').toUpperCase()].filter(Boolean);
box.innerHTML =
'<div class="vdl-owned-row">' +
'<span class="vdl-owned-ic">✓</span>' +
'<span class="vdl-owned-txt"><strong>In your library</strong>' + (bits.length ? ' · ' + esc(bits.join(' · ')) : '') + '</span>' +
'<span class="vdl-verdict vdl-verdict--pending" data-vdl-verdict>checking…</span>' +
'</div>' +
'<div class="vdl-reasons" data-vdl-reasons></div>';
box.hidden = false;
postJSON('/api/video/downloads/evaluate', { file: file }).then(function (v) {
if (!modalEl || !v) return;
var badge = box.querySelector('[data-vdl-verdict]');
if (badge) {
badge.classList.remove('vdl-verdict--pending');
badge.classList.add(v.meets ? 'vdl-verdict--ok' : 'vdl-verdict--up');
badge.textContent = v.meets ? 'Meets your target' : 'Eligible for upgrade';
}
var rs = box.querySelector('[data-vdl-reasons]');
if (rs && v.reasons && v.reasons.length) {
rs.innerHTML = v.reasons.map(function (r) {
return '<div class="vdl-reason vdl-reason--' + (r.ok ? 'ok' : 'no') + '">' +
(r.ok ? '✓' : '↑') + ' ' + esc(r.text) + '</div>';
}).join('');
}
});
}
function renderSources(list) {
var box = modalEl.querySelector('[data-vdl-sources]'); if (!box) return;
list = (list || []).filter(function (s) { return SRC_META[s]; });
if (!list.length) {
box.innerHTML = '<div class="vdl-src-empty">No download source configured — pick one on Settings → Downloads.</div>';
return;
}
box.innerHTML = list.map(function (s) {
var m = SRC_META[s];
return '<div class="vdl-src" data-vdl-src="' + s + '">' +
'<span class="vdl-src-emoji">' + m.emoji + '</span>' +
'<span class="vdl-src-name">' + esc(m.name) + '</span>' +
'<span class="vdl-src-status" data-vdl-status>Ready</span>' +
'<button class="vdl-src-search" type="button" data-vdl-search="' + s + '">⌕ Search</button>' +
'</div>';
}).join('');
}
// Scaffold: flip the targeted row(s) to a "coming soon" state. No backend yet.
function stubSearch(which) {
if (!modalEl) return;
var sel = which === '*' ? '[data-vdl-src]' : '[data-vdl-src="' + which + '"]';
var rows = modalEl.querySelectorAll(sel);
for (var i = 0; i < rows.length; i++) {
var st = rows[i].querySelector('[data-vdl-status]');
if (st) { st.textContent = 'Search engine coming soon'; st.className = 'vdl-src-status vdl-src-status--soon'; }
}
toast('Automatic search isnt wired up yet — coming soon', 'info');
}
window.VideoDownload = { open: open };
})();

View file

@ -165,6 +165,8 @@
'<div class="vgm-actions">' +
'<span class="vgm-sel-count" data-vgm-count></span>' +
'<button class="discog-cancel-btn" type="button" data-vgm-open>Full page &rarr;</button>' +
'<button class="discog-cancel-btn vgm-download-btn" type="button" data-vgm-download>' +
'<span class="vgm-download-ic">⤓</span> Download</button>' +
'<button class="discog-submit-btn" type="button" data-vgm-wishlist>' +
'<span class="discog-submit-icon">⬇</span>' +
'<span class="discog-submit-text" data-vgm-add-label>+ Add to Wishlist</span>' +
@ -199,6 +201,19 @@
}
return;
}
if (e.target.closest('[data-vgm-download]')) {
// Hand off to the universal Download modal (movie/show/youtube). Pass
// what we know; it fetches its own detail + quality verdict.
var st = modalState || {};
closeModal();
if (window.VideoDownload) {
VideoDownload.open({ kind: o.kind, id: o.id, source: o.source || 'library',
title: st.title || o.title || '' });
} else {
toast('Download module not loaded', 'error');
}
return;
}
if (e.target.closest('[data-vgm-wishlist]')) { submitWishlist(ov); }
});
ov.addEventListener('change', function (e) {

View file

@ -3014,3 +3014,96 @@ body[data-side="video"] #soulsync-toggle { display: none; }
background: rgb(var(--accent-rgb)); cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.4); }
.vq-slider::-moz-range-thumb { width: 18px; height: 18px; border: none; border-radius: 50%;
background: rgb(var(--accent-rgb)); cursor: pointer; }
/* ── get-modal "Download" action button ───────────────────────────────────── */
.vgm-download-btn { display: inline-flex; align-items: center; gap: 7px; }
.vgm-download-btn .vgm-download-ic { font-size: 15px; font-weight: 900; line-height: 1; }
/* ── universal Download modal (.vdl-*) — mirrors the get-modal vibe ────────── */
.vdl-overlay { position: fixed; inset: 0; z-index: 9200; display: flex; align-items: center; justify-content: center;
padding: 24px; background: rgba(4, 4, 7, 0.78); backdrop-filter: blur(10px); opacity: 0; transition: opacity 0.2s ease; }
.vdl-overlay.vdl-open { opacity: 1; }
.vdl-modal { position: relative; width: min(600px, 100%); max-height: 90vh; overflow-y: auto;
border-radius: 22px; background: #101015; border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 50px 130px rgba(0, 0, 0, 0.72), 0 0 90px -30px hsla(var(--vdl-h, 220), 70%, 50%, 0.45);
transform: translateY(16px) scale(0.985); transition: transform 0.26s cubic-bezier(0.2, 0.7, 0.2, 1); }
.vdl-overlay.vdl-open .vdl-modal { transform: none; }
.vdl-modal::-webkit-scrollbar { width: 9px; }
.vdl-modal::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.14); border-radius: 5px; }
.vdl-close { position: absolute; top: 14px; right: 14px; z-index: 3; width: 36px; height: 36px; border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.18); background: rgba(0, 0, 0, 0.45); color: #fff; font-size: 22px;
cursor: pointer; backdrop-filter: blur(6px); display: flex; align-items: center; justify-content: center;
line-height: 1; transition: all 0.15s ease; }
.vdl-close:hover { background: rgba(0, 0, 0, 0.72); border-color: rgba(255, 255, 255, 0.4); }
.vdl-hero { position: relative; min-height: 188px; border-radius: 22px 22px 0 0; overflow: hidden;
display: flex; align-items: flex-end; background-size: cover; background-position: center 24%; background-color: #16161d; }
.vdl-hero-scrim { position: absolute; inset: 0;
background: linear-gradient(0deg, #101015 5%, rgba(16, 16, 21, 0.55) 55%, rgba(16, 16, 21, 0.08)),
radial-gradient(130% 120% at 0% 100%, hsla(var(--vdl-h, 220), 70%, 50%, 0.34), transparent 60%); }
.vdl-hero-content { position: relative; z-index: 1; padding: 22px 26px 18px; width: 100%; }
.vdl-eyebrow { font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.06em;
color: hsla(var(--vdl-h, 220), 80%, 78%, 0.95); }
.vdl-title { font-size: 26px; font-weight: 900; letter-spacing: -0.025em; line-height: 1.06; margin: 7px 0 0;
text-shadow: 0 2px 18px rgba(0, 0, 0, 0.55); }
.vdl-poster { position: absolute; left: 26px; bottom: 16px; z-index: 2; width: 90px; aspect-ratio: 2 / 3;
object-fit: cover; border-radius: 9px; box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6); border: 1px solid rgba(255, 255, 255, 0.12); }
.vdl-poster[hidden] { display: none; }
.vdl-hero.vdl-has-poster .vdl-hero-content { padding-left: 134px; }
.vdl-meta { display: flex; gap: 14px; margin-top: 9px; flex-wrap: wrap; font-size: 12.5px; font-weight: 700;
color: rgba(255, 255, 255, 0.72); }
.vdl-body { padding: 20px 26px 26px; }
.vdl-section { margin-bottom: 20px; }
.vdl-section:last-child { margin-bottom: 0; }
.vdl-sec-label { font-size: 12px; font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase;
color: rgba(255, 255, 255, 0.55); margin-bottom: 10px; }
.vdl-sec-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.vdl-sec-head .vdl-sec-label { margin-bottom: 0; }
/* target chips */
.vdl-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.vdl-chip { padding: 6px 12px; border-radius: 999px; font-size: 12.5px; font-weight: 700;
background: hsla(var(--vdl-h, 220), 45%, 50%, 0.16); border: 1px solid hsla(var(--vdl-h, 220), 50%, 60%, 0.32);
color: #fff; }
.vdl-chip--ghost { background: rgba(255, 255, 255, 0.05); border-color: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.5); }
.vdl-chip--rej { background: rgba(239, 68, 68, 0.16); border-color: rgba(239, 68, 68, 0.4); color: #fecaca; }
.vdl-chip--req { background: rgba(245, 158, 11, 0.16); border-color: rgba(245, 158, 11, 0.4); color: #fde2b3; }
/* owned card + verdict */
.vdl-owned { margin-bottom: 20px; padding: 13px 15px; border-radius: 13px;
background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.08); }
.vdl-owned[hidden] { display: none; }
.vdl-owned-row { display: flex; align-items: center; gap: 9px; }
.vdl-owned-ic { font-size: 14px; font-weight: 900; color: #6cd391; flex-shrink: 0; }
.vdl-owned-txt { font-size: 13px; color: rgba(255, 255, 255, 0.72); flex: 1; min-width: 0; }
.vdl-owned-txt strong { color: #8fe7af; font-weight: 800; }
.vdl-verdict { flex-shrink: 0; padding: 4px 11px; border-radius: 999px; font-size: 11px; font-weight: 800;
letter-spacing: 0.02em; }
.vdl-verdict--pending { background: rgba(255, 255, 255, 0.08); color: rgba(255, 255, 255, 0.5); }
.vdl-verdict--ok { background: rgba(108, 211, 145, 0.18); color: #8fe7af; }
.vdl-verdict--up { background: rgba(245, 158, 11, 0.18); color: #fbcd7a; }
.vdl-reasons { margin-top: 10px; display: flex; flex-direction: column; gap: 5px; }
.vdl-reasons:empty { display: none; }
.vdl-reason { font-size: 12px; font-weight: 600; }
.vdl-reason--ok { color: rgba(143, 231, 175, 0.85); }
.vdl-reason--no { color: rgba(251, 205, 122, 0.92); }
/* sources */
.vdl-search-all { padding: 6px 13px; border-radius: 9px; cursor: pointer; font-size: 12px; font-weight: 800;
background: rgb(var(--accent-rgb)); color: #06210f; border: none; transition: filter 0.15s; }
.vdl-search-all:hover { filter: brightness(1.08); }
.vdl-sources { display: flex; flex-direction: column; gap: 8px; }
.vdl-src { display: flex; align-items: center; gap: 12px; padding: 11px 14px; border-radius: 12px;
background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.07); }
.vdl-src-emoji { font-size: 17px; flex-shrink: 0; }
.vdl-src-name { font-size: 14px; font-weight: 700; color: #fff; flex: 1; }
.vdl-src-status { font-size: 11.5px; font-weight: 700; color: rgba(255, 255, 255, 0.4); }
.vdl-src-status--soon { color: #fbcd7a; }
.vdl-src-search { padding: 6px 13px; border-radius: 8px; cursor: pointer; font-size: 12px; font-weight: 700;
background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.14); color: #fff; transition: all 0.15s; }
.vdl-src-search:hover { background: rgba(var(--accent-rgb), 0.25); border-color: rgba(var(--accent-rgb), 0.5); }
.vdl-src-empty { font-size: 13px; color: rgba(255, 255, 255, 0.5); padding: 12px 14px; border-radius: 12px;
background: rgba(255, 255, 255, 0.03); border: 1px dashed rgba(255, 255, 255, 0.12); }
.vdl-foot-note { margin-top: 11px; font-size: 11.5px; font-weight: 600; color: rgba(255, 255, 255, 0.38);
font-style: italic; }
@media (max-width: 560px) {
.vdl-src { flex-wrap: wrap; }
.vdl-hero.vdl-has-poster .vdl-hero-content { padding-left: 110px; }
.vdl-poster { width: 72px; left: 20px; }
}