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) <[email protected]>
This commit is contained in:
ghassanandClaude Opus 4.7 committed 2026-08-10 18:34:31 +03:00
1 parent 5b6bd86d70
commit 04d63cccdb
16 files changed
+2351 -411

No files matched your search

+391
View File
@@ -0,0 +1,391 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a0e; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #0a0a0e; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<polygon points="0,0 620,0 480,800 0,800" fill="#7a2226"></polygon>
<polygon points="580,0 1200,0 1200,800 720,800" fill="#22357a"></polygon>
<text x="600" y="470" font-family="Arial Black, Arial, sans-serif" font-size="220" font-weight="900" font-style="italic" text-anchor="middle" fill="#fffdf5" stroke="#e8c265" stroke-width="4">VS</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">
{"634cd639-9aa5-4e99-93cf-a35ae77b2724":{"mime":"image/webp","compressed":false,"data":"UklGRvTaAQBXRUJQVlA4WAoAAAAgAAAAkQMArwQASUNDUMgBAAAAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADZWUDggBtkBAHBCB50BKpIDsAQ+USaPRaOiISclkmqI4AoJaUCysa0j+oobVxBkm/YlAdvU4vUrVn6p/y94G65+i5wliAHgw8xP2g6YP/E9Bf6W6fv+j48ftDHnGLqhM79X/6s2CCH3N0IXpT46b8k8mP0D/A72/z37l/sf5H0JPq7xJex/4P/1/3/qP/R/0d/h/y/tn/zPAn9o/wPQO/WP8l/5vVqgWeGqBO4q+9/+3+29Zf2L/V//L/S/AJ/Rv8B/6P8t7c/9zxXfyf/S/cz4Cf7D/r/WD/5f/96Cv2n/pew5+037adu0oBDuCHcEO4IdwQ7gh3BDuCHcEO4IdwQ7gh3BDuCHcEO4IdwQ7gh3BDuCHcEO4IdwQ7gh3BDuCHcEO4IdwQ7gh3BDuCHcEO4IdwQ7gh3BDuCHcEO4IdwQ7gh3BDuCHcEO4IdwQ7gh3BDuCHcEO4IdwQ7gh3BDuCHcEO4IdwQ7gh3BDuCHcEO4IdwQ7gh3BDuCHcEO4IdwQ7gh3BDuCHcEO4IdwQ7gh3BDuCHcEO4IdwQ7gh3BDuCHcEO4IdwQ7gh3BDuCHcDjwXbEMuTM5CeDg0/WbfYG+wN9gb7A32BvsDfYG+wN9gb7A32BTOiZ/s/u9oCmXy05wZPHPMfTnL633x7vzZzRd7E74ap4Mp2WUd0VyWrhkClM+spcZ1VTpB0aQjld7eiVLVtmAfWEdANCJ0eFvsDfYG+wN9gb7A32BvsDfYG+wN9gbJLYFTAG0xAzI6yHjv74Wcp42UesdhAWEll0Mz77JaUHlulxgqy+mQczgDbGkrVJcHFlufap6I14MODzHIj4F5x2QValCk7J2+xcfxzZ5PjhYM+KVx57CekqiCUSkolJRKSiUlEpKJSUSkolJRI5nMN8+PfaP/ppGXaK90lr75ZWuF9bNrauU95rEzdUb1C1qZvsyk6ee95lySUR1MMLe+EaEbA+E+8nlgXyG6OR9nH0ZnNIalWTZMQkfeTPYorOy1Sbnr6WUqF6yG3G36Um43KVRDvK9XenCUSkolJRKSiUlEpKJSUSkolJQw8SkbOxva7a/dbRhay/9p6k1ps2tLL3PsGeB3wmUc2O1r6Ko/YIvzJQc3U823Qht+gk3DOwMXSPzPd1Rvk4qGS4lADC6jMNLREiRDWbffaISXTWM1SYEApKk342E26eK2J/CS/KKc8EVwEA5hmsX9nP8COuVi3YchhVhMrBh3BDuCHcEO4IdwQ7gh3BDuBiybDoJyHc6CgsI2nhNxcK4ivBgZvAqaCNDzcelDlw/42MaI44iLd6aOUkPa8wOZjckLrCfg74lJifr6Fl7z9i11am5PIHpxW5Lh4k3BIe8znZeLqHsmSU30RQmaCNtofvX3laWBKIwywl3UO+EhtmqXCHcXy9ynkiDU0YfurvhvE7zx522PN2UlEpKJSUSkolJRKSiUlEpJMXhiiR1qJ8wdnNkUdpHrFPa6B9Trd/6xgHcCtMzbEUX/yr6C3e1ucYiafg9kimrNPFPNxmJOBhvdJ6EmCz8EO4r/DHV+z4OTGDW6wudaAGkifd4DZNTLCVHs3KSzJSwdsATv7gqklIqM1QZFTGQHReShA0/1asKVrvbdM+rofcn/JfddtaGmGOujFdGK6MV0YroxXRiujFNpLVXaxIvpx3hifcd7liCmLjfmrHyV65wDvnMSivzOk/Z1pIBa4mVBTb0y7SwY+7z8xyyMmQh7kNSCrFl216OJd35YarFe6rX2AfRpOHXPXxGUOSSr2krq58AObG7z0K9/pMo/OCzuaroHIiScJkSv32huF6MHQ1EzJJR5UpRGEUD3GcQugdd7ZLSdd7C/gNgidIbgEXlrqT32z1GaJEh6JD0SHokPRIeiQ9Eh6AoJPaRhTn8rsZCbovqMXKFY/fqFYv4kryByMluIi9b85ACjpfIpD/n+uHIEY1/vy9NW8J3mHKIVMWvDrHwvArTbWahABxEn/4MfQwOL8e7qWBz036DLHmnEb6wb/G5cLoWVPRU0HNWVPMI0D+hUvL4wSEXmqJSoqWpegUU2X3xLFRTvB+Ad8dVl8Mu0Q9+vxUoVpeb8YgLCE9kxSkIOV+AfV5eCHcEO4IdwQ7gh3BDuCFudkHNn5AZZJm1k14VrlyYOrkTelg986azm/sU7cIQaLRH3awJCRbooUEC1Ek75bzWrEehXhOOoXXASBjvqypB4A+nf45d6rSL0PzbPObtdlLIInJHdRsx3AXtWpgkXu56zIjFFO/o+8RrGqY5H+7+eh0q2fm/qMZo82AyCujHkwpC6uQnxt+ql1w7GbuDk37GsOcp5hMnB2QCSIgqpHEs1irSpVkFAqnf9QB4e32BvsDfYG+wN9gb7A3xLiZxiATkzmUJil8/0ZNBhV4qpM2PxOXeZXpwESkXrLO9Y72EVtFaCZOhDEXNyNUDS3bqkmbuaB//9p5MGu0n7KE+Sua45KtgLXknZ58fqHl3wp/qhSl1RmR4H7OCRsH1LSr9cTAqRdB1XbiZQXi8l5dgFyzr8NYMcytr0Rma04S4DasMs7H7eT6pgUE4VvYL1kuHUgW6rEZHE3cyrodkBF3xzc8cEUIbKndn0KwnRBKJSUSkolJRKSiUlEo6kOI5c0wxEFWb8fKoVSLzQhwoEV2MY9584Z4wHZdqj5hEWu76ISmrh5Uwg1qecy/b56qr4pOkVwH191QtcafaXzo+AIveV774pKr6odSyZVIg5ZIljpmLXwcOuu7Z6h73OvualT+AcqPMvtZtPHvgrjbmUdKcPrrHt+qPrbYijbyHrtylxcO+2AYr3qSd0GzGfmHYF7w1WGCGtFUQygCMWGgHldn3uCTnMm4LrWiOLj+zKHZXpLqgva1UJn0QN4gbxA3iBvEDeIG8BeGRNnhORiTRsYCTwm/Wyboie+RH2TDnrkA49wBhTEJOPZdDoPkh9bID5uGOVRyd6KJ/LZKWedSQT6xv/kd1jsNnX4Gcq+KpK+ks1iLdxAvL9dVOZgQCfbhRJ9Ud6IbNRhpNxA8CHurPmsBAQnl3oGyYIchZBfFZhEx4htp24RLwPhFdG3ePXKgWQKgdLAnipfcb4hsmdCbQcb8YTYK9jf0I+Lf8SWjeRLN123mPthgspiCA9QroxXRiujFdGK6MV0Il+A16DJE5OEN3WAAO8kM0Gy485YQCYkQdlxoxxq4P6dDkoToeua2tOq3FGHc7nVRxF9OZOwhai69+EtTg3HXkEzcTBRRL6ZG9FrDA386i697WywkkzNmh1WSxHSWZ7lLLNn1WEr7lcy0VqClSbvIvMah3YoGEMG0WJDTxY79+LgT4lHBErwe/2kgQ/nCotjPRHK+BonkFVqtkCujFdGK6MV0YroxXRfPkMZzx6+PtaN2HAqNrO/UfyYO3iuO87jvHAsmWu9zk45ub4UsguiESaAjsu1dn2Evvmgtl0hy4xNxwTlgC2xlW7XU+C2J4TxB0u55GgYDmwnDNbdsArbMWq5ZGOMAjfUFR8gH+RmxmtRJ72OF3+RbVt+d8nIILRohEJLI7oIEqjvf/t+dzC3h3MqL4+Hgpa1WQgB2/1A7fYG+wN9gb7A32BukcZ3BdYzmixZOKsedwI+gJZIEDEVvq2SYOBlfC5HWKGRKbyrrUdklIWAX0QMNmf7j/DfV98yzm0asQT1pPnq5/xg+0+QOWgJIvkpUWXp/o5qqGwgmPBfImydZJwnTmzk1+K4jAWI6RWlDOaXMTWLuMH4WlOUc1FaDooj0wDXJ7+mJnAkLpNQEYHjah7GD3EV2Yk3a6MV0YroxXRiujFbwBf8c8AZMz+AWk6vaLearIoD6L134Ryaahu7ieexHDqchVojiWPc1bOmbZjGao22xkxeqLOSMQA3uOLPoN0tJ0OsldnW3U0bW9KtZD7LP57F5tAmYRkz7j8cHvUnSzVbfIwFVz7ESOXwQ2qG1BjGg1r3M3oAzfNa2lk1krwG0o8BqOng2vr1+YPQvF+GJWIRH2QyotACNbIfhvoN570YroxXRiujFdGK6EeiOZU5zmBSHaf6HJggxi2sKqoXKnYq+OYy5NwTDmHOB0MjNm79HKeAxMnaXfn6KNXaUIuyrUAg3l0+HUpmJF7pUSrIROA4y+eqbDB4QUTuRnZszseLy5uQSnW+2kbHuwEj9WeRW8TXS5r4mhL7MkqNxC3L4lijAjLundafKs4kV3W+sqBcE8VcdxLbE36k9MaVY93F882ef8bVdIG7nQL9sfp8ThWqUlEpLine truncated
</script>
<script type="__bundler/ext_resources">
[{"id":"flagJp","uuid":"8f819462-17b1-46f6-8aa3-2de374e257f7"},{"id":"flagBr","uuid":"cc3aaea8-173f-4338-b7fa-2bae75a84d2b"},{"id":"https://unpkg.com/[email protected]/umd/react.production.min.js","uuid":"dc229245-056d-4703-a7d0-641172e38b84"},{"id":"https://unpkg.com/[email protected]/umd/react-dom.production.min.js","uuid":"96676beb-9588-43e1-aef2-459578e9a205"}]
</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"efd1862c-8a2a-4b74-9741-9eb4a7b79af6\"><\u002Fscript>\n\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<style>/* vietnamese */\n@font-face {\n font-family: 'Anton';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"786f769a-eb68-482a-90e4-5f4c6341055c\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Anton';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e50aced0-6710-4914-b239-a64494fce97d\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Anton';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"66dcdbee-b198-4b92-90c2-93c3b6f9111b\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Barlow Condensed';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"0be5c901-33ac-4507-bb55-5dafed4159e7\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Barlow Condensed';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"2f8f7fe2-ce99-4a10-b583-ecf8c782a45d\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Barlow Condensed';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"754fb47d-ebf2-4909-b5a0-f9abb2ed5c4d\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Barlow Condensed';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"7b059d00-08e1-424c-9715-c5b65f9a9411\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Barlow Condensed';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"faa370e3-1d36-48e0-b7c4-1a760ef73c77\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Barlow Condensed';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"dcfb04b8-57f1-4948-b81e-dcad7d26a879\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Barlow Condensed';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"eb64d5f4-973e-4865-8cd1-89be941b16be\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Barlow Condensed';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"11b56c5f-a515-42d7-bf11-6aa8bed5ecf6\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Barlow Condensed';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"3cf32598-e7d4-4db2-8d36-ceee8f725195\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152Line truncated
</script>
</body>
</html>