Fix joys regressions + self-host Work Sans for cards (Codex audit)
- Restore desktop/tablet two-up Small Joys (arrows rotate which two show); the swipe rail is now phone-only (≤520). One isNarrow flag drives both the DOM (2 vs 3 cards) and the layout, so they can't disagree. - Contain the overflow that let the phone rail widen the whole page: min-width:0 + max-width:100% down the chain (bento/rightcol/pair-wrap/joys-shelf/joys), mobile bento column to minmax(0,1fr), and overflow-x:clip on .page as a seatbelt. - Read-time badge: overflow-wrap:anywhere + line-height so a long "1 min brief · ~N min full story" never causes width pressure. - Self-host Work Sans (latin variable woff2, OFL) and apply it to the cards — the bolder/darker look from CD's mockup; headings stay Newsreader. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -39,15 +39,26 @@
|
||||
|
||||
// small-joys shelf: a single-row, swipeable rail (all 3 cards live in the row; the next
|
||||
// peeks). Arrows scroll it; the counter/dots track the snapped card from scroll position.
|
||||
// Phones get a swipeable rail (all 3 cards in a scroll row, the next peeking). Wider
|
||||
// screens keep the original two-up view with the arrows rotating which two show — a
|
||||
// rail there felt like a broken carousel. isNarrow drives BOTH the DOM and the layout
|
||||
// so they never disagree (no "3 cards wrapping in a 2-col grid").
|
||||
let railEl = $state(null);
|
||||
let joyIdx = $state(0);
|
||||
function goJoy(i) {
|
||||
let isNarrow = $state(false);
|
||||
$effect(() => {
|
||||
const mq = window.matchMedia('(max-width: 520px)');
|
||||
const sync = (e) => { isNarrow = e.matches; };
|
||||
sync(mq);
|
||||
mq.addEventListener('change', sync);
|
||||
return () => mq.removeEventListener('change', sync);
|
||||
});
|
||||
|
||||
function railTo(i) {
|
||||
joyIdx = Math.max(0, Math.min(2, i));
|
||||
const card = railEl?.children?.[joyIdx];
|
||||
if (card) railEl.scrollTo({ left: card.offsetLeft, behavior: 'smooth' });
|
||||
}
|
||||
const prevJoy = () => goJoy(joyIdx - 1);
|
||||
const nextJoy = () => goJoy(joyIdx + 1);
|
||||
let scrollRAF = 0;
|
||||
function onRailScroll() {
|
||||
if (!railEl) return;
|
||||
@@ -62,6 +73,9 @@
|
||||
joyIdx = best;
|
||||
});
|
||||
}
|
||||
// desktop = rotate which two are shown; mobile = scroll the rail
|
||||
const prevJoy = () => (isNarrow ? railTo(joyIdx - 1) : (joyIdx = (joyIdx + 2) % 3));
|
||||
const nextJoy = () => (isNarrow ? railTo(joyIdx + 1) : (joyIdx = (joyIdx + 1) % 3));
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
@@ -252,10 +266,15 @@
|
||||
<button class="arrow" onclick={nextJoy} aria-label="Next small joy">›</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="joys" bind:this={railEl} onscroll={onRailScroll}>
|
||||
{@render joyCard(0)}
|
||||
{@render joyCard(1)}
|
||||
{@render joyCard(2)}
|
||||
<div class="joys" class:rail={isNarrow} bind:this={railEl} onscroll={onRailScroll}>
|
||||
{#if isNarrow}
|
||||
{@render joyCard(0)}
|
||||
{@render joyCard(1)}
|
||||
{@render joyCard(2)}
|
||||
{:else}
|
||||
{@render joyCard(joyIdx)}
|
||||
{@render joyCard((joyIdx + 1) % 3)}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,6 +287,7 @@
|
||||
@font-face { font-family: 'Hanken Grotesk'; src: url('/fonts/hanken-var.woff2') format('woff2'); font-weight: 400 700; font-style: normal; font-display: swap; }
|
||||
@font-face { font-family: 'Newsreader'; src: url('/fonts/newsreader-var.woff2') format('woff2'); font-weight: 400 600; font-style: normal; font-display: swap; }
|
||||
@font-face { font-family: 'Newsreader'; src: url('/fonts/newsreader-italic-var.woff2') format('woff2'); font-weight: 400 500; font-style: italic; font-display: swap; }
|
||||
@font-face { font-family: 'Work Sans'; src: url('/fonts/work-sans-var.woff2') format('woff2'); font-weight: 400 700; font-style: normal; font-display: swap; }
|
||||
|
||||
.page {
|
||||
--ink: #1c1916; --body: #6b6256; --muted: #a89e8c; --teal: #0083ad;
|
||||
@@ -275,6 +295,7 @@
|
||||
min-height: 100vh; background: var(--canvas); color: #23201b;
|
||||
font-family: 'Hanken Grotesk', ui-sans-serif, system-ui, sans-serif;
|
||||
display: flex; flex-direction: column;
|
||||
overflow-x: clip; /* seatbelt: a wide child (e.g. the joys rail) can never scroll the page */
|
||||
}
|
||||
.page :global(*) { box-sizing: border-box; }
|
||||
|
||||
@@ -288,12 +309,17 @@
|
||||
.hero h1 .b { color: #E0852C; }
|
||||
.hero .sub { font-family: 'Newsreader', Georgia, serif; font-style: italic; font-size: clamp(1rem, 2vw, 19px); color: #857b6c; margin: 14px 0 0; }
|
||||
|
||||
/* Bento grid */
|
||||
/* Bento grid. The cards use Work Sans (per CD's mockup — a touch bolder/darker than our
|
||||
body Hanken); headings stay Newsreader (set on h2,h3 below). */
|
||||
.bento {
|
||||
max-width: 1180px; width: 100%; margin: 0 auto; box-sizing: border-box;
|
||||
padding: 0 clamp(18px, 5vw, 44px) 16px;
|
||||
display: grid; grid-template-columns: minmax(0, 1.18fr) minmax(0, 1.82fr); gap: 16px;
|
||||
font-family: 'Work Sans', 'Hanken Grotesk', ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
/* Containment: every link in the chain must be allowed to shrink below its content,
|
||||
or the phone joys rail forces the whole page wider than the viewport. */
|
||||
.bento, .rightcol, .pair-wrap, .joys-shelf, .joys { min-width: 0; max-width: 100%; }
|
||||
/* right column matches the News height; Art stays pinned to the TOP and the Play/Moment
|
||||
pair to the BOTTOM, with the extra space distributed BETWEEN them (FIX1). The cards
|
||||
themselves keep their natural size and never stretch. */
|
||||
@@ -356,7 +382,7 @@
|
||||
}
|
||||
.summary { font-size: 15px; line-height: 1.5; color: #5a5346; margin: 12px 0 0; }
|
||||
.news-foot { display: flex; align-items: center; justify-content: flex-end; padding-top: 18px; }
|
||||
.meta { font-size: 12px; color: var(--muted); }
|
||||
.meta { font-size: 12px; color: var(--muted); line-height: 1.35; overflow-wrap: anywhere; }
|
||||
/* divider sets the secondary "feed" link apart as its own thing */
|
||||
.news-div { border: none; border-top: 1px solid #e6d9bf; margin: 14px 0 12px; }
|
||||
.news-more { display: inline-block; font-size: 13px; font-weight: 600; color: var(--teal); text-decoration: none; }
|
||||
@@ -441,19 +467,21 @@
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.arrow:hover { background: #fff; color: #9a7b3e; }
|
||||
/* one swipeable row; the next card peeks. Scrollbar hidden; snap so it settles per card. */
|
||||
.joys {
|
||||
display: flex; gap: 16px; overflow-x: auto; scroll-snap-type: x mandatory;
|
||||
padding-bottom: 6px; scrollbar-width: none; -webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.joys::-webkit-scrollbar { display: none; }
|
||||
/* Desktop/tablet: the original two-up view (arrows rotate which two show). */
|
||||
.joys { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
|
||||
.joy {
|
||||
flex: none; width: clamp(280px, 86%, 344px); scroll-snap-align: start;
|
||||
position: relative; overflow: hidden; border-radius: 20px; padding: 18px 22px; min-height: 170px;
|
||||
box-sizing: border-box; display: block; text-decoration: none; color: inherit;
|
||||
min-width: 0; position: relative; overflow: hidden; border-radius: 20px; padding: 18px 22px;
|
||||
min-height: 170px; box-sizing: border-box; display: block; text-decoration: none; color: inherit;
|
||||
transition: transform 0.16s ease, box-shadow 0.16s ease;
|
||||
}
|
||||
.joy:hover { transform: translateY(-2px); }
|
||||
/* Phone: a single swipeable row; the next card peeks. Scrollbar hidden; snap per card. */
|
||||
.joys.rail {
|
||||
display: flex; gap: 16px; overflow-x: auto; scroll-snap-type: x mandatory;
|
||||
padding-bottom: 6px; scrollbar-width: none; -webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.joys.rail::-webkit-scrollbar { display: none; }
|
||||
.joys.rail .joy { flex: 0 0 min(86%, 344px); scroll-snap-align: start; }
|
||||
.joy-in { position: relative; } /* content sits above the watermark */
|
||||
.wm { position: absolute; font-family: 'Newsreader', Georgia, serif; line-height: 1; pointer-events: none; }
|
||||
|
||||
@@ -492,7 +520,7 @@
|
||||
|
||||
/* responsive — collapse the bento on narrow screens */
|
||||
@media (max-width: 860px) {
|
||||
.bento { grid-template-columns: 1fr; }
|
||||
.bento { grid-template-columns: minmax(0, 1fr); } /* minmax(0,…) so a wide child can't widen the page */
|
||||
.news { grid-row: auto; }
|
||||
/* single column = natural card height, so the gist is never truncated; drop the
|
||||
bottom fade (it would otherwise dim the final line for no reason) */
|
||||
|
||||
@@ -5,3 +5,5 @@ Source: https://github.com/sharanda/manrope (via fontsource).
|
||||
hanken-var.woff2 — "Hanken Grotesk" by Alfredo Marco Pradil. License: SIL Open Font License 1.1.
|
||||
newsreader-var.woff2 / newsreader-italic-var.woff2 — "Newsreader" by Production Type (Google Fonts).
|
||||
License: SIL Open Font License 1.1. All self-hosted (no Google hotlink) for the /home3 design direction.
|
||||
|
||||
Work Sans — Wei Huang, OFL 1.1 (Google Fonts). Latin subset, variable 400–700.
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user