Camp location card draft
The location-level card for a venue that hosts multiple camps — the unit the browse page lists. Shares the camp card's surface, media header and metadata rows, but summarises a venue rather than a session: the camp count sits as quiet secondary text under the title, pricing is an optional 'from' comparison, and the action is View details. Session-level dates, ages and inventory stay on the camp card.
Source: specs/camp-location-card/spec.json · specs/camp-location-card/notes.md
Anatomy
root- The card surface: bg-primary fill, 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 location.
media- The 16:9 venue 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. Falls back to a clearly-marked image placeholder when no photo is published — never stock or mock photography.
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. Falls back to a clearly-marked logo placeholder when the partner has no artwork.
content- The padded body stack below the media — 24px inline padding, 20px block padding, 12px between groups.
title- The location name — text-lg bold, the card's only heading.
camp-summary- The quiet secondary line beneath the title: a count and the words 'upcoming camps'. Tertiary text, deliberately NOT a status pill. In State=No upcoming camps the count is hidden and the line reads 'No upcoming camps'.
tags- Descriptive tags for the camp formats offered at this venue, revealed by Show tags. Note this card has NO overflow mechanism: unlike the camp card it carries no Tags/Overflow count and no Show tag overflow boolean (verified against the v5 snapshot), and its description is silent on what happens when the tags exceed the row. Treat that as unspecified rather than as licence to wrap — see notes.md.
essential-details- Location, season and club rows, each a 24x24 Beestera icon (location, calendar, cleat) beside text-xs metadata.
price- Optional Early bird pricing: a 'From' eyebrow with an Early bird badge, then a struck-through regular price followed by the emphasised current from-price and its unit. Hidden by default and in State=No upcoming camps.
early-bird-badge- The promotional Early bird pill in the price eyebrow — brand-toned, never a success/green tone.
action- The View details button: a brand-gradient pill that fills the row's width when no price is shown. Retained in every state, including No upcoming camps.
Token bindings
Each binding's value is resolved live from the built
@beestera/tokens output — tier-2 component tokens
(--camp-location-card-*) collapse through the foundation tier.
| Part | Property | Token | Resolved | Applies to |
|---|---|---|---|---|
root | background | --bg-primary | #ffffff --bg-primary → --color-base-white | — |
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 | — |
title | color | --text-primary | #181818 --text-primary → --color-neutral-900 | — |
title | font-size | --font-size-text-lg | 1.125rem | — |
camp-summary | color | --text-tertiary | #52525b --text-tertiary → --color-neutral-600 | — |
camp-summary | font-size | --font-size-text-xs | 0.75rem | — |
camp-summary | gap | --spacing-xs | 0.25rem --spacing-xs → --spacing-scale-1 | — |
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 | — |
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:regular-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 | 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
-
stateDefaultHoverFocusedNo upcoming camps - The Figma State axis. Default/Hover/Focused are the card details link's interaction states. 'No upcoming camps' is the single source of truth for a venue with no published future sessions: the location and all of its details stay accessible, the camp-summary line goes quiet ('No upcoming camps'), pricing is hidden, and View details is retained. Never model it as a separate overlay or boolean. There is deliberately no Disabled state — a location stays discoverable even with no future sessions. Neither card system has one.
State matrix
default- Resting card. The whole surface links to the location-detail page; View details repeats that destination as an explicit control.
hover- The card details link lifts to the medium elevation (--shadow-md).
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 the link to the location-detail page (a real <a>, or a card-wide link overlay) — decided 2026-08-06. View details points at the same destination and is therefore NOT a second control: render it as a visual affordance inside the card link (aria-hidden, or a plain <span> styled as a button), never as its own <a>/<button>. Exactly one tab stop per card.
- Contrast
- The camp-summary line is text-tertiary on bg-primary (>= 4.5:1). The Early bird badge keeps brand-900 on brand-50. Nothing on this card conveys status through colour alone.
- Focus order
- One tab stop per card — the card link itself. View details is inside it and takes no focus of its own, so a list of venues tabs one card at a time.
- Target size
- View details is 44px tall in the Figma reference and must stay >= 44px, even though it is not separately focusable — it is still a pointer target within the card link.
- 'No upcoming camps' must be readable text, not a colour or icon cue — it replaces the count in the same summary line.
- The venue photo is decorative — the title carries the location name, so the image takes an empty alt.
- The partner logo is decorative whenever the club name is already in the essential-details rows.
- This card has no tag-overflow indicator, so if the implementation adds one it must not become a focusable control — the card is already the only tab stop.
Usage
Do
- Use this card on the browse page, where each entry is a venue that may host many camps. The location-detail page uses the camp card instead.
- Keep the camp count as quiet secondary text beneath the title.
- Use State=No upcoming camps when a venue has no published future sessions, and keep View details reachable.
- Keep the tag row to one line. The camp card's overflow pattern is NOT part of this component in Figma, so if this card needs one, raise it with the designer rather than importing the camp card's silently.
- Pair the struck-through regular price with the emphasised current from-price when Early bird pricing applies.
Don't
- Don't promote the camp count into a status pill — it is not a badge.
- Don't put exact ages, session dates or inventory counts on this card; those belong on the camp card or the details screen.
- Don't add a separate 'no camps' overlay or boolean alongside the state axis.
- Don't make View details its own <a>/<button> — the card is the link, and a second control would be a duplicate tab stop to the same destination on every card in the list.
- Don't mask or crop partner logos — the 48x48 viewport is deliberately unmasked around 40x40 Fit artwork.
- Don't let a tag wrap to a second line, truncate mid-word, or extend past the card edge.
- Don't assume this card has the camp card's Tags/Overflow count — it doesn't, and inventing one here would put a component in the build that the design file has never drawn.
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 location card