Actually fix no-scroll: fixed-centered card scaled to fit + cache busting
My previous 'no scrolling' commit didn't actually work — two real bugs: 1. Stale assets: nginx cached css/js for a week (max-age=604800), so after a redeploy returning visitors kept the OLD stylesheet/script and never saw the fit logic. Switched assets to Cache-Control:no-cache (tiny files, 304s) and added ?v= query strings to bust the existing week-long cache. 2. Bad centering geometry: grid-centering pins an over-tall card to the top, and .winner-card re-declared position:relative, overriding the fix. Now the card is position:fixed at 50%/50% and fitActiveScreen() folds the fit scale into one transform: translate(-50%,-50%) scale(fit). Removed the winner-card position override and switched the screen fade to opacity-only (a transform on .screen would capture the fixed card and break centering). Verified in real Chromium against the container — setup, game, AND winner all show fits:true / scrolls:false at 1366x640 (short laptop), 375x667, and 360x640 (small phone). No scrolling on any screen at any size. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aa2c3b989d
commit
450d46c55d
4 changed files with 33 additions and 12 deletions
|
|
@ -8,7 +8,7 @@
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Fredoka:wght@500;600;700&family=Metal+Mania&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Fredoka:wght@500;600;700&family=Metal+Mania&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css?v=5">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Animated backdrop: stars (sparkle) + embers/lightning (shred); both show on mixed -->
|
<!-- Animated backdrop: stars (sparkle) + embers/lightning (shred); both show on mixed -->
|
||||||
|
|
@ -165,6 +165,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="script.js"></script>
|
<script src="script.js?v=5"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,15 @@ server {
|
||||||
gzip_types text/css application/javascript image/svg+xml;
|
gzip_types text/css application/javascript image/svg+xml;
|
||||||
gzip_min_length 256;
|
gzip_min_length 256;
|
||||||
|
|
||||||
# Static assets: cache for a week (filenames are stable for this app).
|
# Assets use stable filenames and we redeploy in place, so revalidate on
|
||||||
|
# every load (cheap — tiny files, 304s) instead of caching for a week.
|
||||||
|
# A week-long cache here means redeploys leave returning players on stale
|
||||||
|
# CSS/JS until expiry; no-cache avoids that footgun.
|
||||||
location ~* \.(css|js|svg|png|ico)$ {
|
location ~* \.(css|js|svg|png|ico)$ {
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||||
add_header Cache-Control "public, max-age=604800";
|
add_header Cache-Control "no-cache" always;
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTML entry point: always revalidate so redeploys show up immediately.
|
# HTML entry point: always revalidate so redeploys show up immediately.
|
||||||
|
|
|
||||||
|
|
@ -442,12 +442,17 @@ function fitActiveScreen() {
|
||||||
const card = screen.querySelector('.card');
|
const card = screen.querySelector('.card');
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
// offsetWidth/Height ignore transforms, so we always read the natural size.
|
// offsetWidth/Height ignore transforms, so we always read the natural size.
|
||||||
|
// offsetWidth/Height are layout sizes (they ignore transforms), so we
|
||||||
|
// always measure the card's natural size even after a prior scale.
|
||||||
const w = card.offsetWidth;
|
const w = card.offsetWidth;
|
||||||
const h = card.offsetHeight;
|
const h = card.offsetHeight;
|
||||||
if (!w || !h) return;
|
if (!w || !h) return;
|
||||||
const margin = 16; // small breathing room around the card
|
const margin = 16; // small breathing room around the card
|
||||||
const scale = Math.min(1, (window.innerWidth - margin) / w, (window.innerHeight - margin) / h);
|
const scale = Math.min(1, (window.innerWidth - margin) / w, (window.innerHeight - margin) / h);
|
||||||
card.style.setProperty('--fit', String(scale));
|
// Fold the fit scale into the centering translate. The card is position:fixed
|
||||||
|
// at top/left 50%, so translate(-50%,-50%) centers it on the viewport and the
|
||||||
|
// scale shrinks it to fit — together, no scrolling at any size.
|
||||||
|
card.style.transform = `translate(-50%, -50%) scale(${scale})`;
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', scheduleFit);
|
window.addEventListener('resize', scheduleFit);
|
||||||
window.addEventListener('orientationchange', scheduleFit);
|
window.addEventListener('orientationchange', scheduleFit);
|
||||||
|
|
|
||||||
27
style.css
27
style.css
|
|
@ -113,7 +113,7 @@
|
||||||
/* ---------- Base layout (full-screen, never scroll) ----------
|
/* ---------- Base layout (full-screen, never scroll) ----------
|
||||||
The viewport is locked to the screen; the active card is scaled by JS
|
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. */
|
(--fit) to always fit. It's tic-tac-toe — everything stays in view. */
|
||||||
html { height: 100%; }
|
html { height: 100%; overflow: hidden; }
|
||||||
body {
|
body {
|
||||||
font-family: var(--body-font);
|
font-family: var(--body-font);
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
|
|
@ -219,12 +219,14 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Cards & screens ---------- */
|
/* ---------- Cards & screens ---------- */
|
||||||
.screen { display: none; animation: fadeIn 0.5s ease; }
|
/* Opacity-only fade: a transform on .screen would become the containing block
|
||||||
|
for the fixed-positioned card and break its viewport centering mid-animation. */
|
||||||
|
.screen { display: none; animation: fadeIn 0.4s ease; }
|
||||||
.screen.active { display: block; }
|
.screen.active { display: block; }
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; transform: scale(0.96) translateY(10px); }
|
from { opacity: 0; }
|
||||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
|
@ -234,15 +236,26 @@ 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 */
|
/* Fixed + viewport-centered so a card taller than the screen still centers
|
||||||
transform: scale(var(--fit, 1));
|
(grid centering pins an over-tall item to the top). fitActiveScreen() in
|
||||||
|
script.js folds the fit scale into the transform:
|
||||||
|
translate(-50%, -50%) scale(fit)
|
||||||
|
so the whole card always fits with no scrolling, at any window size. */
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: min(var(--maxw), calc(100vw - 16px));
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
transform-origin: center center;
|
transform-origin: center center;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name-card { padding: 30px 30px 34px; }
|
.name-card { padding: 30px 30px 34px; }
|
||||||
.game-card { padding: 36px; }
|
.game-card { padding: 36px; }
|
||||||
.winner-card { padding: 56px 40px; text-align: center; position: relative; overflow: hidden; }
|
/* No position override here — inherit .card's position:fixed so the card stays
|
||||||
|
viewport-centered. fixed is still a positioning context for .celebration. */
|
||||||
|
.winner-card { padding: 56px 40px; text-align: center; overflow: hidden; }
|
||||||
|
|
||||||
/* ---------- Headings ---------- */
|
/* ---------- Headings ---------- */
|
||||||
.title, .game-title, .winner-title {
|
.title, .game-title, .winner-title {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue