The rotated fullscreen clipped the top rail: the image's short edge plus rail/mat
exceeded the phone's narrow width. Cap the short edge (max-height pre-rotation),
use a modest fixed rail/mat in this view so the moulding always fits regardless
of the thickness slider, and hide the (now-sideways) caption — the placard is on
the page anyway.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Conditional 90° rotate of the fullscreen stage (frame + caption together): only
when the artwork is landscape AND the screen is a narrow portrait phone — so a
wide painting fills the long axis (turn phone to view level, tap to close).
Portrait/square art and desktop stay upright. Landscape detected from the loaded
image's natural dimensions; CSS decides WHEN via (orientation: portrait). Image
caps use swapped vh/vw units post-rotation so the whole framed piece always fits.
No screen-orientation lock API (unreliable on mobile web) — pure CSS transform.
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>
- IPA pronunciation bumped to 23px (it reads small at body size).
- Vertical part-of-speech label 11px -> 13px and darkened (#7fa0bd -> #5f86a6).
- Watermark nudged down so the glyph's top curve no longer overlaps the date.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Content quality ("LLM polishes, dictionary anchors"):
- New wotd._polish: rewrites the real dictionary gloss into ONE warm plain
sentence + two clear everyday example sentences, grounded in the real
definition (no invented meanings). Stored in new wotd_pool/daily_wotd columns
gloss + usage, alongside the raw definition/examples which stay the anchor.
- harvest() polishes each new word; pick_daily() lazily polishes + caches back
any older pooled word that lacks a gloss (client threaded through run_daily).
- Admin word-add polishes on insert; re-pick passes an LLM client so quote
meaning / word gloss fill on a forced fresh pick.
- /api/word/today now prefers gloss + usage, falling back to the raw dictionary
def/examples when polish is absent (so it's always safe).
- db._migrate adds gloss/usage to wotd_pool + daily_wotd (idempotent ALTER).
Frontend — /word redesigned to CD's "Editorial Asymmetric": faded oversized
initial bleeding off the right, vertical part-of-speech rail, big Newsreader
word, airy definition, left-ruled italic example sentences, outline Listen
button + date. (Uses our self-hosted Newsreader/Hanken stack rather than the
mockup's Google fonts; the made-up syllable respelling is omitted since we only
have real IPA.)
Tests: _polish parse/trim/cap, harvest stores gloss/usage, pick lazy-polishes
older words, admin gloss flows through to /api/word/today. 403 backend + 27 fe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex audit polish:
- Cold deep-link Back now goto('/home3', { replaceState: true }) instead of
pushing a new entry, so the browser Back from the hub can't bounce the reader
straight back into the detail page. In-app arrivals still history.back().
- HubBar closes the hamburger when crossing to desktop width (matchMedia change),
so `open` can't go stale and reappear if the viewport shrinks back to mobile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#3 Back buttons: HubShell now renders a top-left "‹ Back" on each detail page
(/word /quote /onthisday). It mirrors the News reader's rule — if you arrived by
an in-app navigation, history.back() returns you to the hub with state/scroll
intact (and the browser Back button traverses the same single history); a direct
deep-link has no app history, so it falls back to goto('/home3'). Opt-out via
back={false}; relabel via backLabel.
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>
- Admin joy item route moved to /api/admin/joys/{kind}/items/{item_id} so the
/add and /repick verbs resolve to their own routes instead of 422-ing as a
non-int item id (the launch blocker). Frontend mutate URL updated to match.
- Re-pick now excludes the currently-shown item: the endpoint reads today's
daily pool_id and passes it as `avoid`, so "Re-pick today" yields a different
item. Added `avoid` to pick_daily/_candidates across wotd/quote/onthisday.
- WOTD sense selection: the LLM now proposes word + intended part of speech, and
_lookup prefers that sense (fixes "serene" returning the archaic noun).
- On This Day tone prompt tightened to favor genuinely uplifting events and
exclude merely procedural/political-administrative ones.
- Caddy @hidden now also noindexes /word /quote /onthisday /admin (+ .html).
- Regression tests: add/repick resolve (401 not 422), add/feature/block/delete,
re-pick excludes current; WOTD pos-preference + proposal parsing units.
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>
- All four cards now tinted with a taller (18px), darker (~24%) top color strip.
- Photos (news top, art right) sit inset + rounded so the card colour frames them.
- Static cards: header centered; CTA pinned to the bottom so Play/Daily Moment links line
up. News tint = soft amber, Art = soft lilac, Play = sky, Moment = sage.
- Hub spacing: intro pulled up a touch, more gap between the intro and the cards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Art photo moved to the RIGHT (text/icon left) so it no longer clusters with the news
photo in the center.
- Unified header: every card now has icon + title side-by-side; card titles switched from
serif (too 'document') to Manrope bold. Art icon/title now match Play/Daily Moment sizes.
- Static cards (Play/Daily Moment) gain a darker top color strip (Monopoly-card feel) over
the tinted body, derived from each tint via color-mix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Self-hosted Manrope (OFL) as the hub sans; nav lighter (weight 500, soft slate, not all
"on"). Logo up to 58px.
- News card: photo on top + headline below, and it now respects the reader's saved
Closer-to-Home filter (goodnews:home/homeScope) so the headline matches their Brief.
- Art card: rectangular cover-cropped thumbnail on the LEFT (crops ragged scan edges),
text on the right — variety against the photo-top news card.
- Play/Daily Moment: tinted backgrounds, bigger centered icon+title, blurb left-aligned.
- /fonts/* + /textures/* served immutable (Caddy live + snapshot).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New sections registry (lib/rooms.js): each room is one data entry (title/blurb/href/cta/
size/preview/icon) — add or resize by editing the list.
- Reusable RoomCard (lib/components/RoomCard.svelte) with size variants and hybrid previews:
Art shows today's live thumbnail, News shows today's top headline, others are blurb+CTA.
- /home2 hidden prototype (noindex, unlinked) with a bigger top bar (logo 50px, larger nav
labels) and a reflowing grid hub of the four rooms (News/Art/Play/Daily Moment). Iterate
the look here, then promote to / and remove the clone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- /api/art/image/{id} now answers HEAD as well as GET (was 404 on HEAD) — mirrors the
/a/{id} fix. Added tests/test_art_api.py (GET+HEAD+size=full fallback + today payload).
- /textures/* served immutable (long cache) instead of no-cache; excluded from the
revalidate matcher. Live Caddyfile + repo snapshot both updated.
- Lightbox: Escape closes it, and focus moves to it on open (keyboard-friendly).
- Trimmed the gallery's top padding so "Daily Art" sits closer to the bar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Woods are now built from four real mitered rails clipped to 45° joints, each showing the
grain along its length — horizontal on top/bottom (rotated texture wood-grain-h.jpg),
vertical on the sides — so the grain TURNS at every corner like real cut moulding. Each
rail carries a directional bevel (lit top-left) for a rounded profile; per-species recolor
moved onto the rails. Rendered via a Svelte snippet in both page + full-screen frames.
- Gold/Silver gain fine brushed-metal striations over the existing sheen.
- New asset: wood-grain-h.jpg (the CC0 texture rotated 90°), credited in textures/CREDITS.txt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- No frame in full-screen now fills the screen (img max 96vw / 88vh instead of the
framed cap).
- Full-screen backdrop is a soft top-lit gallery wall (light, not dark) so the Black
frame — and every frame — reads like a piece hung on a real wall; caption text darkened
to match.
- Mat now sits recessed below the moulding: a rabbet groove line at the lip, frame shadow
on the mat's top/left, catch-light on bottom/right — a small chiseled step in place of
the flat frame→mat transition.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>