Three guardrails that saved the spend budget→
Shipping a paid-API product without a single "oh god" moment.
Deferred from v5 item 4. This doc answers the card-level questions that v5 couldn't cover without derailing the accessibility + critique pass: variant structure (one component or many) and imagery placement. Scope is the card component as a whole, across every surface it ships on — not just the writing variant.
Usage surfaces in scope: landing 2×2 case study grid, landing Writing zone, /writing index, /work index, and the related-work footer inside case study bodies. Density is locked to v5's moderate baseline (three proximity groups). Imagery is optional per Q2 — work cards use screenshots or diagram fragments, writing cards use big numerals or gradient washes.
Despite the work/writing register split and five usage surfaces, there's enough structural overlap to justify a single <Card> Astro component with a props API. The alternatives duplicate ~70% of the markup and CSS for differences that are mostly presentational. A prop-driven component lets register and imagery flex independently while keeping the valid combinations explicit. Featured becomes a boolean flag, not a third type.
// Single component; register + imagery + featured are orthogonal props interface Props { /** Register — drives kicker colour, left rail, arrow colour, tag styling */ register: "work" | "writing"; /** Featured — adds gradient border + "FEATURED" pin. At most one per page. */ featured?: boolean; /** Content — always required */ kicker: string; // "Selected work · 01" title: string; description: string; tags: string[]; href: string; /** Optional imagery — if omitted, card is typography-only */ image?: | { kind: "screenshot"; src: string; alt: string } // work | { kind: "diagram"; src: string; alt: string } // work | { kind: "numeral"; content: string } // writing ("01", "02"…) | { kind: "gradient-mark"; motif?: "rules" | "grid" }; // writing } // Valid combinations (enforced via narrowing, not brute validation): // register "work" + image.kind "screenshot" | "diagram" | undefined // register "writing" + image.kind "numeral" | "gradient-mark" | undefined
Why not two components? The work/writing split lives in three places only: kicker colour, left rail presence, arrow colour. Padding, proximity, title font, tag shape, hover behaviour are identical. Splitting into WorkCard/WritingCard just means two files drifting apart.
Why not modifier classes? Astro component boundaries are stronger than CSS class discipline. Props make "featured writing card with a numeral" either valid or invalid at the type layer; class-based modifiers make it look possible in CSS even if it's meaningless.
Each option shown twice: left card is work register with a screenshot mock; right card is writing register with a numeral or gradient wash. The mocks are CSS-drawn — no real assets — so the question is layout, not craft. Tradeoffs listed under each. Summary table and recommendation at the bottom.
The most familiar web pattern. 160px image strip at the top, full content block below. Every card with imagery reads as an editorial tile; cards without imagery read as plain text blocks. The tradeoff: mixing image and non-image cards in the same grid creates two visual species that don't sit together cleanly. Works if you commit to imagery grid-wide, fails if you want imagery to be truly optional.
Shipping a paid-API product without a single "oh god" moment.
150px image on the left, content filling the rest. Reads as a list row rather than a grid tile. Breaks at narrow widths: in a 2×2 landing grid each card is ~420px wide, and giving 150px to imagery leaves only 270px for content — the title wraps aggressively and tags break onto multiple rows. Works well in list contexts (/writing index, related-work footer) but fails in grid contexts.
Shipping a paid-API product without a single "oh god" moment.
Image fills the card; a bottom-up dark gradient scrim makes the content legible over it. Strongest visual impact, highest content risk: body copy contrast depends on where the image's content sits, so every card needs a carefully composed image with dark bottom space. Hover states are invisible (the image covers the background change). Breaks with abstract marks and numerals — those don't read as atmospheric backgrounds.
Shipping a paid-API product without a single "oh god" moment.
64×64 mark in the top-right corner, content takes the rest. The image is a tag rather than decoration — it identifies the project or essay at a glance without demanding visual weight. Key property: cards with and without the corner mark sit together cleanly in the same grid; the difference reads as "some cards have an icon" rather than "some cards are different animals". Lowest author cost: a 64px square asset for work, a two-character numeral for writing.
Shipping a paid-API product without a single "oh god" moment.
58/42 asymmetric split — content on the left, image on the right taking the full height of the card. The split is deliberately off-centre to feel editorial rather than utilitarian. Distinctive look, but has the same mixing problem as A: cards with and without imagery create two visual species. Also needs the card to have a minimum height, which loses the "natural height" property of the current card.
Shipping a paid-API product without a single "oh god" moment.
Image fills the top 140px of the card; content sits in a bordered inset that rises 28px into the image. Reads as a two-layer collage — image and content are clearly separate layers, not stacked blocks. Distinctive when it works, but requires precise image framing: the top third of every image is wasted behind the card's visible edge, and the bottom 28px sits behind the content inset.
Shipping a paid-API product without a single "oh god" moment.
Scored against the five things that actually matter for this portfolio: fit in the 2×2 landing grid, fit in list surfaces (/writing, related-work), author cost, content-first alignment (boring > clever), and tolerance for mixing image and non-image cards in the same group.
| Name | 2×2 grid | List mode | Author cost | Content-first | Mix tolerance | |
|---|---|---|---|---|---|---|
| A | Top third | Pass | Pass | High | Partial | Fail |
| B | Left half | Fail | Pass | Medium | Pass | Partial |
| C | Background | Pass | Fail | Very high | Fail | Fail |
| D | Corner accent | Pass | Pass | Low | Pass | Pass |
| E | Asymmetric half | Pass | Fail | High | Partial | Fail |
| F | Full-bleed overlap | Pass | Partial | High | Partial | Fail |
D is the only option that passes all five criteria. The other five each fail at least one — mostly mix tolerance, which matters because "optional imagery" (Q2=B) implies some cards will have imagery and some won't, in the same grid. If that's the answer, only a layout that tolerates mixing survives contact with reality.
The deeper reason D wins: it reframes imagery as a tag rather than a hero. A 64px corner mark identifies the card ("this is the planner app", "this is essay 01") without making a visual claim. Cards without the mark look like cards that haven't been given a mark yet, not cards from a different system. That's the exact property you want when the content itself is the point — which this portfolio's style guide says it is.
The tradeoff to be honest about: D's visual impact is low. If you want the landing grid to feel visually striking, D won't get you there on its own. Striking comes from the hero block, the gradient CTA, and the signature divider — the card grid is supposed to feel orderly, not loud. If you later decide the landing grid needs more weight, the upgrade path is to swap D for A with the rule "when used on the landing grid, all four cards must have imagery" (making A work despite its mix intolerance).
Second choice: A, but only on the landing grid specifically, and only with the all-or-nothing rule. I'd rather ship D everywhere than ship A with a mix of image and non-image cards.
Next step if you pick D: I'll mock the corner accent at higher fidelity — featured variant (with FEATURED pin + gradient border), loading state, imagery-absent fallback, and /writing index density. Then we wire it into the token spec and move to chunk 4b.
If you pick something else: tell me which one and I'll mock that at higher fidelity, same follow-ups.