Mobile responsive pass
- PixelLogo scales to viewport via a responsive hook so FACERE no longer overflows narrow screens. - styles.css mobile breakpoints (760px / 480px / short-landscape) unwrap headlines/body copy, compact the HUD, allow the install command to wrap on tiny phones, and re-aim the bg pan origins so the meaningful slice of each landscape image stays in frame. - Shorten scroll-track to 480vh on phones — the GSAP timeline still scrubs by progress so the choreography is preserved, just less exhausting to swipe through. - Bump cache-busting query string on css/jsx. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
47
app.jsx
47
app.jsx
@@ -68,11 +68,46 @@ const GLYPHS = {
|
||||
],
|
||||
};
|
||||
|
||||
function PixelLogo({ word = "FACERE", scale = 11, gap = 0 }) {
|
||||
function useResponsiveLogoScale(word = "FACERE") {
|
||||
const [scale, setScale] = useState(() => computeLogoScale(word));
|
||||
useEffect(() => {
|
||||
let raf = 0;
|
||||
const onResize = () => {
|
||||
cancelAnimationFrame(raf);
|
||||
raf = requestAnimationFrame(() => setScale(computeLogoScale(word)));
|
||||
};
|
||||
window.addEventListener("resize", onResize);
|
||||
window.addEventListener("orientationchange", onResize);
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
window.removeEventListener("resize", onResize);
|
||||
window.removeEventListener("orientationchange", onResize);
|
||||
};
|
||||
}, [word]);
|
||||
return scale;
|
||||
}
|
||||
|
||||
function computeLogoScale(word) {
|
||||
// Total logo width = letters * GLYPH_W * scale + (letters-1) * scale * 1.6
|
||||
// Solve for scale that fits the viewport with side margins.
|
||||
if (typeof window === "undefined") return 11;
|
||||
const vw = window.innerWidth;
|
||||
const letters = word.length;
|
||||
const sideMargin = vw < 480 ? 0.10 : vw < 760 ? 0.12 : 0.16; // each side
|
||||
const usable = vw * (1 - 2 * sideMargin);
|
||||
const denom = letters * GLYPH_W + (letters - 1) * 1.6;
|
||||
const fit = Math.floor(usable / denom);
|
||||
// Clamp so it doesn't get too small or absurdly large on ultrawide.
|
||||
return Math.max(4, Math.min(11, fit));
|
||||
}
|
||||
|
||||
function PixelLogo({ word = "FACERE", scale, gap = 0 }) {
|
||||
const responsiveScale = useResponsiveLogoScale(word);
|
||||
const px = scale ?? responsiveScale;
|
||||
const letters = word.split("");
|
||||
return (
|
||||
<div className="pixel-logo" data-text={word}>
|
||||
<div className="pixel-logo-grid" style={{ gap: `0 ${scale * 1.6}px` }}>
|
||||
<div className="pixel-logo-grid" style={{ gap: `0 ${px * 1.6}px` }}>
|
||||
{letters.map((ch, li) => {
|
||||
const g = GLYPHS[ch];
|
||||
if (!g) return null;
|
||||
@@ -81,8 +116,8 @@ function PixelLogo({ word = "FACERE", scale = 11, gap = 0 }) {
|
||||
key={li}
|
||||
className="pixel-letter"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${GLYPH_W}, ${scale}px)`,
|
||||
gridTemplateRows: `repeat(${GLYPH_H}, ${scale}px)`,
|
||||
gridTemplateColumns: `repeat(${GLYPH_W}, ${px}px)`,
|
||||
gridTemplateRows: `repeat(${GLYPH_H}, ${px}px)`,
|
||||
gap: `${gap}px`,
|
||||
}}
|
||||
>
|
||||
@@ -100,10 +135,10 @@ function PixelLogo({ word = "FACERE", scale = 11, gap = 0 }) {
|
||||
</div>
|
||||
{/* Ghost layers for subtle electric jitter */}
|
||||
<div className="pixel-logo-ghost ghost-r" aria-hidden="true">
|
||||
<PixelLogoStatic word={word} scale={scale} gap={gap} color="rgba(255,80,120,0.45)" />
|
||||
<PixelLogoStatic word={word} scale={px} gap={gap} color="rgba(255,80,120,0.45)" />
|
||||
</div>
|
||||
<div className="pixel-logo-ghost ghost-c" aria-hidden="true">
|
||||
<PixelLogoStatic word={word} scale={scale} gap={gap} color="rgba(33,234,255,0.55)" />
|
||||
<PixelLogoStatic word={word} scale={px} gap={gap} color="rgba(33,234,255,0.55)" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600&family=VT323&display=swap" rel="stylesheet" />
|
||||
|
||||
<link rel="stylesheet" href="styles.css?v=20260502b" />
|
||||
<link rel="stylesheet" href="styles.css?v=20260503-mobile" />
|
||||
|
||||
<!-- Preload background images -->
|
||||
<link rel="preload" as="image" href="assets/exterior-factory.png" />
|
||||
@@ -28,6 +28,6 @@
|
||||
<script src="https://unpkg.com/gsap@3.12.5/dist/gsap.min.js"></script>
|
||||
<script src="https://unpkg.com/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
|
||||
|
||||
<script type="text/babel" src="app.jsx?v=20260502b"></script>
|
||||
<script type="text/babel" src="app.jsx?v=20260503-mobile"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
100
styles.css
100
styles.css
@@ -905,31 +905,103 @@ a { color: inherit; text-decoration: none; }
|
||||
|
||||
/* ───────────────── Mobile recompose ───────────────── */
|
||||
@media (max-width: 760px) {
|
||||
/* Shorter scroll on phones — 700vh of swiping is exhausting.
|
||||
The GSAP timeline scrubs by progress (0..1) so the choreography
|
||||
still plays out, just over a shorter total scroll distance. */
|
||||
.scroll-track { height: 480vh; }
|
||||
|
||||
/* Re-aim the camera so the meaningful part of each landscape image
|
||||
stays roughly in frame on a portrait viewport. */
|
||||
.bg-wrap { transform-origin: 70% 60% !important; }
|
||||
.scene-product .bg-wrap { transform-origin: 55% 70% !important; }
|
||||
.scene-install .bg-wrap { transform-origin: 70% 55% !important; }
|
||||
|
||||
/* Hero: center the (now responsively sized) logo. */
|
||||
.content-hero {
|
||||
align-items: flex-start;
|
||||
padding: 22vh 8vw 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 6vw;
|
||||
}
|
||||
.pixel-letter > * { /* allow scaling? handled by JS, here just nudge */ }
|
||||
.bg-wrap { transform-origin: 75% 70% !important; }
|
||||
.scene-product .bg-wrap { transform-origin: 60% 80% !important; }
|
||||
.scene-install .bg-wrap { transform-origin: 70% 65% !important; }
|
||||
|
||||
.content-product, .content-install {
|
||||
padding: 18vh 8vw 0;
|
||||
padding: 14vh 7vw 14vh;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.22em;
|
||||
margin-bottom: 22px;
|
||||
padding: 7px 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.headline {
|
||||
font-size: clamp(30px, 8.5vw, 52px);
|
||||
white-space: normal;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.body, .subheadline {
|
||||
white-space: normal;
|
||||
max-width: 100%;
|
||||
}
|
||||
.headline { font-size: clamp(34px, 9vw, 56px); white-space: normal; }
|
||||
.body, .subheadline { white-space: normal; max-width: 100%; }
|
||||
.cmd-body { font-size: 12px; padding: 14px 14px 16px; }
|
||||
.body { font-size: clamp(15px, 4vw, 18px); line-height: 1.5; }
|
||||
.subheadline { font-size: 13px; letter-spacing: 0.16em; margin-bottom: 22px; }
|
||||
|
||||
.cmd-box { font-size: 14px; max-width: 100%; }
|
||||
.cmd-bar { padding: 7px 10px; font-size: 10px; letter-spacing: 0.14em; }
|
||||
.cmd-title { display: none; }
|
||||
.cmd-body {
|
||||
font-size: 13px;
|
||||
padding: 14px 12px 16px;
|
||||
/* Allow long install command to wrap on tiny screens instead of scrolling. */
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
overflow-x: visible;
|
||||
}
|
||||
.cmd-hint { display: none; }
|
||||
.hud-nav { right: 14px; gap: 12px; }
|
||||
|
||||
.docs-link { font-size: 13px; letter-spacing: 0.14em; }
|
||||
|
||||
/* HUD: compact, less metadata, no labels. */
|
||||
.hud-brand { top: 16px; left: 14px; font-size: 10px; letter-spacing: 0.32em; }
|
||||
.hud-meta {
|
||||
top: 16px; right: 14px;
|
||||
gap: 8px;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.22em;
|
||||
}
|
||||
.hud-meta-line { width: 16px; }
|
||||
.hud-nav { right: 10px; gap: 14px; }
|
||||
.hud-step-label { display: none; }
|
||||
.hud-meta { right: 14px; }
|
||||
.hud-meta-line { width: 18px; }
|
||||
.hud-brand { left: 14px; }
|
||||
|
||||
.scroll-cue { bottom: 18px; }
|
||||
.scroll-cue-line { height: 28px; }
|
||||
|
||||
.chip-glow { width: 180px; height: 180px; margin: -90px 0 0 -90px; }
|
||||
.arm-left, .arm-right { width: 40px; height: 40px; margin: -20px 0 0 -20px; }
|
||||
}
|
||||
|
||||
/* Narrow phones — tighten further */
|
||||
@media (max-width: 480px) {
|
||||
.content-product, .content-install { padding: 12vh 6vw 12vh; }
|
||||
.headline { font-size: clamp(26px, 9vw, 40px); margin-bottom: 18px; }
|
||||
.body { font-size: 14px; }
|
||||
.tag { font-size: 10px; padding: 6px 10px; margin-bottom: 18px; }
|
||||
.cmd-body { font-size: 12px; padding: 12px 10px 14px; }
|
||||
.docs-link { font-size: 12px; }
|
||||
.hud-meta span:not(.hud-meta-line) { /* keep depth readout but shrink */ font-size: 9px; }
|
||||
.scroll-cue-label { font-size: 8px; }
|
||||
}
|
||||
|
||||
/* Short / landscape phones — pull content tighter so HUD doesn't collide. */
|
||||
@media (max-height: 520px) and (max-width: 900px) {
|
||||
.content-product, .content-install { padding: 9vh 6vw; }
|
||||
.headline { font-size: clamp(24px, 5vw, 36px); margin-bottom: 14px; }
|
||||
.scroll-cue { display: none; }
|
||||
}
|
||||
|
||||
/* Reduced motion */
|
||||
|
||||
Reference in New Issue
Block a user