Anatomy

root
The card surface: an emphasis-dependent fill (bg-primary for Standard, bg-brand-primary for Residential), a card hairline, the generic card radius, content clipped. Fluid between 280px and 460px wide (360px is the Figma reference width).
featured-border
A 4px brand rule across the very top, shown only for a featured camp.
media
The 16:9 camp photo (360x202.5 at the reference width — an exact ratio, not the rounded 360x203 the frame previously used), cover-cropped, with a centered 70% subject-safe area. When no photo is published it falls back to a clearly-marked image placeholder on bg-secondary — a marked placeholder, never stock or mock photography. The photo is retained in every state, including Sold out.
club-logo
A 48x48 radius-xl viewport pinned 16px from the media's bottom-right, holding 40x40 partner artwork scaled to Fit — never Crop, so irregular crests and wordmarks are not clipped. Unmasked on purpose. Falls back to a clearly-marked logo placeholder when the club has no artwork.
content
The padded body stack below the media — 24px inline padding, 20px block padding, 12px between groups.
calendar-date
The 54x54 Data display/Calendar date tile in Style=Beestera: brand-yellow outer stroke, brand month header over a brand-tinted day body, no internal divider. Month is a three-letter uppercase abbreviation.
title
The camp name — text-lg bold, the card's only heading.
availability-badge
The pill stating whether registration is possible: Success 'Registration Open' normally, Error 'Sold out' when State=Sold out.
remaining-spots
The free-text inventory count beside the badge ('Only 8 left'), set at text-xs semibold. Its colour encodes urgency; it is omitted entirely when inventory is healthy, and when the camp is sold out. In State=Sold out the availability area holds the 63x22 'Sold out' badge and nothing else — no empty box is reserved where the count would have been.
tags
Descriptive tags for the camp — one camp-format tag (Overnight, Extended Day or Day Camp) plus seasonal and geographic metadata. A single-line, fill-width row with clipping protection: it never wraps and never extends past the card. At the 360px reference width it shows two priority tags plus the overflow count.
tag-overflow
The Tags/Overflow count component — a compact, tokenized '+N' indicator closing the tag row, revealed by Show tag overflow. Informational, not an interactive control: the card itself is the link, so this must never become a nested button.
feature-callout
The Cards/Camp feature callout — a GK marker plus a one-line label, revealed by Show goalkeeper training. More prominent than a tag, secondary to price and registration.
essential-details
Location, dates/ages and club rows, each a 24x24 Beestera icon (location, calendar, cleat) beside text-xs metadata.
price
A 'From' eyebrow with an optional Early bird badge, then an optional struck-through previous price followed by the current price and its unit.
early-bird-badge
The promotional Early bird pill in the price eyebrow — brand-toned, never a success/green tone.
action
The Register button: a brand-gradient pill on the shared Button's Primary treatment, the explicit primary action. In State=Sold out it renders the shared Button's Disabled treatment — a bg-quaternary fill with text-quaternary label, no gradient — and accepts neither pointer nor keyboard activation.

Token bindings

Each binding's value is resolved live from the built @beestera/tokens output — tier-2 component tokens (--camp-card-*) collapse through the foundation tier.

Part Property Token Resolved Applies to
root background --bg-primary #ffffff --bg-primary → --color-base-white campTypeEmphasis:Standard
root background --bg-brand-primary #ffffff --bg-brand-primary → --color-base-white campTypeEmphasis:Residential
root border-color --card-border #e4e4e7 --card-border → --color-border
root border-radius --card-radius 0.75rem --card-radius → --radius-card
root box-shadow --shadow-md 0px 2px 4px -2px #0000000f, 0px 0px 10px -1px #0000001a state:Hover
root box-shadow --focus-ring #3d3d3d --focus-ring → --color-neutral-700 state:Focused
featured-border background --bg-brand-solid #181818 --bg-brand-solid → --color-neutral-900
media background --bg-secondary #fafafa --bg-secondary → --color-neutral-50
club-logo background --bg-primary #ffffff --bg-primary → --color-base-white
club-logo border-color --card-border #e4e4e7 --card-border → --color-border
club-logo border-radius --radius-xl 0.75rem
content padding-inline --spacing-3xl 1.5rem --spacing-3xl → --spacing-scale-6
content padding-block --spacing-2xl 1.25rem --spacing-2xl → --spacing-scale-5
content gap --spacing-lg 0.75rem --spacing-lg → --spacing-scale-3
calendar-date border-color --fg-brand-primary #181818 --fg-brand-primary → --color-neutral-900
calendar-date border-radius --radius-lg 0.875rem
calendar-date background --bg-brand-solid #181818 --bg-brand-solid → --color-neutral-900 slot:month-header
calendar-date background --bg-brand-primary #ffffff --bg-brand-primary → --color-base-white slot:day-body,campTypeEmphasis:Standard
calendar-date background --bg-primary #ffffff --bg-primary → --color-base-white slot:day-body,campTypeEmphasis:Residential
title color --text-primary #181818 --text-primary → --color-neutral-900
title font-size --font-size-text-lg 1.125rem
availability-badge border-radius --radius-full 9999px
availability-badge background --utility-green-50 #f0fdf4 --utility-green-50 → --color-green-50 availability:Registration open
availability-badge border-color --utility-green-200 #bbf7d0 --utility-green-200 → --color-green-200 availability:Registration open
availability-badge color --utility-green-700 #15803d --utility-green-700 → --color-green-700 availability:Registration open
availability-badge background --utility-red-50 #fef2f2 --utility-red-50 → --color-red-50 state:Sold out
availability-badge border-color --utility-red-200 #fecaca --utility-red-200 → --color-red-200 state:Sold out
availability-badge color --utility-red-700 #b91c1c --utility-red-700 → --color-red-700 state:Sold out
remaining-spots font-size --font-size-text-xs 0.75rem
remaining-spots font-weight --font-weight-semibold 600
remaining-spots color --text-secondary #3d3d3d --text-secondary → --color-neutral-700 availability:Healthy
remaining-spots color --text-warning-primary #ca8a04 --text-warning-primary → --color-yellow-600 availability:Running low
remaining-spots color --text-error-primary #dc2626 --text-error-primary → --color-red-600 availability:Nearly gone
tags background --bg-primary #ffffff --bg-primary → --color-base-white
tags border-color --border-primary #d4d4d8 --border-primary → --color-neutral-300
tags border-radius --radius-sm 0.375rem
tags color --text-secondary #3d3d3d --text-secondary → --color-neutral-700
tags font-size --font-size-text-xs 0.75rem
tags gap --spacing-sm 0.375rem --spacing-sm → --spacing-scale-1-5
tag-overflow background --bg-primary #ffffff --bg-primary → --color-base-white
tag-overflow border-color --border-primary #d4d4d8 --border-primary → --color-neutral-300
tag-overflow border-radius --radius-sm 0.375rem
tag-overflow color --text-secondary #3d3d3d --text-secondary → --color-neutral-700
tag-overflow font-size --font-size-text-xs 0.75rem
feature-callout background --utility-brand-50 #fffbeb --utility-brand-50 → --color-brand-25
feature-callout border-radius --radius-md 0.625rem
feature-callout color --text-primary #181818 --text-primary → --color-neutral-900
feature-callout font-size --font-size-text-sm 0.875rem
feature-callout background --bg-brand-solid #181818 --bg-brand-solid → --color-neutral-900 slot:gk-marker
essential-details color --text-secondary #3d3d3d --text-secondary → --color-neutral-700
essential-details font-size --font-size-text-xs 0.75rem
essential-details gap --spacing-sm 0.375rem --spacing-sm → --spacing-scale-1-5
price color --text-tertiary #52525b --text-tertiary → --color-neutral-600 slot:eyebrow
price color --text-tertiary #52525b --text-tertiary → --color-neutral-600 slot:previous-price
price color --text-primary #181818 --text-primary → --color-neutral-900 slot:current-price
price font-size --font-size-text-xl 1.25rem slot:current-price
price color --text-secondary #3d3d3d --text-secondary → --color-neutral-700 slot:price-unit
early-bird-badge background --utility-brand-50 #fffbeb --utility-brand-50 → --color-brand-25
early-bird-badge border-color --utility-brand-900 #c27c00 --utility-brand-900 → --color-brand-800
early-bird-badge color --utility-brand-900 #c27c00 --utility-brand-900 → --color-brand-800
early-bird-badge border-radius --radius-full 9999px
action background --gradient-brand-500-600 linear-gradient(180deg, var(--color-brand-500) 0%, var(--color-brand-600) 100%)
action color --text-primary #181818 --text-primary → --color-neutral-900
action background --button-disabled-bg #d4d4d8 --button-disabled-bg → --bg-quaternary → --color-neutral-300 state:Sold out
action color --button-disabled-fg #71717a --button-disabled-fg → --text-quaternary → --color-neutral-500 state:Sold out
action font-size --font-size-text-md 1rem
action border-radius --radius-full 9999px
action box-shadow --button-shadow 0px 1px 2px 0px #0000000d, inset 0px -2px 0px 0px #0000000d, inset 0px 0px 0px 1px #0000002e --button-shadow → --shadow-xs-skeuomorphic

Variants

state DefaultHoverFocusedSold out
The Figma State axis. Default/Hover/Focused are the card details link's interaction states. Sold out is NOT an interaction state — it is the single source of truth for zero inventory: the Error 'Sold out' badge replaces the Success badge, the photo and every other detail stay fully readable, and Register renders the shared Button's Disabled treatment. Never model sold-out as a separate overlay or boolean alongside this axis. There is deliberately no card-level Disabled state: a finished camp leaves the active Browse Camps inventory rather than sitting in the list greyed out.
campTypeEmphasis StandardResidential
Standard is the day-camp presentation on a bg-primary (white) surface. Residential identifies the broader residential program family and applies its branded treatment: the card surface becomes bg-brand-primary, and the calendar-date tile inverts its day-body to bg-primary so the tile stays legible against the tinted card. Structure, spacing and every other token are shared with Standard. Residential is an emphasis, NOT a schedule-format tag — see the tags part for the format vocabulary.
showTagOverflow truefalse
The Figma boolean revealing the Tags/Overflow count component at the end of the tag row. True whenever the camp has more tags than fit the row at the current width; the count itself is an editable Count property (+1, +2, +3...) in Figma and a computed value in code.
availability Registration openHealthyRunning lowNearly gone
Derived from live inventory, not chosen as a Figma variant property. 'Registration open' is the Success badge shown whenever the camp is not sold out. The other three tier the remaining-spots count: Healthy (>25% AND >10 spaces) stays neutral or omits the count entirely; Running low (11-25% OR 4-10 spaces) uses warning text; Nearly gone (<=10% OR 1-3 spaces) uses error text. When percentage and count disagree, take the more urgent of the two.

State matrix

default
Resting card. The whole surface is the camp-details link; Register sits inside it as a separate control.
hover
The card details link lifts to the medium elevation (--shadow-md). Hovering Register is the button's own concern.
focus
Keyboard focus on the card details link paints the shared focus ring — a 4px ring offset from the card by a 2px bg-primary halo.

Accessibility

Role
The root is a link to the camp-details page (a real <a>, or a card-wide link overlay). Register is a separate <button>/<a> that MUST NOT be nested inside the card link — two controls, not one.
Contrast
Badge labels sit on their own tint at >= 4.5:1 (green-700 on green-50, red-700 on red-50, brand-900 on brand-50). The availability urgency tiers are colour-only signals, so the count text must always state the number ('Only 3 left') — colour never carries the meaning alone.
Focus order
Card details link first, then Register. The Focused variant is the card link's focus, not the button's; each control keeps its own visible ring.
Target size
Register is 44px tall in the Figma reference and must stay >= 44px. The card link's own target is the full card, so it always clears the minimum.

Usage

Do

  • Use this card on the location-detail page, where each entry is one bookable camp session. The browse page uses the camp location card instead.
  • Drive Sold out from capacity being reached, not from a stored 'closed' status.
  • Keep the count text honest and specific ('Only 3 left'), and drop it entirely when inventory is healthy.
  • Compute the visible tags dynamically rather than hard-coding two: preserve the tag source order, prioritise camp-format tags ahead of seasonal and geographic metadata, reserve space for the '+N' indicator, show as many COMPLETE tags as fit, then set N = total tags - visible tags. Recalculate whenever the card width or the tag content changes.
  • Give a Residential camp exactly one schedule-format tag — Overnight (24-hour supervision and accommodation), Extended Day (9am-9pm) or Day Camp (9am-3pm).
  • Keep the previous price muted and struck through, and the current price bold and primary.

Don't

  • Don't add a separate sold-out boolean, overlay or scrim — State=Sold out is the only source of truth for zero inventory.
  • Don't colour the Early bird badge with a success/green tone; it is promotional Brand.
  • Don't nest the Register button inside the card's details link.
  • Don't mask or crop the club logo — the 48x48 viewport is deliberately unmasked around 40x40 Fit artwork.
  • Don't place 'Overnight' and 'Residential' beside each other as if they were sibling tag values: Residential is the program family (the Camp type emphasis), Overnight is one of its three schedule formats.
  • Don't let a tag wrap to a second line, truncate mid-word, or extend past the card edge — the row is single-line and clipped, and drops to one visible tag plus '+N' on narrow screens.
  • Don't put session-level inventory or exact ages on the location card — they belong here.

Implementations

Not implemented in any product repo yet. This repo owns the spec; implementations land in each product repo on their own cadence.

Figma source: Cards/Camp card