Agents: this site has a machine-readable contract. The home page (googlefontfinder.com) carries a self-contained quickstart for composing a shareable type direction in one fetch; the full contract is at /agent.md, indexed at /llms.txt.
status means "the page differs from your ask" — not "a note exists"
0.1Same-day fix to the note model shipped with /compose.json. Two bugs, both of which punished exactly the behavior the surface is built to encourage.
0.2statusOf was `notes.length ? degraded : ok` — it computed a severity field and then ignored it. So `?bogus=1` (an ignored param, nothing rendered differently) reported `degraded`, and so did a brand color deliberately rendered below AA. An agent optimizing for a clean status would rationally stop stating colors, which is the opposite of the hex freedom shipped hours earlier. `status` is now `degraded` only when a note is `warn`.
0.3`contrast_below_bar` meant two opposite things. It fired at the page-chrome site, where the color WAS adjusted, and at the card-theme site, where it was NOT — so a consumer branching on it could not tell whether its hex survived. Split into `contrast_below_bar` (advisory, your hex rendered untouched, info) and `contrast_adjusted` (we moved it, warn).
0.4Severity now answers one question everywhere: did the rendered page end up different from what was asked? warn = card_dropped, font_substituted, pairs_truncated, pairs_fallback, theme_fallback, hex_invalid, url_stripped, text_truncated, contrast_adjusted. info = param_ignored, value_clamped, slug_resolved, contrast_below_bar. text_truncated moved info -> warn (your headline got cut) and pairs_truncated was mixed across its two sites, now warn at both.
0.5Net: the yoga and party URLs go back to `status: ok` with the contrast fact still attached as an advisory note. The information is preserved; the false verdict is gone.
Key files
lib/compose-notes.ts — statusOf keys off severity; contrast_adjusted added; the severity rule documented as one question
lib/card-themes.ts — the two contrast sites take different codes and severities
lib/compose-params.ts — text_truncated and pairs_truncated corrected to warn
lib/compose-resolve.test.ts — 6 new tests pinning ok-vs-degraded per case
public/agent.md — status documented as the differs-from-your-ask question, with an explicit 'do not avoid stating colors to keep this clean'
/compose.json — the composed page as data, with colors that say where they came from
1.1Swap `/compose?` for `/compose.json?`, keep every param identical, and get the resolved page as JSON. This closes the revision loop: a user says 'let's go with the second one' and the agent already holds that card's fonts, palette and tokens. Previously the payload existed only as visually-hidden HTML, so consuming it meant regex over markup while stepping around the Next RSC payload.
1.2A mirror, not a scrape. New lib/compose-resolve.ts holds the single resolved model; the page and the JSON are two serializers over it, so they cannot drift. parseComposeParams was already ~90% of this — the extraction only had to absorb param normalization, the default-direction fallback, voice defaulting, and buildHandoff.
1.3Color provenance is the load-bearing addition and the one thing HTML could not express. Every role in cards[].colors is { hex, source }: `stated` (the user wrote it — rendered exactly, contrast bar or not), `derived` (we computed it, so it was clamped), or `from:<role>` (title from fg, subtitle/label from accent, paragraph from muted). `source` names the origin, `hex` is what rendered, and they need not match — a from:accent subtitle is routinely darker because derived values get clamped and stated ones don't. This is what tells a developer which colors are the client's and must be pinned.
1.4Fixed the themes[0] handoff defect: buildHandoff emitted deck-wide Tailwind/CSS tokens from the first card's palette only, so a three-direction page handed off one card's colors and silently misdescribed the other two. Token builders are extracted and now run per card as well as deck-wide.
1.5Notes are structured. All 28 push sites across compose-params.ts and card-themes.ts now emit { code, severity, target, message } with the prose byte-identical, so the rendered page is unchanged and a consumer can branch on a stable token instead of regexing English. Codes ship WITH the endpoint rather than after it — adding them later would have been a breaking change to a published contract.
1.6Cards are numbered 01/02/03 on the page. The revision loop is spoken ('the second one') and nothing carried a number, so the human's reference and the JSON's index were related only by counting. Cards also carry requestedIndex: if a card drops for an unknown slug, render position and the order the agent asked for diverge, and both are now reported.
1.7Deliberate deviations from the draft spec: camelCase throughout (the existing /api routes are camelCase; two conventions on one surface makes agents guess wrong); content fields are always { text, source } rather than sometimes a bare string; contrast covers every text role rather than three, since subtitle — the accent-derived, clamped one — was the notable omission; no Accept negotiation, .json path only, so a CDN never has to Vary.
Key files
app/compose.json/route.ts — new; the serializer, with provenance, per-card contrast, and per-card tokens
lib/compose-resolve.ts — new; the shared resolved model behind both surfaces
Every painted slot takes a hex; omitted ones come from taste, not arithmetic
2.1A composed page came back flat: the subtitle and the paragraph rendered the same dull grey, and an explicitly chosen accent did no visible work. Three separate causes, all fixed.
2.2The subtitle and paragraph were identical BY CONSTRUCTION — neither was ever derived. ComposeCard painted both from `theme.muted` via `?? theme.muted`, so omitting either guaranteed they matched. The CardTheme doc comment already named this as a known defect.
2.3`muted` itself was `mix(fg, bg, 0.42)`. Blending the ink into the field is an average, and averages trend to grey; no coefficient fixes that. It now borrows from the curated palette nearest the background — a designer's actual choice — the same way `accent` already did.
2.4A stated hex did not survive: `accent:EF5DA8` shipped as `E358A0`, silently walked toward black until it cleared 4.5:1. Contrast handling now splits by who chose the color. A hex an agent wrote down is an intention (a brand color, a palette from a reference) and renders exactly, with a note if it sits under the bar; a value we derived carries no intention and is still clamped. The tradeoff is real — a fully-specified palette can now be made unreadable — and the note is how the contract stays honest instead of silently substituting.
2.5Every painted element is now addressable: `rule` (the hairline above the font-name line) and `label` (the line itself) join the existing roles, so nothing on the card is off-limits to a hex. New `resolveTheme` fills whatever is unstated and is applied to EVERY card theme, curated ones included, so `theme=3` gets the same treatment as a hand-written palette. Dominant text roles come from curated relationships; ancillary chrome derives off the accent, which is where derivation is genuinely fine.
2.6Net effect: `bg` + `fg` + `accent` — the three an agent actually knows — now produce a page with a point of view rather than a neutral box with one colored word in it.
2.7The color key follows the cards. With per-card `themes=` it was flattening three palettes into a single row, leaving the reader to work out which swatch belonged to which card; each palette now sits in a column under its card, sharing the card row's flex sizing. A single shared palette has nothing to align to, so it stays a left-aligned strip of fixed chips rather than stretching five swatches to card width.
2.8Nudges agents toward `themes=` (plural) when showing more than one direction — the run that prompted this used `theme=` singular for a three-option request, which makes three options look like one option typeset three ways.
Near-miss font slugs resolve; the surface stops asking agents to verify
3.1An evaluation run showed /compose working but over-processed: asked for 'three cards for a kid's party', the agent spent several sequential round-trips (query /api/fonts, query /api/pairings, fetch the composed page back to read #agent-notes) before emitting a URL. Asked afterward to hand-write one from the grammar, it produced an equally good result in zero tool calls. This closes the gap between those two runs.
3.2New lib/font-match.ts: a slug an agent guessed from memory now resolves to the nearest real family instead of dropping its card. Four ordered tiers — exact, normalized (`Playfair_Display`), prefix expansion for a missing version suffix (`source-serif` → Source Serif 4), then Levenshtein ≤ 2 gated on a 25%-of-input ratio and a 5-character floor. The guard is as load-bearing as the match: `helvetica` still resolves to nothing, because a matcher that always finds something turns a wrong guess into a silently wrong specimen. Every substitution is named in the render notes, and the canonical URL already re-emitted resolved slugs, so a corrected URL comes back for free.
3.3This is the real fix for latency. An agent's verification round-trip was a rational response to genuine residual risk — 'guessing is safe' covered params but not slugs. Removing the risk removes the round-trip; telling the agent not to worry would not have.
3.4One verification stance everywhere, resolving a contradiction an agent had to arbitrate: the root quickstart said 'Do NOT fetch the URL back', agent.md's worked example said 'Fetch that URL back, confirm data-status=ok'. Fetch-back is now documented as a debugging tool, never a step. The honest exception is stated rather than papered over — #agent-specs only exists on the composed page, so a user who is going to *build* the direction does cost one fetch, for the payload, not to check the work.
3.5agent.md gains a front-loaded 'If you read nothing else' block: the fill-in URL (byte-identical to the root quickstart's), the full param table, the slug rule, and the guessing-is-safe guarantee — enough to emit correctly from the first screen alone. The JSON endpoints are reframed as worth a round-trip only for data the agent can't recall (feeling tags, curated pairings, real axis ranges).
3.6Fixed: `?strict=1` carried over from an /api route no longer produces a spurious 'is not a compose parameter' note on /compose (it's inert there by design). Removed `kind` from /api/pairings, which was accepted and echoed in `applied` but filtered nothing and was documented nowhere.
Key files
lib/font-match.ts — new; matchSlug + a ceiling-abandoning Levenshtein, self-contained (depends only on lib/slug.ts)
lib/font-match.test.ts — new; 7 tests, half of them asserting what must NOT match
lib/font-index.ts — the resolver closure falls through to matchSlug on a miss
lib/compose-params.ts — noteNearMatch reports substitutions; `strict` added to KNOWN_PARAMS
public/agent.md — one-shot block up top, verify stance rewritten, API round-trips reframed, near-miss added to 'Nothing here fails'
app/page.tsx — quickstart: guessing-is-safe covers slugs, APIs demoted to 'only when you need data you can't recall'
public/llms.txt — compose entry: nothing to check
app/api/pairings/route.ts — dropped the no-op `kind` param
docs/agent-story.md — friction #7, the round-trip tax, and the generalizable lesson
Per-card palettes: three distinct looks in one /compose URL
4.1New `themes=` param (plural): a ;-separated list of palettes, one per card, so a single URL can show three cards with three distinct color looks — the reaction-loop the product is built around ('love the red one, warm up the first'). Each item uses the exact same grammar as `theme` (a curated index or named bg:/fg:/accent: roles). One item applies to all cards; N map by card index; fewer-than-cards cycle; an unusable item falls back to a curated palette with a note. `themes` wins over `theme`/`mood`.
4.2The render layer already indexed a themes array per card (ComposeCard, ComposeColorKey which de-dupes distinct palettes, and buildHandoff), so this was a parser-only change — parseOneTheme was extracted as the shared unit behind both single `theme=` and each `themes=` item.
4.3Contract clarity, no behavior change: agent.md now states the 'two facets per element' model plainly (title/subtitle/paragraph each have a text facet as a top-level param and a color facet as a role inside theme=/themes= — not a collision), reframes #agent-notes fetch-back as optional (sandboxed single-fetch agents can't do it and don't need to), and explicitly blesses guess-and-degrade (an unknown param is ignored and the page still renders, so guessing beats dropping intent).
4.4The root quickstart gains a 'three distinct looks' worked example (a comic-book birthday party in three palettes) and a themes= reference entry.
4.5Added docs/agent-story.md — the real cold-agent 'three birthday-invite looks' run that drove this work, kept as the rationale behind the surface.
Fill-in template: the agent edits a finished URL, the human just says 'give me ideas'
5.1The root quickstart now leads with a single fully-formed /compose URL — every parameter filled with a real, working value. The instruction to the agent is just 'change the values to match your user, delete what you don't need, hand it over.' Editing a finished URL is far more reliable for a weak agent than composing one from grammar, and it's the floor: even a swap-the-copy-and-colors pass yields a working page.
5.2The human never touches a URL. They point at the domain and say 'give me ideas'; the finished template lives in the response for the agent to adapt.
5.3The ceiling, stated right after the floor: don't reuse the default fonts — query /api/fonts?feeling=calm (20 feelings) for candidates and /api/pairings?font=<slug> for a display partner, each carrying a ready-to-paste composeValue, then drop it into pairs=. So a capable agent researches real faces that fit the request; a limited one still succeeds by editing the template.
5.4The parameter list is demoted to reference ('only if you need to hand-tune a value') — it's no longer the primary path.
Key files
app/page.tsx — the AgentQuickstart now leads with the fully-formed fill-in URL and the /api enrichment note; grammar demoted to reference
The one-fetch fix: the agent contract now lives in the root response
6.1The bug: a coding agent that could fetch /agent.md composed links fine, but a plain chat agent given only the site URL failed cold — it saw the 'Agent Ready' badge, couldn't make the second hop to the grammar, and gave up. The badge promised a capability the first (and often only) reliable fetch didn't deliver.
6.2The fix: the whole construct-a-URL contract is now inlined into the home page's server-rendered HTML — the /compose grammar, the real param names (title/subtitle/paragraph, not body), the slug rule, a dark starting palette, a worked example URL, and an explicit 'hand it to the user, don't fetch it back, nothing here fails' instruction. One fetch of the bare domain is now enough; /agent.md and /llms.txt stay as enrichment for agents that can follow a link, never the critical path.
6.3Every route (not just the home page) now carries a short agent pointer in its markup, so whatever URL a user pastes, the agent learns the contract exists and where the full quickstart is.
6.4Both blocks are present in the DOM as real, text-extractable content but aria-hidden and visually clipped — silent to screen readers, invisible on screen, and surviving the HTML-to-text conversion most fetch tools apply. This replaces the old sr-only pointer, which was both incomplete (a prose 'go fetch /agent.md' with no grammar) and read aloud to screen-reader users who wanted fonts, not a css2 URL.
Key files
app/page.tsx — replaced the thin sr-only /agent.md pointer with a full, self-contained AgentQuickstart block inlined into the root response
app/layout.tsx — a site-wide, text-extractable agent pointer on every route
Agent surface V1: the site is now usable by agents, not just readable
7.1A cold agent can discover the whole contract in one fetch: /llms.txt indexes /agent.md, which carries the query surface, the /compose grammar, the palettes with hexes, and a worked example. Plus /robots.txt and a generated sitemap covering every pairing page.
7.2The JSON APIs got honest. Unknown query params are still ignored (a stale URL shouldn't strand a human on an empty page), but every response now returns an `ignored` array naming them and an `applied` object echoing the filters that actually ran — so a guessed param name no longer looks identical to a broken feature. `?strict=1` opts into a 400 instead.
7.3The feelings filter is documented and reachable as `feeling` as well as its original `tag`, and takes a `minWeight` threshold to narrow a tag to fonts that genuinely lean that way.
7.4Two new endpoints: /api/palettes (the 11 curated palettes with computed WCAG contrast ratios and the four moods) and /api/pairings (curated + algorithmic partners, each with a ready-to-paste composeValue).
7.5New /compose route: an agent writes a plain-query-param URL and gets a curated, server-rendered page — up to four pairing cards, a curated palette or bring-your-own hex, optional copy, and coarse dials for size, density, weight and measure. No valid URL produces an ugly page; every malformed one degrades and reports itself at #agent-notes rather than failing.
7.6Every composed page carries a paste-ready implementation block: the css2 link and @import, a Tailwind v4 @theme block, CSS custom properties, and per-face stacks, weights and variable-axis ranges — all derived from the same catalog metadata that drove the render.
7.7The composed page became a design surface rather than a report: no site nav or footer, no page header, and the render notes and implementation config moved into a visually-hidden block (still in the DOM as real text, so agents fetching the page still get them — HTML comments would have been stripped by the markdown conversion most fetch tools apply).
7.8Agents now control the whole viewport, not just the cards. A new `page` param sets the field behind them; omitted, it derives from the card theme, so a light composition gets a light page and a dark one a near-black. The `for` param finally has a home as the single small line above the cards.
7.9Per-element color overrides: `theme=title:…,subtitle:…,paragraph:…` each override one text element and fall back to the palette roles. Previously `muted` carried both the subtitle and the paragraph, so there was no way to color the deck without dragging the body copy along.
7.10A color key below the cards shows every specified color as a labeled swatch with its hex — including the page field. It exists for the person, not the agent: it gives them the vocabulary to revise with ("make A32B25 darker" rather than "the red's too bright").
7.11Cards are centered at any count and size themselves to it: one renders wide as the direction being refined, two or more share the width evenly, converging on the old fixed 22rem at four. The shared browse `Grid` used `auto-fill`, which left one or two cards stranded in the leftmost track.
7.12An 'Agent-ready' note on the Fonts page announces the capability to humans — the contract that powers it is machine-readable and therefore invisible from the UI.
Key files
lib/compose-params.ts — the /compose grammar: parse, validate, clamp, default, canonicalize, and collect degradation notes (pure, unit-tested)
lib/color.ts — hex parsing and WCAG luminance/contrast math, palette-agnostic
lib/card-themes.ts — MOODS (ordered subsets of CARD_THEMES) and completeTheme(), which derives and contrast-checks the roles an agent omits
lib/handoff.ts — the implementation block, built from catalog metadata via lib/css2-url.ts
lib/font-stack.ts — fontStack/fallbackFor split out of the "use client" font-loader so server components can call them
lib/api-params.ts — the ignored[] / ?strict=1 contract shared by the JSON routes
lib/font-index.ts — memoized slug→family lookup over the catalog
app/compose/page.tsx — the server-rendered compose route, notes block and specs block
app/components/ComposeCard.tsx — the one template; deliberately not SpecimenCard, so a viewer's saved voice/theme can't leak into a page composed for someone else
app/api/palettes/route.ts, app/api/pairings/route.ts — new endpoints
Visual category selector on Fonts, plus a grid breakpoint fix
8.1The Fonts category filter is now visual: a row of tiles above the cards, each showing a big 'Aa' in a font representative of that style (Playfair Display for Serif, Space Grotesk for Sans, Anton for Display, Space Mono for Mono, Pacifico for Script; All in the UI sans). The selected tile inverts to dark-on-light.
8.2The search box moved to the far right and gained a magnifying-glass icon; the old text pills are gone.
8.3Fixed a layout gap: between roughly 640 and 784px the grid showed a single narrow card stranded in the middle. It now stays a full-width single card until two cards genuinely fit.
8.4The Colors page now opens on the Swatches view by default.
Key files
app/components/CategoryTiles.tsx — the Aa specimen tiles (representative fonts in a single TILES constant)
app/components/BrowseView.tsx — search moved right + icon, pills replaced by CategoryTiles
app/components/ui/Grid.tsx — switch to 22rem columns at min-[49rem] (when two fit), not sm
app/components/ColorsView.tsx — default to the Swatches view
Pairing cards: paragraph support, always-on fonts, and pivot to a partner
9.1Pairing cards now include the paragraph too (in the text font) when you turn paragraph on in the gear — so the visibility setting reaches pairings, not just single fonts.
9.2A pairing always shows both of its fonts: the title and subtitle stay visible on pairing cards no matter your hide settings, since hiding one would hide half the pairing. Only the paragraph is optional there.
9.3Click the partner font's name in a pairing's footer to open its own pairings in a new tab — so you can branch off to explore it without losing your place. The font you're already viewing isn't a link; on the home and favorites cards both names link.
Key files
app/components/PairingCard.tsx — paragraph passthrough, pairing flag, partner-name links (new tab)
app/components/SpecimenCard.tsx — `pairing` forces title+subtitle visible; only paragraph follows visibility
app/components/PairingsView.tsx — passes the source font so only the partner links
app/components/SuggestedPairings.tsx + FavoritesView.tsx — paragraph override threaded through
Show/hide title, subtitle, and paragraph on cards from the gear
10.1The voice editor (the gear) now has an eyeball next to each element — title, subtitle, paragraph — to show or hide it on every card across the app.
10.2Cards now show just the title and subtitle by default; the longer paragraph is hidden until you turn it on (a cleaner, more compact default).
10.3At least one element always stays visible — the last one's eyeball is disabled so a card can never be blank.
10.4Your choice is remembered across visits.
Key files
app/components/VoiceProvider.tsx — global, persisted visibility state + constrained toggle
app/components/TypographicVoiceModal.tsx — per-element eyeball toggles in the gear
app/components/SpecimenCard.tsx — every card honors the visibility preference
Customizable page-chrome colors, and the card-theme toggle in the gear
12.1You can now recolor the whole app's chrome — the shell behind the cards, the nav and footer, and the active-control highlight. The Colors page has a swatch editor under Page chrome with a color picker per role and a Restore defaults button; changes apply site-wide instantly and persist.
12.2The Randomize / Use selected theme switch is now also in the gear menu (it has an app-wide effect, so it's reachable from anywhere). The gear and the Colors page share one setting and stay in sync; pick which theme on the Colors page.
12.3Page-chrome colors now flow through CSS variables, so a single edit repaints the entire shell at once. Defaults are unchanged — nothing looks different until you customize.
Key files
lib/card-themes.ts — PAGE_THEME/HIGHLIGHT now reference --page-* CSS vars; literals kept as *_DEFAULT + PAGE_CHROME_DEFAULTS for the editor
app/globals.css — --page-bg/-fg/-muted/-accent/-highlight defaults; body grounds on --page-bg
app/components/CardThemeProvider.tsx — per-role page-chrome overrides written to the document root and persisted
app/components/ColorsView.tsx — PageChromeEditor (color picker per role + Restore defaults)
app/components/TypographicVoiceModal.tsx — Card color (Randomize / Use selected theme) section in the gear
Colors page: preview, swatch view, and a pick-a-theme-for-everything setting
13.1Added a Colors page (in the nav) that shows every card color theme. It opens as a split panel like the pairings page: a sample font card on the left, the themes on the right — click a theme and it paints onto the sample.
13.2A Preview / Swatches toggle flips the theme cards between full sample cards and tight square-swatch strips for scanning the palette at a glance.
13.3You can now pick one theme and apply it to every card across the whole app. A Randomize / Use selected theme toggle under the sample card decides whether cards cycle through all themes (the default) or all use your chosen one.
13.4Your chosen theme now persists — it survives a refresh and is remembered across the app, instead of resetting each visit.
13.5The page also shows the fixed page-chrome colors (the shell behind the grid and the active-control highlight).
Split-panel pairings page and scroll memory on the way back
14.1Redesigned the per-font pairings page: the font you picked now sits on its own to the left as a full specimen, with its partners laid out in their own grid to the right — so the pairing relationship reads at a glance.
14.2The selected font stays put (sticky) as you scroll its partners, keeping the comparison anchored.
14.3Gave the pairings page room to breathe — it now spreads to the full width of the window so more partners show at once, instead of the four-across measure the rest of the app uses.
14.4Returning from a pairings page now drops you right back where you were on the Fonts grid — your search, category, how many fonts you'd loaded, and your scroll position are all remembered, instead of resetting to the top.
Key files
app/components/PairingsView.tsx — two-column split layout, sticky source card, scroll-to-top on open
app/pairings/[slug]/page.tsx — full-bleed page, back-caret gutter guard, scroll={false} on the back link
app/components/BrowseView.tsx — per-tab snapshot restores filters, loaded count, and scroll on return
app/components/FontSpecimenCard.tsx — Get Pairings link opts out of scroll-to-top so the browse position survives
Bookmarkable pairings, a Fonts-first nav, and backlog status
15.1Opening a font's pairings now changes the URL instead of a pop-up, so any pairing can be bookmarked and shared — every font has its own /pairings/{font} page.
15.2Renamed and reordered the nav: the font browser is now the home page under "Fonts", the old home page of suggested pairings moved under "Pairings", and Favorites is unchanged.
15.3Cleaned up the pairings page — one "Pairings for {Font}" heading lined up with the cards, all pairings shown as a single set, and a round back button tucked into the left margin opposite the voice gear.
15.4Gave the backlog an open/closed view: two pills toggle between active ideas and shipped ones, with closed items set apart in blue.
15.5Marked "Bookmarkable pairing routes" as shipped — the first closed backlog item.
Key files
app/pairings/[slug]/page.tsx — per-font pairings page (SSG, one per font)
app/pairings/page.tsx — the suggested-pairings showcase (was the home page)
app/page.tsx — root is now the Fonts browser; /explorer redirects here
app/components/PairingsView.tsx — shared pairings grid (replaced the modal)
lib/slug.ts — font-name <-> URL-slug, the single source of truth
app/backlog/BacklogList.tsx — Open/Closed pills and the closed (blue) card theme
Unified page header, neutral palette, and a footer
16.1Unified every view's header into one element — the title sits in the same place and style on Home, Explorer, Favorites, and Changelog; the eyebrow and descriptive sub-text were dropped for a cleaner read.
16.2Folded the Explorer search and category filters into that shared header, and removed the sort options — results default to most popular.
16.3Made page headers line up with the cards at every width, and the changelog header line up with its narrower card column.
16.4Repainted the whole site in a neutral near-black, with the nav blending into the page.
16.5Settled on a single amber highlight for active controls (filter pills, the voice gear), keeping the red as a small accent.
16.6Added a global footer with the license and a GitHub link — it sits below the content, or at the bottom of short pages.
16.7Gave the light voice pop-up bold labels and trimmed its heading.
Key files
app/components/ui/PageHeader.tsx — the one shared page header
app/components/ui/GridAlign.tsx — pins headers to the centered card block
app/components/Footer.tsx + app/layout.tsx — the global footer and sticky-footer layout
app/components/BrowseView.tsx — Explorer in the unified layout (filters in header, no sort)
lib/card-themes.ts — neutral PAGE_THEME background + the HIGHLIGHT accent
Global typographic voice, unified cards, and this changelog
17.1Made the typographic voice global: one editor pop-up, opened from a gear at the right of the nav, now drives Explorer specimens, Home and favorite pairings alike — replacing the two separate editors that had drifted apart.
17.2Reworked that editor into a light-mode pop-up with stacked, auto-growing fields, reachable from any page and any scroll depth.
17.3Unified every card surface (Explorer, Home, pairings, favorites) onto one card component and one grid — a single fluid column on small screens, then fixed-width cards instead of stretching.
17.4Added this changelog at /changelog — a card per dated entry — plus the /changes command and README notes for keeping it current.
Key files
app/components/VoiceProvider.tsx — global voice state + the single editor pop-up
app/components/SpecimenCard.tsx — the shared card; FontSpecimenCard / PairingCard wrap it
app/components/ChangelogCard.tsx + app/changelog/page.tsx — the changelog surface
app/globals.css — the theme-light inverted-surface convention