Match player + preview
- Rebuild VS intro as a 1920×1080 scaled canvas (arena design from drafts/):
angled RED/BLUE panels with clip-paths, slam-in VS with pulse+shine,
event/stage/weight header, Match/Court/Referee chips, per-fighter
Record/Rank/Stats chip row. Every placeholder from the artifact is
always in the DOM; empty ones are hidden by `.vs-nullable:empty`
and `.vs-nullable-hide` so the layout adapts without losing the
design's HTML shape. Country codes always render as full country
names (via new App\Data\Countries::name()).
- Real-time 6s countdown Skip button; video pinned at t=0 while
the intro runs, released to play at zero or on Skip.
- Standalone preview page at /videos/{video}/vs-preview.
Video cards (home + everywhere)
- Match cards get a `vs-mini` overlay of the same arena design
(fixed canvas, per-card ResizeObserver, animations replay every
time the mouse leaves the thumb). On hover the mini explodes
outward (panels fly out, VS scales+blurs+fades) and the hover
video plays over the cleared thumb.
- Match cards also render a `scorebar-mini` overlay (verbatim of
the full-player msb-scale scorebar + msb-feed) that shows
during hover playback. Live-scoring is wired per-card via a
data-sbm-state payload of rounds+points; RED/BLUE score, round
label, per-round clock, and the top-right Live Scoring ticker
(4 entries, sport-aware Yuko/Waza-ari/Ippon or Punch/Body
kick/… labels) all update from the mini video's timeupdate.
Bug fixes
- Hover video wasn't playing sound: dropped the `muted` attribute
I'd added while debugging match previews.
- White flash strobing on the left of match cards: removed the
`.vs-mini-flash` element (intro-only flourish that fired on
every mouseleave replay).
- Sports-match modal was showing "Please complete the required
fields" when saving basic info for match videos that had no
SportsMatch row yet: the submit gate only checked matchId, not
the attached videoId; now it correctly skips uploadVideoFirst()
in both edit and attach-existing-video modes.
Country names project-wide
- New helper App\Data\Countries::name($iso2) → full country name.
- Swapped `?? country` fallbacks to `?? Countries::name(country)`
in admin dashboard, video-analytics, video-insights payloads
(VideoController + SuperAdminController), and the VS partials.
Now user-visible country labels always show "Bahrain" instead
of "BH", including chart tooltips and modal titles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
560 lines
26 KiB
PHP
560 lines
26 KiB
PHP
{{-- ══════════════════════════════════════════════════════════════════════
|
||
VS INTRO SCREEN — arena card overlaid on the first 5 s of the match.
|
||
Rendered as a FIXED 1920×1080 (landscape) or 1080×1920 (portrait)
|
||
canvas that is transform-scaled to fit the parent player box. Every
|
||
child uses ABSOLUTE PIXELS on that canvas so the layout is identical
|
||
at every zoom level and never scales off the viewport width.
|
||
══════════════════════════════════════════════════════════════════════ --}}
|
||
@php
|
||
$vsRed = $hd['red'] ?? null;
|
||
$vsBlue = $hd['blue'] ?? null;
|
||
$vsShow = $hd && (($vsRed['name'] ?? null) || ($vsBlue['name'] ?? null));
|
||
|
||
$vsFlagBg = fn(?string $c) => (($c = strtolower(trim((string) $c))) !== '' ? $c : 'xx');
|
||
$vsCountryName = fn (?string $code) => \App\Data\Countries::name($code);
|
||
$vsImg = fn(?string $path) => $path ? route('media.thumbnail', $path) : null;
|
||
|
||
// Every placeholder from the artifact renders regardless of whether the
|
||
// data exists — CSS `.vs-nullable:empty { display: none }` hides any
|
||
// element that came back empty, so the layout adapts but the DOM shape
|
||
// matches the original design file 1:1.
|
||
$vsStr = fn ($x) => (is_string($x) && trim($x) !== '') ? trim($x) : '';
|
||
|
||
$vsEvent = $vsStr($hd['championship'] ?? '');
|
||
$vsStage = $vsStr($hd['division'] ?? '') ?: $vsStr($hd['format'] ?? '');
|
||
$vsWeight = $vsStr($hd['weight_category'] ?? '');
|
||
$vsMatchNo = $vsStr($hd['match_number'] ?? '');
|
||
$vsCourt = $vsStr($hd['court'] ?? '');
|
||
$vsRef = $vsStr($hd['referee']['name'] ?? '');
|
||
$vsRefFlag = $vsStr($hd['referee']['flag'] ?? '');
|
||
$vsVenue = $vsStr($hd['venue']['name'] ?? '');
|
||
|
||
// Extra per-fighter chips from the artifact (record / rank / stats).
|
||
// headerData() doesn't expose them today; fall back to the raw
|
||
// participants JSON so the placeholders wire up the moment the fields
|
||
// start being written by the uploader form.
|
||
$vsParts = $video->sportsMatch?->participants ?? [];
|
||
$vsRedRecord = $vsStr($vsParts['p2_record'] ?? '');
|
||
$vsRedRank = $vsStr($vsParts['p2_rank'] ?? '');
|
||
$vsRedStats = $vsStr($vsParts['p2_stats'] ?? '');
|
||
$vsBlueRecord = $vsStr($vsParts['p1_record'] ?? '');
|
||
$vsBlueRank = $vsStr($vsParts['p1_rank'] ?? '');
|
||
$vsBlueStats = $vsStr($vsParts['p1_stats'] ?? '');
|
||
|
||
// Fighter identity strings.
|
||
$vsRedName = $vsStr($vsRed['name'] ?? '');
|
||
$vsBlueName = $vsStr($vsBlue['name'] ?? '');
|
||
$vsRedClub = $vsStr($vsRed['club'] ?? '');
|
||
$vsBlueClub = $vsStr($vsBlue['club'] ?? '');
|
||
$vsRedFlag = $vsStr($vsRed['flag'] ?? '');
|
||
$vsBlueFlag = $vsStr($vsBlue['flag'] ?? '');
|
||
$vsRedLogo = $vsImg($vsRed['club_logo'] ?? null);
|
||
$vsBlueLogo = $vsImg($vsBlue['club_logo'] ?? null);
|
||
@endphp
|
||
|
||
@if ($vsShow)
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Anton&family=Barlow+Condensed:wght@400;600;700;800&display=swap">
|
||
|
||
<div id="vsScreen" class="vs-screen" role="dialog" aria-label="Match introduction">
|
||
<button type="button" class="vs-skip" id="vsSkip" aria-label="Skip intro">
|
||
Skip <span id="vsSkipTimer">6</span>
|
||
</button>
|
||
|
||
{{-- Fixed 1920×1080 (or 1080×1920 in portrait) canvas.
|
||
JS sets --vs-scale on #vsScreen to fit whichever parent this sits in. --}}
|
||
<div class="vs-stage">
|
||
|
||
{{-- RED side panel --}}
|
||
<div class="vs-panel vs-panel-red">
|
||
<div class="vs-panel-photo vs-panel-photo-red"
|
||
@if($vsImg($vsRed['headshot'] ?? null)) style="background-image:url('{{ $vsImg($vsRed['headshot']) }}')"@endif></div>
|
||
<div class="vs-panel-scrim vs-panel-scrim-red"></div>
|
||
<div class="vs-panel-gradient"></div>
|
||
</div>
|
||
|
||
{{-- BLUE side panel --}}
|
||
<div class="vs-panel vs-panel-blue">
|
||
<div class="vs-panel-photo vs-panel-photo-blue"
|
||
@if($vsImg($vsBlue['headshot'] ?? null)) style="background-image:url('{{ $vsImg($vsBlue['headshot']) }}')"@endif></div>
|
||
<div class="vs-panel-scrim vs-panel-scrim-blue"></div>
|
||
<div class="vs-panel-gradient"></div>
|
||
</div>
|
||
|
||
{{-- Center divider glow line --}}
|
||
<div class="vs-divider"></div>
|
||
|
||
{{-- ══ RED fighter identity (left) — every placeholder from the
|
||
original artifact is always in the DOM. Empty ones are hidden
|
||
by `.vs-nullable:empty { display: none }` in the CSS below. ══ --}}
|
||
<div class="vs-info vs-info-red">
|
||
<div class="vs-corner-tag vs-corner-tag-red">AKA · RED</div>
|
||
<div class="vs-flag-row vs-nullable-row">
|
||
<span class="vs-flag fi fi-{{ $vsFlagBg($vsRedFlag) }} vs-flag-slot {{ $vsRedFlag === '' ? 'vs-nullable-hide' : '' }}"></span>
|
||
<div class="vs-country vs-country-red vs-nullable">{{ $vsRedFlag !== '' ? strtoupper($vsCountryName($vsRedFlag) ?? $vsRedFlag) : '' }}</div>
|
||
</div>
|
||
<div class="vs-name vs-nullable">{{ $vsRedName }}</div>
|
||
<div class="vs-club-row vs-nullable-row">
|
||
<div class="vs-club-logo vs-club-logo-slot {{ $vsRedLogo ? '' : 'vs-nullable-hide' }}"
|
||
@if($vsRedLogo) style="background-image:url('{{ $vsRedLogo }}')"@endif></div>
|
||
<div class="vs-club vs-nullable">{{ $vsRedClub }}</div>
|
||
</div>
|
||
<div class="vs-chips-row vs-nullable-row">
|
||
<div class="vs-chip-fighter vs-nullable">{{ $vsRedRecord }}</div>
|
||
<div class="vs-chip-fighter vs-chip-fighter-gold vs-nullable">{{ $vsRedRank }}</div>
|
||
<div class="vs-chip-fighter vs-nullable">{{ $vsRedStats }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ══ BLUE fighter identity (right) ══ --}}
|
||
<div class="vs-info vs-info-blue">
|
||
<div class="vs-corner-tag vs-corner-tag-blue">AO · BLUE</div>
|
||
<div class="vs-flag-row vs-flag-row-r vs-nullable-row">
|
||
<span class="vs-flag fi fi-{{ $vsFlagBg($vsBlueFlag) }} vs-flag-slot {{ $vsBlueFlag === '' ? 'vs-nullable-hide' : '' }}"></span>
|
||
<div class="vs-country vs-country-blue vs-nullable">{{ $vsBlueFlag !== '' ? strtoupper($vsCountryName($vsBlueFlag) ?? $vsBlueFlag) : '' }}</div>
|
||
</div>
|
||
<div class="vs-name vs-nullable">{{ $vsBlueName }}</div>
|
||
<div class="vs-club-row vs-club-row-r vs-nullable-row">
|
||
<div class="vs-club-logo vs-club-logo-slot {{ $vsBlueLogo ? '' : 'vs-nullable-hide' }}"
|
||
@if($vsBlueLogo) style="background-image:url('{{ $vsBlueLogo }}')"@endif></div>
|
||
<div class="vs-club vs-nullable">{{ $vsBlueClub }}</div>
|
||
</div>
|
||
<div class="vs-chips-row vs-nullable-row">
|
||
<div class="vs-chip-fighter vs-nullable">{{ $vsBlueRecord }}</div>
|
||
<div class="vs-chip-fighter vs-chip-fighter-gold vs-nullable">{{ $vsBlueRank }}</div>
|
||
<div class="vs-chip-fighter vs-nullable">{{ $vsBlueStats }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Top: event + stage + weight (all three always in DOM) --}}
|
||
<div class="vs-top">
|
||
<div class="vs-top-event vs-nullable">{{ $vsEvent }}</div>
|
||
<div class="vs-top-stage-row {{ $vsStage === '' ? 'vs-nullable-hide' : '' }}">
|
||
<div class="vs-top-stage-line vs-top-stage-line-l"></div>
|
||
<div class="vs-top-stage">{{ $vsStage }}</div>
|
||
<div class="vs-top-stage-line vs-top-stage-line-r"></div>
|
||
</div>
|
||
<div class="vs-top-weight vs-nullable">{{ $vsWeight }}</div>
|
||
</div>
|
||
|
||
{{-- Center VS --}}
|
||
<div class="vs-center">
|
||
<div class="vs-word">VS
|
||
<div class="vs-shine-wrap"><div class="vs-shine"></div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="vs-flash"></div>
|
||
|
||
{{-- Bottom: match + court + referee chips. All three chips + both
|
||
diamond separators are always in the DOM; each chip hides when
|
||
its value is empty and each diamond hides when either neighbour
|
||
is hidden (via :has() below). --}}
|
||
<div class="vs-bottom">
|
||
<div class="vs-chip vs-nullable-chip" data-vs-empty="{{ $vsMatchNo === '' ? '1' : '0' }}">
|
||
<span class="vs-chip-label">Match</span>
|
||
<span class="vs-chip-value vs-nullable">{{ $vsMatchNo }}</span>
|
||
</div>
|
||
<div class="vs-diamond vs-diamond-match-court"></div>
|
||
<div class="vs-chip vs-nullable-chip" data-vs-empty="{{ $vsCourt === '' ? '1' : '0' }}">
|
||
<span class="vs-chip-label">Court</span>
|
||
<span class="vs-chip-value vs-nullable">{{ $vsCourt }}</span>
|
||
</div>
|
||
<div class="vs-diamond vs-diamond-court-ref"></div>
|
||
<div class="vs-chip vs-nullable-chip" data-vs-empty="{{ $vsRef === '' ? '1' : '0' }}">
|
||
<span class="vs-chip-label">Referee</span>
|
||
<span class="vs-chip-value vs-chip-value-name vs-nullable">{{ $vsRef }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
/* ═══════════════════════════════════════════════════════════════════════
|
||
Fixed 1920×1080 canvas, transform-scaled to fit its parent (.ytp
|
||
inside the player, or .vs-preview-stage on the preview page). Every
|
||
child dimension is in ABSOLUTE PIXELS on that canvas — never vw/vh —
|
||
so the layout is identical whether the box is 400 px or 4000 px wide.
|
||
JS writes --vs-scale via ResizeObserver on the parent.
|
||
═══════════════════════════════════════════════════════════════════════ */
|
||
#vsScreen.vs-screen {
|
||
position: absolute; inset: 0; z-index: 40;
|
||
background: #050507;
|
||
font-family: 'Barlow Condensed', sans-serif;
|
||
color: #e8e6e0;
|
||
overflow: hidden;
|
||
opacity: 1;
|
||
transition: opacity .35s ease;
|
||
--vs-scale: 1;
|
||
--vs-stage-w: 1920px;
|
||
--vs-stage-h: 1080px;
|
||
}
|
||
#vsScreen.vs-hide { opacity: 0; pointer-events: none; }
|
||
|
||
#vsScreen .vs-stage {
|
||
position: absolute;
|
||
left: 50%; top: 50%;
|
||
width: var(--vs-stage-w); height: var(--vs-stage-h);
|
||
transform: translate(-50%, -50%) scale(var(--vs-scale));
|
||
transform-origin: center center;
|
||
background: radial-gradient(120% 90% at 50% 40%, #16161f 0%, #0a0a0e 65%, #050507 100%);
|
||
overflow: hidden;
|
||
will-change: transform;
|
||
}
|
||
|
||
/* Skip button lives OUTSIDE the scaled stage so it stays at real pixels
|
||
in the top-right of the video area at any container size. */
|
||
#vsScreen .vs-skip {
|
||
position: absolute; top: 12px; right: 14px; z-index: 50;
|
||
background: rgba(10,10,14,.72);
|
||
color: #fff; border: 1px solid rgba(255,255,255,.28);
|
||
padding: 6px 14px;
|
||
font: 700 12px/1 'Barlow Condensed', sans-serif;
|
||
letter-spacing: .22em; text-transform: uppercase;
|
||
cursor: pointer;
|
||
backdrop-filter: blur(6px);
|
||
transition: background .15s, border-color .15s;
|
||
}
|
||
#vsScreen .vs-skip:hover { background: rgba(10,10,14,.92); border-color: rgba(255,255,255,.55); }
|
||
#vsScreen .vs-skip #vsSkipTimer { display: inline-block; margin-left: 6px; opacity: .8; }
|
||
|
||
/* ══ EVERYTHING BELOW: absolute pixels on the 1920×1080 canvas ══ */
|
||
|
||
/* ── Side panels ── */
|
||
#vsScreen .vs-panel { position: absolute; overflow: hidden; }
|
||
#vsScreen .vs-panel-red {
|
||
inset: 0 auto 0 0; width: 56%;
|
||
background: oklch(0.28 0.09 25);
|
||
clip-path: polygon(0 0, 100% 0, 82% 100%, 0 100%);
|
||
animation: vsPanelL .9s cubic-bezier(.22,1,.36,1) both;
|
||
}
|
||
#vsScreen .vs-panel-blue {
|
||
inset: 0 0 0 auto; width: 56%;
|
||
background: oklch(0.28 0.09 255);
|
||
clip-path: polygon(18% 0, 100% 0, 100% 100%, 0 100%);
|
||
animation: vsPanelR .9s cubic-bezier(.22,1,.36,1) both;
|
||
}
|
||
#vsScreen .vs-panel-photo {
|
||
position: absolute; inset: 0;
|
||
background-size: cover; background-position: center;
|
||
animation: vsSlowDrift 18s ease-in-out infinite;
|
||
}
|
||
#vsScreen .vs-panel-photo-blue { animation-direction: reverse; }
|
||
#vsScreen .vs-panel-scrim-red { position: absolute; inset: 0; pointer-events: none;
|
||
background: linear-gradient(115deg, oklch(0.45 0.18 25 / 0.55) 0%, transparent 55%); }
|
||
#vsScreen .vs-panel-scrim-blue { position: absolute; inset: 0; pointer-events: none;
|
||
background: linear-gradient(245deg, oklch(0.45 0.15 255 / 0.55) 0%, transparent 55%); }
|
||
#vsScreen .vs-panel-gradient {
|
||
position: absolute; inset: 0; pointer-events: none;
|
||
background:
|
||
linear-gradient(to top, rgba(5,5,7,.95) 0%, rgba(5,5,7,.72) 22%, rgba(5,5,7,.30) 42%, transparent 62%),
|
||
linear-gradient(to bottom, rgba(5,5,7,.82) 0%, rgba(5,5,7,.35) 18%, transparent 32%);
|
||
}
|
||
|
||
/* ── Divider ── */
|
||
#vsScreen .vs-divider {
|
||
position: absolute; top: -6%; bottom: -6%; left: 50%;
|
||
width: 3px; margin-left: -1.5px;
|
||
transform: rotate(10.15deg);
|
||
background: linear-gradient(to bottom, transparent, oklch(0.85 0.16 85 / .9) 20%, oklch(0.85 0.16 85 / .9) 80%, transparent);
|
||
pointer-events: none; filter: blur(1px);
|
||
}
|
||
|
||
/* ── Fighter identity blocks ── (absolute px on the 1920×1080 canvas) */
|
||
#vsScreen .vs-info {
|
||
position: absolute; z-index: 6; max-width: 44%;
|
||
display: flex; flex-direction: column; gap: 13px;
|
||
}
|
||
#vsScreen .vs-info-red { left: 43.2px; bottom: 118.8px; align-items: flex-start; animation: vsRiseUp .8s .7s cubic-bezier(.22,1,.36,1) both; }
|
||
#vsScreen .vs-info-blue { right: 43.2px; bottom: 118.8px; align-items: flex-end; text-align: right; animation: vsRiseUp .8s .85s cubic-bezier(.22,1,.36,1) both; }
|
||
|
||
#vsScreen .vs-corner-tag {
|
||
font: 800 21.6px/1 'Barlow Condensed', sans-serif;
|
||
letter-spacing: .35em; color: #fff;
|
||
padding: 5.4px 15.1px 5.4px 18.9px;
|
||
}
|
||
#vsScreen .vs-corner-tag-red { background: oklch(0.55 0.20 25); }
|
||
#vsScreen .vs-corner-tag-blue { background: oklch(0.50 0.16 255); }
|
||
|
||
#vsScreen .vs-flag-row { display: flex; align-items: center; gap: 15.1px; }
|
||
#vsScreen .vs-flag-row-r { flex-direction: row-reverse; }
|
||
#vsScreen .vs-flag {
|
||
width: 56.2px; height: auto; aspect-ratio: 4/3;
|
||
background-size: cover !important; background-position: center !important;
|
||
border: 1px solid rgba(255,255,255,.35);
|
||
box-shadow: 0 4px 18px rgba(0,0,0,.6);
|
||
display: inline-block; line-height: 0;
|
||
}
|
||
#vsScreen .vs-country { font: 700 32.4px/1 'Barlow Condensed', sans-serif; letter-spacing: .28em; }
|
||
#vsScreen .vs-country-red { color: oklch(0.85 0.05 25); }
|
||
#vsScreen .vs-country-blue { color: oklch(0.85 0.05 255); }
|
||
|
||
#vsScreen .vs-name {
|
||
font-family: 'Anton', 'Barlow Condensed', sans-serif;
|
||
font-size: 71.3px; line-height: .95;
|
||
text-transform: uppercase; color: #fff;
|
||
text-shadow: 0 6px 30px rgba(0,0,0,.8);
|
||
}
|
||
|
||
#vsScreen .vs-club-row { display: flex; align-items: center; gap: 13px; margin-top: 4.3px; }
|
||
#vsScreen .vs-club-row-r { flex-direction: row-reverse; }
|
||
#vsScreen .vs-club-logo {
|
||
width: 69.1px; height: 69.1px;
|
||
border-radius: 50%;
|
||
background: rgba(255,255,255,.06);
|
||
background-size: cover; background-position: center;
|
||
border: 1px solid rgba(255,255,255,.2);
|
||
}
|
||
#vsScreen .vs-club {
|
||
font: 600 30.2px/1.1 'Barlow Condensed', sans-serif;
|
||
letter-spacing: .12em; text-transform: uppercase;
|
||
color: rgba(232,230,224,.9);
|
||
}
|
||
|
||
/* Fighter chips row (Record / Rank / Stats) — from the original artifact. */
|
||
#vsScreen .vs-chips-row {
|
||
display: flex; flex-wrap: wrap; gap: 9.7px; margin-top: 5.4px;
|
||
}
|
||
#vsScreen .vs-info-blue .vs-chips-row { justify-content: flex-end; }
|
||
#vsScreen .vs-chip-fighter {
|
||
font: 700 23.8px/1 'Barlow Condensed', sans-serif;
|
||
letter-spacing: .12em; text-transform: uppercase;
|
||
padding: 5.4px 14px;
|
||
background: rgba(10,10,14,.62);
|
||
border: 1px solid rgba(255,255,255,.25);
|
||
color: #fff;
|
||
}
|
||
#vsScreen .vs-chip-fighter-gold {
|
||
border-color: oklch(0.85 0.16 85 / .6);
|
||
color: oklch(0.87 0.14 85);
|
||
}
|
||
|
||
/* ══ Placeholder hiding ══
|
||
Every placeholder DOM element is always rendered. Empty ones are hidden
|
||
here — this keeps the design's HTML shape intact (same as the artifact)
|
||
while adapting the visible layout to whatever data actually exists. */
|
||
#vsScreen .vs-nullable:empty { display: none; }
|
||
/* When every direct child of a "row" wrapper is hidden, the row collapses too. */
|
||
#vsScreen .vs-nullable-row:not(:has(> :not(.vs-nullable-hide):not(.vs-nullable:empty))) { display: none; }
|
||
/* Image slots explicitly marked as missing (flag / club logo without data). */
|
||
#vsScreen .vs-nullable-hide { display: none; }
|
||
|
||
/* Bottom chips: whole chip hides when its value is empty, and the
|
||
diamond separators next to a hidden chip hide too. */
|
||
#vsScreen .vs-chip.vs-nullable-chip[data-vs-empty="1"] { display: none; }
|
||
#vsScreen .vs-bottom .vs-diamond-match-court:has(+ .vs-chip[data-vs-empty="1"]),
|
||
#vsScreen .vs-bottom .vs-chip[data-vs-empty="1"] + .vs-diamond-court-ref,
|
||
#vsScreen .vs-bottom .vs-chip[data-vs-empty="1"] + .vs-diamond-match-court,
|
||
#vsScreen .vs-bottom .vs-diamond-court-ref:has(+ .vs-chip[data-vs-empty="1"]) { display: none; }
|
||
|
||
/* ── Top (event / stage / weight) ── */
|
||
#vsScreen .vs-top {
|
||
position: absolute; top: 34.6px; left: 50%; transform: translateX(-50%);
|
||
display: flex; flex-direction: column; align-items: center; gap: 10.8px;
|
||
z-index: 8; width: 92%; pointer-events: none;
|
||
animation: vsDropIn .8s .5s cubic-bezier(.22,1,.36,1) both;
|
||
}
|
||
#vsScreen .vs-top-event {
|
||
font: 700 30.2px/1.05 'Barlow Condensed', sans-serif;
|
||
letter-spacing: .42em; text-transform: uppercase;
|
||
color: rgba(232,230,224,.92); text-align: center;
|
||
text-shadow: 0 2px 14px rgba(0,0,0,.9);
|
||
}
|
||
#vsScreen .vs-top-stage-row { display: flex; align-items: center; gap: 17.3px; }
|
||
#vsScreen .vs-top-stage-line { height: 2px; width: 64.8px; }
|
||
#vsScreen .vs-top-stage-line-l { background: linear-gradient(to left, oklch(0.85 0.16 85), transparent); }
|
||
#vsScreen .vs-top-stage-line-r { background: linear-gradient(to right, oklch(0.85 0.16 85), transparent); }
|
||
#vsScreen .vs-top-stage {
|
||
font-family: 'Anton', 'Barlow Condensed', sans-serif;
|
||
font-size: 38.9px;
|
||
letter-spacing: .30em; padding-left: .3em;
|
||
color: oklch(0.85 0.16 85); text-transform: uppercase;
|
||
}
|
||
#vsScreen .vs-top-weight {
|
||
font: 700 42px/1.1 'Anton', 'Barlow Condensed', sans-serif;
|
||
letter-spacing: .28em; padding-left: .28em; text-transform: uppercase;
|
||
color: #ffffff;
|
||
text-shadow: 0 2px 14px rgba(0,0,0,.9);
|
||
}
|
||
|
||
/* ── Center VS ── */
|
||
#vsScreen .vs-center {
|
||
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -52%);
|
||
z-index: 7; pointer-events: none;
|
||
display: flex; align-items: center; justify-content: center;
|
||
animation: vsSlam .7s 1.1s cubic-bezier(.22,1,.36,1) both;
|
||
}
|
||
#vsScreen .vs-word {
|
||
position: relative;
|
||
font-family: 'Anton', 'Barlow Condensed', sans-serif;
|
||
font-size: 183.6px;
|
||
font-style: italic;
|
||
color: #fffdf5; line-height: 1;
|
||
-webkit-text-stroke: 2px oklch(0.85 0.16 85 / 0.6);
|
||
animation: vsPulse 2.4s ease-in-out infinite;
|
||
overflow: visible;
|
||
}
|
||
#vsScreen .vs-shine-wrap { position: absolute; inset: -10% -20%; overflow: hidden; pointer-events: none; }
|
||
#vsScreen .vs-shine {
|
||
position: absolute; top: 0; bottom: 0; width: 34%;
|
||
background: linear-gradient(to right, transparent, rgba(255,255,255,.16), transparent);
|
||
animation: vsShine 5s ease-in-out infinite;
|
||
}
|
||
|
||
#vsScreen .vs-flash {
|
||
position: absolute; inset: 0; background: #fff;
|
||
opacity: 0; pointer-events: none; z-index: 9;
|
||
animation: vsFlash .9s 1.5s ease-out both;
|
||
}
|
||
|
||
/* ── Bottom chips ── */
|
||
#vsScreen .vs-bottom {
|
||
position: absolute; bottom: 32.4px; left: 50%; transform: translateX(-50%);
|
||
display: flex; gap: 17.3px; z-index: 8; align-items: center;
|
||
flex-wrap: wrap; justify-content: center; max-width: 94%;
|
||
animation: vsRiseC .8s 1.3s cubic-bezier(.22,1,.36,1) both;
|
||
}
|
||
#vsScreen .vs-chip {
|
||
display: flex; align-items: baseline; gap: 8.6px;
|
||
background: rgba(10,10,14,.72);
|
||
border: 1px solid oklch(0.85 0.16 85 / .45);
|
||
padding: 10.8px 23.8px; backdrop-filter: blur(6px);
|
||
}
|
||
#vsScreen .vs-chip-label {
|
||
font: 600 23.8px/1 'Barlow Condensed', sans-serif;
|
||
letter-spacing: .30em; text-transform: uppercase;
|
||
color: rgba(232,230,224,.65);
|
||
}
|
||
#vsScreen .vs-chip-value {
|
||
font-family: 'Anton', 'Barlow Condensed', sans-serif;
|
||
font-size: 34.6px; color: #fff;
|
||
}
|
||
#vsScreen .vs-chip-value-name {
|
||
font-family: 'Barlow Condensed', sans-serif;
|
||
font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
|
||
font-size: 28.1px;
|
||
}
|
||
#vsScreen .vs-diamond {
|
||
width: 6px; height: 6px; transform: rotate(45deg);
|
||
background: oklch(0.85 0.16 85);
|
||
}
|
||
|
||
/* ── Portrait canvas (1080×1920): stack layout ── */
|
||
#vsScreen.vs-portrait { --vs-stage-w: 1080px; --vs-stage-h: 1920px; }
|
||
#vsScreen.vs-portrait .vs-panel-red { inset: 0 0 auto 0; width: 100%; height: 56%;
|
||
clip-path: polygon(0 0, 100% 0, 100% 82%, 0 96%); animation-name: vsPanelT; }
|
||
#vsScreen.vs-portrait .vs-panel-blue { inset: auto 0 0 0; width: 100%; height: 56%;
|
||
clip-path: polygon(0 18%, 100% 4%, 100% 100%, 0 100%); animation-name: vsPanelB; }
|
||
#vsScreen.vs-portrait .vs-divider {
|
||
left: -4%; right: -4%; top: 50%; bottom: auto;
|
||
width: auto; height: 3px; margin-top: -1.5px; margin-left: 0;
|
||
transform: rotate(-4.48deg);
|
||
background: linear-gradient(to right, transparent, oklch(0.85 0.16 85 / .9) 20%, oklch(0.85 0.16 85 / .9) 80%, transparent);
|
||
}
|
||
#vsScreen.vs-portrait .vs-info-red { left: 43.2px; top: 129.6px; bottom: auto; }
|
||
#vsScreen.vs-portrait .vs-info-blue { right: 43.2px; bottom: 129.6px; top: auto; }
|
||
|
||
/* ── Animations ── */
|
||
@keyframes vsPulse {
|
||
0%,100% { text-shadow: 0 0 30px rgba(255,215,120,.55), 0 0 90px rgba(255,170,60,.3); transform: scale(1); }
|
||
50% { text-shadow: 0 0 65px rgba(255,220,130,1), 0 0 160px rgba(255,170,60,.7); transform: scale(1.045); }
|
||
}
|
||
@keyframes vsShine { 0% { transform: translateX(-130%) skewX(-18deg); } 60%,100% { transform: translateX(230%) skewX(-18deg); } }
|
||
@keyframes vsSlowDrift { 0% { transform: translate3d(0,0,0) scale(1.02); } 50% { transform: translate3d(0,-1.2%,0) scale(1.05); } 100% { transform: translate3d(0,0,0) scale(1.02); } }
|
||
@keyframes vsPanelL { from { transform: translateX(-105%); } to { transform: translateX(0); } }
|
||
@keyframes vsPanelR { from { transform: translateX( 105%); } to { transform: translateX(0); } }
|
||
@keyframes vsPanelT { from { transform: translateY(-105%); } to { transform: translateY(0); } }
|
||
@keyframes vsPanelB { from { transform: translateY( 105%); } to { transform: translateY(0); } }
|
||
@keyframes vsRiseUp { from { opacity: 0; transform: translateY(43.2px); } to { opacity: 1; transform: translateY(0); } }
|
||
@keyframes vsRiseC { from { opacity: 0; transform: translate(-50%, 43.2px); } to { opacity: 1; transform: translate(-50%, 0); } }
|
||
@keyframes vsDropIn { from { opacity: 0; transform: translate(-50%, -32.4px); } to { opacity: 1; transform: translate(-50%, 0); } }
|
||
@keyframes vsSlam {
|
||
0% { opacity: 0; transform: translate(-50%,-52%) scale(3.4) rotate(-6deg); }
|
||
60% { opacity: 1; transform: translate(-50%,-52%) scale(.92) rotate(1deg); }
|
||
80% { transform: translate(-50%,-52%) scale(1.06); }
|
||
100% { opacity: 1; transform: translate(-50%,-52%) scale(1) rotate(0deg); }
|
||
}
|
||
@keyframes vsFlash { 0% { opacity: 0; } 12% { opacity: .85; } 100% { opacity: 0; } }
|
||
</style>
|
||
|
||
<script>
|
||
(function () {
|
||
const VS_INTRO_SECONDS = 6;
|
||
|
||
const vs = document.getElementById('vsScreen');
|
||
if (!vs) return;
|
||
const video = document.getElementById('videoPlayer');
|
||
const timerEl = document.getElementById('vsSkipTimer');
|
||
let hidden = false, tickInt = null;
|
||
|
||
// ── Fit the fixed 1920×1080 (or 1080×1920) canvas to the parent box.
|
||
function fit() {
|
||
const host = vs.parentElement;
|
||
if (!host) return;
|
||
const w = host.clientWidth, h = host.clientHeight;
|
||
if (!w || !h) return;
|
||
const portrait = h > w * 1.05;
|
||
vs.classList.toggle('vs-portrait', portrait);
|
||
const sw = portrait ? 1080 : 1920;
|
||
const sh = portrait ? 1920 : 1080;
|
||
const scale = Math.min(w / sw, h / sh);
|
||
vs.style.setProperty('--vs-scale', scale);
|
||
}
|
||
fit();
|
||
if (window.ResizeObserver && vs.parentElement) {
|
||
new ResizeObserver(fit).observe(vs.parentElement);
|
||
} else {
|
||
window.addEventListener('resize', fit);
|
||
}
|
||
|
||
// ── Hold the video at t=0 while the intro counts down ──────────
|
||
// Autoplay may already have kicked in before this script ran; pause
|
||
// it, keep it pinned at the start, and only release it at 0.
|
||
function pinVideo() {
|
||
if (!video) return;
|
||
try { video.pause(); } catch (_) {}
|
||
try { video.currentTime = 0; } catch (_) {}
|
||
}
|
||
pinVideo();
|
||
// Some browsers fire 'play' immediately after we pause (autoplay
|
||
// retry). Re-pause until the intro finishes.
|
||
const playGuard = () => { if (!hidden) pinVideo(); };
|
||
if (video) video.addEventListener('play', playGuard);
|
||
|
||
function releaseVideo() {
|
||
if (!video) return;
|
||
video.removeEventListener('play', playGuard);
|
||
try { video.currentTime = 0; } catch (_) {}
|
||
const p = video.play();
|
||
if (p && typeof p.catch === 'function') p.catch(() => {});
|
||
}
|
||
|
||
// ── Dismiss overlay + start playback ───────────────────────────
|
||
function hide() {
|
||
if (hidden) return;
|
||
hidden = true;
|
||
if (tickInt) { clearInterval(tickInt); tickInt = null; }
|
||
releaseVideo();
|
||
vs.classList.add('vs-hide');
|
||
setTimeout(() => { vs.remove(); }, 400);
|
||
}
|
||
|
||
document.getElementById('vsSkip')?.addEventListener('click', (e) => {
|
||
e.stopPropagation(); e.preventDefault(); hide();
|
||
});
|
||
|
||
// ── Real-time countdown, independent of video timeline ─────────
|
||
const started = performance.now();
|
||
function tick() {
|
||
const elapsed = (performance.now() - started) / 1000;
|
||
const left = Math.max(0, VS_INTRO_SECONDS - elapsed);
|
||
if (timerEl) timerEl.textContent = Math.ceil(left);
|
||
if (left <= 0) hide();
|
||
}
|
||
tick();
|
||
tickInt = setInterval(tick, 100);
|
||
})();
|
||
</script>
|
||
@endif
|