Post-audit, post-decisions. Royal + Violet Signal is locked. Accessibility fixes from the WCAG 2.1 AA audit are applied (ink-faint restricted to decorative, button labels use #FFFFFF, transcript role labels mandatory). This doc mocks every reusable component the portfolio will need, then critiques the library through gestalt and other visual principles, with a prioritised fix list.
Structure: foundations → content blocks → interactive → cards → artefacts → structural. Each component has a label, one-line purpose, a rendered demo, and a "uses" footnote naming the tokens it consumes. Critique section starts after the library.
Exactly one hero per page. Uses the signature gradient in one place (CTA button). Optional keyword underline in --grad-rv-soft for a stronger signature move.
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.
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.
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.
.btn-thinking: three button variants, not four.Nothing in the critique undermines the palette decisions or the overall structure. These are component-level fit-and-finish items that become much cheaper to fix now (before globals.css gets written) than after.