v5 actions the critique from v4. Applied: hero keyword underline removed (focal point cleanup), card tags proximity fix (24px gap), arrow inlined with title (whole title clickable), meta row tags dropped, ticker title aligned to 96px date column, tag pill padding bumped to 3px, .btn-thinking dropped, card hover treatment added, kicker rhythm rule documented.
Deferred: writing card differentiation — separate deep-dive session will cover card design properly, including whether cards should carry imagery. See v5 changelog at the bottom.
Exactly one hero per page. Uses the signature gradient in one place — the CTA button. v5: keyword underline removed; the gradient CTA is the sole focal accent so the eye lands once, not ping-pongs.
All button labels are 18px minimum (post-audit fix) and use #FFFFFF rather than --royal-12 / --violet-12. Ghost button is the low-emphasis variant for secondary actions.
.btn-thinking dropped. Writing-register CTAs use inline "Read the essay →" links (see 03b) — a solid violet button duplicated what the link + ghost button already cover.In the planner app, I learned to put paid API calls behind a single choke point so that budget caps and killswitches can't be bypassed. The full writeup is in the essay on defense in depth.
Three card variants. Case study (royal) is the default. Featured is case study + gradient border, at most one per landing. Writing card uses a violet left rail to signal the thinking register. v5: title + inline arrow is the clickable target (continuity fix); tags sit 24px below the body (proximity fix); hover state lifts the card via background + inset highlight (figure/ground). Deferred: separate writing-card deep-dive (differentiation + imagery) scheduled next.
Meal-kit POC pulling Coles recipes into a consolidated trolley. Static React/TS/Vite/Tailwind, Netlify deploy.
GTD planner with persistent SMS reminders. Migrated Python/Railway to TypeScript/Cloudflare with single-choke-point spend protection.
Shipping a paid-API product without a single "oh god what have I done" moment. Defense in depth, with real numbers.
The idempotency layer turned out to be the most interesting one. The naive approach is to check "have I sent this already" before calling Twilio, but that leaves a race window: two concurrent invocations both see no prior send, both insert the log after, both call Twilio.
The fix is to invert the check: insert a row representing the intent to send first, with a UNIQUE constraint on (recipient, occurrence_id, date), then call Twilio.
The four artefact types that live inside case study bodies. All share a figure caption, padding system, and break out of the 62ch text column.
UNIQUE constraint on (recipient, occurrence_id, date) and let the database reject the second write. On vendor failure after the insert, delete the row or mark it failed; never leave it in limbo.Each entry names a principle, diagnoses where the library succeeds or falls short against it, and gives a concrete action. Verdict tags: FIX = change before shipping; WATCH = monitor in context; KEEP = working as intended.
In the case study card, the kicker ("Selected work · 01") has only 12px of space below it before the title. The kicker acts as a label for the title, so tight proximity is correct, but the tags sit 12px below the body paragraph as well, meaning tags and body are proximity-grouped with the title instead of reading as separate metadata. This collapses the card into one visual group instead of three (label, content, metadata).
Action: increase the gap above .tags to 20px (space-5), keep kicker→title at 12px. Creates three clear proximity groups: label / content / metadata.
The writing card is distinguished only by a 3px left border and a violet kicker. In the 3-card grid, the three cards look almost identical at a glance; the writing card barely registers as a different kind of artefact. Similarity is working too hard — the cards look so alike that the meaningful difference (work vs thinking) disappears.
Action: widen the left rail on writing cards to 4px, and consider adding a small violet dot or label prefix ("ESSAY ·") that's typographically distinct from "Selected work ·". The kicker itself should feel different, not just be a different colour.
FIXThe accessibility audit flagged that --card vs --bg is 1.20:1 — technically exempt from WCAG, but users with low vision will perceive the card as "a rectangle floating on a slightly different void". The border saves it, but the border is also low contrast (1.94:1). Together they work; either alone would fail to separate figure from ground.
Action: add a subtle inner shadow (or brighter top edge highlight) on card hover so the card lifts off the page on interaction. Static cards rely on border + fill together, which is OK but borderline. Do not remove either.
WATCHThe kicker uses --royal-11 #A5B4FF at 9.14:1 contrast; the body uses --ink-dim on card at 5.16:1. The small, decorative element is more luminous than the primary content. Classic hierarchy says bright = important; here, bright = label. This is defensible (the kicker is a wayfinding device) but unusual. It works because the kicker is physically small and uses small-caps; the eye reads it as an annotation, not a headline.
Action: keep as-is for now, but watch in user testing. If readers report confusion about what to read first, swap card body text to --ink (6.18+:1) on the card variants so body is brighter than kicker. For now, keep.
In the card layout, the arrow sits at the bottom below tags, separated from the title it references by body text and tag pills. The eye path from title → arrow breaks across three intervening elements. The arrow is also the same size and weight as the body text; nothing draws the eye back to it after reading the body.
Action: either (a) attach the arrow directly to the card title so the whole title is clickable and has a trailing arrow inline, or (b) pull the arrow out as a distinct "read more" strip at the card foot with its own visual treatment (slightly indented, accent colour, maybe a subtle left divider line). Option (a) is simpler and more web-native.
FIXThe gradient border on the transcript creates a strong closure — the conversation feels contained, discrete, set apart from the surrounding prose. This is exactly right: transcripts are verbatim quotes and should feel bounded rather than mixed into the author voice. Closure is doing identity work here, not just decoration.
Action: keep. The gradient border is not decorative; it carries the closure principle. Confirm that the transcript always renders with the gradient border applied even in long-form case studies where other figures use plain frames.
KEEPThe spacing scale (--s1 to --s9) is well-defined, but I'm not always hitting it. The hero uses 48px between kicker and title; the card uses 12px between kicker and title. That ratio difference is correct (the hero is bigger) but the kicker-to-title gap as a fraction of the title size drifts between components. This is a rhythm crack.
Action: define a rule: kicker-to-title gap is always equal to the kicker's line-height × 1.5. Apply this everywhere a kicker precedes a serif display. Documents a relationship rather than a fixed pixel value.
FIXThe date column is 96px fixed, which creates a clean left-edge alignment for all dates and a consistent action column. However, the actions wrap and their left edge (column 2 start) does not align with anything else in the library — not the ticker title, not the live dot, not the bottom-padding edge. The ticker has a local alignment grid that doesn't connect to the surrounding layout.
Action: align the ticker title and live dot to the 96px date-column edge, so the vertical stroke runs title → date column → action column all sharing a left edge. Pulls the ticker into a single unified rhythm.
FIXTag pills are 11px mono with 2px × 8px padding. Under a 17px serif title and a 15px body paragraph, they read as the smallest element in the card — which is correct, they're tertiary. But the vertical padding (2px) is tight enough that descenders in lowercase mono characters almost touch the bottom border. Reads as "type trapped in a box".
Action: bump vertical padding on .tag from 2px to 3px. Trivial change, visible improvement in the "breathing" of the pill.
Royal, violet, and the off-white ink are all cool. The palette has no warm relief — every surface, accent, and text colour sits in the blue/purple/cool-grey range. This is intentional (monochromatic discipline) but it means long reading sessions can feel "chilled" or clinical. There's nothing to anchor warmth, which is part of what makes a portfolio feel human.
Action: this is a deliberate trade-off, not a fix. The off-white #E6E2D9 is slightly warm (hinted toward cream), which is what's preventing the palette from reading as "sterile". Keep ink as-is, and resist the temptation to add a warm status colour just for relief. If reader warmth becomes a concern, address it through the writing voice rather than the colour.
"Published · Reading · Tags" is three distinct data points across the meta row. Published date is load-bearing (trust + freshness); reading time is nice-to-have; tags are redundant with the kicker and the card tags. Three equal-weight segments means none of them gets emphasis — the row reads as a strip of grey metadata rather than useful orientation.
Action: either (a) drop the tags from the meta row (the kicker and inline tags already do that job), or (b) weight the three segments so published date is brighter (--ink) and the others dim to --ink-dim. Option (a) is cleaner.
The hero currently has four elements competing for first attention: the kicker, the large serif title, the gradient keyword underline within the title, and the gradient CTA button. The underline and the CTA both carry the gradient signature, so they visually echo each other — this is good (repetition creates pattern) but it fragments the focal moment. The eye doesn't land; it ping-pongs.
Action: pick one. Either (a) keep the gradient CTA and drop the keyword underline (CTA is the commitment; word highlight is decoration), or (b) keep the keyword underline and make the CTA solid royal (the title becomes the signature, the CTA becomes quiet). Option (b) is more sophisticated: it lets the title carry meaning and makes the CTA functional-not-decorative. Recommend (b).
FIXFour button variants (primary, thinking, ghost, gradient signature) on display. In practice, the page only needs three: primary royal (everyday CTA), ghost (secondary action), and gradient signature (the one hero CTA). The thinking-violet button is tempting but it duplicates what a ghost button + a "Read the essay →" link already do. Adding it as a full button variant increases the decision surface without adding clarity.
Action: drop .btn-thinking from the component library. Writing pages use inline "Read the essay →" links and the gradient hero CTA. If a writing-specific full-button CTA ever appears, promote ghost + violet border as the pattern rather than a solid violet fill — violet solids compete with the identity of royal as "the" primary brand.
--ink. Unused .grad-underline class deleted..card .tags gets margin-top: var(--s3) — stacks on the 12px flex gap to produce a 24px body→tags gap. Three proximity groups: label, content, metadata..arrow div removed. Title is now <a class="card-link"> with an inline <span class="arrow-inline">. Whole title is the clickable target; arrow nudges 3px right on card hover..ticker-head to a 96px/1fr grid matching the entries. Read as weird in practice (live dot floated in empty column; title started mid-row). Reverted to flex layout. Revisit if/when the ticker moves from 2-line entries to a dense table-style render.line-height: 1; gap below kicker = kicker line-height × 1.5 ≈ 16px. Applied via card h3 margin-top: var(--s1) on top of 12px flex gap, and hero .hero-kicker margin-bottom: var(--s4)..tag vertical padding 2px → 3px. Descenders no longer graze the border..btn-thinking (9A): CSS rules and HTML demo removed. Three button variants now: primary, ghost, signature. Writing CTAs use inline "Read the essay →" links..card:hover now lifts via --card-hover background + inner top-edge highlight + --royal-8 border. Static figure/ground stays borderline; interaction pulls the card cleanly off the page.Writing card differentiation (item 4) postponed to a separate card-focused session. Open questions for that session:
The v4 critique section above remains intact as the historical diagnosis — it documents which principles caught what. The changelog here documents the response.