diff --git a/frontend/src/lib/components/ArticleCard.svelte b/frontend/src/lib/components/ArticleCard.svelte index 492e66e..a4400db 100644 --- a/frontend/src/lib/components/ArticleCard.svelte +++ b/frontend/src/lib/components/ArticleCard.svelte @@ -2,7 +2,7 @@ import { auth, savedIds, toggleSave } from '$lib/auth.svelte.js'; import { track } from '$lib/analytics.js'; - let { article, onaction, onreplace, ontag, onimageerror, onview, hero = false } = $props(); + let { article, onaction, onreplace, ontag, onimageerror, onview, hero = false, thumb = false } = $props(); function opened() { // Records history; the /a page itself fires the summary_viewed event on load. @@ -38,8 +38,12 @@ failed = false; }); let hasImg = $derived(!!article.image_url && !failed); - // The hero is the ONLY image slot; every other card is typographic. - let showImage = $derived(hero && hasImg); + // Hero shows a large image. A `thumb` card (brief rest cards) ALWAYS carries a + // banner so the grid stays uniform: a real image when it has one, otherwise a + // calm topic-colored placeholder. Every other card stays typographic. + let showImage = $derived(hasImg && (hero || thumb)); + let isThumb = $derived(thumb && !hero); + let usePlaceholder = $derived(isThumb && !hasImg); let safeHref = $derived( typeof article.url === 'string' && /^https?:\/\//.test(article.url) ? article.url : '#' ); @@ -91,6 +95,7 @@ class:hero={hero && hasImg} class:herotype={hero && !hasImg} class:tile={!hero} + class:hasthumb={isThumb} data-topic={article.topic ?? ''} > {#if showImage} @@ -98,6 +103,10 @@ { failed = true; onimageerror?.(); }} /> + {:else if usePlaceholder} + {/if}
@@ -277,6 +286,37 @@ article:focus-within .actions .mute { opacity: 1; } } + /* ---- Compact top image for brief rest cards (thumb) ---- */ + /* A modest, fixed-ratio banner — selective image confidence, not a media wall. + Light desaturation for calm cohesion; never a heavy tint. */ + .tile .media { + display: block; position: relative; width: 100%; + aspect-ratio: 16 / 9; overflow: hidden; border-bottom: 1px solid var(--line); + background: linear-gradient(135deg, var(--accent-soft), #f1ece0); + } + .tile .media img { + width: 100%; height: 100%; object-fit: cover; display: block; + filter: saturate(0.9); + } + /* Image-less cards get a calm placeholder banner in the card's topic color + (--c), so every card in the grid is the same size — uniform, not lacking. */ + .tile .media.placeholder { + display: flex; align-items: center; justify-content: center; text-decoration: none; + background: + radial-gradient(130% 150% at 0% 0%, color-mix(in srgb, var(--c) 26%, transparent), transparent 62%), + linear-gradient(135deg, color-mix(in srgb, var(--c) 13%, var(--surface)), var(--surface)); + } + .tile .media.placeholder .ph-word { + font-family: var(--serif); font-size: 2rem; line-height: 1; + color: var(--c); opacity: 0.34; text-transform: lowercase; letter-spacing: -0.01em; + } + /* A thumb card's banner carries the topic identity, so drop the faint corner + watermark there to avoid a doubled word. */ + .tile.hasthumb::after { display: none; } + /* Every thumb card carries a banner, so trim the summary to keep total height + compact and uniform across the grid. */ + .tile.hasthumb .summary { -webkit-line-clamp: 2; } + /* ---- Typographic editorial tile (every non-hero card) ---- */ .tile { position: relative; } /* The .cardhead owns the top padding (above the accent); avoid doubling it. */ diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 292e4dc..16e9555 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -350,7 +350,7 @@ {#if restArticles.length}
{#each restArticles as a (a.id)} - select('tag:' + t)} onview={record} /> + select('tag:' + t)} onview={record} /> {/each}
{/if}