Trade the tutorial for a screen that counts the dead

The menus were placeholders next to the art. Three screens now: a title over
an empty, dimmed, blood-stained court; a MATCH SETUP screen where each court
wears meters read straight off its own bounce numbers; and BODY COUNT, which
needed the game to start measuring anything at all.

stats.js keeps one capped array of per-run records — all time, last match and
seven days are three filters over it rather than three counters that can drift
apart. Kills file under the body AND the weapon: killZombie only ever knew an
index, so every damage loop stamps the cause on its way past.

The tutorial is gone. Marvin explained what the in-game modals already teach,
and his stage is where the stats live now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-04 16:01:59 +02:00
1 parent 411bff8724
commit f3b0b791d6
21 files changed
+619 -210

No files matched your search

+211 -100
View File
@@ -63,98 +63,188 @@
#hint { position: fixed; top: 6px; right: 10px; text-align: right;
font: 400 10px Silkscreen, monospace; color: #6b7688; }
@media (pointer: coarse) { #hint { display: none; } }
#intro { position: fixed; inset: 0; z-index: 10; touch-action: none; display: grid; place-items: center;
background: radial-gradient(circle at 50% 34%, rgba(34,16,20,0.92), rgba(8,10,16,0.96)); }
#intro .card { font: 400 11px Silkscreen, monospace; color: #dfe6f2; text-align: center; line-height: 1.9;
padding: 18px 26px; max-width: 92vw; animation: drop 0.5s ease-out; }
/* ---- THE MENU PALETTE. The canvas HUD keeps its own blue-grey and gold; the menus are
deliberately a different room — dark court green, one acid accent, and one crimson reserved for
the thing you actually came here to press. Five names so a repaint is five edits, not fifty. */
:root { --ink:#0d1408; --panel:rgba(30,45,18,0.55); --line:#3e5a26; --acid:#c8e04a;
--cream:#dfe6c9; --dim:#7f9160; --red:#b3201c; --bad:#e0564a;
/* the same green as --panel over --ink, but OPAQUE: the pinned header and footer have
content scrolling underneath them and a translucent bar shows it through */
--solid:#161f0d; }
/* touch-action is pan-y, not none: the BODY COUNT list is taller than a phone and has to scroll.
align-content is `safe center` rather than place-items — plain centring on a grid whose item
overflows pushes the top of the item out of the scrollport, where nothing can reach it. */
#intro { position: fixed; inset: 0; z-index: 10; touch-action: pan-y; background: var(--ink);
display: grid; grid-template-columns: 1fr; justify-items: center;
align-content: safe center; overflow-y: auto; overscroll-behavior: contain; }
/* the SPLASH sits on the court; the other stages are lists and want a flat field behind them.
The art is dimmed here rather than baked dark, so the same file can be reused at another level. */
#intro[data-stage="splash"] {
background: linear-gradient(rgba(9,14,7,0.55), rgba(9,14,7,0.88)),
url('assets/menu_bg.png?v=85') 50% 46% / cover no-repeat var(--ink);
image-rendering: pixelated; }
#intro .card { font: 400 11px Silkscreen, monospace; color: var(--cream); text-align: center;
line-height: 1.9; padding: 16px 14px 20px; width: min(370px, 94vw);
animation: drop 0.5s ease-out; }
@keyframes drop { from { transform: translateY(-14px); opacity: 0; } }
/* the logo is generated pixel art now (assets/logo.png), not stacked CSS text */
#intro .logo { display: block; width: min(320px, 78vw); height: auto; margin: 0 auto 8px;
/* EVERY SCREEN FILLS THE VIEWPORT, and every screen is the same three bands: something pinned to
the top, something pinned to the bottom, and whatever is between them taking the slack. The
card is a full-height flex column, so `margin-top:auto` on the footer drops it to the bottom
when the content is short and `position:sticky` keeps it there once the content is long enough
to scroll. Centring the card instead left the buttons floating in the middle of a tall phone. */
#intro { align-content: stretch; }
#intro .card { display: flex; flex-direction: column; min-height: 100dvh; padding: 0 14px; }
/* the slack band: the title sits in the middle of whatever is left over */
.mid { flex: 1; display: flex; flex-direction: column; justify-content: center; padding: 12px 0; }
/* the logo is generated pixel art (assets/logo.png), not stacked CSS text */
#intro .logo { display: block; width: min(300px, 74vw); height: auto; margin: 0 auto 6px;
image-rendering: pixelated; filter: drop-shadow(0 3px 0 rgba(0,0,0,0.45)); }
/* the slogan carries most of the gap between the logo's blood and the first button */
#intro .slogan { margin: 10px 0 26px; font-size: 12px; color: #b6a0a4; letter-spacing: 0.5px; }
#intro .slogan { margin: 8px 0 20px; font-size: 11px; color: #b6a0a4; letter-spacing: 0.5px; }
#intro .slogan b { color: #c9564a; }
/* which build is served — filled in by main.js, empty (and invisible) if the header is missing */
#build { margin: 10px 0 0; font-size: 9px; color: #5a6270; letter-spacing: 0.5px; }
#build { margin: 14px 0 0; font-size: 9px; color: #4d5a3c; letter-spacing: 0.5px; }
/* ONE overlay, four panels, swapped by #intro[data-stage]. Keeping the backdrop and the card frame
alive across every screen is the point — a screen that unmounts the whole overlay flashes the
court through between menus. */
#splash, #courts, #tutorial { display: none; }
#intro[data-stage="splash"] #splash { display: block; }
#intro[data-stage="courts"] #courts { display: block; animation: drop 0.3s ease-out; }
#intro[data-stage="tutorial"] #tutorial { display: block; animation: drop 0.3s ease-out; }
#splash, #setup, #stats, #ranks { display: none; }
#intro[data-stage="splash"] #splash { display: flex; flex-direction: column; flex: 1; }
/* flex COLUMN, not block: it is what lets the footer's margin-top:auto reach the bottom of a
screen whose content does not fill it */
#intro[data-stage="setup"] #setup,
#intro[data-stage="stats"] #stats,
#intro[data-stage="ranks"] #ranks { display: flex; flex-direction: column; flex: 1;
animation: drop 0.3s ease-out; }
/* the overlay is HIDDEN during play, not removed: a restart puts it straight back, and the
display:grid above would otherwise win over the hidden attribute */
#intro[hidden] { display: none; }
/* headline: the ANNOUNCEMENT face, the same register the canvas uses for SET and CHAPTER — a
screen title is a thing that shouts, not a line of HUD. Bangers carries its own weight, so it
wants size and air rather than Silkscreen's letter-spacing. */
#intro h2 { margin: 0 0 26px; font: 400 30px Bangers, cursive; color: #e0cc72;
letter-spacing: 2px; line-height: 1.2; padding-bottom: 6px; }
/* Screen titles and the primary button are BANGERS — the same announcement face the canvas uses
for SET and CHAPTER. A screen title is a thing that shouts. Everything that is a NUMBER or a
label stays Silkscreen, so the two registers keep meaning what they have always meant. */
#intro h2 { margin: 0; font: 400 26px Bangers, cursive; color: var(--acid); letter-spacing: 1.5px; }
#intro h3 { display: flex; justify-content: space-between; margin: 18px 0 8px;
font: 400 9px Silkscreen, monospace; color: var(--dim); letter-spacing: 2px;
text-align: left; }
/* header strip: back chevron + title. Every screen but the splash wears one, pinned to the top. */
.hdr { position: sticky; top: 0; z-index: 3; display: flex; align-items: center; gap: 10px;
padding: 10px 12px; margin: 0 -14px; border-bottom: 2px solid var(--line);
background: var(--solid);
padding-top: max(10px, env(safe-area-inset-top)); }
.hdr h2 { padding-left: 2px; }
.chev { width: 34px; height: 34px; flex: none; background: none; border: 2px solid var(--line);
color: var(--acid); font: 400 20px Bangers, cursive; line-height: 1; cursor: pointer;
-webkit-tap-highlight-color: transparent; }
/* the splash stat bar and the BODY COUNT headline row: boxed figures sharing one frame */
.bar, .figs { display: flex; border: 2px solid var(--line); background: var(--panel); }
/* the bar is the splash's top band — pinned like a header, and the safe-area inset is its job
because nothing else on that screen sits above it */
.bar { flex: none; margin-top: max(12px, env(safe-area-inset-top)); }
.bar .fig { padding: 6px 10px; text-align: left; }
.figs .fig { padding: 7px 6px; text-align: center; }
.fig { flex: 1; min-width: 0; }
.bar .fig + .fig { text-align: right; }
.fig + .fig { border-left: 2px solid var(--line); }
.fig b { display: block; font: 400 8px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px; }
.fig i { display: block; font: 700 15px Silkscreen, monospace; color: var(--acid);
font-style: normal; letter-spacing: 1px; }
.fig.bad i { color: var(--bad); }
/* one button look for every menu screen */
.btn { display: block; width: min(340px, 84vw); margin: 0 auto 10px; padding: 12px 10px;
background: rgba(255,255,255,0.04); border: 2px solid #3b4354; color: #dfe6f2;
.btn { display: block; width: 100%; margin: 0 auto 10px; padding: 12px 10px;
background: var(--panel); border: 2px solid var(--line); color: var(--cream);
font: 400 12px Silkscreen, monospace; letter-spacing: 1px; cursor: pointer;
-webkit-tap-highlight-color: transparent; }
.btn.go { border-color: #e0cc72; color: #e0cc72; }
/* The two menu buttons wear generated plates. The LETTERING stays live text on top rather than
being drawn into the art: generated type is a lottery and would not match the rest of the HUD.
A text-shadow is what keeps it readable where a drip runs behind it. The border is CSS, not
art — the plate crops are interior-only, so both buttons get the same clean frame. */
.btn.plate, .btn.plate2 { padding: 15px 10px; font-size: 15px; letter-spacing: 2px;
background-size: 100% 100%; background-repeat: no-repeat;
image-rendering: pixelated; text-shadow: 0 2px 0 rgba(0,0,0,0.9); }
/* No gold frame on START any more — the sweep below is what draws the eye, and the two buttons
now share one border so the pair reads as a pair. */
.btn.plate { background-image: url('assets/btn_start.png?v=94'); color: #f7ecb8;
border-color: #3b4354; position: relative; overflow: hidden; }
/* a light passing over the sign: across in ~0.8s, then still for the rest of the cycle.
pointer-events:none so the sweep can never eat the tap. */
.btn.plate::after { content: ''; position: absolute; inset: 0; pointer-events: none;
background: linear-gradient(105deg, transparent 42%, rgba(255,255,255,0.34) 50%,
transparent 58%);
transform: translateX(-140%); animation: shine 4.5s ease-in-out infinite; }
@keyframes shine { 0% { transform: translateX(-140%); } 18%, 100% { transform: translateX(140%); } }
@media (prefers-reduced-motion: reduce) { .btn.plate::after { animation: none; opacity: 0; } }
.btn.plate2 { background-image: url('assets/btn_tut.png?v=94'); color: #d6dfec; }
.btn.back { font-size: 10px; padding: 9px 10px; color: #8a96ab; }
/* The generated button plates are gone with the tutorial. The primary is a flat crimson block
now: it survives being resized, which a fixed-size plate crop never did, and the mockup's
hierarchy is carried by colour rather than by art. */
.btn.big { padding: 13px 10px 11px; background: var(--red); border-color: #6d1310; color: #f6e7dc;
font: 400 26px Bangers, cursive; letter-spacing: 2px; line-height: 1.15; }
.two { display: flex; gap: 8px; }
.two .btn { margin-bottom: 0; }
.btn.sec i { display: block; margin-top: 3px; font: 400 8px Silkscreen, monospace;
font-style: normal; color: var(--dim); letter-spacing: 0.5px; }
/* the tour: two tiles SIDE BY SIDE, not two more full-width rows. The figures face the camera —
in play you only ever see your player from behind, so a rear view would show nothing. */
#tours { display: flex; gap: 8px; }
#tours button { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px;
padding: 8px 6px 10px; background: var(--panel); border: 2px solid var(--line);
color: inherit; font: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; }
#tours button.on { border-color: var(--acid); background: rgba(200,224,74,0.10); }
#tours img { height: 88px; width: auto; display: block; image-rendering: pixelated; }
#tours b { font: 400 10px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px; }
/* a row, not a stacked tile: three 3:2 tiles stacked fill a phone twice over, and the art still
reads at this size. -webkit-tap-highlight-color kills iOS's blue flash over the pixel art. */
#courts button.court { display: flex; align-items: center; gap: 12px; width: min(340px, 84vw);
margin: 0 auto 8px; padding: 6px; background: rgba(255,255,255,0.04);
border: 2px solid #3b4354; color: inherit; font: inherit; text-align: left;
#courtList button.court { display: flex; align-items: center; gap: 11px; width: 100%;
margin: 0 0 8px; padding: 7px; background: var(--panel);
border: 2px solid var(--line); color: inherit; font: inherit; text-align: left;
cursor: pointer; -webkit-tap-highlight-color: transparent; }
#courts button.court.on { border-color: #e0cc72; background: rgba(224,204,114,0.10); }
/* scoped to #courtList, not #courts: the tour tiles above are portrait figures and must not
inherit the 108x72 court-thumbnail box */
#courtList img { width: 108px; height: 72px; display: block; image-rendering: pixelated; flex: none; }
#courtList b { display: block; font: 400 11px Silkscreen, monospace; color: #dfe6f2; letter-spacing: 0.5px; }
#courtList i { display: block; margin-top: 5px; font: 400 9px Silkscreen, monospace; color: #8a96ab; font-style: normal; }
#courts .acts { margin-top: 16px; }
/* the tour: two tiles SIDE BY SIDE, not two more full-width rows. The court screen already carries
three rows and a pair of buttons, and a phone has no room left. The figures face the camera —
in play you only ever see your player from behind, so a rear view would show nothing. */
#tours { display: flex; gap: 8px; width: min(340px, 84vw); margin: 0 auto 10px; }
#tours button { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px;
padding: 8px 6px 10px; background: rgba(255,255,255,0.04); border: 2px solid #3b4354;
color: inherit; font: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; }
#tours button.on { border-color: #e0cc72; background: rgba(224,204,114,0.10); }
#tours img { height: 92px; width: auto; display: block; image-rendering: pixelated; }
#tours b { font: 400 10px Silkscreen, monospace; color: #dfe6f2; letter-spacing: 0.5px; }
/* tutorial: Marvin on the left, his line and the picture on the right */
#tutorial .page { display: flex; align-items: flex-end; gap: 14px; width: min(360px, 88vw);
margin: 0 auto; text-align: left; min-height: 190px; }
#tutorial .marvin { width: 84px; flex: none; image-rendering: pixelated; align-self: flex-end; }
#tutorial .say { flex: 1; }
#tutorial .say p { margin: 0 0 10px; font: 400 10px Silkscreen, monospace; color: #dfe6f2; line-height: 1.9; }
#tutorial .say em { color: #e0cc72; font-style: normal; }
/* the panel is 2:1 and it is the point of the page, so it gets the full card width above the
Marvin row rather than being squeezed into the text column beside him */
#tutorial .shot { display: block; width: min(360px, 88vw); margin: 0 auto 12px;
image-rendering: pixelated; border: 2px solid #2a3140; }
#tutorial .dots { margin: 14px 0 10px; font: 400 10px Silkscreen, monospace; color: #5a6270; letter-spacing: 3px; }
#tutorial .acts { display: flex; gap: 8px; width: min(340px, 84vw); margin: 0 auto; }
#tutorial .acts .btn { margin: 0; flex: 1; }
#courtList button.court.on { border-color: var(--acid); background: rgba(200,224,74,0.10); }
#courtList img { width: 92px; height: 66px; display: block; image-rendering: pixelated; flex: none;
object-fit: cover; }
#courtList .cb { flex: 1; min-width: 0; }
#courtList b { display: block; font: 400 11px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px; }
#courtList i { display: block; margin: 2px 0 5px; font: 400 8px Silkscreen, monospace; color: var(--dim); font-style: normal; }
/* The two meters read straight off the court's own bounce numbers (see main.js), so they cannot
drift away from what the ball actually does on it. */
.meter { display: flex; align-items: center; gap: 6px; margin-top: 3px;
font: 400 7px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px; }
.meter u { flex: none; width: 40px; text-decoration: none; }
.meter s { flex: 1; height: 5px; background: rgba(0,0,0,0.45); text-decoration: none; display: block; }
.meter s > i { display: block; height: 100%; background: var(--acid); }
.meter.b s > i { background: #5aa9d0; }
/* the commit strip. #intro is the scrollport, so sticky resolves against it and the button stays
reachable however long the list above grows. */
/* margin-top:auto pins it to the BOTTOM of a short screen; sticky keeps it there on a long one */
.foot { position: sticky; bottom: 0; z-index: 2; margin: auto -14px 0; padding: 10px 14px;
padding-bottom: max(10px, env(safe-area-inset-bottom));
border-top: 2px solid var(--line); background: var(--solid); }
.foot .btn { margin-bottom: 0; }
/* the splash's bottom band: no bar, no rule — the court art has to keep running under it */
.foot.bare { border: 0; background: none; }
.foot.bare .btn.big { margin-bottom: 10px; }
#sSum { display: flex; justify-content: space-between; gap: 8px; margin: 0 0 8px;
font: 400 8px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px; }
.tabs { display: flex; gap: 6px; margin: 12px 0 14px; }
.tabs button { flex: 1; padding: 9px 2px; background: var(--panel); border: 2px solid var(--line);
color: var(--dim); font: 400 8px Silkscreen, monospace; letter-spacing: 1px;
cursor: pointer; -webkit-tap-highlight-color: transparent; }
.tabs button.on { border-color: var(--acid); color: var(--acid); }
/* one opponent. The sprite is the walk frame the game already draws, sat on its own baseline. */
.zrow { display: flex; align-items: center; gap: 10px; padding: 7px 9px; margin-bottom: 6px;
border: 2px solid var(--line); background: var(--panel); text-align: left; }
.zrow.boss { border-color: #7d2420; background: rgba(120,26,22,0.28); }
.zrow img { width: 34px; height: 46px; object-fit: contain; object-position: bottom;
image-rendering: pixelated; flex: none; }
.zb { flex: 1; min-width: 0; display: grid; grid-template-columns: 1fr auto; gap: 0 8px;
line-height: 1.4; }
.zb b { font: 400 10px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.zb > span { font: 700 12px Silkscreen, monospace; color: var(--acid); text-align: right; }
.zrow.boss .zb > span { color: var(--bad); }
.pb { grid-column: 1 / -1; height: 5px; margin-top: 4px; background: rgba(0,0,0,0.45); }
.pb s { display: block; height: 100%; background: var(--acid); text-decoration: none; }
.zrow.boss .pb s { background: var(--bad); }
.zb i { grid-column: 1 / -1; margin-top: 3px; font: 400 7px Silkscreen, monospace;
font-style: normal; color: var(--dim); letter-spacing: 1px; }
#stW { display: flex; flex-wrap: wrap; gap: 6px; }
.wtile { flex: 1 1 54px; max-width: 76px; padding: 6px 2px; border: 2px solid var(--line);
background: var(--panel); }
.wtile img { width: 26px; height: 26px; object-fit: contain; display: block; margin: 0 auto 2px;
image-rendering: pixelated; }
.wtile b { display: block; font: 400 9px Silkscreen, monospace; color: var(--acid); }
.none { margin: 4px 0 0; font: 400 8px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px;
text-align: left; }
#stFoot { display: flex; justify-content: space-between; align-items: baseline;
border-top: 2px solid var(--line); }
#stFoot b { font: 400 8px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px; }
#stFoot i { font: 700 13px Silkscreen, monospace; color: var(--acid); font-style: normal; }
.rkrow { display: flex; align-items: center; gap: 10px; padding: 8px 10px; margin-bottom: 5px;
border: 2px solid var(--line); background: var(--panel); text-align: left; }
.rkrow.me { border-color: var(--acid); background: rgba(200,224,74,0.12); }
.rkrow b { flex: none; width: 60px; font: 700 10px Silkscreen, monospace; color: var(--dim); }
.rkrow.me b { color: var(--acid); }
.rkrow span { flex: 1; min-width: 0; font: 400 9px Silkscreen, monospace; color: var(--cream);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rkrow i { font: 400 9px Silkscreen, monospace; font-style: normal; color: var(--dim); }
</style>
</head>
<body>
@@ -164,42 +254,63 @@
<div id="intro" data-stage="splash">
<div class="card">
<!-- The title screen IS the menu. A separate menu stage behind it was the same logo over the
same backdrop with two buttons, so it read as the title screen failing to advance. The
control list that used to sit here is the tutorial's job now. -->
same backdrop with two buttons, so it read as the title screen failing to advance. -->
<!-- three bands: the figures at the top, the title floating in the middle, the buttons under
your thumb. Same anchoring as every other screen. -->
<div id="splash">
<img class="logo" src="assets/logo.png" alt="Splatter Tennis">
<p class="slogan">Fifteen. Thirty. Forty. <b>Dead.</b></p>
<button class="btn plate" id="mStart">START GAME</button>
<button class="btn plate2" id="mTut">TUTORIAL</button>
<p id="build"></p>
<div class="bar">
<div class="fig"><b>BEST RUN</b><i id="sBest">—</i></div>
<div class="fig"><b>WORLD RANK</b><i id="sRank">—</i></div>
</div>
<div class="mid">
<img class="logo" src="assets/logo.png" alt="Splatter Tennis">
<p class="slogan">Fifteen. Thirty. Forty. <b>Dead.</b></p>
</div>
<div class="foot bare">
<button class="btn big" id="mStart">PLAY MATCH</button>
<div class="two">
<button class="btn sec" id="mStats">STATS<i id="mKills">0 KILLS</i></button>
<button class="btn sec" id="mRanks">RANKINGS<i>SEASON 1</i></button>
</div>
<p id="build"></p>
</div>
</div>
<!-- tour tiles and court rows are both built from tables in main.js (config.TOURS / config.COURTS):
adding either is a table row, never markup -->
<div id="courts">
<h2>CHOOSE YOUR MATCH</h2>
<div id="setup">
<div class="hdr"><button class="chev" id="sBack">&lsaquo;</button><h2>MATCH SETUP</h2></div>
<h3>01 · PLAYER</h3>
<div id="tours"></div>
<h3>02 · COURT</h3>
<div id="courtList"></div>
<div class="acts">
<button class="btn go" id="cStart">START GAME</button>
<button class="btn back" id="cBack">BACK</button>
<div class="foot">
<p id="sSum"></p>
<button class="btn big" id="cStart">SERVE UP</button>
</div>
</div>
<!-- tutorial pages are built from a table in main.js -->
<div id="tutorial">
<h2>HOW IT WORKS</h2>
<img class="shot" id="tShot" alt="">
<div class="page">
<img class="marvin" id="tMarvin" src="assets/marvin1.png" alt="">
<div class="say" id="tSay"></div>
</div>
<p class="dots" id="tDots"></p>
<div class="acts">
<button class="btn back" id="tBack">BACK</button>
<button class="btn go" id="tNext">NEXT</button>
<!-- BODY COUNT. Every row in here is built by stats.js from the run records in localStorage. -->
<div id="stats">
<div class="hdr"><button class="chev" id="stBack">&lsaquo;</button><h2>BODY COUNT</h2></div>
<div class="tabs" id="stTabs">
<button class="on" data-p="all">ALL TIME</button>
<button data-p="last">LAST MATCH</button>
<button data-p="7d">7 DAYS</button>
</div>
<div class="figs" id="stFigs"></div>
<h3>BY OPPONENT<span id="stPeriod"></span></h3>
<div id="stZ"></div>
<h3>BY WEAPON</h3>
<div id="stW"></div>
<div class="foot" id="stFoot"></div>
</div>
<!-- placeholder ladder until the leaderboard task lands — see stats.drawRanks -->
<div id="ranks">
<div class="hdr"><button class="chev" id="rkBack">&lsaquo;</button><h2>RANKINGS</h2></div>
<h3>SEASON 1<span>UNRANKED · OFFLINE</span></h3>
<div id="rkList"></div>
</div>
</div>
</div>
<script type="module" src="main.js?v=121"></script>
<script type="module" src="main.js?v=122"></script>
</body>
</html>