Audience research
Consideration, conversion, retention. Delivered as something your leadership can circulate.
Every default element, in one place, for all three properties. Switch property and the whole page reskins from the same markup. That is the test: if a component only works in one theme, it is not a component.
Canonical values, transcribed from the Brand Guide PDF, not from code.
Radius, settled: 8px in the app, 10px on marketing cards, full pill on buttons. One value per context.
The guide's own rule: “Avoid using orange with black/brown (Halloween effect).” That is why there is no darkened orange anywhere on this page.
Accents carry status meaning only, as dots and fills. #00CCBB is 2.03:1 on white and must never hold text. Do not darken it to make it legible — that produces a dark green, which is off brand. Text is navy.
Two reds because one cannot do both jobs. #FF0C0C is a fill; as text on paper it measures 3.6:1 and fails. #D00810 is the text red. On the ink sections the direction reverses and the red has to get lighter, not darker — darkening a red toward maroon is the same mistake as browning the orange.
No #0099FF anywhere. Using Social Fresh blue made the personal site read as an agency sub-page.
| Pair | Ratio | Verdict |
|---|---|---|
White on #FF6600 | 2.94 | Fails |
Navy #003A66 on canvas | 10.82 | AAA |
#0099FF on white | 3.00 | Large only |
#00CCBB on white | 2.03 | Fails |
#FF0C0C on #0A0A0A | 5.01 | AA |
White on #FF6600 is 2.94:1. Darkening the orange is banned, so the fix is structural rather than chromatic. Ruled 2026-08-28.
Small body copy set in white directly on orange. This is the 2.94:1 case and it is the one to avoid.
Headline size is fine
Anything smaller sits in a white box on the orange, with navy text inside. Same band, same brand, legible.
Measured, not estimated. Three link colors were failing WCAG AA in production and were corrected on 2026-08-31.
| Where | Was | Ratio | Now | Ratio |
|---|---|---|---|---|
| socialfresh.com blog body | #0099FF | 3.0:1 | #005599 | 7.6:1 |
| jasonkeath.com blog body | #2EA3F2 | 2.8:1 | #D00810 | 5.6:1 |
| jasonkeath.com on ink | #FF0C0C | 3.5:1 | #FF6B63 | 5.0:1 |
The rule: brand blue is structural. It belongs on rules, borders and underlines, never on body-size text — #0099FF is 3.0:1 on white and 2.8:1 on Seafoam, so it fails even the large-text floor on our own canvas. #005599 is already in the palette and clears 7.6:1, so the fix needs no new color. Keep brand blue on the underline and the link still reads as Social Fresh blue.
Note these links are 18px bold, which sits just under the 18.66px large-text threshold. The bar is 4.5:1, not 3.0:1. That is the trap: bold type reads as though it should qualify, and does not.
Body copy sets in the body face at 17px with a 1.6 line height and a measure around 38rem. Long enough to read, short enough not to lose the line.
Brand guide specifies Avenir Heavy, which is print only. Outfit is the approved web stand-in (Jason, 2026-08-28): getting close is good enough. Body is Lato.Lato throughout. No display face in the app.Archivo Black over Archivo.
Every pill is a pale brand tint with navy text and a solid brand dot. Draft is Orchid, Live is Poolside, Needs review is Primary light, Blocked is Dragonfruit, and the promotional pill is outlined in orange rather than filled.
Why it is built this way: navy on a pale tint clears 10:1 every time, so no status colour ever has to hold small text. That is what killed the old maroon, the greens and the brown-yellow. There is no dark green or dark teal anywhere in the system now; Poolside appears only as a dot or a pale tint. It also means orange can appear on a pill without breaking its own contrast rule.
Pika 500 by Dutchicon, already licensed and on disk at SOCIAL FRESH/LOGOs - SF/Pika Supreme Pack/. Filled shapes on a 64px grid, single fill, so they recolor with one find-and-replace. 502 available; a sample below.
Gaps found: the set has binoculars but no eyeglasses and no sunglasses. Anything missing has to be drawn to match the 64px filled grid.
Sortable headers, sticky head, hover row, right-aligned tabular numerals. Click a header to sort.
| Event▲▼ | Status▲▼ | Tickets▲▼ | Revenue▲▼ |
|---|---|---|---|
| Social Fresh 2027 | On sale | 17 | $17,452 |
| Social Fresh 2026 | Closed | 228 | $223,598 |
| Workshops 2027 | Draft | 0 | $0 |
| FOFM 2025 | Closed | 62 | $41,300 |
The Airtable pattern: a typeahead at the top, grouped options, a colour swatch per option, the selected state shown by weight and background rather than a tick, and a footer that tells you what happens next. Click to open.
Three rules this pattern encodes: search appears once a list passes about seven items; groups are labelled rather than separated by a bare rule; and a destructive or notifying consequence is stated in the menu, not discovered after.
Toggles use position and fill. No tick, anywhere in the system.
Consideration, conversion, retention. Delivered as something your leadership can circulate.
Where you sit against the brands you are measured against, and the openings they left.
We run the account, not a report about the account.
Selected tab carries the accent underline. Never more than six.
When someone registers they will appear here with their ticket type and order date.
Confirmation is a word, not a tick. Errors say what went wrong and what to do.
Skeletons mirror the shape of what is coming, so the layout does not jump when data lands.
Two shapes only. A modal for a decision, a drawer for a record. Never a modal inside a modal.
This removes Social Fresh 2027 and its 17 registrations. Attendees are not notified automatically.
dana@example.com
Ordered March 2, 2027
The destructive modal asks you to type the name. Confirmation friction scales with how hard the action is to undo.
Showing 1 to 25 of 287. Always say the range and the total; a page number alone tells nobody how much is left.
The vocabulary the app actually uses, read from prisma/schema.prisma. These are the states every screen has to render, and there are more of them than a generic five-pill set can carry.
Left column is live in the app today. Right column is the same state on brand tokens.
| State | Live today | On brand |
|---|---|---|
REGISTERED | Registered | Registered |
CHECKED_IN | Checked in | Checked in |
WAITLISTED | Waitlisted | Waitlisted |
TRANSFERRED | Transferred | Transferred |
REFUNDED | Refunded | Refunded |
PARTIAL_REFUND | Partial refund | Partial refund |
PENDING | Pending | Pending |
CANCELLED | Cancelled | Cancelled |
The two hard ones. Refunded and Partial refund are different states that must not look alike, and there is only one pink. Solution above: filled for full, outlined for partial. Pending and Cancelled are both grey; Cancelled carries reduced opacity because it is terminal.
Comp is Orchid rather than orange: it is a $0 ticket, not a discount, and it needs to be visually separable when scanning a discount list.
Deliberately not the same object as a status pill. A session type is a category, not a state: it never changes, nothing is ever "wrong", and a schedule shows twenty of them at once. So they are squared, lighter weight, and carry a leading rule instead of a dot.
Each content type gets its own brand colour so a day's schedule reads as a pattern of formats. The two non-content blocks (Break, Reception) drop the rule entirely and go grey, so the gaps in a schedule are visible without reading a word.
Ticket types carry features (TicketFeature), which is the one place a tick is traditionally used. It is not used here.
Included is the default; excluded is marked. Included features are plain, excluded ones are struck and muted. No ticks, no crosses. A feature list where every row carries a tick is a list where the ticks carry no information.
EventPriceStage and TicketStagePrice are the early-bird ladder. The buyer sees one number and a deadline; the admin sees the whole ladder.
Decisions already made in PricingCountdown.tsx: the buyer-facing stage label wins over the internal admin label; an empty public label falls back to the generic “Current pricing expires in”; and the component renders hidden until the client mounts, because the server does not know now and the first tick would otherwise flash a wrong time.
Read off the real widget on socialfresh.com/conference/social-media-conference-2027/, from the rendered DOM. This is what buyers see today.
rgba(255,102,0,0.08) fill with a 2px #FF6600 border on three sides. Reads instantly without shouting. This is the right way to use orange as emphasis at scale.rgba(0,153,255,.1) on rgba(0,153,255,.55). Quiet, which is right for an absence.| Element | Live now | Change to |
|---|---|---|
| Included marker | 22px orange circle, white check drawn with stroke-width:3 and stroke-linecap:round |
Square the linecap. Round caps are exactly the soft tick you rejected. Same circle, same orange, geometric glyph. |
| “Most Popular” pill | White on #FF6600 at 10px / weight 700 |
2.94:1 at 10px, the worst instance of the orange rule on the site. Make it white fill, 1.5px orange border, navy text — it sits inside an orange-tinted column already, so it does not lose emphasis. |
| “Early Bird” tag | #0099FF at 10px / weight 700 |
Blue on white is 3.00:1, fine for large text and not for a 10px label. Navy. Blue is structural, not a text colour. |
The live page loads the embed from https://sf-events-seven.vercel.app/embed/widget.js, a Vercel preview hostname, not socialfresh.app. It works, but the ticket module on the revenue page depends on a preview URL. Worth pointing at the real host.
Already in globals.css and I missed it on the first pass. .stamp-card, .stamp-canvas and .perforation are a real design language for anything that represents a ticket or a registration.
--color-border-perf.Use this for tickets, registrations, order confirmations and check-in screens. Do not use it for generic content cards.
The most decision-dense component in the suite. ui/date-picker.tsx is 575 lines and every one of these rules was learned from a bug.
body at fixed coordinates. Absolute positioning gets clipped by any overflow-x:auto ancestor — the pricing schedule table is one — and fights z-index inside modals.The time picker is a separate component (admin/time-picker.tsx), not a native time input, for the same reason.
A native <input type="date"> fires change on every intermediate valid value, so stepping the month emits a complete date combining the new month with whatever day was already set. Any onChange-to-save handler then commits a date the user never picked. Its stepper and arrows are OS chrome and cannot be styled.
YYYY-MM-DD strings, matching Prisma's @db.Date, and never parsed into a local Date first. That is the UTC-midnight bug.A long page needs rhythm, and rhythm comes from alternating grounds, not from rules. The grounds each property allows, and when to use them.
Four grounds and one accent, not six. There is no blue and no orange here — the whole system is the graphite ramp plus a single red, and that is the point of it.
The shapes a marketing page is built from. Where more than one works, both are shown with the rule for choosing.
For 8 or more. Faces at equal size, name and company under each. Four up on desktop, two on mobile. Order is not a ranking, so do not feature one.
For under 8, or when one name sells the event. One large portrait, the rest as a styled list with role and company. Never a plain bulleted list of names.
Default. Time column left at a fixed width, title right. Day headers are the only rules. Breaks drop to grey so gaps read without reading.
Only when there are genuinely parallel tracks. A single-track event in a grid reads as an empty column, which looks like a mistake.
The workhorse. Three parallel things of equal weight: services, benefits, ticket types. Never four on desktop; four reads as a grid of filler.
Four things that pair. Use when the items have longer copy: a 2×2 gives each twice the width of a 4-up and stays readable.
Copy left, image or module right. The default for a hero and for any section where one thing explains another. Alternate the side down the page.
One statement, centred, on a heavy ground. A full stop between two dense sections. Never more than two per page.
Default for the app. Card on the canvas ground, logo above, one column, single primary action. Nothing else on the screen, no nav.
For checkout and registration, where the buyer needs reassurance mid-transaction. Form left, order summary or event detail right, pinned.
Status still reads better as a dot plus a word. A check is optional, never the default affordance.
Fires change on every intermediate value, so stepping the month commits a date nobody picked.
Popover calendar. Browsing never commits.
Darkened or browned orange. Orange beside black or brown. Maroon, and any off-brand red. Dark green or dark teal of any kind, including darkened Poolside such as #008378 or #007A72. Emoji anywhere, ever, icons only. Em-dashes in Jason's voice. Auto-advancing carousels. Unsourced statistics. toISOString() for a local date.
All seven answered 2026-08-28. Recorded in ~/.claude/CLAUDE.md so they apply to every session.
| Question | Ruling |
|---|---|
| Avenir | Not used on web. Outfit is the approved stand-in; getting close is good enough. |
| Emoji | Never. Icons only on web. This supersedes the Emoji Bank page in the Brand Guide, which is outdated. |
| Fortune 500 | 350 is right. The guide's 300 is stale. |
| Primary light | #FAD4BB confirmed. |
| Orange contrast | Orange stays #FF6600. White on it for headline size only; smaller text goes in a white box on the orange. Never darken toward brown. |
| Radius | 8px app, 10px marketing cards, pill buttons. |
| Button radius | Pill everywhere, app included. 8px stays on blocks, cards and inputs. The button must not change shape when a customer crosses from the marketing site into checkout. (Jason, 2026-09-20.) |
| Status labels | All four set in navy #003A66 with a solid brand dot. #D62B81 was a darkened Dragonfruit carrying label text and is gone. (Jason, 2026-09-20.) |
| Darkened Orchid | #9258DB removed — declared, never used, and a darkened brand colour. A fifth status, if ever needed, gets a pale tint, a navy label and an undarkened dot. (Jason, 2026-09-20.) |
| Seafoam | The proposed rename of #F7F6F4 to Salt was left pending on 2026-08-30 and is now closed: it stays Seafoam, matching the Brand Guide. (Jason, 2026-09-20.) |