diff --git a/index.html b/index.html index 86263d8..84c5e1c 100644 --- a/index.html +++ b/index.html @@ -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') { - + diff --git a/src/main.js b/src/main.js index 725a9db..ce48241 100644 --- a/src/main.js +++ b/src/main.js @@ -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 = `${card.name}${card.sub}`; + b.innerHTML = `${card.name}${card.sub}` + + (last ? 'LAST' : ''); 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 ? 'LAST' : ''); 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; }