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:
jay
2026-06-23 13:57:55 -04:00
parent 1c6c907f7f
commit 5657494988
3 changed files with 49 additions and 19 deletions
+47 -19
View File
@@ -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) */
+2
View File
@@ -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 400700.
Binary file not shown.