ghassan 665bad76bf Match cards: composed title, header stage from match title, equal-sized grid
- SportsMatchController syncs Video.title with SportsMatch.title on
  store/update so the "Match title" field drives the video's display
  title everywhere.
- headerData(): "championship" now prefers event_name (event title)
  and adds a dedicated "stage" key sourced from SportsMatch.title
  (shown between the yellow lines above the weight class in both the
  in-player VS overlay and the vs-mini gallery card).
- Video cards render match titles with flags + corner colours
  ("FINALS – 🇧🇭 Blue vs 🇧🇭 Red (-61 kg)"), matching the video
  page header, and clip long titles to one line.
- Video gallery grid uses repeat(N, minmax(0, 1fr)) + grid-auto-rows:1fr
  so every card thumbnail renders at exactly the same size; hover-preview
  videos use object-fit:cover to stop portrait sources looking smaller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-11 09:14:25 +03:00

562 lines
27 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{-- ══════════════════════════════════════════════════════════════════════
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'] ?? '');
// Sub-header stage between the two yellow lines above the weight class.
// Sourced from headerData().stage (championship_name → division → format).
$vsStage = $vsStr($hd['stage'] ?? '');
$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