/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>
Names the ritual (this date in history) rather than describing it; matches the
/onthisday page + engine. Hero tag becomes "{year} in history" (the old
"ON THIS DAY" there was now redundant with the eyebrow).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- home3 dropped source_read_minutes when mapping the brief item, so the badge
only ever showed "1 min brief" — include it so "· ~N min full story" appears.
- /art is public but shipped the homepage title/canonical/OG (SSR-off shell, so
svelte:head can't fix scrapers) — add /art to patch-static-heads.mjs.
- Bloom: center the circular motif in the tile's visual field (~43%) instead of
the top illustration zone, so it reads as a medallion (per Jay + Codex).
- Polish: prefers-reduced-motion disables the tile hover-lift; fix stale
patch-play-head.mjs comment reference.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Desktop: the gist flex-fills the card with flex-basis:0 so it never inflates the
row height — the right column always sets it and is never stretched — and fades
softly into a comfortable margin above the read-time. Mobile keeps the clean
3-line clamp (natural height, no fade).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Desktop: .news align-self:start so it sizes to its content instead of stretching
to the right column's height — the "1 min brief" footer sits just under the gist
rather than floating at the bottom of a tall card. Right column sets row height;
bento stays tight.
- Mobile: tighten the gap under the gist before "1 min brief" (was ~a full line).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
It's already a summary, so show only a taste and let the reader click through.
This also stops the News card being the tallest element, which had stretched the
right column and spread its cards — they pack naturally again. Footer (brief ·
read-more) pinned to the card bottom via margin-top:auto.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The min-width:0/max-width:100% containment rule included .bento, and being a later
rule of equal specificity it overrode .bento's own max-width:1180px → grid went
edge-to-edge. Scope the rule to the grid items + flex chain (.news/.rightcol/
.pair-wrap/.joys-shelf/.joys) instead; .bento keeps its 1180px cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Restore desktop/tablet two-up Small Joys (arrows rotate which two show); the
swipe rail is now phone-only (≤520). One isNarrow flag drives both the DOM
(2 vs 3 cards) and the layout, so they can't disagree.
- Contain the overflow that let the phone rail widen the whole page: min-width:0
+ max-width:100% down the chain (bento/rightcol/pair-wrap/joys-shelf/joys),
mobile bento column to minmax(0,1fr), and overflow-x:clip on .page as a seatbelt.
- Read-time badge: overflow-wrap:anywhere + line-height so a long
"1 min brief · ~N min full story" never causes width pressure.
- Self-host Work Sans (latin variable woff2, OFL) and apply it to the cards —
the bolder/darker look from CD's mockup; headings stay Newsreader.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- /play: hub view now shows a "Back" row under HubBar too (leaves Play to where
you came from), so every hub page has the same bar+back rhythm — no shift.
- Eyelash on every card: each .label now opens with a short accent dash (via
currentColor, so each label's colour drives it) + tighter tracking (.18em, 600),
on desktop and mobile alike.
- Card headings to Newsreader 500 (calmer), body to 15px/1.5 #5a5346 (per CD).
- Small Joys is now a single swipeable row (all 3 cards in a scroll-snap rail, the
next peeking; arrows scroll, counter tracks the snapped card) instead of a
2-up rotation — more elegant, and the 1/3 ‹ › affordance is honest.
- Daily Art card: more breathing room before "View today".
Note: used our self-hosted Hanken Grotesk for the card sans (CD's mockup used
Google Work Sans) — same metrics applied; can self-host Work Sans if preferred.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- WOTD /word: watermark was nudged too far down last round; bring it back up to
sit JUST below the date (clears it by a few px instead of dropping markedly).
- home3 Entertainment card: stacked on mobile it lost the height it borrowed from
Play on desktop and felt crowded — add vertical breathing (padding + gaps), not
as tall as desktop.
- /art lightbox: on a narrow portrait phone the framed piece (image + mat + rail)
overflowed the viewport (rail clipped → looked distorted). Cap the image so the
WHOLE frame fits, leaving room for the caption.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per the convo with Codex: keep the value proposition on the card rather than
hiding everything when full-story time is unknown.
- summary + full-story time → "1 min brief · ~10 min full story"
- summary only → "1 min brief"
- no brief/summary → hide the row
"brief" over "summary" (more editorial); full-story time still never faked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the gist-based read-time with the SOURCE article's full read time — the
contrast that sells the gist ("calm 1-min version here; ~10 min for the deep dive").
- goodnews/readtime.py: word_count_from_html (strips script/style/nav/header/
footer/form/button/aside furniture before counting) + source_read_minutes
(~225 wpm, 200-word floor, None when extraction looks failed/too thin).
- articles.source_words + read_checked_at columns (count only, never the body;
fits the privacy posture). Idempotent migration.
- enrich.fetch_source_words + enrich_read_times: a bounded, retry-guarded cycle
step (mirrors the image enrichers) that counts words for recent accepted
articles. Only ever writes a real count; never overwrites good with zero. Wired
into the cycle after recent-image enrichment.
- queries: source_words flows through _ARTICLE_COLUMNS; api exposes
source_read_minutes on Article (null when unknown).
- home3: News card shows "Full story · ~N min", hidden entirely when null (no
misleading "1 min").
- Tests: furniture stripping, threshold/rounding, enrich idempotency + no
zero-overwrite, API null handling. 412 backend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- News card "3 min read" → computed from our own gist (~200 wpm, floor 1). We
summarize, so it's honestly ~"1 min read" — the good news in about a minute.
- Generalized the build-time head patch (patch-play-head → patch-static-heads):
now also rewrites build/word.html, quote.html, onthisday.html so each ships
its own <title>/description/canonical/OG/Twitter tags instead of the homepage
head + canonical="/". Non-JS scrapers and canonical dedup are correct before
these pages are ever un-noindexed. Same fail-loud guard as before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#2 Mobile top bar → hamburger: extracted the editorial bar (brand + nav + new
collapsible hamburger drop-panel) into a shared lib/components/HubBar.svelte,
used by both /home3 and HubShell (the /word /quote /onthisday detail pages), so
there's one nav to maintain/audit. Full horizontal nav ≥721px; hamburger + drop
panel ≤720px. Escape + link-click close it; panel is hidden on desktop as a
safety. Removed the duplicated bar markup/CSS from home3 + HubShell.
#1 Mobile layout / Art card: on phones the Art card now stacks image-first with
the painting in a proper 3:2 frame (aspect-ratio) instead of a stubby fixed
130px band that cropped the work to a sliver. Also drop the News gist's bottom
fade once the bento is single-column (natural height = no truncation, so the
fade was just dimming the final line), and let the joys header wrap on phones.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- /home3: small-joys rail now reads live /api/word|quote|onthisday/today (placeholders only
as fallback); each cell links to its detail page.
- HubShell component (shared bar/footer/fonts/tokens) for the hub + detail pages.
- /word: big word, IPA, Listen (cached clip + browser-TTS fallback), definition, sentences.
- /quote: the quote, attribution, and the AI "what it means".
- /onthisday: the date, year + fact, image, summary, source.
- Admin "Small Joys" tab: per-pool list with feature/block/delete/add + re-pick, for all
three kinds. New admin API: GET/POST /api/admin/joys/{kind}[/{id}|/add|/repick].
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two jewel cards at a time, reader-advanced (no auto-motion); 3 cells total, wraps. Keeps
each card at generous size instead of cramming three.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Decorative tiles (no live answer = no spoilers); placeholder copy. Built from CD's
Play Card Graphic handoff.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- News summary + headline clamped to 3 lines so the long brief gist no longer stretches
the bento (option 1; placeholder-bubble option still available if preferred).
- News CTA "Read the brief" → "Read the good news" (clearer for first-time visitors).
- Art swatch crops a few px off every edge (::after inset) to remove the black scan edge
at the top of paintings.
- Logo 42→48px, nav 15→16.5px, card titles bumped + weight 500→600 so they pop on scan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Rebuilds the design handoff's preferred Frame A: Newsreader serif headlines + Hanken
Grotesk body (both self-hosted, OFL, no Google hotlink), warm cream canvas, per-card
accent tints (News teal, Art plum, Play amber, Moment green), bento grid.
- Uses our real /logo.svg instead of the mock's Bricolage wordmark + sunrise.
- Wired live: Good News pulls the top headline/summary/photo (respects the saved
Closer-to-Home filter); Daily Art pulls today's Met piece (title/artist/year + thumbnail).
- Hidden prototype (noindex), spacing tuned per the /home2 pass (hero pulled up, more air
before the bento). Sits beside /home2 for comparison.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>