git / moonbite-network / templates/worldcup.html · raw

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"><meta name="theme-color" content="#0B0D12"><title>The Mining World Cup — MoonBite</title><meta name="description" content="Every block has a country. Claim your nation's permanent miner number and put your flag on the board."><link rel="icon" type="image/svg+xml" href="/static/moonbite-logo.svg"><link rel="stylesheet" href="/static/fonts/moonbite-fonts.css"><style>/* Doc 04: three roles. A heavy grotesque for display, a quiet sans for   reading, a monospace for anything that is a number.   Archivo Black and Inter are self-hosted from /static/fonts (latin subset,   OFL-licensed). They are deliberately not loaded from Google Fonts: /privacy   states the site loads nothing that phones home, and a webfont CDN sends   every visitor's IP to Google on every page load. Every figure that is   money, a hash, an address or a block height is monospace with tabular   figures — amounts that shift width as they update look untrustworthy. */:root{  --font-display:'Archivo Black','Helvetica Neue','Arial Black',sans-serif;  --font-body:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;  --font-data:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}h1,h2,.big,.reveal .big,.balance-amount,.elapsed,.mort-big{  font-family:var(--font-display); font-weight:400; letter-spacing:-.02em;}.num,.mono,.addr,.cert .amt,.cl .v,.tot .v,.mcell .v,.odds .v,.loss .v,.count .v,.zero-band .v,.total .v,.vault-v,td.n,th.n{  font-family:var(--font-data); font-variant-numeric:tabular-nums;}* { margin:0; padding:0; box-sizing:border-box; }body { background:#0B0D12; color:#F5F2EA;  font-family:var(--font-body);  overflow-x:hidden; -webkit-font-smoothing:antialiased; }::selection { background:#D9A441; color:#0B0D12; }a,button,select { -webkit-tap-highlight-color:transparent; font-family:inherit; }.wrap { position:relative; z-index:1; max-width:560px; margin:0 auto; padding:0 18px 70px; }.num { font-variant-numeric:tabular-nums; }.kicker { letter-spacing:.3em; font-size:.62rem; color:#D9A441; text-transform:uppercase;  font-weight:800; }/* ------------------------------------------------------------- STADIUM */.stage { position:relative; text-align:center; padding-top:2vh; }.globe-wrap { position:relative; width:min(88vw,420px); aspect-ratio:1; margin:0 auto; }#globe { width:100%; height:100%; display:block; }.globe-badge { position:absolute; left:50%; bottom:2%; transform:translateX(-50%);  background:rgba(11,13,18,.86); border:1px solid rgba(217,164,65,.32); border-radius:999px;  padding:7px 15px; font-size:.62rem; letter-spacing:.2em; font-weight:800; color:#D9A441;  white-space:nowrap; backdrop-filter:blur(6px); }h1 { font-size:clamp(2.1rem,9vw,3.4rem); font-weight:800; line-height:1.02;  letter-spacing:-.025em; margin:14px 0 10px; }h1 .g { color:#D9A441; }.lede { color:#8A8580; font-size:.97rem; line-height:1.62; max-width:430px; margin:0 auto; }.totals { display:grid; grid-template-columns:repeat(3,1fr); gap:9px; margin-top:22px; }.tot { background:#161A22; border:1px solid rgba(245,242,234,.08); border-radius:10px;  padding:14px 6px; }.tot .v { font-size:clamp(1.25rem,5.6vw,1.7rem); font-weight:800; color:#D9A441; line-height:1; }.tot .k { font-size:.52rem; letter-spacing:.15em; color:#8A8580; text-transform:uppercase;  margin-top:6px; font-weight:700; }/* --------------------------------------------------------------- PODIUM */.podium { display:flex; align-items:flex-end; justify-content:center; gap:10px; margin-top:26px;  min-height:150px; }.pod { flex:1; max-width:110px; display:flex; flex-direction:column; align-items:center; }.pod .fl { font-size:1.9rem; line-height:1; margin-bottom:6px; }.pod .nm { font-size:.66rem; color:#F5F2EA; font-weight:800; letter-spacing:.06em;  text-align:center; margin-bottom:7px; min-height:2.1em; line-height:1.15; }.pod .bar { width:100%; border-radius:10px 11px 0 0; display:flex; flex-direction:column;  align-items:center; justify-content:flex-start; padding-top:9px; height:0;  transition:height .9s cubic-bezier(.2,1,.3,1); }.pod .bar .r { font-size:1.15rem; font-weight:800; color:#0B0D12; }.pod .bar .m { font-size:.55rem; font-weight:800; color:rgba(11,13,18,.65); letter-spacing:.1em; }.pod.p1 .bar { background:linear-gradient(180deg,#E8C878,#D9A441); }.pod.p2 .bar { background:linear-gradient(180deg,#A9A29B,#8A8580); }.pod.p3 .bar { background:linear-gradient(180deg,#A9763B,#8F6B24); }.podium.empty { display:none; }/* ------------------------------------------------------------ YOUR CARD */.card { background:#161A22; border:1px solid rgba(217,164,65,.22); border-radius:20px;  padding:20px 18px; margin-top:24px; box-shadow:0 20px 60px rgba(0,0,0,.45); }.card-t { font-size:.6rem; letter-spacing:.22em; color:#8A8580; text-transform:uppercase;  font-weight:800; margin-bottom:14px; }.you { display:flex; align-items:center; gap:15px; }.you .fl { font-size:clamp(40px,10vw,56px); line-height:1; }.you .m { flex:1; text-align:left; min-width:0; }.you .nm { font-size:clamp(1.1rem,4.4vw,1.45rem); font-weight:800; }.you .no { color:#D9A441; font-weight:800; font-size:.94rem; margin-top:3px; }.you .hint { color:#8A8580; font-size:.8rem; margin-top:6px; line-height:1.5; }.pick-row { display:flex; gap:9px; margin-top:16px; flex-wrap:wrap; }select { flex:1; min-width:170px; background:#0B0D12; color:#F5F2EA; font-size:.95rem;  border:1px solid rgba(217,164,65,.3); border-radius:10px; padding:14px 12px; min-height:50px; }select:focus { outline:none; border-color:#D9A441; }.btn { border:none; border-radius:10px; padding:14px 22px; min-height:50px; font-weight:800;  font-size:.9rem; cursor:pointer; text-decoration:none; display:inline-flex;  align-items:center; justify-content:center; letter-spacing:.06em; }.btn-gold { background:#D9A441; color:#0B0D12; }.btn-gold:disabled { opacity:.5; }.btn-ghost { background:transparent; color:#F5F2EA; border:1px solid rgba(245,242,234,.24); }/* ----------------------------------------------------------- THE RIVAL */.rival { display:none; background:linear-gradient(135deg,rgba(200,64,47,.13),rgba(217,164,65,.06));  border:1px solid rgba(200,64,47,.3); border-radius:20px; padding:20px 18px; margin-top:16px; }.rival.on { display:block; }.rival .line { font-size:clamp(1rem,4.2vw,1.2rem); font-weight:700; line-height:1.5; }.rival .line b { color:#D9A441; }.rbar { height:9px; border-radius:5px; background:rgba(245,242,234,.08); overflow:hidden;  margin-top:14px; }.rfill { height:100%; width:0; border-radius:5px; background:linear-gradient(90deg,#D9A441,#B8863B);  transition:width 1s cubic-bezier(.2,1,.3,1); }.rends { display:flex; justify-content:space-between; font-size:.74rem; color:#8A8580; margin-top:7px; }/* ------------------------------------------------------------ STANDINGS */table { width:100%; border-collapse:collapse; margin-top:6px; }th { text-align:left; font-size:.55rem; letter-spacing:.16em; color:#8A8580; text-transform:uppercase;  padding:0 6px 11px; font-weight:800; }th.n, td.n { text-align:right; font-variant-numeric:tabular-nums; }td { padding:13px 6px; border-top:1px solid rgba(245,242,234,.07); font-size:.92rem; }tr.me td { background:rgba(217,164,65,.09); }tr.me td:first-child { box-shadow:inset 3px 0 0 #D9A441; }.rk { color:#8A8580; width:34px; font-variant-numeric:tabular-nums; font-weight:700; }.rk.top { color:#D9A441; font-weight:800; }.cflag { font-size:1.15rem; margin-right:8px; }.ccode { display:inline-block; font-family:ui-monospace,Menlo,Consolas,monospace; font-size:.65rem;  color:#D9A441; border:1px solid rgba(217,164,65,.35); border-radius:5px; padding:1px 5px;  margin-left:7px; }.no-flags .ccode { display:none; }.no-flags .cflag, .no-flags .you .fl, .no-flags .pod .fl { color:#D9A441; }.empty { text-align:center; color:#8A8580; padding:30px 10px; line-height:1.75; font-size:.9rem; }.fine { font-size:.74rem; color:#8A8580; line-height:1.75; text-align:center; margin-top:24px; }.fine a { color:rgba(217,164,65,.75); }.toast { position:fixed; left:50%; bottom:24px; transform:translateX(-50%) translateY(16px);  background:#2E9E6B; color:#04160E; font-weight:800; padding:12px 22px; border-radius:10px;  opacity:0; transition:all .3s; z-index:60; pointer-events:none; font-size:.88rem; }.toast.show { opacity:1; transform:translateX(-50%) translateY(0); }#card { display:none; }@media (prefers-reduced-motion: reduce){ .pod .bar,.rfill { transition:none; } }/* Doc 17: visible keyboard focus on every interactive element. */a:focus-visible, button:focus-visible, input:focus-visible,select:focus-visible, [tabindex]:focus-visible {  outline:2px solid #D9A441; outline-offset:3px; border-radius:6px;}</style></head><body>{% include 'partials/nav.html' %}<div class="wrap">  <!-- ========================================================== STADIUM -->  <section class="stage">    <div class="globe-wrap">      <canvas id="globe"></canvas>      <div class="globe-badge" id="globeBadge">● LIVE — NO NATION HAS CLAIMED A FLAG YET</div>    </div>    <div class="kicker">THE MINING WORLD CUP</div>    <h1>Every block has<br>a <span class="g">country</span>.</h1>    <p class="lede">No presale decides this table. No exchange, no fund, no founder —      only how many people from your country turned their computer on.</p>    <div class="totals">      <div class="tot"><div class="v num" id="tC">0</div><div class="k">Countries</div></div>      <div class="tot"><div class="v num" id="tM">0</div><div class="k">Miners</div></div>      <div class="tot"><div class="v num" id="tB">0</div><div class="k">Blocks</div></div>    </div>  </section>  <!-- =========================================================== PODIUM -->  <div class="podium empty" id="podium">    <div class="pod p2" id="pod2"><div class="fl"></div><div class="nm"></div>      <div class="bar"><div class="r"></div><div class="m">MINERS</div></div></div>    <div class="pod p1" id="pod1"><div class="fl"></div><div class="nm"></div>      <div class="bar"><div class="r"></div><div class="m">MINERS</div></div></div>    <div class="pod p3" id="pod3"><div class="fl"></div><div class="nm"></div>      <div class="bar"><div class="r"></div><div class="m">MINERS</div></div></div>  </div>  <!-- ======================================================== YOUR CARD -->  <section class="card">    <div class="card-t">YOUR COUNTRY</div>    <div class="you">      <div class="fl" id="youFlag"></div>      <div class="m">        <div class="nm" id="youName">Choose your country</div>        <div class="no" id="youNo"></div>        <div class="hint" id="youHint">Your number is permanent. The earlier you claim it,          the lower it is — and it can never be bought, only taken first.</div>      </div>    </div>    <div class="pick-row">      <select id="pick" aria-label="Country"></select>      <button class="btn btn-gold" id="btnClaim">CLAIM MY NUMBER</button>      <button class="btn btn-ghost" id="btnCard" style="display:none;">Share card</button>    </div>  </section>  <!-- =========================================================== RIVAL -->  <section class="rival" id="rival">    <div class="card-t" style="margin-bottom:10px;">THE GAP</div>    <div class="line" id="rLine">—</div>    <div class="rbar"><div class="rfill" id="rFill"></div></div>    <div class="rends"><span id="rYou">—</span><span id="rThem">—</span></div>  </section>  <!-- ======================================================== STANDINGS -->  <section class="card">    <div class="card-t">STANDINGS</div>    <table>      <thead><tr><th style="width:34px;">#</th><th>Country</th>        <th class="n">Miners</th><th class="n">Blocks</th></tr></thead>      <tbody id="board"></tbody>    </table>    <div class="empty" id="empty">The board is empty.<br>      No one has claimed a country yet — the first flag on this table is still available.</div>  </section>  <p class="fine">    Countries are <strong>declared by miners themselves</strong>. A coinbase transaction carries    no country, so these are community figures — not a consensus metric, and not Sybil-proof.    Your country comes from your browser's language setting and you can change it; no IP address    is read or stored.<br>    MBITE has no market value and is not an investment. <a href="/start">Start mining →</a>  </p></div>{% include 'partials/footer.html' %}<canvas id="card" width="1080" height="1080"></canvas><div class="toast" id="toast"></div><script nonce="{{ csp_nonce }}">(function () {  'use strict';  if (!CanvasRenderingContext2D.prototype.roundRect) {    CanvasRenderingContext2D.prototype.roundRect = function (x,y,w,h,r) {      r=Math.min(r,w/2,h/2); this.moveTo(x+r,y); this.arcTo(x+w,y,x+w,y+h,r);      this.arcTo(x+w,y+h,x,y+h,r); this.arcTo(x,y+h,x,y,r); this.arcTo(x,y,x+w,y,r);      this.closePath(); return this; };  }  /* --------------------------------------------------------- countries */  var NAMES = ('AF Afghanistan|AL Albania|DZ Algeria|AR Argentina|AM Armenia|AU Australia|' +    'AT Austria|AZ Azerbaijan|BH Bahrain|BD Bangladesh|BY Belarus|BE Belgium|BO Bolivia|' +    'BA Bosnia and Herzegovina|BW Botswana|BR Brazil|BN Brunei|BG Bulgaria|BF Burkina Faso|' +    'KH Cambodia|CM Cameroon|CA Canada|CL Chile|CN China|CO Colombia|CR Costa Rica|HR Croatia|' +    'CU Cuba|CY Cyprus|CZ Czechia|CD DR Congo|DK Denmark|DO Dominican Republic|EC Ecuador|' +    'EG Egypt|SV El Salvador|EE Estonia|ET Ethiopia|FI Finland|FR France|GE Georgia|DE Germany|' +    'GH Ghana|GR Greece|GT Guatemala|HN Honduras|HK Hong Kong|HU Hungary|IS Iceland|IN India|' +    'ID Indonesia|IR Iran|IQ Iraq|IE Ireland|IL Israel|IT Italy|CI Ivory Coast|JM Jamaica|' +    'JP Japan|JO Jordan|KZ Kazakhstan|KE Kenya|KW Kuwait|KG Kyrgyzstan|LA Laos|LV Latvia|' +    'LB Lebanon|LY Libya|LT Lithuania|LU Luxembourg|MK North Macedonia|MG Madagascar|' +    'MY Malaysia|MV Maldives|ML Mali|MT Malta|MU Mauritius|MX Mexico|MD Moldova|MN Mongolia|' +    'ME Montenegro|MA Morocco|MZ Mozambique|MM Myanmar|NA Namibia|NP Nepal|NL Netherlands|' +    'NZ New Zealand|NI Nicaragua|NE Niger|NG Nigeria|KP North Korea|NO Norway|OM Oman|' +    'PK Pakistan|PS Palestine|PA Panama|PG Papua New Guinea|PY Paraguay|PE Peru|PH Philippines|' +    'PL Poland|PT Portugal|PR Puerto Rico|QA Qatar|RO Romania|RU Russia|RW Rwanda|' +    'SA Saudi Arabia|SN Senegal|RS Serbia|SG Singapore|SK Slovakia|SI Slovenia|SO Somalia|' +    'ZA South Africa|KR South Korea|ES Spain|LK Sri Lanka|SD Sudan|SE Sweden|CH Switzerland|' +    'SY Syria|TW Taiwan|TJ Tajikistan|TZ Tanzania|TH Thailand|TN Tunisia|TR Turkey|' +    'TM Turkmenistan|UG Uganda|UA Ukraine|AE United Arab Emirates|GB United Kingdom|' +    'US United States|UY Uruguay|UZ Uzbekistan|VE Venezuela|VN Vietnam|YE Yemen|ZM Zambia|' +    'ZW Zimbabwe').split('|');  var NAME = {};  NAMES.forEach(function (s) { NAME[s.slice(0,2)] = s.slice(3); });  /* centroid lat/lng, for the globe */  var GEO = {};  ('AF 33.9 67.7|AL 41.2 20.2|DZ 28.0 1.7|AR -38.4 -63.6|AM 40.1 45.0|AU -25.3 133.8|' +   'AT 47.5 14.6|AZ 40.1 47.6|BH 26.0 50.6|BD 23.7 90.4|BY 53.7 27.9|BE 50.5 4.5|' +   'BO -16.3 -63.6|BA 43.9 17.7|BW -22.3 24.7|BR -14.2 -51.9|BN 4.5 114.7|BG 42.7 25.5|' +   'BF 12.2 -1.6|KH 12.6 104.9|CM 7.4 12.4|CA 56.1 -106.3|CL -35.7 -71.5|CN 35.9 104.2|' +   'CO 4.6 -74.3|CR 9.7 -83.8|HR 45.1 15.2|CU 21.5 -77.8|CY 35.1 33.4|CZ 49.8 15.5|' +   'CD -4.0 21.8|DK 56.3 9.5|DO 18.7 -70.2|EC -1.8 -78.2|EG 26.8 30.8|SV 13.8 -88.9|' +   'EE 58.6 25.0|ET 9.1 40.5|FI 61.9 25.7|FR 46.2 2.2|GE 42.3 43.4|DE 51.2 10.5|GH 7.9 -1.0|' +   'GR 39.1 21.8|GT 15.8 -90.2|HN 15.2 -86.2|HK 22.3 114.2|HU 47.2 19.5|IS 65.0 -19.0|' +   'IN 20.6 79.0|ID -0.8 113.9|IR 32.4 53.7|IQ 33.2 43.7|IE 53.4 -8.2|IL 31.0 34.9|' +   'IT 41.9 12.6|CI 7.5 -5.5|JM 18.1 -77.3|JP 36.2 138.3|JO 30.6 36.2|KZ 48.0 66.9|' +   'KE 0.0 37.9|KW 29.3 47.5|KG 41.2 74.8|LA 19.9 102.5|LV 56.9 24.6|LB 33.9 35.9|' +   'LY 26.3 17.2|LT 55.2 23.9|LU 49.8 6.1|MK 41.6 21.7|MG -18.8 46.9|MY 4.2 102.0|' +   'MV 3.2 73.2|ML 17.6 -4.0|MT 35.9 14.4|MU -20.3 57.6|MX 23.6 -102.6|MD 47.4 28.4|' +   'MN 46.9 103.8|ME 42.7 19.4|MA 31.8 -7.1|MZ -18.7 35.5|MM 21.9 96.0|NA -23.0 18.5|' +   'NP 28.4 84.1|NL 52.1 5.3|NZ -40.9 174.9|NI 12.9 -85.2|NE 17.6 8.1|NG 9.1 8.7|' +   'KP 40.3 127.5|NO 60.5 8.5|OM 21.5 55.9|PK 30.4 69.3|PS 32.0 35.2|PA 8.5 -80.8|' +   'PG -6.3 144.0|PY -23.4 -58.4|PE -9.2 -75.0|PH 12.9 121.8|PL 51.9 19.1|PT 39.4 -8.2|' +   'PR 18.2 -66.6|QA 25.4 51.2|RO 45.9 25.0|RU 61.5 105.3|RW -1.9 29.9|SA 23.9 45.1|' +   'SN 14.5 -14.5|RS 44.0 21.0|SG 1.4 103.8|SK 48.7 19.7|SI 46.2 15.0|SO 5.2 46.2|' +   'ZA -30.6 22.9|KR 35.9 127.8|ES 40.5 -3.7|LK 7.9 80.8|SD 12.9 30.2|SE 60.1 18.6|' +   'CH 46.8 8.2|SY 34.8 39.0|TW 23.7 121.0|TJ 38.9 71.3|TZ -6.4 34.9|TH 15.9 101.0|' +   'TN 33.9 9.5|TR 39.0 35.2|TM 39.0 59.6|UG 1.4 32.3|UA 48.4 31.2|AE 23.4 53.8|' +   'GB 55.4 -3.4|US 37.1 -95.7|UY -32.5 -55.8|UZ 41.4 64.6|VE 6.4 -66.6|VN 14.1 108.3|' +   'YE 15.6 48.5|ZM -13.1 27.8|ZW -19.0 29.2').split('|').forEach(function (s) {    var p = s.split(' ');    GEO[p[0]] = { lat: parseFloat(p[1]), lng: parseFloat(p[2]) };  });  function flag(code){ return code.replace(/./g, function(ch){    return String.fromCodePoint(0x1F1E6 + ch.charCodeAt(0) - 65); }); }  function num(n){ return (n||0).toLocaleString('en-US'); }  function $(id){ return document.getElementById(id); }  /* Windows draws flag emoji as two letter-boxes that already spell the code,     so the separate code chip would be duplication there. */  (function(){    try { var c=document.createElement('canvas').getContext('2d'); c.font='40px serif';      if(!(c.measureText('').width < c.measureText('').width*1.6))        document.body.classList.add('no-flags');    } catch(e){}  })();  /* ------------------------------------------------------------- token */  var token = localStorage.getItem('mb_wc_token');  if (!token){ token='wc'+Math.random().toString(36).slice(2)+Date.now().toString(36);    localStorage.setItem('mb_wc_token', token); }  function myBlocks(){    try { var a=JSON.parse(localStorage.getItem('mb_miner_profiles')||'{}');      return Object.keys(a).reduce(function(s,k){ return s+(a[k].totalBlocks||0); },0);    } catch(e){ return 0; }  }  /* ============================================================= GLOBE */  var gc = $('globe'), g = gc.getContext('2d');  var DPR = Math.min(window.devicePixelRatio||1, 2);  var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;  var rot = 0, spin = 0.0022, targetRot = null, pulse = 0;  var lit = {};        /* code -> miners */  var maxMiners = 1;  var mineCode = null;  function project(lat, lng, R, cx, cy){    var la = lat*Math.PI/180, lo = (lng*Math.PI/180) + rot;    var x = Math.cos(la)*Math.sin(lo);    var y = -Math.sin(la);    var z = Math.cos(la)*Math.cos(lo);    return { x: cx + x*R, y: cy + y*R, z: z };  }  function drawGlobe(){    var S = gc.clientWidth;    if (S>0 && gc.width !== S*DPR){ gc.width=S*DPR; gc.height=S*DPR; }    g.setTransform(DPR,0,0,DPR,0,0);    g.clearRect(0,0,S,S);    var cx=S/2, cy=S/2, R=S*0.40;    pulse += reduced?0:0.045;    if (targetRot !== null){      var d = targetRot - rot;      while (d >  Math.PI) d -= 6.2832;      while (d < -Math.PI) d += 6.2832;      if (Math.abs(d) < 0.004){ rot = targetRot; targetRot = null; }      else rot += d*0.06;    } else if (!reduced) { rot += spin; }    /* atmosphere */    var atm = g.createRadialGradient(cx,cy,R*0.85,cx,cy,R*1.35);    atm.addColorStop(0,'rgba(217,164,65,0.13)');    atm.addColorStop(1,'rgba(217,164,65,0)');    g.fillStyle=atm; g.beginPath(); g.arc(cx,cy,R*1.35,0,6.2832); g.fill();    /* sphere body */    var body = g.createRadialGradient(cx-R*0.35,cy-R*0.4,R*0.1,cx,cy,R);    body.addColorStop(0,'#161B26'); body.addColorStop(1,'#0C0F16');    g.fillStyle=body; g.beginPath(); g.arc(cx,cy,R,0,6.2832); g.fill();    /* graticule — latitude rings */    g.strokeStyle='rgba(245,242,234,0.075)'; g.lineWidth=1;    for (var la=-60; la<=60; la+=30){      g.beginPath();      var started=false;      for (var lo=-180; lo<=180; lo+=4){        var p=project(la,lo,R,cx,cy);        if (p.z>0){ if(!started){ g.moveTo(p.x,p.y); started=true; } else g.lineTo(p.x,p.y); }        else started=false;      }      g.stroke();    }    /* meridians */    for (var lo2=0; lo2<180; lo2+=30){      g.beginPath(); started=false;      for (var la2=-90; la2<=90; la2+=4){        var q=project(la2,lo2,R,cx,cy);        if (q.z>0){ if(!started){ g.moveTo(q.x,q.y); started=true; } else g.lineTo(q.x,q.y); }        else started=false;      }      g.stroke();    }    /* rim */    g.strokeStyle='rgba(217,164,65,0.34)'; g.lineWidth=1.4;    g.beginPath(); g.arc(cx,cy,R,0,6.2832); g.stroke();    /* enlisted nations */    Object.keys(lit).forEach(function(code){      var geo = GEO[code]; if(!geo) return;      var p = project(geo.lat, geo.lng, R, cx, cy);      if (p.z <= 0.02) return;                       /* far side */      var share = lit[code]/maxMiners;      var rad = 2.4 + share*5.5;      var a = 0.35 + 0.65*p.z;      var isMine = (code === mineCode);      if (isMine){        var ring = rad + 5 + 3.5*Math.sin(pulse);        g.strokeStyle='rgba(217,164,65,'+(0.75*a)+')'; g.lineWidth=1.6;        g.beginPath(); g.arc(p.x,p.y,ring,0,6.2832); g.stroke();      }      var glow = g.createRadialGradient(p.x,p.y,0,p.x,p.y,rad*3.6);      glow.addColorStop(0,'rgba(217,164,65,'+(0.42*a)+')');      glow.addColorStop(1,'rgba(217,164,65,0)');      g.fillStyle=glow; g.beginPath(); g.arc(p.x,p.y,rad*3.6,0,6.2832); g.fill();      g.fillStyle = isMine ? '#FFFFFF' : 'rgba(233,199,120,'+a+')';      g.beginPath(); g.arc(p.x,p.y,rad,0,6.2832); g.fill();    });    requestAnimationFrame(drawGlobe);  }  requestAnimationFrame(drawGlobe);  function faceCountry(code){    var geo = GEO[code]; if(!geo) return;    targetRot = -geo.lng*Math.PI/180;   /* bring that longitude to the front */  }  /* ------------------------------------------------------------- state */  var state = { countries: [], you: null, total_miners: 0, total_blocks: 0 };  var pick = $('pick');  Object.keys(NAME).sort(function(a,b){ return NAME[a]<NAME[b]?-1:1; }).forEach(function(c){    var o=document.createElement('option'); o.value=c;    o.textContent = flag(c)+'  '+NAME[c]; pick.appendChild(o);  });  /* Browser language only — no IP lookup, no geolocation prompt. */  (function(){    var L = navigator.languages || [navigator.language||''];    for (var i=0;i<L.length;i++){      var m=/[-_]([A-Za-z]{2})$/.exec(L[i]||'');      if (m && NAME[m[1].toUpperCase()]){ pick.value=m[1].toUpperCase(); return; }    }    pick.value='US';  })();  function toast(m){ var t=$('toast'); t.textContent=m; t.classList.add('show');    setTimeout(function(){ t.classList.remove('show'); },2600); }  function render(){    var list = state.countries;    $('tC').textContent = num(list.length);    $('tM').textContent = num(state.total_miners);    $('tB').textContent = num(state.total_blocks);    /* globe data */    lit = {}; maxMiners = 1;    list.forEach(function(c){ lit[c.code]=c.miners; if(c.miners>maxMiners) maxMiners=c.miners; });    $('globeBadge').textContent = list.length      ? '● LIVE — ' + num(state.total_miners) + ' MINERS ACROSS ' + num(list.length) +        (list.length===1?' NATION':' NATIONS')      : '● LIVE — NO NATION HAS CLAIMED A FLAG YET';    var mine = state.you ? state.you.code : null;    mineCode = mine;    if (mine) pick.value = mine;    var code = pick.value;    $('youFlag').textContent = flag(code);    $('youName').textContent = NAME[code] || code;    var row=null, rank=0;    for (var i=0;i<list.length;i++)      if (list[i].code === (mine||code)){ row=list[i]; rank=list[i].rank; break; }    if (state.you){      $('youNo').textContent = 'Miner #'+state.you.ordinal+' of '+(NAME[mine]||mine)+        (rank ? '  ·  ranked #'+rank+' worldwide' : '');      $('youHint').textContent = state.you.ordinal===1        ? 'You are the first miner your country ever had. That is not a rank — it is a fact about history.'        : 'Your number is fixed forever. Everyone who arrives after you gets a higher one.';      $('btnClaim').textContent='CLAIMED'; $('btnClaim').disabled=true;      pick.disabled=true; $('btnCard').style.display='';    }    /* podium */    var pod=$('podium');    if (list.length >= 1){      pod.classList.remove('empty');      [['pod1',0],['pod2',1],['pod3',2]].forEach(function(pair){        var el=$(pair[0]), c=list[pair[1]];        if(!c){ el.style.visibility='hidden'; return; }        el.style.visibility='';        el.querySelector('.fl').textContent=flag(c.code);        el.querySelector('.nm').textContent=NAME[c.code]||c.code;        el.querySelector('.r').textContent=num(c.miners);        var h = 40 + 74*(c.miners/list[0].miners);        setTimeout(function(){ el.querySelector('.bar').style.height=h+'px'; }, 60);      });    } else pod.classList.add('empty');    /* rivalry */    var rv=$('rival');    if (row && rank>1){      var above=list[rank-2], gap=above.miners-row.miners;      $('rLine').innerHTML = flag(row.code)+' <b>'+(NAME[row.code]||row.code)+'</b> is '+        (gap===0 ? '<b>level with</b> ' : '<b>'+gap+' miner'+(gap===1?'':'s')+'</b> behind ')+        flag(above.code)+' <b>'+(NAME[above.code]||above.code)+'</b>.';      $('rFill').style.width = Math.max(5, 100*row.miners/Math.max(1,above.miners))+'%';      $('rYou').textContent = flag(row.code)+' '+num(row.miners);      $('rThem').textContent = num(above.miners)+' '+flag(above.code);      rv.classList.add('on');    } else if (row && rank===1){      $('rLine').innerHTML = flag(row.code)+' <b>'+(NAME[row.code]||row.code)+        '</b> is <b>first in the world</b>. Someone is going to come for that.';      $('rFill').style.width='100%';      $('rYou').textContent=flag(row.code)+' '+num(row.miners);      $('rThem').textContent='LEADER';      rv.classList.add('on');    } else rv.classList.remove('on');    /* standings */    $('board').innerHTML = list.map(function(c){      return '<tr class="'+(c.code===mine?'me':'')+'">'+        '<td class="rk '+(c.rank<=3?'top':'')+'">'+c.rank+'</td>'+        '<td><span class="cflag">'+flag(c.code)+'</span>'+(NAME[c.code]||c.code)+        '<span class="ccode">'+c.code+'</span></td>'+        '<td class="n">'+num(c.miners)+'</td><td class="n">'+num(c.blocks)+'</td></tr>';    }).join('');    $('empty').style.display = list.length ? 'none' : '';  }  function apply(d){    state.countries = d.countries||[];    state.total_miners = d.total_miners||0;    state.total_blocks = d.total_blocks||0;    if (d.you) state.you = d.you;    render();    if (state.you) faceCountry(state.you.code);  }  fetch('/api/worldcup?token='+encodeURIComponent(token))    .then(function(r){return r.json();}).then(apply).catch(render);  $('btnClaim').addEventListener('click', function(){    var b=this; b.disabled=true; b.textContent='…';    fetch('/api/worldcup/enlist',{method:'POST',headers:{'Content-Type':'application/json'},      body:JSON.stringify({token:token, code:pick.value, blocks:myBlocks()})})      .then(function(r){return r.json();}).then(function(d){        if(d.status!=='success') throw new Error(d.message||'failed');        apply(d);        toast(d.you.new ? 'You are miner #'+d.you.ordinal+' of '+(NAME[d.you.code]||d.you.code)                        : 'Already claimed — you are #'+d.you.ordinal);      }).catch(function(){        b.disabled=false; b.textContent='CLAIM MY NUMBER';        toast('Could not reach the scoreboard. Try again.');      });  });  pick.addEventListener('change', function(){ render(); faceCountry(pick.value); });  /* -------------------------------------------------------- share card */  $('btnCard').addEventListener('click', function(){    if(!state.you) return;    var code=state.you.code, name=NAME[code]||code, rank=0, miners=0;    state.countries.forEach(function(c){ if(c.code===code){ rank=c.rank; miners=c.miners; } });    var cv=$('card'), x=cv.getContext('2d');    var bg=x.createLinearGradient(0,0,0,1080);    bg.addColorStop(0,'#0B0D12'); bg.addColorStop(1,'#161A22');    x.fillStyle=bg; x.fillRect(0,0,1080,1080);    for(var i=0;i<90;i++){ x.globalAlpha=.12+Math.random()*.5; x.fillStyle='#F5F2EA';      x.beginPath(); x.arc(Math.random()*1080,Math.random()*1080,Math.random()*1.9+.4,0,6.2832); x.fill(); }    x.globalAlpha=1; x.textAlign='center';    x.beginPath(); x.arc(540,160,70,0,6.2832); x.fillStyle='#F5F2EA'; x.fill();    x.beginPath(); x.arc(578,107,54,0,6.2832); x.fillStyle='#0B0D12'; x.fill();    x.fillStyle='#D9A441';    x.beginPath(); x.roundRect(432,135,216,9,4.5); x.fill();    x.beginPath(); x.roundRect(439,173,202,9,4.5); x.fill();    x.fillStyle='#8A8580'; x.font='700 28px Arial';    x.fillText('MOONBITE · MINING WORLD CUP', 540, 285);    x.font='118px Arial'; x.fillText(flag(code), 540, 425);    x.fillStyle='#F5F2EA'; x.font='800 58px Arial'; x.fillText(name.toUpperCase(), 540, 512);    x.fillStyle='#D9A441'; x.font='800 94px Arial';    x.fillText('MINER #'+state.you.ordinal, 540, 630);    x.fillStyle='#F5F2EA'; x.font='400 36px Arial';    x.fillText(name+' is ranked #'+rank+' in the world', 540, 700);    x.fillStyle='#8A8580'; x.font='400 32px Arial';    x.fillText(num(miners)+' miner'+(miners===1?'':'s')+' flying this flag', 540, 752);    x.fillStyle='#F5F2EA'; x.font='italic 34px Arial';    x.fillText('No presale decides this table.', 540, 848);    x.fillStyle='#D9A441'; x.font='700 32px Arial';    x.fillText('moonbite.org/world-cup', 540, 930);    x.fillStyle='rgba(245,242,234,.32)'; x.font='400 19px Arial';    x.fillText('Countries are self-declared by miners. MBITE is not an investment.', 540, 1000);    var a=document.createElement('a');    a.download='moonbite-world-cup-'+code+'.png';    a.href=cv.toDataURL('image/png'); a.click();  });})();</script></body></html>