ghassan 04d63cccdb VS intro screen (arena design), card VS mini + live scorebar, full country names, sports-match edit fix
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>
2026-08-10 18:34:31 +03:00

560 lines
26 KiB
PHP
Raw 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'] ?? '');
$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