DMV Arts Guide
Living Interaction Design Specification
DMV Arts Guide helps adults of color in the DMV find art and dance classes. The hardest part of starting a class is often walking into an unfamiliar room and wondering whether you belong. The guide is built to make that step easier.
Platform
Web
Stack
Next.js + Tailwind
Data
Static JSON, 49 studios
Updated
September 2026
System Specification
This living specification documents the interaction patterns, design constraints, and system decisions that govern how the product behaves.
It is the reference for design tokens, component patterns, and interaction rules used across the product.
Product Philosophy
The thinking behind the design decisions.
The core insight
A search bar assumes you already know what you want. Most people starting an art or dance class do not, and what they want to know is whether a studio is a place for them. So the guide leads with what someone is trying to do, and it names each studio's traditions the way the studio names them.
Design principles
Name the tradition
Generic labels hide the thing a person is looking for. The editorial rules ban "world dance," "ethnic dance," and "African dance" when the specific form is known, and ask for the form by name: West African, Bharatanatyam, Soca, Hand Dance.
Roots are claimed, never assigned
A cultural root is recorded only when a studio or its instructors claim it. Identity tags such as "Centers Black joy and community" and "LGBTQ+ affirming" follow the same rule and come only from a studio's own public materials.
Goals before search
The homepage opens on six goals rather than a search box. Search is still there for people who already know what they are looking for.
Only what can be confirmed
Every claim in a listing has to trace to the studio's website, class pages, registration page, instructor bios, or named press. A field nobody can confirm stays empty, and a studio is marked beginner-friendly only when that has been confirmed.
A context layer, not a directory
The guide sits just before someone signs up for a class. It is not a marketplace, a booking platform, or an events calendar, and it does not reduce a studio to location, time, category, and price.
The six goals
Rendered with the site's own classes and token values from HomepagePersonaCard.tsx. This is not a screenshot.
Start here
What are you looking for?
Language Standard
Canonical terms and phrasing, from docs/UX_PATTERNS.md and the editorial rules that govern every listing.
Studios to visitors, listings in the code
Internally the record is a listing: the route is /listings and the type is Listing. To a visitor it is always a studio. The nav says Studios, the results count says "10 studios," and the empty state says "No studios match your current filters."
Name the form
- West African, and the specific form (Senegalese, Ghanaian) when it is known
- Bharatanatyam, Kuchipudi, Odissi
- Caribbean dancehall, Soca, Afrobeat
- Merengue, Cumbia, On1 and On2 salsa
- Hand Dance
- Afro-diasporic
Never flatten it
"World dance""Ethnic dance""Global styles""Cultural movement""Traditional dance"without naming the tradition"African dance"when the specific form is known
Comparatives
Avoid
largest, best, top, only, one of the few, one of the biggest, most acclaimed, most established
Use instead
long-running, established, a major, one of the larger, not commonly found at regional studios
Claims nobody can check
Banned unless a named source supports them:
"highly regarded," "award-winning," "beloved by the community," "a DC institution," "known for its welcoming teachers," "one of the city's hidden gems"
Register
A listing reads like a museum program catalog or a city cultural guide: factual, neutral, concise, and culturally respectful. It does not read like a brochure, a Yelp review, a press release, or a travel blog. Sentences aim at 20 to 25 words, and there are no em dashes.
Also banned as filler: "welcoming environment," "creative journey," "vibrant community," "safe space," "supportive atmosphere," "passion for the craft."
Design Tokens
Every token rendered as a live specimen. Source of truth: tailwind.config.ts + src/lib/palette.ts. Contrast ratios are measured on white.
Colors
Accent and brand
accent #1A56C4
Focus and primary action, 6.62:1
accent-hover #1544a0
Hover on filled accent, 8.87:1
brand-accent #E8581F
Brand color: logo rule and hero underline
Neutrals
ink #111111, 18.88:1, headings and titles
charcoal #3F4A5A, 8.98:1, body copy and chips
eyebrow #595959, 7.00:1, eyebrow labels
body #666666, 5.74:1, card descriptions
muted #7A6F5D, 4.93:1, nav links and meta text
divider #EBEBEB, grid lines only
surface-sunk #FAFAFA, listing card hover
One switch, four palettes
ACTIVE_PALETTE = 'civic' injects one palette onto the page. Brand orange is the same in all four; only the accent changes.
| Palette | Accent | On white |
|---|---|---|
| Civic (active) | #1A56C4 | 6.62:1 |
| Gallery | #3E4A4F | 9.14:1 |
| Studio | #8E3B2F | 7.46:1 |
| Clay | #E8581F | 3.59:1 |
Typography
Families
Barlow Condensed: headings and card titles
Inter: body text, labels, and controls
Scale in use
The homepage headline sizes itself against its own column with clamp(36px, 13cqw, 72px), so the accent phrase keeps one underline per line at 320, 375, 768, and 1600.
Shape, spacing, and focus
Corners and shadows
Sharp edges, no shadows. Rounded shapes are reserved for bullet dots and the map's own internals.
Page width
max-w-5xl mx-auto px-6
Touch targets
44px minimum on every control
Focus
A 2px accent outline by default; grid cards draw a 1px accent ring inside their edge
The gap grid. Every card grid draws its lines from borders, never from a gap showing a background through:
grid border-l border-t border-divider
// each child
h-full border-r border-b border-divider bg-white
Motion
Cards
Background color only, 150ms. A card never scales on press.
Buttons
Visit website and the List toggle scale to 0.96 on press.
Map to list
A studio chosen on the map switches to the list, scrolls to its card, and highlights it for 1200ms.
Reduced motion
Every duration drops to 0.01ms.
Component Library
Live specimens rendered with the site's classes and token values.
Goal card
Source: src/components/HomepagePersonaCard.tsx · Every state is a background step, so each can be told apart from the others at a glance.
Default
01
I want to dance and meet people
Hover · accent at 6%
01
I want to dance and meet people
Press · accent at 12%
01
I want to dance and meet people
Keyboard focus · inset ring
01
I want to dance and meet people
min-h-[160px] border-r border-b border-divider bg-white px-7 py-7 hover:bg-accent/[0.06] active:bg-accent/[0.12] transition-colors duration-150 focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-accent
Listing card
Source: src/components/ListingCard.tsx · The only card the guide uses. Its footer depends on showBestFor and on whether a studio is confirmed beginner-friendly.
Dance
DC
Salsa With Silvia
Beginner salsa and bachata taught in a social, partner-based setting, no experience needed.
Studio Arts
DC
Winter Palace Studio
Georgetown and Bethesda studio offering regular figure-drawing and portrait sessions with live models, open-format, by appointment.
Dance
DC
Baila DC
A structured 6-week beginner series for On2 salsa, progressive levels with partner rotation built in.
Good for
Adults new to salsa who want a structured, progressive course that leads directly into local Latin social dance events.
Left: confirmed beginner-friendly. Middle: confirmed not, so the footer shows the level. Right: the homepage variant with showBestFor, shown in its hover state.
Studio page parts
Source: src/app/listings/[id]/page.tsx, OutboundStudioLink.tsx
Discipline tags
Beginner callout · only when beginnerFriendly === true
New to this?
This studio welcomes beginners. No prior experience required.
Visit website · the accent-filled conversion action
Visit websitebg-accent text-white hover:bg-accent-hover active:scale-[0.96] · opens in a new tab
Search, view, and area
Source: src/components/FilteredListings.tsx, FilterBar.tsx · When a goal is active, only the area filter shows.
10 studios
Clear filtersEmpty state
Source: src/components/EmptyState.tsx · The ink-outline button is the recovery tier.
No studios match your current filters. Try removing one to see more options.
Clear all filtersNewsletter signup
Source: src/components/EmailSignup.tsx · Posts to Formspree.
Monthly
What's new in the DMV arts scene
New studios, seasonal classes, and finds worth knowing about across DC, Maryland, and Northern Virginia. One email per month.
Unsubscribe anytime.
Success · role="status"
You're on the list. See you next month.
Error · role="alert"
We couldn't add you to the list. Please try again, or email hello@articulatedigital.co and we'll add you.
While sending: the button disables and reads "Sending," so a second click cannot send a second request.
Interactive Prototype
A clickable phone frame using the site's classes and all 49 public studios, in the order the site shows them. Choose a goal, open a studio, then follow a similar studio from there.
Home
Interaction Patterns
Recurring patterns across the site, from docs/UX_PATTERNS.md and docs/state-matrices.md.
Goal card states
The card's states are planned in a state matrix. tests/homepage-primary-action.spec.ts asserts that rest, hover, and press are three different backgrounds.
| State | What the person sees | What they can do |
|---|---|---|
| Default | Six cards under an accent top edge | Choose a goal |
| Hover | Accent background at 6% | Choose a goal |
| Press | Accent background at 12% | Choose a goal |
| Keyboard focus | A 1px accent ring inside the card's edge | Choose a goal |
| Phone width | One column | Choose a goal |
| Reduced motion | The same steps, instantly | Choose a goal |
| No goals configured | The section does not render | Browse studios |
No overlays
No modals, dialogs, or drawers anywhere on the site. The one exception is the map's popup, which is part of the map library.
Empty and no results
One state in different containers, told apart by what caused it rather than by which view is open. Inside the results, recovery calls clearAllFilters() in place, so every studio returns without a page load.
List, filters active
"No studios match your current filters." + Clear all filters
List, area filter only
"No studios found in this area." + Clear area filter
Map, studios without a location
"This studio isn't on the map yet."
Map tiles fail three times
"The map isn't loading right now." with a path back to List
Errors show a reference, not a message
A render error page shows the error's digest. Production builds redact the message, and the digest is the part that is both safe to show and useful for tracing.
Where a studio link returns to
A card on a results page carries the results URL. The studio page's back link then reads "Back to studios" and returns there; opened any other way, it reads "Browse all studios."
Planned before built
Changes are planned with a state matrix: what a person sees in each state, what they can still do, how they recover, and which component renders it. docs/state-matrices.md holds ten dated sections. States that cannot occur are named rather than skipped: the site is fully public, so there is no permission state.
Data Architecture
Studio data is one JSON file built into the site, so no list or studio page waits on a request for it. There are no accounts.
The catalog, counted from the data
| Public studios | 49 |
| Category | 31 studio arts, 18 dance |
| Area | 21 DC, 18 Maryland, 10 Northern Virginia |
| Beginner-friendly | 48 confirmed yes, 1 confirmed no, 0 unconfirmed |
| With a named cultural root | 11 |
Listing, the fields that shape the experience
id: string; name: string;
category: 'dance' | 'studio-arts';
disciplines: string[];
area: 'DC' | 'Maryland' | 'Northern Virginia';
culturalRoots: string; // cultural lineage of the practice; empty string if not applicable
environmentTags: string[]; // only from public studio descriptions
beginnerFriendly: boolean | null; // true = yes, false = no, null = not yet confirmed
beginnerPersonaTags: string[]; // powers homepage intent matching
cardSummary?: string; // plain, not promotional
bestFor: string[];
studioImages?: string[];
visibility?: 'hidden'; // suppress from all public queries without deleting
}
Excerpt from src/types/listing.ts. Comments are the file's own.
Three beginner states, two displays
true shows "Beginners welcome" on the default card and the "New to this?" callout on the studio page. false and null both show the level instead. "Beginners welcome" always means confirmed.
Cultural roots feed search, not the page
culturalRoots is matched when someone searches, so a search for a tradition finds the studios rooted in it. The chips on a studio page come from disciplines.
How a goal matches studios
if (persona && !l.beginnerPersonaTags.includes(persona)) return false;
| Goal | Public studios |
|---|---|
| I want to dance and meet people | 5 |
| I want a creative hobby | 31 |
| I want something culturally rooted | 10 |
| I want to move and reduce stress | 7 |
| I want to train seriously | 21 |
| I just want to try something | 19 |
Visible listings and similar studios
const publicListings = listings.filter((l) => l.visibility !== 'hidden');
// [id]/page.tsx: up to 3, same category and a shared goal, then same category
primary = same category && shares a beginnerPersonaTag;
return [...primary, ...sameCategoryFiller].slice(0, 3);
Decision Log
Product decisions with rationale. The "why" behind the product.
Goals are the primary navigation
Context
Most people starting a class do not know what to search for.
Decision
Six goal cards are the homepage's primary navigation, marked by the accent edge along the top of the grid. Search sits below them.
Rationale
The person self-selects an intent, then views or searches from there.
Trade-offs
Someone who already knows a studio's name takes one more step to reach it.
Cobalt is the accent, orange is the brand color
Context
The guide needs a recognizable brand color and an interaction color that reads at every size.
Decision
The Civic palette makes cobalt #1A56C4 the accent for focus and the primary action, and keeps orange #E8581F as the brand color.
Rationale
Measured on white, cobalt is 6.62:1, which clears the 4.5:1 text threshold at any size.
Trade-offs
Cobalt is cooler and less distinctive than the brand orange.
Press is shown by background, never by scale
Context
On a phone there is no hover, and opening a goal loads a new page, so a tap needs a visible response.
Decision
Pressing a goal card deepens its background to 12% accent.
Rationale
The cards share borders in a grid. Scaling one inward would expose the container behind it on all four edges.
Trade-offs
A color step is less tactile than a shrink, so the three backgrounds are tested to stay distinct.
No paid placement
Context
The guide states it takes no paid submissions, so any badge on a studio has to be unmistakably editorial.
Decision
Studio pages carry no placement badge, and the site has no paid placement. The featured field drives only the editorial Editor's Picks.
Rationale
A reader choosing a studio can trust that no listing was paid for.
Trade-offs
The guide gives up placement as a source of revenue.
Only the studio's own photo
Context
A visitor reads a photo on a studio page as a picture of that studio.
Decision
A studio page shows a photo only when it is the studio's own. Otherwise the page uses its no-photo layout.
Rationale
A photo that is not the studio would misdescribe it, on a guide whose value is describing studios accurately.
Trade-offs
Studio pages are less visual than a directory filled with stock imagery.
How listings are made
Context
A visitor decides whether to walk into a studio based on its listing.
Decision
Each listing moves through structured intake from the studio's own materials, a draft checked against that source, and a review in which every draft is read, usually edited, and approved.
Rationale
Nothing reaches the page without being checked against the studio's own materials and reviewed.
Trade-offs
Adding a studio takes longer than publishing submissions as written.