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}
+
+ {humanize(article.topic) || 'upbeat bytes'}
+
{/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}