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); + } + }); +});