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