A poster answers, a border promises: the question screens drop their checkbox half

Oliver named the wizard's wrongness precisely: a pre-highlighted card that
also navigates is "a checkbox and a button combined in one". The border said
"this is a setting", the tap said "this was an answer", and the screen left
before the setting-reading of it could cash out.

So the two roles live on different screens now. The question screens (PICK A
GAME, DIFFICULTY, the zombie pair) carry NO selection: every poster and chip
is full-bright and equal — the dimming went too, it read as disabled — and
tapping answers and advances. The remembered run survives only as a small
LAST tag in the card's corner: an indicator, not a selection. The final
screen keeps its acid borders honestly, because SERVE UP is a real confirm —
the rule is "a border means SERVE UP will use this; a bright poster means
tap me to answer". Question screens rebuild on entry so LAST follows what
was actually played, and the first card clears the header by the margin the
old h3 rows used to give (Oliver's padding note).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-14 11:45:50 +02:00
1 parent 65be60b139
commit 6b795f77cc
2 files changed
+47 -33

No files matched your search

+14 -10
View File
@@ -221,16 +221,19 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
letter-spacing: 1px; }
/* 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 buttons are the BANNER row: the whole button is a bright scene of its mode (main.js sets
the art + a scrim gradient as the background stack) with the caption written over the bottom.
Unpicked buttons sit dimmed and desaturated so the chosen mode is the one that glows — the
same "one bright thing means selected" rule the menu palette already follows. */
/* 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;
filter: saturate(0.45) brightness(0.62); transition: filter 0.15s; }
#modeList button.mode.on, #modeSub button.mode.on { border-color: var(--acid); filter: none; }
position: relative; }
.last { position: absolute; top: 7px; right: 9px; font: 400 8px Silkscreen, monospace;
color: var(--acid); background: rgba(0,0,0,0.72); padding: 3px 6px;
letter-spacing: 1px; border: 1px solid rgba(188,212,85,0.5); }
#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"
@@ -240,14 +243,15 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
/* 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 */
/* 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%; padding: 22px 0; background: var(--panel); border: 2px solid var(--line);
color: var(--cream); font: 400 13px Silkscreen, monospace; letter-spacing: 2px;
cursor: pointer; -webkit-tap-highlight-color: transparent; }
#modeSub button.chip.on { border-color: var(--acid); color: var(--acid); background: rgba(188,212,85,0.09); }
cursor: pointer; -webkit-tap-highlight-color: transparent; position: relative; }
#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;
@@ -467,6 +471,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
</div>
</div>
</div>
<script type="module" src="src/main.js?v=234"></script>
<script type="module" src="src/main.js?v=235"></script>
</body>
</html>
+33 -23
View File
@@ -82,17 +82,22 @@ const modeOf = id => MODES.find(x => x.id === id);
const topOf = m => MENU.find(t => t.mode === m || (t.kids && t.kids.includes(m))) || MENU[0];
const modeList = document.getElementById('modeList');
const modeSub = document.getElementById('modeSub');
const banner = (card, cls, on) => {
// QUESTION SCREENS CARRY NO SELECTION (Oliver: a highlighted card that also navigates is "a
// checkbox and a button combined"). Every poster is full-bright and equal — all of them are live
// answers — and the only memory shown is the LAST tag, an indicator rather than a selection. The
// select-and-confirm border lives solely on the final screen, where SERVE UP actually confirms.
const banner = (card, cls, last) => {
const b = document.createElement('button');
b.className = cls + (on ? ' on' : '');
b.className = cls;
// the scrim rides IN the background stack (gradient over art), so the caption needs no extra box
b.style.backgroundImage =
`linear-gradient(180deg, rgba(0,0,0,0) 34%, rgba(0,0,0,0.78)), url(assets/${card.art}.png?v=${SPRV})`;
b.innerHTML = `<span class="cb"><b>${card.name}</b><i>${card.sub}</i></span>`;
b.innerHTML = `<span class="cb"><b>${card.name}</b><i>${card.sub}</i></span>`
+ (last ? '<u class="last">LAST</u>' : '');
return b;
};
const pickMode = id => { pickedMode = id; try { localStorage.setItem('mode', id); } catch {} };
// TAP ADVANCES. Each screen's tap both selects and moves on — the wizard's whole promise is one
// TAP ADVANCES. Each screen's tap both answers and moves on — the wizard's whole promise is one
// decision per screen, and a NEXT button under a single row of choices is a second tap for nothing.
function buildSub() {
modeSub.innerHTML = '';
@@ -100,12 +105,11 @@ function buildSub() {
document.getElementById('subTitle').textContent = t.diffs ? 'DIFFICULTY' : (t.name || '');
if (t.diffs) for (const d of DIFFS) {
const b = document.createElement('button');
b.className = 'chip' + (d.id === pickedDiff ? ' on' : '');
b.textContent = d.name;
b.className = 'chip';
b.innerHTML = d.name + (d.id === pickedDiff ? '<u class="last">LAST</u>' : '');
b.addEventListener('click', () => {
pickedDiff = d.id;
try { localStorage.setItem('diff', d.id); } catch {}
for (const el of modeSub.children) el.classList.toggle('on', el === b);
summary();
go('setup');
});
@@ -115,25 +119,28 @@ function buildSub() {
const b = banner(modeOf(id), 'mode', id === pickedMode);
b.addEventListener('click', () => {
pickMode(id);
for (const el of modeSub.children) el.classList.toggle('on', el === b);
summary();
go('setup');
});
modeSub.appendChild(b);
}
}
for (const t of MENU) {
const b = banner(t.mode ? modeOf(t.mode) : t, 'mode', topOf(pickedMode) === t);
b.addEventListener('click', () => {
// a group remembers which child you were on; entering it fresh lands on its first card
pickMode(t.mode || (t.kids.includes(pickedMode) ? pickedMode : t.kids[0]));
for (const el of modeList.children) el.classList.toggle('on', el === b);
buildSub();
summary();
go(t.diffs || t.kids ? 'modesub' : 'setup');
});
modeList.appendChild(b);
// the top posters are rebuilt on every visit so the LAST tag follows what was actually played
function buildTops() {
modeList.innerHTML = '';
for (const t of MENU) {
const b = banner(t.mode ? modeOf(t.mode) : t, 'mode', topOf(pickedMode) === t);
b.addEventListener('click', () => {
// a group remembers which child you were on; entering it fresh lands on its first card
pickMode(t.mode || (t.kids.includes(pickedMode) ? pickedMode : t.kids[0]));
buildSub();
summary();
go(t.diffs || t.kids ? 'modesub' : 'setup');
});
modeList.appendChild(b);
}
}
buildTops();
buildSub();
// --- tour tiles, built from the table. Same "tap selects, SERVE UP starts" rule as the court rows —
@@ -238,7 +245,9 @@ const openStats = () => {
for (const el of document.getElementById('stModes').children) el.classList.toggle('on', el.dataset.m === stMode);
stats.drawStats(stPeriod, SPRV, stMode); go('stats');
};
document.getElementById('mStart').addEventListener('click', () => go('mode'));
// entering a question screen rebuilds it, so the LAST tag always names the run actually played
const goMode = () => { buildTops(); go('mode'); };
document.getElementById('mStart').addEventListener('click', goMode);
document.getElementById('mStats').addEventListener('click', openStats);
document.getElementById('mRanks').addEventListener('click', () => { go('ranks'); stats.drawRanks(); });
@@ -291,10 +300,11 @@ for (const id of ['mBack', 'stBack', 'rkBack', 'scBack'])
document.getElementById(id).addEventListener('click', () => go('splash'));
// the wizard walks back the way it came: who-and-where returns to the mode's own second step when
// it has one, the second step returns to the posters
document.getElementById('subBack').addEventListener('click', () => go('mode'));
document.getElementById('subBack').addEventListener('click', goMode);
document.getElementById('sBack').addEventListener('click', () => {
const t = topOf(pickedMode);
go(t.diffs || t.kids ? 'modesub' : 'mode');
if (t.diffs || t.kids) { buildSub(); go('modesub'); }
else goMode();
});
document.getElementById('bsBack').addEventListener('click', () => go('secret'));
@@ -423,7 +433,7 @@ addEventListener('keydown', e => {
if (e.key === 'Escape') return go('splash');
if (stage === 'splash') {
if (!loaded()) return; // the boot bar hides the buttons; a key must not walk past it
if (e.key === 'Enter' || e.key === ' ') go('mode');
if (e.key === 'Enter' || e.key === ' ') goMode();
else if (e.key === 's' || e.key === 'S') openStats();
return;
}