Joy cards finalize (Codex pass): robust year align, image guard, a11y, honesty

/onthisday:
- Year alignment restructured per Codex: "2013" is the sole in-flow baseline
  anchor; "IN HISTORY" is absolutely positioned in reserved start-padding, so it
  can't drag the shared baseline (no more viewport-math/top-offset compensation).
  Rule baseline-aligns too. Narrow phones deliberately wrap the year lockup onto
  its own row (rule+date below) instead of accidental flex wrapping.
- Render the hero only when image_url exists (pool has imageless items → was a
  blank dark hero).
- "The story" → "A little context": the Wikipedia summary is context about an
  associated subject, not a narrative of the event. Honest without backend work.
- Figure detection also forces contain on filename hints (seal/flag/logo/map/
  diagram/crest/emblem/coat-of-arms) so JPEG logos/maps aren't cropped.

A11y contrast (AA): clay #b06a45→#9a5a38 (eyebrow, button, + homepage card
accent so they still match), small green IN HISTORY #3a7d5b→#367653, rose
attribution/share + Copy button → #8b596d.

/quote: native Share now carries the attributed text (author included, matching
Copy); narrow-phone guard so "QUOTE OF THE DAY" + eyelash don't crowd at 320px.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-26 21:10:11 -04:00
parent 36b3df5d40
commit 883c37b428
3 changed files with 44 additions and 28 deletions
+1 -1
View File
@@ -493,7 +493,7 @@
.joy-word .wm { right: -14px; bottom: -30px; font-size: 150px; font-weight: 400; color: rgba(79, 125, 168, 0.13); }
.joy-quote { background: linear-gradient(165deg, #F9EDF1, #F1DEE6); border: 1px solid #eed6df; box-shadow: 0 10px 30px -22px rgba(150, 85, 115, 0.5); --accent: #b06a86; --rule: #b06a86; }
.joy-quote .wm { left: 14px; top: -26px; font-size: 120px; color: rgba(176, 106, 134, 0.16); }
.joy-fact { background: linear-gradient(165deg, #F7EAE1, #EFDACB); border: 1px solid #ecd5c4; box-shadow: 0 10px 30px -22px rgba(150, 90, 55, 0.5); --accent: #b06a45; --rule: #b06a45; }
.joy-fact { background: linear-gradient(165deg, #F7EAE1, #EFDACB); border: 1px solid #ecd5c4; box-shadow: 0 10px 30px -22px rgba(150, 90, 55, 0.5); --accent: #9a5a38; --rule: #9a5a38; }
.tag { display: flex; align-items: center; gap: 8px; }
.tag .rule { width: 18px; height: 2px; border-radius: 2px; background: var(--rule); }
+24 -19
View File
@@ -24,6 +24,8 @@
let heroFit = $derived.by(() => {
const url = f?.image_url || '';
if (/\.(png|svg)(\?|#|$)/i.test(url)) return 'contain';
// filename hints catch JPEG logos/maps/crests the aspect check would crop
if (/(seal|flag|logo|map|diagram|crest|emblem|coat[_-]?of[_-]?arms)/i.test(url)) return 'contain';
if (heroAspect == null) return 'cover';
return (heroAspect < 0.9 || heroAspect > 2.0) ? 'contain' : 'cover';
});
@@ -67,18 +69,18 @@
<span class="dl-label">{dateline(f.date)}</span>
</div>
<div class="hero" class:figure={heroFit === 'contain'}>
{#if f.image_url}
{#if f.image_url}
<div class="hero" class:figure={heroFit === 'contain'}>
<img class="hero-img" src={f.image_url} alt="" loading="lazy" onload={onHeroLoad} />
{/if}
</div>
</div>
{/if}
<p class="headline">{f.text}</p>
{#if f.summary}
<div class="story-head">
<span class="sh-rule"></span>
<span class="sh-label">The story</span>
<span class="sh-label">A little context</span>
</div>
<p class="story">{f.summary}</p>
{/if}
@@ -117,23 +119,26 @@
padding: clamp(26px, 5vw, 34px) clamp(20px, 5vw, 34px) clamp(34px, 6vw, 44px); overflow: hidden;
}
/* eyebrow matches the homepage "On this day" card accent (clay/brorange) so the card → page read as one */
/* eyebrow matches the homepage "On this day" card accent (clay) so the card → page read as one */
.eyebrow { display: flex; align-items: center; gap: 14px; }
.eye-rule { width: 34px; height: 3px; background: #b06a45; border-radius: 2px; flex: none; }
.eye-label { font-family: 'Hanken Grotesk', sans-serif; font-size: clamp(17px, 2.4vw, 22px); font-weight: 700; letter-spacing: 0.13em; text-transform: uppercase; color: #b06a45; }
.eye-rule { width: 34px; height: 3px; background: #9a5a38; border-radius: 2px; flex: none; }
.eye-label { font-family: 'Hanken Grotesk', sans-serif; font-size: clamp(17px, 2.4vw, 22px); font-weight: 700; letter-spacing: 0.13em; text-transform: uppercase; color: #9a5a38; }
/* extra breathing room between the title and the content below */
.dateline { display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px 16px; margin: clamp(32px, 5vw, 48px) 0 16px; }
.dl-rule { flex: 1 1 30px; min-width: 24px; height: 1.5px; align-self: flex-end; margin-bottom: 0.7em; background: rgba(70, 120, 90, 0.26); }
.dl-rule { flex: 1 1 30px; min-width: 24px; height: 1.5px; align-self: baseline; margin: 0; background: rgba(70, 120, 90, 0.26); }
.dl-label { font-family: 'Hanken Grotesk', sans-serif; font-weight: 700; font-size: clamp(14px, 1.7vw, 17px); letter-spacing: 0.1em; text-transform: uppercase; color: #2c5d44; }
/* year lives here (off the image) — "IN HISTORY" top-aligned beside a big "2013", sharing
ONE baseline with the date so "2013" sits ON the line (not below it). Both green, so the
line ties in the green Read-more button. The label is raised post-layout via position:
relative, which does NOT move the baseline the date aligns to. */
.year-block { --ys: clamp(34px, 5.5vw, 46px); display: inline-flex; align-items: baseline; gap: 7px; flex: none; }
.yb-label { position: relative; top: calc(11px - 0.7 * var(--ys)); font-family: 'Hanken Grotesk', sans-serif; font-size: 12px; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: #3a7d5b; }
.yb-year { position: relative; top: -2px; font-family: 'Playfair Display', Georgia, serif; font-weight: 700; font-size: var(--ys); line-height: 1; color: #2c5d44; }
/* "2013" is the SOLE in-flow baseline anchor (shares one baseline with the date + rule);
"IN HISTORY" is absolutely positioned in the reserved start-padding so raising it can never
drag that baseline (Codex's structure). Both green so the line ties in the Read-more button. */
.year-block { --ys: clamp(34px, 5.5vw, 46px); position: relative; display: inline-block; padding-inline-start: 90px; line-height: 1; flex: none; }
.yb-label { position: absolute; inset-inline-start: 0; top: clamp(4px, 0.7vw, 6px); line-height: 1; font-family: 'Hanken Grotesk', sans-serif; font-size: 12px; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: #367653; }
.yb-year { position: static; display: inline-block; font-family: 'Playfair Display', Georgia, serif; font-weight: 700; font-size: var(--ys); line-height: 1; color: #2c5d44; }
/* the full line can't fit cleanly on a narrow phone — wrap it deliberately: year lockup on its
own row, rule + date on the next (instead of accidental mid-line flex wrapping) */
@media (max-width: 520px) { .year-block { flex: 0 0 100%; } }
.hero {
position: relative; border-radius: 12px; overflow: hidden; background: #21392e;
@@ -159,14 +164,14 @@
}
.cta-row { margin-top: clamp(24px, 4vw, 30px); }
/* clay to match the "On This Day" title — gives this card its own identity */
/* clay to match the "On This Day" title — gives this card its own identity (AA: white on #9a5a38) */
.cta {
display: inline-flex; align-items: center; gap: 8px; background: #b06a45; color: #fff;
display: inline-flex; align-items: center; gap: 8px; background: #9a5a38; color: #fff;
text-decoration: none; font-family: 'Hanken Grotesk', sans-serif; font-size: 14px; font-weight: 600;
padding: 11px 20px; border-radius: 999px; -webkit-tap-highlight-color: transparent;
transition: background 0.15s ease;
}
.cta:hover { background: #9a5a38; }
.cta:hover { background: #854c2f; }
.note { text-align: center; color: var(--muted); font-size: 1.05rem; margin-top: 60px; }
</style>
+19 -8
View File
@@ -13,11 +13,13 @@
return MONTHS[m - 1] ? { mon: MONTHS[m - 1], day: d } : null;
});
// one attributed string for both Copy and Share, so native sharing keeps the author too
let attributed = $derived(q ? `“${q.text}${q.author ? ` — ${q.author}${q.work ? ', ' + q.work : ''}` : ''}` : '');
let copied = $state(false);
async function copyQuote() {
const attr = q?.author ? ` — ${q.author}${q.work ? ', ' + q.work : ''}` : '';
try {
await navigator.clipboard.writeText(`“${q.text}${attr}`);
await navigator.clipboard.writeText(attributed);
copied = true; setTimeout(() => (copied = false), 1800);
} catch { /* no clipboard — silent */ }
}
@@ -26,7 +28,7 @@
async function share() {
const url = location.href;
try {
if (navigator.share) { await navigator.share({ title: 'Quote of the Day · upbeatBytes', text: `“${q.text}”`, url }); return; }
if (navigator.share) { await navigator.share({ title: 'Quote of the Day · upbeatBytes', text: attributed, url }); return; }
await navigator.clipboard.writeText(url);
shared = true; setTimeout(() => (shared = false), 1800);
} catch { /* cancelled / unsupported */ }
@@ -150,7 +152,7 @@
.attrib { position: relative; display: flex; align-items: center; gap: 14px; margin-top: clamp(20px, 3vw, 26px); }
.at-rule { width: 28px; height: 1.5px; background: #c79bac; flex: none; }
.at-by { font-family: 'Newsreader', Georgia, serif; font-size: clamp(1rem, 1.6vw, 1.13rem); color: #8f5e72; }
.at-by { font-family: 'Newsreader', Georgia, serif; font-size: clamp(1rem, 1.6vw, 1.13rem); color: #8b596d; }
.work { font-style: italic; }
.hr { position: relative; height: 1px; background: rgba(120, 90, 40, 0.16); margin: clamp(28px, 4vw, 38px) 0 24px; }
@@ -168,10 +170,19 @@
font-size: 14px; font-weight: 600; padding: 10px 18px; border-radius: 999px; cursor: pointer;
-webkit-tap-highlight-color: transparent; transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.act-copy { background: #a4607a; color: #fff; border: none; }
.act-copy:hover { background: #8f5165; }
.act-share { background: transparent; color: #8f5e72; border: 1px solid #d3b3c0; }
.act-share:hover { border-color: #a4607a; color: #a4607a; }
/* AA contrast: white on #8b596d passes; rose text uses the accessible secondary */
.act-copy { background: #8b596d; color: #fff; border: none; }
.act-copy:hover { background: #774a5c; }
.act-share { background: transparent; color: #8b596d; border: 1px solid #d3b3c0; }
.act-share:hover { border-color: #8b596d; color: #774a5c; }
.note { text-align: center; color: var(--muted); font-size: 1.05rem; margin-top: 60px; }
/* narrow phones: keep "QUOTE OF THE DAY" + its eyelash from crowding/clipping */
@media (max-width: 380px) {
.deckle { padding-left: clamp(18px, 6vw, 24px); padding-right: clamp(18px, 6vw, 24px); }
.eyebrow { gap: 9px; }
.eye-rule { width: 24px; }
.eye-label { letter-spacing: 0.08em; }
}
</style>