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:
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user