Files
tomtomgames/index.php
T
myron 3e642b97a7 Add mouse drag, wheel, and arrow buttons to token package pill scroller
- Click-drag: grab cursor, drag left/right to scroll
- Mouse wheel: vertical scroll converts to horizontal pan
- Arrow buttons (< >) appear at edges when more pills are off-screen,
  hide when scrolled to the end

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 11:30:45 +00:00

3417 lines
192 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.
<?php
ob_start();
require_once __DIR__ . '/../includes/config.php';
require_once __DIR__ . '/../includes/square.php';
ob_end_clean();
// Prevent Cloudflare Rocket Loader from deferring our scripts (breaks DOMContentLoaded listener)
header('Cache-Control: no-transform');
$_appVersion = '1.0.2';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#0a0a12">
<!-- ═══ PRIMARY SEO ═══════════════════════════════════════════ -->
<title>TomTomGames — Buy Game Tokens | VBlink777, Fire Kirin, Milky Way &amp; More</title>
<meta name="description" content="TomTomGames is your trusted token portal for top fish table and skill games. Buy tokens instantly for VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777 &amp; eGame99. Fast deposits, secure payments, 24/7 support.">
<meta name="keywords" content="buy game tokens, fish table games, VBlink777 tokens, Fire Kirin tokens, Milky Way game, Ultra Panda game, Panda Master tokens, Noble777, eGame99, online skill games, token portal, game credits">
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1">
<meta name="author" content="TomTomGames">
<link rel="canonical" href="https://tomtomgames.com/">
<!-- ═══ OPEN GRAPH (Facebook, Discord, iMessage) ═══════════════ -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://tomtomgames.com/">
<meta property="og:title" content="TomTomGames — Buy Game Tokens Instantly">
<meta property="og:description" content="The fastest way to buy tokens for VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777 &amp; eGame99. Secure payments, instant delivery.">
<meta property="og:image" content="https://tomtomgames.com/assets/img/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:site_name" content="TomTomGames">
<meta property="og:locale" content="en_US">
<!-- ═══ TWITTER / X CARD ══════════════════════════════════════ -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="TomTomGames — Buy Game Tokens Instantly">
<meta name="twitter:description" content="Buy tokens for Fish Table games — VBlink777, Fire Kirin, Milky Way &amp; more. Fast, secure, 24/7.">
<meta name="twitter:image" content="https://tomtomgames.com/assets/img/og-image.png">
<!-- ═══ STRUCTURED DATA (JSON-LD Schema) ═══════════════════════ -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://tomtomgames.com/#organization",
"name": "TomTomGames",
"url": "https://tomtomgames.com",
"logo": {
"@type": "ImageObject",
"url": "https://tomtomgames.com/assets/img/logo-icon.svg"
},
"contactPoint": {
"@type": "ContactPoint",
"contactType": "customer support",
"availableLanguage": "English"
},
"sameAs": []
},
{
"@type": "WebSite",
"@id": "https://tomtomgames.com/#website",
"url": "https://tomtomgames.com",
"name": "TomTomGames",
"description": "Token portal for fish table and skill games including VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777 and eGame99.",
"publisher": {"@id": "https://tomtomgames.com/#organization"},
"potentialAction": {
"@type": "SearchAction",
"target": "https://tomtomgames.com/?q={search_term_string}",
"query-input": "required name=search_term_string"
}
},
{
"@type": "WebApplication",
"@id": "https://tomtomgames.com/#app",
"name": "TomTomGames Token Portal",
"url": "https://tomtomgames.com",
"applicationCategory": "GameApplication",
"operatingSystem": "Web, iOS, Android",
"description": "Buy and manage game tokens for popular fish table games. Supports VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777, and eGame99.",
"offers": {
"@type": "AggregateOffer",
"priceCurrency": "USD",
"lowPrice": "5",
"highPrice": "500",
"offerCount": "6"
},
"featureList": [
"Instant token delivery",
"Secure Square payments",
"24/7 support chat",
"Multiple game platforms supported",
"Mobile-first design"
]
},
{
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What games does TomTomGames support?",
"acceptedAnswer": {
"@type": "Answer",
"text": "TomTomGames supports VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777, and eGame99 fish table and skill game platforms."
}
},
{
"@type": "Question",
"name": "How do I buy game tokens?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Create an account, select your game platform, choose a token package (5 to 100+ tokens), and pay securely by credit card, Venmo, Cash App, Chime, or Zelle. Tokens are credited to your account instantly after payment."
}
},
{
"@type": "Question",
"name": "How fast are tokens delivered?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Card payments are processed instantly. Manual payments via Venmo, Zelle, Cash App, or Chime are credited within minutes after we confirm receipt."
}
},
{
"@type": "Question",
"name": "Is TomTomGames safe?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. All card transactions are processed through Square, a PCI-compliant payment processor. We never store your full card details."
}
}
]
}
]
}
</script>
<!-- ═══ PWA / APP META ════════════════════════════════════════ -->
<link rel="manifest" href="/manifest.json">
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@400;600;700;900&family=Rajdhani:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#0a0a12;--bg2:#10101e;--bg3:#181828;--card:#1a1a2e;
--border:rgba(255,255,255,0.07);--gold:#f0c040;--gold2:#ffdc73;
--cyan:#00e5ff;--purple:#9b5de5;--green:#00e676;--red:#ff4444;
--yellow:#ffd60a;--text:#e8e8f0;--text2:#8888aa;
--radius:16px;--rsm:10px;
/* ── Typography Scale ── */
--fs-xs:11px;--fs-sm:13px;--fs-base:15px;--fs-md:16px;
--fs-lg:18px;--fs-xl:20px;--fs-2xl:24px;--fs-3xl:28px;
--lh:1.6;--lh-tight:1.3;
--glow-gold:0 0 20px rgba(240,192,64,0.4);
--max:480px;--nav-h:64px
}
html{-webkit-tap-highlight-color:transparent}
body{background:var(--bg);color:var(--text);font-family:'Rajdhani',sans-serif;font-size:17px;min-height:100dvh;overflow-x:hidden;-webkit-overflow-scrolling:touch}
body::before{content:'';position:fixed;inset:0;background-image:linear-gradient(rgba(0,229,255,0.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,229,255,0.03) 1px,transparent 1px);background-size:40px 40px;pointer-events:none;z-index:0}
/* AUTH */
.auth-screen{display:flex;flex-direction:column;min-height:100vh;min-height:100dvh;background:var(--bg);padding:40px 24px 32px;max-width:var(--max);margin:0 auto;width:100%}
.auth-logo{text-align:center;margin-bottom:36px}
.auth-logo-text{font-family:'Exo 2',sans-serif;font-weight:900;font-size:32px;display:flex;align-items:center;justify-content:center;gap:12px}.auth-logo-text span{background:linear-gradient(135deg,var(--gold),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.auth-logo-sub{font-size:15px;color:var(--text2);margin-top:4px;letter-spacing:1px}
.auth-tabs{display:flex;margin-bottom:24px;background:var(--bg3);border-radius:var(--rsm);padding:4px}
.auth-tab{flex:1;padding:11px;border:none;background:none;color:var(--text2);font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;border-radius:8px;cursor:pointer;transition:all .2s;letter-spacing:.5px}
.auth-tab.active{background:linear-gradient(135deg,rgba(240,192,64,.15),rgba(0,229,255,.08));color:var(--gold)}
/* MAIN */
#main-app{display:none;flex-direction:column;min-height:100vh;max-width:var(--max);margin:0 auto;position:relative;z-index:1;padding-bottom:0}
.topbar{position:sticky;top:0;z-index:100;background:rgba(10,10,18,.96);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 16px;height:58px;display:flex;align-items:center;justify-content:space-between}
.topbar-logo{font-family:'Exo 2',sans-serif;font-weight:900;font-size:18px;display:flex;align-items:center;gap:7px;letter-spacing:.5px}.topbar-logo span{background:linear-gradient(135deg,var(--gold),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.topbar-right{display:flex;align-items:center;gap:10px}
.token-badge{background:linear-gradient(135deg,#1a1a2e,#252540);border:1px solid var(--gold);border-radius:20px;padding:4px 12px;display:flex;align-items:center;gap:5px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--gold2);box-shadow:var(--glow-gold)}
.avatar-btn{width:34px;height:34px;border-radius:50%;background:linear-gradient(135deg,var(--purple),var(--cyan));border:none;cursor:pointer;font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:#fff;display:flex;align-items:center;justify-content:center}
/* PAGES */
.page{display:none;padding:16px;padding-bottom:84px}
/* ─── TOKEN COIN ──────────────────────────────────────────── */
.token-coin{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;flex-shrink:0}
.token-coin svg{display:block}
.page.active{display:block}
.page.active{display:block;animation:fadeUp .2s ease}
@keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
/* NAV */
.navbar{position:fixed;bottom:0;left:50%;transform:translateX(-50%);width:100%;max-width:var(--max);height:var(--nav-h);background:rgba(10,10,18,.98);backdrop-filter:blur(16px);border-top:1px solid var(--border);display:flex;align-items:center;justify-content:space-around;z-index:100;padding-bottom:env(safe-area-inset-bottom)}
.nav-btn{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;border:none;background:none;color:var(--text2);cursor:pointer;padding:8px 4px;transition:color .2s;font-family:'Rajdhani',sans-serif;font-weight:600;font-size:12px;letter-spacing:.5px}
.nav-btn svg{width:21px;height:21px}
.nav-btn.active{color:var(--gold)}
.nav-btn.active svg{filter:drop-shadow(0 0 5px rgba(240,192,64,.6))}
/* SECTION TITLE */
.section-title{font-family:'Exo 2',sans-serif;font-weight:700;font-size:18px;margin-bottom:16px;display:flex;align-items:center;gap:8px}
.section-title span{background:linear-gradient(135deg,var(--gold),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
/* CARDS */
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;margin-bottom:14px}
/* HERO BALANCE */
.hero-balance{background:linear-gradient(135deg,#1a1228,#121a28);border:1px solid rgba(240,192,64,.2);border-radius:var(--radius);padding:22px 20px;text-align:center;margin-bottom:20px;position:relative;overflow:hidden;box-shadow:0 0 0 1px rgba(240,192,64,.15),0 8px 32px rgba(0,0,0,.4)}
.hero-balance::before{content:'';position:absolute;top:-50px;right:-50px;width:160px;height:160px;background:radial-gradient(circle,rgba(240,192,64,.12) 0%,transparent 70%)}
.balance-label{font-size:13px;font-weight:700;color:var(--text2);letter-spacing:2px;text-transform:uppercase;margin-bottom:6px}
.balance-amount{font-family:'Exo 2',sans-serif;font-weight:900;font-size:48px;color:var(--gold);line-height:1;text-shadow:var(--glow-gold)}
.balance-unit{font-size:15px;color:var(--text2);margin-top:2px;letter-spacing:1px}
.balance-alias{margin-top:12px;font-size:15px;color:var(--text2)}
.balance-alias strong{color:var(--cyan)}
/* PLATFORM GRID */
.platform-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.platform-card{background:linear-gradient(145deg,#1a1a2e,#111120);border:1px solid var(--border);border-radius:14px;padding:14px 10px 12px;text-align:center;cursor:pointer;transition:all .2s;text-decoration:none;display:flex;flex-direction:column;align-items:center;gap:8px;position:relative;overflow:hidden}
.platform-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:var(--p-color);box-shadow:0 0 10px var(--p-color)}
.platform-card:active{transform:scale(.96)}
.platform-img-wrap{width:58px;height:58px;border-radius:14px;overflow:hidden;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.08)}
.platform-img-wrap img{width:58px;height:58px;object-fit:cover}
.platform-name{font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;color:var(--text)}
.play-btn{font-size:12px;color:var(--text2);font-weight:600;letter-spacing:.5px}
/* ─── PAYMENT FORM (milkyswipe style) ─── */
.pay-form{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);overflow:visible;margin-bottom:14px}
.pay-form-header{background:linear-gradient(135deg,#0d1a2e,#0a1220);padding:16px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;border-radius:var(--radius) var(--radius) 0 0}
.pay-form-icon{width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,var(--gold),#d4a017);display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
.pay-form-title{font-family:'Exo 2',sans-serif;font-weight:700;font-size:17px;color:var(--text)}
.pay-form-sub{font-size:14px;color:var(--text2);margin-top:1px}
.pay-form-body{padding:18px}
/* Package pills */
.pkg-scroll{display:flex;gap:8px;overflow-x:auto;padding-bottom:4px;margin-bottom:18px;-webkit-overflow-scrolling:touch;scrollbar-width:none;touch-action:pan-x}
.pkg-scroll::-webkit-scrollbar{display:none}
.pkg-pill{flex-shrink:0;background:var(--bg3);border:1.5px solid var(--border);border-radius:30px;padding:8px 16px;cursor:pointer;transition:all .2s;text-align:center;min-width:72px}
.pkg-pill.popular{border-color:var(--gold)}
.pkg-pill.selected{background:linear-gradient(135deg,rgba(240,192,64,.15),rgba(0,229,255,.08));border-color:var(--gold);box-shadow:0 0 12px rgba(240,192,64,.2)}
.pkg-pill-tokens{font-family:'Exo 2',sans-serif;font-weight:900;font-size:17px;color:var(--gold2)}
.pkg-pill-price{font-size:13px;color:var(--text2);font-weight:600;margin-top:1px}
.pkg-pill.popular .pkg-pill-tokens::after{content:' ★';font-size:12px;color:var(--gold)}
/* Amount display */
.amount-display{background:var(--bg3);border:1px solid var(--border);border-radius:var(--rsm);padding:14px 16px;margin-bottom:14px;display:flex;align-items:center;justify-content:space-between}
.amount-display-label{font-size:14px;color:var(--text2);font-weight:700;text-transform:uppercase;letter-spacing:1px}
.amount-display-value{font-family:'Exo 2',sans-serif;font-weight:900;font-size:22px;color:var(--gold)}
/* Form fields */
.fg{margin-bottom:14px}
.fg label{display:block;font-size:13px;font-weight:700;color:var(--text2);letter-spacing:1px;text-transform:uppercase;margin-bottom:7px}
.fi{width:100%;background:var(--bg3);border:1.5px solid var(--border);border-radius:var(--rsm);padding:13px 14px;color:var(--text);font-family:'Rajdhani',sans-serif;font-size:17px;font-weight:500;outline:none;transition:border-color .2s;-webkit-appearance:none;appearance:none}
.fi:focus{border-color:var(--cyan);box-shadow:0 0 0 3px rgba(0,229,255,.08)}
.fi-select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M0 0l6 8 6-8z' fill='%238888aa'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 14px center;padding-right:36px}
.fi-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
/* Custom token amount */
.custom-amt-row{background:linear-gradient(135deg,rgba(240,192,64,.06),rgba(0,229,255,.03));border:1.5px dashed rgba(240,192,64,.3);border-radius:var(--rsm);padding:12px 14px;margin-top:8px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
.custom-amt-label{font-size:13px;font-weight:700;color:var(--gold);letter-spacing:.5px;margin-bottom:6px}
.custom-amt-input-wrap{display:flex;align-items:center;background:var(--bg3);border:1.5px solid rgba(240,192,64,.3);border-radius:8px;overflow:hidden}
.custom-amt-dollar{padding:0 10px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:18px;color:var(--gold);background:rgba(240,192,64,.08);border-right:1px solid rgba(240,192,64,.2)}
.custom-amt-input{background:transparent;border:none;padding:10px 12px;color:var(--text);font-family:'Exo 2',sans-serif;font-size:18px;font-weight:700;width:110px;outline:none;-moz-appearance:textfield}
.custom-amt-input::-webkit-outer-spin-button,.custom-amt-input::-webkit-inner-spin-button{-webkit-appearance:none}
.custom-amt-input::placeholder{color:var(--text2);font-size:15px;font-weight:400}
.custom-amt-equiv{font-size:15px;color:var(--text2);white-space:nowrap}
.custom-amt-equiv strong{color:var(--gold2);font-family:'Exo 2',sans-serif;font-size:18px}
/* Order summary box */
.order-summary{background:linear-gradient(135deg,#1a1228,#0d1820);border:1px solid rgba(240,192,64,.2);border-radius:var(--rsm);padding:14px 16px;margin-bottom:16px}
.order-summary-row{display:flex;justify-content:space-between;align-items:center;padding:4px 0}
.order-summary-label{font-size:14px;color:var(--text2);font-weight:600;letter-spacing:.5px}
.order-summary-val{font-size:15px;color:var(--text);font-weight:600}
.order-summary-divider{height:1px;background:rgba(255,255,255,.08);margin:8px 0}
.order-summary-total{font-family:'Exo 2',sans-serif;font-weight:900;font-size:24px;color:var(--gold)}
/* Payment method tabs */
.pay-method-tabs{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;margin-bottom:16px}
.pmt{background:var(--bg3);border:1.5px solid var(--border);border-radius:var(--rsm);padding:10px 4px;text-align:center;cursor:pointer;transition:all .2s}
.pmt.active{border-color:var(--cyan);background:rgba(0,229,255,.06)}
.pmt-icon{font-size:20px;display:block;margin-bottom:3px}
.pmt-label{font-size:11px;font-weight:700;color:var(--text2);letter-spacing:.3px;line-height:1.2}
.pmt.active .pmt-label{color:var(--cyan)}
/* Card container */
#card-container{background:#fff;border:1.5px solid var(--border);border-radius:var(--rsm);padding:4px;margin-bottom:14px;min-height:90px;overflow:hidden}
/* Manual pay instructions */
.manual-box{background:linear-gradient(135deg,rgba(0,229,255,.04),rgba(0,0,0,.2));border:1px solid rgba(0,229,255,.15);border-radius:var(--rsm);padding:14px;margin-bottom:14px}
.manual-box-title{font-weight:700;font-size:15px;color:var(--cyan);margin-bottom:8px;display:flex;align-items:center;gap:6px}
.manual-step{display:flex;gap:10px;margin-bottom:8px;align-items:flex-start}
.manual-step-num{width:20px;height:20px;border-radius:50%;background:var(--cyan);color:#000;font-size:13px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}
.manual-step-text{font-size:15px;color:var(--text);line-height:1.5}
.manual-step-text strong{color:var(--gold2)}
.manual-step-text .handle{background:rgba(240,192,64,.1);border:1px solid rgba(240,192,64,.3);border-radius:6px;padding:1px 8px;font-family:'Exo 2',sans-serif;font-weight:700;color:var(--gold);font-size:15px;letter-spacing:.5px}
/* Buttons */
.btn{width:100%;padding:15px;border:none;border-radius:var(--rsm);font-family:'Exo 2',sans-serif;font-weight:700;font-size:16px;letter-spacing:1px;cursor:pointer;transition:all .2s;text-transform:uppercase;display:flex;align-items:center;justify-content:center;gap:8px}
.btn:active{transform:scale(.98)}
.btn-gold{background:linear-gradient(135deg,var(--gold),#d4a017);color:#000;box-shadow:0 4px 20px rgba(240,192,64,.4)}
.btn-outline{background:transparent;border:1.5px solid var(--gold);color:var(--gold)}
.btn-cyan{background:linear-gradient(135deg,var(--cyan),#0099cc);color:#000;box-shadow:0 4px 20px rgba(0,229,255,.3)}
.btn-danger{background:linear-gradient(135deg,var(--red),#cc0000);color:#fff}
.btn-green{background:linear-gradient(135deg,var(--green),#00aa55);color:#000}
/* Alerts */
.alert{padding:12px 14px;border-radius:var(--rsm);margin-bottom:14px;font-weight:600;font-size:15px;display:none;line-height:1.4}
.alert.show{display:block}
.alert-error{background:rgba(255,68,68,.1);border:1px solid rgba(255,68,68,.3);color:var(--red)}
.alert-success{background:rgba(0,230,118,.1);border:1px solid rgba(0,230,118,.3);color:var(--green)}
.alert-info{background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.2);color:var(--cyan)}
/* Divider */
.divider{display:flex;align-items:center;gap:10px;margin:18px 0;color:var(--text2);font-size:13px;font-weight:700;letter-spacing:1px}
.divider::before,.divider::after{content:'';flex:1;height:1px;background:var(--border)}
/* Cashout items */
.cashout-item{background:var(--bg3);border:1px solid var(--border);border-radius:var(--rsm);padding:13px 14px;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between;gap:8px}
.badge{display:inline-block;font-size:12px;font-weight:700;padding:3px 9px;border-radius:20px;letter-spacing:.3px}
.badge-pending{background:rgba(255,214,10,.15);color:var(--yellow)}
.badge-approved{background:rgba(0,230,118,.15);color:var(--green)}
.badge-rejected{background:rgba(255,68,68,.15);color:var(--red)}
.badge-completed{background:rgba(0,229,255,.15);color:var(--cyan)}
/* Profile */
.profile-header{text-align:center;padding:20px 0 14px}
.profile-avatar{width:76px;height:76px;border-radius:50%;background:linear-gradient(135deg,var(--purple),var(--cyan));display:flex;align-items:center;justify-content:center;font-family:'Exo 2',sans-serif;font-weight:900;font-size:30px;margin:0 auto 10px;border:3px solid var(--gold);box-shadow:var(--glow-gold)}
/* Modal */
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:200;align-items:flex-end;backdrop-filter:blur(4px)}
.modal-overlay.show{display:flex}
.modal-sheet{background:var(--bg2);border-radius:22px 22px 0 0;border-top:1px solid var(--border);padding:20px;width:100%;max-width:var(--max);margin:0 auto;max-height:90dvh;overflow-y:auto;animation:slideUp .3s ease}
@keyframes slideUp{from{transform:translateY(100%)}to{transform:translateY(0)}}
.modal-handle{width:40px;height:4px;background:var(--border);border-radius:2px;margin:0 auto 18px}
/* Spinner */
.spinner{width:18px;height:18px;border:2px solid rgba(255,255,255,.2);border-top-color:currentColor;border-radius:50%;animation:spin .8s linear infinite;display:inline-block;flex-shrink:0}
@keyframes spin{to{transform:rotate(360deg)}}
/* Order ref badge */
.ref-badge{background:rgba(240,192,64,.1);border:1px solid rgba(240,192,64,.3);border-radius:8px;padding:10px 14px;margin-bottom:14px;font-size:15px;color:var(--text2)}
.ref-badge strong{color:var(--gold2);font-family:'Exo 2',sans-serif}
/* ─── PROFILE TABS ───────────────────────────────────── */
.form-label{font-size:13px;font-weight:700;color:var(--text2);letter-spacing:1px;text-transform:uppercase;margin-bottom:6px;display:block}
.profile-tabs{display:flex;flex-wrap:wrap;gap:3px;background:var(--bg3);border-radius:var(--rsm);padding:4px;margin-bottom:14px}
.profile-tab{flex:1 1 calc(25% - 3px);min-width:72px;padding:10px 4px;border:none;background:none;color:var(--text2);font-family:'Exo 2',sans-serif;font-weight:700;font-size:13px;border-radius:8px;cursor:pointer;transition:all .2s;text-align:center}
.profile-tab.active{background:linear-gradient(135deg,rgba(240,192,64,.15),rgba(0,229,255,.08));color:var(--gold)}
.profile-tab-panel{display:none}
.profile-tab-panel.active{display:block;animation:fadeUp .2s ease}
/* ─── SAVED CARD CHIP ────────────────────────────────── */
.saved-card-chip{background:linear-gradient(135deg,#1c1c3a,#2d1b69);border-radius:16px;padding:20px;margin-bottom:12px;position:relative;overflow:hidden;border:1px solid rgba(255,255,255,.1)}
.saved-card-chip::before{content:'';position:absolute;top:-40px;right:-40px;width:140px;height:140px;border-radius:50%;background:rgba(255,255,255,.04)}
.saved-card-chip-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}
.saved-card-brand{font-family:'Exo 2',sans-serif;font-weight:900;font-size:18px;color:#fff;letter-spacing:2px;text-transform:uppercase}
.saved-card-number{font-family:'Courier New',monospace;font-size:18px;font-weight:700;color:#fff;letter-spacing:4px;margin-bottom:20px}
.saved-card-bottom{display:flex;justify-content:space-between;color:#fff}
/* ─── PAYMENT PROCESSING OVERLAY ─────────────────────── */
@keyframes rotateRing{to{transform:rotate(360deg)}}
@keyframes pulseIcon{0%,100%{transform:scale(1)}50%{transform:scale(1.08)}}
@keyframes stepFadeIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:translateX(0)}}
@keyframes successBurst{0%{transform:scale(.5);opacity:0}60%{transform:scale(1.1)}100%{transform:scale(1);opacity:1}}
.pay-proc-ring{width:110px;height:110px;position:relative;margin-bottom:28px;flex-shrink:0}
.pay-proc-spinner{position:absolute;inset:0;border-radius:50%;border:3px solid rgba(240,192,64,.15);border-top-color:var(--gold);border-right-color:var(--gold);animation:rotateRing 1s linear infinite}
.pay-proc-icon{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:40px;animation:pulseIcon 2s ease infinite}
.pay-proc-title{font-family:'Exo 2',sans-serif;font-weight:900;font-size:24px;color:var(--text);margin-bottom:8px;text-align:center}
.pay-proc-sub{font-size:15px;color:var(--text2);margin-bottom:32px;text-align:center}
.pay-proc-steps{display:flex;flex-direction:column;gap:10px;width:100%;max-width:260px}
.pay-proc-step{font-size:15px;font-weight:600;color:rgba(255,255,255,.2);padding:8px 14px;border-radius:8px;transition:all .4s;display:flex;align-items:center;gap:8px}
.pay-proc-step.active{color:var(--text);background:rgba(240,192,64,.08);border-left:3px solid var(--gold);animation:stepFadeIn .3s ease}
.pay-proc-step.done{color:var(--green);background:rgba(0,230,118,.06)}
/* ─── PAYMENT RESULT OVERLAY ─────────────────────────── */
.pay-result-wrap{text-align:center;width:100%;max-width:380px}
.pay-result-icon{font-size:80px;margin-bottom:20px;display:block;animation:successBurst .5s ease}
.pay-result-title{font-family:'Exo 2',sans-serif;font-weight:900;font-size:28px;margin-bottom:8px}
.pay-result-title.success{background:linear-gradient(135deg,var(--gold),var(--green));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.pay-result-title.error{color:var(--red)}
.pay-result-title.pending{color:var(--gold)}
.pay-balance-bump{font-family:'Exo 2',sans-serif;font-weight:900;font-size:56px;color:var(--gold);line-height:1;margin:12px 0;text-shadow:0 0 30px rgba(240,192,64,.5)}
.pay-result-detail{background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:12px;padding:14px;margin:16px 0;font-size:15px;color:var(--text2);line-height:1.8;text-align:left}
.pay-result-detail strong{color:var(--text)}
.pay-result-btns{display:flex;flex-direction:column;gap:10px;margin-top:20px}
/* ─── GAME ALIAS CARDS ────────────────────────────────── */
.game-alias-card{background:var(--card);border:1px solid var(--border);border-radius:var(--rsm);padding:12px 14px;margin-bottom:10px;display:flex;align-items:center;gap:12px}
.game-alias-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
.game-alias-name{font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--text);margin-bottom:5px}
.game-alias-input{width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:8px 12px;color:var(--text);font-family:'Rajdhani',sans-serif;font-size:15px;outline:none;transition:border-color .15s;box-sizing:border-box}
.game-alias-input:focus{border-color:var(--cyan)}
.game-alias-input::placeholder{color:var(--text2)}
/* ─── ACTIVITY DASHBOARD ──────────────────────────────── */
.activity-ftab{background:var(--bg3);border:1px solid var(--border);color:var(--text2);border-radius:20px;padding:6px 14px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;cursor:pointer;white-space:nowrap;transition:all .15s;flex-shrink:0}
.activity-ftab.active{background:rgba(240,192,64,.12);border-color:rgba(240,192,64,.35);color:var(--gold)}
.activity-card{background:var(--bg3);border:1px solid var(--border);border-radius:var(--rsm);padding:12px 14px;margin-bottom:8px;display:flex;gap:12px;align-items:flex-start;transition:border-color .2s}
.activity-card:hover{border-color:rgba(255,255,255,.12)}
.activity-icon{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:17px;flex-shrink:0}
.activity-body{flex:1;min-width:0}
.activity-title{font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--text);margin-bottom:2px}
.activity-meta{font-size:13px;color:var(--text2)}
.activity-status{display:inline-block;font-size:12px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;border-radius:4px;padding:2px 7px;margin-top:4px}
.ac-pending{background:rgba(240,192,64,.12);color:var(--gold)}
.ac-locked{background:rgba(0,229,255,.1);color:var(--cyan)}
.ac-completed,.ac-sent,.ac-approved{background:rgba(0,230,118,.1);color:var(--green)}
.ac-rejected,.ac-failed,.ac-deleted{background:rgba(255,68,68,.1);color:var(--red)}
.ac-new{background:rgba(240,192,64,.15);color:var(--gold)}
.ac-read{background:rgba(255,255,255,.06);color:var(--text2)}
.activity-amount{font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;text-align:right;flex-shrink:0}
.summary-pill{background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:6px 12px;font-size:14px;font-family:'Exo 2',sans-serif;font-weight:700;white-space:nowrap}
/* ─── CASHOUT REQUEST CARDS ───────────────────────────── */
.co-card{background:var(--card);border:1px solid var(--border);border-radius:var(--rsm);padding:14px;margin-bottom:10px;transition:border-color .2s}
.co-card.pending{border-color:rgba(240,192,64,.25)}
.co-card.locked{border-color:rgba(0,229,255,.25);background:linear-gradient(135deg,rgba(0,229,255,.03),var(--card))}
.co-card.sent,.co-card.approved{border-color:rgba(0,230,118,.25)}
.co-card.rejected{border-color:rgba(255,68,68,.2);opacity:.75}
.co-card.deleted{opacity:.45}
.co-status-pill{display:inline-block;font-size:12px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;border-radius:4px;padding:3px 8px;margin-left:8px}
.co-status-pending{background:rgba(240,192,64,.12);color:var(--gold)}
.co-status-locked{background:rgba(0,229,255,.1);color:var(--cyan)}
.co-status-sent,.co-status-approved{background:rgba(0,230,118,.1);color:var(--green)}
.co-status-rejected{background:rgba(255,68,68,.1);color:var(--red)}
.co-status-deleted{background:rgba(255,255,255,.05);color:var(--text2)}
.co-edit-row{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
/* ─── REFERRAL STYLES ─────────────────────────────────── */
.ref-share-btn{border-radius:8px;padding:8px 14px;font-size:14px;font-weight:700;cursor:pointer;white-space:nowrap;transition:opacity .15s}
.ref-share-btn:hover{opacity:.8}
.ref-status-pending{color:var(--gold)}
.ref-status-verified{color:var(--green)}
.ref-status-denied{color:var(--red)}
/* ─── PLATFORM ACCOUNTS ───────────────────────────────── */
.pa-card{background:var(--bg3);border:1px solid var(--border);border-radius:var(--rsm);padding:14px;margin-bottom:10px}
.pa-card.approved{border-color:rgba(0,230,118,.3);background:rgba(0,230,118,.03)}
.pa-card.pending{border-color:rgba(240,192,64,.2)}
.pa-card.denied{border-color:rgba(255,68,68,.2);opacity:.75}
.pa-cred-box{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:10px 14px;margin-top:10px;font-family:monospace;font-size:15px}
.pa-cred-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}
.pa-cred-label{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--text2)}
.pa-cred-val{font-size:15px;font-weight:700;color:var(--green)}
.pa-cred-pass{filter:blur(4px);transition:filter .2s;cursor:pointer;user-select:all}
.pa-cred-pass:hover,.pa-cred-pass.revealed{filter:none}
/* Onboarding modal */
#onboarding-modal{position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:900;display:flex;align-items:center;justify-content:center;padding:20px}
.onboarding-box{background:var(--bg3);border:1px solid rgba(240,192,64,.3);border-radius:16px;padding:28px 24px;max-width:420px;width:100%;text-align:center}
.onboarding-title{font-family:'Exo 2',sans-serif;font-weight:900;font-size:22px;color:var(--gold);margin-bottom:10px}
.onboarding-sub{font-size:15px;color:var(--text2);margin-bottom:24px;line-height:1.6}
/* ─── BROADCAST STYLES ────────────────────────────────── */
.bc-card{background:var(--card);border:1px solid var(--border);border-radius:var(--rsm);margin-bottom:12px;overflow:hidden;transition:border-color .2s}
.bc-card.unread{border-color:rgba(240,192,64,.3);background:linear-gradient(135deg,rgba(240,192,64,.04),var(--card))}
.bc-header{padding:14px 16px 10px;display:flex;gap:10px;align-items:flex-start}
.bc-avatar{width:38px;height:38px;border-radius:50%;background:linear-gradient(135deg,var(--gold),#d4a017);display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
.bc-subject{font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--text)}
.bc-meta{font-size:13px;color:var(--text2);margin-top:2px}
.bc-body{padding:0 16px 12px;font-size:15px;color:var(--text2);line-height:1.6;white-space:pre-wrap}
.bc-footer{padding:8px 16px;border-top:1px solid var(--border);display:flex;gap:10px;align-items:center}
.bc-reply-wrap{padding:0 16px 14px;display:none}
.bc-reply-wrap.open{display:block}
.bc-replies-list{max-height:220px;overflow-y:auto;margin-bottom:10px}
.bc-reply-row{display:flex;gap:8px;margin-bottom:8px}
.bc-reply-bubble{background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:8px 12px;flex:1}
.bc-reply-who{font-size:13px;font-weight:700;color:var(--cyan);margin-bottom:3px}
.bc-reply-who.admin{color:var(--gold)}
.bc-reply-msg{font-size:15px;color:var(--text)}
/* ─── CHAT STYLES ─────────────────────────────────────── */
@keyframes pulse2{from{opacity:.5}to{opacity:1}}
@keyframes dotBounce{0%,60%,100%{transform:translateY(0)}30%{transform:translateY(-6px)}}
.chat-header-avatar{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,var(--gold),#d4a017);display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:var(--glow-gold)}
.chat-status-dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--green);margin-right:4px;animation:pulse2 2s ease infinite alternate}
/* Quick reply chips */
.chat-quick-replies{display:flex;flex-wrap:wrap;gap:8px;padding:8px 14px 4px;flex-shrink:0}
.chat-chip{background:rgba(240,192,64,.08);border:1px solid rgba(240,192,64,.25);border-radius:20px;padding:7px 14px;color:var(--gold);font-family:'Exo 2',sans-serif;font-weight:600;font-size:14px;cursor:pointer;transition:all .15s;white-space:nowrap}
.chat-chip:active{background:rgba(240,192,64,.18);transform:scale(.97)}
/* Loading dots */
.chat-loading{display:flex;justify-content:center;align-items:center;padding:30px;color:var(--text2);font-size:15px}
.chat-loading-dots{display:flex;gap:5px}
.chat-loading-dots span{width:8px;height:8px;border-radius:50%;background:var(--text2);animation:dotBounce 1.2s ease infinite}
.chat-loading-dots span:nth-child(2){animation-delay:.2s}
.chat-loading-dots span:nth-child(3){animation-delay:.4s}
/* Send button pulse when has text */
.chat-send-btn.has-text{background:linear-gradient(135deg,var(--cyan),#0088cc);box-shadow:0 0 16px rgba(0,229,255,.35)}
#page-chat{display:none;flex-direction:column;padding:0;padding-bottom:0;position:fixed;top:0;left:0;right:0;bottom:var(--nav-h);max-width:var(--max);margin:0 auto;overflow:hidden;z-index:10}
#page-chat.active{display:flex}
.chat-header{display:flex;align-items:center;gap:12px;padding:12px 16px;background:rgba(16,16,30,.98);border-bottom:1px solid var(--border);flex-shrink:0}
.chat-header-avatar{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,var(--gold),#d4a017);display:flex;align-items:center;justify-content:center;font-family:'Exo 2',sans-serif;font-weight:900;font-size:18px;color:#000;flex-shrink:0;box-shadow:var(--glow-gold)}
.chat-header-name{font-family:'Exo 2',sans-serif;font-weight:700;font-size:16px;color:var(--text)}
.chat-header-status{font-size:13px;color:var(--green);font-weight:600;margin-top:2px}
.chat-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:8px;-webkit-overflow-scrolling:touch;padding-bottom:8px}
.chat-loading{color:var(--text2);font-size:15px;text-align:center;padding:20px}
.chat-bubble-wrap{display:flex;flex-direction:column;max-width:78%}
.chat-bubble-wrap.mine{align-self:flex-end;align-items:flex-end}
.chat-bubble-wrap.theirs{align-self:flex-start;align-items:flex-start}
.chat-bubble{padding:10px 14px;border-radius:18px;font-size:15px;line-height:1.45;word-break:break-word;position:relative}
.chat-bubble.mine{background:linear-gradient(135deg,var(--gold),#d4a017);color:#000;border-bottom-right-radius:4px;font-weight:500}
.chat-bubble.theirs{background:var(--card);color:var(--text);border:1px solid var(--border);border-bottom-left-radius:4px}
.chat-time{font-size:12px;color:var(--text2);margin-top:3px;padding:0 4px}
.chat-date-divider{text-align:center;font-size:13px;color:var(--text2);font-weight:700;letter-spacing:.5px;margin:8px 0;display:flex;align-items:center;gap:8px}
.chat-date-divider::before,.chat-date-divider::after{content:'';flex:1;height:1px;background:var(--border)}
.chat-input-bar{display:flex;gap:8px;padding:10px 12px;background:rgba(10,10,18,.98);border-top:1px solid var(--border);flex-shrink:0;padding-bottom:calc(10px + env(safe-area-inset-bottom));padding-bottom:max(10px,calc(10px + env(safe-area-inset-bottom)))}
.chat-input{flex:1;background:var(--bg3);border:1.5px solid var(--border);border-radius:24px;padding:10px 16px;color:var(--text);font-family:'Rajdhani',sans-serif;font-size:16px;line-height:1.6;outline:none;transition:border-color .2s}
.chat-input:focus{border-color:var(--cyan)}
.chat-send-btn{width:42px;height:42px;border-radius:50%;background:linear-gradient(135deg,var(--gold),#d4a017);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .2s;color:#000}
.chat-send-btn:active{transform:scale(.93)}
.chat-send-btn svg{stroke:#000}
.chat-nav-badge{position:absolute;top:4px;right:14px;background:var(--red);color:#fff;font-size:11px;font-weight:700;min-width:16px;height:16px;border-radius:8px;display:flex;align-items:center;justify-content:center;padding:0 4px}
.chat-empty{text-align:center;padding:40px 20px;color:var(--text2);flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center}
.chat-empty-icon{font-size:52px;margin-bottom:14px}
.chat-empty-text{font-size:15px;line-height:1.7}
.chat-sender-label{font-size:12px;font-weight:700;color:var(--text2);letter-spacing:.5px;text-transform:uppercase;margin-bottom:3px;padding:0 4px}
/* ── Responsive Typography ─────────────────────────────── */
/* Tablet (≥768px) */
@media (min-width: 768px) {
body { font-size: 17px; }
.card { font-size: 16px; padding: 22px; }
.page-title { font-size: 26px !important; }
.card-title { font-size: 18px; }
.btn { font-size: 16px; padding: 14px 24px; }
.activity-item { font-size: 15px; }
}
/* Desktop (≥1200px) */
@media (min-width: 1200px) {
body { font-size: 18px; }
.card { font-size: 17px; padding: 24px; }
.page-title { font-size: 28px !important; }
.btn { font-size: 17px; }
}
</style>
</head>
<body>
<!-- AUTH -->
<!-- ONBOARDING MODAL -->
<div id="onboarding-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.88);z-index:200;overflow-y:auto;padding:20px;-webkit-overflow-scrolling:touch">
<div style="max-width:440px;margin:0 auto;background:var(--card);border:1px solid rgba(240,192,64,.25);border-radius:18px;padding:28px 24px;position:relative;top:10%">
<div id="ob-step1">
<div style="text-align:center;margin-bottom:20px">
<div style="font-size:52px;margin-bottom:10px">🎮</div>
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:22px;color:var(--gold);margin-bottom:8px">Welcome to TomTomGames!</div>
<div style="font-size:14px;color:var(--text2);line-height:1.7">Do you already have a username/login<br>on any of our game platforms?</div>
</div>
<div style="display:flex;flex-direction:column;gap:10px">
<button class="btn btn-gold" onclick="obShowStep2()" style="font-size:15px;padding:14px">✅ Yes — I have existing logins</button>
<button class="btn" onclick="obShowStep2()" style="background:rgba(0,229,255,.07);border:1px solid rgba(0,229,255,.2);color:var(--cyan);padding:14px;font-size:15px">🆕 No — I want to request new logins</button>
<button onclick="obDismiss()" style="background:none;border:none;color:var(--text2);font-size:14px;cursor:pointer;margin-top:2px;padding:8px">Skip for now →</button>
</div>
</div>
<div id="ob-step2" style="display:none">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:18px">
<button onclick="document.getElementById('ob-step2').style.display='none';document.getElementById('ob-step1').style.display='block';" style="background:none;border:none;color:var(--cyan);cursor:pointer;font-size:22px;padding:0;line-height:1">←</button>
<div>
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:17px">Request Game Accounts</div>
<div style="font-size:14px;color:var(--text2);margin-top:2px">Select platforms — our team will create your logins</div>
</div>
</div>
<div id="ob-platforms-list" style="margin-bottom:14px;max-height:300px;overflow-y:auto"></div>
<div id="ob-alert" class="alert" style="margin-bottom:10px"></div>
<div style="display:flex;gap:8px">
<button class="btn btn-gold" onclick="obSubmitRequests()" style="flex:1">📨 Submit Requests</button>
<button class="btn btn-outline" onclick="obDismiss()" style="width:80px;font-size:15px">Done</button>
</div>
</div>
</div>
</div>
<div id="auth-screen" class="auth-screen" style="display:flex">
<div class="auth-logo">
<div class="auth-logo-text" style="display:flex;align-items:center;justify-content:center;gap:12px"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48" style="display:inline-block;vertical-align:middle;flex-shrink:0"><defs><linearGradient id="al1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f0c040"/><stop offset="100%" stop-color="#ff6b35"/></linearGradient><linearGradient id="al2" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00e5ff"/><stop offset="100%" stop-color="#7b2fbe"/></linearGradient><filter id="agl"><feGaussianBlur stdDeviation="1.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><rect x="6" y="16" width="36" height="22" rx="11" fill="url(#al1)" filter="url(#agl)"/><rect x="12" y="23" width="8" height="3" rx="1.5" fill="rgba(0,0,0,0.45)"/><rect x="15" y="20" width="3" height="8" rx="1.5" fill="rgba(0,0,0,0.45)"/><circle cx="32" cy="22" r="2.2" fill="#e63946" opacity=".9"/><circle cx="36" cy="25" r="2.2" fill="#2ec4b6" opacity=".9"/><circle cx="32" cy="28" r="2.2" fill="#7b2fbe" opacity=".9"/><circle cx="28" cy="25" r="2.2" fill="#f4a261" opacity=".9"/><rect x="21" y="24" width="6" height="3" rx="1.5" fill="rgba(0,0,0,0.3)"/><rect x="8" y="30" width="8" height="7" rx="4" fill="url(#al2)" opacity=".7"/><rect x="32" y="30" width="8" height="7" rx="4" fill="url(#al2)" opacity=".7"/><rect x="14" y="13" width="8" height="5" rx="2.5" fill="url(#al1)" opacity=".8"/><rect x="26" y="13" width="8" height="5" rx="2.5" fill="url(#al1)" opacity=".8"/><circle cx="24" cy="7" r="2" fill="#f0c040" opacity=".9"/><circle cx="39" cy="10" r="1.2" fill="#00e5ff" opacity=".8"/><circle cx="9" cy="10" r="1.2" fill="#f0c040" opacity=".7"/></svg><span>TomTomGames</span></div>
<div class="auth-logo-sub">YOUR ULTIMATE GAMING PORTAL</div>
</div>
<div class="auth-tabs">
<button class="auth-tab active" onclick="switchTab('login')">LOGIN</button>
<button class="auth-tab" onclick="switchTab('register')">CREATE ACCOUNT</button>
</div>
<div id="tab-login">
<div id="login-alert" class="alert"></div>
<!-- Unverified notice (hidden by default) -->
<div id="login-unverified-box" style="display:none;background:rgba(255,214,10,.08);border:1px solid rgba(255,214,10,.25);border-radius:10px;padding:14px;margin-bottom:14px">
<div style="font-size:15px;color:#ffd60a;font-weight:700;margin-bottom:6px">📧 Email Not Verified</div>
<div style="font-size:14px;color:#cccc88;line-height:1.6;margin-bottom:10px">Check your inbox for the verification link, or resend it below.</div>
<div id="login-resend-alert" class="alert" style="margin-bottom:8px"></div>
<button class="btn btn-outline" onclick="resendFromLogin()" id="login-resend-btn" style="padding:10px;font-size:14px">Resend Verification Email</button>
</div>
<div class="fg"><label>Username</label><input class="fi" id="login-user" type="text" placeholder="your_username" autocomplete="username" autocapitalize="none"></div>
<div class="fg"><label>Password</label><input class="fi" id="login-pass" type="password" placeholder="••••••••" autocomplete="current-password"></div>
<button class="btn btn-gold" onclick="doLogin()" style="margin-top:6px" id="login-btn">LOGIN</button>
</div>
<div id="tab-register" style="display:none">
<!-- FORM STATE -->
<div id="reg-form-state">
<div id="reg-alert" class="alert"></div>
<div class="fg"><label>Username</label><input class="fi" id="reg-user" type="text" placeholder="choose_a_username" autocapitalize="none" autocomplete="username"></div>
<div class="fg"><label>Your Display Name <span style="font-size:15px;color:var(--text2);font-weight:400">— what you want to be known as</span></label><input class="fi" id="reg-alias" type="text" placeholder="e.g. LuckyAce, GameKing22..." autocomplete="nickname"></div>
<div class="fg">
<label>Email Address <span style="color:var(--red)">*</span></label>
<input class="fi" id="reg-email" type="email" placeholder="your@email.com" autocomplete="email">
<div style="font-size:15px;color:var(--text2);margin-top:5px">📧 Required — we'll send a verification link</div>
</div>
<div class="fg"><label>Password</label><input class="fi" id="reg-pass" type="password" placeholder="Min 6 characters" autocomplete="new-password"></div>
<button class="btn btn-gold" onclick="doRegister()" style="margin-top:6px" id="reg-btn">CREATE ACCOUNT</button>
</div>
<!-- VERIFY PENDING STATE (shown after registration) -->
<div id="reg-pending-state" style="display:none">
<div style="text-align:center;padding:12px 0 20px">
<div style="font-size:52px;margin-bottom:16px">📧</div>
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:20px;color:var(--gold);margin-bottom:10px">Check Your Email!</div>
<p style="font-size:14px;color:var(--text2);line-height:1.7;margin-bottom:6px">
We sent a verification link to:<br>
<strong style="color:var(--cyan)" id="reg-pending-email">—</strong>
</p>
<p style="font-size:15px;color:var(--text2);line-height:1.6;margin-bottom:24px">
Open the email and click the link to activate your account. Check your spam folder if you don't see it.
</p>
<div style="background:rgba(0,229,255,.06);border:1px solid rgba(0,229,255,.15);border-radius:10px;padding:14px;margin-bottom:20px;font-size:15px;color:var(--text2);text-align:left;line-height:1.7">
<strong style="color:var(--cyan)">Steps:</strong><br>
1. Open your email inbox<br>
2. Find email from <strong style="color:var(--text)">noreply@tomtomgames.com</strong><br>
3. Click <strong style="color:var(--gold)">VERIFY MY ACCOUNT</strong><br>
4. You'll be automatically logged in ✅
</div>
<div id="resend-alert" class="alert" style="margin-bottom:12px"></div>
<button class="btn btn-outline" onclick="resendVerify()" id="resend-btn" style="margin-bottom:10px">Resend Verification Email</button>
<button class="btn" onclick="showRegForm()" style="background:none;color:var(--text2);font-size:15px;padding:8px">← Use different email</button>
</div>
</div>
</div>
</div>
<!-- MAIN APP -->
<div id="main-app" style="display:none">
<div class="topbar">
<div class="topbar-logo" style="display:flex;align-items:center;gap:8px"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" style="display:inline-block;vertical-align:middle;flex-shrink:0"><defs><linearGradient id="li1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f0c040"/><stop offset="100%" stop-color="#ff6b35"/></linearGradient><linearGradient id="li2" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00e5ff"/><stop offset="100%" stop-color="#7b2fbe"/></linearGradient></defs><rect x="6" y="16" width="36" height="22" rx="11" fill="url(#li1)"/><rect x="12" y="23" width="8" height="3" rx="1.5" fill="rgba(0,0,0,0.45)"/><rect x="15" y="20" width="3" height="8" rx="1.5" fill="rgba(0,0,0,0.45)"/><circle cx="32" cy="22" r="2.2" fill="#e63946" opacity=".9"/><circle cx="36" cy="25" r="2.2" fill="#2ec4b6" opacity=".9"/><circle cx="32" cy="28" r="2.2" fill="#7b2fbe" opacity=".9"/><circle cx="28" cy="25" r="2.2" fill="#f4a261" opacity=".9"/><rect x="21" y="24" width="6" height="3" rx="1.5" fill="rgba(0,0,0,0.3)"/><rect x="8" y="30" width="8" height="7" rx="4" fill="url(#li2)" opacity=".7"/><rect x="32" y="30" width="8" height="7" rx="4" fill="url(#li2)" opacity=".7"/><rect x="14" y="13" width="8" height="5" rx="2.5" fill="url(#li1)" opacity=".8"/><rect x="26" y="13" width="8" height="5" rx="2.5" fill="url(#li1)" opacity=".8"/></svg><span>TomTomGames</span></div>
<div class="topbar-right">
<div class="token-badge" id="header-tokens-wrap"><span id="header-tokens">0</span></div>
<button class="avatar-btn" id="avatar-btn" onclick="showPage('profile')">?</button>
</div>
</div>
<!-- HOME -->
<div class="page active" id="page-home">
<div class="hero-balance">
<div class="balance-label">YOUR TOKENS</div>
<div class="balance-amount" id="home-balance">0</div>
<div class="balance-unit">TOKENS</div>
<div class="balance-alias">Playing as <strong id="home-alias">—</strong></div>
<div style="display:flex;gap:10px;margin-top:16px">
<button class="btn btn-gold" style="flex:1;padding:12px" onclick="showPage('buy')">BUY</button>
<button class="btn btn-outline" style="flex:1;padding:12px" onclick="showPage('cashout')">💸 CASH OUT</button>
</div>
</div>
<div class="section-title"><span>PLAY NOW</span></div>
<div class="platform-grid" id="platform-grid"></div>
<div id="platform-alias-toast" style="position:fixed;bottom:80px;left:50%;transform:translateX(-50%) translateY(10px);background:rgba(0,0,0,.88);color:#fff;font-size:14px;font-weight:700;padding:10px 18px;border-radius:24px;white-space:nowrap;opacity:0;transition:opacity .3s,transform .3s;pointer-events:none;z-index:999;border:1px solid rgba(0,229,255,.25)"></div>
</div>
<!-- ═══ BUY TOKENS (milkyswipe style) ═══ -->
<div class="page" id="page-buy">
<div class="section-title"><span>BUY TOKENS</span></div>
<div id="buy-alert" class="alert"></div>
<!-- Payment Form Card -->
<div class="pay-form">
<div class="pay-form-header">
<div class="pay-form-icon" id="buy-form-icon" style="font-size:0"><!-- coin --></div>
<div>
<div class="pay-form-title">Token Purchase</div>
<div class="pay-form-sub">$1 = 1 Token · All payments secure</div>
</div>
</div>
<div class="pay-form-body">
<!-- 1. Select Platform -->
<div class="fg">
<label>1. Select Your Gaming Platform</label>
<select class="fi fi-select" id="buy-platform" onchange="updatePayForm();prefillBuyAlias()">
<option value="">— Choose Platform —</option>
</select>
</div>
<!-- 2. Game Alias -->
<div class="fg">
<label>2. Your In-Game Username / Alias</label>
<input class="fi" id="buy-alias" type="text" placeholder="e.g. LuckyAce777" oninput="updatePayForm()" onblur="saveAliasFromBuyPage()">
</div>
<!-- 3. Token Package -->
<div class="fg">
<label>3. Select Token Package <span style="color:var(--text2);font-weight:400;font-size:15px">($1 = 1 Token)</span></label>
<div style="position:relative">
<button id="pkg-prev" onclick="pkgScroll(-1)" style="display:none;position:absolute;left:-10px;top:50%;transform:translateY(-50%);z-index:2;background:var(--bg3);border:1px solid var(--border);color:var(--text);border-radius:50%;width:28px;height:28px;font-size:14px;cursor:pointer;line-height:1;padding:0"></button>
<div class="pkg-scroll" id="pkg-scroll"></div>
<button id="pkg-next" onclick="pkgScroll(1)" style="display:none;position:absolute;right:-10px;top:50%;transform:translateY(-50%);z-index:2;background:var(--bg3);border:1px solid var(--border);color:var(--text);border-radius:50%;width:28px;height:28px;font-size:14px;cursor:pointer;line-height:1;padding:0"></button>
</div>
<!-- Custom amount row -->
<div class="custom-amt-row" id="custom-pkg-row">
<div class="custom-amt-wrap">
<div class="custom-amt-label">✏️ Custom Amount</div>
<div class="custom-amt-input-wrap">
<span class="custom-amt-dollar">$</span>
<input class="custom-amt-input" id="custom-amt" type="number" min="1" max="500" step="1"
placeholder="Enter amount" oninput="selectCustomPkg(this.value)">
</div>
</div>
<div class="custom-amt-equiv" id="custom-amt-equiv" style="display:none">
= <strong id="custom-token-count">0</strong> Tokens
</div>
</div>
</div>
<!-- Amount / Order Summary display -->
<div class="order-summary" id="order-summary" style="display:none">
<div class="order-summary-row">
<span class="order-summary-label">Package</span>
<span class="order-summary-val" id="summary-pkg">—</span>
</div>
<div class="order-summary-row">
<span class="order-summary-label">Tokens</span>
<span class="order-summary-val" style="color:var(--gold2)" id="summary-tokens">0</span>
</div>
<div class="order-summary-divider"></div>
<div class="order-summary-row">
<span class="order-summary-label" style="font-weight:700;color:var(--text)">Total Due</span>
<span class="order-summary-total" id="summary-total">$0.00</span>
</div>
</div>
<!-- 4. Payment Method -->
<div class="fg">
<label>4. Payment Method</label>
<div class="pay-method-tabs" id="manual-payment-btns">
<!-- Populated dynamically by buildPaymentMethods() -->
<div class="pmt active" onclick="selectPMT(this,'card')" title="Credit/Debit Card">
<span class="pmt-icon">💳</span><span class="pmt-label">Card</span>
</div>
</div>
</div>
<!-- CARD fields (Square Web Payments) -->
<div id="section-card">
<div class="fg">
<label>Card Details <span style="font-size:14px;color:var(--green);font-weight:400" id="card-ready-label"></span></label>
<div id="card-container"></div>
</div>
<div class="fi-row">
<div class="fg">
<label>First Name</label>
<input class="fi" id="card-first-name" type="text" placeholder="First" autocomplete="given-name">
</div>
<div class="fg">
<label>Last Name</label>
<input class="fi" id="card-last-name" type="text" placeholder="Last" autocomplete="family-name">
</div>
</div>
<div class="fg">
<label>Billing Address</label>
<input class="fi" id="card-address" type="text" placeholder="Street address" autocomplete="street-address" style="margin-bottom:8px">
<div style="display:grid;grid-template-columns:1fr 60px 90px;gap:8px">
<input class="fi" id="card-city" type="text" placeholder="City" autocomplete="address-level2">
<input class="fi" id="card-state" type="text" placeholder="ST" maxlength="2" autocomplete="address-level1" style="text-transform:uppercase">
<input class="fi" id="card-zip" type="text" placeholder="ZIP" maxlength="10" autocomplete="postal-code">
</div>
</div>
<div class="fg">
<label>Email for Receipt</label>
<input class="fi" id="card-email" type="email" placeholder="your@email.com" autocomplete="email">
</div>
</div>
<!-- MANUAL payment instructions -->
<div id="section-manual" style="display:none">
<div class="manual-box" id="manual-box"></div>
<div class="ref-badge" id="ref-badge" style="display:none">
📋 Reference: <strong id="ref-platform">—</strong> | Player: <strong id="ref-alias">—</strong> | Include this in your payment note!
</div>
</div>
<!-- Save billing checkbox -->
<div id="save-billing-wrap" style="display:flex;align-items:center;gap:10px;margin-bottom:14px;padding:10px 14px;background:rgba(0,229,255,.04);border:1px solid rgba(0,229,255,.12);border-radius:var(--rsm)">
<input type="checkbox" id="save-billing-cb" style="width:16px;height:16px;accent-color:var(--cyan);cursor:pointer" checked>
<label for="save-billing-cb" style="font-size:15px;color:var(--text2);cursor:pointer;line-height:1.4">
Save billing info for faster checkout next time
</label>
</div>
<!-- Submit -->
<button class="btn btn-gold" id="pay-submit-btn" onclick="submitPayment()">
COMPLETE PURCHASE
</button>
<div style="display:flex;align-items:center;justify-content:center;gap:8px;margin-top:10px">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#8888aa" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
<p style="font-size:15px;color:var(--text2)">256-bit SSL · Secured by Square</p>
</div>
</div>
</div>
</div>
<!-- CASHOUT -->
<div class="page" id="page-cashout">
<div class="section-title"><span>CASH OUT</span></div>
<div class="pay-form">
<div class="pay-form-header">
<div class="pay-form-icon">💸</div>
<div>
<div class="pay-form-title">Cashout Request</div>
<div class="pay-form-sub">Request withdrawal of your token balance</div>
</div>
</div>
<div class="pay-form-body">
<div style="display:flex;align-items:center;justify-content:space-between;background:var(--bg3);border:1px solid rgba(240,192,64,.2);border-radius:var(--rsm);padding:14px;margin-bottom:16px">
<div>
<div style="font-size:15px;color:var(--text2);font-weight:700;letter-spacing:1px;text-transform:uppercase">Available Balance</div>
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:28px;color:var(--gold)"><span id="cashout-balance">0</span> <span style="font-size:14px;color:var(--text2)">TOKENS</span></div>
</div>
<span style="font-size:36px">💰</span>
</div>
<div id="cashout-alert" class="alert"></div>
<!-- Payout Method Selector -->
<div class="fg">
<label>Send Payment To
<a onclick="showPage('profile');setTimeout(()=>{switchProfileTab('payout',document.querySelector('.profile-tab[onclick*=payout]'))},100)"
style="font-size:15px;color:var(--cyan);cursor:pointer;margin-left:8px;font-weight:400">+ Manage Methods</a>
</label>
<div id="cashout-payout-list" style="margin-bottom:8px">
<div style="color:var(--text2);font-size:15px;padding:10px;text-align:center">Loading payout methods...</div>
</div>
<div id="cashout-no-payout" style="display:none;background:rgba(240,192,64,.07);border:1px solid rgba(240,192,64,.2);border-radius:8px;padding:12px;font-size:15px;color:var(--gold);text-align:center">
No payout method saved. <a onclick="showPage('profile');setTimeout(()=>{switchProfileTab('payout',document.querySelector('[onclick*=payout]'))},100)" style="cursor:pointer;text-decoration:underline">Add one in your profile →</a>
</div>
</div>
<div class="fg"><label>Select Platform</label><select class="fi fi-select" id="cashout-platform" onchange="prefillCashoutAlias(this.value)"><option value="">— Choose Platform —</option></select></div>
<div class="fg"><label>Your In-Game Alias / Username</label><input class="fi" id="cashout-alias" type="text" placeholder="Your alias on that platform" onblur="saveCashoutAlias()"></div>
<div class="fg"><label>Tokens to Cash Out</label><input class="fi" id="cashout-tokens" type="number" min="1" placeholder="Enter amount (min: 1)"></div>
<button class="btn btn-cyan" onclick="submitCashout()">💸 SUBMIT CASHOUT REQUEST</button>
<p style="text-align:center;font-size:15px;color:var(--text2);margin-top:10px">Cashouts are reviewed and paid out within a few minutes</p>
</div>
</div>
<div class="divider">MY CASHOUT REQUESTS</div>
<div id="cashout-history">
<div style="color:var(--text2);font-size:15px;text-align:center;padding:20px">Loading your requests...</div>
</div>
</div>
<!-- PROFILE -->
<div class="page" id="page-profile">
<!-- Profile header -->
<div class="profile-header">
<div class="profile-avatar" id="profile-avatar">?</div>
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:22px;color:var(--text)" id="profile-username">—</div>
<div style="color:var(--text2);font-size:14px;margin-top:2px" id="profile-alias">—</div>
</div>
<!-- Balance card -->
<div class="card" style="margin-bottom:10px;background:linear-gradient(135deg,#1a1228,#121a28);border-color:rgba(240,192,64,.2)">
<div style="display:flex;justify-content:space-between;align-items:center">
<div>
<div style="font-size:15px;color:var(--text2);font-weight:700;letter-spacing:1px;text-transform:uppercase;margin-bottom:4px">Token Balance</div>
<div style="display:flex;align-items:center;gap:6px" id="profile-tokens"><span style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:28px;color:var(--gold)">0</span></div>
</div>
<button class="btn btn-gold" style="width:auto;padding:10px 18px;font-size:15px" onclick="showPage('buy')">BUY</button>
</div>
</div>
<!-- Profile tabs -->
<div class="profile-tabs" id="profile-tabs">
<button class="profile-tab active" onclick="switchProfileTab('activity',this)">Activity</button>
<button class="profile-tab" onclick="switchProfileTab('games',this)">🎮 Games</button>
<button class="profile-tab" onclick="switchProfileTab('logins',this)">🔑 Logins</button>
<button class="profile-tab" onclick="switchProfileTab('payout',this)">💸 Payout</button>
<button class="profile-tab" onclick="switchProfileTab('referrals',this)">🎁 Refer</button>
<button class="profile-tab" onclick="switchProfileTab('billing',this)">Billing</button>
<button class="profile-tab" onclick="switchProfileTab('account',this)">Account</button>
</div>
<!-- TAB: ACTIVITY -->
<div class="profile-tab-panel active" id="ptab-activity">
<!-- Filter tabs -->
<div style="display:flex;gap:6px;margin-bottom:14px;overflow-x:auto;padding-bottom:2px">
<button class="activity-ftab active" onclick="filterActivity('all',this)">All</button>
<button class="activity-ftab" onclick="filterActivity('purchases',this)">💳 Purchases</button>
<button class="activity-ftab" onclick="filterActivity('cashouts',this)">💸 Cashouts</button>
<button class="activity-ftab" onclick="filterActivity('broadcasts',this)">📢 News</button>
</div>
<!-- Status summary bar -->
<div id="activity-summary" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px"></div>
<!-- Live feed -->
<div id="activity-feed">
<div style="text-align:center;padding:24px;color:var(--text2);font-size:15px">
<span class="spinner" style="border-top-color:var(--gold)"></span>
</div>
</div>
<button class="btn btn-outline" onclick="showPage('cashout')" style="margin-top:12px;width:100%">💸 Request Cash Out</button>
</div>
<!-- TAB: PLATFORM LOGINS -->
<!-- TAB: LOGINS / PLATFORM ACCOUNTS -->
<div class="profile-tab-panel" id="ptab-logins">
<div style="font-size:14px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;margin-bottom:4px">Platform Logins</div>
<div style="font-size:14px;color:var(--text2);margin-bottom:14px;line-height:1.5">Your game platform accounts. Approved logins show your credentials below. Tap password to reveal.</div>
<!-- Accounts list - shows all requests + approved credentials -->
<div id="platform-logins-list">
<div style="text-align:center;padding:20px;color:var(--text2);font-size:15px">Loading...</div>
</div>
<!-- Request new account -->
<div class="card" style="margin-top:14px">
<div class="card-title" style="font-size:15px"> Request Platform Account</div>
<div style="font-size:14px;color:var(--text2);margin-bottom:10px">Don't have a login for a platform? Request one and our team will set it up for you.</div>
<div id="req-platform-alert" class="alert" style="margin-bottom:8px"></div>
<select class="fi fi-select" id="req-platform-slug" style="width:100%;margin-bottom:10px">
<option value="">— Select Platform —</option>
</select>
<button class="btn btn-gold" onclick="requestPlatformLogin()" style="width:100%">📨 REQUEST ACCOUNT</button>
</div>
</div>
<!-- TAB: REFERRALS -->
<div class="profile-tab-panel" id="ptab-referrals">
<div style="text-align:center;padding:16px 0 20px">
<div style="font-size:36px;margin-bottom:8px">🎁</div>
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:18px;color:var(--gold)">Refer &amp; Earn Tokens!</div>
<div style="font-size:15px;color:var(--text2);margin-top:6px;line-height:1.6">Share your referral link. Earn tokens for every friend who joins and gets verified.</div>
</div>
<!-- Referral link -->
<div class="card" style="margin-bottom:12px;border-color:rgba(240,192,64,.2)">
<div style="font-size:15px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Your Referral Link</div>
<div style="display:flex;gap:8px;align-items:center">
<input class="fi" id="referral-link-input" type="text" readonly style="flex:1;font-size:14px;cursor:pointer" onclick="this.select()">
<button onclick="copyReferralLink()" style="background:rgba(240,192,64,.1);border:1px solid rgba(240,192,64,.25);color:var(--gold);border-radius:8px;padding:10px 14px;font-size:14px;font-weight:700;cursor:pointer;white-space:nowrap">📋 Copy</button>
</div>
<div id="ref-copy-msg" style="font-size:15px;color:var(--green);margin-top:6px;display:none">✓ Copied to clipboard!</div>
</div>
<!-- Stats -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:14px">
<div class="card" style="text-align:center;padding:12px 8px">
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:22px;color:var(--gold)" id="ref-count">0</div>
<div style="font-size:14px;color:var(--text2);text-transform:uppercase;letter-spacing:.5px">Referrals</div>
</div>
<div class="card" style="text-align:center;padding:12px 8px">
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:22px;color:var(--green)" id="ref-earned">0</div>
<div style="font-size:14px;color:var(--text2);text-transform:uppercase;letter-spacing:.5px">Tokens Earned</div>
</div>
<div class="card" style="text-align:center;padding:12px 8px">
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:16px;color:var(--cyan)" id="ref-tier">—</div>
<div style="font-size:14px;color:var(--text2);text-transform:uppercase;letter-spacing:.5px">Current Tier</div>
</div>
</div>
<!-- Next tier progress -->
<div id="ref-next-tier" style="margin-bottom:14px;display:none">
<div class="card" style="padding:12px 14px">
<div style="font-size:14px;color:var(--text2);margin-bottom:6px" id="ref-next-label">Next tier progress</div>
<div style="background:rgba(255,255,255,.08);border-radius:4px;height:6px;overflow:hidden">
<div id="ref-progress-bar" style="height:6px;border-radius:4px;background:linear-gradient(90deg,var(--gold),var(--cyan));transition:width .5s"></div>
</div>
</div>
</div>
<!-- Social share -->
<div class="card" style="margin-bottom:12px">
<div style="font-size:14px;font-weight:700;color:var(--text2);margin-bottom:6px;text-transform:uppercase;letter-spacing:1px">Share &amp; Earn Bonus Tokens</div>
<div style="font-size:14px;color:var(--text2);margin-bottom:12px;line-height:1.5">Share your referral link anywhere — Reddit, Twitter/X, a forum, your blog. Paste the link to your post below and we'll try to verify it automatically.</div>
<div style="display:flex;gap:8px;align-items:center;margin-bottom:6px">
<input class="fi" id="share-url-input" type="url" placeholder="https://reddit.com/r/gaming/comments/…" style="flex:1;font-size:14px">
<button onclick="submitShareUrl()" style="background:var(--gold);border:none;color:#000;border-radius:8px;padding:10px 16px;font-size:14px;font-weight:700;cursor:pointer;white-space:nowrap">🔍 Submit</button>
</div>
<div style="font-size:13px;color:var(--text2)">Public posts (Reddit, forums, blogs) verify instantly. Facebook/Instagram/TikTok need manual review.</div>
<div id="ref-share-alert" class="alert" style="margin-top:8px"></div>
<div id="ref-shares-list" style="margin-top:10px"></div>
</div>
<!-- Tiers list -->
<div class="card">
<div style="font-size:14px;font-weight:700;color:var(--text2);margin-bottom:10px;text-transform:uppercase;letter-spacing:1px">Referral Tiers</div>
<div id="ref-tiers-list"></div>
</div>
<!-- My referrals -->
<div class="card" style="margin-top:12px">
<div style="font-size:14px;font-weight:700;color:var(--text2);margin-bottom:10px;text-transform:uppercase;letter-spacing:1px">My Referrals</div>
<div id="ref-list"></div>
</div>
</div>
<!-- TAB: PAYOUT METHODS -->
<div class="profile-tab-panel" id="ptab-payout">
<div style="font-size:14px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;margin-bottom:4px">Payout Methods</div>
<div style="font-size:14px;color:var(--text2);margin-bottom:14px;line-height:1.5">Where to send your cashout payments. The default method is used automatically.</div>
<div id="payout-methods-list"></div>
<!-- Add Method Form -->
<div class="card" style="margin-top:12px">
<div class="card-title" style="font-size:15px"> Add Payout Method</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
<div>
<label style="font-size:15px;color:var(--text2);font-weight:700;display:block;margin-bottom:4px">Type</label>
<select class="fi" id="payout-type-new" style="font-size:15px">
<option value="">Loading...</option>
</select>
</div>
<div>
<label style="font-size:15px;color:var(--text2);font-weight:700;display:block;margin-bottom:4px">Label</label>
<input class="fi" id="payout-label-new" type="text" placeholder="e.g. My Venmo" style="font-size:15px">
</div>
</div>
<div style="margin-bottom:10px">
<label style="font-size:15px;color:var(--text2);font-weight:700;display:block;margin-bottom:4px">Handle / Phone / Email</label>
<input class="fi" id="payout-handle-new" type="text" placeholder="e.g. @username, phone, or email">
</div>
<div id="payout-add-alert" class="alert" style="margin-bottom:8px"></div>
<button class="btn btn-gold" onclick="addPayoutMethod()" style="width:100%">💾 SAVE METHOD</button>
</div>
</div>
<!-- TAB: PLATFORM ACCOUNTS -->
<!-- TAB: GAMES -->
<div class="profile-tab-panel" id="ptab-games">
<div style="font-size:14px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;margin-bottom:4px">Your Game Aliases</div>
<div style="font-size:14px;color:var(--text2);margin-bottom:14px;line-height:1.5">Save your in-game username for each platform. Your alias auto-fills when you buy tokens.</div>
<div id="game-aliases-list"><div style="color:var(--text2);text-align:center;padding:20px;font-size:15px">Loading games...</div></div>
<div id="game-aliases-alert" class="alert" style="margin-top:10px"></div>
<button class="btn btn-gold" onclick="saveAllAliases()" style="margin-top:12px">💾 SAVE ALL ALIASES</button>
</div>
<!-- TAB: BILLING -->
<div class="profile-tab-panel" id="ptab-billing">
<!-- Saved card display -->
<div id="saved-card-display" style="display:none">
<div class="saved-card-chip">
<div class="saved-card-chip-top">
<span class="saved-card-brand" id="saved-card-brand">VISA</span>
<span id="saved-card-dots">💳</span>
</div>
<div class="saved-card-number" id="saved-card-number">···· ···· ···· ····</div>
<div class="saved-card-bottom">
<div><div style="font-size:15px;color:rgba(255,255,255,.5);margin-bottom:2px">CARD HOLDER</div><div id="saved-card-name" style="font-size:15px;font-weight:700">—</div></div>
<div style="text-align:right"><div style="font-size:15px;color:rgba(255,255,255,.5);margin-bottom:2px">EXPIRES</div><div id="saved-card-exp" style="font-size:15px;font-weight:700">—</div></div>
</div>
</div>
<button class="btn btn-danger" onclick="clearSavedCard()" style="margin-top:10px;padding:10px;font-size:14px">🗑 Remove Saved Card</button>
</div>
<!-- Billing form -->
<div id="billing-form-section">
<div style="font-size:14px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;margin-bottom:12px">Billing Information</div>
<div id="billing-alert" class="alert" style="margin-bottom:12px"></div>
<div class="fi-row" style="margin-bottom:10px">
<div><label class="form-label">First Name</label><input class="fi" id="p-first" type="text" placeholder="First" autocomplete="given-name"></div>
<div><label class="form-label">Last Name</label><input class="fi" id="p-last" type="text" placeholder="Last" autocomplete="family-name"></div>
</div>
<div class="fg"><label class="form-label">Email</label><input class="fi" id="p-email" type="email" placeholder="your@email.com" autocomplete="email"></div>
<div class="fg"><label class="form-label">Street Address</label><input class="fi" id="p-address" type="text" placeholder="123 Main St" autocomplete="street-address"></div>
<div style="display:grid;grid-template-columns:1fr 60px 90px;gap:8px;margin-bottom:10px">
<div><label class="form-label">City</label><input class="fi" id="p-city" type="text" placeholder="City" autocomplete="address-level2"></div>
<div><label class="form-label">ST</label><input class="fi" id="p-state" type="text" placeholder="TX" maxlength="2" autocomplete="address-level1" style="text-transform:uppercase"></div>
<div><label class="form-label">ZIP</label><input class="fi" id="p-zip" type="text" placeholder="75001" maxlength="10" autocomplete="postal-code"></div>
</div>
<button class="btn btn-cyan" onclick="saveBillingProfile()" style="margin-bottom:8px">💾 SAVE BILLING INFO</button>
<button class="btn" onclick="clearAllBilling()" style="background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);color:var(--red);font-size:14px;padding:10px">🗑 Clear All Saved Info</button>
</div>
</div>
<!-- TAB: ACCOUNT -->
<div class="profile-tab-panel" id="ptab-account">
<div style="font-size:14px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;margin-bottom:12px">Account Info</div>
<div class="card" style="margin-bottom:10px">
<div style="display:flex;flex-direction:column;gap:10px">
<div style="display:flex;justify-content:space-between"><span style="color:var(--text2);font-size:15px">Username</span><span id="acct-username" style="font-weight:700;color:var(--text)">—</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:var(--text2);font-size:15px">Alias</span><span id="acct-alias" style="font-weight:700;color:var(--cyan)">—</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:var(--text2);font-size:15px">Email</span><span id="acct-email" style="font-weight:700;font-size:15px">—</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:var(--text2);font-size:15px">Member Since</span><span id="acct-joined" style="font-weight:700;color:var(--text2);font-size:14px">—</span></div>
</div>
</div>
<button class="btn btn-outline" onclick="showPage('buy')" style="margin-bottom:8px">Buy Tokens</button>
<button class="btn btn-outline" onclick="showPage('cashout')" style="margin-bottom:8px">💸 Cash Out</button>
<button class="btn btn-outline" onclick="showPage('chat')" style="margin-bottom:8px">💬 Contact Support</button>
<button class="btn btn-danger" onclick="doLogout()" style="margin-top:4px">LOG OUT</button>
</div>
</div>
<!-- ONBOARDING MODAL -->
<div id="onboarding-modal" style="display:none">
<div class="onboarding-box" id="onboarding-step-1">
<div style="font-size:48px;margin-bottom:12px">🎮</div>
<div class="onboarding-title">Welcome to TomTomGames!</div>
<div class="onboarding-sub">Do you already have a login account on any of our game platforms?</div>
<div style="display:flex;gap:10px;justify-content:center">
<button class="btn btn-gold" onclick="onboardingHasAccount()" style="flex:1;max-width:160px">Yes, I do</button>
<button class="btn btn-outline" onclick="onboardingNoAccount()" style="flex:1;max-width:160px">No, not yet</button>
</div>
</div>
<div class="onboarding-box" id="onboarding-step-2" style="display:none">
<div style="font-size:40px;margin-bottom:12px">📋</div>
<div class="onboarding-title">Request Platform Login</div>
<div class="onboarding-sub">Select which platform you want an account created on. Our team will set it up and send you your login details.</div>
<div id="onboarding-alert" class="alert" style="margin-bottom:12px"></div>
<div class="fg" style="margin-bottom:14px;text-align:left">
<label style="font-size:14px;font-weight:700;color:var(--text2);display:block;margin-bottom:6px">Select Platform</label>
<select class="fi fi-select" id="onboarding-platform" style="width:100%">
<option value="">— Choose a platform —</option>
</select>
</div>
<div style="display:flex;gap:10px">
<button class="btn btn-gold" onclick="onboardingRequestAccount()" style="flex:1">Request Account</button>
<button class="btn btn-outline" onclick="onboardingDone()" style="flex:1">Skip for now</button>
</div>
</div>
<div class="onboarding-box" id="onboarding-step-3" style="display:none">
<div style="font-size:48px;margin-bottom:12px">✅</div>
<div class="onboarding-title">Request Submitted!</div>
<div class="onboarding-sub" id="onboarding-confirm-text">Your account request has been sent. You can see the status in your Profile under Games.</div>
<div style="display:flex;gap:10px;justify-content:center">
<button class="btn btn-gold" onclick="onboardingRequestAnother()" style="flex:1;max-width:160px">+ Another Platform</button>
<button class="btn btn-outline" onclick="onboardingDone()" style="flex:1;max-width:160px">Go to Home</button>
</div>
</div>
</div>
<!-- BROADCASTS PAGE -->
<div class="page" id="page-broadcasts">
<div class="section-title"><span>📢 NEWS &amp; ANNOUNCEMENTS</span></div>
<div id="broadcasts-list">
<div style="color:var(--text2);text-align:center;padding:32px;font-size:15px">Loading...</div>
</div>
</div>
<!-- CHAT PAGE -->
<div class="page" id="page-chat">
<div class="chat-header">
<div class="chat-header-avatar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<div style="flex:1">
<div class="chat-header-name">TomTomGames Support</div>
<div class="chat-header-status" id="chat-status-text">
<span class="chat-status-dot"></span> We reply within minutes
</div>
</div>
<div id="chat-typing-indicator" style="display:none;font-size:15px;color:var(--cyan);font-weight:600"></div>
</div>
<div class="chat-messages" id="chat-messages">
<div class="chat-loading">Loading messages...</div>
</div>
<div class="chat-quick-replies" id="chat-quick-replies" style="display:none">
<button class="chat-chip" onclick="quickReply('I need help with my tokens')">&#127918; Token issue</button>
<button class="chat-chip" onclick="quickReply('I have a cashout question')">&#128184; Cashout help</button>
<button class="chat-chip" onclick="quickReply('I need help with my account')">&#128100; Account help</button>
<button class="chat-chip" onclick="quickReply('I have a payment question')">&#128179; Payment help</button>
</div>
<div class="chat-input-bar">
<input class="chat-input" id="chat-input" type="text" placeholder="Type your message to support..." maxlength="1000"
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendChatMsg();}">
<button class="chat-send-btn" onclick="sendChatMsg()" id="chat-send-btn" title="Send message">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="18" height="18"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</div>
</div>
<nav class="navbar">
<button class="nav-btn active" data-page="home" onclick="showPage('home')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>HOME
</button>
<button class="nav-btn" data-page="buy" onclick="showPage('buy')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>BUY
</button>
<button class="nav-btn" data-page="cashout" onclick="showPage('cashout')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>CASH OUT
</button>
<button class="nav-btn chat-nav-btn" data-page="chat" onclick="showPage('chat')" style="position:relative">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<span class="chat-nav-badge" id="chat-nav-badge" style="display:none"></span>
SUPPORT
</button>
<button class="nav-btn" data-page="broadcasts" onclick="showPage('broadcasts')" style="position:relative">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 11.5 19.79 19.79 0 0 1 1.61 2.84 2 2 0 0 1 3.58 1H6.5a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.5a16 16 0 0 0 5.54 5.54l.86-.87a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 21 15.5"/><path d="m22 2-7 7"/><path d="M15 2h7v7"/></svg>
<span class="chat-nav-badge" id="broadcast-nav-badge" style="display:none"></span>
NEWS
</button>
<button class="nav-btn" data-page="profile" onclick="showPage('profile')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>PROFILE
</button>
</nav>
<div id="copyright-footer" style="position:fixed;bottom:0;left:50%;transform:translateX(-50%);width:100%;max-width:var(--max);text-align:center;padding-bottom:calc(var(--nav-h) + 2px);pointer-events:none;z-index:99">
<span style="font-size:11px;color:rgba(255,255,255,.18);font-family:'Exo 2',sans-serif;letter-spacing:.3px">&copy; 2026 TomTomGames.com, a division of TomTom Enterprises &nbsp;&middot;&nbsp; v<?= $_appVersion ?></span>
</div>
</div>
<!-- SUCCESS MODAL -->
<div class="modal-overlay" id="success-modal">
<div class="modal-sheet">
<div class="modal-handle"></div>
<div style="text-align:center;padding:16px 0 8px">
<div style="font-size:56px;margin-bottom:14px" id="modal-icon">🎉</div>
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:20px;color:var(--text);margin-bottom:16px" id="modal-title">Tokens Added!</div>
<div id="modal-card-content"></div>
<button class="btn btn-gold" style="margin-top:20px" id="modal-action-btn">AWESOME!</button>
</div>
</div>
</div>
<!-- PAYMENT PROCESSING OVERLAY -->
<div id="pay-processing-overlay" style="display:none;position:fixed;inset:0;background:rgba(5,5,15,.96);z-index:500;flex-direction:column;align-items:center;justify-content:center;gap:0;backdrop-filter:blur(8px)">
<div class="pay-proc-ring">
<div class="pay-proc-spinner"></div>
<div class="pay-proc-icon" id="pay-proc-icon">🔒</div>
</div>
<div class="pay-proc-title" id="pay-proc-title">Processing Payment</div>
<div class="pay-proc-sub" id="pay-proc-sub">Please wait — do not close the app</div>
<div class="pay-proc-steps" id="pay-proc-steps">
<div class="pay-proc-step active" id="step-1">🔐 Securing connection</div>
<div class="pay-proc-step" id="step-2">💳 Authorizing card</div>
<div class="pay-proc-step" id="step-3">✅ Confirming payment</div>
<div class="pay-proc-step" id="step-4">Crediting tokens</div>
</div>
</div>
<!-- PAYMENT SUCCESS OVERLAY (replaces old modal) -->
<div id="pay-success-overlay" style="display:none;position:fixed;inset:0;background:rgba(5,5,15,.97);z-index:501;flex-direction:column;align-items:center;justify-content:center;padding:32px;backdrop-filter:blur(8px)">
<div class="pay-result-wrap" id="pay-result-wrap"><!-- filled by JS --></div>
</div>
<!-- Square SDK loads async — won't block app startup -->
<script src="<?= \SquarePayment::sdkUrl() ?>"></script>
<script>
const CFG = {
sqAppId: '<?= SQUARE_APP_ID ?>',
sqLocId: '<?= SQUARE_LOCATION_ID ?>',
sqEnv: '<?= SQUARE_ENV ?>',
packages: <?= TOKEN_PACKAGES ?>,
platforms: [], // loaded dynamically from /api/platforms.php
pay: {
venmo: '<?= PAY_VENMO ?>',
chime: '<?= PAY_CHIME ?>',
cashapp: '<?= PAY_CASHAPP ?>',
zelle: '<?= PAY_ZELLE ?>',
}
};
let S = { user:null, pkg:null, method:'card', sqCard:null };
async function doLogin() {
const btn=document.getElementById('login-btn'); const al=document.getElementById('login-alert');
const u=document.getElementById('login-user').value.trim(); const p=document.getElementById('login-pass').value;
if (!u||!p){showAlert(al,'Please fill in all fields.','error');return;}
document.getElementById('login-unverified-box').style.display='none';
btn.innerHTML='<span class="spinner"></span>'; btn.disabled=true;
try {
const d=await api('/api/login.php',{username:u,password:p});
console.log('Login response:', JSON.stringify(d));
if(d.success){
if (d.user && d.user.is_admin == 1) {
window.location.href = '/admin/';
return;
}
console.log('Login success, calling showApp...');
S.user=d.user; showApp();
} else if(d.unverified) {
// Show unverified box with resend option
hideAlert(al);
S._unverifiedEmail = d.email || '';
document.getElementById('login-unverified-box').style.display='block';
} else {
showAlert(al,d.error||'Login failed.','error');
}
} catch{showAlert(al,'Network error.','error');}
btn.innerHTML='LOGIN'; btn.disabled=false;
}
// ─── INIT ──────────────────────────────────────────────────
window.addEventListener('DOMContentLoaded', function() {
// Load platforms and payment methods dynamically, then build UI
Promise.all([
fetch('/api/platforms.php?action=list').then(r=>r.json()).catch(()=>({success:false})),
fetch('/api/payment_settings.php?action=list').then(r=>r.json()).catch(()=>({success:false}))
]).then(function(results) {
if (results[0].success) CFG.platforms = results[0].platforms;
if (results[1].success) CFG.payMethods = results[1].methods;
try { buildPlatforms(); } catch(e) {}
try { buildPkgPills(); } catch(e) {}
try { buildCashoutPlatforms(); } catch(e) {}
try { buildPaymentMethods(); } catch(e) {}
}).catch(function() {
try { buildPlatforms(); } catch(e) {}
try { buildPkgPills(); } catch(e) {}
try { buildCashoutPlatforms(); } catch(e) {}
});
fetch('/api/me.php')
.then(function(r) { return r.json(); })
.then(function(d) {
if (d && d.success && d.user) {
if (d.user.is_admin == 1) {
window.location.href = '/admin/';
} else {
S.user = d.user; showApp();
}
} else showAuth();
})
.catch(function() { showAuth(); });
});
function api(url, body) {
const opts = { method: body ? 'POST' : 'GET', headers: { 'Content-Type': 'application/json' } };
if (body) opts.body = JSON.stringify(body);
return fetch(url, opts).then(r => r.json());
}
function showApp() {
var auth = document.getElementById('auth-screen');
var app = document.getElementById('main-app');
if (auth) auth.setAttribute('style', 'display:none');
if (app) app.setAttribute('style', 'display:block;min-height:100vh;');
// Rebuild dynamic UI now that app is visible (covers race where me.php resolves before data fetches)
if (CFG.platforms && CFG.platforms.length) {
try { buildPlatforms(); buildCashoutPlatforms(); } catch(e) {}
}
if (CFG.payMethods && CFG.payMethods.length) {
try { buildPaymentMethods(); } catch(e) {}
}
try { updateUI(); } catch(e) { console.warn('updateUI err:', e); }
try { loadCashoutHistory(); } catch(e) { console.warn('cashout err:', e); }
try { pollChatBadge(); } catch(e) { console.warn('badge err:', e); }
// Load aliases for auto-prefill on buy page
api('/api/game_aliases.php?action=get').then(d => {
if (d.success) savedAliases = d.aliases || {};
}).catch(() => {});
loadCashoutMethodTypes();
setTimeout(pollBroadcastBadge, 1000);
setInterval(pollBroadcastBadge, 60000);
setTimeout(checkOnboarding, 1500);
// Inject coin into static elements
setTimeout(function() {
var bfi = document.getElementById('buy-form-icon');
if (bfi) bfi.innerHTML = tokenCoin(48);
}, 100);
}
function showAuth() {
var auth = document.getElementById('auth-screen');
var app = document.getElementById('main-app');
if (app) app.setAttribute('style', 'display:none;');
if (auth) auth.setAttribute('style', 'display:flex');
}
function switchTab(t) {
document.getElementById('tab-login').style.display = t === 'login' ? 'block' : 'none';
document.getElementById('tab-register').style.display = t === 'register' ? 'block' : 'none';
document.querySelectorAll('.auth-tab').forEach((el,i) =>
el.classList.toggle('active',(i===0&&t==='login')||(i===1&&t==='register'))
);
}
function showPage(n) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
document.getElementById('page-'+n)?.classList.add('active');
document.querySelector(`.nav-btn[data-page="${n}"]`)?.classList.add('active');
const _cr=document.getElementById('copyright-footer'); if(_cr) _cr.style.display=n==='home'?'block':'none';
if (n === 'buy') { updatePayForm(); initBuyPageBilling(); setTimeout(initSquare, 150); }
if (n === 'broadcasts') { loadBroadcasts_player(); }
if (n === 'cashout') {
updateUI();
loadCashoutHistory();
loadCashoutPayoutMethods();
// Prefill alias for currently selected platform if any
const pid = document.getElementById('cashout-platform')?.value;
if (pid) prefillCashoutAlias(pid);
}
if (n === 'profile') { loadActivityDashboard(); switchProfileTab('activity', document.querySelector('.profile-tab')); }
if (n === 'chat') {
// Clear badge when opening chat
document.getElementById('chat-nav-badge').style.display = 'none';
initChat();
setTimeout(scrollChat, 100);
}
}
function setText(id, val) { var el=document.getElementById(id); if(el) el.textContent=val; }
function displayName(u) { return u ? (u.alias || u.username || '?') : '?'; }
function updateUI() {
if (!S.user) return;
const t = parseFloat(S.user.tokens) || 0;
const name = displayName(S.user);
const init = name.charAt(0).toUpperCase();
const setHTML = (id, html) => { const el=document.getElementById(id); if(el) el.innerHTML=html; };
setText('home-alias', name);
setText('cashout-balance', t);
setText('profile-username', name);
setText('profile-alias', S.user.username ? '@' + S.user.username : '');
setText('avatar-btn', init);
setText('profile-avatar', init);
setHTML('header-tokens', t + '&nbsp;' + tokenCoin(18));
setHTML('home-balance', '<span style="font-family:\'Exo 2\',sans-serif">' + t + '</span>');
setHTML('profile-tokens', t + '&nbsp;' + tokenCoin(24));
}
async function refreshUser() {
const d = await api('/api/me.php');
if (d.success) { S.user = d.user; updateUI(); }
}
// ─── PLATFORMS ─────────────────────────────────────────────
function openPlatform(slug, url, aliasParam) {
const alias = savedAliases[slug] || '';
let launchUrl = url;
if (alias && aliasParam) {
const sep = url.includes('?') ? '&' : '?';
launchUrl = url + sep + encodeURIComponent(aliasParam) + '=' + encodeURIComponent(alias);
}
window.open(launchUrl, '_blank', 'noopener');
if (alias) {
navigator.clipboard?.writeText(alias).catch(() => {});
const t = document.getElementById('platform-alias-toast');
if (t) {
t.textContent = '📋 "' + alias + '" copied — paste into login';
t.style.opacity = '1'; t.style.transform = 'translateY(0)';
clearTimeout(t._timer);
t._timer = setTimeout(() => { t.style.opacity='0'; t.style.transform='translateY(10px)'; }, 3000);
}
}
}
function buildPlatforms() {
if (!CFG.platforms || !CFG.platforms.length) return;
const grid = document.getElementById('platform-grid');
if (grid) {
grid.innerHTML = CFG.platforms.map(p => `
<div class="platform-card" style="--p-color:${p.color};cursor:pointer"
data-slug="${escHtml(p.id)}" data-url="${escHtml(p.url)}" data-param="${escHtml(p.alias_param||'')}">
<div class="platform-img-wrap">
<img src="/assets/img/${p.id}.svg" alt="${escHtml(p.name)}" onerror="this.style.display='none'">
</div>
<div class="platform-name">${escHtml(p.name)}</div>
<div class="play-btn">TAP TO PLAY →</div>
</div>`).join('');
grid.querySelectorAll('.platform-card[data-slug]').forEach(card => {
card.addEventListener('click', () =>
openPlatform(card.dataset.slug, card.dataset.url, card.dataset.param));
});
}
// Populate selects — clear dynamic options first to prevent duplicates on re-call
const buySel = document.getElementById('buy-platform');
if (buySel) {
while (buySel.options.length > 1) buySel.remove(1);
CFG.platforms.forEach(p => {
const o = document.createElement('option');
o.value = p.id; o.textContent = p.name; buySel.appendChild(o);
});
}
}
function buildCashoutPlatforms() {
const sel = document.getElementById('cashout-platform');
if (!sel) return;
while (sel.options.length > 1) sel.remove(1);
CFG.platforms.forEach(p => {
const o = document.createElement('option'); o.value = p.id; o.textContent = p.name; sel.appendChild(o);
});
}
// ─── TOKEN PACKAGES ────────────────────────────────────────
function pkgScroll(dir) {
const el = document.getElementById('pkg-scroll');
if (el) el.scrollBy({ left: dir * 160, behavior: 'smooth' });
}
function initPkgScrollMouse() {
const el = document.getElementById('pkg-scroll');
if (!el) return;
// Show/hide arrows based on scroll position
const prev = document.getElementById('pkg-prev');
const next = document.getElementById('pkg-next');
function updateArrows() {
if (!prev || !next) return;
const canLeft = el.scrollLeft > 4;
const canRight = el.scrollLeft < el.scrollWidth - el.clientWidth - 4;
prev.style.display = canLeft ? 'block' : 'none';
next.style.display = canRight ? 'block' : 'none';
}
el.addEventListener('scroll', updateArrows, { passive: true });
setTimeout(updateArrows, 100);
// Mouse wheel → horizontal scroll
el.addEventListener('wheel', e => {
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
e.preventDefault();
el.scrollBy({ left: e.deltaY * 1.5, behavior: 'auto' });
}
}, { passive: false });
// Click-drag scroll
let down = false, startX = 0, startScroll = 0, moved = false;
el.addEventListener('mousedown', e => {
down = true; moved = false;
startX = e.pageX; startScroll = el.scrollLeft;
el.style.cursor = 'grabbing'; el.style.userSelect = 'none';
});
document.addEventListener('mousemove', e => {
if (!down) return;
const dx = e.pageX - startX;
if (Math.abs(dx) > 4) moved = true;
el.scrollLeft = startScroll - dx;
});
document.addEventListener('mouseup', () => {
if (!down) return;
down = false;
el.style.cursor = 'grab'; el.style.userSelect = '';
// If the mouse barely moved, let the click on the pill go through
if (!moved) el.style.cursor = '';
});
el.style.cursor = 'grab';
}
function buildPkgPills() {
const container = document.getElementById('pkg-scroll');
container.innerHTML = CFG.packages.map((pkg, i) => `
<div class="pkg-pill${pkg.popular?' popular':''}" onclick="selectPkg(${i},this)">
<div class="pkg-pill-tokens">${pkg.tokens}</div>
<div class="pkg-pill-price">$${pkg.price}</div>
</div>`).join('');
const popularIdx = CFG.packages.findIndex(p => p.popular);
const idx = popularIdx >= 0 ? popularIdx : 0;
selectPkg(idx, container.querySelectorAll('.pkg-pill')[idx]);
initPkgScrollMouse();
}
function selectPkg(i, el) {
S.pkg = CFG.packages[i]; S.customPkg = false;
document.querySelectorAll('.pkg-pill').forEach(p => p.classList.remove('selected'));
el?.classList.add('selected');
const ci = document.getElementById('custom-amt');
if (ci) ci.value = '';
document.getElementById('custom-amt-equiv').style.display = 'none';
updateOrderSummary();
}
function selectCustomPkg(val) {
const amt = parseInt(val);
if (!amt || amt < 1) {
document.getElementById('custom-amt-equiv').style.display = 'none';
S.pkg = null; S.customPkg = false; updateOrderSummary(); return;
}
document.querySelectorAll('.pkg-pill').forEach(p => p.classList.remove('selected'));
S.customPkg = true;
S.pkg = { tokens: amt, price: amt, label: amt + ' Tokens (Custom)', popular: false };
document.getElementById('custom-token-count').textContent = amt;
document.getElementById('custom-amt-equiv').style.display = 'flex';
updateOrderSummary();
}
function updateOrderSummary() {
const el = document.getElementById('order-summary');
if (!S.pkg) { el.style.display='none'; return; }
document.getElementById('summary-pkg').textContent = S.pkg.label || S.pkg.tokens+' Tokens';
const sumTok = document.getElementById('summary-tokens'); if(sumTok) sumTok.innerHTML = S.pkg.tokens + '&nbsp;' + tokenCoin(18);
document.getElementById('summary-total').textContent = '$'+S.pkg.price+'.00';
el.style.display = 'block';
}
function updateAmountDisplay() { updateOrderSummary(); }
function updatePayForm() {
updateAmountDisplay();
const alias = document.getElementById('buy-alias').value.trim();
const platformId = document.getElementById('buy-platform').value;
document.getElementById('ref-alias').textContent = alias || '—';
document.getElementById('ref-platform').textContent = platformId ? CFG.platforms.find(p=>p.id===platformId)?.name : '—';
if (S.method !== 'card') renderManualInstructions();
}
// ─── PAYMENT METHOD ────────────────────────────────────────
function selectPMT(el, method) {
document.querySelectorAll('.pmt').forEach(m => m.classList.remove('active'));
el.classList.add('active');
S.method = method;
document.getElementById('section-card').style.display = method === 'card' ? 'block' : 'none';
document.getElementById('section-manual').style.display = method !== 'card' ? 'block' : 'none';
if (method !== 'card') renderManualInstructions();
}
function renderManualInstructions() {
const method = S.method;
const pkg = S.pkg;
const alias = document.getElementById('buy-alias').value.trim();
const platformId = document.getElementById('buy-platform').value;
const platformName = platformId ? CFG.platforms.find(p => p.id === platformId)?.name || platformId : '(your platform)';
const amt = pkg ? `$${pkg.price}.00` : '(select a package)';
const handle = CFG.pay[method] || '—';
const iconMap = { venmo:'💙', chime:'🟢', cashapp:'💚', zelle:'💜' };
const nameMap = {
venmo: (CFG.payLabels&&CFG.payLabels.venmo)||'Venmo',
chime: (CFG.payLabels&&CFG.payLabels.chime)||'Chime',
cashapp:(CFG.payLabels&&CFG.payLabels.cashapp)||'Cash App',
zelle: (CFG.payLabels&&CFG.payLabels.zelle)||'Zelle',
};
document.getElementById('manual-box').innerHTML = `
<div class="manual-box-title">${iconMap[method]} ${nameMap[method]} Instructions</div>
<div class="manual-step">
<div class="manual-step-num">1</div>
<div class="manual-step-text">Open your <strong>${nameMap[method]}</strong> app and send <strong>${amt}</strong> to <span class="handle">${handle}</span></div>
</div>
<div class="manual-step">
<div class="manual-step-num">2</div>
<div class="manual-step-text">In the <strong>note/memo</strong>, include: <strong>${platformName}</strong> — <strong>${alias || 'your alias'}</strong></div>
</div>
<div class="manual-step">
<div class="manual-step-num">3</div>
<div class="manual-step-text">Click <strong>Submit Request</strong> below. We'll verify your payment and credit your tokens within a few hours.</div>
</div>`;
const refBadge = document.getElementById('ref-badge');
if (alias && platformId) { refBadge.style.display = 'block'; }
else { refBadge.style.display = 'none'; }
}
// ─── SQUARE ────────────────────────────────────────────────
async function initSquare() {
if (!window.Square) {
console.warn('Square SDK not loaded — card unavailable');
document.getElementById('card-container').innerHTML =
'<div style="color:#ff6666;font-size:14px;padding:8px">Card form unavailable. Please refresh the page.</div>';
return;
}
try {
// Destroy existing instance before re-attaching
if (S.sqCard) {
try { await S.sqCard.destroy(); } catch(e) {}
S.sqCard = null;
}
// Clear container so Square has a clean div to inject into
var container = document.getElementById('card-container');
container.innerHTML = '';
var pay = window.Square.payments(CFG.sqAppId, CFG.sqLocId);
S.sqCard = await pay.card({
style: {
'.input-container' : { borderColor: '#dddddd', borderRadius: '8px' },
'.input-container.is-focus': { borderColor: '#0095d9' },
'input' : { color: '#1a1a2e', fontSize: '16px' },
'input::placeholder' : { color: '#aaaaaa' },
}
});
await S.sqCard.attach('#card-container');
console.log('Square card attached OK');
var lbl = document.getElementById('card-ready-label');
if (lbl) lbl.textContent = '✓ Ready';
} catch(e) {
console.error('Square init error:', e);
document.getElementById('card-container').innerHTML =
'<div style="color:#ff9999;font-size:14px;padding:8px">Card form error: ' + e.message + '</div>';
}
}
// ─── SUBMIT PAYMENT ────────────────────────────────────────
// ─── PAYMENT PROCESSING OVERLAY ──────────────────────────
const PROC_STEPS = ['step-1','step-2','step-3','step-4'];
let procTimer = null;
function showProcessingOverlay() {
const ov = document.getElementById('pay-processing-overlay');
ov.style.display = 'flex';
PROC_STEPS.forEach(s => {
const el = document.getElementById(s);
el.classList.remove('active','done');
});
document.getElementById('pay-proc-icon').textContent = '🔒';
document.getElementById('pay-proc-title').textContent = 'Processing Payment';
document.getElementById('pay-proc-sub').textContent = 'Please wait — do not close the app';
// Animate steps
let i = 0;
procTimer = setInterval(() => {
if (i > 0) document.getElementById(PROC_STEPS[i-1]).classList.replace('active','done');
if (i < PROC_STEPS.length) {
document.getElementById(PROC_STEPS[i]).classList.add('active');
i++;
} else { clearInterval(procTimer); }
}, 700);
}
function hideProcessingOverlay() {
clearInterval(procTimer);
document.getElementById('pay-processing-overlay').style.display = 'none';
}
function showPayResult(type, data) {
hideProcessingOverlay();
const ov = document.getElementById('pay-success-overlay');
const wrap = document.getElementById('pay-result-wrap');
ov.style.display = 'flex';
if (type === 'success') {
const newBal = parseFloat(S.user?.tokens || 0);
wrap.innerHTML = `
<span class="pay-result-icon">🎉</span>
<div class="pay-result-title success">Payment Approved!</div>
<div class="pay-balance-bump">+${data.tokens_added}&nbsp;${tokenCoin(28)}</div>
<div style="font-size:14px;color:var(--text2);margin-bottom:4px">New Balance</div>
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:26px;color:var(--gold2);margin-bottom:16px">${data.new_balance !== undefined ? data.new_balance : parseFloat(S.user?.tokens||0)} Tokens</div>
<div class="pay-result-detail">
<strong>Amount charged:</strong> $${data.amount || ''}<br>
<strong>Platform:</strong> ${data.platform || ''}<br>
<strong>Game alias:</strong> <span style="color:var(--cyan)">${data.alias || ''}</span><br>
${data.card ? `<strong>Card:</strong> ${data.card}` : ''}
</div>
<div class="pay-result-btns">
<button class="btn btn-gold" onclick="closePayResult();showPage('home')">🎰 PLAY NOW!</button>
<button class="btn btn-outline" onclick="closePayResult();showPage('buy')" style="padding:10px;font-size:15px">Buy More Tokens</button>
</div>`;
} else if (type === 'manual') {
wrap.innerHTML = `
<span class="pay-result-icon">⏳</span>
<div class="pay-result-title pending">Request Submitted!</div>
<div style="font-size:14px;color:var(--text2);margin:12px 0 4px">Tokens pending — awaiting payment confirmation</div>
<div style="display:flex;align-items:center;justify-content:center;gap:8px;font-family:'Exo 2',sans-serif;font-weight:900;font-size:40px;color:var(--gold);margin-bottom:4px">${data.tokens}${tokenCoin(36)}</div>
<div style="font-size:14px;color:var(--text2);margin-bottom:16px">will be added to your account once payment is received</div>
<div class="pay-result-detail">
Order <strong style="color:var(--cyan)">#${data.purchase_id}</strong> received.<br><br>
<strong style="color:var(--gold)">📲 Next steps:</strong><br>
1. Send <strong>$${data.amount}</strong> via <strong>${data.method}</strong><br>
2. Include your order # <strong style="color:var(--cyan)">#${data.purchase_id}</strong> in the memo<br>
3. Tokens are credited <strong>within a few minutes</strong> after we confirm receipt<br><br>
<span style="color:var(--text2);font-size:14px">⚠️ Your token balance will not change until payment is verified by our team.</span>
</div>
<div class="pay-result-btns">
<button class="btn btn-gold" onclick="closePayResult();showPage('home')">GOT IT — HOME</button>
<button class="btn btn-outline" onclick="closePayResult();showPage('chat')" style="padding:10px;font-size:15px">💬 Contact Support</button>
</div>`;
} else {
wrap.innerHTML = `
<span class="pay-result-icon">❌</span>
<div class="pay-result-title error">Payment Failed</div>
<div class="pay-result-detail">${data.error || 'Your payment could not be processed. Please check your card details and try again.'}</div>
<div class="pay-result-btns">
<button class="btn btn-gold" onclick="closePayResult()">TRY AGAIN</button>
<button class="btn btn-outline" onclick="closePayResult();showPage('chat')" style="padding:10px;font-size:15px">💬 Contact Support</button>
</div>`;
}
}
function closePayResult() {
document.getElementById('pay-success-overlay').style.display = 'none';
}
async function submitPayment() {
const btn = document.getElementById('pay-submit-btn');
const al = document.getElementById('buy-alert');
const platformId = document.getElementById('buy-platform').value;
const gameAlias = document.getElementById('buy-alias').value.trim();
const platformName = platformId ? CFG.platforms.find(p=>p.id===platformId)?.name || platformId : '';
if (!platformId) { showAlert(al, 'Please select a gaming platform.', 'error'); return; }
if (!gameAlias) { showAlert(al, 'Please enter your in-game alias.', 'error'); return; }
if (!S.pkg) { showAlert(al, 'Please select a token package or enter a custom amount.', 'error'); return; }
if (S.pkg.price < 1) { showAlert(al, 'Minimum purchase is $1.', 'error'); return; }
const billing = {
first_name: (document.getElementById('card-first-name')?.value || '').trim(),
last_name: (document.getElementById('card-last-name')?.value || '').trim(),
address: (document.getElementById('card-address')?.value || '').trim(),
city: (document.getElementById('card-city')?.value || '').trim(),
state: (document.getElementById('card-state')?.value || '').trim().toUpperCase(),
zip: (document.getElementById('card-zip')?.value || '').trim(),
email: (document.getElementById('card-email')?.value || '').trim(),
};
if (S.method === 'card') {
if (!billing.first_name || !billing.last_name) { showAlert(al, 'Please enter the name on your card.', 'error'); return; }
if (!billing.zip) { showAlert(al, 'Billing ZIP code is required.', 'error'); return; }
}
btn.disabled = true;
hideAlert(al);
// ── Card tokenize BEFORE showing overlay (Square SDK needs DOM visible)
let sourceId = '';
if (S.method === 'card') {
if (!S.sqCard) { showAlert(al,'Card form not ready. Please refresh.','error'); btn.disabled=false; return; }
try {
const result = await S.sqCard.tokenize();
if (result.status !== 'OK') {
showAlert(al, result.errors?.[0]?.message || 'Card declined. Check your card details.', 'error');
btn.disabled=false; return;
}
sourceId = result.token;
} catch { showAlert(al,'Payment error. Try again.','error'); btn.disabled=false; return; }
}
// ── Show full-screen processing overlay
showProcessingOverlay();
try {
const d = await api('/api/purchase.php', {
source_id: sourceId,
tokens: S.pkg.tokens,
price_cents: S.pkg.price * 100,
method: S.method,
platform_id: platformId,
game_alias: gameAlias,
player_name: S.user?.alias || '',
is_custom: S.customPkg || false,
billing,
});
// Mark all steps done
PROC_STEPS.forEach(s => {
const el = document.getElementById(s);
el.classList.remove('active');
el.classList.add('done');
});
await new Promise(r => setTimeout(r, 600)); // brief pause for visual satisfaction
if (d.success) {
// Update local token balance immediately from API response
if (d.new_balance !== undefined && S.user) {
S.user.tokens = d.new_balance;
updateUI();
}
await refreshUser(); // then sync full user object from server
// Auto-save billing if checkbox checked
const saveCb = document.getElementById('save-billing-cb');
if (saveCb?.checked && billing.first_name && S.method === 'card') {
const cardBrand = d.card_brand || '';
const cardLast4 = d.card_last4 || '';
await api('/api/billing.php?action=save', { ...billing, card_brand: cardBrand, card_last4: cardLast4 });
}
// Auto-save game alias for this platform
if (gameAlias && platformId) {
savedAliases[platformId] = gameAlias;
api('/api/game_aliases.php?action=save', { platform_slug: platformId, alias: gameAlias }).catch(()=>{});
}
// Reset form
const ci = document.getElementById('custom-amt');
if (ci) ci.value = '';
document.getElementById('custom-amt-equiv').style.display = 'none';
document.getElementById('order-summary').style.display = 'none';
if (d.manual) {
// Manual payment (Venmo, Zelle, Cash App, Chime) — pending admin approval
const methodLabels = { venmo:'Venmo', chime:'Chime', cashapp:'Cash App', zelle:'Zelle' };
showPayResult('manual', {
tokens: S.pkg.tokens,
amount: (S.pkg.price).toFixed(2),
method: (CFG.payLabels && CFG.payLabels[S.method]) || methodLabels[S.method] || S.method,
purchase_id: d.purchase_id,
});
} else {
// Card payment — tokens credited immediately
const newBalance = d.new_balance !== undefined ? parseFloat(d.new_balance) : parseFloat(S.user?.tokens || 0);
showPayResult('success', {
tokens_added: d.tokens_added || S.pkg.tokens,
new_balance: newBalance,
amount: (S.pkg.price).toFixed(2),
platform: platformName,
alias: gameAlias,
card: d.card_brand ? d.card_brand + ' ····' + d.card_last4 : null,
});
}
} else {
showPayResult('error', { error: d.error || 'Payment failed.' });
}
} catch {
hideProcessingOverlay();
showAlert(al, 'Network error. Please try again.', 'error');
}
btn.disabled = false;
}
// ─── PROFILE TABS ─────────────────────────────────────────
function switchProfileTab(name, btn) {
document.querySelectorAll('.profile-tab').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.profile-tab-panel').forEach(p => p.classList.remove('active'));
btn?.classList.add('active');
document.getElementById('ptab-'+name)?.classList.add('active');
if (name === 'billing') loadBillingProfile();
if (name === 'account') populateAccount();
if (name === 'games') loadGameAliases();
if (name === 'payout') { loadPayoutMethods(); loadCashoutMethodTypes(); }
if (name === 'activity') loadActivityDashboard();
if (name === 'referrals') loadReferralDashboard();
if (name === 'logins') { loadPlatformLogins(); populatePlatformSelect('req-platform-slug'); }
if (name === 'logins') loadPlatformLogins();
}
// ─── BROADCASTS (PLAYER) ───────────────────────────────────
async function loadBroadcasts_player() {
const el = document.getElementById('broadcasts-list');
const d = await api('/api/broadcast.php?action=list');
if (!d.success || !d.broadcasts.length) {
el.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text2)"><div style="font-size:40px;margin-bottom:12px">📢</div><div style="font-size:14px">No announcements yet.</div><div style="font-size:14px;margin-top:4px">Check back later for news and updates.</div></div>';
return;
}
el.innerHTML = d.broadcasts.map(b => {
const unread = parseInt(b.is_read) === 0;
return `<div class="bc-card${unread?' unread':''}" id="bc-card-${b.id}">
<div class="bc-header">
<div class="bc-avatar">📢</div>
<div style="flex:1;min-width:0">
<div style="display:flex;align-items:center;gap:8px">
<div class="bc-subject">${escHtml(b.subject)}</div>
${unread?'<span style="font-size:15px;font-weight:700;color:var(--gold);background:rgba(240,192,64,.15);border:1px solid rgba(240,192,64,.3);border-radius:4px;padding:2px 6px">NEW</span>':''}
</div>
<div class="bc-meta">From <strong>${escHtml(b.sender_name)}</strong> · ${(b.sent_at||'').substring(0,16)}</div>
</div>
</div>
<div class="bc-body">${escHtml(b.message)}</div>
<div class="bc-footer">
<button onclick="toggleBcReplies(${b.id})" style="background:none;border:none;color:var(--cyan);font-size:14px;font-weight:700;cursor:pointer;padding:0">
💬 ${b.reply_count} repl${parseInt(b.reply_count)!==1?'ies':'y'}
</button>
<span style="color:var(--text2);font-size:15px">👁 ${b.read_count} read</span>
</div>
<div class="bc-reply-wrap" id="bc-replies-${b.id}">
<div class="bc-replies-list" id="bc-rlist-${b.id}"></div>
<div style="display:flex;flex-direction:column;gap:8px">
<textarea id="bc-rinput-${b.id}" rows="3" placeholder="Write a reply..." style="width:100%;box-sizing:border-box;background:var(--bg3);border:1px solid var(--border);border-radius:10px;padding:12px 14px;color:var(--text);font-family:'Rajdhani',sans-serif;font-size:14px;resize:none;outline:none;transition:border-color .15s" onfocus="this.style.borderColor='var(--cyan)'" onblur="this.style.borderColor=''" onkeydown="if(event.key==='Enter'&&event.ctrlKey){event.preventDefault();playerBcReply(${b.id});}"></textarea>
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="font-size:15px;color:var(--text2)">Ctrl+Enter to send</span>
<button class="btn btn-gold" onclick="playerBcReply(${b.id})" style="padding:10px 20px;font-size:15px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="14" height="14" style="margin-right:5px"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>SEND REPLY
</button>
</div>
</div>
</div>
</div>`;
}).join('');
// Mark all as read
d.broadcasts.forEach(b => {
if (!parseInt(b.is_read)) {
api('/api/broadcast.php?action=mark_read',{broadcast_id:b.id}).catch(()=>{});
}
});
pollBroadcastBadge();
}
async function toggleBcReplies(id) {
const wrap = document.getElementById('bc-replies-'+id);
const isOpen = wrap.classList.contains('open');
wrap.classList.toggle('open', !isOpen);
if (!isOpen) await loadBcReplies_player(id);
}
async function loadBcReplies_player(id) {
const el = document.getElementById('bc-rlist-'+id);
el.innerHTML = '<div style="color:var(--text2);font-size:14px;padding:8px">Loading...</div>';
const d = await api('/api/broadcast.php?action=replies&broadcast_id='+id);
if (!d.success||!d.replies.length) { el.innerHTML='<div style="color:var(--text2);font-size:14px;padding:8px;text-align:center">No replies yet — be the first!</div>'; return; }
el.innerHTML = d.replies.map(r => {
const isAdm = parseInt(r.is_admin);
return `<div class="bc-reply-row">
<div style="width:28px;height:28px;border-radius:50%;background:${isAdm?'linear-gradient(135deg,#f0c040,#d4a017)':'linear-gradient(135deg,#7b2fbe,#457b9d)'};display:flex;align-items:center;justify-content:center;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;color:#fff;flex-shrink:0">${(r.username||'?').charAt(0).toUpperCase()}</div>
<div class="bc-reply-bubble">
<div class="bc-reply-who${isAdm?' admin':''}">${escHtml(r.username)}${isAdm?' 🔑':''} <span style="font-weight:400;color:var(--text2)">· ${(r.created_at||'').substring(11,16)}</span></div>
<div class="bc-reply-msg">${escHtml(r.message)}</div>
</div>
</div>`;
}).join('');
}
async function playerBcReply(id) {
const input = document.getElementById('bc-rinput-'+id);
const msg = input.value.trim();
if (!msg) return;
input.value = '';
input.style.borderColor = '';
const d = await api('/api/broadcast.php?action=reply',{broadcast_id:id,message:msg});
if (d.success) {
await loadBcReplies_player(id);
// Update reply count
const card = document.getElementById('bc-card-'+id);
if (card) {
const btn = card.querySelector('.bc-footer button');
if (btn) {
const current = parseInt(btn.textContent.match(/\d+/)?.[0]||0) + 1;
btn.textContent = `💬 ${current} repl${current!==1?'ies':'y'}`;
}
}
}
}
async function pollBroadcastBadge() {
try {
const d = await api('/api/broadcast.php?action=unread_count');
const badge = document.getElementById('broadcast-nav-badge');
if (badge) badge.style.display = d.count > 0 ? 'block' : 'none';
} catch(e) {}
}
const PAYOUT_ICONS = { venmo:'💙', cashapp:'💚', zelle:'💜', chime:'🟢', bank:'🏦', other:'💰' };
const PAYOUT_LABELS = { venmo:'Venmo', cashapp:'Cash App', zelle:'Zelle', chime:'Chime', bank:'Bank Transfer', other:'Other' };
let payoutMethods = [];
function prefillCashoutAlias(platformId) {
const input = document.getElementById('cashout-alias');
if (!input) return;
input.value = (platformId && savedAliases[platformId]) ? savedAliases[platformId] : '';
}
function saveCashoutAlias() {
const platformId = document.getElementById('cashout-platform')?.value;
const alias = document.getElementById('cashout-alias')?.value.trim();
if (!platformId || !alias || !S.user) return;
savedAliases[platformId] = alias;
api('/api/game_aliases.php?action=save', { platform_slug: platformId, alias }).catch(() => {});
}
async function loadPayoutMethods() {
const el = document.getElementById('payout-methods-list');
const d = await api('/api/payout_methods.php?action=list');
payoutMethods = d.success ? (d.methods || []) : [];
renderPayoutMethodsList(el, payoutMethods);
}
async function loadCashoutPayoutMethods() {
const el = document.getElementById('cashout-payout-list');
const noEl = document.getElementById('cashout-no-payout');
const d = await api('/api/payout_methods.php?action=list');
payoutMethods = d.success ? (d.methods || []) : [];
if (!payoutMethods.length) {
if (el) el.innerHTML = '';
if (noEl) noEl.style.display = 'block';
return;
}
if (noEl) noEl.style.display = 'none';
if (el) {
el.innerHTML = payoutMethods.map(m => {
const on = parseInt(m.admin_enabled);
const bg = on && parseInt(m.is_default) ? 'rgba(0,229,255,.07)' : 'var(--bg3)';
const border = on && parseInt(m.is_default) ? 'rgba(0,229,255,.25)' : 'var(--border)';
const badge = !on
? '<span style="font-size:13px;font-weight:700;color:var(--text2);border:1px solid var(--border);border-radius:4px;padding:2px 7px;white-space:nowrap">UNAVAILABLE</span>'
: parseInt(m.is_default) ? '<span style="font-size:14px;font-weight:700;color:var(--cyan);border:1px solid rgba(0,229,255,.2);border-radius:4px;padding:2px 7px">DEFAULT</span>' : '';
return `
<label style="display:flex;align-items:center;gap:10px;background:${bg};border:1px solid ${border};border-radius:8px;padding:10px 12px;margin-bottom:6px;cursor:${on?'pointer':'not-allowed'};opacity:${on?'1':'0.45'}">
<input type="radio" name="cashout-payout" value="${m.id}" ${on && parseInt(m.is_default)?'checked':''} ${!on?'disabled':''} style="accent-color:var(--cyan)">
<span style="font-size:18px">${PAYOUT_ICONS[m.method_type]||'💰'}</span>
<div style="flex:1;min-width:0">
<div style="font-weight:700;font-size:15px">${escHtml(m.label)}</div>
<div style="font-size:14px;color:var(--text2)">${escHtml(m.account_handle)}</div>
</div>
${badge}
</label>`;
}).join('');
}
}
function renderPayoutMethodsList(el) {
if (!el) return;
if (!payoutMethods.length) {
el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:16px">No payout methods saved yet.</div>';
return;
}
el.innerHTML = payoutMethods.map(m => {
const on = parseInt(m.admin_enabled);
const bg = on && parseInt(m.is_default) ? 'rgba(0,229,255,.07)' : 'var(--bg3)';
const border = on && parseInt(m.is_default) ? 'rgba(0,229,255,.25)' : 'var(--border)';
return `
<div style="display:flex;align-items:center;gap:10px;background:${bg};border:1px solid ${border};border-radius:10px;padding:12px 14px;margin-bottom:8px;opacity:${on?'1':'0.45'}">
<span style="font-size:22px">${PAYOUT_ICONS[m.method_type]||'💰'}</span>
<div style="flex:1;min-width:0">
<div style="font-weight:700;font-size:14px">${escHtml(m.label)}
${!on
? '<span style="font-size:13px;font-weight:700;color:var(--text2);border:1px solid var(--border);border-radius:4px;padding:2px 6px;margin-left:6px">DISABLED BY ADMIN</span>'
: parseInt(m.is_default) ? '<span style="font-size:14px;font-weight:700;color:var(--cyan);border:1px solid rgba(0,229,255,.2);border-radius:4px;padding:2px 7px;margin-left:6px">DEFAULT</span>' : ''}
</div>
<div style="font-size:14px;color:var(--text2)">${PAYOUT_LABELS[m.method_type]||m.method_type} · ${escHtml(m.account_handle)}</div>
</div>
<div style="display:flex;gap:6px;flex-shrink:0">
${on && !parseInt(m.is_default)?`<button onclick="setDefaultPayout(${m.id})" style="background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.2);color:var(--cyan);border-radius:6px;padding:5px 10px;font-size:15px;font-weight:700;cursor:pointer;border:none">Set Default</button>`:''}
<button onclick="deletePayoutMethod(${m.id})" style="background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:6px;padding:5px 10px;font-size:15px;font-weight:700;cursor:pointer">✕</button>
</div>
</div>`;
}).join('');
}
async function addPayoutMethod() {
const al = document.getElementById('payout-add-alert');
const type = document.getElementById('payout-type-new').value;
const label = document.getElementById('payout-label-new').value.trim();
const handle = document.getElementById('payout-handle-new').value.trim();
if (!label || !handle) { showAlert(al,'Please fill in all fields.','error'); return; }
const d = await api('/api/payout_methods.php?action=add', { method_type:type, label, account_handle:handle, is_default: payoutMethods.length===0 ? 1 : 0 });
if (d.success) {
document.getElementById('payout-label-new').value = '';
document.getElementById('payout-handle-new').value = '';
showAlert(al,'✓ Payout method saved!','success');
setTimeout(() => { al.className='alert'; al.textContent=''; }, 3000);
loadPayoutMethods();
} else showAlert(al, d.error||'Error saving','error');
}
async function setDefaultPayout(id) {
const d = await api('/api/payout_methods.php?action=set_default', { id });
if (d.success) { loadPayoutMethods(); loadCashoutPayoutMethods(); }
}
async function deletePayoutMethod(id) {
if (!confirm('Remove this payout method?')) return;
const d = await api('/api/payout_methods.php?action=delete', { id });
if (d.success) { loadPayoutMethods(); loadCashoutPayoutMethods(); }
}
function getSelectedPayoutId() {
const sel = document.querySelector('input[name="cashout-payout"]:checked');
return sel ? parseInt(sel.value) : null;
}
// ─── TOKEN COIN SVG ────────────────────────────────────────
function tokenCoin(size) {
size = size || 22;
const s = size, r = s/2, ri = r*0.72, sh = r*0.14;
const id = 'tcg' + Math.random().toString(36).slice(2,6);
return `<svg width="${s}" height="${s+Math.round(sh*1.5)}" viewBox="0 0 ${s} ${s+Math.round(sh*1.5)}" style="display:inline-block;vertical-align:middle;flex-shrink:0" aria-label="token">
<defs>
<radialGradient id="cf${id}" cx="42%" cy="38%" r="58%">
<stop offset="0%" stop-color="#ffe87a"/>
<stop offset="50%" stop-color="#f0c040"/>
<stop offset="100%" stop-color="#c8900a"/>
</radialGradient>
<radialGradient id="ce${id}" cx="50%" cy="50%" r="50%">
<stop offset="65%" stop-color="#b07a18"/>
<stop offset="100%" stop-color="#7a5008"/>
</radialGradient>
</defs>
<ellipse cx="${r}" cy="${r+sh*1.5}" rx="${ri*0.7}" ry="${sh*0.6}" fill="rgba(0,0,0,0.18)"/>
<circle cx="${r}" cy="${r+sh}" r="${ri}" fill="url(#ce${id})"/>
<circle cx="${r}" cy="${r}" r="${ri}" fill="url(#cf${id})"/>
<circle cx="${r}" cy="${r}" r="${ri}" fill="none" stroke="#ffe87a" stroke-width="${s*0.04}" opacity="0.55"/>
<circle cx="${r}" cy="${r}" r="${ri*0.73}" fill="none" stroke="#c8900a" stroke-width="${s*0.025}" opacity="0.4"/>
<text x="${r}" y="${r+s*0.1}" font-family="'Exo 2',sans-serif" font-weight="900" font-size="${s*0.42}px" fill="#7a5008" text-anchor="middle" opacity="0.9">T</text>
<ellipse cx="${r*0.6}" cy="${r*0.55}" rx="${ri*0.28}" ry="${ri*0.15}" fill="rgba(255,255,220,0.38)" transform="rotate(-28 ${r*0.6} ${r*0.55})"/>
</svg>`;
}
// Replace all 🪙 emoji in text nodes with coin SVG
function coinify(el) {
if (!el) return;
el.innerHTML = el.innerHTML.replace(/🪙/g, tokenCoin(20));
}
// ─── ONBOARDING MODAL ─────────────────────────────────────
let obSelectedPlatforms = new Set();
function showOnboardingIfNeeded() {
if (!S.user) return;
// Show once: only for verified players who haven't completed onboarding
if (S.user.email_verified && !parseInt(S.user.onboarding_done||0) && !S.user.is_admin) {
setTimeout(() => {
document.getElementById('onboarding-overlay').style.display = 'block';
obLoadPlatforms();
}, 800);
}
}
async function obLoadPlatforms() {
const d = await api('/api/platforms.php?action=list').catch(()=>({success:false}));
if (!d.success) return;
// Also load existing requests to skip already-requested ones
const existing = await api('/api/platform_accounts.php?action=my_accounts').catch(()=>({accounts:[]}));
const existingSlugs = new Set((existing.accounts||[]).filter(a=>['pending','approved'].includes(a.status)).map(a=>a.platform_slug));
const el = document.getElementById('ob-platforms-list');
if (!el || !d.platforms.length) return;
el.innerHTML = d.platforms.filter(p => !existingSlugs.has(p.id)).map(p => `
<label style="display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--bg3);border:1px solid var(--border);border-radius:8px;margin-bottom:6px;cursor:pointer" id="ob-plat-${p.id}">
<input type="checkbox" value="${p.id}" data-name="${escHtml(p.name)}"
onchange="obTogglePlatform('${p.id}',this.checked,'ob-plat-${p.id}')"
style="accent-color:var(--gold);width:16px;height:16px;flex-shrink:0">
<div style="width:10px;height:10px;border-radius:50%;background:${p.color};flex-shrink:0"></div>
<span style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px">${escHtml(p.name)}</span>
</label>`).join('') || '<div style="color:var(--text2);text-align:center;padding:12px;font-size:15px">No platforms available to request.</div>';
}
function obTogglePlatform(id, checked, labelId) {
if (checked) obSelectedPlatforms.add(id);
else obSelectedPlatforms.delete(id);
const lbl = document.getElementById(labelId);
if (lbl) lbl.style.borderColor = checked ? 'rgba(240,192,64,.4)' : 'var(--border)';
}
function obShowStep2() {
document.getElementById('ob-step1').style.display = 'none';
document.getElementById('ob-step2').style.display = 'block';
obLoadPlatforms();
}
function obRequestNew() {
obShowStep2();
}
async function obSubmitRequests() {
const al = document.getElementById('ob-alert');
if (obSelectedPlatforms.size === 0) { showAlert(al,'Select at least one platform.','error'); return; }
let ok = 0;
for (const slug of obSelectedPlatforms) {
const name = document.querySelector(`input[value="${slug}"]`)?.dataset.name || slug;
const d = await api('/api/platform_accounts.php?action=request', { platform_slug: slug, platform_name: name });
if (d.success) ok++;
}
if (ok > 0) {
showAlert(al, `✓ ${ok} request${ok>1?'s':''} submitted! Our team will create your accounts shortly.`, 'success');
await api('/api/platform_accounts.php?action=skip_onboarding', {});
setTimeout(obDismiss, 2500);
} else showAlert(al, 'Something went wrong. Try again.', 'error');
}
async function obDismiss() {
document.getElementById('onboarding-overlay').style.display = 'none';
await api('/api/platform_accounts.php?action=skip_onboarding', {}).catch(()=>{});
if (S.user) S.user.onboarding_done = 1;
}
function obSkip() { obDismiss(); }
// ─── PLATFORM LOGINS (Profile tab) ────────────────────────
async function loadPlatformLogins() {
const el = document.getElementById('platform-logins-list');
if (!el) return;
el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:16px">Loading...</div>';
const d = await api('/api/platform_accounts.php?action=my_accounts');
if (!d.success) { el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:16px">Unable to load.</div>'; return; }
// Populate request dropdown - exclude already requested/approved
const existing = new Set(d.accounts.filter(a=>['pending','approved'].includes(a.status)).map(a=>a.platform_slug));
const sel = document.getElementById('req-platform-slug');
if (sel && CFG.platforms.length) {
sel.innerHTML = '<option value="">— Select Platform —</option>' +
CFG.platforms.filter(p => !existing.has(p.id)).map(p =>
`<option value="${p.id}" data-name="${escHtml(p.name)}">${escHtml(p.name)}</option>`
).join('');
}
if (!d.accounts.length) {
el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:20px">No platform account requests yet.<br><span style="font-size:15px">Use the form below to request one.</span></div>';
return;
}
const STATUS_INFO = {
pending: { icon:'⏳', label:'Awaiting Approval', cls:'ac-pending', msg:'Our team will review and create your account shortly.' },
approved: { icon:'✅', label:'Account Ready', cls:'ac-completed', msg:'' },
denied: { icon:'❌', label:'Request Denied', cls:'ac-failed', msg:'Contact support for more info.' },
deleted: { icon:'🗑', label:'Deleted', cls:'ac-failed', msg:'' },
};
el.innerHTML = d.accounts.map(a => {
const si = STATUS_INFO[a.status] || STATUS_INFO.pending;
const canDelete = a.status === 'pending';
const showCreds = a.status === 'approved' && a.provided_username;
return `<div class="activity-card" style="margin-bottom:10px;flex-direction:column;gap:0">
<div style="display:flex;gap:12px;align-items:flex-start;width:100%">
<div class="activity-icon" style="background:rgba(240,192,64,.1)">${si.icon}</div>
<div style="flex:1;min-width:0">
<div class="activity-title">${escHtml(a.display_name||a.platform_slug)}</div>
<span class="activity-status ${si.cls}">${si.label}</span>
${a.admin_note ? `<div style="font-size:15px;color:var(--gold);margin-top:4px">📝 ${escHtml(a.admin_note)}</div>` : ''}
${si.msg ? `<div style="font-size:15px;color:var(--text2);margin-top:4px">${si.msg}</div>` : ''}
</div>
${canDelete ? `<button onclick="deletePlatformRequest(${a.id})" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:18px;padding:0;flex-shrink:0">✕</button>` : ''}
</div>
${showCreds ? `
<div style="margin-top:12px;background:rgba(0,229,255,.06);border:1px solid rgba(0,229,255,.2);border-radius:8px;padding:12px 14px;width:100%">
<div style="font-size:15px;font-weight:700;color:var(--cyan);margin-bottom:8px;letter-spacing:.5px">🔑 YOUR LOGIN CREDENTIALS</div>
<div style="display:grid;grid-template-columns:auto 1fr;gap:6px 12px;font-size:15px">
<span style="color:var(--text2)">Username:</span>
<span style="font-weight:700;color:var(--text);font-family:monospace">${escHtml(a.provided_username)}</span>
<span style="color:var(--text2)">Password:</span>
<span style="font-weight:700;color:var(--gold);font-family:monospace;cursor:pointer" onclick="this.style.filter='none'" style="filter:blur(5px)" title="Tap to reveal">${escHtml(a.provided_password)}</span>
</div>
<div style="font-size:14px;color:var(--text2);margin-top:8px">⚠️ Keep your credentials secure. Tap password to reveal.</div>
${a.player_url ? `<a href="${escHtml(a.player_url)}" target="_blank" class="btn btn-gold" style="display:block;text-align:center;margin-top:10px;padding:8px;font-size:15px;text-decoration:none">▶ Play ${escHtml(a.display_name)}</a>` : ''}
</div>` : ''}
</div>`;
}).join('');
}
async function requestPlatformAccount() {
const sel = document.getElementById('req-platform-slug');
const al = document.getElementById('req-platform-alert');
const slug = sel?.value;
const name = sel?.options[sel.selectedIndex]?.dataset.name || slug;
if (!slug) { showAlert(al,'Select a platform.','error'); return; }
const d = await api('/api/platform_accounts.php?action=request', { platform_slug: slug, platform_name: name });
if (d.success) {
showAlert(al,'✓ Request submitted! We\'ll create your account shortly.','success');
setTimeout(() => { al.className='alert'; al.textContent=''; }, 3500);
loadPlatformLogins();
} else showAlert(al, d.error||'Request failed','error');
}
async function deletePlatformRequest(id) {
if (!confirm('Cancel this account request?')) return;
const d = await api('/api/platform_accounts.php?action=delete_request', { id });
if (d.success) loadPlatformLogins();
}
let cashoutMethodTypes = [];
// ─── REFERRAL SYSTEM ──────────────────────────────────────
let _refData = null;
// Capture ?ref= code from URL on page load
const _urlRef = new URLSearchParams(window.location.search).get('ref') || '';
async function loadReferralDashboard() {
const d = await api('/api/referrals.php?action=status');
if (!d.success) {
const inp = document.getElementById('referral-link-input');
if (inp) inp.placeholder = d.error || 'Please log in to see your referral link';
return;
}
_refData = d;
// Referral link — build URL defensively
const inp = document.getElementById('referral-link-input');
if (inp) {
const code = d.referral_code || '';
const url = code
? (d.referral_url || ('https://tomtomgames.com/?ref=' + code))
: '';
inp.value = url;
inp.placeholder = code ? '' : 'Generating your referral link...';
// If no code yet, retry in 2 seconds
if (!code) setTimeout(loadReferralDashboard, 2000);
}
// Stats
setText('ref-count', d.verified_count || 0);
setText('ref-earned', d.total_earned || 0);
setText('ref-tier', d.current_tier?.name || '—');
// Next tier progress
if (d.next_tier) {
const nextEl = document.getElementById('ref-next-tier');
const labelEl = document.getElementById('ref-next-label');
const barEl = document.getElementById('ref-progress-bar');
const curr = d.current_tier?.min_referrals || 0;
const next = d.next_tier.min_referrals;
const count = d.verified_count || 0;
const pct = Math.min(100, Math.round(((count - curr) / (next - curr)) * 100));
if (nextEl) nextEl.style.display = 'block';
if (labelEl) labelEl.textContent = `${count}/${next} referrals to ${d.next_tier.name} (+${d.next_tier.tokens_per_ref} tokens/ref)`;
if (barEl) barEl.style.width = pct + '%';
}
// Tiers list
const tiersEl = document.getElementById('ref-tiers-list');
if (tiersEl) {
const tiers = await api('/api/referrals.php?action=tiers');
if (tiers.success) {
tiersEl.innerHTML = (tiers.tiers || []).map(t => `
<div style="display:flex;align-items:center;gap:10px;padding:10px 0;border-bottom:1px solid var(--border)">
<div style="flex:1">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:${d.current_tier?.id==t.id?'var(--gold)':'var(--text)'}">${escHtml(t.name)} ${d.current_tier?.id==t.id?'★':''}</div>
<div style="font-size:15px;color:var(--text2)">${escHtml(t.description||'')}</div>
</div>
<div style="text-align:right;font-size:14px">
<div style="font-weight:700;color:var(--green)">${t.tokens_per_ref} tokens/ref</div>
${t.bonus_tokens>0?'<div style="color:var(--gold);font-size:15px">+'+t.bonus_tokens+' bonus</div>':''}
<div style="color:var(--text2);font-size:14px">${t.min_referrals} referrals</div>
</div>
</div>`).join('') || '<div style="color:var(--text2);font-size:15px">No tiers configured.</div>';
}
}
// My referrals list
const refListEl = document.getElementById('ref-list');
if (refListEl) {
const refs = d.referrals || [];
if (!refs.length) {
refListEl.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:12px">No referrals yet. Share your link!</div>';
} else {
refListEl.innerHTML = refs.map(r => `
<div style="display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--border)">
<div style="flex:1">
<div style="font-size:15px;font-weight:700">${escHtml(r.alias||r.username)}</div>
<div style="font-size:15px;color:var(--text2)">${(r.created_at||'').substring(0,10)}</div>
</div>
<div style="text-align:right">
<div class="ref-status-${r.status}" style="font-size:15px;font-weight:700;text-transform:uppercase">${r.status}</div>
${r.tokens_awarded>0?'<div style="color:var(--green);font-size:15px">+'+r.tokens_awarded+' tokens</div>':''}
</div>
</div>`).join('');
}
}
// Social shares
const sharesList = document.getElementById('ref-shares-list');
if (sharesList) {
const shares = d.social_shares || [];
if (!shares.length) {
sharesList.innerHTML = '';
} else {
sharesList.innerHTML = '<div style="font-size:13px;font-weight:700;color:var(--text2);margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px">Your Submitted Shares</div>' +
shares.map(s => {
const autoTag = parseInt(s.auto_verified) ? ' <span style="font-size:12px;background:rgba(0,255,128,.1);border:1px solid rgba(0,255,128,.2);color:var(--green);border-radius:4px;padding:1px 5px">⚡ auto</span>' : '';
const urlFrag = s.share_url
? `<div style="margin-top:3px"><a href="${escHtml(s.share_url)}" target="_blank" rel="noopener" style="color:var(--cyan);font-size:12px;word-break:break-all">${escHtml(s.share_url.length>60?s.share_url.substring(0,60)+'…':s.share_url)}</a></div>`
: '';
return `<div style="padding:8px 0;border-bottom:1px solid var(--border)">
<div style="font-size:14px"><strong>${escHtml(s.platform)}</strong> — <span class="ref-status-${s.status}">${s.status}</span>${autoTag}${s.status==='approved'?' <span style="color:var(--green)">+'+s.bonus_tokens+' tokens</span>':''}</div>
${urlFrag}
</div>`;
}).join('');
}
}
}
function copyReferralLink() {
const inp = document.getElementById('referral-link-input');
if (!inp) return;
inp.select();
navigator.clipboard?.writeText(inp.value).catch(() => {});
document.execCommand('copy');
const msg = document.getElementById('ref-copy-msg');
if (msg) { msg.style.display='block'; setTimeout(()=>msg.style.display='none', 2000); }
}
async function submitShareUrl() {
const al = document.getElementById('ref-share-alert');
const inp = document.getElementById('share-url-input');
const url = inp?.value.trim();
if (!url) { showAlert(al, 'Please paste the URL of your post.', 'error'); return; }
showAlert(al, '🔍 Checking your post…', 'info');
const d = await api('/api/referrals.php?action=submit_share', { url });
if (d.success) {
showAlert(al, d.message, d.auto_verified ? 'success' : 'info');
if (inp) inp.value = '';
loadReferralDashboard();
} else {
showAlert(al, d.error || 'Error submitting share.', 'error');
}
}
// Pass referral code during registration
function getUrlRefCode() { return _urlRef; }
// ─── PLATFORM LOGINS (unified) ───────────────────────────
async function loadPlatformLogins() {
const el = document.getElementById('platform-logins-list');
if (!el) return;
el.innerHTML = '<div style="text-align:center;padding:16px;color:var(--text2);font-size:15px"><span class="spinner" style="border-top-color:var(--gold)"></span></div>';
try {
const d = await api('/api/platform_accounts.php?action=list');
if (!d.success || !d.accounts.length) {
el.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text2);font-size:15px">No platform account requests yet. Request one below!</div>';
return;
}
el.innerHTML = d.accounts.map(a => buildLoginCard(a)).join('');
} catch(e) {
el.innerHTML = '<div style="color:var(--red);text-align:center;padding:16px;font-size:15px">Unable to load. Try again.</div>';
}
}
function buildLoginCard(a) {
const statusLabel = { pending:'⏳ Request Pending', approved:'✅ Account Ready', denied:'❌ Request Denied', deleted:'🗑 Deleted' };
const statusCls = { pending:'ac-pending', approved:'ac-completed', denied:'ac-failed', deleted:'ac-failed' };
const note = a.admin_note ? `<div style="font-size:15px;color:var(--gold);margin-top:6px;padding:6px 8px;background:rgba(240,192,64,.07);border-radius:6px">📝 ${escHtml(a.admin_note)}</div>` : '';
let creds = '';
if (a.status === 'approved' && a.platform_username) {
const passHtml = a.platform_password
? `<div class="pa-cred-row">
<span class="pa-cred-label">Password</span>
<span class="pa-cred-val pa-cred-pass" onclick="this.classList.toggle('revealed')" title="Tap to reveal">${escHtml(a.platform_password)}</span>
</div>
<div style="font-size:14px;color:var(--text2);margin-top:3px">Tap password to reveal · Keep this safe!</div>`
: '';
creds = `<div class="pa-cred-box">
<div class="pa-cred-row"><span class="pa-cred-label">Username</span><span class="pa-cred-val">${escHtml(a.platform_username)}</span></div>
${passHtml}
</div>`;
}
const pendingMsg = a.status === 'pending'
? '<div style="font-size:15px;color:var(--text2);margin-top:8px;padding:8px;background:rgba(255,255,255,.03);border-radius:6px">Our team is setting up your account. Your login credentials will appear here once ready.</div>'
: '';
return `<div class="pa-card ${a.status}" style="margin-bottom:10px">
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px">
<div>
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px">${escHtml(a.platform_name || a.platform_slug)}</div>
<div style="font-size:15px;color:var(--text2);margin-top:2px">${(a.requested_at||'').substring(0,10)}</div>
<span class="activity-status ${statusCls[a.status]||'ac-pending'}">${statusLabel[a.status]||a.status}</span>
</div>
${a.status === 'pending' ? '<div style="font-size:15px;color:var(--text2);text-align:right">Pending<br>review</div>' : ''}
</div>
${note}${creds}${pendingMsg}
</div>`;
}
async function requestPlatformLogin() {
const al = document.getElementById('req-platform-alert');
const slug = document.getElementById('req-platform-slug')?.value;
if (!slug) { showAlert(al, 'Please select a platform.', 'error'); return; }
const platName = CFG.platforms.find(p => p.id === slug)?.name || slug;
const d = await api('/api/platform_accounts.php?action=request', { platform_slug: slug });
if (d.success) {
showAlert(al, `✓ Request submitted for ${platName}! Check back here once approved.`, 'success');
setTimeout(() => { al.className='alert'; al.textContent=''; }, 4000);
document.getElementById('req-platform-slug').value = '';
loadPlatformLogins();
} else {
showAlert(al, d.error || 'Error submitting request.', 'error');
}
}
function populatePlatformSelect(selId) {
const sel = document.getElementById(selId);
if (!sel || !CFG.platforms.length) return;
sel.querySelectorAll('option:not([value=""])').forEach(o => o.remove());
CFG.platforms.forEach(p => {
const o = document.createElement('option');
o.value = p.id; o.textContent = p.name;
sel.appendChild(o);
});
}
// ─── ONBOARDING FLOW ──────────────────────────────────────
function showOnboarding() {
const modal = document.getElementById('onboarding-modal');
if (!modal) return;
populatePlatformSelect('onboarding-platform');
modal.style.display = 'flex';
}
function hideOnboarding() { const m=document.getElementById('onboarding-modal'); if(m)m.style.display='none'; }
function showOnboardingStep(n) {
[1,2,3].forEach(i => { const el=document.getElementById('onboarding-step-'+i); if(el) el.style.display=i===n?'block':'none'; });
}
function onboardingHasAccount() {
api('/api/platform_accounts.php?action=dismiss_onboarding',{}).catch(()=>{});
hideOnboarding();
}
function onboardingNoAccount() { showOnboardingStep(2); }
function onboardingRequestAnother() {
const al=document.getElementById('onboarding-alert'); if(al){al.className='alert';al.textContent='';}
document.getElementById('onboarding-platform').value='';
showOnboardingStep(2);
}
async function onboardingRequestAccount() {
const al=document.getElementById('onboarding-alert');
const slug=document.getElementById('onboarding-platform')?.value;
if (!slug) { showAlert(al,'Please select a platform.','error'); return; }
const platName=CFG.platforms.find(p=>p.id===slug)?.name||slug;
const d=await api('/api/platform_accounts.php?action=request',{platform_slug:slug});
if (d.success) {
const ct=document.getElementById('onboarding-confirm-text');
if (ct) ct.textContent=`Request for ${platName} submitted! Find your login details in Profile → Accounts once approved.`;
showOnboardingStep(3);
} else showAlert(al, d.error||'Error','error');
}
async function onboardingDone() {
api('/api/platform_accounts.php?action=dismiss_onboarding',{}).catch(()=>{});
hideOnboarding();
}
async function checkOnboarding() {
if (!S.user || S.user.is_admin) return;
try {
const d=await api('/api/platform_accounts.php?action=check_onboarding');
if (d.success && d.needs_onboarding) showOnboarding();
} catch(e){}
}
async function loadCashoutMethodTypes() {
try {
const d = await api('/api/cashout_method_types.php?action=list');
if (!d.success) return;
cashoutMethodTypes = d.types || [];
// Populate the payout type selector
const sel = document.getElementById('payout-type-new');
if (sel) {
sel.innerHTML = cashoutMethodTypes.map(t =>
`<option value="${escHtml(t.slug)}">${escHtml(t.icon)} ${escHtml(t.label)}</option>`
).join('');
}
// Update PAYOUT_ICONS and PAYOUT_LABELS from live data
cashoutMethodTypes.forEach(t => {
PAYOUT_ICONS[t.slug] = t.icon;
PAYOUT_LABELS[t.slug] = t.label;
});
} catch(e) { /* use defaults */ }
}
// Also load on app init so icons/labels are available everywhere
function populateAccount() {
const u = S.user;
if (!u) return;
document.getElementById('acct-username').textContent = u.username || '—';
document.getElementById('acct-alias').textContent = u.alias || '—';
document.getElementById('acct-email').textContent = u.email || 'Not set';
document.getElementById('acct-joined').textContent = (u.created_at||'').substring(0,10);
}
// ─── DYNAMIC PAYMENT METHODS ──────────────────────────────
function buildPaymentMethods() {
const methods = CFG.payMethods || [];
const container = document.getElementById('manual-payment-btns');
if (!container) return;
// Check if card is enabled from payMethods (or default to enabled if no card entry)
const cardSetting = methods.find(m => m.method_key === 'card');
const cardEnabled = cardSetting ? parseInt(cardSetting.is_enabled) : 1;
const manualMethods = methods.filter(m => m.method_key !== 'card' && parseInt(m.is_enabled));
let html = '';
// Card button — only show if enabled
if (cardEnabled) {
html += `<div class="pmt" onclick="selectPMT(this,'card')" title="Credit/Debit Card">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="22" height="22"><rect x="1" y="4" width="22" height="16" rx="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg>
<span>Card</span>
</div>`;
}
const iconMap = { venmo:'💙', chime:'🟢', cashapp:'💚', zelle:'💜' };
manualMethods.forEach(m => {
html += `<div class="pmt" onclick="selectPMT(this,'${m.method_key}')" title="${escHtml(m.label)}">
<span style="font-size:20px">${iconMap[m.method_key]||'💰'}</span>
<span>${escHtml(m.label)}</span>
</div>`;
});
container.innerHTML = html;
// If card is disabled and current method is card, switch to first available
if (!cardEnabled && S.method === 'card') {
const firstManual = manualMethods[0];
if (firstManual) {
const firstBtn = container.querySelector('.pmt');
if (firstBtn) selectPMT(firstBtn, firstManual.method_key);
}
// Also hide the card section entirely
const cardSection = document.getElementById('section-card');
if (cardSection) cardSection.style.display = 'none';
const manSection = document.getElementById('section-manual');
if (manSection) manSection.style.display = 'block';
}
// If no methods at all, show message
if (!cardEnabled && !manualMethods.length) {
container.innerHTML = '<div style="font-size:15px;color:var(--red);padding:8px">No payment methods are currently available. Please contact support.</div>';
}
// Update CFG.pay handles from DB
methods.forEach(m => {
if (CFG.pay && m.handle) CFG.pay[m.method_key] = m.handle;
if (!CFG.payLabels) CFG.payLabels = {};
CFG.payLabels[m.method_key] = m.label;
if (m.instructions) {
if (!CFG.payInstructions) CFG.payInstructions = {};
CFG.payInstructions[m.method_key] = m.instructions;
}
});
}
// Save alias from buy page back to profile
async function saveAliasFromBuyPage() {
const platformId = document.getElementById('buy-platform')?.value;
const alias = document.getElementById('buy-alias')?.value.trim();
if (!platformId || !alias || !S.user) return;
savedAliases[platformId] = alias;
await api('/api/game_aliases.php?action=save', { platform_slug: platformId, alias });
}
let savedAliases = {}; // { platform_slug: alias }
async function loadGameAliases() {
const el = document.getElementById('game-alias-list') || document.getElementById('game-aliases-list');
try {
const [aliasRes, platRes] = await Promise.all([
api('/api/game_aliases.php?action=get'),
api('/api/platforms.php?action=list')
]);
if (aliasRes.success) savedAliases = aliasRes.aliases || {};
const platforms = platRes.success ? platRes.platforms : CFG.platforms;
renderGameAliasList(platforms);
} catch(e) {
if (el) el.innerHTML = '<div style="color:var(--text2);text-align:center;padding:16px;font-size:15px">Could not load games.</div>';
}
}
function renderGameAliasList(platforms) {
const el = document.getElementById('game-aliases-list');
if (!el) return;
if (!platforms || !platforms.length) {
el.innerHTML = '<div style="color:var(--text2);text-align:center;padding:16px;font-size:15px">No games configured yet.</div>';
return;
}
el.innerHTML = platforms.map(p => `
<div class="game-alias-card">
<div class="game-alias-dot" style="background:${p.color};box-shadow:0 0 8px ${p.color}"></div>
<div style="flex:1;min-width:0">
<div class="game-alias-name">${escHtml(p.name)}</div>
<input class="game-alias-input" id="alias-${p.id}"
type="text" maxlength="100"
placeholder="Your in-game username..."
value="${escHtml(savedAliases[p.id] || '')}"
onchange="saveOneAlias('${p.id}',this.value)">
</div>
</div>`).join('');
}
async function saveOneAlias(slug, alias) {
savedAliases[slug] = alias.trim();
await api('/api/game_aliases.php?action=save', { platform_slug: slug, alias: alias.trim() });
}
async function saveAllAliases() {
const al = document.getElementById('game-aliases-alert');
const aliases = {};
(CFG.platforms || []).forEach(p => {
const el = document.getElementById('alias-' + p.id);
if (el) aliases[p.id] = el.value.trim();
});
// Also grab any inputs by the alias-* pattern
document.querySelectorAll('[id^="alias-"]').forEach(el => {
const slug = el.id.replace('alias-', '');
aliases[slug] = el.value.trim();
});
savedAliases = {...savedAliases, ...aliases};
const d = await api('/api/game_aliases.php?action=save_all', { aliases });
if (d.success) {
showAlert(al, '✓ All aliases saved!', 'success');
setTimeout(() => hideAlert(al), 3000);
} else {
showAlert(al, 'Save failed. Try again.', 'error');
}
}
// Auto-prefill alias on buy page when platform changes
function prefillBuyAlias() {
const platformId = document.getElementById('buy-platform')?.value;
const aliasInput = document.getElementById('buy-alias');
if (platformId && aliasInput && savedAliases[platformId]) {
aliasInput.value = savedAliases[platformId];
} else if (aliasInput && platformId && !savedAliases[platformId]) {
aliasInput.value = '';
}
}
function escHtml(s) { return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
async function loadBillingProfile() {
const d = await api('/api/billing.php?action=get');
if (d.success && d.billing) {
const b = d.billing;
// Show saved card chip if card on file
if (b.card_last4) {
document.getElementById('saved-card-brand').textContent = b.card_brand || 'CARD';
document.getElementById('saved-card-number').textContent = '···· ···· ···· ' + b.card_last4;
document.getElementById('saved-card-name').textContent = [b.first_name, b.last_name].filter(Boolean).join(' ') || '—';
const exp = (b.card_exp_month && b.card_exp_year) ? b.card_exp_month + '/' + b.card_exp_year.slice(-2) : '—';
document.getElementById('saved-card-exp').textContent = exp;
document.getElementById('saved-card-display').style.display = 'block';
} else {
document.getElementById('saved-card-display').style.display = 'none';
}
// Fill form fields
setVal('p-first', b.first_name);
setVal('p-last', b.last_name);
setVal('p-email', b.email);
setVal('p-address', b.address);
setVal('p-city', b.city);
setVal('p-state', b.state);
setVal('p-zip', b.zip);
// Also pre-fill buy form
prefillBuyForm(b);
} else {
document.getElementById('saved-card-display').style.display = 'none';
}
}
function prefillBuyForm(b) {
if (!b) return;
setVal('card-first-name', b.first_name);
setVal('card-last-name', b.last_name);
setVal('card-address', b.address);
setVal('card-city', b.city);
setVal('card-state', b.state);
setVal('card-zip', b.zip);
setVal('card-email', b.email);
}
function setVal(id, val) {
const el = document.getElementById(id);
if (el && val) el.value = val;
}
async function saveBillingProfile() {
const al = document.getElementById('billing-alert');
const data = {
first_name: (document.getElementById('p-first')?.value || '').trim(),
last_name: (document.getElementById('p-last')?.value || '').trim(),
email: (document.getElementById('p-email')?.value || '').trim(),
address: (document.getElementById('p-address')?.value || '').trim(),
city: (document.getElementById('p-city')?.value || '').trim(),
state: (document.getElementById('p-state')?.value || '').trim().toUpperCase(),
zip: (document.getElementById('p-zip')?.value || '').trim(),
};
const d = await api('/api/billing.php?action=save', data);
if (d.success) {
showAlert(al, 'Billing info saved!', 'success');
prefillBuyForm(data);
} else showAlert(al, d.error || 'Save failed.', 'error');
}
async function clearSavedCard() {
if (!confirm('Remove saved card info?')) return;
const d = await api('/api/billing.php?action=clear_card', {});
if (d.success) {
document.getElementById('saved-card-display').style.display = 'none';
// Clear card fields in buy form
['card-first-name','card-last-name'].forEach(id => { const el=document.getElementById(id); if(el) el.value=''; });
}
}
async function clearAllBilling() {
if (!confirm('Clear all saved billing information? This cannot be undone.')) return;
const d = await api('/api/billing.php?action=clear_all', {});
if (d.success) {
loadBillingProfile();
['p-first','p-last','p-email','p-address','p-city','p-state','p-zip'].forEach(id => {
const el=document.getElementById(id); if(el) el.value='';
});
const al = document.getElementById('billing-alert');
showAlert(al, 'All billing info cleared.', 'success');
}
}
// Auto-load billing when going to buy page
async function initBuyPageBilling() {
const d = await api('/api/billing.php?action=get');
if (d.success && d.billing) prefillBuyForm(d.billing);
}
// ─── CASHOUT ───────────────────────────────────────────────
async function submitCashout() {
const al = document.getElementById('cashout-alert');
const pid = document.getElementById('cashout-platform').value;
const alias = document.getElementById('cashout-alias').value.trim();
const toks = parseFloat(document.getElementById('cashout-tokens').value);
const payoutId = getSelectedPayoutId();
if (!payoutId) { showAlert(al,'Please select a payout method above.','error'); return; }
if (!pid) { showAlert(al,'Please select a platform.','error'); return; }
if (!alias) { showAlert(al,'Please enter your in-game alias.','error'); return; }
if (!toks || toks<1) { showAlert(al,'Enter a valid token amount.','error'); return; }
// Get payout method details
const pm = payoutMethods.find(m => m.id == payoutId);
try {
const d = await api('/api/cashout.php', {
platform_id: pid,
alias,
tokens: toks,
payout_method_id: payoutId,
payout_method_type: pm ? pm.method_type : '',
payout_handle: pm ? pm.account_handle : '',
});
if (d.success) {
showAlert(al, '✓ Request created! Review and submit to admin when ready.', 'success');
S.user.tokens = d.new_balance; updateUI();
document.getElementById('cashout-tokens').value = '';
document.getElementById('cashout-alias').value = '';
// Reload list and scroll to it
await loadCashoutHistory();
setTimeout(() => {
const hist = document.getElementById('cashout-history');
if (hist) hist.scrollIntoView({behavior:'smooth', block:'start'});
// Briefly highlight the first (newest) card
const first = hist?.querySelector('.co-card');
if (first) { first.style.boxShadow='0 0 0 2px var(--gold)'; setTimeout(()=>first.style.boxShadow='',2000); }
}, 200);
} else showAlert(al, d.error||'Request failed.','error');
} catch { showAlert(al,'Network error.','error'); }
}
async function loadCashoutHistory() {
const el = document.getElementById('cashout-history');
if (!el) return;
try {
const d = await api('/api/cashout.php?action=list');
if (!d.success || !d.requests.length) {
el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:20px">No cashout requests yet. Submit one above.</div>';
return;
}
el.innerHTML = d.requests.map(r => buildCashoutCard(r)).join('');
} catch(e) {
el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:16px">Unable to load requests.</div>';
}
}
function buildCashoutCard(r) {
const editable = r.status === 'pending';
const locked = r.status === 'locked';
const statusLabel = { pending:'⏳ Pending', locked:'🔒 Sent to Admin', sent:'✅ Sent', approved:'✅ Sent', rejected:'❌ Denied', deleted:'🗑 Deleted' };
const payout = r.payout_handle ? `<div style="font-size:15px;color:var(--cyan);margin-top:2px">${escHtml(r.payout_handle)}</div>` : '';
const adminNote= r.admin_note ? `<div style="font-size:15px;color:var(--gold);margin-top:4px;padding:6px 8px;background:rgba(240,192,64,.07);border-radius:6px">📝 ${escHtml(r.admin_note)}</div>` : '';
const actionBtns = editable ? `
<div class="co-edit-row">
<input class="fi" id="co-edit-tokens-${r.id}" type="number" min="1" value="${r.tokens}"
style="width:90px;padding:7px 10px;font-size:15px" placeholder="Tokens">
<input class="fi" id="co-edit-alias-${r.id}" type="text" value="${escHtml(r.alias||'')}"
style="flex:1;min-width:100px;padding:7px 10px;font-size:15px" placeholder="Game alias">
<button onclick="updateCashoutRequest(${r.id})"
style="background:rgba(0,229,255,.1);border:1px solid rgba(0,229,255,.25);color:var(--cyan);border-radius:8px;padding:7px 12px;font-size:14px;font-weight:700;cursor:pointer;white-space:nowrap">
✏️ Update
</button>
<button onclick="deleteCashoutRequest(${r.id})"
style="background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:8px;padding:7px 12px;font-size:14px;font-weight:700;cursor:pointer">
🗑
</button>
<button onclick="lockCashoutRequest(${r.id})"
style="background:rgba(240,192,64,.1);border:1px solid rgba(240,192,64,.25);color:var(--gold);border-radius:8px;padding:7px 12px;font-size:14px;font-weight:700;cursor:pointer;white-space:nowrap">
🔒 Submit to Admin
</button>
</div>` : '';
const lockNote = locked ? `<div style="font-size:15px;color:var(--cyan);margin-top:8px;padding:7px 10px;background:rgba(0,229,255,.06);border-radius:6px;border:1px solid rgba(0,229,255,.15)">🔒 This request has been submitted to our team. They will process it shortly.</div>` : '';
return `<div class="co-card ${r.status}" id="co-card-${r.id}">
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:10px">
<div style="flex:1;min-width:0">
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:4px;margin-bottom:4px">
<span style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:16px;color:var(--gold)">${r.tokens}</span>
<span style="margin-left:2px">${tokenCoin(18)}</span>
<span class="co-status-pill co-status-${r.status}">${statusLabel[r.status]||r.status}</span>
</div>
<div style="font-size:14px;color:var(--text2)">${escHtml(r.platform_name||r.platform_id)} · <span style="color:var(--cyan)">${escHtml(r.alias||'')}</span></div>
${payout}
${adminNote}
${lockNote}
</div>
<div style="text-align:right;flex-shrink:0">
<div style="font-size:15px;color:var(--text2)">${(r.created_at||'').substring(0,10)}</div>
<div style="font-size:14px;color:var(--text2);margin-top:2px">#${r.id}</div>
</div>
</div>
${actionBtns}
</div>`;
}
async function updateCashoutRequest(id) {
const tokens = parseFloat(document.getElementById('co-edit-tokens-'+id)?.value||0);
const alias = document.getElementById('co-edit-alias-'+id)?.value.trim();
if (!tokens||tokens<1) { showAlert(document.getElementById('cashout-alert'),'Enter a valid token amount.','error'); return; }
const d = await api('/api/cashout.php?action=update&id='+id+'&tokens='+tokens+'&alias='+encodeURIComponent(alias));
if (d.success) {
showAlert(document.getElementById('cashout-alert'),'✓ Request updated!','success');
setTimeout(()=>{ const al=document.getElementById('cashout-alert'); if(al){al.className='alert';al.textContent='';} }, 3000);
loadCashoutHistory();
} else showAlert(document.getElementById('cashout-alert'), d.error||'Update failed','error');
}
async function deleteCashoutRequest(id) {
if (!confirm('Cancel this cashout request?\n\nYour tokens will be returned to your balance immediately.')) return;
const d = await api('/api/cashout.php?action=delete&id='+id);
if (d.success) {
if (d.new_balance !== undefined && S.user) { S.user.tokens = d.new_balance; updateUI(); }
const card = document.getElementById('co-card-'+id);
if (card) { card.style.transition='opacity .3s'; card.style.opacity='0'; setTimeout(()=>card.remove(), 300); }
showAlert(document.getElementById('cashout-alert'),'✓ Request cancelled. Tokens returned.','success');
setTimeout(()=>{ const al=document.getElementById('cashout-alert'); if(al){al.className='alert';al.textContent='';} }, 3000);
} else showAlert(document.getElementById('cashout-alert'), d.error||'Delete failed','error');
}
async function lockCashoutRequest(id) {
if (!confirm('Submit this cashout request to our team for processing?\n\nYou won\'t be able to edit it after this.')) return;
const d = await api('/api/cashout.php?action=lock&id='+id);
if (d.success) {
loadCashoutHistory();
showAlert(document.getElementById('cashout-alert'),'✓ Submitted! Our team will process it shortly.','success');
setTimeout(()=>{ const al=document.getElementById('cashout-alert'); if(al){al.className='alert';al.textContent='';} }, 4000);
} else showAlert(document.getElementById('cashout-alert'), d.error||'Error','error');
}
// ─── PLAYER ACTIVITY DASHBOARD ────────────────────────────
let activityData = null;
let activityFilter = 'all';
let activityPollTimer = null;
async function loadProfileHistory() {
// Renamed alias kept for compatibility
await loadActivityDashboard();
}
async function loadActivityDashboard() {
const feed = document.getElementById('activity-feed');
const sumEl = document.getElementById('activity-summary');
if (!feed) return;
try {
const d = await api('/api/my_activity.php?action=all');
if (!d.success) throw new Error('Failed');
activityData = d;
renderActivitySummary(d, sumEl);
renderActivityFeed(activityFilter);
// Poll for real-time updates every 15s while profile is open
clearTimeout(activityPollTimer);
activityPollTimer = setTimeout(async () => {
const fresh = await api('/api/my_activity.php?action=all').catch(()=>null);
if (fresh?.success) { activityData = fresh; renderActivitySummary(fresh, sumEl); renderActivityFeed(activityFilter); }
}, 15000);
} catch(e) {
feed.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:20px">Unable to load activity.</div>';
}
}
function filterActivity(filter, btn) {
activityFilter = filter;
document.querySelectorAll('.activity-ftab').forEach(b => b.classList.remove('active'));
if (btn) btn.classList.add('active');
renderActivityFeed(filter);
}
function renderActivitySummary(d, el) {
if (!el) return;
const purchases = d.purchases || [];
const cashouts = d.cashouts || [];
const pending_p = purchases.filter(p => p.status === 'pending').length;
const pending_c = cashouts.filter(c => c.status === 'pending' || c.status === 'locked').length;
const total_spent = purchases.filter(p => p.status !== 'failed').reduce((s,p) => s + (p.amount_cents/100), 0);
const total_cashed = cashouts.filter(c => c.status === 'sent' || c.status === 'approved').reduce((s,c) => s + parseFloat(c.tokens||0), 0);
const pills = [
total_spent > 0 ? `<div class="summary-pill">💳 $${total_spent.toFixed(2)} purchased</div>` : '',
total_cashed > 0 ? `<div class="summary-pill" style="color:var(--green)">💸 ${total_cashed} tokens cashed out</div>` : '',
pending_p > 0 ? `<div class="summary-pill" style="color:var(--gold);border-color:rgba(240,192,64,.3)">⏳ ${pending_p} purchase${pending_p>1?'s':''} pending</div>` : '',
pending_c > 0 ? `<div class="summary-pill" style="color:var(--cyan);border-color:rgba(0,229,255,.2)">⏳ ${pending_c} cashout${pending_c>1?'s':''} in review</div>` : '',
].filter(Boolean).join('');
el.innerHTML = pills;
}
function renderActivityFeed(filter) {
const feed = document.getElementById('activity-feed');
if (!feed || !activityData) return;
const items = buildActivityItems(activityData, filter);
if (!items.length) {
const labels = {all:'activity', purchases:'purchases', cashouts:'cashout requests', broadcasts:'announcements'};
feed.innerHTML = `<div style="text-align:center;padding:28px;color:var(--text2);font-size:15px">No ${labels[filter]||filter} yet.</div>`;
return;
}
feed.innerHTML = items.map(renderActivityCard).join('');
}
function buildActivityItems(d, filter) {
let items = [];
if (filter === 'all' || filter === 'purchases') {
(d.purchases||[]).forEach(p => items.push({ _type:'purchase', _ts: p.created_at, ...p }));
}
if (filter === 'all' || filter === 'cashouts') {
(d.cashouts||[]).forEach(c => items.push({ _type:'cashout', _ts: c.created_at, ...c }));
}
if (filter === 'all' || filter === 'broadcasts') {
(d.broadcasts||[]).forEach(b => items.push({ _type:'broadcast', _ts: b.sent_at, ...b }));
}
// Sort newest first
items.sort((a,b) => new Date(b._ts) - new Date(a._ts));
// For 'all', interleave with date dividers
if (filter === 'all') {
const grouped = []; let lastDate = '';
items.forEach(item => {
const dt = (item._ts||'').substring(0,10);
if (dt !== lastDate) {
lastDate = dt;
const d0 = new Date(dt+'T00:00:00'), now = new Date();
now.setHours(0,0,0,0);
const diff = Math.round((now-d0)/86400000);
const label = diff===0?'Today':diff===1?'Yesterday':d0.toLocaleDateString('en-US',{month:'short',day:'numeric'});
grouped.push({ _type:'divider', label });
}
grouped.push(item);
});
return grouped;
}
return items;
}
const PMT_ICON = { card:'💳', venmo:'💙', cashapp:'💚', zelle:'💜', chime:'🟢' };
function renderActivityCard(item) {
if (item._type === 'divider') {
return `<div style="font-size:15px;font-weight:700;color:var(--text2);letter-spacing:1px;text-transform:uppercase;padding:10px 0 6px;border-bottom:1px solid var(--border);margin-bottom:6px">${item.label}</div>`;
}
if (item._type === 'purchase') {
const statusMap = { completed:'✅ Completed', pending:'⏳ Awaiting Payment', failed:'❌ Failed', approved:'✅ Approved' };
const statusClass = { completed:'ac-completed', pending:'ac-pending', failed:'ac-failed', approved:'ac-completed' };
const icon = PMT_ICON[item.payment_method] || '💳';
const amt = item.amount_cents ? '$'+(item.amount_cents/100).toFixed(2) : '';
const card = item.card_last4 ? `${item.card_brand||'Card'} ····${item.card_last4}` : '';
const note = item.admin_note ? `<div style="font-size:15px;color:var(--gold);margin-top:4px">📝 ${escHtml(item.admin_note)}</div>` : '';
return `<div class="activity-card">
<div class="activity-icon" style="background:rgba(0,230,118,.1)">${icon}</div>
<div class="activity-body">
<div class="activity-title">Token Purchase · ${item.tokens} ${tokenCoin(14)}</div>
<div class="activity-meta">${pname(item.platform_id)}${item.game_alias?' · '+escHtml(item.game_alias):''}</div>
${card?`<div class="activity-meta">${card}</div>`:''}
<span class="activity-status ${statusClass[item.status]||'ac-pending'}">${statusMap[item.status]||item.status}</span>
${note}
</div>
<div class="activity-amount">
<div style="color:var(--green)">${amt}</div>
<div style="font-size:14px;color:var(--text2);font-weight:400;margin-top:2px">${(item.created_at||'').substring(0,10)}</div>
</div>
</div>`;
}
if (item._type === 'cashout') {
const statusMap = {
pending:'⏳ Draft — not submitted',
locked:'🔒 Submitted to admin',
sent:'✅ Payment Sent',
approved:'✅ Payment Sent',
rejected:'❌ Denied',
deleted:'🗑 Deleted'
};
const statusClass = { pending:'ac-pending', locked:'ac-locked', sent:'ac-completed', approved:'ac-completed', rejected:'ac-failed', deleted:'ac-failed' };
const payout = item.payout_handle ? `<div class="activity-meta">${escHtml(item.payout_handle)}</div>` : '';
const note = item.admin_note ? `<div style="font-size:15px;color:var(--gold);margin-top:4px">📝 ${escHtml(item.admin_note)}</div>` : '';
const canEdit = item.status === 'pending';
return `<div class="activity-card" style="${item.status==='pending'?'border-color:rgba(240,192,64,.2)':item.status==='locked'?'border-color:rgba(0,229,255,.15)':''}">
<div class="activity-icon" style="background:rgba(240,192,64,.1)">💸</div>
<div class="activity-body">
<div class="activity-title">Cashout Request · ${item.tokens} ${tokenCoin(14)}</div>
<div class="activity-meta">${escHtml(item.platform_name||item.platform_id)}${item.alias?' · '+escHtml(item.alias):''}</div>
${payout}
<span class="activity-status ${statusClass[item.status]||'ac-pending'}">${statusMap[item.status]||item.status}</span>
${note}
${canEdit?`<div style="margin-top:8px"><button onclick="showPage('cashout')" style="background:none;border:none;color:var(--cyan);font-size:15px;font-weight:700;cursor:pointer;padding:0">✏️ Edit on Cashout page →</button></div>`:''}
</div>
<div class="activity-amount">
<div style="font-size:14px;color:var(--text2);font-weight:400;margin-top:2px">${(item.created_at||'').substring(0,10)}</div>
<div style="font-size:14px;color:var(--text2)">#${item.id}</div>
</div>
</div>`;
}
if (item._type === 'broadcast') {
const isNew = !parseInt(item.is_read);
return `<div class="activity-card" style="${isNew?'border-color:rgba(240,192,64,.2)':''}">
<div class="activity-icon" style="background:rgba(240,192,64,.08)">📢</div>
<div class="activity-body">
<div class="activity-title">${escHtml(item.subject)}</div>
<div class="activity-meta">From ${escHtml(item.sender)} · ${(item.sent_at||'').substring(0,10)}</div>
<div style="font-size:14px;color:var(--text2);margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px">${escHtml(item.message)}</div>
${isNew?'<span class="activity-status ac-new">● New</span>':'<span class="activity-status ac-read">Read</span>'}
${parseInt(item.replied)?'<span class="activity-status ac-locked" style="margin-left:4px">Replied</span>':''}
</div>
<div>
<button onclick="showPage('broadcasts')" style="background:none;border:none;color:var(--cyan);font-size:15px;font-weight:700;cursor:pointer;padding:0;white-space:nowrap">View →</button>
</div>
</div>`;
}
return '';
}
function pname(id) {
return CFG.platforms?.find(p=>p.id===id)?.name || id || '—';
}
// ─── AUTH ──────────────────────────────────────────────────
async function resendFromLogin() {
const btn=document.getElementById('login-resend-btn');
const al=document.getElementById('login-resend-alert');
const email=S._unverifiedEmail;
if(!email){showAlert(al,'Please register first.','error');return;}
btn.innerHTML='<span class="spinner"></span>'; btn.disabled=true;
try {
const d=await api('/api/resend_verify.php',{email});
if(d.success) showAlert(al,'Verification email resent! Check your inbox.','success');
else showAlert(al,d.error||'Could not resend.','error');
} catch{showAlert(al,'Network error.','error');}
btn.innerHTML='Resend Verification Email'; btn.disabled=false;
}
async function doRegister() {
const btn=document.getElementById('reg-btn'); const al=document.getElementById('reg-alert');
const u=document.getElementById('reg-user').value.trim();
const alias=document.getElementById('reg-alias').value.trim();
const email=document.getElementById('reg-email').value.trim();
const p=document.getElementById('reg-pass').value;
if(!u||!alias||!p){showAlert(al,'Username, alias and password are required.','error');return;}
if(!email){showAlert(al,'Email address is required for account verification.','error');return;}
btn.innerHTML='<span class="spinner"></span>'; btn.disabled=true;
try {
const d=await api('/api/register.php',{username:u,alias,email,password:p,referral_code:getUrlRefCode()});
if(d.success){
// Show pending verification state
document.getElementById('reg-pending-email').textContent = d.email || email;
S._pendingEmail = d.email || email;
showPendingState();
if (d.mail_warning) {
// Email failed to send - show resend option prominently
showAlert(document.getElementById('pending-alert')||al,
'Account created! We had trouble sending the verification email. Use the Resend button below.','error');
}
} else {
showAlert(al,d.error||'Registration failed.','error');
}
} catch{showAlert(al,'Network error.','error');}
btn.innerHTML='CREATE ACCOUNT'; btn.disabled=false;
}
function showPendingState() {
document.getElementById('reg-form-state').style.display='none';
document.getElementById('reg-pending-state').style.display='block';
}
function showRegForm() {
document.getElementById('reg-form-state').style.display='block';
document.getElementById('reg-pending-state').style.display='none';
}
async function resendVerify() {
const btn=document.getElementById('resend-btn');
const al=document.getElementById('resend-alert');
const email=S._pendingEmail;
if(!email){showAlert(al,'No email found. Please register again.','error');return;}
btn.innerHTML='<span class="spinner"></span>'; btn.disabled=true;
try {
const d=await api('/api/resend_verify.php',{email});
if(d.success) showAlert(al,'Verification email resent! Check your inbox.','success');
else showAlert(al,d.error||'Could not resend.','error');
} catch{showAlert(al,'Network error.','error');}
btn.innerHTML='Resend Verification Email'; btn.disabled=false;
}
async function doLogout() {
try { await fetch('/api/logout.php'); } catch(e) {}
S.user = null;
S.pkg = null;
// Clear any cached state
if (window._refData) window._refData = null;
showAuth();
}
// ─── HELPERS ──────────────────────────────────────────────
function showAlert(el,msg,type){el.textContent=msg;el.className='alert show alert-'+type;}
function hideAlert(el){el.className='alert';}
function showModal(id){document.getElementById(id).classList.add('show');}
function closeModal(id){document.getElementById(id).classList.remove('show');}
document.addEventListener('keydown',e=>{if(e.key==='Enter'){const t=document.getElementById('tab-login');if(t&&t.style.display!=='none')doLogin();}});
// ─── CHAT ─────────────────────────────────────────────────
let chatState = { lastId: 0, polling: null, initialized: false };
function initChat() {
// Load/reload messages every time chat opens
loadChatMessages(true);
if (chatState.initialized) return;
chatState.initialized = true;
// Auto-refresh every 3s while chat is open
chatState.polling = setInterval(() => {
if (document.getElementById('page-chat').classList.contains('active')) {
loadChatMessages(false);
}
pollChatBadge();
}, 3000);
}
async function loadChatMessages(scrollToBottom) {
try {
const d = await api('/api/chat.php?action=messages&since=' + chatState.lastId);
if (!d.success) return;
const container = document.getElementById('chat-messages');
const quickReplies = document.getElementById('chat-quick-replies');
if (d.messages.length === 0 && chatState.lastId === 0) {
container.innerHTML = '<div class="chat-empty">' +
'<div class="chat-empty-icon">&#128172;</div>' +
'<div class="chat-empty-text">Have a question about tokens,<br>a cashout, or your account?<br>' +
'<strong style="color:var(--gold)">Send us a message below!</strong></div></div>';
if (quickReplies) quickReplies.style.display = 'flex';
return;
}
if (d.messages.length > 0) {
if (quickReplies) quickReplies.style.display = 'none';
const emptyEl = container.querySelector('.chat-empty, .chat-loading');
if (emptyEl) emptyEl.remove();
let lastDate = '';
d.messages.forEach(msg => {
// Skip if bubble already in DOM
if (container.querySelector('[data-id="' + msg.id + '"]')) return;
const msgDate = msg.created_at.substring(0,10);
if (msgDate !== lastDate) {
lastDate = msgDate;
const div = document.createElement('div');
div.className = 'chat-date-divider';
div.textContent = formatChatDate(msgDate);
container.appendChild(div);
}
const bubble = buildBubble(msg);
// Animate new incoming admin messages
if (msg.sender === 'admin' && chatState.lastId > 0) {
bubble.style.animation = 'fadeUp .3s ease';
}
container.appendChild(bubble);
chatState.lastId = Math.max(chatState.lastId, parseInt(msg.id));
});
if (scrollToBottom) scrollChat();
else {
const near = container.scrollHeight - container.scrollTop - container.clientHeight < 160;
if (near) scrollChat();
}
}
} catch(e) { console.warn('Chat poll error', e); }
}
function buildBubble(msg) {
const isMine = msg.sender === 'user';
const wrap = document.createElement('div');
wrap.className = 'chat-bubble-wrap ' + (isMine ? 'mine' : 'theirs');
wrap.dataset.id = msg.id;
const time = msg.created_at.substring(11,16);
const senderLabel = isMine ? '' : '<div class="chat-sender-label">Support</div>';
wrap.innerHTML = senderLabel +
'<div class="chat-bubble ' + (isMine ? 'mine' : 'theirs') + '">' + escHtml(msg.message) + '</div>' +
'<div class="chat-time">' + time + '</div>';
return wrap;
}
function scrollChat() {
const c = document.getElementById('chat-messages');
if (c) { c.scrollTop = c.scrollHeight; }
}
function onChatInput() {
const btn = document.getElementById('chat-send-btn');
const input = document.getElementById('chat-input');
if (btn) btn.classList.toggle('has-text', input.value.trim().length > 0);
}
function quickReply(text) {
const input = document.getElementById('chat-input');
if (input) {
input.value = text;
input.focus();
onChatInput();
}
}
async function sendChatMsg() {
const input = document.getElementById('chat-input');
const btn = document.getElementById('chat-send-btn');
const msg = input.value.trim();
if (!msg) return;
input.value = '';
if (btn) btn.classList.remove('has-text');
const container = document.getElementById('chat-messages');
const quickReplies = document.getElementById('chat-quick-replies');
const emptyEl = container.querySelector('.chat-empty, .chat-loading');
if (emptyEl) emptyEl.remove();
if (quickReplies) quickReplies.style.display = 'none';
// Optimistic bubble
const tempBubble = buildBubble({
id: 'temp_' + Date.now(), sender: 'user', message: msg,
created_at: new Date().toISOString().replace('T',' ').substring(0,19)
});
tempBubble.style.opacity = '0.65';
container.appendChild(tempBubble);
scrollChat();
try {
const d = await api('/api/chat.php?action=send', { message: msg });
if (d.success) {
tempBubble.style.opacity = '1';
tempBubble.dataset.id = d.id;
chatState.lastId = Math.max(chatState.lastId, d.id);
} else {
tempBubble.remove();
}
} catch { tempBubble.remove(); }
}
async function pollChatBadge() {
try {
const d = await api('/api/chat.php?action=unread');
const badge = document.getElementById('chat-nav-badge');
if (d.success && d.count > 0) {
badge.textContent = d.count > 9 ? '9+' : d.count;
badge.style.display = 'flex';
} else {
badge.style.display = 'none';
}
} catch(e) {}
}
function formatChatDate(dateStr) {
const d = new Date(dateStr + 'T00:00:00');
const today = new Date(); today.setHours(0,0,0,0);
const diff = Math.round((today - d) / 86400000);
if (diff === 0) return 'Today';
if (diff === 1) return 'Yesterday';
return d.toLocaleDateString('en-US', {month:'short', day:'numeric'});
}
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>');
}
</script>
</body>
<!-- v<?= $_appVersion ?> --></html>