git / moonbite-network / templates/halving.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 Halving — MoonBite</title>
<meta name="description" content="The MoonBite block reward halves at block {{ '{:,}'.format(consensus.halving_interval) }} and never rises again. Watch the countdown, and the staircase down to nothing.">
<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 { -webkit-tap-highlight-color:transparent; font-family:inherit; }
#sky { position:fixed; inset:0; z-index:0; }
.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; }

/* ----------------------------------------------------------- COUNTDOWN */
.head { text-align:center; padding-top:5vh; }
h1 { font-size:clamp(2.1rem,9vw,3.4rem); font-weight:800; line-height:1.02;
  letter-spacing:-.025em; margin:13px 0 12px; }
h1 .r { color:#D9A441; }   /* accent word: Moongold; red stays functional-only */
.lede { color:#8A8580; font-size:.96rem; line-height:1.62; max-width:420px; margin:0 auto; }

.clock { display:grid; grid-template-columns:repeat(4,1fr); gap:8px; margin-top:26px; }
.cl { background:#161A22; border:1px solid rgba(217,164,65,.22); border-radius:10px;
  padding:15px 4px 11px; }
.cl .v { font-size:clamp(1.5rem,7vw,2.3rem); font-weight:800; color:#D9A441; line-height:1;
  letter-spacing:-.02em; }
.cl .k { font-size:.5rem; letter-spacing:.18em; color:#8A8580; text-transform:uppercase;
  margin-top:7px; font-weight:800; }

.prog { background:#161A22; border:1px solid rgba(245,242,234,.08); border-radius:20px;
  padding:17px 16px; margin-top:14px; }
.prog-top { display:flex; justify-content:space-between; font-size:.62rem; letter-spacing:.16em;
  color:#8A8580; text-transform:uppercase; font-weight:800; }
.prog-top b { color:#D9A441; }
.pbar { height:9px; border-radius:5px; background:rgba(245,242,234,.08); overflow:hidden;
  margin-top:11px; }
.pfill { height:100%; width:0; min-width:3px; border-radius:5px;
  background:linear-gradient(90deg,#D9A441,#B8863B); transition:width 1s ease; }
.prog-b { display:flex; justify-content:space-between; font-size:.68rem; color:#8A8580;
  margin-top:8px; }

/* ---------------------------------------------------------- STAIRCASE */
.stair-card { background:#161A22; border:1px solid rgba(217,164,65,.2); border-radius:20px;
  padding:20px 14px 14px; margin-top:16px; }
.stair-t { font-size:.6rem; letter-spacing:.2em; color:#8A8580; text-transform:uppercase;
  font-weight:800; }
.stair-s { color:#8A8580; font-size:.77rem; margin-top:5px; margin-bottom:12px; }
#stairs { width:100%; height:264px; display:block; }

/* ------------------------------------------------------------- CHANGE */
.swap { display:grid; grid-template-columns:1fr auto 1fr; gap:10px; align-items:center;
  margin-top:16px; }
.sw { background:#161A22; border:1px solid rgba(245,242,234,.09); border-radius:20px;
  padding:18px 12px; text-align:center; }
.sw.now { border-color:rgba(217,164,65,.4); }
.sw .v { font-size:clamp(1.3rem,6vw,1.9rem); font-weight:800; line-height:1; }
.sw.now .v { color:#D9A441; }
.sw.after .v { color:#C8402F; }
.sw .k { font-size:.53rem; letter-spacing:.16em; color:#8A8580; text-transform:uppercase;
  margin-top:7px; font-weight:800; }
.arrow { color:#8A8580; font-size:1.4rem; font-weight:800; }

/* --------------------------------------------------------------- ERAS */
.card { background:#161A22; border:1px solid rgba(245,242,234,.09); border-radius:20px;
  padding:20px 16px; margin-top:16px; }
.card-t { font-size:.6rem; letter-spacing:.2em; color:#8A8580; text-transform:uppercase;
  font-weight:800; margin-bottom:12px; }
table { width:100%; border-collapse:collapse; }
th { text-align:left; font-size:.52rem; letter-spacing:.14em; color:#8A8580; text-transform:uppercase;
  padding:0 5px 10px; font-weight:800; }
th.n, td.n { text-align:right; font-variant-numeric:tabular-nums; }
td { padding:11px 5px; border-top:1px solid rgba(245,242,234,.07); font-size:.86rem; }
tr.cur td { background:rgba(217,164,65,.1); }
tr.cur td:first-child { box-shadow:inset 3px 0 0 #D9A441; }
tr.cur td { color:#F5F2EA; font-weight:800; }
.era { color:#8A8580; width:34px; font-weight:700; }
.rw { color:#D9A441; font-weight:800; }

.actions { display:flex; gap:10px; justify-content:center; flex-wrap:wrap; margin-top:24px; }
.btn { border:none; border-radius:10px; padding:15px 24px; min-height:50px; font-weight:800;
  font-size:.94rem; cursor:pointer; text-decoration:none; display:inline-flex;
  align-items:center; justify-content:center; gap:8px; }
.btn-gold { background:#D9A441; color:#0B0D12; box-shadow:0 12px 36px rgba(217,164,65,.28); }
.btn-ghost { background:transparent; color:#F5F2EA; border:1px solid rgba(245,242,234,.24); }
.fine { font-size:.73rem; color:#8A8580; line-height:1.72; text-align:center; margin-top:22px; }
.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){ .pfill { 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>
<canvas id="sky"></canvas>
{% include 'partials/nav.html' %}

<div class="wrap">
  <section class="head">
    <div class="kicker">THE HALVING</div>
    <h1>{{ consensus.initial_subsidy_coins }} becomes {{ consensus.initial_subsidy_coins // 2 }}.<br>It <span class="r">never</span> comes back.</h1>
    <p class="lede">At block {{ '{:,}'.format(consensus.halving_interval) }} the reward for every
      MoonBite block is cut in half — permanently. Then again. And again, until there is nothing
      left to cut.</p>

    <div class="clock">
      <div class="cl"><div class="v num" id="cD">—</div><div class="k">Days</div></div>
      <div class="cl"><div class="v num" id="cH">—</div><div class="k">Hours</div></div>
      <div class="cl"><div class="v num" id="cM">—</div><div class="k">Minutes</div></div>
      <div class="cl"><div class="v num" id="cS">—</div><div class="k">Seconds</div></div>
    </div>

    <div class="prog">
      <div class="prog-top"><span>ERA <b id="pEra">0</b> PROGRESS</span><span id="pPct">0.00%</span></div>
      <div class="pbar"><div class="pfill" id="pFill"></div></div>
      <div class="prog-b"><span class="num" id="pNow">block 0</span>
        <span class="num">of {{ '{:,}'.format(consensus.halving_interval) }}</span></div>
    </div>
  </section>

  <!-- the staircase -->
  <div class="stair-card">
    <div class="stair-t">THE STAIRCASE DOWN</div>
    <div class="stair-s">Every {{ consensus.halving_years }} years the step halves. There is no
      step back up — the shape of the money supply is decided and cannot be voted on.</div>
    <canvas id="stairs"></canvas>
  </div>

  <!-- what changes -->
  <div class="swap">
    <div class="sw now">
      <div class="v num">{{ consensus.initial_subsidy_coins }}</div>
      <div class="k">MBITE per block now</div>
    </div>
    <div class="arrow">→</div>
    <div class="sw after">
      <div class="v num">{{ consensus.initial_subsidy_coins // 2 }}</div>
      <div class="k">After the halving</div>
    </div>
  </div>
  <div class="swap" style="margin-top:10px;">
    <div class="sw now">
      <div class="v num">{{ '{:,}'.format(consensus.daily_emission_coins) }}</div>
      <div class="k">Mined per day now</div>
    </div>
    <div class="arrow">→</div>
    <div class="sw after">
      <div class="v num">{{ '{:,}'.format(consensus.daily_emission_coins // 2) }}</div>
      <div class="k">Per day after</div>
    </div>
  </div>

  <!-- eras -->
  <div class="card">
    <div class="card-t">EVERY ERA, TO THE END</div>
    <table>
      <thead><tr><th style="width:34px;">ERA</th><th>REWARD</th>
        <th class="n">ENDS</th><th class="n">SUPPLY MINED</th></tr></thead>
      <tbody id="eras"></tbody>
    </table>
    <p class="fine" style="margin-top:14px; text-align:left;">
      The subsidy reaches zero around <b style="color:#D9A441;">{{ consensus.final_block_year }}</b>.
      Dates assume {{ consensus.block_time_min }}-minute blocks and shift with real network timing.</p>
  </div>

  <div class="actions">
    <a class="btn btn-gold" href="/start">Mine at {{ consensus.initial_subsidy_coins }} MBITE</a>
    <button class="btn btn-ghost" id="btnShare">Share the clock</button>
  </div>

  <p class="fine">
    Half of every MBITE that will ever exist is mined in era 0. Whoever is here for it cannot be
    joined later — only remembered.<br>
    MBITE has no market value and is not an investment.
  </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';
  var C = {
    reward: {{ consensus.initial_subsidy_coins }},
    blockSec: {{ consensus.block_time_sec }},
    blocksPerDay: {{ consensus.blocks_per_day }},
    halving: {{ consensus.halving_interval }},
    nextH: {{ consensus.next_halving_height }},
    height: {{ consensus.height or 0 }},
    years: {{ consensus.halving_years }},
    launch: {{ consensus.launch_year }},
    cap: {{ consensus.max_supply_coins }}
  };
  function $(id){ return document.getElementById(id); }
  function fmt(n){ return Math.round(n).toLocaleString('en-US'); }
  /* Doc 11: block heights are never thousands-separated — only amounts are. */
  function ht(n){ return String(Math.round(n||0)); }
  /* The subsidy is an exact integer number of cents, so its decimal always
     terminates (12.5, 6.25, 3.125 ...). Print it exactly — rounding would
     misstate consensus on the very page that explains it. */
  function coin(n){ return n.toLocaleString('en-US',{maximumFractionDigits:8}); }
  function toast(m){ var t=$('toast'); t.textContent=m; t.classList.add('show');
    setTimeout(function(){ t.classList.remove('show'); },2200); }
  function copyText(s){
    try { if(navigator.clipboard && navigator.clipboard.writeText)
      return navigator.clipboard.writeText(s).catch(function(){}); } catch(e){}
    return null;
  }
  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; };
  }

  var reduced=window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  var DPR=Math.min(window.devicePixelRatio||1,2);

  /* --------------------------------------------------------------- sky */
  var sky=$('sky'), sg=sky.getContext('2d'), W,H,stars=[];
  function resize(){
    W=innerWidth; H=innerHeight;
    sky.width=W*DPR; sky.height=H*DPR; sky.style.width=W+'px'; sky.style.height=H+'px';
    sg.setTransform(DPR,0,0,DPR,0,0); stars=[];
    for(var i=0,n=Math.min(140,(W*H/10000)|0); i<n; i++)
      stars.push({x:Math.random()*W,y:Math.random()*H,r:Math.random()*1.1+.25,tw:Math.random()*6.28});
  }
  addEventListener('resize',function(){ resize(); drawStairs(); }); resize();
  var t=0;
  (function loop(){
    t+=reduced?0:.008;
    sg.fillStyle='#0B0D12'; sg.fillRect(0,0,W,H);
    for(var i=0;i<stars.length;i++){ var s=stars[i];
      sg.globalAlpha=.15+.3*Math.abs(Math.sin(t+s.tw)); sg.fillStyle='#F5F2EA';
      sg.beginPath(); sg.arc(s.x,s.y,s.r,0,6.2832); sg.fill(); }
    sg.globalAlpha=1; requestAnimationFrame(loop);
  })();

  /* ------------------------------------------------------------ eras */
  var eras=[], cum=0, r=C.reward, e=0;
  while (r >= 0.00000001 && e < 34){
    var coins = r * C.halving;
    cum += coins;
    eras.push({ i:e, reward:r, endYear: Math.round(C.launch + (e+1)*C.years),
                cum:cum, pct: 100*cum/C.cap });
    r = r/2; e++;
  }
  var curEra = Math.floor(C.height / C.halving);

  $('eras').innerHTML = eras.slice(0,9).map(function(x){
    var rw = coin(x.reward);
    return '<tr class="'+(x.i===curEra?'cur':'')+'">'+
      '<td class="era">'+x.i+'</td>'+
      '<td class="rw">'+rw+' MBITE</td>'+
      '<td class="n">'+x.endYear+'</td>'+
      '<td class="n">'+x.pct.toFixed(1)+'%</td></tr>';
  }).join('') + '<tr><td class="era">…</td><td style="color:#8A8580;">down to zero</td>'+
    '<td class="n" style="color:#8A8580;">'+eras[eras.length-1].endYear+'</td>'+
    '<td class="n" style="color:#8A8580;">100%</td></tr>';

  /* -------------------------------------------------------- staircase */
  var sc=$('stairs'), g=sc.getContext('2d'), grow=0, growRAF=null;
  function drawStairs(){
    var Wc=sc.clientWidth, Hc=264;
    if(sc.width!==Wc*DPR){ sc.width=Wc*DPR; sc.height=Hc*DPR; }
    sc.style.height=Hc+'px';
    g.setTransform(DPR,0,0,DPR,0,0); g.clearRect(0,0,Wc,Hc);

    var N=7, padB=40, padT=22, gap=5;
    var bw=(Wc-gap*(N-1))/N, maxH=Hc-padB-padT;

    for(var i=0;i<N;i++){
      var x=i*(bw+gap);
      var full=maxH*(eras[i].reward/C.reward);
      var h=Math.max(3, full*grow);
      var y=Hc-padB-h;
      var isCur = (i===curEra);
      var grad=g.createLinearGradient(0,y,0,y+h);
      if(isCur){ grad.addColorStop(0,'#E8C878'); grad.addColorStop(1,'#D9A441'); }
      else if(i<curEra){ grad.addColorStop(0,'rgba(138,133,128,.5)'); grad.addColorStop(1,'rgba(138,133,128,.25)'); }
      else { var f=(i-curEra)/N;
        grad.addColorStop(0,'rgba(217,164,65,'+(0.5-0.4*f)+')');
        grad.addColorStop(1,'rgba(217,164,65,'+(0.16-0.12*f)+')'); }
      g.fillStyle=grad;
      g.beginPath(); g.roundRect(x,y,bw,h,4); g.fill();

      /* the drop line — shows the step down to the next era */
      if(i<N-1){
        var nextH2=maxH*(eras[i+1].reward/C.reward)*grow;
        g.strokeStyle='rgba(200,64,47,.42)';
        g.lineWidth=1; g.setLineDash([3,3]);
        g.beginPath();
        g.moveTo(x+bw, y);
        g.lineTo(x+bw+gap, Hc-padB-nextH2);
        g.stroke(); g.setLineDash([]);
      }

      /* reward label */
      g.textAlign='center';
      g.fillStyle = isCur ? '#F5F2EA' : '#8A8580';
      g.font=(isCur?'800 ':'700 ')+'10px system-ui,sans-serif';
      var lbl = coin(eras[i].reward);
      g.fillText(lbl, x+bw/2, y-7);
      /* year */
      g.fillStyle = isCur ? '#D9A441' : 'rgba(138,133,128,.8)';
      g.font='700 9px system-ui,sans-serif';
      g.fillText(eras[i].endYear, x+bw/2, Hc-padB+15);
      if(isCur){ g.fillStyle='#D9A441'; g.font='800 8px system-ui,sans-serif';
        g.fillText('YOU ARE HERE', x+bw/2, Hc-padB+28); }
    }
    g.strokeStyle='rgba(245,242,234,.1)'; g.lineWidth=1;
    g.beginPath(); g.moveTo(0,Hc-padB+.5); g.lineTo(Wc,Hc-padB+.5); g.stroke();
  }
  (function animateStairs(){
    if(reduced){ grow=1; drawStairs(); return; }
    grow=0;
    (function step(){
      grow += (1-grow)*0.1 + 0.006;
      if(grow>=1){ grow=1; drawStairs(); return; }
      drawStairs(); growRAF=requestAnimationFrame(step);
    })();
  })();

  /* --------------------------------------------------------- countdown */
  var height=C.height, t0=Date.now();
  function paint(){
    var left=Math.max(0, C.nextH-height);
    var secs=Math.max(0, left*C.blockSec - (Date.now()-t0)/1000);
    var d=Math.floor(secs/86400), h=Math.floor(secs%86400/3600),
        m=Math.floor(secs%3600/60), s=Math.floor(secs%60);
    $('cD').textContent=fmt(d);
    $('cH').textContent=(h<10?'0':'')+h;
    $('cM').textContent=(m<10?'0':'')+m;
    $('cS').textContent=(s<10?'0':'')+s;
    var into=height-(curEra*C.halving);
    var pct=100*into/C.halving;
    $('pEra').textContent=curEra;
    $('pPct').textContent=pct.toFixed(2)+'%';
    $('pFill').style.width=Math.max(.4,pct)+'%';
    $('pNow').textContent='block '+ht(height);
  }
  paint(); setInterval(paint,1000);

  function pollChain(){
    fetch('/api/blockchain/info').then(function(r){return r.json();}).then(function(j){
      var h=j.height||j.blocks||0;
      if(h>=0 && h!==height){ height=h; t0=Date.now(); paint(); }
    }).catch(function(){});
  }
  pollChain(); setInterval(pollChain,20000);

  /* ------------------------------------------------------------ share */
  function shareText(){
    return 'MoonBite pays '+C.reward+' MBITE a block today. At block '+fmt(C.halving)+
      ' that halves — forever, and it never rises again. '+
      $('cD').textContent+' days left. moonbite.org/halving';
  }
  $('btnShare').addEventListener('click', function(){
    if(navigator.share){ navigator.share({text:shareText()}).catch(function(){}); }
    else { copyText(shareText()); toast('Copied'); }
    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<80;i++){ x.globalAlpha=.12+Math.random()*.45; x.fillStyle='#F5F2EA';
      x.beginPath(); x.arc(Math.random()*1080,Math.random()*1080,Math.random()*1.8+.4,0,6.2832); x.fill(); }
    x.globalAlpha=1; x.textAlign='center';
    x.beginPath(); x.arc(540,150,64,0,6.2832); x.fillStyle='#F5F2EA'; x.fill();
    x.beginPath(); x.arc(575,100,50,0,6.2832); x.fillStyle='#0B0D12'; x.fill();
    x.fillStyle='#D9A441';
    x.beginPath(); x.roundRect(442,128,196,8,4); x.fill();
    x.beginPath(); x.roundRect(448,162,184,8,4); x.fill();
    x.fillStyle='#8A8580'; x.font='700 28px Arial';
    x.fillText('THE MOONBITE HALVING', 540, 268);
    /* mini staircase on the card */
    var bx=250, by=470, bwc=88, gp=10;
    for(var k=0;k<5;k++){
      var hh=170*(eras[k].reward/C.reward);
      x.fillStyle = k===curEra ? '#D9A441' : 'rgba(217,164,65,'+(0.4-0.06*k)+')';
      x.beginPath(); x.roundRect(bx+k*(bwc+gp), by-hh, bwc, hh, 5); x.fill();
      x.fillStyle='rgba(245,242,234,.6)'; x.font='700 19px Arial';
      x.fillText(coin(eras[k].reward),
                 bx+k*(bwc+gp)+bwc/2, by-hh-11);
    }
    x.fillStyle='#C8402F'; x.font='800 76px Arial';
    x.fillText(C.reward+' → '+(C.reward/2), 540, 610);
    x.fillStyle='#F5F2EA'; x.font='400 38px Arial';
    x.fillText('and it never rises again.', 540, 672);
    x.fillStyle='#D9A441'; x.font='800 60px Arial';
    x.fillText($('cD').textContent+' DAYS LEFT', 540, 790);
    x.fillStyle='#8A8580'; x.font='400 30px Arial';
    x.fillText('Half of all MBITE is mined in this era.', 540, 850);
    x.fillStyle='#D9A441'; x.font='700 30px Arial';
    x.fillText('moonbite.org/halving', 540, 936);
    x.fillStyle='rgba(245,242,234,.32)'; x.font='400 19px Arial';
    x.fillText('MBITE has no market value and is not an investment.', 540, 1000);
    var a=document.createElement('a');
    a.download='moonbite-halving.png'; a.href=cv.toDataURL('image/png'); a.click();
  });
})();
</script>
</body>
</html>