The boot shows nothing, and every screen opens at its top

Two things the phone found. The splash's loading state was applied by main.js —
but this is a module script, so the browser paints the markup, placeholders and
all, before it runs: LEVEL 1 stood there and then snapped to the real values.
The busy class ships in index.html now and main.js only takes it off; the
keyboard hints hide with it.

And #intro is the one scroller the menus share, so a scroll earned deep in the
stats screen outlived it: going back put the splash up scrolled, with its
buttons off screen. go() resets it, at the funnel every stage change passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-31 13:19:26 +02:00
1 parent 3d9400cd25
commit 380f98f3a3
3 files changed
+34 -3

No files matched your search

+11
View File
@@ -589,6 +589,17 @@ door beside it lost its subject problem: an unqualified `PLAY AGAIN` next to it
"start something", so it names its mode — `GORE AGAIN` / `CLIMB AGAIN` / `CLASSIC AGAIN` / "start something", so it names its mode — `GORE AGAIN` / `CLIMB AGAIN` / `CLASSIC AGAIN` /
`TRAIN AGAIN`, derived in the summary from G.classic / G.match.train / G.match. `TRAIN AGAIN`, derived in the summary from G.classic / G.match.train / G.match.
**THE BOOT STATE SHIPS IN THE MARKUP.** Hiding the plaque behind `#splash.busy` was not enough:
this is a MODULE script, so the browser paints index.html — placeholders and all — before main.js
can add the class, and Oliver kept seeing `LEVEL 1 · LEAGUE 1` snap to the real values. `class="busy"`
is in the HTML now and main.js only ever takes it off. The keyboard-hint strip hides with it
(`body:has(#splash.busy) #hint`), so the loading screen really is court, title and bar.
**EVERY SCREEN OPENS AT ITS TOP.** `#intro` is the one scroller the menus share, so a scroll earned
on the stats screen outlived it — go back and the splash came up scrolled, buttons off screen
(Oliver). `go()` resets `intro.scrollTop`, at the single funnel every stage change already passes
through.
Open for the phone: whether the fixture button should ever go to the setup screen (it does not Open for the phone: whether the fixture button should ever go to the setup screen (it does not
now), and whether the promotion's WALK ON should enter the cup instead of the clubhouse. now), and whether the promotion's WALK ON should enter the cup instead of the clubhouse.
+9 -1
View File
@@ -116,6 +116,9 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
.qBox p { margin: 0 0 6px; font: 400 12px Silkscreen, monospace; color: var(--cream); .qBox p { margin: 0 0 6px; font: 400 12px Silkscreen, monospace; color: var(--cream);
text-align: center; letter-spacing: 1px; } text-align: center; letter-spacing: 1px; }
@media (pointer: coarse) { #hint { display: none; } } @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 /* ---- 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 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. */ the thing you actually came here to press. Five names so a repaint is five edits, not fifty. */
@@ -643,7 +646,12 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
same backdrop with two buttons, so it read as the title screen failing to advance. --> 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 <!-- three bands: the figures at the top, the title floating in the middle, the buttons under
your thumb. Same anchoring as every other screen. --> your thumb. Same anchoring as every other screen. -->
<div id="splash"> <!-- 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 id="hot"></div>
<div class="mid"> <div class="mid">
<img class="logo" src="assets/logo.png" alt="Splatter Tennis"> <img class="logo" src="assets/logo.png" alt="Splatter Tennis">
+14 -2
View File
@@ -84,7 +84,17 @@ let pickedTour = remembered('tour') || TOURS[0].id;
// every stage change is a gesture, so it is also a safe place to (re)start the theme. The setup // every stage change is a gesture, so it is also a safe place to (re)start the theme. The setup
// screen syncs its picker rows on entry: the identity screen writes the same picked* state, and // screen syncs its picker rows on entry: the identity screen writes the same picked* state, and
// rows that only ever toggle on their OWN taps would keep showing the boot-time selection. // rows that only ever toggle on their OWN taps would keep showing the boot-time selection.
const go = stage => { if (stage === 'setup') syncSetup(); if (stage === 'train') buildTrain(); intro.dataset.stage = stage; menuMusic(); }; // EVERY SCREEN OPENS AT ITS TOP (2026-08-31, Oliver: scroll the stats to the bottom, swipe back,
// and the splash comes up scrolled with its buttons off screen). #intro is the one scroller the
// menus share, so its offset outlives the stage that earned it unless it is reset here — the one
// place every stage change goes through.
const go = stage => {
if (stage === 'setup') syncSetup();
if (stage === 'train') buildTrain();
intro.dataset.stage = stage;
intro.scrollTop = 0;
menuMusic();
};
// --- MODE selection in TWO STEPS (config.MENU + config.MODES): three posters on top, and the // --- MODE selection in TWO STEPS (config.MENU + config.MODES): three posters on top, and the
// picked one deals its own second step underneath — classic deals the difficulty chips, zombie // picked one deals its own second step underneath — classic deals the difficulty chips, zombie
@@ -976,7 +986,9 @@ setTimeout(() => { profileDone = true; }, 4000);
// whatever the fallback was. onerror counts too (render.js), so a dead network still finishes the // whatever the fallback was. onerror counts too (render.js), so a dead network still finishes the
// bar rather than parking the player on it forever. // bar rather than parking the player on it forever.
const splash = document.getElementById('splash'), bootFill = document.querySelector('#boot i'); const splash = document.getElementById('splash'), bootFill = document.querySelector('#boot i');
splash.classList.add('busy'); splash.classList.add('busy'); // belt and braces: the class ships in index.html so the very
// first paint is already the loading state — this only matters if
// that ever gets dropped from the markup
const loaded = () => !splash.classList.contains('busy'); const loaded = () => !splash.classList.contains('busy');
// setInterval, NOT requestAnimationFrame: rAF does not fire in a hidden tab, and a phone that // setInterval, NOT requestAnimationFrame: rAF does not fire in a hidden tab, and a phone that
// launches the app and locks — or an install that loads in the background — would come back to a // launches the app and locks — or an install that loads in the background — would come back to a