home2 round 2: Manrope nav, bigger logo, photo-top news / photo-left art, tinted static cards
- Self-hosted Manrope (OFL) as the hub sans; nav lighter (weight 500, soft slate, not all "on"). Logo up to 58px. - News card: photo on top + headline below, and it now respects the reader's saved Closer-to-Home filter (goodnews:home/homeScope) so the headline matches their Brief. - Art card: rectangular cover-cropped thumbnail on the LEFT (crops ragged scan edges), text on the right — variety against the photo-top news card. - Play/Daily Moment: tinted backgrounds, bigger centered icon+title, blurb left-aligned. - /fonts/* + /textures/* served immutable (Caddy live + snapshot). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -62,10 +62,10 @@ upbeatbytes.com {
|
|||||||
header @immutable Cache-Control "public, max-age=31536000, immutable"
|
header @immutable Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
|
||||||
# Static texture + font assets — large/unchanging. Cache them forever like
|
# Static texture + font assets — large/unchanging. Cache them forever like
|
||||||
# them forever like immutable assets; rename the file if the texture ever changes.
|
# immutable assets; rename the file if one ever changes.
|
||||||
@textures path /textures/*
|
@assets path /textures/* /fonts/*
|
||||||
header @textures Cache-Control "public, max-age=31536000, immutable"
|
header @assets Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
|
||||||
# The SPA shell: "/" and extensionless client routes (try_files → index.html).
|
# The SPA shell: "/" and extensionless client routes (try_files → index.html).
|
||||||
# Briefly cacheable at the CDN edge (s-maxage) so a first paint never depends
|
# Briefly cacheable at the CDN edge (s-maxage) so a first paint never depends
|
||||||
# on this origin's uplink; browsers still revalidate every visit (max-age=0).
|
# on this origin's uplink; browsers still revalidate every visit (max-age=0).
|
||||||
@@ -86,6 +86,7 @@ upbeatbytes.com {
|
|||||||
not path /_app/immutable/*
|
not path /_app/immutable/*
|
||||||
not path /textures/*
|
not path /textures/*
|
||||||
not path /fonts/*
|
not path /fonts/*
|
||||||
|
path *.*
|
||||||
}
|
}
|
||||||
header @revalidate Cache-Control "no-cache"
|
header @revalidate Cache-Control "no-cache"
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +1,104 @@
|
|||||||
<script>
|
<script>
|
||||||
// A single hub "room" card. Presentational — the page passes any live preview data in.
|
// A single hub "room" card. Presentational — the page passes live preview data in.
|
||||||
let { room, artImg = null, newsHeadline = '' } = $props();
|
// Layout varies by preview type: news = photo on top, art = photo on the left,
|
||||||
|
// static = centered icon+title over a tinted background.
|
||||||
|
let { room, artImg = null, newsImg = null, newsHeadline = '' } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a class="card card--{room.size}" class:live={room.preview !== 'static'} href={room.href}>
|
<a class="card card--{room.size} card--{room.preview}" href={room.href}
|
||||||
{#if room.preview === 'art' && artImg}
|
style={room.tint ? `--tint:${room.tint}` : ''}>
|
||||||
<div class="thumb" style="background-image:url({artImg})"></div>
|
{#if room.preview === 'news'}
|
||||||
{/if}
|
{#if newsImg}<div class="photo photo--top" style="background-image:url({newsImg})"></div>{/if}
|
||||||
|
<div class="body">
|
||||||
|
<div class="eyebrow"><span class="ic">{room.icon}</span>{room.title}</div>
|
||||||
|
{#if newsHeadline}<p class="headline">“{newsHeadline}”</p>{:else}<p class="blurb">{room.blurb}</p>{/if}
|
||||||
|
<span class="cta">{room.cta} <span class="arr">→</span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="body">
|
{:else if room.preview === 'art'}
|
||||||
<div class="eyebrow"><span class="ic">{room.icon}</span>{room.title}</div>
|
{#if artImg}<div class="photo photo--left" style="background-image:url({artImg})"></div>{/if}
|
||||||
|
<div class="body">
|
||||||
{#if room.preview === 'news' && newsHeadline}
|
<div class="eyebrow"><span class="ic">{room.icon}</span>{room.title}</div>
|
||||||
<p class="headline">“{newsHeadline}”</p>
|
|
||||||
{:else}
|
|
||||||
<p class="blurb">{room.blurb}</p>
|
<p class="blurb">{room.blurb}</p>
|
||||||
{/if}
|
<span class="cta">{room.cta} <span class="arr">→</span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span class="cta">{room.cta} <span class="arr">→</span></span>
|
{:else}
|
||||||
</div>
|
<div class="head">
|
||||||
|
<span class="ic-big">{room.icon}</span>
|
||||||
|
<span class="s-title">{room.title}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tail">
|
||||||
|
<p class="blurb">{room.blurb}</p>
|
||||||
|
<span class="cta">{room.cta} <span class="arr">→</span></span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.card {
|
.card {
|
||||||
position: relative; display: flex; flex-direction: column; justify-content: flex-end;
|
position: relative; display: flex; overflow: hidden; text-decoration: none; color: var(--ink);
|
||||||
overflow: hidden; text-decoration: none; color: var(--ink);
|
|
||||||
background: var(--surface); border: 1px solid var(--line); border-radius: 18px;
|
background: var(--surface); border: 1px solid var(--line); border-radius: 18px;
|
||||||
padding: clamp(18px, 2.2vw, 26px); min-height: 160px;
|
|
||||||
box-shadow: 0 10px 30px rgba(20, 30, 45, 0.06);
|
box-shadow: 0 10px 30px rgba(20, 30, 45, 0.06);
|
||||||
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
|
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
|
||||||
}
|
}
|
||||||
.card:hover { transform: translateY(-4px); box-shadow: 0 20px 46px rgba(20, 30, 45, 0.12); border-color: #ddd4c4; }
|
.card:hover { transform: translateY(-4px); box-shadow: 0 20px 46px rgba(20, 30, 45, 0.12); border-color: #ddd4c4; }
|
||||||
|
|
||||||
/* size variants (grid spans set by the page) */
|
/* size variants — grid spans set by the page; these set the heights */
|
||||||
.card--large { min-height: 340px; }
|
.card--large { min-height: 340px; }
|
||||||
.card--wide { min-height: 200px; }
|
.card--wide { min-height: 200px; }
|
||||||
.card--tall { min-height: 340px; }
|
.card--small { min-height: 168px; }
|
||||||
.card--small { min-height: 160px; }
|
|
||||||
|
|
||||||
/* live art preview fills behind the body */
|
|
||||||
.thumb {
|
|
||||||
position: absolute; inset: 0; background-size: cover; background-position: center;
|
|
||||||
}
|
|
||||||
.thumb::after {
|
|
||||||
content: ""; position: absolute; inset: 0;
|
|
||||||
background: linear-gradient(to top, rgba(20, 24, 30, 0.78) 6%, rgba(20, 24, 30, 0.20) 46%, rgba(20, 24, 30, 0) 70%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.body { position: relative; z-index: 1; }
|
|
||||||
.card.live:has(.thumb) .body { color: #fff; }
|
|
||||||
|
|
||||||
.eyebrow {
|
.eyebrow {
|
||||||
display: flex; align-items: center; gap: 9px;
|
display: flex; align-items: center; gap: 9px;
|
||||||
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
|
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
|
||||||
font-size: clamp(1.15rem, 2vw, 1.6rem); font-weight: 600; letter-spacing: -0.01em;
|
font-size: clamp(1.15rem, 2vw, 1.5rem); font-weight: 600; letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
.ic { font-size: 1.1em; line-height: 1; }
|
.ic { font-size: 1.05em; line-height: 1; }
|
||||||
.card--large .eyebrow { font-size: clamp(1.5rem, 3vw, 2.1rem); }
|
.blurb { margin: 9px 0 15px; color: var(--muted); font-size: 1rem; line-height: 1.45; max-width: 42ch; }
|
||||||
|
|
||||||
.blurb { margin: 10px 0 16px; color: var(--muted); font-size: 1rem; line-height: 1.45; max-width: 42ch; }
|
|
||||||
.headline {
|
|
||||||
margin: 12px 0 16px; font-family: Georgia, serif; font-style: italic;
|
|
||||||
font-size: clamp(1.05rem, 1.8vw, 1.35rem); line-height: 1.35; max-width: 40ch;
|
|
||||||
}
|
|
||||||
.card.live:has(.thumb) .blurb { color: rgba(255, 255, 255, 0.86); }
|
|
||||||
|
|
||||||
.cta { display: inline-flex; align-items: center; gap: 6px; font-weight: 700; color: var(--accent); font-size: 0.98rem; }
|
.cta { display: inline-flex; align-items: center; gap: 6px; font-weight: 700; color: var(--accent); font-size: 0.98rem; }
|
||||||
.card.live:has(.thumb) .cta { color: #fff; }
|
|
||||||
.arr { transition: transform 0.18s ease; }
|
.arr { transition: transform 0.18s ease; }
|
||||||
.card:hover .arr { transform: translateX(4px); }
|
.card:hover .arr { transform: translateX(4px); }
|
||||||
|
|
||||||
|
/* News — photo on top, headline below */
|
||||||
|
.card--news { flex-direction: column; }
|
||||||
|
.card--news .photo--top {
|
||||||
|
width: 100%; flex: 1 1 auto; min-height: 150px;
|
||||||
|
background-size: cover; background-position: center;
|
||||||
|
}
|
||||||
|
.card--news .body { padding: clamp(18px, 2.2vw, 26px); }
|
||||||
|
.card--news .eyebrow { font-size: clamp(1.4rem, 2.6vw, 1.9rem); }
|
||||||
|
.card--news .headline {
|
||||||
|
margin: 11px 0 15px; font-family: Georgia, serif; font-style: italic;
|
||||||
|
font-size: clamp(1.1rem, 1.9vw, 1.4rem); line-height: 1.34; max-width: 40ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Art — rectangular photo on the left (cover-cropped to hide ragged scan edges) */
|
||||||
|
.card--art { flex-direction: row; align-items: stretch; }
|
||||||
|
.card--art .photo--left {
|
||||||
|
flex: 0 0 42%; min-width: 130px;
|
||||||
|
background-size: cover; background-position: center;
|
||||||
|
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
.card--art .body {
|
||||||
|
flex: 1; padding: clamp(16px, 2vw, 26px);
|
||||||
|
display: flex; flex-direction: column; justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Static — tinted, big centered icon + title, body left-aligned below */
|
||||||
|
.card--static {
|
||||||
|
flex-direction: column; justify-content: center; align-items: stretch;
|
||||||
|
background: var(--tint, var(--surface)); border-color: rgba(20, 30, 45, 0.07);
|
||||||
|
padding: clamp(18px, 2.4vw, 26px);
|
||||||
|
}
|
||||||
|
.card--static .head {
|
||||||
|
display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center;
|
||||||
|
}
|
||||||
|
.card--static .ic-big { font-size: clamp(2.2rem, 4vw, 2.9rem); line-height: 1; }
|
||||||
|
.card--static .s-title {
|
||||||
|
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
|
||||||
|
font-size: clamp(1.2rem, 2.2vw, 1.55rem); font-weight: 600; letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.card--static .tail { margin-top: 16px; text-align: left; }
|
||||||
|
.card--static .blurb { margin: 0 0 12px; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const ROOMS = [
|
|||||||
size: 'small',
|
size: 'small',
|
||||||
preview: 'static',
|
preview: 'static',
|
||||||
icon: '🎲',
|
icon: '🎲',
|
||||||
|
tint: '#e9f1f9', // soft sky
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'moment',
|
id: 'moment',
|
||||||
@@ -42,5 +43,6 @@ export const ROOMS = [
|
|||||||
size: 'small',
|
size: 'small',
|
||||||
preview: 'static',
|
preview: 'static',
|
||||||
icon: '🌿',
|
icon: '🌿',
|
||||||
|
tint: '#eaf4ea', // soft sage
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,12 +8,21 @@
|
|||||||
// promote to / and remove this clone — same approach we used for /art.
|
// promote to / and remove this clone — same approach we used for /art.
|
||||||
let artImg = $state(null);
|
let artImg = $state(null);
|
||||||
let newsHeadline = $state('');
|
let newsHeadline = $state('');
|
||||||
|
let newsImg = $state(null);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try { artImg = (await getJSON('/api/art/today'))?.image_url ?? null; } catch { /* card falls back to blurb */ }
|
try { artImg = (await getJSON('/api/art/today'))?.image_url ?? null; } catch { /* card falls back to blurb */ }
|
||||||
|
// Respect the reader's saved Closer-to-Home filter so the headline matches their Brief.
|
||||||
|
let homeq = '';
|
||||||
try {
|
try {
|
||||||
const b = await getJSON('/api/brief?limit=1');
|
const hv = localStorage.getItem('goodnews:home') || '';
|
||||||
newsHeadline = b?.items?.[0]?.title ?? '';
|
const hs = localStorage.getItem('goodnews:homeScope') || 'nearby';
|
||||||
|
if (hv && hs !== 'world') homeq = `&home=${encodeURIComponent(hv)}&scope=${hs}`;
|
||||||
|
} catch { /* default global brief */ }
|
||||||
|
try {
|
||||||
|
const it = (await getJSON(`/api/brief?limit=1${homeq}`))?.items?.[0];
|
||||||
|
newsHeadline = it?.title ?? '';
|
||||||
|
newsImg = it?.image_url ?? null;
|
||||||
} catch { /* card falls back to blurb */ }
|
} catch { /* card falls back to blurb */ }
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -51,7 +60,7 @@
|
|||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{#each ROOMS as r (r.id)}
|
{#each ROOMS as r (r.id)}
|
||||||
<RoomCard room={r} {artImg} {newsHeadline} />
|
<RoomCard room={r} {artImg} {newsImg} {newsHeadline} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -60,24 +69,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* Self-hosted modern sans (OFL) — privacy-friendly, no Google hotlink. */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Manrope';
|
||||||
|
src: url('/fonts/manrope-var.woff2') format('woff2');
|
||||||
|
font-weight: 200 800; font-style: normal; font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
.room {
|
.room {
|
||||||
--canvas: #faf6ee; --surface: #ffffff; --ink: #232a31; --muted: #707b86;
|
--canvas: #faf6ee; --surface: #ffffff; --ink: #232a31; --muted: #707b86;
|
||||||
--line: #ece5d8; --accent: #0a93c0; --accent-deep: #066c8e;
|
--line: #ece5d8; --accent: #0a93c0; --accent-deep: #066c8e;
|
||||||
|
--sans: 'Manrope', ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||||
min-height: 100vh; background: var(--canvas); color: var(--ink);
|
min-height: 100vh; background: var(--canvas); color: var(--ink);
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
font-family: var(--sans);
|
||||||
display: flex; flex-direction: column;
|
display: flex; flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bigger, more present top bar — logo + nav labels scaled up. */
|
/* Bigger, more present top bar — logo + lighter, modern nav labels. */
|
||||||
.bar {
|
.bar {
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
padding: 20px clamp(18px, 5vw, 64px);
|
padding: 20px clamp(18px, 5vw, 64px);
|
||||||
max-width: 1180px; width: 100%; margin: 0 auto; box-sizing: border-box;
|
max-width: 1180px; width: 100%; margin: 0 auto; box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.brand { display: block; line-height: 0; }
|
.brand { display: block; line-height: 0; }
|
||||||
.brand img { height: 50px; width: auto; display: block; }
|
.brand img { height: 58px; width: auto; display: block; }
|
||||||
.nav { display: flex; align-items: center; gap: clamp(18px, 3vw, 38px); }
|
.nav { display: flex; align-items: center; gap: clamp(18px, 3vw, 40px); }
|
||||||
.nav a { color: var(--ink); text-decoration: none; font-weight: 600; font-size: 1.12rem; }
|
.nav a {
|
||||||
|
font-family: var(--sans); color: #4a525c; text-decoration: none;
|
||||||
|
font-weight: 500; font-size: 1.1rem; letter-spacing: 0.01em;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
.nav a:hover { color: var(--accent); }
|
.nav a:hover { color: var(--accent); }
|
||||||
.acct {
|
.acct {
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
manrope-var.woff2 — "Manrope" variable font by Mikhail Sharanda / Mirko Velimirovic.
|
||||||
|
License: SIL Open Font License 1.1 (free to use/bundle, no attribution required in product UI).
|
||||||
|
Source: https://github.com/sharanda/manrope (via fontsource).
|
||||||
Binary file not shown.
Reference in New Issue
Block a user