A chimney of rising embers on a trophy read as a fire. Three points ON the shield now, dark for most of a long cycle and flaring for a fraction of it, with delays and durations that share no common multiple — so a glint lands somewhere on the row every couple of seconds and never in the same rhythm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1083 lines
78 KiB
HTML
1083 lines
78 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Splatter Tennis</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<!-- save-to-homescreen: a bloody tennis ball. iOS needs the apple-* tags (it ignores the
|
|
manifest's display/name); Android reads the manifest. -->
|
|
<link rel="icon" type="image/png" href="assets/favicon.png">
|
|
<link rel="apple-touch-icon" href="assets/icon-180.png">
|
|
<link rel="manifest" href="manifest.webmanifest">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-title" content="Splatter">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<meta name="theme-color" content="#2b2735">
|
|
<!-- STAGING WEARS A DIFFERENT FACE. Both builds get added to the same phone's homescreen, and two
|
|
identical bloody tennis balls is a coin toss every time you open one. Driven by HOSTNAME, not by
|
|
the branch: this block is identical on staging and on main, so it never conflicts on a merge and
|
|
never has to be remembered. Prod is the one named host; everything else (staging, localhost, an
|
|
IP) is a test build and says so. Runs in <head>, before the install prompt can read any of it. -->
|
|
<script>
|
|
// (`Capacitor`: the SHELL is a prod build whatever its origin says — inside the app the hostname is
|
|
// `localhost`, which would otherwise put the staging face on the shipped game.)
|
|
if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNativePlatform?.()) {
|
|
for (const [sel, href] of [['link[rel=icon]', 'assets/favicon-stg.png'],
|
|
['link[rel=apple-touch-icon]', 'assets/icon-180-stg.png'],
|
|
['link[rel=manifest]', 'manifest-stg.webmanifest']])
|
|
document.querySelector(sel).setAttribute('href', href);
|
|
document.querySelector('meta[name=apple-mobile-web-app-title]').setAttribute('content', 'Splatter STG');
|
|
document.querySelector('meta[name=theme-color]').setAttribute('content', '#14334a');
|
|
document.title = 'Splatter Tennis — STAGING';
|
|
}
|
|
</script>
|
|
<style>
|
|
/* pixel HUD font — bundled so it renders offline; used by the canvas scoreboard + rack.
|
|
?v= like the sprites: server.py caches a VERSIONED asset for a year and an unversioned one for
|
|
a day, so these three files are fetched once per release instead of revalidated on every cold
|
|
start. Bump alongside SPRV in render.js if a font file is ever replaced. */
|
|
@font-face { font-family: 'Silkscreen'; font-weight: 400; font-display: block;
|
|
src: url('assets/fonts/Silkscreen-Regular.ttf?v=86') format('truetype'); }
|
|
@font-face { font-family: 'Silkscreen'; font-weight: 700; font-display: block;
|
|
src: url('assets/fonts/Silkscreen-Bold.ttf?v=86') format('truetype'); }
|
|
/* the ANNOUNCEMENT face. Silkscreen is the HUD and the instructions — small, even, quiet. The
|
|
things that shout (SET, CHAPTER, EPIC!, PERFECT) are a second register on purpose, so a message
|
|
tells you which kind it is before you have read a word of it. Bundled, like the other one. */
|
|
@font-face { font-family: 'Bangers'; font-weight: 400; font-display: block;
|
|
src: url('assets/fonts/Bangers-Regular.ttf?v=86') format('truetype'); }
|
|
/* ...and the UPRIGHT display face (2026-08-31). Bangers is drawn slanted — there is no upright
|
|
cut of it, which is also why the chevrons are Silkscreen — so everything that NAMES rather
|
|
than announces needed a third voice. Bungee held it for an afternoon; JERSEY 15 took it in
|
|
the menu-text-reduction handoff, because it sits on the same pixel grid as Silkscreen (it
|
|
reads as one family with the sprite art) and runs proportionally lighter, where Bungee was
|
|
single-weight and blocky. SIL OFL, licence beside it, SUBSET to the characters this game
|
|
actually sets — 9 KB; the pyftsubset command is in PROJECT.md. Sizes run ~+40% against the
|
|
Bungee values: Jersey sits lower in its box. */
|
|
@font-face { font-family: 'Jersey 15'; font-weight: 400; font-display: block;
|
|
src: url('assets/fonts/Jersey15-Subset.ttf?v=87') format('truetype'); }
|
|
/* html needs a real HEIGHT: the root element's background paints the viewport, but a GRADIENT is
|
|
laid out in the root's own box — and with the canvas position:fixed the root has no content and
|
|
collapses, so the gradient render.js sets had nowhere to go. */
|
|
html { height: 100%; height: 100dvh; }
|
|
html, body { margin: 0; padding: 0; background: #10141c; overflow: hidden; }
|
|
/* invisible probe: env() is only readable from CSS, so park the insets on an element and let
|
|
render.js measure them. The canvas stays FULL-BLEED (the stands under the notch look good) —
|
|
it is the HUD that has to move, see SAFE in render.js. */
|
|
#safe { position: fixed; top: 0; left: 0; width: 0; height: 0; pointer-events: none; visibility: hidden;
|
|
padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px); }
|
|
/* The canvas STRETCHES to the viewport and render.js derives its logical height from the box it
|
|
actually got. Sizing it the other way round (compute a scale from innerWidth/innerHeight, then set
|
|
explicit px) meant it was laid out against whatever innerHeight reported at load — before iOS
|
|
Safari's toolbars had settled — and it stayed a strip short of the bottom forever after.
|
|
100dvh is the dynamic viewport height, so it tracks those toolbars. */
|
|
/* TOP inset only. The status bar is opaque UI and the scoreboard must clear it, so the canvas starts
|
|
below it — but the home indicator is a slim translucent bar, so the canvas runs all the way to the
|
|
PHYSICAL bottom edge and the court art fills it. Only the weapon RACK lifts clear of the indicator,
|
|
via SAFE.bottom in render.js. Reserving the bottom inset here instead just left a dead band.
|
|
100dvh is the dynamic viewport height, so it tracks iOS Safari's toolbars appearing/hiding. */
|
|
canvas { image-rendering: pixelated; display: block; position: fixed; touch-action: none;
|
|
top: env(safe-area-inset-top, 0px);
|
|
left: env(safe-area-inset-left, 0px);
|
|
width: calc(100vw - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px));
|
|
height: calc(100vh - env(safe-area-inset-top, 0px));
|
|
height: calc(100dvh - env(safe-area-inset-top, 0px)); }
|
|
/* ...but NOT installed. Launched from the homescreen, WebKit does not initialise the DYNAMIC
|
|
viewport until the geometry is "exercised" — 100dvh stays provisional until you rotate the
|
|
phone, which is exactly the bug. Standalone has no toolbars, so 100vh is both correct and
|
|
stable from the first frame; dvh only exists for the browser's disappearing toolbars. */
|
|
@media (display-mode: standalone) {
|
|
html { height: 100vh; }
|
|
canvas { height: calc(100vh - env(safe-area-inset-top, 0px)); }
|
|
}
|
|
/* desktop key hint — top-right, shifted left of the quit chip that owns the corner now */
|
|
#hint { position: fixed; top: 6px; right: 52px; text-align: right;
|
|
font: 400 10px Silkscreen, monospace; color: #6b7688; }
|
|
/* THE DOOR OUT (Oliver: every tennis screen lacked an exit — force-closing the app was the only
|
|
way back to the menu). A quiet chip during play; tapping it freezes the world and asks. */
|
|
/* THE CORNER IS THE DOOR. It used to hang a row below, under the BLOOD strip, which is where a
|
|
thumb never looks for an exit — the BLOOD panel steps aside for it instead (drawBlood in
|
|
render.js reserves this chip's width). Same recessed-panel language as the canvas HUD:
|
|
dark green field, a hairline of the board's own highlight, a 1px inner bevel — and it goes
|
|
crimson while pressed, because red is reserved for the thing you actually came to press. */
|
|
/* --hud is the canvas HUD's own 6-logical-px margin, converted to CSS px by render.js, so the
|
|
chip's top edge lands on the same line as the scoreboard's and the BLOOD panel's. */
|
|
#quit { position: fixed; z-index: 5;
|
|
top: calc(env(safe-area-inset-top, 0px) + var(--hud, 8px));
|
|
right: calc(env(safe-area-inset-right, 0px) + var(--hud, 8px)); }
|
|
/* ...and it is the BLOOD panel's own height: that panel is 24 logical px, which is four --hud. */
|
|
#quit button { width: calc(var(--hud, 8px) * 4); height: calc(var(--hud, 8px) * 4);
|
|
border-radius: 3px; display: grid; place-items: center;
|
|
background: rgba(8,26,18,0.82); border: 1px solid rgba(63,149,96,0.35);
|
|
/* the same drop shadow the canvas HUD carries (render.hudShadow): the chip sits on
|
|
the crowd like the scoreboard does, and half the overlay lifting off the
|
|
backdrop while the other half does not is worse than neither. */
|
|
box-shadow: inset 0 1px 0 rgba(242,234,214,0.10), 3px 3px 3px rgba(0,0,0,0.68);
|
|
/* a plain X, not ✕: Silkscreen has no U+2715, so the dingbat fell back to the
|
|
system font and drew a smooth glyph inside a pixel HUD */
|
|
color: var(--cream); font: 700 14px Silkscreen, monospace; line-height: 1;
|
|
cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
|
#quit button:active { background: var(--red); border-color: var(--bad); color: #f2ead6; }
|
|
#qAsk { position: fixed; inset: 0; z-index: 6; background: rgba(0,0,0,0.55);
|
|
display: flex; align-items: center; justify-content: center; }
|
|
#quit[hidden], #qAsk[hidden] { display: none; }
|
|
.qBox { background: var(--solid); border: 2px solid var(--line); padding: 18px 16px;
|
|
width: min(320px, 84vw); display: flex; flex-direction: column; gap: 10px; }
|
|
/* it is a QUESTION, and a question calls — the one Bangers line on this overlay */
|
|
.qBox p { margin: 0 0 10px; font: 400 20px Bangers, cursive; color: var(--cream);
|
|
text-align: center; letter-spacing: 1px; }
|
|
@media (pointer: coarse) { #hint { display: none; } }
|
|
/* ...and it is UI like any other: gone while the splash is still loading (the keyboard hints
|
|
live outside #intro, so this reaches them through the ancestor) */
|
|
body:has(#splash.busy) #hint { display: none; }
|
|
/* ---- 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. */
|
|
/* COOL, not olive. The first pass sat around hue 80 — yellow-green — and read loud and warm next
|
|
to the art. These are the same idea rotated towards blue-green and desaturated: the field is
|
|
nearly neutral, the borders are quiet, and the acid accent is the only thing allowed to be
|
|
bright, so it still means "this is the number / this is selected". */
|
|
:root { --ink:#0e1714; --panel:rgba(42,62,54,0.42); --line:#2c4238; --acid:#bcd455;
|
|
--cream:#ccd7ca; --dim:#6c7f74; --red:#a4231f; --bad:#cf5a4e;
|
|
/* 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:#16211d; }
|
|
/* 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"] {
|
|
/* Three stops, not two. The logo needs a dark, quiet band behind it and the blood needs to be
|
|
READABLE, and those pull opposite ways — so the scrim is heaviest at the top, lifts over the
|
|
court where the stain is, and comes back down only far enough to seat the buttons. */
|
|
background: linear-gradient(rgba(12,20,17,0.60) 0%, rgba(12,20,17,0.30) 46%,
|
|
rgba(12,20,17,0.58) 100%),
|
|
url('assets/menu_bg.png?v=86') 50% 42% / cover no-repeat var(--ink);
|
|
image-rendering: pixelated; }
|
|
/* THE BACK-ROOM HOTSPOT: five taps here open it (main.js). Top-left CORNER, not the logo and not
|
|
the screen — a hidden door has to be somewhere you do not put your thumb by accident, and the
|
|
whole splash was reachable by every stray tap on the way to PLAY MATCH. Invisible on purpose,
|
|
and a child of #splash so it exists on no other stage. */
|
|
#hot { position: fixed; top: 0; left: 0; width: 26vw; max-width: 150px; height: 15vh;
|
|
max-height: 130px; z-index: 4; }
|
|
#intro .card { font: 400 12px Silkscreen, monospace; color: var(--cream); text-align: center;
|
|
line-height: 1.9; padding: 16px 14px 20px; width: min(370px, 100vw - 28px); /* CONTENT width (content-box) beside 14px+14px padding:
|
|
94vw put the card 5-28px past the viewport on every phone (0.94w+28 > w below
|
|
466px) and #intro's overflow-y:auto computes overflow-x to auto — that was the
|
|
sliver of horizontal scroll Oliver caught on the splash */
|
|
animation: drop 0.5s ease-out; }
|
|
@keyframes drop { from { transform: translateY(-14px); opacity: 0; } }
|
|
/* 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: 8px 0 20px; font-size: 11px; color: #a8a09e; 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: 14px 0 0; font-size: 9px; color: #4a5a52; 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, #setup, #stats, #ranks, #newbest, #secret, #bosses, #stmode, #stsub, #vs, #train, #player, #promo, #card { 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="mode"] #stmode,
|
|
#intro[data-stage="modesub"] #stsub,
|
|
#intro[data-stage="stats"] #stats,
|
|
#intro[data-stage="ranks"] #ranks,
|
|
#intro[data-stage="secret"] #secret,
|
|
#intro[data-stage="bosses"] #bosses,
|
|
#intro[data-stage="train"] #train,
|
|
#intro[data-stage="vs"] #vs,
|
|
#intro[data-stage="player"] #player,
|
|
#intro[data-stage="promo"] #promo,
|
|
#intro[data-stage="card"] #card,
|
|
#intro[data-stage="newbest"] #newbest { display: flex; flex-direction: column; flex: 1; }
|
|
/* ...and every screen DEALS ITS BANDS IN (2026-08-31, Oliver: "wäre natürlich auch toll für die
|
|
untermenüs"). The panel used to drop in as one slab; now its header, its content and its
|
|
footer arrive one after the other on the same overshoot the splash and the canvas modals use.
|
|
The children of the stage, not the stage itself — a slab that drops says "a screen appeared",
|
|
a deal says "the machine is laying this out for you". The splash keeps its own finer stagger
|
|
below (its two bands would otherwise fly as two lumps), hence the :not. */
|
|
#intro.deal > .card > div:not(#splash) > * {
|
|
animation: dealIn 0.28s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; }
|
|
#intro.deal > .card > div:not(#splash) > *:nth-child(1) { animation-delay: 0.02s; }
|
|
#intro.deal > .card > div:not(#splash) > *:nth-child(2) { animation-delay: 0.09s; }
|
|
#intro.deal > .card > div:not(#splash) > *:nth-child(3) { animation-delay: 0.16s; }
|
|
#intro.deal > .card > div:not(#splash) > *:nth-child(4) { animation-delay: 0.23s; }
|
|
#intro.deal > .card > div:not(#splash) > *:nth-child(n+5) { animation-delay: 0.30s; }
|
|
/* the poster wall deals its ROOMS, not one lump: the list is the whole screen there, so letting
|
|
it arrive as a single band wastes the entrance. The list itself stops animating — a moving
|
|
child inside a moving parent starts at twice the offset and reads as a stutter. */
|
|
#intro.deal #stmode > #modeList { animation: none; }
|
|
#intro.deal #modeList > .mgroup {
|
|
animation: dealIn 0.28s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; }
|
|
#intro.deal #modeList > .mgroup:nth-child(1) { animation-delay: 0.09s; }
|
|
#intro.deal #modeList > .mgroup:nth-child(2) { animation-delay: 0.17s; }
|
|
#intro.deal #modeList > .mgroup:nth-child(3) { animation-delay: 0.25s; }
|
|
@media (prefers-reduced-motion: reduce) {
|
|
#intro.deal > .card > div:not(#splash) > *,
|
|
#intro.deal #modeList > .mgroup { animation: none; } }
|
|
/* 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; }
|
|
/* THREE REGISTERS NOW (2026-08-31). Bangers still ANNOUNCES — the primary button, PROMOTED.,
|
|
NEW PERSONAL BEST, and everything the canvas shouts (SET, CHAPTER, EPIC!). Silkscreen is
|
|
still the HUD: numbers, labels, anything you read at leisure. And the SCREEN TITLES moved to
|
|
Jersey 15: upright signage, which is what the strip at the top of a screen actually is — you
|
|
are not being shouted at, you are being told which room you are in. */
|
|
#intro h2 { margin: 0; font: 400 26px Bangers, cursive; color: var(--acid); letter-spacing: 1.5px; }
|
|
/* #intro h2 above carries an id, so this needs one too or the announcement face wins */
|
|
#intro .hdr h2 { font: 400 33px/1 'Jersey 15', Bangers, cursive; letter-spacing: 1px; }
|
|
/* A RUBRIC IS ONE WORD (2026-08-31, the text reduction), in the face that names rooms — and any
|
|
hint it carries is a chip on the right, not a second sentence. */
|
|
#intro h3 span { font: 400 8px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px; }
|
|
#intro h3 { display: flex; justify-content: space-between; align-items: baseline; margin: 20px 0 9px;
|
|
font: 400 17px/1.2 'Jersey 15', Silkscreen, monospace; color: var(--acid);
|
|
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; } /* (its face is set with the other titles above) */
|
|
/* UPRIGHT, not Bangers: the announcement face is drawn slanted, and a slanted arrow read as
|
|
italic on every screen. Silkscreen's ASCII < > are straight pixel arrows. */
|
|
.chev { width: 34px; height: 34px; flex: none; background: none; border: 2px solid var(--line);
|
|
color: var(--acid); font: 700 14px Silkscreen, monospace; line-height: 1; cursor: pointer;
|
|
-webkit-tap-highlight-color: transparent; }
|
|
/* the BODY COUNT headline row: three boxed figures sharing one frame */
|
|
.figs { display: flex; border: 2px solid var(--line); background: var(--panel); }
|
|
/* four numbers read better two-up than four-across on a phone — at four the labels wrap and
|
|
the row becomes a wall of two-line captions */
|
|
.figs.pair { display: grid; grid-template-columns: 1fr 1fr; }
|
|
.figs.pair .fig:nth-child(2n) { border-left: 2px solid var(--line); }
|
|
.figs.pair .fig:nth-child(n+3) { border-top: 2px solid var(--line); }
|
|
.fig { flex: 1; min-width: 0; padding: 11px 8px 12px; text-align: center; }
|
|
.fig + .fig { border-left: 2px solid var(--line); }
|
|
/* THE NUMBER NEVER MOVES (2026-08-31, Oliver: "manchmal sind die überschriften ein- manchmal
|
|
zweizeilig, das drückt die zahl runter"). The label gets two lines of room whether it needs
|
|
them or not, so every figure in a row sits on the same baseline. */
|
|
.fig b { display: flex; align-items: flex-start; justify-content: center; min-height: 22px;
|
|
font: 400 9px/1.25 Silkscreen, monospace; color: var(--dim); letter-spacing: 1px; }
|
|
.fig i { display: block; font: 700 18px Silkscreen, monospace; color: var(--acid);
|
|
font-style: normal; letter-spacing: 1px; }
|
|
.fig.bad i { color: var(--bad); }
|
|
/* one button look for every menu screen */
|
|
/* OPAQUE, not the translucent panel fill. On the list screens it makes no visible difference —
|
|
they sit on flat ink — but on the splash a see-through button lets the court's white lines run
|
|
straight through the lettering, and the button stops reading as a button. */
|
|
.btn { display: block; width: 100%; margin: 0 auto 10px; padding: 12px 10px;
|
|
background: var(--solid); border: 2px solid var(--line); color: var(--cream);
|
|
font: 400 13px Silkscreen, monospace; letter-spacing: 1px; cursor: pointer;
|
|
-webkit-tap-highlight-color: transparent; }
|
|
/* 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: #69201c; color: #f6e7dc;
|
|
font: 400 26px Bangers, cursive; letter-spacing: 2px; line-height: 1.15; }
|
|
/* THE FIXTURE BUTTON (THE SEASON): the shout stays the shout, and the fixture itself is the
|
|
quiet line under it — the same two-register rule the cards follow (Bangers announces, Silkscreen
|
|
informs). `b` is not bold here, it IS the announcement face. */
|
|
.btn.big b { display: block; font: inherit; }
|
|
.btn.big i { display: block; margin-top: 3px; font: 400 9px Silkscreen, monospace;
|
|
font-style: normal; letter-spacing: 0.5px; color: #f0cfc4; }
|
|
.two { display: flex; gap: 8px; }
|
|
.two .btn { margin-bottom: 0; }
|
|
/* the secondary doors NAME (Jersey) and carry a NUMBER (Silkscreen, acid) — the text reduction
|
|
turned "RANKINGS / WORLD BOARD" into "BOARD / BEST 612": a label and a fact, no description */
|
|
.btn.sec { font: 400 18px/1.2 'Jersey 15', Silkscreen, monospace; letter-spacing: 1px; }
|
|
#qNo { font: 400 17px/1.2 'Jersey 15', Silkscreen, monospace; letter-spacing: 1px; }
|
|
.btn.sec i { display: block; margin-top: 3px; font: 700 10px Silkscreen, monospace;
|
|
font-style: normal; color: var(--acid); letter-spacing: 1px; }
|
|
/* THE PLAYER BLOCK: the live man on the left, everything that changes him on the right, so a tap
|
|
and its result are never more than a thumb apart and the whole of "who am I" is one screenful. */
|
|
#who, #who2 { display: flex; gap: 12px; align-items: flex-start; }
|
|
#whoCtl, #pCtl { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 7px; }
|
|
/* The live player. `position: static` is not decoration: the bare `canvas` rule above is the COURT
|
|
— fixed, full-bleed, 100dvh — and it catches any canvas in the document, so this one flew to the
|
|
top-left corner of the screen until it was told otherwise. Every geometry property that rule
|
|
sets has to be undone here. The buffer is the sprite's own 96x224, so width follows height. */
|
|
#look, #pLook { position: static; display: block; flex: none; margin: 0;
|
|
width: auto; height: 176px; touch-action: auto; image-rendering: pixelated; }
|
|
/* the tour is now TWO WORDS, not two portraits — the live player next to them is the picture, and
|
|
a second drawing of the same man (front-facing, and deaf to every colour on this screen) was
|
|
both a duplicate and the reason the court list started below the fold */
|
|
#tours, #pTours { display: flex; gap: 7px; }
|
|
#tours button, #pTours button { flex: 1; padding: 10px 2px; background: var(--panel); border: 2px solid var(--line);
|
|
color: var(--dim); font: 400 10px Silkscreen, monospace; letter-spacing: 1px;
|
|
cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
|
#tours button.on, #pTours button.on { border-color: var(--acid); background: rgba(188,212,85,0.09); color: var(--cream); }
|
|
/* THE IDENTITY SCREEN (the leagues, phase 1): name row = the input plus the die. */
|
|
#nameRow { display: flex; gap: 8px; }
|
|
#nameRow .tin { flex: 1; margin: 0; }
|
|
#pDice { width: 52px; flex: none; margin: 0; padding: 0; font-size: 20px; line-height: 1; }
|
|
/* a LOCKED court: the tile stays visible — it is a goal — but it answers no tap and sits back */
|
|
.court.lock { opacity: 0.45; filter: saturate(0.4); cursor: default; }
|
|
.court.lock i { color: var(--bad); }
|
|
/* THE PROMOTION CEREMONY: one centred column, the walk-on moment of the meta game. The court
|
|
tile is the reward made visible; the staggered drops are cheap CSS, no rAF (menu parks it). */
|
|
#promo .mid { gap: 10px; }
|
|
#prTag { margin: 0; font: 700 10px Silkscreen, monospace; color: var(--dim); letter-spacing: 3px;
|
|
animation: drop 0.5s ease-out; }
|
|
#prTitle { font: 400 44px Bangers, cursive; color: var(--acid); letter-spacing: 2px; margin: 0;
|
|
animation: drop 0.5s ease-out 0.15s backwards; }
|
|
#prLeague { margin: 0; font: 700 12px Silkscreen, monospace; color: var(--cream);
|
|
letter-spacing: 2px; animation: drop 0.5s ease-out 0.3s backwards; }
|
|
#prCourt { width: 74%; max-width: 300px; margin: 10px auto 0; align-self: center;
|
|
border: 2px solid var(--line);
|
|
image-rendering: pixelated; animation: drop 0.5s ease-out 0.5s backwards; }
|
|
#prCourtName { margin: 0; font: 400 10px Silkscreen, monospace; color: var(--acid);
|
|
letter-spacing: 1.5px; animation: drop 0.5s ease-out 0.62s backwards; }
|
|
/* THE CHARACTER CARD (the leagues, phase 3): the player writ large — sprite, crest, level,
|
|
honours, ledger. The honours row is a collection with visible empty slots: the unearned
|
|
badges sit dark, which is the whole incentive. */
|
|
#cdTop { display: flex; gap: 14px; align-items: flex-start; margin-top: 14px; }
|
|
#cdLook { position: static; display: block; flex: none; margin: 0; width: auto; height: 200px;
|
|
touch-action: auto; image-rendering: pixelated; }
|
|
#cdMeta { flex: 1; min-width: 0; display: grid; gap: 8px 11px; align-items: center;
|
|
grid-template-columns: auto 1fr;
|
|
grid-template-areas: "crest league" "crest level" "bar bar" "xp xp" "career career"; }
|
|
/* THE CREST STANDS BESIDE THE LEAGUE (2026-08-31, Oliver: on its own row it pushed the text
|
|
down), so the two things that say WHERE YOU ARE share a line instead of stacking. */
|
|
#cdBadge { grid-area: crest; width: 56px; image-rendering: pixelated; align-self: center; }
|
|
#cdLeague { grid-area: league; align-self: end; font: 700 10px/1.5 Silkscreen, monospace;
|
|
text-decoration: none; letter-spacing: 1.5px; }
|
|
#cdLeague i { display: block; font-style: normal; font-size: 9px; opacity: 0.85; }
|
|
#cdBar { grid-area: bar; }
|
|
#cdXp { grid-area: xp; }
|
|
#cdCareer { grid-area: career; }
|
|
#cdLevel { grid-area: level; align-self: start; font: 400 30px Bangers, cursive;
|
|
color: var(--cream); letter-spacing: 1.5px; }
|
|
#cdXp { display: block; margin-top: 5px; font: 400 9px Silkscreen, monospace; color: var(--dim);
|
|
text-decoration: none; letter-spacing: 1px; }
|
|
/* THE HONOURS ROW: a shield per league, and under an earned one the INSCRIPTION — the level it
|
|
was taken at (the cup board's tie-break, in the one place a once-in-a-career figure belongs). */
|
|
#cdBadges { display: flex; gap: 10px; align-items: flex-end; }
|
|
#cdBadges .badge { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column;
|
|
align-items: center; gap: 4px; }
|
|
/* ...and an earned shield SMOULDERS: two-pixel embers drifting off it, each on its own delay and
|
|
lane (main.embers). Pure CSS — the menu parks the game loop and this has no business waking it. */
|
|
/* ...and it CATCHES THE LIGHT: three points on the shield, dark for most of a long cycle and
|
|
flaring for a fraction of it (main.glints). Not a fire — a glint, the way metal behaves. */
|
|
#cdBadges .badge em { position: absolute; width: 4px; height: 4px; border-radius: 50%;
|
|
background: #fff6dc; box-shadow: 0 0 7px 2px rgba(255,231,170,0.9);
|
|
opacity: 0; pointer-events: none; animation-name: glint;
|
|
animation-timing-function: ease-out; animation-iteration-count: infinite; }
|
|
@keyframes glint {
|
|
0%, 88%, 100% { opacity: 0; transform: scale(0.3); }
|
|
92% { opacity: 1; transform: scale(1); }
|
|
96% { opacity: 0.35; transform: scale(0.6); } }
|
|
@media (prefers-reduced-motion: reduce) { #cdBadges .badge em { display: none; } }
|
|
#cdBadges img { height: 46px; image-rendering: pixelated; }
|
|
#cdBadges .badge u { font: 700 7px Silkscreen, monospace; color: var(--acid); letter-spacing: 0.5px;
|
|
text-decoration: none; white-space: nowrap; }
|
|
#cdBadges .badge.dark img { filter: grayscale(1) brightness(0.3); }
|
|
#cdBadges .badge.dark u { color: #3f4d45; }
|
|
#cdBadges img.dark { filter: grayscale(1) brightness(0.3); }
|
|
/* earned badges BREATHE — a subtle gold halo, CSS-only (the menu parks the game loop, CSS
|
|
animations do not care). The dark slots stay dead, which is the point of them. */
|
|
#cdBadges .badge:not(.dark) img, #cdBadges img:not(.dark), #cdBadge {
|
|
animation: badgeGlow 2.6s ease-in-out infinite; }
|
|
@keyframes badgeGlow {
|
|
0%, 100% { filter: drop-shadow(0 0 2px rgba(232, 178, 58, 0.25)); }
|
|
50% { filter: drop-shadow(0 0 7px rgba(232, 178, 58, 0.75)); } }
|
|
.rkbadge { height: 15px; margin-right: 6px; image-rendering: pixelated; flex: none; }
|
|
/* THE LEAGUE PANEL: the setup's league-mode face. The badge breathes like the card's, the
|
|
name is the announcement face in the league's own colour, and the band's roster stands in a
|
|
row — felled bodies in colour with their all-time count, the rest grey and waiting. */
|
|
#lgPanel { display: none; }
|
|
#lgHead { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
|
|
#lgBadge { height: 56px; image-rendering: pixelated; animation: badgeGlow 2.6s ease-in-out infinite; }
|
|
#lgHead b { display: block; font: 400 24px Bangers, cursive; letter-spacing: 1.5px; }
|
|
#lgHead i { display: block; margin-top: 3px; font: 400 9px Silkscreen, monospace;
|
|
color: var(--dim); font-style: normal; letter-spacing: 1px; }
|
|
#lgFoes { display: flex; gap: 7px; }
|
|
.lgFoe { flex: 1; min-width: 0; padding: 7px 2px 6px; background: var(--panel);
|
|
border: 2px solid var(--line); text-align: center; }
|
|
.lgFoe img { height: 42px; image-rendering: pixelated; }
|
|
.lgFoe b { display: block; margin-top: 4px; font: 700 8px Silkscreen, monospace;
|
|
color: var(--cream); letter-spacing: 0.5px; white-space: nowrap; overflow: hidden; }
|
|
.lgFoe i { display: block; margin-top: 2px; font: 400 8px Silkscreen, monospace;
|
|
color: var(--acid); font-style: normal; }
|
|
.lgFoe.un { opacity: 0.55; }
|
|
.lgFoe.un img { filter: grayscale(1) brightness(0.6); }
|
|
.lgFoe.un i { color: var(--dim); }
|
|
/* the band's top rung is the final — it wears the door's own red */
|
|
.lgFoe.fin { border-color: var(--red); }
|
|
/* league modes: the venue is the league's own and not a choice — one row, no tap */
|
|
#courtList.fixed .court { cursor: default; }
|
|
/* the WAY-UP RIBBON, horizontal (Oliver's call after two diagonal cuts — a rotated pixel font
|
|
was the one anti-aliased thing in the menu): a red band running in from OVER the left border,
|
|
dissolving rightward through a real PIXEL FADE — an inline-SVG mask of 6px cells thresholded
|
|
against a Bayer matrix (four density steps), the dither the game's own art would use, never a
|
|
smooth gradient. The BAND lives on ::before and carries the mask; the HOST carries the text
|
|
and a drop-shadow FILTER — a filter on the parent shadows the already-masked child, so the
|
|
shadow survives the mask and follows the dither blocks (a box-shadow on the masked element
|
|
itself was eaten with it). */
|
|
.way { position: absolute; top: 12px; left: -8px; z-index: 2; white-space: nowrap;
|
|
padding: 8px 64px 8px 16px; font: 700 13px Silkscreen, monospace; font-style: normal;
|
|
color: #fff;
|
|
text-shadow: 1px 1px 0 rgba(0,0,0,0.55);
|
|
filter: drop-shadow(0 2px 3px rgba(0,0,0,0.5));
|
|
letter-spacing: 1px; text-decoration: none; }
|
|
.way::before { content: ''; position: absolute; inset: 0; z-index: -1; background: var(--red);
|
|
border-top: 1px solid rgba(255,255,255,0.35); border-bottom: 1px solid #69201c;
|
|
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%20200%2024%22%20shape-rendering=%22crispEdges%22%3E%3Cg%20fill=%22%23000%22%3E%3Crect%20x=%220%22%20y=%220%22%20width=%22140%22%20height=%2224%22/%3E%3Crect%20x=%22140%22%20y=%220%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22140%22%20y=%226%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22140%22%20y=%2212%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22140%22%20y=%2218%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22146%22%20y=%220%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22146%22%20y=%2212%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22152%22%20y=%226%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22152%22%20y=%2218%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22158%22%20y=%220%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22158%22%20y=%2212%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22170%22%20y=%220%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22170%22%20y=%2212%22%20width=%226%22%20height=%226%22/%3E%3C/g%3E%3C/svg%3E");
|
|
-webkit-mask-size: 100% 100%; -webkit-mask-repeat: no-repeat;
|
|
mask-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%20200%2024%22%20shape-rendering=%22crispEdges%22%3E%3Cg%20fill=%22%23000%22%3E%3Crect%20x=%220%22%20y=%220%22%20width=%22140%22%20height=%2224%22/%3E%3Crect%20x=%22140%22%20y=%220%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22140%22%20y=%226%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22140%22%20y=%2212%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22140%22%20y=%2218%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22146%22%20y=%220%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22146%22%20y=%2212%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22152%22%20y=%226%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22152%22%20y=%2218%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22158%22%20y=%220%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22158%22%20y=%2212%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22170%22%20y=%220%22%20width=%226%22%20height=%226%22/%3E%3Crect%20x=%22170%22%20y=%2212%22%20width=%226%22%20height=%226%22/%3E%3C/g%3E%3C/svg%3E");
|
|
mask-size: 100% 100%; mask-repeat: no-repeat; }
|
|
/* THE PLAYER PLAQUE: a LETTERHEAD, not a centred stack (Oliver: the stack sat cramped) — the
|
|
crest on the left, name over the big LEVEL · LEAGUE line beside it, the chevron on the
|
|
right; the bar runs the full width below and the goal line closes left-aligned. Translucent
|
|
surface, generous rows — the splash art, blood pool included, stays visible around it. */
|
|
/* ONE ROW (2026-08-31, the text reduction): crest, then name and level on one baseline, the bar
|
|
under them with the level it fills toward at its right end. The GOAL LINE is gone — the
|
|
fixture button right below says what to do, and saying it twice was the whole complaint. */
|
|
#plaque { position: relative; display: flex; align-items: center; gap: 13px; text-align: left;
|
|
margin: 24px auto 0; width: min(330px, 92%); box-sizing: border-box;
|
|
padding: 13px 36px 13px 15px; background: rgba(8, 14, 10, 0.48); border: 0;
|
|
border-radius: 12px; cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
|
#plBadge { flex: none; height: 50px; image-rendering: pixelated;
|
|
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5)); }
|
|
#plCol { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 7px; }
|
|
#plTop { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
|
|
#plName { font: 400 21px 'Jersey 15', Bangers, cursive; color: var(--cream); letter-spacing: 0.5px;
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
/* the CALL belongs to the button below, not to the plaque — the level is a number now */
|
|
#plLv { flex: none; font: 700 10px Silkscreen, monospace; color: var(--acid); letter-spacing: 1px;
|
|
text-decoration: none; }
|
|
/* the bar wears the level it fills TOWARD at its right end — "▓▓░░ 13" says which way it runs
|
|
without a word of explanation (Oliver read a bare bar as total progress) */
|
|
#plRow { display: flex; align-items: center; gap: 7px; }
|
|
#plRow u, #cdBar u { font: 700 9px Silkscreen, monospace; color: var(--cream);
|
|
text-decoration: none; text-shadow: 1px 1px 0 rgba(0,0,0,0.5); }
|
|
#plaque em { position: absolute; right: 14px; top: 50%; transform: translateY(-50%);
|
|
font: 700 15px Silkscreen, monospace; font-style: normal; color: var(--acid); }
|
|
#plRow s, #cdBar s, #cdCareer s { flex: 1; height: 6px; background: rgba(255,255,255,0.10);
|
|
border: 1px solid var(--line); text-decoration: none; display: block; }
|
|
#cdBar s { height: 8px; }
|
|
#cdBar { display: flex; align-items: center; gap: 8px; }
|
|
#cdCareer { display: flex; align-items: center; gap: 8px; }
|
|
#cdCareer u { font: 700 8px Silkscreen, monospace; color: var(--dim); text-decoration: none;
|
|
letter-spacing: 1px; }
|
|
#plRow s i, #cdBar s i, #cdCareer s i { display: block; height: 100%; width: 0%;
|
|
background: linear-gradient(90deg, #3f9560, var(--acid) 55%, #e8b23a);
|
|
transition: width 0.4s ease; }
|
|
#cdBar s i, #cdCareer s i { transition: width 0.7s cubic-bezier(0.2, 0.7, 0.3, 1); }
|
|
/* KIT and SKIN: a row of colour squares, under a label that names whatever is selected. That is
|
|
the whole UI — the pick has no meters and no art to show, and a labelled tile per colour would
|
|
be five times the screen for a choice you make by looking at the colour. */
|
|
.swlab { display: flex; justify-content: space-between; margin-top: 2px; text-decoration: none;
|
|
font: 400 9px Silkscreen, monospace; color: var(--dim); letter-spacing: 1.5px; }
|
|
.swlab i { font-style: normal; color: var(--cream); }
|
|
.swrow { display: flex; gap: 6px; }
|
|
.swrow button { flex: 1; aspect-ratio: 1; min-width: 0; padding: 0; border: 2px solid var(--line);
|
|
cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
|
.swrow button.on { border-color: var(--acid); box-shadow: inset 0 0 0 2px var(--ink); }
|
|
/* THE BOSS PICKER (back room). Two columns of portraits, because you pick a boss by looking at
|
|
him — the tile is mostly art. Same plate as the tour tiles above; the grid is what makes ten
|
|
of them fit on a phone. */
|
|
#bossList, #trainList { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding-bottom: 14px; }
|
|
#bossList button, #trainList button { display: flex; flex-direction: column; align-items: center; gap: 5px;
|
|
padding: 8px 6px 10px; background: var(--panel); border: 2px solid var(--line);
|
|
color: inherit; font: inherit; cursor: pointer;
|
|
-webkit-tap-highlight-color: transparent; }
|
|
#bossList img { height: 78px; width: auto; display: block; image-rendering: pixelated; }
|
|
#bossList b, #trainList b { font: 400 9px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px;
|
|
text-align: center; line-height: 1.35; }
|
|
#bossList i, #trainList i { font: 400 8px Silkscreen, monospace; color: var(--dim); font-style: normal;
|
|
letter-spacing: 1px; }
|
|
/* THE REVANCHE ROW (THE SEASON, widened in the text reduction): the one that put you out of the
|
|
cup, across the top of the grid — one body out of eighteen with an actual reason, so it gets
|
|
the width to say so in three words. */
|
|
#trainList button.put { grid-column: 1 / -1; flex-direction: row; align-items: center; gap: 12px;
|
|
padding: 10px 12px; text-align: left; position: relative;
|
|
border-color: var(--red); background: rgba(164,35,31,0.16); }
|
|
#trainList button.put img { height: 52px; }
|
|
#trainList button.put > span { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px;
|
|
text-align: left; }
|
|
#trainList button.put b { font: 400 24px/1.05 'Jersey 15', Silkscreen, monospace; text-align: left; }
|
|
#trainList button.put i { color: #e08a7c; letter-spacing: 1px; }
|
|
#trainList button.put em { position: absolute; right: 12px; font: 700 15px Silkscreen, monospace;
|
|
font-style: normal; color: var(--bad); }
|
|
/* the rung's NAME (Jersey) over its number — "No. 7" was three characters of nothing */
|
|
#trainList b { font: 400 17px/1 'Jersey 15', Silkscreen, monospace; letter-spacing: 0.5px; }
|
|
/* ...and the list ends on the league that opens the rest of the ladder, drawn as the goal it is */
|
|
#trainList .goal { grid-column: 1 / -1; padding: 14px 6px; border: 2px dashed var(--line);
|
|
text-align: center; }
|
|
#trainList .goal b { display: block; color: #4a5a52; }
|
|
#trainList .goal i { font: 400 9px Silkscreen, monospace; font-style: normal; color: #4a5a52;
|
|
letter-spacing: 1px; }
|
|
/* the training picker is 18 rungs deep — the tile carries the HEAD (the same z_*head crop the
|
|
draw and the walk-on wear), which reads at a third of the boss portrait's height */
|
|
#trainList img { height: 44px; width: auto; display: block; image-rendering: pixelated; }
|
|
/* START is genuinely unreachable until the draw has landed — the wait IS the ceremony */
|
|
.btn[disabled] { opacity: 0.32; cursor: default; }
|
|
/* the fixture line under the new court: the same two registers the plaque uses — the name in
|
|
the league's own acid, what it costs you in the quiet face */
|
|
#prNext { margin: 12px 0 0; font: 400 10px Silkscreen, monospace; color: var(--acid);
|
|
letter-spacing: 1px; line-height: 1.6; }
|
|
#prNext u { display: block; margin-top: 3px; font-size: 9px; color: var(--dim);
|
|
text-decoration: none; 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. */
|
|
/* MODE posters are pure BUTTONS, not selectors (Oliver: the remembered highlight made each one
|
|
"a checkbox and a button combined"). Every card is full-bright and equal because every card is
|
|
a live answer to the header's question; tapping answers and advances. The only memory shown is
|
|
the LAST tag — an indicator, not a selection. A border that means "SERVE UP will use this"
|
|
exists solely on the final screen, where a confirm button actually follows. */
|
|
#modeList button.mode, #modeSub button.mode { display: flex; align-items: flex-end; width: 100%; min-height: 96px;
|
|
margin: 0 0 8px; padding: 7px 9px; background-size: cover; background-position: center bottom;
|
|
image-rendering: pixelated; border: 2px solid var(--line); color: inherit; font: inherit;
|
|
text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent;
|
|
position: relative; }
|
|
/* THE CAPTION'S VEIL (2026-08-31). The blur went in and came straight back out — on
|
|
nearest-neighbour art it read as a smear, not as depth (Oliver: "sieht komisch aus"). What is
|
|
left is what he asked for instead: a gradient RISING FROM THE BOTTOM EDGE, solid where the
|
|
words are and gone by the time it reaches the art. Same job, no filter, no edge. */
|
|
#modeList button.mode::after, #modeSub button.mode::after {
|
|
content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 62%;
|
|
background: linear-gradient(0deg, rgba(6,10,8,0.94) 0%, rgba(6,10,8,0.86) 22%,
|
|
rgba(6,10,8,0.5) 58%, rgba(6,10,8,0) 100%);
|
|
pointer-events: none; }
|
|
/* the words and the chips ride ABOVE the veil — a pseudo-element is the last thing painted */
|
|
#modeList .cb, #modeSub .cb { position: relative; z-index: 2; }
|
|
#modeList b, #modeSub b { text-shadow: 0 1px 0 #000, 0 0 6px rgba(0,0,0,0.9); }
|
|
/* the subtitle carries the mode's one-line rules over busy art, so it gets WHITE at full
|
|
opacity, a size up from the court rows' 9px, and a solid black underlay — "barely readable"
|
|
at cream-on-crowd was the report from the first live look */
|
|
#modeList i, #modeSub i { color: #fff; font-size: 10px; margin-bottom: 0;
|
|
text-shadow: 0 1px 0 #000, 1px 0 0 #000, -1px 0 0 #000, 0 -1px 0 #000, 0 0 7px #000; }
|
|
/* THE SECTIONS (THE SEASON): the wall is three rooms — one path up, a room to practise in, a
|
|
room where nothing counts — so each is a real section with its posters INSIDE it, not a label
|
|
floating over a stack. The heading is deliberately NOT the announcement face (Oliver: it read
|
|
as a second card headline and the two competed); Silkscreen, big and letter-spaced, keeps it
|
|
a label of a room. No rule, no left band either — the air above each heading does the
|
|
grouping, which is what air is for. */
|
|
#modeList .mgroup { margin: 0 0 10px; }
|
|
#modeList .mgroup + .mgroup { margin-top: 17px; }
|
|
/* HOW A ROOM DEALS ITS POSTERS (the text reduction). The wall used to be six identical 148px
|
|
slabs and ran over two screens; the fixture is a HERO now, the training pair stands side by
|
|
side, and the casual three stack shorter. All of it fits one screen. */
|
|
#modeList .mgroup .shelf { display: flex; flex-direction: column; }
|
|
#modeList .mgroup.pair .shelf { flex-direction: row; gap: 8px; }
|
|
#modeList .mgroup.pair button.mode { flex: 1; min-width: 0; min-height: 106px; margin-bottom: 0; }
|
|
#modeList .mgroup.stack button.mode { min-height: 96px; }
|
|
#modeList .mgroup.hero button.mode { min-height: 142px; }
|
|
/* the poster names: Jersey NAMES, and the hero is the one poster that CALLS (Bangers) */
|
|
#modeList .cb b { font: 400 30px/1 'Jersey 15', Bangers, cursive; letter-spacing: 0.5px; }
|
|
#modeList .mgroup.pair .cb b { font-size: 27px; }
|
|
#modeList .mgroup.hero .cb b { font: 400 32px/0.95 Bangers, cursive; letter-spacing: 1px; }
|
|
/* ...and the hero's second line is the fixture, the only strap left on the wall */
|
|
#modeList .mgroup.hero .cb i { color: #f0cfc4; font-size: 10px; margin-top: 3px; }
|
|
/* text-align rides down from the card, which centres everything on these screens — the section
|
|
header is a header, and headers on this screen are left-aligned (the .hdr title, the posters'
|
|
own captions) */
|
|
#modeList .mgroup > header { margin: 0 0 11px; text-align: left; }
|
|
/* THE SHELF LABELS SHOUT, quietly (2026-08-31, Oliver tried three: Bangers first, then the HUD
|
|
face, then the platform's UI face, and came back to the announcement face). It works here now
|
|
because the header lost its rule and its band and gained air: the size does the hierarchy
|
|
(20 against the screen title's 26) instead of the typeface having to. */
|
|
/* SCOPED TO THE HEADER, not to the section: the posters live INSIDE .mgroup now, so a bare
|
|
descendant selector here restyled every caption on the wall (it stayed invisible while this
|
|
rule still named Silkscreen — the moment the family changed, the posters changed with it). */
|
|
#modeList .mgroup > header b { display: block; letter-spacing: 0.5px; color: var(--acid);
|
|
text-shadow: none; font: 400 17px/1.2 'Jersey 15', Bangers, cursive;
|
|
letter-spacing: 2px; }
|
|
/* ...and the strap goes back to the quiet register under it: a shouted line over a system-sans
|
|
line was two foreign voices in one header */
|
|
#modeList .mgroup > header i { display: block; margin: 4px 0 0; letter-spacing: 0.5px;
|
|
color: var(--dim); font-style: normal; text-shadow: none;
|
|
font: 400 9px/1.4 Silkscreen, monospace; }
|
|
#modeList .mgroup button.mode:last-child { margin-bottom: 0; }
|
|
/* THE SECOND STEP (config.MENU) has its own SCREEN in the wizard: sub-mode cards are
|
|
full-height posters centred on their scene, and the difficulty chips are plain acid-outline
|
|
buttons stacked tall — art for EASY/MEDIUM/HARD would be forced, and the chip reads faster. */
|
|
/* alone on their own screens, the posters get room to be posters — and the first card clears
|
|
the header by the same breath the h3 rows used to give the old single page */
|
|
#stmode #modeList, #stsub #modeSub { margin-top: 12px; }
|
|
#stmode button.mode, #modeSub button.mode { min-height: 148px; }
|
|
#modeSub { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
#modeSub button.mode { background-position: center; }
|
|
#modeSub button.chip { flex: 1 1 100%; display: flex; align-items: center; justify-content: space-between;
|
|
gap: 10px; padding: 26px 16px; background: var(--panel); border: 2px solid var(--line);
|
|
color: var(--cream); cursor: pointer; -webkit-tap-highlight-color: transparent;
|
|
position: relative; text-align: left; }
|
|
#modeSub button.chip b { font: 400 30px/1 'Jersey 15', Silkscreen, monospace; letter-spacing: 1px; }
|
|
/* how hard it is, counted: as many pips lit as the chip is steep, and the last one wears the
|
|
colour the game uses for a threat */
|
|
.pips { display: flex; gap: 5px; }
|
|
.pips i { width: 10px; height: 10px; background: rgba(255,255,255,0.12); }
|
|
.pips i.on { background: var(--acid); }
|
|
#modeSub button.chip.hot .pips i.on { background: var(--bad); }
|
|
#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; }
|
|
#courtList button.court.on { border-color: var(--acid); background: rgba(188,212,85,0.09); }
|
|
#courtList img { width: 84px; height: 60px; display: block; image-rendering: pixelated; flex: none;
|
|
object-fit: cover; }
|
|
#courtList .cb, #modeList .cb, #modeSub .cb { flex: 1; min-width: 0; }
|
|
#courtList b, #modeList b, #modeSub b { display: block; font: 700 15px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px; }
|
|
/* the court NAMES itself (Jersey) and its two meters say the rest — the description line is gone */
|
|
#courtList b { font: 400 22px/1.1 'Jersey 15', Silkscreen, monospace; letter-spacing: 0.5px; }
|
|
#courtList i { margin: 3px 0 5px; font: 700 9px Silkscreen, monospace; color: var(--bad);
|
|
letter-spacing: 1px; }
|
|
#courtList i[hidden] { display: none; }
|
|
#courtList i, #modeList i, #modeSub i { display: block; margin: 4px 0 6px; font: 400 10px Silkscreen, monospace; color: #c9c4b8; 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: 4px;
|
|
font: 400 8px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px; }
|
|
.meter u { flex: none; width: 46px; 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: #4e8fa6; }
|
|
/* 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; }
|
|
/* ...except the splash's own secondary door: OTHER GAMES sat flush on the STATS/RANKINGS row */
|
|
.foot.bare #mOther { margin-bottom: 12px; }
|
|
/* 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; }
|
|
/* THE DEAL-IN (2026-08-31, Oliver: "es wäre toll, wenn sich die buttons nach dem laden aufbauen
|
|
würden — arcade style"). The canvas modals have always arrived on an easeOutBack overshoot
|
|
rather than decelerating politely into place; this is that same snap, in CSS, for the one
|
|
moment the menu has an entrance to make. It plays on EVERY arrival at the title screen, not
|
|
just the boot (Oliver) — main.dealSplash re-arms the class, because an animation whose class
|
|
never left cannot restart itself. */
|
|
@keyframes dealIn {
|
|
from { opacity: 0; transform: translateX(-56px); }
|
|
60% { opacity: 1; }
|
|
to { opacity: 1; transform: translateX(0); }
|
|
}
|
|
#intro.deal #splash #plaque,
|
|
#intro.deal #splash .foot.bare > * {
|
|
animation: dealIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; }
|
|
#intro.deal #splash #plaque { animation-delay: 0.02s; }
|
|
#intro.deal #splash #mStart { animation-delay: 0.12s; }
|
|
#intro.deal #splash #mOther { animation-delay: 0.19s; }
|
|
/* the pair of secondary buttons is a CLASS, so it needs the same weight as the shorthand above
|
|
to override its delay — the id selectors around it get there on their own */
|
|
#intro.deal #splash .foot.bare > .two { animation-delay: 0.26s; }
|
|
#intro.deal #splash #build { animation-delay: 0.33s; }
|
|
@media (prefers-reduced-motion: reduce) {
|
|
#intro.deal #splash #plaque, #intro.deal #splash .foot.bare > * { animation: none; } }
|
|
/* the boot bar wears the stats screen's kill bar, doing duty as a loading bar. While #splash is
|
|
.busy it stands in the buttons' place — same band, same height, so nothing jumps when it goes. */
|
|
#boot { display: none; margin-bottom: 10px; }
|
|
#boot s { display: block; height: 6px; background: rgba(0,0,0,0.45); text-decoration: none; }
|
|
#boot s > i { display: block; width: 0; height: 100%; background: var(--acid);
|
|
transition: width 0.15s linear; }
|
|
#boot u { display: block; margin-top: 7px; text-align: center; text-decoration: none;
|
|
font: 400 9px Silkscreen, monospace; color: var(--dim); letter-spacing: 2px; }
|
|
#splash.busy #boot { display: block; }
|
|
#splash.busy .btn, #splash.busy .two, #splash.busy #build { display: none; }
|
|
/* NOTHING DATA-DRIVEN DURING THE BOOT (2026-08-31, Oliver: "die oberfläche ist hier regelmäßig
|
|
in einem undefinierten zustand"). The plaque used to stand there as a skeleton, which is still
|
|
a plaque saying LEVEL 1 to a level-25 player for as long as the profile takes to land. The
|
|
title and the court stay — they are the background and they are never undefined. */
|
|
#splash.busy #plaque { visibility: hidden; } /* hidden, but it KEEPS ITS SPACE: display:none
|
|
let the title slide down the screen and jump back up the moment the profile landed */
|
|
#brSum { display: flex; justify-content: space-between; gap: 8px; margin: 0 0 8px;
|
|
font: 400 9px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px; }
|
|
.tabs { display: flex; gap: 6px; margin: 12px 0 14px; }
|
|
.tabs button { flex: 1; padding: 11px 2px; background: var(--panel); border: 2px solid var(--line);
|
|
color: var(--dim); font: 400 10px 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: 11px; padding: 9px 10px; margin-bottom: 6px;
|
|
border: 2px solid var(--line); background: var(--panel); text-align: left; }
|
|
.zrow.boss { border-color: #6e2a24; background: rgba(110,42,36,0.22); }
|
|
.zrow img { width: 48px; height: 62px; 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 12px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px;
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.zb > span { font: 700 15px Silkscreen, monospace; color: var(--acid); text-align: right; }
|
|
.zrow.boss .zb > span { color: var(--bad); }
|
|
.pb { grid-column: 1 / -1; height: 6px; 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: 4px; font: 400 9px Silkscreen, monospace;
|
|
font-style: normal; color: var(--dim); letter-spacing: 1px; }
|
|
/* a GRID, not a wrapping flex row: `flex: 1 1 62px` gave the last row's tiles the whole width
|
|
to share, so a shelf of nine came out in three different sizes (Oliver) */
|
|
#stW { display: grid; grid-template-columns: repeat(auto-fill, minmax(66px, 1fr)); gap: 6px; }
|
|
.wtile { padding: 8px 2px; border: 2px solid var(--line); background: var(--panel); }
|
|
.wtile img { width: 34px; height: 34px; object-fit: contain; display: block; margin: 0 auto 2px;
|
|
image-rendering: pixelated; }
|
|
.wtile b { display: block; font: 400 11px Silkscreen, monospace; color: var(--acid); }
|
|
.none { margin: 4px 0 0; font: 400 9px 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 9px Silkscreen, monospace; color: var(--dim); letter-spacing: 1px; }
|
|
#stFoot i { font: 700 16px 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(188,212,85,0.11); }
|
|
.rkrow b { flex: none; width: 68px; font: 700 12px Silkscreen, monospace; color: var(--dim); }
|
|
.rkrow.me b { color: var(--acid); }
|
|
.rkrow span { flex: 1; min-width: 0; font: 400 11px Silkscreen, monospace; color: var(--cream);
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.rkrow i { font: 400 11px Silkscreen, monospace; font-style: normal; color: var(--dim); }
|
|
/* ---- typed fields. Two screens ask for typing — the name prompt and the netplay lobby — and
|
|
both want the same field: big, centred, acid on the panel, no rounded corners anywhere near it.
|
|
One class, two size overrides. */
|
|
.cent { text-align: center; }
|
|
.tin { display: block; width: 100%; box-sizing: border-box; margin: 0 auto;
|
|
padding: 12px 10px; background: var(--solid); border: 2px solid var(--line);
|
|
border-radius: 0; color: var(--acid); font: 700 16px Silkscreen, monospace;
|
|
letter-spacing: 3px; text-align: center; text-transform: uppercase;
|
|
-webkit-appearance: none; -webkit-tap-highlight-color: transparent; }
|
|
.tin:focus { outline: none; border-color: var(--acid); }
|
|
.tin::placeholder { color: var(--dim); letter-spacing: 2px; font-size: 12px; }
|
|
#newbest h2 { font-size: 30px; margin-bottom: 6px; }
|
|
#nbScore { margin: 0 0 22px; font: 400 11px Silkscreen, monospace; color: var(--dim);
|
|
letter-spacing: 1px; }
|
|
#nbScore b { color: var(--acid); font-weight: 400; }
|
|
#nbName { margin-bottom: 10px; padding: 14px 10px; font-size: 20px; }
|
|
#nbName::placeholder { font-size: 14px; }
|
|
/* ---- THE NETPLAY LOBBY. Everything the mode needs before there is a court: who you are, the
|
|
two ways to host, and the friend's code. The room you are waiting in replaces the picker in
|
|
place (#vsWait), so the code and the SHARE button stand where the buttons were. */
|
|
#vsErr:empty { display: none; }
|
|
#vsErr { margin: 10px 0 0; padding: 8px; border: 2px solid #6e2a24; background: rgba(110,42,36,0.22);
|
|
font: 400 10px Silkscreen, monospace; color: var(--bad); letter-spacing: 1px;
|
|
text-align: center; }
|
|
#vs .btn { margin-bottom: 8px; }
|
|
/* the room takes the whole screen the picker had: the code floats in the middle band and CANCEL
|
|
sits under your thumb, same three-band anchoring as every other screen */
|
|
#vsWait { display: flex; flex-direction: column; flex: 1; }
|
|
#vsWait[hidden] { display: none; }
|
|
.joinrow { display: flex; gap: 8px; align-items: stretch; }
|
|
.joinrow .tin { flex: 1; letter-spacing: 8px; }
|
|
/* JOIN is the act, not a label beside a box: the crimson plate every primary button in the game
|
|
wears, and NO padding of its own — the row stretches it to exactly the field's height, which
|
|
its own 13px padding could only ever approximate. */
|
|
/* #vs .btn's margin-bottom outranks a bare .joinrow .btn — and a margin on a stretched flex item
|
|
is exactly the 8px the button was coming up short by */
|
|
#vs .joinrow .btn { flex: none; width: 104px; margin: 0; padding: 0 6px; background: var(--red);
|
|
border-color: #69201c; color: #f6e7dc; font: 400 22px Bangers, cursive;
|
|
letter-spacing: 2px; }
|
|
/* the code is the biggest thing on the screen — it is read aloud across a room as often as it is
|
|
shared, and a four-letter code you have to squint at is the whole friction being removed */
|
|
#vsBig { margin: 4px 0 20px; font: 700 42px Silkscreen, monospace; color: var(--acid);
|
|
letter-spacing: 10px; text-indent: 10px; }
|
|
#vsMsg { margin-top: 12px; }
|
|
/* the h3 rows are label-left/hint-right everywhere else; this one has no hint and sits over a
|
|
centred code, so it centres too (text-align cannot reach it — the row is a flex box) */
|
|
#vsWHdr { justify-content: center; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="c" width="480" height="270"></canvas>
|
|
<div id="safe"></div>
|
|
<div id="hint">move: WASD/arrows · serve/swing: SPACE · aim: hold a direction while swinging · T: tuning panel</div>
|
|
<div id="quit" hidden><button id="qChip">X</button></div>
|
|
<div id="qAsk" hidden>
|
|
<div class="qBox">
|
|
<p>LEAVE THE MATCH?</p>
|
|
<button class="btn big" id="qYes">BACK TO THE MENU</button>
|
|
<button class="btn sec" id="qNo">PLAY ON</button>
|
|
</div>
|
|
</div>
|
|
<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. -->
|
|
<!-- three bands: the figures at the top, the title floating in the middle, the buttons under
|
|
your thumb. Same anchoring as every other screen. -->
|
|
<!-- BUSY FROM THE FIRST PAINT (2026-08-31, Oliver: "zeigt sich während des ladens noch immer
|
|
die box mit LEVEL 1 und springt dann erst auf die tatsächlichen werte"). This module is
|
|
deferred, so the browser paints this markup BEFORE main.js can add the class — the
|
|
placeholder values in the tags below were on screen for the whole of that gap. The class
|
|
ships in the HTML now; main.js only ever takes it OFF (see the boot gate there). -->
|
|
<div id="splash" class="busy">
|
|
<div id="hot"></div>
|
|
<div class="mid">
|
|
<img class="logo" src="assets/logo.png" alt="Splatter Tennis">
|
|
<p class="slogan">Fifteen. Thirty. Forty. <b>Dead.</b></p>
|
|
<!-- THE PLAYER PLAQUE (the leagues): centred under the title on a translucent surface,
|
|
so the foot's blood pool stays visible. One tap opens the card (refreshSplash fills). -->
|
|
<button id="plaque">
|
|
<img id="plBadge" alt="">
|
|
<span id="plCol">
|
|
<span id="plTop"><b id="plName"></b><u id="plLv">LEVEL 1</u></span>
|
|
<span id="plRow"><s><i id="plFill"></i></s><u id="plTo">2</u></span>
|
|
</span>
|
|
<em>></em>
|
|
</button>
|
|
</div>
|
|
<div class="foot bare">
|
|
<!-- THE BOOT BAR. Every sprite is requested the moment render.js is imported; this only
|
|
shows how far that got, and stands where the buttons will be until it is done — a menu
|
|
you can press before the sheets have landed is how you reach the stats screen with half
|
|
the zombies missing. Swapped by #splash.busy, see main.js. -->
|
|
<div id="boot"><s><i></i></s><u>LOADING</u></div>
|
|
<!-- THE FRONT DOOR IS THE FIXTURE (2026-08-31, THE SEASON). The primary button used to say
|
|
PLAY MATCH and open a wall of posters, which asked the player a question the game
|
|
already knows the answer to. It names the next cup fixture now and enters the draw;
|
|
everything else lives one tap down, behind OTHER GAMES. -->
|
|
<button class="btn big" id="mStart"><b id="msTitle">THE CUP</b><i id="msLine"></i></button>
|
|
<button class="btn sec" id="mOther">OTHER GAMES</button>
|
|
<div class="two">
|
|
<button class="btn sec" id="mStats">STATS<i id="mKills">0</i></button>
|
|
<button class="btn sec" id="mRanks">BOARD<i id="mBest">BEST 0</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 -->
|
|
<!-- THE SETUP IS A WIZARD now (Oliver: one screen of posters + chips + tiles + meters was
|
|
"really overwhelming"). Three stages, tap advances: the mode posters, then the mode's own
|
|
second step when it has one (classic: difficulty, zombie tennis: arcade or the cup), then
|
|
who-and-where with SERVE UP. Every step comes up pre-selected from last time, so a
|
|
returning player is three taps from serving. -->
|
|
<div id="stmode">
|
|
<div class="hdr"><button class="chev" id="mBack"><</button><h2>PICK A GAME</h2></div>
|
|
<div id="modeList"></div>
|
|
</div>
|
|
<div id="stsub">
|
|
<div class="hdr"><button class="chev" id="subBack"><</button><h2 id="subTitle">DIFFICULTY</h2></div>
|
|
<div id="modeSub"></div>
|
|
</div>
|
|
<div id="setup">
|
|
<div class="hdr"><button class="chev" id="sBack"><</button><h2>MATCH SETUP</h2></div>
|
|
<!-- ONE PLAYER BLOCK. It was four stacked sections — two big portrait tiles, then the live
|
|
player, then a kit row, then a skin row — which said the same thing twice (the tiles were
|
|
a second, FRONT-FACING drawing of a man who does not react to any of the colours) and ran
|
|
the court off the bottom of the phone. The tiles are gone: the live player IS the picture,
|
|
so who-you-are shrinks to two words, and every control that changes him stands beside him
|
|
where you can watch it land. Kit and skin rows are built in main.js from the tables. -->
|
|
<h3 id="s01">01 · PLAYER</h3>
|
|
<div id="who">
|
|
<canvas id="look" width="128" height="224"></canvas>
|
|
<div id="whoCtl">
|
|
<div id="tours"></div>
|
|
<u class="swlab">KIT<i id="kitName"></i></u>
|
|
<div id="kitRow" class="swrow"></div>
|
|
<u class="swlab">SKIN<i id="skinName"></i></u>
|
|
<div id="skinRow" class="swrow"></div>
|
|
</div>
|
|
</div>
|
|
<!-- THE LEAGUE PANEL (the leagues): on the league modes this stands where the player block
|
|
does elsewhere — the identity already lives on the card, so the setup shows the thing
|
|
that matters HERE instead: your league, writ large, and who of its band you have
|
|
already felled (all-time, from the stats' own per-foe tallies). Built by
|
|
main.buildLeaguePanel, toggled by syncSetup. -->
|
|
<div id="lgPanel">
|
|
<div id="lgHead">
|
|
<img id="lgBadge" alt="">
|
|
<div><b id="lgName"></b><i id="lgSub"></i></div>
|
|
</div>
|
|
<div id="lgFoes"></div>
|
|
</div>
|
|
<h3 id="s02">02 · COURT</h3>
|
|
<div id="courtList"></div>
|
|
<div class="foot">
|
|
<button class="btn big" id="cStart">SERVE UP</button>
|
|
</div>
|
|
</div>
|
|
<!-- THE IDENTITY (the leagues, phase 1): the first boot asks who walks on — a name (prefilled
|
|
with a zombie name, the die rolls another), the tour and the colours. Everything here
|
|
writes the SAME stores the setup screen reads (stats' name key, the picker localStorage
|
|
keys), so the two screens cannot disagree about who you are. Rebuilt on every visit by
|
|
main.buildPlayer, so the .on states track the setup screen's. -->
|
|
<div id="player">
|
|
<div class="hdr"><button class="chev" id="pBack"><</button><h2>YOUR PLAYER</h2></div>
|
|
<h3>NAME<span>YOURS ON EVERY BOARD</span></h3>
|
|
<div id="nameRow">
|
|
<input id="pName" class="tin" maxlength="12" autocomplete="off" spellcheck="false">
|
|
<button class="btn" id="pDice" aria-label="roll a zombie name">🎲</button>
|
|
</div>
|
|
<h3>THE BODY</h3>
|
|
<div id="who2">
|
|
<canvas id="pLook" width="128" height="224"></canvas>
|
|
<div id="pCtl">
|
|
<div id="pTours"></div>
|
|
<u class="swlab">KIT<i id="pKitName"></i></u>
|
|
<div id="pKitRow" class="swrow"></div>
|
|
<u class="swlab">SKIN<i id="pSkinName"></i></u>
|
|
<div id="pSkinRow" class="swrow"></div>
|
|
</div>
|
|
</div>
|
|
<div class="foot">
|
|
<p class="cent">THE CLUB WILL LEARN YOUR NAME.</p>
|
|
<button class="btn big" id="pGo">ENTER THE CLUB</button>
|
|
</div>
|
|
</div>
|
|
<!-- THE PROMOTION (the leagues): the ceremony between the cup summary and the title screen.
|
|
Built by main.buildPromo from the profile's fresh league; the court tile IS the reward. -->
|
|
<div id="promo">
|
|
<div class="mid cent">
|
|
<p id="prTag">THE CUP IS YOURS</p>
|
|
<h2 id="prTitle">PROMOTED.</h2>
|
|
<p id="prLeague"></p>
|
|
<img id="prCourt" alt="">
|
|
<p id="prCourtName"></p>
|
|
<!-- THE SEASON'S SECOND BEAT (2026-08-31): the promotion is also the season opener, so it
|
|
names the fixture waiting in the new league rather than ending on a court tile. No
|
|
extra screen — a tap between the player and the court is what we were avoiding. -->
|
|
<p id="prNext"></p>
|
|
</div>
|
|
<div class="foot bare">
|
|
<button class="btn big" id="prGo">WALK ON</button>
|
|
</div>
|
|
</div>
|
|
<!-- THE CHARACTER CARD (the leagues): tap the splash's level strip to open. Built by
|
|
main.buildCard from the profile + stats; the honours row shows every badge slot, earned
|
|
bright and unearned dark. -->
|
|
<div id="card">
|
|
<div class="hdr"><button class="chev" id="cdBack"><</button><h2 id="cdName">YOUR PLAYER</h2></div>
|
|
<div id="cdTop">
|
|
<canvas id="cdLook" width="128" height="224"></canvas>
|
|
<div id="cdMeta">
|
|
<img id="cdBadge" alt="">
|
|
<u id="cdLeague"></u>
|
|
<b id="cdLevel">LEVEL 1</b>
|
|
<div id="cdBar"><u id="cdFrom">1</u><s><i id="cdFill"></i></s><u id="cdTo">2</u></div>
|
|
<u id="cdXp"></u>
|
|
<div id="cdCareer"><u>CAREER</u><s><i id="cdCareerFill"></i></s></div>
|
|
</div>
|
|
</div>
|
|
<h3>HONOURS<span>WIN THE CUP TO CLIMB</span></h3>
|
|
<div id="cdBadges"></div>
|
|
<h3>THE LEDGER<span>ALL TIME</span></h3>
|
|
<div class="figs" id="cdFigs1"></div>
|
|
<div style="height:8px"></div>
|
|
<div class="figs" id="cdFigs2"></div>
|
|
<div class="foot bare">
|
|
<button class="btn sec" id="cdEdit">EDIT PLAYER<i>NAME · TOUR · COLOURS</i></button>
|
|
</div>
|
|
</div>
|
|
<!-- THE TRAINING COURT's opponent picker: the whole ladder face-up, tap a body to drill it.
|
|
Built in main.js from config's roster (matchFoe/LADDER), so a new rung needs no markup. -->
|
|
<div id="train">
|
|
<div class="hdr"><button class="chev" id="trBack"><</button><h2>TRAINING</h2></div>
|
|
<div id="trainList"></div>
|
|
</div>
|
|
<!-- VS A FRIEND. The whole lobby: your name (the world board's name — one name per player),
|
|
the host's one decision, and the friend's code. #vsWait replaces the picker in place once
|
|
a socket is open, so the room code and its SHARE button stand where the buttons were and
|
|
the game stays parked behind this overlay until the room fills (main.js, THE LOBBY). -->
|
|
<div id="vs">
|
|
<div class="hdr"><button class="chev" id="vsBack"><</button><h2>VS A FRIEND</h2></div>
|
|
<div id="vsPick">
|
|
<h3>YOU</h3>
|
|
<input id="vsName" class="tin" maxlength="12" placeholder="YOUR NAME" autocomplete="off"
|
|
autocapitalize="characters" spellcheck="false">
|
|
<h3>HOST</h3>
|
|
<button class="btn" id="vsHostC">CLASSIC</button>
|
|
<button class="btn" id="vsHostD">DEATH MATCH</button>
|
|
<h3>JOIN</h3>
|
|
<div class="joinrow">
|
|
<input id="vsCode" class="tin" maxlength="4" placeholder="CODE" autocomplete="off"
|
|
autocapitalize="characters" spellcheck="false">
|
|
<button class="btn" id="vsJoin">GO</button>
|
|
</div>
|
|
<p id="vsErr"></p>
|
|
</div>
|
|
<div id="vsWait" hidden>
|
|
<div class="mid cent">
|
|
<h3 id="vsWHdr" class="cent">ROOM CODE</h3>
|
|
<p id="vsBig">····</p>
|
|
<button class="btn big" id="vsShare">SEND THE CODE</button>
|
|
<p class="none cent" id="vsMsg">WAITING FOR THEM TO JOIN</p>
|
|
</div>
|
|
<div class="foot bare"><button class="btn" id="vsCancel">CANCEL</button></div>
|
|
</div>
|
|
</div>
|
|
<!-- 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"><</button><h2>BODY COUNT</h2></div>
|
|
<!-- NO TABS (2026-08-31). This screen carried three MODE tabs times three PERIOD tabs, and
|
|
the same three boxes meant something different in every one of the nine states. It is
|
|
your career now, in four numbers that always mean the same thing, plus the two things
|
|
the screen is actually good at: the bodies you have collected, and the rack you fired. -->
|
|
<div class="figs pair" id="stFigs"></div>
|
|
<h3>BY OPPONENT<span>ALL TIME</span></h3>
|
|
<div id="stZ"></div>
|
|
<h3>BY WEAPON</h3>
|
|
<div id="stW"></div>
|
|
</div>
|
|
<!-- THREE BOARDS, one per game mode — the three do not share a currency, so they cannot share
|
|
a ranking. Same tab strip as the stats screen's, and stats.drawRanks rewrites the column
|
|
heading with the tab, because the number means something different on each one. -->
|
|
<div id="ranks">
|
|
<div class="hdr"><button class="chev" id="rkBack"><</button><h2>WORLD BOARD</h2></div>
|
|
<!-- TWO boards, not three (2026-08-31): classic tennis is out of competition by design now
|
|
— no league, no record, no stakes — so ranking it contradicted the room it lives in. -->
|
|
<div class="tabs" id="rkModes">
|
|
<button class="on" data-m="u">THE CUP</button>
|
|
<button data-m="g">SPLATTER</button>
|
|
<button data-m="z">THE CLIMB</button>
|
|
</div>
|
|
<h3 id="rkHdr">BEST SINGLE RUN<span>KILLS · GAMES</span></h3>
|
|
<div id="rkList"></div>
|
|
</div>
|
|
<!-- THE BACK ROOM. Five taps on the logo (main.js) and nothing else — no button anywhere hints
|
|
at it, which is the whole point: it replaced a triple-tap on the in-play scoreboard that
|
|
players kept tripping by accident. -->
|
|
<div id="secret">
|
|
<div class="hdr"><button class="chev" id="scBack"><</button><h2>BACK ROOM</h2></div>
|
|
<div class="foot bare">
|
|
<button class="btn big" id="scWeapons">WEAPON TEST</button>
|
|
<button class="btn big" id="scBosses">BOSS TEST</button>
|
|
<button class="btn big" id="scDrill">DRILL TEST</button>
|
|
<button class="btn big" id="scMatch">MATCH TEST</button>
|
|
<button class="btn big" id="scBench">SPRITE BENCH</button>
|
|
</div>
|
|
</div>
|
|
<!-- one tile per chapter boss, built from config.bossArt in main.js: adding a design to the
|
|
roster puts it on this screen and never touches this markup -->
|
|
<div id="bosses">
|
|
<div class="hdr"><button class="chev" id="bsBack"><</button><h2>BOSS TEST</h2></div>
|
|
<h3>PICK A CHAPTER<span>FULL ARSENAL</span></h3>
|
|
<div id="bossList"></div>
|
|
</div>
|
|
<!-- shown only when a run beats your own best kill count — see main.js askName -->
|
|
<div id="newbest">
|
|
<div class="mid">
|
|
<h2 class="cent">NEW PERSONAL BEST</h2>
|
|
<p id="nbScore"></p>
|
|
<input id="nbName" maxlength="12" placeholder="YOUR NAME" autocomplete="off"
|
|
autocapitalize="characters" spellcheck="false">
|
|
<p class="none cent" id="nbMsg">PUT IT ON THE WORLD BOARD.</p>
|
|
</div>
|
|
<div class="foot bare">
|
|
<button class="btn big" id="nbSend">SIGN THE BOARD</button>
|
|
<button class="btn" id="nbSkip">NOT THIS TIME</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script type="module" src="src/main.js?v=343"></script>
|
|
</body>
|
|
</html>
|