Hero emotional-safety guardrail + calmer card polish
Hero guardrail (core to the promise, not cosmetic): - New hero.py: the lead story is chosen with a stricter filter than the rest of the brief — very low cortisol/ragebait and no grief/medical/violence terms (cancer, glioblastoma, death, diagnosis, ...). Such constructive-but-charged stories stay among the five; they just never lead by default. - /api/brief applies user avoid-terms FIRST, then lead_with_gentle, so personal boundaries always take precedence over the general guardrail. - Verified live: the brief no longer leads with a glioblastoma story. Card polish (per review): - Secondary cards with no real image are now text-first (no row of empty media bands); hero still always shows media or a typographic fallback. - Inline tuning actions are quiet until hover/focus on pointer devices, and stay visible (softer) on touch — less interface machinery. Tests: hero safety + lead reordering (70 total). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,15 +15,20 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<article class:hero>
|
||||
<a class="media" href={safeHref} target="_blank" rel="noopener">
|
||||
{#if article.image_url && imgOk}
|
||||
<img src={article.image_url} alt="" loading="lazy" referrerpolicy="no-referrer"
|
||||
onerror={() => (imgOk = false)} />
|
||||
{:else}
|
||||
<span class="fallback">{article.topic ?? 'good news'}</span>
|
||||
{/if}
|
||||
</a>
|
||||
<article class:hero class:textfirst={!hero && !(article.image_url && imgOk)}>
|
||||
<!-- Hero always shows media (image or fallback). Secondary cards only show a
|
||||
media band when a real image exists, so image-less cards are text-first
|
||||
rather than a row of empty blocks. -->
|
||||
{#if hero || (article.image_url && imgOk)}
|
||||
<a class="media" href={safeHref} target="_blank" rel="noopener">
|
||||
{#if article.image_url && imgOk}
|
||||
<img src={article.image_url} alt="" loading="lazy" referrerpolicy="no-referrer"
|
||||
onerror={() => (imgOk = false)} />
|
||||
{:else}
|
||||
<span class="fallback">{article.topic ?? 'good news'}</span>
|
||||
{/if}
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<div class="body">
|
||||
<div class="tags">
|
||||
@@ -95,9 +100,24 @@
|
||||
.actions { margin-top: auto; padding-top: 10px; display: flex; gap: 14px; flex-wrap: wrap; }
|
||||
.actions button {
|
||||
background: none; border: none; padding: 0; color: var(--muted);
|
||||
font-size: 0.78rem; border-bottom: 1px dotted var(--line);
|
||||
font-size: 0.76rem; border-bottom: 1px dotted var(--line);
|
||||
}
|
||||
.actions button:hover { color: var(--sage-deep); border-bottom-color: var(--sage); }
|
||||
/* On pointer devices, keep the tuning controls quiet until the card is hovered
|
||||
or focused — they shouldn't read as interface machinery. Touch devices (no
|
||||
hover) keep them visible. */
|
||||
@media (hover: hover) {
|
||||
.actions { opacity: 0; transition: opacity 0.16s ease; }
|
||||
article:hover .actions,
|
||||
article:focus-within .actions { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Text-first secondary cards (no real image): a little breathing room up top. */
|
||||
.textfirst .body { padding-top: 18px; }
|
||||
.textfirst .body::before {
|
||||
content: ""; display: block; width: 28px; height: 3px;
|
||||
background: var(--sage-soft); border-radius: 2px; margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.hero { display: grid; grid-template-columns: 1.1fr 1fr; }
|
||||
.hero .media { aspect-ratio: auto; height: 100%; min-height: 280px; }
|
||||
|
||||
Reference in New Issue
Block a user