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'));
startLabel.textContent = STAGES[stage].startLabel;
if (persist) saveState();
scheduleFit(); // font swap (e.g. Metal Mania) changes height → refit
}
/* ---------- Setup interactions ---------- */
@ -424,9 +425,34 @@ function applyPlayerCamps() {
function showScreen(screen) {
[nameScreen, gameScreen, winnerScreen].forEach((s) => s.classList.remove('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 ---------- */
function startGame() {
player1Name = player1Input.value.trim() || 'Player 1';
@ -520,5 +546,6 @@ function init() {
applyStage(currentStage, { persist: false });
applyPlayerCamps();
updatePlayerHighlight();
scheduleFit();
}
init();

View file

@ -110,18 +110,22 @@
.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); }
/* ---------- 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 {
font-family: var(--body-font);
background: var(--bg);
background-size: var(--bg-size);
animation: gradientShift 15s ease infinite;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
height: 100dvh;
min-height: 100dvh;
display: grid;
place-items: center;
overflow: hidden;
position: relative;
padding: 28px 16px;
padding: 8px;
color: var(--text);
transition: color 0.4s ease;
}
@ -230,6 +234,10 @@ body {
backdrop-filter: blur(10px);
border: 3px solid var(--surface-border);
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; }