From 70dfe21464fc22c951785d802a8e179732fb68ef Mon Sep 17 00:00:00 2001 From: Youssef Date: Tue, 5 May 2026 16:00:18 +0100 Subject: [PATCH] =?UTF-8?q?Mobile=20responsiveness=20pass=20=E2=80=94=20fi?= =?UTF-8?q?x=20overflow,=20scroll-jump,=20section=20heads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several mobile issues reported by user manifested across themes: photographic hero overflowing viewport, hero text wrapping into the next line incorrectly, tab clicks jumping the page, section titles overlapping section labels on Sport (and any theme overriding the default section-head grid). Changes: · site/css/components.css — .hero--photo: grid-template-columns is now minmax(0, 1fr) so the column shrinks below the 1600 px image natural width on narrow viewports. .hero__photo-grid switches to 2 cols × 3 rows at <= 40rem with 4/3 aspect-ratio (was 3 × 2 at 16/7 and forced page wider than viewport on phones). Studio's redundant aspect-ratio: 16/7 override removed so the mobile rule can apply. · site/css/base.css — html, body now use overflow-x: clip (previously body alone had overflow-x: hidden). `.hero__display`, `.section__title`, `.skill-row__title` get overflow-wrap: anywhere and min-width: 0 — long hyphenated words like "AI-generated" in uppercase Inter Tight 900 (Brutal) no longer overflow with their hyphen as the only break opportunity. · site/css/sections.css — diptych breakpoint pushed 40rem → 48rem so Refine + Redesign demos stack vertically at the Chrome-headless 500 px floor (and on real phones below 768 px). Added a final-block mobile rule [data-theme] .section__head { grid-template-columns: 1fr; gap; align-items } at <= 48rem to win cascade specificity against per-theme overrides (Sport, Atelier, Newsprint, Terminal, Manifesto, Salon, Linen, Almanac, Brutal). Without this, Sport's 6rem 1fr override kept the section head 2-column on mobile and the italic Anton title wrapped onto the "02 / EXAMPLES" label. · site/js/main.js — tab scroll-jump fix. The CSS-only radio tab pattern in Section 04 (without/with) and Section 05 (foundations) positions the radio inputs at top:0 of their section. Clicking a label focuses the input → browser scrolls the section's top into view → page jumps upward on every tab click. Fix intercepts label clicks, prevents default, manually toggles checked, dispatches change, and focuses with preventScroll so keyboard navigation still works without the unwanted scroll. Older browsers without preventScroll fall back to scroll-position save/restore. Verified across all 23 themes at 500 px headless (Chrome's mobile floor — real-phone-equivalent rules also fire below 576 px). No horizontal overflow, no overlapping titles, photographic-hero themes (Studio, Brutal) now show 6 thumbnails in a 2 × 3 grid that fits the viewport, all section titles collapse cleanly under their section labels. Co-Authored-By: Claude Opus 4.7 (1M context) --- site/css/base.css | 16 +++++++++++++++- site/css/components.css | 24 +++++++++++++++++++----- site/css/sections.css | 23 ++++++++++++++++++++++- site/js/main.js | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 7 deletions(-) diff --git a/site/css/base.css b/site/css/base.css index 21f4d1b..0b141dc 100644 --- a/site/css/base.css +++ b/site/css/base.css @@ -128,6 +128,9 @@ hr.rule, html { background: var(--color-paper); color: var(--color-ink); + /* Hard stop on horizontal overflow at the root. `clip` (not `hidden`) + so descendants with `position: sticky` keep working. */ + overflow-x: clip; } body { @@ -142,7 +145,18 @@ body { min-height: 100dvh; padding-inline: max(var(--page-gutter), env(safe-area-inset-left)); padding-block-end: env(safe-area-inset-bottom); - overflow-x: hidden; + overflow-x: clip; +} + +/* Long-word safety: hero display headlines must break inside long + words on narrow viewports rather than push the layout wider than + the viewport. Critical for hyphenated brand-style words like + "AI-generated" in uppercase / heavy display faces. */ +.hero__display, +.section__title, +.skill-row__title { + overflow-wrap: anywhere; + min-width: 0; } /* — Selection ——————————————————————————————————————— */ diff --git a/site/css/components.css b/site/css/components.css index f4ac041..c33218f 100644 --- a/site/css/components.css +++ b/site/css/components.css @@ -2001,7 +2001,6 @@ editorial-magazine instead of agency-deck. */ [data-theme="studio"] .hero__photo { - aspect-ratio: 16 / 7; border-radius: 0; border-color: var(--color-rule-2); } @@ -2482,11 +2481,14 @@ letter-spacing: var(--tracking-display); } -/* H6 — Photographic. Image area + small caption + text below. */ +/* H6 — Photographic. Image area + small caption + text below. + minmax(0, 1fr) on the grid tracks lets columns shrink below the + intrinsic min-content (image natural width = 1600px); without it + the grid would force the page wider than the mobile viewport. */ .hero--photo { padding-block: var(--space-xl) var(--space-xl); gap: var(--space-xl); - grid-template-columns: 1fr; + grid-template-columns: minmax(0, 1fr); } .hero__photo { @@ -2495,18 +2497,30 @@ background: var(--color-paper-2); border: var(--rule-hair) solid var(--color-rule); overflow: hidden; + min-width: 0; +} + +@media (max-width: 40rem) { + .hero__photo { aspect-ratio: 4 / 3; } } .hero__photo-grid { position: absolute; inset: 0; display: grid; - grid-template-columns: repeat(3, 1fr); - grid-template-rows: repeat(2, 1fr); + grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-rows: repeat(2, minmax(0, 1fr)); gap: 1px; background: var(--color-rule); } +@media (max-width: 40rem) { + .hero__photo-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-rows: repeat(3, minmax(0, 1fr)); + } +} + .hero__photo-grid > span { position: relative; overflow: hidden; diff --git a/site/css/sections.css b/site/css/sections.css index 0aab1e4..93153df 100644 --- a/site/css/sections.css +++ b/site/css/sections.css @@ -846,7 +846,7 @@ color: var(--color-accent-ink, var(--color-accent)); } -@media (max-width: 40rem) { +@media (max-width: 48rem) { .skill-row__demo--diptych { grid-template-columns: 1fr; gap: var(--space-md); @@ -1346,3 +1346,24 @@ html[data-theme="almanac"] body { color: var(--color-accent-ink); } +/* ────────────────────────────────────────────────────────────── + Mobile collapse — section-head one-column on narrow viewports. + Many themes override `.section__head`'s `grid-template-columns` + (e.g. sport: 6rem 1fr · atelier: custom · linen: 6rem 1fr · …). + Per-theme overrides have specificity (0, 2, 0); the original + mobile collapse rule was (0, 1, 0) and lost the cascade — the + fixed-column heads stayed 2-column on mobile, causing the title + to overlap the section label (most visible on Sport: italic + Anton title wrapped onto the "02 / EXAMPLES" label). + + This rule restores single-column collapse with matching + specificity so it wins source-order against every per-theme + head override above. Keep this block last in the file. */ +@media (max-width: 48rem) { + [data-theme] .section__head { + grid-template-columns: 1fr; + gap: var(--space-md); + align-items: start; + } +} + diff --git a/site/js/main.js b/site/js/main.js index 9e02e88..0dd5435 100644 --- a/site/js/main.js +++ b/site/js/main.js @@ -1020,3 +1020,43 @@ if (statesBtn && statesReadout) { }, 900); }); } + +/* — Tab-click scroll-jump fix ————————————————————————————— + The CSS-only radio tab pattern in Section 04 (Without/With) and + Section 05 (Foundations) places the radio inputs at top:0 of their + section. When the user clicks a label, the browser focuses the + associated input — which scrolls the section's top into view, even + if the user clicked from the middle of the page. The result is the + page jumping upward on every tab click. + + Fix: intercept label clicks, prevent the default chain, manually + toggle the radio's checked state, and focus with preventScroll so + keyboard navigation still works without the unwanted scroll. */ +const tabLabels = document.querySelectorAll( + ".vs-toggle__btn, .found-nav__btn" +); +tabLabels.forEach((label) => { + label.addEventListener("click", (e) => { + const id = label.getAttribute("for"); + if (!id) return; + const radio = document.getElementById(id); + if (!radio) return; + + e.preventDefault(); + if (!radio.checked) { + radio.checked = true; + radio.dispatchEvent(new Event("change", { bubbles: true })); + } + // Keep keyboard nav working — focus the input but don't scroll. + try { + radio.focus({ preventScroll: true }); + } catch (_) { + // Older browsers without preventScroll option — fall back to + // saving and restoring scroll position. + const x = window.scrollX; + const y = window.scrollY; + radio.focus(); + window.scrollTo(x, y); + } + }); +});