Files
upbeatBytes/frontend/src/routes/home3/+page.svelte
T

380 lines
21 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
import { onMount } from 'svelte';
import { getJSON } from '$lib/api.js';
// /home3 — the Claude Design "Frame A" direction (editorial, with colour), rebuilt in
// our codebase with our real logo + self-hosted fonts, wired to live data. Hidden
// prototype (noindex), alongside /home2 so we can compare.
let news = $state(null); // {id, title, summary, image}
let art = $state(null); // {title, artist, year, image}
// headline truncated on a whole-word boundary (no mid-word ellipsis)
function clampWords(s, max = 48) {
if (!s || s.length <= max) return s;
const cut = s.slice(0, max);
const i = cut.lastIndexOf(' ');
return (i > 0 ? cut.slice(0, i) : cut).replace(/[\s,;:.]+$/, '') + '…';
}
let headline = $derived(clampWords(news?.title ?? 'What went right this week: the good news that actually matters'));
onMount(async () => {
try {
const a = await getJSON('/api/art/today');
if (a) art = { title: a.title, artist: a.artist, year: a.date_text, image: a.image_url };
} catch { /* fall back to gradient swatch */ }
let homeq = '';
try {
const hv = localStorage.getItem('goodnews:home') || '';
const hs = localStorage.getItem('goodnews:homeScope') || 'nearby';
if (hv && hs !== 'world') homeq = `&home=${encodeURIComponent(hv)}&scope=${hs}`;
} catch { /* global brief */ }
try {
const it = (await getJSON(`/api/brief?limit=1${homeq}`))?.items?.[0];
if (it) news = { id: it.id, title: it.title, summary: it.summary || it.description || '', image: it.image_url || null };
} catch { /* fall back to design copy */ }
});
</script>
<svelte:head>
<title>upbeatBytes — a calmer, brighter corner of the internet</title>
<meta name="robots" content="noindex" />
<meta name="description" content="A calmer, brighter corner of the internet: good news, daily art, small games, and little resets." />
</svelte:head>
<div class="page">
<header class="bar">
<a class="brand" href="/home3" aria-label="upbeatBytes home">
<img src="/logo.svg" alt="upbeatBytes" width="586" height="196" />
</a>
<nav class="nav">
<a class="on" href="/">News</a>
<a href="/play">Games</a>
<a href="/art">Art</a>
<a class="acct" href="/account" aria-label="Your account">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#C98A2E" stroke-width="2" aria-hidden="true">
<circle cx="12" cy="8" r="4" /><path d="M4 21c0-4 4-6 8-6s8 2 8 6" />
</svg>
</a>
</nav>
</header>
<section class="hero">
<h1>A <span class="t">calmer</span>, <span class="b">brighter</span> corner of the internet.</h1>
<p class="sub">Good news, daily art, small games, and little resets.</p>
</section>
<main class="bento">
<!-- Good News (tall) — a card with TWO links, so it's a div, not a single anchor -->
<div class="card news">
<div class="news-photo" style={news?.image ? `background-image:url(${news.image})` : ''}></div>
<div class="news-body">
<span class="label" style="color:#0083ad">GOOD NEWS</span>
<h2>{headline}</h2>
<p class="summary">{news?.summary || "We read the week so you don't have to doomscroll it. Five quietly hopeful stories, summarised to the gist."}</p>
<div class="news-foot">
<a class="link news-link" href={news?.id ? `/a/${news.id}` : '/'}>Read article</a>
<span class="meta">3 min read</span>
</div>
<a class="news-more" href="/">Read more good news →</a>
</div>
</div>
<div class="rightcol">
<!-- Daily Art (wide) -->
<a class="card art" href="/art">
<div class="art-body">
<span class="label" style="color:#8857C2">DAILY ART</span>
<h3>A masterwork a day</h3>
<p class="art-today">
{#if art}Today: <span class="ital">{art.title}</span>{#if art.artist} — {art.artist}{/if}{#if art.year}, {art.year}{/if}.
{:else}Today: <span class="ital">Among the Sierra Nevada</span> — Bierstadt, 1868.{/if}
</p>
<span class="link art-link">View today</span>
</div>
<div class="art-swatch" style={art?.image ? `--art:url(${art.image})` : ''}></div>
</a>
<!-- bottom pair — vertically centered in the space beneath the pinned Art card -->
<div class="pair-wrap">
<div class="pair">
<a class="card play" href="/play">
<div class="play-top">
<span class="label" style="color:#A8650F">PLAY</span>
<h3>A little daily puzzle</h3>
</div>
<!-- "bleeding boards": three game motifs clipping at the card edges (decorative) -->
<div class="play-band" aria-hidden="true">
<div class="wb">
<div class="wb-row"><span class="wb-t wb-a">E</span><span class="wb-t wb-n">A</span><span class="wb-t wb-a">T</span></div>
<div class="wb-row"><span class="wb-t wb-g">Y</span><span class="wb-t wb-g">T</span><span class="wb-t wb-g">E</span></div>
<div class="wb-row"><span class="wb-t wb-e"></span><span class="wb-t wb-e"></span><span class="wb-t wb-d"></span></div>
</div>
<div class="ws">
<span>K</span><span>R</span><span>O</span><span>A</span><span>E</span><span>S</span>
<span class="hl">B</span><span class="hl">Y</span><span class="hl">T</span><span class="hl">E</span><span class="hl">S</span><span>W</span>
<span>T</span><span>I</span><span>M</span><span>U</span><span>H</span><span>P</span>
<span>G</span><span>E</span><span>B</span><span>O</span><span>R</span><span>L</span>
<span>F</span><span>I</span><span>N</span><span>D</span><span>C</span><span>A</span>
<span>Z</span><span>O</span><span>S</span><span>E</span><span>K</span><span>Y</span>
</div>
<div class="mm">
<span class="mm-a"></span>
<span class="mm-w"><span class="mm-dot" style="background:#6bbf8c"></span></span>
<span class="mm-a"></span>
<span class="mm-w"><span class="mm-dot" style="background:#6bbf8c"></span></span>
<span class="mm-a"></span>
<span class="mm-a"></span>
<span class="mm-a"></span>
<span class="mm-w"><span class="mm-dot" style="background:#D2861B"></span></span>
<span class="mm-a"></span>
</div>
</div>
<div class="play-foot"><span class="link play-link">Enter</span></div>
</a>
<div class="card moment">
<div class="moment-top">
<span class="label" style="color:#3F9A66">ENTERTAINMENT</span>
<span class="soon">SOON</span>
</div>
<p class="moment-line">A little something to enjoy.</p>
<span class="moment-meta">Shows, clips, and curiosities. Coming soon.</span>
</div>
</div>
</div>
<!-- "small joys" rail — little jewels: focal point + watermark + tag + soft gradient -->
<div class="joys">
<div class="joy joy-word">
<span class="wm" aria-hidden="true">S</span>
<div class="joy-in">
<div class="tag"><span class="rule"></span><span class="tag-label">Word of the day</span></div>
<p class="word-line"><span class="word">Serene</span> <span class="word-pos">adj.</span></p>
<p class="word-pron">/səˈriːn/</p>
<p class="def">Calm, peaceful, and untroubled. The quiet after a storm passes.</p>
</div>
</div>
<div class="joy joy-quote">
<span class="wm wm-q" aria-hidden="true"></span>
<div class="joy-in">
<div class="tag"><span class="rule"></span><span class="tag-label">Quote of the day</span></div>
<p class="quote">Very little is needed to make a happy life.</p>
<div class="attrib"><span class="attrib-rule"></span><span class="attrib-by">Marcus Aurelius</span></div>
</div>
</div>
<div class="joy joy-fact">
<div class="joy-in">
<div class="joy-top">
<div class="tag"><span class="rule"></span><span class="tag-label">A good thing today</span></div>
<span class="soon">SOON</span>
</div>
<p class="fact-hero"><span class="year">1928</span> <span class="onthis">ON THIS DAY</span></p>
<p class="fact">Penicillin was discovered by a happy accident.</p>
</div>
</div>
</div>
</div>
</main>
<footer class="foot">upbeatBytes — no ads, no paywalls, no doomscrolling.</footer>
</div>
<style>
@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; }
.page {
--ink: #1c1916; --body: #6b6256; --muted: #a89e8c; --teal: #0083ad;
--canvas: #FFF9EF; --news-border: #f2e7d3;
min-height: 100vh; background: var(--canvas); color: #23201b;
font-family: 'Hanken Grotesk', ui-sans-serif, system-ui, sans-serif;
display: flex; flex-direction: column;
}
.page :global(*) { box-sizing: border-box; }
/* Header — real logo left, nav right */
.bar {
display: flex; align-items: center; justify-content: space-between;
max-width: 1180px; width: 100%; margin: 0 auto; box-sizing: border-box;
padding: 26px clamp(18px, 5vw, 44px) 0;
}
.brand { display: block; line-height: 0; }
.brand img { height: 48px; width: auto; display: block; }
.nav { display: flex; align-items: center; gap: clamp(16px, 2.4vw, 32px); font-size: 16.5px; font-weight: 500; }
.nav a { color: var(--body); text-decoration: none; }
.nav a.on { color: #23201b; }
.nav a:hover { color: var(--teal); }
.acct {
width: 32px; height: 32px; border-radius: 50%; border: 1.5px solid #e6c9a0; background: #FCEFD7;
display: flex; align-items: center; justify-content: center;
}
.acct:hover { background: #fbe6c4; }
/* Hero — spacing tuned per the /home2 pass: pulled up a touch, more air before cards */
.hero { text-align: center; max-width: 1180px; width: 100%; margin: 0 auto; padding: clamp(24px, 4vw, 34px) clamp(18px, 5vw, 44px) clamp(38px, 5vw, 48px); }
.hero h1 {
font-family: 'Newsreader', Georgia, serif; font-weight: 500;
font-size: clamp(2.1rem, 5vw, 50px); line-height: 1.04; letter-spacing: -0.015em; margin: 0; color: var(--ink);
}
.hero h1 .t { color: #0083ad; }
.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 {
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;
}
/* 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. */
.rightcol { display: flex; flex-direction: column; gap: 16px; }
.rightcol .art { flex: none; } /* Art pinned to the top */
.pair-wrap { flex: 1; display: flex; align-items: center; } /* fill the rest; pair vertically centered */
.card {
border-radius: 18px; overflow: hidden; text-decoration: none; color: inherit;
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
a.card:hover { transform: translateY(-2px); }
.label { font-size: 11px; font-weight: 700; letter-spacing: 0.16em; }
.link { font-size: 14px; font-weight: 600; padding-bottom: 2px; align-self: flex-start; }
/* card titles a touch larger + bolder so they jump on hover/scan */
h2, h3 { font-family: 'Newsreader', Georgia, serif; font-weight: 600; letter-spacing: -0.01em; color: var(--ink); }
/* Good News — tall, photo on top */
.news {
background: #fff; border: 1px solid var(--news-border);
display: flex; flex-direction: column; box-shadow: 0 6px 20px -14px rgba(0, 131, 173, 0.4);
}
.news-photo { aspect-ratio: 5/4; background: linear-gradient(160deg, #6fc4d8, #9bd4c9 55%, #f6cf7e); background-size: cover; background-position: center; }
.news-body { padding: 24px 26px; flex: 1; display: flex; flex-direction: column; }
.news h2 {
font-size: clamp(1.55rem, 2.6vw, 30px); line-height: 1.14; margin: 12px 0 0;
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden;
}
/* the gist fades out at the bottom (FIX 1) instead of a hard ellipsis — full read on /news */
.summary {
font-size: 14.5px; line-height: 1.55; color: var(--body); margin: 12px 0 0;
max-height: 4.6em; overflow: hidden;
-webkit-mask-image: linear-gradient(to bottom, #000 56%, transparent);
mask-image: linear-gradient(to bottom, #000 56%, transparent);
}
.news-foot { margin-top: auto; display: flex; align-items: center; justify-content: space-between; padding-top: 20px; }
.news-link { color: var(--teal); border-bottom: 2px solid #6fc4d8; text-decoration: none; }
.meta { font-size: 12px; color: var(--muted); }
/* secondary link: straight to the full brief, without reading the highlighted article */
.news-more { display: inline-block; margin-top: 12px; font-size: 13px; font-weight: 600; color: var(--muted); text-decoration: none; }
.news-more:hover { color: var(--teal); }
/* Daily Art — wide, text left + artwork swatch right */
.art { background: #F3EEF9; border: 1px solid #e4d8f1; display: flex; min-height: 188px; }
.art-body { flex: 1; padding: 24px 26px; display: flex; flex-direction: column; }
.art h3 { font-size: clamp(1.35rem, 2.1vw, 25px); line-height: 1.16; margin: 10px 0 0; color: #2a1c3d; }
.art-today { font-size: 13.5px; line-height: 1.5; color: #6f6280; margin: 9px 0 0; }
.ital { font-style: italic; font-family: 'Newsreader', Georgia, serif; }
.art-link { margin-top: auto; color: #8857C2; border-bottom: 2px solid #c9aef0; }
/* swatch crops a few px off every edge (::after inset) so scanned paintings don't show
their ragged/black canvas edge at the top */
.art-swatch {
width: 46%; min-width: 130px; position: relative; overflow: hidden;
background: linear-gradient(170deg, #bfe0f0 0%, #a9cf9a 50%, #d89a4e 100%);
}
.art-swatch::after {
content: ""; position: absolute; inset: -6px;
background-image: var(--art); background-size: cover; background-position: center;
}
/* bottom pair */
.pair { width: 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.play { background: #FFF3DC; border: 1px solid #f6e2b8; display: flex; flex-direction: column; }
.play-top { padding: 22px 24px 0; }
.play h3 { font-size: clamp(1.25rem, 1.9vw, 23px); margin: 14px 0 0; color: #5c3d0c; }
.play-foot { margin-top: auto; padding: 16px 24px 22px; }
.play-link { color: #A8650F; border-bottom: 2px solid #e0a94f; }
/* "bleeding boards" — three game motifs clipping at the card edges (decorative) */
/* Word search is the centred highlight; the two side games are the SAME size and each
bleeds ~half a column off its edge (consistent both sides) to imply "more under the hood". */
.play-band { position: relative; height: 124px; margin-top: 24px; overflow: hidden; }
.wb { position: absolute; top: 50%; left: -12px; transform: translateY(-50%); display: flex; flex-direction: column; gap: 4px; }
.wb-row { display: flex; gap: 4px; }
.wb-t { width: 24px; height: 24px; border-radius: 5px; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 11px; color: #fff; }
.wb-g { background: #6bbf8c; } .wb-a { background: #E6A02C; } .wb-n { background: #d9c39a; }
.wb-e { background: #fff; border: 1.5px solid #ecca84; } .wb-d { background: #fff; border: 1.5px dashed #e0bb6f; }
.ws {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
display: grid; grid-template-columns: repeat(6, 14px); gap: 3px;
font-weight: 600; font-size: 10.5px; line-height: 14px; color: #d4b576; text-align: center;
background: #fff; border: 1.5px solid #f0d597; border-radius: 10px; padding: 9px;
box-shadow: 0 5px 16px -8px rgba(210, 134, 27, 0.55);
}
.ws .hl { color: #B5701A; font-weight: 800; }
.mm { position: absolute; top: 50%; right: -12px; transform: translateY(-50%); display: grid; grid-template-columns: repeat(3, 24px); grid-auto-rows: 24px; gap: 4px; }
.mm > span { border-radius: 6px; }
.mm-a { background: #E6A02C; }
.mm-w { background: #fff; border: 1.5px solid #f0d597; display: flex; align-items: center; justify-content: center; }
.mm-dot { width: 7px; height: 7px; border-radius: 50%; display: block; }
.moment { background: #E6F3E9; border: 1px solid #cee6d3; padding: 22px 24px; display: flex; flex-direction: column; }
.moment-top { display: flex; align-items: center; justify-content: space-between; }
.soon { font-size: 10px; font-weight: 700; letter-spacing: 0.08em; color: #3F9A66; background: #fff; border-radius: 999px; padding: 3px 8px; }
.moment-line { font-family: 'Newsreader', Georgia, serif; font-style: italic; font-size: 18px; line-height: 1.3; color: #214a35; margin: 14px 0 0; }
.moment-meta { margin-top: auto; padding-top: 16px; font-size: 13px; color: #6f9683; }
/* "small joys" rail — little jewels: one big focal point per card, a faint oversized
watermark glyph, an accent-tag label, soft diagonal gradient + long low shadow. */
.joys { flex: none; display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; }
.joy { position: relative; overflow: hidden; border-radius: 20px; padding: 22px 24px; }
.joy-in { position: relative; } /* content sits above the watermark */
.wm { position: absolute; font-family: 'Newsreader', Georgia, serif; line-height: 1; pointer-events: none; }
.joy-word { background: linear-gradient(165deg, #FBF3E2, #F6EAD2); border: 1px solid #ecdcbb; box-shadow: 0 10px 30px -22px rgba(120, 85, 20, 0.6); --accent: #bd8f33; --rule: #D2861B; }
.joy-word .wm { right: -14px; bottom: -30px; font-size: 150px; font-weight: 400; color: rgba(193, 150, 60, 0.09); }
.joy-quote { background: linear-gradient(165deg, #F2ECF9, #E9E0F4); border: 1px solid #ddd0ee; box-shadow: 0 10px 30px -22px rgba(90, 60, 140, 0.6); --accent: #8857C2; --rule: #8857C2; }
.joy-quote .wm { left: 14px; top: -26px; font-size: 120px; color: rgba(136, 87, 194, 0.16); }
.joy-fact { background: linear-gradient(165deg, #E9F4EB, #DCEDDF); border: 1px solid #cee6d3; box-shadow: 0 10px 30px -22px rgba(40, 120, 75, 0.55); --accent: #3F9A66; --rule: #3F9A66; }
.tag { display: flex; align-items: center; gap: 8px; }
.tag .rule { width: 18px; height: 2px; border-radius: 2px; background: var(--rule); }
.tag-label { font-size: 10px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--accent); }
.word-line { margin: 14px 0 0; display: flex; align-items: baseline; gap: 9px; }
.joy .word { font-family: 'Newsreader', Georgia, serif; font-weight: 500; font-size: 32px; letter-spacing: -0.01em; color: #3a2f1c; }
.word-pos { font-family: 'Newsreader', Georgia, serif; font-style: italic; font-size: 13px; color: #b39a64; }
.word-pron { font-family: 'Newsreader', Georgia, serif; font-style: italic; font-size: 13px; color: #b39a64; margin: 3px 0 0; }
.joy .def { font-size: 13.5px; color: #6b5d44; margin: 12px 0 0; line-height: 1.5; }
.joy .quote { font-family: 'Newsreader', Georgia, serif; font-style: italic; font-size: 21px; line-height: 1.3; color: #2f2240; margin: 26px 0 0; }
.attrib { display: flex; align-items: center; gap: 9px; margin-top: 16px; }
.attrib-rule { width: 22px; height: 1px; background: #b69ad8; }
.attrib-by { font-family: 'Newsreader', Georgia, serif; font-size: 13px; color: #7c64a0; }
.joy-fact .joy-top { display: flex; align-items: center; justify-content: space-between; }
.fact-hero { display: flex; align-items: baseline; gap: 8px; margin: 16px 0 0; }
.year { font-family: 'Newsreader', Georgia, serif; font-weight: 500; font-size: 30px; color: #1e5b3b; line-height: 0.9; }
.onthis { font-size: 11px; color: #6f9683; letter-spacing: 0.04em; }
.joy .fact { font-family: 'Newsreader', Georgia, serif; font-size: 16px; color: #214a35; margin: 10px 0 0; line-height: 1.34; }
.foot {
text-align: center; max-width: 1180px; width: 100%; margin: 14px auto 0; box-sizing: border-box;
padding: 20px clamp(18px, 5vw, 44px) 30px; font-size: 13px; color: var(--muted);
border-top: 1px solid var(--news-border);
}
/* responsive — collapse the bento on narrow screens */
@media (max-width: 860px) {
.bento { grid-template-columns: 1fr; }
.news { grid-row: auto; }
}
@media (max-width: 520px) {
.art { flex-direction: column; }
.art-swatch { width: 100%; min-height: 130px; }
.pair { grid-template-columns: 1fr; }
.joys { grid-template-columns: 1fr; }
}
</style>