Portfolio · design exploration · v2 · 2026-04-12
Royal + Violet
token block & component spec
v1 picked direction: Royal + Violet (option 4B), with the gradient border move from 4C preserved as an option for sparing use. This doc closes three decisions before the tokens land in globals.css.
Decision 1 — the full token block for violet (does the scale feel right; what semantic aliases does it earn). Decision 2 — where the royal-to-violet gradient is allowed to appear, since "sparingly" needs a rule. Decision 3 — how Claude and Dylan get distinct identities inside the chat transcript component.
01 · token block
Violet only lives in steps 8 to 12
A full 12-step violet scale would double the palette for little gain. Backgrounds, borders, and chrome are already fully solved by Royal Tonal; violet only needs to show up where it carries meaning (brand signal, kickers, links, on-brand text). Five steps, each with a role.
Royal Tonal (chrome) · unchanged
Violet (signal) · NEW
--violet-8
brand · thinking#8C3BDB
--violet-10
link · thinking#B47CFF
--violet-11
kicker · thinking#C89BFF
Signature gradient · NEW
--grad-rv · 135deg · royal-8 → violet-8
Why 5 violet steps, not 12
- Chrome (backgrounds, borders, cards) is a solved problem with royal. Violet doesn't need its own dark-end scale.
- Semantic split: violet is content signal, not surface. It appears on text, icons, borders, kickers — never as a fill for navigation, cards, or page surfaces.
- Fewer steps means fewer wrong choices. A developer writing a component has one violet text colour (
violet-11) and one violet brand colour (violet-8), not twelve slightly-different purples to pick between.
- The 5-step scale can grow later if a legitimate need shows up (e.g. chunk 4d illustrations wanting a violet tint background). Not before.
02 · role split
What royal does, what violet does
Meaning per hue keeps the family coherent. Without this rule, violet becomes "second accent" (decorative) and the cohesion collapses.
Page chrome
Backgrounds, cards, borders, hover states
—
Primary action
CTAs, "Get in touch", form submit, main nav
—
Content · work
Case study cards, project kickers, work log entries
—
Content · thinking
—
Writing posts, essay kickers, transcript "Claude" role, thinking-mode callouts
Signature moments
Royal → Violet gradient (border, divider, hero underline) — see section 3 for placement rules
Body text
Always ink (#E6E2D9) — neither hue touches body prose
—
Tension to watch
- Royal must stay dominant. Target split is ~85% royal / 15% violet across a given page. Violet present but not competing.
- A case study about a writing post can use violet accents; a writing post about a case study stays neutral. The hue follows the subject, not the container.
- Don't use violet for status (success / error / warning). That pattern is off-limits for the portfolio; it reads as category.
03 · gradient placement · pick one (or a combination)
Where the Royal → Violet gradient is allowed to appear
You liked the gradient border move from v1's 4C but asked for sparing use. "Sparing" needs a concrete rule or it drifts. Four options below, ordered from most reserved to least. They're not mutually exclusive — A could pair with C, for example — but the more surfaces that carry the gradient, the less signature it becomes.
Option A · most reserved
Hero signature only
Gradient appears exactly once per page: as the hero CTA background and/or a 3px underline on a single hero keyword. Everywhere else, royal and violet stay separated. The gradient becomes a literal signature: "this is where the page begins".
— Product thinker · shipping with AI
I design product decisions, then ship them with Claude Code.
Get in touch →
Rule
- Gradient = hero CTA + hero keyword underline. Nothing else.
- Cards, dividers, transcripts all use solid royal or solid violet — never mixed.
- Easiest to maintain, lowest design risk, lowest visual variety.
Option B · targeted emphasis
Featured case study card
The hero signature (option A), plus: exactly one case study card per landing page gets a gradient border. Used to draw the eye to the strongest or newest case study. All other cards stay royal-bordered.
FEATURED
Selected work · 01
Planner App
GTD planner with persistent SMS reminders. Migrated Python/Railway to TypeScript/Cloudflare with single-choke-point spend protection.
CLOUDFLARE
TYPESCRIPT
D1
Read case study →
Rule
- At most one featured card per page. The pin label "FEATURED" earns the treatment; otherwise the emphasis looks arbitrary.
- Rotate which case study is featured as priorities change — the treatment is navigational, not permanent.
- Hover state on non-featured cards stays royal only.
Option C · structural
Section dividers between modes
Gradient appears as a thin horizontal line separating the "work" zone (case studies, royal) from the "thinking" zone (writing, violet). A structural marker rather than a decorative one. Hero signature still applies on top.
Selected work
Case study cards go here, royal-styled.
Writing
Essay cards go here, violet-styled.
Rule
- Gradient divider = mode transition. Never a cosmetic separator.
- Max one gradient divider per page (landing); writing index and project index stay single-mode so no divider needed.
- Pairs naturally with option A — the hero is the opening signature, the divider is the midpoint signature.
Option D · routine use
Every cross-mode card
Any card that represents a hybrid artefact (a case study that embeds a writing excerpt; a writing post about a shipped feature) gets a gradient left border. Signals "this belongs to both modes". Used several times per page but always tied to a specific meaning.
Essay · about planner app
Three guardrails that saved the spend budget
Shipping a paid-API product without a single "oh god what have I done" moment. A writing post about work.
ARCHITECTURE
POSTMORTEM
Read the essay →
Rule
- Gradient = "this artefact lives in both modes". Triggered by content, not decoration.
- Least signature, most reusable. Risk: if ~30%+ of cards are "hybrid", the treatment stops reading as special.
- Only viable if cross-mode content is actually rare (less than ~1 in 5 cards per page).
Picking between them
- Default recommendation: A + C (hero signature + structural divider). Two fixed appearance points per landing page, total. Maximally sparing; the gradient is never "decorative", only "punctuation".
- Add B only if the landing page needs a visual anchor stronger than the hero already provides. Unlikely until chunk 4c page layout is final.
- Avoid D unless hybrid content becomes a recurring pattern in chunk 4d writing posts. Too broad to start.
04 · chat transcript component · pick one identity treatment
Giving Claude and Dylan distinct voices in transcripts
Transcripts are verbatim (no edits), 2 to 8 turns, embedded in case studies under Process and Lessons. The role of the design is to make who's speaking scannable at a glance, without introducing chat-UI patterns that fight the surrounding prose. Four treatments below, from colour-only to full mix.
Colour assignment is fixed across all four: Dylan = royal (work/craft, dominant brand), Claude = violet (thinking, sibling brand). This matches the section 2 role split. What varies is how loudly the identity is drawn.
Option A · colour only on role label
Simplest. Role label is coloured; bodies are identical in font and weight. Quietest treatment, best when the transcript sits near other coloured callouts.
Transcript · architecture planning
2026-04-10 · 4 turns
Dylan
The spend module needs idempotency or the whole thing is a liability. How do I make "send twice" structurally impossible rather than probabilistically unlikely?
Claude
Insert the row before calling Twilio. Put a 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.
Dylan
Does this need to be in a transaction?
Claude
No. The constraint does the work. A transaction would actually hurt — it holds the row lock across the Twilio call, which might take hundreds of milliseconds.
Option B · colour + left border strip
Adds a 2px coloured bar down the left of each turn. More scannable; you can tell who's speaking from the corner of your eye. Slightly more "chat UI" in feel.
Transcript · architecture planning
2026-04-10 · 4 turns
Dylan
The spend module needs idempotency or the whole thing is a liability. How do I make "send twice" structurally impossible rather than probabilistically unlikely?
Claude
Insert the row before calling Twilio. Put a 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.
Dylan
Does this need to be in a transaction?
Claude
No. The constraint does the work. A transaction would actually hurt — it holds the row lock across the Twilio call, which might take hundreds of milliseconds.
Option C · colour + typography split
Dylan = Geist sans (human voice); Claude = Geist Mono (AI voice). Typography alone does most of the identity work, colour reinforces it. Reads as "natural language in, structured output out" — which is what Claude's replies often look like anyway. This is the most distinctive and the most "opinionated".
Transcript · architecture planning
2026-04-10 · 4 turns
Dylan
The spend module needs idempotency or the whole thing is a liability. How do I make "send twice" structurally impossible rather than probabilistically unlikely?
Claude
Insert the row before calling Twilio. Put a 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.
Dylan
Does this need to be in a transaction?
Claude
No. The constraint does the work. A transaction would actually hurt — it holds the row lock across the Twilio call, which might take hundreds of milliseconds.
Option D · full mix (gradient container + border + typography)
The container itself gets a royal-to-violet gradient border, signalling "this is a conversation between the two brands". Inside, each turn uses the colour + typography split from option C. Heaviest treatment; best if transcripts are rare and signature rather than frequent.
Transcript · architecture planning
2026-04-10 · 4 turns
Dylan
The spend module needs idempotency or the whole thing is a liability. How do I make "send twice" structurally impossible rather than probabilistically unlikely?
Claude
Insert the row before calling Twilio. Put a 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.
Dylan
Does this need to be in a transaction?
Claude
No. The constraint does the work. A transaction would actually hurt — it holds the row lock across the Twilio call, which might take hundreds of milliseconds.
Picking between them
- Leaning recommendation: Option C (colour + typography split). The mono/sans split does double duty — it carries identity AND it correctly anticipates the visual character of Claude's output (which is often structured / code-containing / list-based). The difference reads instantly without needing border treatments that would echo the gradient card rules.
- Option D is the "signature" version; use only if transcripts are genuinely rare (one per case study or fewer). If transcripts show up two or three times in a single case study, D will overwhelm the page.
- Option A is the "quiet" version. Preferable only if the surrounding prose already has too many coloured callouts (which isn't currently the case).
- Don't pick B. It's a middle ground that does nothing C doesn't do more distinctively.
05 · summary of decisions needed
What to lock before tokens land in globals.css
Decision 1 (tokens): confirm the 5-step violet scale (violet-8 to violet-12) plus the --grad-rv signature. Default to yes unless the hex values look wrong in this preview. These become :root variables in globals.css and semantic aliases (--thinking, --thinking-kicker, etc.) get added alongside.
Decision 2 (gradient placement): recommendation is A + C — hero signature (CTA + keyword underline) plus structural divider between work and thinking zones. Two appearance points per landing page, total. Defer B and D until later chunks prove a need.
Decision 3 (transcript identity): recommendation is Option C — colour + typography split. Dylan in Geist sans, Claude in Geist Mono, role labels coloured. Reads instantly, anticipates the shape of Claude's output, and doesn't require extra container chrome.
If all three defaults are accepted: next action is to extend design.tokens.ts (workspace design system) with the violet scale, regenerate the CSS variable block, drop it into portfolio/src/styles/globals.css, and update the existing Royal Tonal design-explorer to v2 so future palette decisions have the full family to compare against. The transcript component (<ChatTranscript>) is still gated on chunk 4c.1 per the plan — this spec feeds it, doesn't build it.