Make it full-screen: no scrolling, ever

Lock the viewport (100dvh, overflow:hidden, grid-centered) and scale the
active card to fit via a --fit custom property set by fitActiveScreen().
Every screen — setup, game, winner — now always fits the browser at any
window size; the card shrinks only when it would otherwise overflow.
Refit on screen change, stage switch, resize, orientation, and font load.

Verified no vertical/horizontal scroll on all three screens at short-laptop
(1366x640) and mobile-portrait (390x844).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Parley Hatch 2026-05-31 18:40:27 -06:00
parent bba0a06a71
commit aa2c3b989d
2 changed files with 42 additions and 7 deletions

View file

@ -230,6 +230,7 @@ function applyStage(stage, { persist = true } = {}) {
opt.setAttribute('aria-checked', opt.dataset.stage === stage ? 'true' : 'false')); opt.setAttribute('aria-checked', opt.dataset.stage === stage ? 'true' : 'false'));
startLabel.textContent = STAGES[stage].startLabel; startLabel.textContent = STAGES[stage].startLabel;
if (persist) saveState(); if (persist) saveState();
scheduleFit(); // font swap (e.g. Metal Mania) changes height → refit
} }
/* ---------- Setup interactions ---------- */ /* ---------- Setup interactions ---------- */
@ -424,9 +425,34 @@ function applyPlayerCamps() {
function showScreen(screen) { function showScreen(screen) {
[nameScreen, gameScreen, winnerScreen].forEach((s) => s.classList.remove('active')); [nameScreen, gameScreen, winnerScreen].forEach((s) => s.classList.remove('active'));
screen.classList.add('active'); screen.classList.add('active');
window.scrollTo({ top: 0, behavior: 'smooth' }); scheduleFit();
} }
/* ---------- Fit-to-viewport ----------
Scale the active card (via the --fit custom prop) so the whole screen
always fits it's tic-tac-toe, no scrolling, ever. */
let fitRAF = 0;
function scheduleFit() {
if (fitRAF) cancelAnimationFrame(fitRAF);
fitRAF = requestAnimationFrame(() => { fitRAF = 0; fitActiveScreen(); });
}
function fitActiveScreen() {
const screen = document.querySelector('.screen.active');
if (!screen) return;
const card = screen.querySelector('.card');
if (!card) return;
// offsetWidth/Height ignore transforms, so we always read the natural size.
const w = card.offsetWidth;
const h = card.offsetHeight;
if (!w || !h) return;
const margin = 16; // small breathing room around the card
const scale = Math.min(1, (window.innerWidth - margin) / w, (window.innerHeight - margin) / h);
card.style.setProperty('--fit', String(scale));
}
window.addEventListener('resize', scheduleFit);
window.addEventListener('orientationchange', scheduleFit);
if (document.fonts && document.fonts.ready) document.fonts.ready.then(scheduleFit);
/* ---------- Lifecycle ---------- */ /* ---------- Lifecycle ---------- */
function startGame() { function startGame() {
player1Name = player1Input.value.trim() || 'Player 1'; player1Name = player1Input.value.trim() || 'Player 1';
@ -520,5 +546,6 @@ function init() {
applyStage(currentStage, { persist: false }); applyStage(currentStage, { persist: false });
applyPlayerCamps(); applyPlayerCamps();
updatePlayerHighlight(); updatePlayerHighlight();
scheduleFit();
} }
init(); init();

View file

@ -110,18 +110,22 @@
.camp-sparkle { --pc: var(--sparkle-color); --pg: var(--sparkle-glow); --pt: var(--sparkle-tint); } .camp-sparkle { --pc: var(--sparkle-color); --pg: var(--sparkle-glow); --pt: var(--sparkle-tint); }
.camp-shred { --pc: var(--shred-color); --pg: var(--shred-glow); --pt: var(--shred-tint); } .camp-shred { --pc: var(--shred-color); --pg: var(--shred-glow); --pt: var(--shred-tint); }
/* ---------- Base layout (scroll-safe centering) ---------- */ /* ---------- Base layout (full-screen, never scroll) ----------
The viewport is locked to the screen; the active card is scaled by JS
(--fit) to always fit. It's tic-tac-toe everything stays in view. */
html { height: 100%; }
body { body {
font-family: var(--body-font); font-family: var(--body-font);
background: var(--bg); background: var(--bg);
background-size: var(--bg-size); background-size: var(--bg-size);
animation: gradientShift 15s ease infinite; animation: gradientShift 15s ease infinite;
min-height: 100vh; height: 100dvh;
display: flex; min-height: 100dvh;
flex-direction: column; display: grid;
overflow-x: hidden; place-items: center;
overflow: hidden;
position: relative; position: relative;
padding: 28px 16px; padding: 8px;
color: var(--text); color: var(--text);
transition: color 0.4s ease; transition: color 0.4s ease;
} }
@ -230,6 +234,10 @@ body {
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border: 3px solid var(--surface-border); border: 3px solid var(--surface-border);
transition: background 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease; transition: background 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease;
/* Scaled to fit the viewport by fitActiveScreen() in script.js */
transform: scale(var(--fit, 1));
transform-origin: center center;
will-change: transform;
} }
.name-card { padding: 30px 30px 34px; } .name-card { padding: 30px 30px 34px; }