diff --git a/frontend/src/routes/home3/+page.svelte b/frontend/src/routes/home3/+page.svelte
index baff20c..9a93fc0 100644
--- a/frontend/src/routes/home3/+page.svelte
+++ b/frontend/src/routes/home3/+page.svelte
@@ -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 @@
-
- {@render joyCard(0)}
- {@render joyCard(1)}
- {@render joyCard(2)}
+
+ {#if isNarrow}
+ {@render joyCard(0)}
+ {@render joyCard(1)}
+ {@render joyCard(2)}
+ {:else}
+ {@render joyCard(joyIdx)}
+ {@render joyCard((joyIdx + 1) % 3)}
+ {/if}
@@ -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) */
diff --git a/frontend/static/fonts/CREDITS.txt b/frontend/static/fonts/CREDITS.txt
index 5f5a8cc..3a086d1 100644
--- a/frontend/static/fonts/CREDITS.txt
+++ b/frontend/static/fonts/CREDITS.txt
@@ -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.
diff --git a/frontend/static/fonts/work-sans-var.woff2 b/frontend/static/fonts/work-sans-var.woff2
new file mode 100644
index 0000000..3a70b25
Binary files /dev/null and b/frontend/static/fonts/work-sans-var.woff2 differ