Loading…
Your account isn't recognized as an admin. If you're an admin on Telegram or Discord, run /link there and enter the code on your account page to connect it to this sign-in -- or ask an existing admin to grant access on the User Management page.
Admin▸Design System
Design System
The rendered counterpart to docs/design-system.md. Every swatch, specimen, and ratio below is generated from the same token values the document specifies, so this page and that file cannot drift apart. Contrast ratios are computed at build time.
These tokens are not yet adopted in the sitewide stylesheet — they are declared locally by this page so the specimens render. The surrounding admin console still uses the old values, which is why this page looks different from the chrome around it. Sequence and outstanding decisions live in docs/design-system-adoption.md.
The rules that get broken most
- Never write a raw color, spacing, radius, or duration value. Use a token.
- Components reference semantic tokens, never primitives.
- Only two font weights exist: 400 and 700. 500 and 600 are browser-synthesized fakes.
- --brand-purple is a fill color only. It cannot be read as text on any of our surfaces.
- Surfaces step down in luminance as they recede. Elevation comes from a border.
- --surface-overlay is not a text surface. Hover fills and dividers only.
- If it repeats, it is a primitive. A hand-styled button in page markup is a defect.
- :focus-visible only, never bare :focus, and never outline:none without a replacement.
- Never transition all. Animate only transform, opacity, filter, and color.
- Any /dashboard/admin/* link needs data-astro-reload.
Surfaces
Surfaces step down in luminance as they recede. Elevation is a 1px border plus, only for things that genuinely float, a shadow — never a lighter fill. On a dark ground a shadow alone is nearly invisible, so borders carry the work.
--surface-sunken#212523Page background, wells, table stripes, the admin rail--surface-base#2e3330Default panel / card fill--surface-raised#3a3f3aPopovers, menus, the surface a card sits on--surface-overlay#464b46Hover fills and dividers only — never a text surfaceThe logo
Four assets in public/media/logos/, all a fixed 7:1 lockup. The logo is an asset, never type — setting “SHUTTERPAWS” as text to stand in for it means a substitute quietly becomes the brand in every screenshot and mockup.
shutterpaws_logo.svgTwo-color (default) · What Layout.astro ships todayshutterpaws-logo-2-white.svgWhite · Dark and busy groundsshutterpaws_workmark_white.svgWordmark, white · When the paw mark is already nearbyBlack or white grounds only
The lockup’s two halves want opposite grounds, so a mid-tone always compromises one of them. Which variant to use follows from the ground: two-color on white, white variant on black. Ratios computed at build time from the logo art’s own values (#6a0d83 / #fb9062), which are not the brand tokens — see below.
logo purple (PAWS)Shutterpaws2.00FAILShutterpaws10.51AAlogo orange (SHUTTER)Shutterpaws9.27AAShutterpaws2.27FAILwhite variantShutterpaws21.00AAShutterpaws1.00FAILLiveThis is shipping today. Layout.astro puts the two-color lockup on a #141614 header, where the purple “PAWS” half measures 1.73:1 against 8.03:1 for the orange half — so half the wordmark is effectively invisible, with no CSS compensating. The white variant on that same header is 18.19:1 and already exists in the repo.
The logo art does not use the brand tokens. Its purple is #6a0d83 against the token’s #692a80 — a perceptual delta of ΔEOK 0.041, roughly twice a just-noticeable difference, so a --brand-purple fill beside the logo is a visibly duller purple. The orange is within tolerance (0.009). Someone needs to pick which is authoritative; until then, do not place a --brand-purple surface directly against the logo.
Color primitives
Generated in OKLCH at fixed lightness steps, chroma clipped to sRGB. Reference these only through the semantic tokens below. Brand purple is a fill-only color — at 1.15:1 against the page it cannot be read as text or an icon stroke, which is arithmetic rather than preference.
Orangeseed #ff6600 · oklch(0.696 0.204 43.5)
Purpleseed #692a80 · oklch(0.414 0.147 -44.2)
Neutralhue-matched to the surface family, not pure gray
Contrast, proven
Every cell is real text in the real color on the real surface, labeled with its computed ratio. AA clears 4.5:1 and is safe for body text; UI clears 3:1 and is valid only for large text, icons, and borders.
--text-primaryBody copy15.51AABody copy12.86AABody copy10.76AA--text-secondaryBody copy9.50AABody copy7.87AABody copy6.58AA--text-mutedCaption text7.26AACaption text6.02AACaption text5.03AA--text-faintMetadata5.40AAMetadata4.48UIMetadata3.74UI--text-linkReserve a spot6.58AAReserve a spot5.45AAReserve a spot4.56AA--text-link-hoverReserve a spot7.97AAReserve a spot6.61AAReserve a spot5.53AA--purple-70Category tag5.46AACategory tag4.53AACategory tag3.79UI--status-successPaid10.10AAPaid8.37AAPaid7.00AA--status-warningPending8.76AAPending7.27AAPending6.08AA--status-dangerNo-show7.69AANo-show6.38AANo-show5.33AATypography
Fluid clamp() interpolation between a 360px and a 1240px viewport — ratio 1.2 at the small pole, 1.25 at the large one. Resize the window and the whole scale moves. The two down-steps are floored at 12px and 14px; the pure ratio wanted 11.1px, which is below a readable floor.
--text-4xl39.8 → 54.9pxHero / wordmark
Fursuiters and photographers
--text-3xl33.2 → 43.9pxh1
Upcoming events
--text-2xl27.6 → 35.2pxh2
NERF Battle Takeover
--text-xl23 → 28.1pxh3
Cooling & Comfort
--text-lg19.2 → 22.5pxLead, h4
Themed photoshoots for fursuiters and photographers, about once a month.
--text-md16 → 18pxBody default
We host high-quality themed photoshoots across the greater D.C. and Baltimore region, handling crowd flow, on-site safety, hydration and cooling so attendees can focus on making great photos.
--text-sm14 → 15pxCaptions, admin tables
Doors at 12:00. Changing room open until 16:00.
--text-xs12 → 13pxLegal, dense metadata
© Shutterpaws 2026. All Rights Reserved.
Only two weights exist
astro.config.mjs registers Avenir Regular and Bold, nothing else. The middle two cards below ask for weights the font does not contain, so the browser fakes them — differently per platform. To de-emphasize, use a smaller size or --text-secondary.
400 · real500 · synthesized600 · synthesized700 · real--font-brand (Library 3 AM, for the wordmark only) has no specimen here because it is not registered in astro.config.mjs yet — rendering it would silently fall back and mislead.
Iconography
One SVG sprite (IconSprite.astro), inlined once per page and referenced with <use href="#icon-name">. These are the real shipped symbols. Never inline a one-off <svg> in a page or island — it escapes every rule below and cannot be audited.
UI icons — 24×24 grid, inherit currentColor
icon-calendaricon-closeicon-copyicon-downloadicon-linkicon-ios-shareSize tokens
--icon-sm16px · Inline with --text-sm--icon-md20px · Default, inline with body--icon-lg24px · Standalone, inside buttons--icon-xl32px · Feature and empty-stateBrand marks are not UI icons
icon-blueskyicon-facebookicon-instagramicon-telegramicon-twittericon-rssPlatform logos are trademarks. They keep their own geometry, may not be redrawn to fit the 24px grid, and may not be recolored beyond monochrome. They belong under an icon-brand-* prefix so a sweep over UI icons cannot accidentally restyle one. An icon-only control needs its own aria-label; the glyph itself stays aria-hidden, and the control gets a 44×44 target regardless of glyph size.
Spacing & radius
A 4px base unit, eight steps. Every margin, padding, and gap comes from it.
--space-14px--space-28px--space-312px--space-416px--space-524px--space-632px--space-748px--space-864px--space-gutter16–32--space-section32–64--radius-smBadges, inputs, tags--radius-mdDefault — buttons, cards, panels--radius-lgModals, large media--radius-xlHero blocks--radius-pillPills, toggles, avatar rowsNesting rule: an inner radius is the outer radius minus the padding between them, so concentric corners stay concentric.
Motion
Hover or focus a card to run it. Reduced-motion preferences disable all of it.
--duration-fast · 150msHover, focus, color and opacity swaps. The default.--duration-base · 200msSmall transforms, disclosure, dropdowns.--duration-slow · 300msEntrances, modals, page transitions. The ceiling.--ease-emphasizedExpressive, rare. Reach for it with a reason.Data visualization
Six categorical series colors, used in order — a two-series chart is --chart-1 and --chart-2, not whichever two look nice. Each clears 3:1 against every text surface, the threshold for non-text UI.
--chart-11 · orange--chart-22 · purple--chart-33 · teal--chart-44 · blue--chart-55 · gold--chart-66 · roseBecause the six sit at near-equal lightness, the palette gives strong hue separation on a dark ground but does not survive grayscale — which is why labeling directly, rather than via a legend, is a rule and not a preference. Status colors stay reserved for status; using --status-success as a series color makes a neutral category read as a judgment. Platform brand colors are valid only when the series is that platform. Magnitude uses the orange ramp, not these.
Interaction & feedback states
Every interactive control specifies all six states below. A control missing a disabled or loading treatment is incomplete, not minimal. A loading control must not change size, or the layout jumps and the pointer lands on something else.
DefaultThe variant's base stylingHover--accent-hover fill, or an --accent-subtle washActiveA visible change distinct from hoverFocusThe single focus ring. Never suppressed.Disabledopacity .45, not-allowed, aria-disabledLoadingIn-flight, blocked, size unchangedFour view states, all designed
A view that can load forever but cannot fail is a defect. A permanent “Loading…” with no path to an error state is the most common serious bug in this codebase's history — the adminApi timeout, AdminErrorBoundary, and showAdminLoadError rules in CLAUDE.md each exist because it shipped.
Twelve of sixteen slots taken.
The content.No events scheduled yet.
Says what would be here, and offers the action.Couldn't load the roster.
What failed, plus a way to recover.Shared primitives
Anything appearing more than twice is a primitive in site/src/components/ui/, not a pattern re-styled per page. Utilities are for layout and genuine one-offs. The controls below are specimens of the target styling, not the real components — but they are live: hover them, and tab through them to see the focus treatment from section 6.1. A reference whose buttons do nothing cannot demonstrate the state rules it specifies.
ErrorThat session is full. Join the waitlist and we will email you if a slot opens.
SavedYour spot is reserved. Check your email for the confirmation.
- Button
- Every button, link-button, and CTA
- Card
- Event cards, press cards, admin panels
- Badge
- Status pills, tier labels, tags
- Field
- Label + input + error + hint
- Panel
- Bordered content panel with an optional head
- Stack
- The flex-column-plus-gap triplet
- Alert
- Error / success / info callouts
- Spinner
- Loading indicators
Mockups
Tokens judged in isolation always look fine. See the homepage and event page rebuilt on this system, so the composition can be judged as a page.
View the page mockups →Full reference with rationale: docs/design-system.md. Adoption sequence and open decisions: docs/design-system-adoption.md.