Canonical sources: Portal philosophy, posture, and language live in
/docs/planning/champion-portal/source/README.md.
Use these sources (includingCHRIST_CENTERED__IDENTITY_STATEMENT.md) when writing specs or user-facing copy. Prefer quoting/paraphrasing over inventing new language.
Version: 2.3
Last Updated: 2026-01-10
Purpose: Ensure the Champion Portal feels warm, welcoming, and distinctly Belmont
Status: Living Document — Use this prominently for all visual refresh work
The Champion Portal is a community, not a corporate portal. Every design decision should reinforce that Champions are welcomed, valued, and connected.
❌ Avoid:
✅ Embrace:
This isn’t just a tool—it’s a place to belong. Design should:
Use Belmont elements as structure, not decoration:
The goal: Feel like a Belmont product without looking like a marketing brochure.
Per JOBS-TO-BE-DONE.md Job C10:
“When I put effort into being a Champion, I want to see that my contributions matter.”
Every feature should ask: How can we recognize the people who contribute?
These are non-negotiables for all new design work:
| # | Principle | What It Means |
|---|---|---|
| 1 | Warmth over sterility | Friendly colors, soft shadows, rounded corners, human language |
| 2 | Scannable layout | Clear visual hierarchy, obvious sections, breathing room |
| 3 | Visual differentiation | Each section has distinct character (icon, accent, tint) |
| 4 | Mobile-first polish | Touch-friendly, thumb-zone aware, responsive by default |
| 5 | Belmont identity with restraint | Brand colors anchor, but don’t overwhelm |
If a page feels cold or corporate, fix it in this order:
Use the Belmont digital palette for the portal UI (it’s explicitly intended for screens with better contrast).
| Color | Digital Hex | Tailwind Class | Usage |
|---|---|---|---|
| Belmont Blue | #001D54 |
bg-belmontblue, text-belmontblue |
Primary anchor — headers, nav, buttons, selected states, focus rings |
| Belmont Red | #B21029 |
bg-belmontred, text-belmontred |
Secondary emphasis — alerts, badges, destructive actions, rare highlights |
⚠️ Belmont Blue is the primary anchor color and should be the dominant brand signal.
Belmont Red is secondary and should show up as emphasis, not wallpaper.
💡 If the UI starts to feel “too blue,” fix it with warm neutrals and spacing — not by swapping Belmont Blue out.
| Color | Hex | Tailwind Class | Usage |
|---|---|---|---|
| Admissions Blue | #1D4289 |
bg-admissionsblue |
Alternative primary for variety |
| Fountain Blue | #2874AF |
bg-fountainblue |
Secondary buttons, hover states |
| Sky Blue | #6AB3E7 |
bg-skyblue |
Highlights, links, engagement indicators |
Use warm-leaning neutrals for surfaces so the portal feels hospitable:
| Purpose | Recommendation |
|---|---|
| Page backgrounds | Off-white / warm gray (avoid pure white everywhere) |
| Borders | Soft, low-contrast (not harsh lines) |
| Cards | Tinted surfaces or soft white with subtle shadow |
| Text | Dark gray (#3D3D3D), not pure black |
Avoid:
Assign each major area a single accent that shows up as:
Keep accents subtle so Belmont Blue remains “home base.”
<!-- Primary button (Belmont Blue) -->
<button class="bg-belmontblue hover:bg-fountainblue text-white px-4 py-2 rounded-lg">
Say Hello
</button>
<!-- Secondary button -->
<button class="bg-white border border-belmontblue text-belmontblue hover:bg-gray-50 px-4 py-2 rounded-lg">
Learn More
</button>
<!-- Destructive action (Belmont Red, sparingly) -->
<button class="bg-belmontred hover:bg-red-800 text-white px-4 py-2 rounded-lg">
Delete Account
</button>
<!-- Card with warmth -->
<div class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm hover:shadow-md transition">
<!-- Content -->
</div>
Belmont’s primary web typefaces:
If licenses unavailable, Belmont approves these substitutes:
Currently configured in application.tailwind.css:
| Purpose | Font | Tailwind Class |
|---|---|---|
| Body text | Inter | font-sans (default) |
| Headings | Montserrat | font-sansalt |
| Rule | Why |
|---|---|
| Bigger default body text | Avoid tiny “admin UI” sizing — aim for 16px+ |
| Softer hierarchy | Fewer ALL CAPS labels, fewer ultra-bold headings |
| Comfortable line-height | For paragraph copy, empty states, instructions |
| Use serif sparingly | For quotes, “story” moments, short callouts — not dense UI text |
<!-- Page titles -->
<h1 class="font-sansalt text-3xl font-bold text-belmontblue">
Welcome, Sarah
</h1>
<!-- Section headers -->
<h2 class="font-sansalt text-xl font-semibold text-gray-800">
Champions in Nashville
</h2>
<!-- Body text (comfortable) -->
<p class="text-base text-gray-700 leading-relaxed">
Connect with fellow Bruins in your city.
</p>
<!-- Helper text -->
<p class="text-sm text-gray-500 leading-relaxed">
This helps other Champions find you.
</p>
Use a single icon set across the portal (consistent stroke, corner radius, visual weight).
Recommended: Heroicons — outline style
| Context | Icon | Usage |
|---|---|---|
| Contact/Message | envelope |
Contact button, messaging |
| Profile | user-circle |
Profile views, account |
| Directory | users |
Champion directory |
| Events | calendar |
Event listings |
| Location | map-pin |
City/location indicators |
| Search | magnifying-glass |
Directory search |
| Success | check-circle |
Confirmations |
| Activity | sparkles |
Recent activity, engagement |
| Settings | cog-6-tooth |
Account settings |
| Help | question-mark-circle |
FAQ, help pages |
<!-- Icon with text (button) -->
<button class="inline-flex items-center gap-2 bg-belmontblue text-white px-4 py-2 rounded-lg">
<%= heroicon "envelope", variant: :outline, options: { class: "w-5 h-5" } %>
Say Hello
</button>
<!-- Icon-only (with accessibility) -->
<button aria-label="View profile" class="p-2 hover:bg-gray-100 rounded-lg">
<%= heroicon "user-circle", variant: :outline, options: { class: "w-6 h-6 text-belmontblue" } %>
</button>
Avoid:
The four Champion roles (Community Builder, Digital Ambassador, Connection Advisor, Giving Advocate) each have two visual assets in /public/:
| Asset | Files | Format | Use Case |
|---|---|---|---|
| Icons | {role}-icon.svg |
Hexagonal badge, no text, currentColor |
Inline next to supporting text |
| Seals | {role}-seal.svg |
Circular badge with role name & virtues baked in | Standalone/hero contexts |
Helpers (in ApplicationHelper):
role_icon_svg(role_key, class_name:, color_class:, title:) — Renders icon inline with fill-current so Tailwind text-{color} controls the color. ViewBox: 0 0 128.1 147.1.role_seal_svg(role_key, class_name:, color_class:, title:) — Renders seal inline with fill-current. ViewBox: 0 0 220 220.When to use which:
| Context | Asset | Why |
|---|---|---|
| Next to role name/title (cards, lists, nav, radio buttons) | Icon | Text provides context; icon is a visual accent |
| Hero/celebration header (standalone, no adjacent text) | Seal | Seal includes the role name and virtues; self-explanatory |
| Email templates | Emoji | Inline SVG is unreliable in email clients; keep emoji fallback |
Sizing rules:
text-{role_color} (never render as black/default)Role color mapping:
| Role | Color Class |
|——|————-|
| Community Builder | text-belmontblue |
| Digital Ambassador | text-skyblue |
| Connection Advisor | text-fountainblue |
| Giving Advocate | text-belmontred |
Sterile UIs often fail because everything is evenly tight. Instead:
| Pattern | Why |
|---|---|
| Generous vertical spacing between sections | Creates clear visual breaks |
| Tight spacing within components | Forms, tables stay scannable |
| 2–3 content zones per page | Not 7–10 little boxes |
Belmont’s homepage leans on:
In the portal, translate that into:
| Element | Warm Approach |
|---|---|
| Cards | Noticeably rounded corners (not barely rounded) |
| Shadows | Soft, subtle shadows with low opacity |
| Dividers | Lighter borders, fewer hard lines |
| Backgrounds | Tinted surfaces instead of stark white |
┌─────────────────────────────────────────────────────────────────────┐
│ [PAGE HEADER] │
│ Title + 1–2 line description + optional action button │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ [SECTION 1] ────────────────────────────────────────────── │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ Card │ │ Card │ │ Card │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ │
│ [SECTION 2] ────────────────────────────────────────────── │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Larger content block or list │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Cards should feel like “inviting surfaces,” not “dashboard tiles.”
Card anatomy:
<!-- Standard card -->
<div class="bg-white rounded-xl border border-gray-200 p-6 shadow-sm hover:shadow-md transition">
<div class="flex items-start gap-3">
<%= heroicon "users", variant: :outline, options: { class: "w-6 h-6 text-belmontblue flex-shrink-0" } %>
<div>
<h3 class="font-semibold text-gray-900">Champions Near You</h3>
<p class="text-sm text-gray-600 mt-1">Connect with 23 Champions in Nashville</p>
</div>
</div>
</div>
<!-- Card with accent border -->
<div class="bg-white rounded-xl border-l-4 border-l-skyblue border border-gray-200 p-6">
<!-- Content -->
</div>
| Type | Color | Usage |
|---|---|---|
| Primary | Belmont Blue | Main actions (“Say Hello,” “Submit Event”) |
| Secondary | White + Blue border | Alternative actions (“Learn More,” “Cancel”) |
| Destructive | Belmont Red | Delete, remove — use sparingly |
| Ghost | Transparent | Subtle actions (“Edit,” “View All”) |
Friendly button rules:
<!-- Primary -->
<button class="bg-belmontblue hover:bg-fountainblue text-white px-5 py-2.5 rounded-lg font-medium">
Say Hello
</button>
<!-- Secondary -->
<button class="bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 px-5 py-2.5 rounded-lg font-medium">
Cancel
</button>
<!-- Destructive (rare) -->
<button class="bg-belmontred hover:bg-red-800 text-white px-5 py-2.5 rounded-lg font-medium">
Delete
</button>
Make forms feel like guidance, not compliance:
| Pattern | Example |
|---|---|
| Always include helper text | “This helps other Champions find you” |
| Human validation | “That email doesn’t look right” vs “Invalid format” |
| Grouped fields | Labeled sections with whitespace |
| Progressive disclosure | Advanced fields behind “Add details” toggle |
<!-- Form field with helper text -->
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700">City</label>
<input type="text" class="w-full rounded-lg border-gray-300 focus:border-belmontblue focus:ring-belmontblue" />
<p class="text-sm text-gray-500">Enter your current city so Champions can find you.</p>
</div>
Tables are inherently “cold” — soften them:
| Pattern | Implementation |
|---|---|
| Zebra striping | Very subtle tint (not harsh alternation) |
| Row padding | More generous than default |
| Empty state | Suggests what to do next |
| Mobile | Switch to card rows (name + key fields + actions) |
Every empty state should include:
Tone examples:
| ❌ Bad | ✅ Good |
|---|---|
| “No Champions found.” | “No Champions in Portland yet.” |
| “No messages.” | “Your inbox is empty — start a conversation!” |
| “No events.” | “No events scheduled. Want to host one?” |
<!-- Empty state example -->
<div class="text-center py-12">
<%= heroicon "users", variant: :outline, options: { class: "w-12 h-12 text-gray-400 mx-auto" } %>
<h3 class="mt-4 text-lg font-medium text-gray-900">No Champions in Portland yet</h3>
<p class="mt-2 text-gray-500">Know an alum there? Invite them to join!</p>
<button class="mt-4 bg-belmontblue text-white px-4 py-2 rounded-lg">
Invite a Champion
</button>
</div>
| State | Approach |
|---|---|
| Loading | Skeleton screens for lists/cards (reduces stress) |
| Success | Short confirmation + what happens next |
| Errors | One clear fix, not a wall of red |
Implemented in Phase 1.9.4 — The dashboard is the “home” of the Champion Portal.
The dashboard should feel like returning to a welcoming community space, not logging into a corporate tool.
| Admin Panel Feel ❌ | Community Home Feel ✅ |
|---|---|
| Generic “Dashboard” title | “Good morning, Jane! 👋” |
| Dense metrics grid | Scannable card hierarchy |
| All sections equal weight | Clear visual priority |
| Static, unchanging | Fresh content encourages return |
| Transactional language | Warm, belonging language |
Personalized greetings based on local time create warmth:
| Time Range | Greeting |
|---|---|
| 5:00 AM – 11:59 AM | “Good morning, [Name]! 👋” |
| 12:00 PM – 4:59 PM | “Good afternoon, [Name]! 👋” |
| 5:00 PM – 4:59 AM | “Good evening, [Name]! 👋” |
Implementation: See Cp::DashboardHelper#time_of_day_greeting
<h1 class="font-sansalt text-2xl sm:text-3xl font-bold text-belmontblue">
<%= time_of_day_greeting(current_cp_champion.display_first_name) %>
</h1>
Champions in their first week get an enhanced welcome hero:
<% if current_cp_champion.created_at > 7.days.ago %>
<!-- Full welcome banner with gradient -->
<% else %>
<!-- Simpler returning champion greeting -->
<% end %>
Dashboard widgets have clear priority order. Higher priority = larger, more prominent.
| Priority | Widget | Visual Treatment |
|---|---|---|
| 1. Hero | Welcome greeting | Full-width, gradient for new users |
| 2. Next Steps | Profile/verification prompts | Prominent card, consolidated |
| 3. Community | District preview | Champion photos, location context |
| 4. Messages | Unread count + previews | Sidebar position, badge for unread |
| 5. Profile | Compact summary | Sidebar, completion progress |
| 6. News | Community content | Grid cards (Phase 1.10) |
| 7. Quick Actions | Edit, search, invite, help | Demoted to subtle row |
Multiple prompts (profile completion, verification pending, location missing) should be consolidated into one card, not scattered across the page.
Card Structure:
Item Types: | Icon | Type | Description | |——|——|————-| | 👤 (profile) | Profile completion | “Complete your profile (85%)” | | ⏳ (clock) | Verification pending | “You’ll get directory access soon” | | 📍 (location) | Missing location | “Add ZIP code to find nearby Champions” |
The Community Snapshot card adapts to the champion’s state:
| State | Display |
|---|---|
| No ZIP code | Prompt to add location |
| Not verified | “Directory unlocks after verification” + count |
| First in district | Celebration + invite CTA + regional fallback |
| Has neighbors | Photo preview grid + “View all →” |
Design now, implement in Phase 3.3. The dashboard needs fresh content for return visits.
| Post Type | Icon | Example |
|---|---|---|
| Story | 📰 | Alumni Spotlight: Sarah Chen |
| Photo | 📷 | Nashville Meetup Recap |
| Announcement | 📢 | New Feature: Direct Messaging |
Environment-aware display:
<% if show_demo_content? %>
<!-- Sample news cards -->
<% else %>
<!-- Coming soon placeholder -->
<% end %>
Quick actions are utility, not primary navigation. Display as subtle row at bottom:
<div class="border-t border-gray-200 pt-6">
<p class="text-xs font-medium text-gray-400 uppercase tracking-wider mb-3">Quick Actions</p>
<div class="flex flex-wrap gap-2">
<!-- Ghost-style buttons: Edit Profile, Find Alumni, Invite, Help, Settings -->
</div>
</div>
Mobile (< 1024px): Single column, full-width cards stacked vertically.
Desktop (≥ 1024px): 3-column grid:
Desktop Layout:
┌─────────────────────────────────────────────────────────────────┐
│ Hero: Welcome greeting (full width) │
├───────────────────────────────────────────┬─────────────────────┤
│ Next Steps (2/3) │ Messages (1/3) │
├───────────────────────────────────────────┼─────────────────────┤
│ Community Snapshot (2/3) │ Your Profile (1/3) │
├───────────────────────────────────────────┴─────────────────────┤
│ News/Posts (full width, 3-card grid) │
├─────────────────────────────────────────────────────────────────┤
│ Quick Actions (subtle row, full width) │
└─────────────────────────────────────────────────────────────────┘
Show who’s active and engaged:
<!-- Active indicator on profile card -->
<div class="relative">
<img src="..." class="w-12 h-12 rounded-full" />
<span class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 border-2 border-white rounded-full"
title="Active this week"></span>
</div>
| Badge | Trigger |
|---|---|
| 🎉 Event Host | Hosted at least 1 event |
| 📖 Storyteller | Shared a story |
| 🤝 Connector | Sent 5+ messages |
| ⭐ Active | Active every week for a month |
New Champions should feel welcomed:
<!-- Dashboard welcome banner -->
<div class="bg-gradient-to-r from-belmontblue to-admissionsblue text-white rounded-xl p-6">
<h2 class="text-2xl font-bold">Welcome to the Champion Portal, Sarah! 🎉</h2>
<p class="mt-2 text-blue-100">You're now connected to 127 Champions across 23 cities.</p>
<a href="..." class="mt-4 inline-block bg-white text-belmontblue px-4 py-2 rounded-lg font-semibold hover:bg-gray-100">
Find Champions Near You
</a>
</div>
For milestone moments (first event submitted, account verified), consider subtle confetti or animation.
See also: Phase 1.17: Mobile Polish for implementation details
| Breakpoint | Size | Design |
|---|---|---|
| Mobile | < 640px | Single column, stacked cards |
| Tablet | 640–1024px | Two columns where appropriate |
| Desktop | > 1024px | Full layout, sidebars |
The Champion Portal uses a fixed bottom navigation bar on mobile. All pages must account for this by adding bottom padding to the main content area:
<%# In layout or main content wrapper %>
<main class="pb-20 sm:pb-0">
<%# Content won't be hidden behind bottom nav %>
</main>
Standardize card styling across all views:
| Property | Mobile | Desktop |
|---|---|---|
| Horizontal margin | mx-4 (16px) |
mx-0 (in grid) |
| Internal padding | p-4 (16px) |
p-6 (24px) |
| Section spacing | space-y-4 |
space-y-6 |
<%# Standard card pattern %>
<div class="mx-4 sm:mx-0">
<div class="bg-white rounded-xl shadow-sm p-4 sm:p-6">
<%# Card content %>
</div>
</div>
On mobile, filter-heavy views (Directory, Events) should collapse filters by default:
┌─────────────────────────────────┐
│ Filters (2 applied) [▼] │
└─────────────────────────────────┘
Hero sections should inform, not dominate. On mobile:
| Guideline | Rule |
|---|---|
| Max viewport height | ~35% of initial viewport |
| Hero padding | py-4 sm:py-8 (reduced on mobile) |
| Action buttons | Move outside hero on mobile |
| Photo size | w-20 h-20 mobile, w-24 h-24 desktop |
Use consistent back navigation across all detail views:
<%# app/views/shared/_back_link.html.erb %>
<%= link_to path, class: "inline-flex items-center gap-1 text-gray-600 hover:text-belmontblue text-sm mb-4" do %>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
Back to <%= section_name %>
<% end %>
Rules:
<)text-gray-600 with hover:text-belmontblueHandle long text gracefully:
| Content Type | Strategy | Tailwind Class |
|---|---|---|
| Names (single line) | Truncate with ellipsis | truncate |
| Descriptions | 2-line clamp | line-clamp-2 |
| Community names | Allow 2-line wrap | line-clamp-2 |
Context: The Alumni Lookup admin portal (.alumnilookup.com/champions/) uses tables for data-heavy views. These tables are replaced with mobile card views on small screens.
For index/list pages with data tables:
<%# 1. Mobile Card Container (visible on mobile only) %>
<div class="sm:hidden mt-6 space-y-3">
<% @items.each do |item| %>
<%# Link wrapper makes entire card clickable %>
<%= link_to item_path(item), class: "block bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5 p-4 hover:bg-gray-50" do %>
<div class="flex items-start gap-3">
<%# Left: Image/Avatar %>
<div class="w-12 h-12 rounded-lg bg-gray-100 flex-shrink-0">
<%# Image or placeholder %>
</div>
<%# Center: Primary info %>
<div class="flex-1 min-w-0">
<span class="font-medium text-gray-900 truncate"><%= item.title %></span>
<p class="text-sm text-gray-500"><%= item.subtitle %></p>
<p class="text-xs text-gray-400 mt-1">📍 <%= item.location %></p>
</div>
<%# Right: Chevron %>
<svg class="w-5 h-5 text-gray-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</div>
<%# Bottom: Badges/metadata row %>
<div class="flex flex-wrap items-center gap-2 mt-3 pt-3 border-t border-gray-100">
<span class="badge">Status</span>
<span class="text-xs text-gray-400 ml-auto">Date</span>
</div>
<% end %>
<% end %>
<%# Mobile-specific pagination and empty state here %>
</div>
<%# 2. Desktop Table (hidden on mobile) %>
<div class="hidden sm:block mt-6 bg-white shadow overflow-hidden rounded-lg">
<table class="min-w-full divide-y divide-gray-200">
<%# ... existing table markup ... %>
</table>
</div>
| Element | Class | Purpose |
|---|---|---|
| Container | sm:hidden mt-6 space-y-3 |
Mobile only, vertical spacing |
| Card | block bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5 p-4 hover:bg-gray-50 |
Clickable, elevated |
| Image/Avatar | w-12 h-12 rounded-lg flex-shrink-0 |
Consistent size, no shrink |
| Title | font-medium text-gray-900 truncate |
Bold, truncate long names |
| Subtitle | text-sm text-gray-500 |
Secondary info |
| Metadata | text-xs text-gray-400 |
Timestamps, locations |
| Chevron | w-5 h-5 text-gray-400 flex-shrink-0 |
Indicates tappable |
| Badge row | flex flex-wrap items-center gap-2 mt-3 pt-3 border-t |
Bottom section |
<%# Published/Active/Complete %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Published
</span>
<%# Pending/Draft %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
Draft
</span>
<%# Archived/Inactive %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
Archived
</span>
<%# Rejected/Error %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
Rejected
</span>
Use emoji sparingly to convey context at a glance:
| Emoji | Meaning | Example |
|---|---|---|
| 📅 | Event | 📅 Nashville Meetup |
| 📍 | Location/District | 📍 Nashville |
| 🌐 | Community | 🌐 Music Business |
| 📷 | Photo count | 📷 12 photos |
| ⭐ | Featured count | ⭐ 3 featured |
| 💬 | Comments/replies | 💬 5 replies |
| 👤 | Person | 👤 Jane Smith |
For detail/show pages in admin portal:
<%# Action buttons should stack on mobile %>
<div class="flex flex-col sm:flex-row gap-2 sm:gap-3">
<%# Primary action %>
<%= link_to "Primary Action", action_path,
class: "inline-flex items-center justify-center gap-2 px-4 py-2 bg-belmontblue text-white rounded-lg hover:bg-fountainblue transition text-center" %>
<%# Secondary action %>
<%= link_to "Secondary", other_path,
class: "inline-flex items-center justify-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition text-center" %>
<%# Destructive action (if needed) %>
<%= button_to "Delete", destroy_path, method: :delete,
data: { turbo_confirm: "Are you sure?" },
class: "inline-flex items-center justify-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition text-center" %>
</div>
Rules:
flex-col sm:flex-row for stacking on mobilejustify-center text-center for full-width mobile buttons| Page Type | Reference File |
|---|---|
| Index with cards | champions/events/index.html.erb |
| Index with complex data | champions/champions/_list.html.erb |
| Show page buttons | champions/feedbacks/show.html.erb |
| Show page with stats | champions/communities/show.html.erb |
| Discussions with moderation | champions/discussions/show.html.erb |
| Photo albums with actions | champions/photo_albums/show.html.erb |
Problem: Calling model.verified? fails because enum methods follow a specific naming pattern.
Pattern: For enum columns, Rails generates status_<value>? methods, NOT <value>? methods.
| ❌ Wrong | ✅ Correct | Why |
|---|---|---|
champion.verified? |
champion.status_champion_verified? |
Enum column is status with value champion_verified |
event.published? |
event.status_published? |
Same pattern |
How to check: Look at the model’s enum definition:
# In model
enum :status, { pending: 0, champion_verified: 1, rejected: 2 }
# Generated methods
champion.status_pending?
champion.status_champion_verified?
champion.status_rejected?
Problem: Index cards that link to show action fail when show action doesn’t exist.
Pattern: Check if the resource has a show action. If except: [:show], link to edit instead.
| Resource | Routes Declaration | Card Links To |
|---|---|---|
| Events | resources :events, except: [:show] |
edit_champions_event_path(event) |
| Communities | resources :communities (has show) |
champions_community_path(community) |
| Discussions | resources :discussions (has show) |
champions_discussion_path(discussion) |
How to verify:
bin/rails routes | rg "champions_events"
# Look for GET /champions/events/:id — if missing, no show action
Problem: Aggressive truncation like truncate max-w-[120px] cuts off content too aggressively.
Pattern: Use line-clamp-n for multi-line text, allow reasonable width.
| Content Type | Pattern | Example |
|---|---|---|
| Card titles | line-clamp-2 |
<span class="font-medium text-gray-900 line-clamp-2"> |
| Community badges | max-w-[200px] line-clamp-1 |
Allows longer names but caps at 200px |
| Descriptions | line-clamp-3 to line-clamp-5 |
Adjust based on layout |
| Names | min-w-0 on container + line-clamp-2 |
Prevents flex overflow |
Rules:
truncate with small fixed widths (max-w-[120px])min-w-0 on flex children to enable truncationline-clamp-2 over line-clamp-1 for titlesProblem: Headers with photo + name + buttons crunch on mobile.
Pattern: Use flex-col on mobile, flex-row on desktop.
<%# Header container %>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<%# Left side: photo + info %>
<div class="flex items-center gap-4">
<%# Photo - prevent shrinking %>
<div class="flex-shrink-0">
<img class="w-16 h-16 rounded-full object-cover" />
</div>
<%# Info - allow truncation %>
<div class="min-w-0">
<h1 class="text-2xl font-bold text-gray-900 line-clamp-2"><%= item.name %></h1>
<p class="text-gray-500 line-clamp-1"><%= item.subtitle %></p>
</div>
</div>
<%# Right side: action buttons %>
<div class="flex flex-wrap gap-2">
<%# Buttons here %>
</div>
</div>
Key classes:
flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4flex-shrink-0 (prevents compression)min-w-0 (enables truncation in flex context)line-clamp-2 (wraps gracefully)Pattern: Icons + short labels, color-coded by action type, stack on mobile.
<div class="flex flex-col sm:flex-row flex-wrap gap-2">
<%# Edit - gray %>
<%= link_to edit_path, class: "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg bg-gray-100 text-gray-700 hover:bg-gray-200" do %>
<svg class="w-4 h-4"><!-- pencil icon --></svg>
Edit
<% end %>
<%# Toggle visibility - gray/green %>
<%= button_to toggle_path, class: "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg #{item.hidden? ? 'bg-green-100 text-green-700 hover:bg-green-200' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}" do %>
<svg class="w-4 h-4"><!-- eye icon --></svg>
<%= item.hidden? ? "Show" : "Hide" %>
<% end %>
<%# Lock - yellow %>
<%= button_to lock_path, class: "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg bg-yellow-100 text-yellow-700 hover:bg-yellow-200" do %>
<svg class="w-4 h-4"><!-- lock icon --></svg>
<%= item.locked? ? "Unlock" : "Lock" %>
<% end %>
<%# Delete - red %>
<%= button_to destroy_path, method: :delete, class: "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg bg-red-100 text-red-700 hover:bg-red-200" do %>
<svg class="w-4 h-4"><!-- trash icon --></svg>
Delete
<% end %>
<%# External link - white with ring %>
<%= link_to external_url, target: "_blank", class: "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50" do %>
<svg class="w-4 h-4"><!-- external link icon --></svg>
View
<% end %>
</div>
Color coding:
| Action Type | Background | Text | Hover |
|————-|————|——|——-|
| Edit/Default | bg-gray-100 | text-gray-700 | hover:bg-gray-200 |
| Show/Restore | bg-green-100 | text-green-700 | hover:bg-green-200 |
| Lock/Caution | bg-yellow-100 | text-yellow-700 | hover:bg-yellow-200 |
| Resolve/Special | bg-purple-100 | text-purple-700 | hover:bg-purple-200 |
| Delete/Danger | bg-red-100 | text-red-700 | hover:bg-red-200 |
| External | bg-white ring-1 ring-gray-300 | text-gray-700 | hover:bg-gray-50 |
Problem: Section headers with action buttons use flex items-center justify-between, which keeps the button stuck to the right of the header text even on mobile, causing cramped layouts.
Pattern: Use flex-col on mobile, flex-row on desktop for section headers with actions.
<%# ❌ WRONG: Button stays right of header on all screen sizes %>
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium">Section Title</h3>
<%= link_to "Action", path, class: "..." %>
</div>
<%# ✅ CORRECT: Button stacks below header on mobile, beside on desktop %>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h3 class="text-lg font-medium">Section Title</h3>
<%= link_to "Action", path, class: "inline-flex items-center justify-center ..." %>
</div>
Rules:
flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4justify-center for full-width appearance on mobileflex items-center justify-between for section headers (this is the OLD pattern)Problem: Destructive actions (Deactivate, Delete) placed in the header with other buttons makes them too easy to click accidentally and clutters the header.
Pattern: Place destructive actions in a “Danger Zone” section at the bottom of show pages.
<!-- Danger Zone -->
<div class="mt-8 bg-white shadow overflow-hidden sm:rounded-lg border border-red-200">
<div class="px-4 py-5 sm:px-6 border-b border-red-200 bg-red-50">
<h3 class="text-lg font-medium text-red-900">Danger Zone</h3>
<p class="text-sm text-red-700">Irreversible and destructive actions</p>
</div>
<div class="px-4 py-5 sm:px-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<p class="text-sm font-medium text-gray-900">Action title</p>
<p class="text-sm text-gray-500">Description of what this action does.</p>
</div>
<%= button_to "Destructive Action", path,
method: :patch,
class: "inline-flex items-center justify-center px-4 py-2 border border-red-300 shadow-sm text-sm font-medium rounded-md text-red-700 bg-white hover:bg-red-50",
data: { turbo_confirm: "Are you sure?" } %>
</div>
</div>
</div>
Rules:
border-red-200) and red header background (bg-red-50)turbo_confirm for destructive actionsProblem: Inconsistent card styling and section organization across admin show pages.
Pattern: Each major section should be its own card with consistent styling.
<%# Standard admin card %>
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<%# Card header %>
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h3 class="text-lg font-medium text-gray-900">Section Title</h3>
<%# Optional action button %>
<%= link_to "Action", path, class: "..." %>
</div>
</div>
<%# Card content %>
<div class="px-4 py-5 sm:px-6">
<%# Section content here %>
</div>
</div>
Principles:
bg-white shadow overflow-hidden sm:rounded-lgpx-4 py-5 sm:px-6 border-b border-gray-200px-4 py-5 sm:px-6mt-8 between cards for consistent spacingProblem: Inconsistent card classes and redundant container wrappers across admin views make the interface feel disjointed.
The layout (application.html.erb) already provides:
mx-auto max-w-7xl — Maximum width centeringpx-4 sm:px-6 lg:px-8 — Horizontal padding responsive breakpointspy-6 — Vertical paddingTherefore, admin views should NOT add redundant wrappers:
<%# ❌ WRONG: Redundant wrapper (layout already provides this) %>
<div class="px-4 sm:px-6 lg:px-8">
<div class="py-6">
<!-- content -->
</div>
</div>
<%# ✅ CORRECT: Content starts directly, no wrapper needed %>
<!-- Page header -->
<div class="sm:flex sm:items-center sm:justify-between">
<h1 class="text-2xl font-semibold text-gray-900">Page Title</h1>
</div>
<!-- Card content -->
<div class="mt-6 bg-white shadow overflow-hidden rounded-md sm:rounded-lg">
<!-- ... -->
</div>
When to use max-w-* constraints:
max-w-4xl mx-auto — For narrow form pages (edit, new) to improve readabilityAll admin cards use this exact pattern:
<div class="bg-white shadow overflow-hidden rounded-md sm:rounded-lg">
<!-- Card content -->
</div>
| Class | Purpose |
|---|---|
bg-white |
White background |
shadow |
Subtle elevation (NOT shadow-sm) |
overflow-hidden |
Required for tables and rounded corners |
rounded-md sm:rounded-lg |
Responsive rounding — 8px on mobile, 12px on tablet+ |
All cards MUST use rounded-md sm:rounded-lg — not just sm:rounded-lg.
| Pattern | Mobile | Desktop | Use Case |
|---|---|---|---|
rounded-md sm:rounded-lg |
8px corners | 12px corners | ✅ Standard admin cards |
sm:rounded-lg only |
Square corners | 12px corners | ❌ Avoid — harsh on mobile |
rounded-lg only |
12px corners | 12px corners | ❌ Avoid — inconsistent with full-bleed pattern |
Why this matters: Cards without mobile rounding appear harsh and “boxy” on small screens. The rounded-md provides subtle softening while sm:rounded-lg gives more pronounced rounding on larger screens where there’s more visual space.
Never use these inconsistent variants:
| ❌ Don’t Use | ✅ Use Instead |
|————–|—————-|
| sm:rounded-lg (no mobile rounding) | rounded-md sm:rounded-lg |
| rounded-lg (always same) | rounded-md sm:rounded-lg |
| rounded-xl | rounded-md sm:rounded-lg |
| shadow-sm | shadow |
| ring-1 ring-gray-900/5 | Just shadow |
| border border-gray-200 | Just shadow (unless it’s a special case like Danger Zone) |
<div class="bg-white shadow overflow-hidden rounded-md sm:rounded-lg">
<%# Header %>
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h3 class="text-lg font-medium text-gray-900">Section Title</h3>
<%= link_to "Action", path, class: "..." %>
</div>
</div>
<%# Body %>
<div class="px-4 py-5 sm:px-6">
<!-- Content -->
</div>
</div>
| Context | Padding Class |
|---|---|
| Card header | px-4 py-5 sm:px-6 |
| Card body | px-4 py-5 sm:px-6 |
| Stats grid | px-4 py-5 sm:p-6 |
| Table container | No padding (table handles it) |
| Form content | p-6 (simpler for forms) |
<%# Mobile cards (hidden on desktop) %>
<div class="sm:hidden mt-6 space-y-3">
<% @items.each do |item| %>
<%= link_to item_path(item), class: "block bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5 p-4 hover:bg-gray-50" do %>
<!-- Mobile card content -->
<% end %>
<% end %>
</div>
<%# Desktop table (hidden on mobile) %>
<div class="hidden sm:block mt-6 bg-white shadow overflow-hidden rounded-md sm:rounded-lg">
<table class="min-w-full divide-y divide-gray-200">
<!-- Table content -->
</table>
</div>
Note: Mobile cards use ring-1 ring-gray-900/5 for a lighter touch, while desktop tables use the standard shadow for stronger elevation.
<%# Page header %>
<div class="sm:flex sm:items-center sm:justify-between">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Page Title</h1>
<p class="mt-1 text-sm text-gray-500">Optional description</p>
</div>
<div class="mt-4 sm:mt-0">
<%= link_to "Action", path, class: "..." %>
</div>
</div>
<%# Stats (optional) %>
<div class="mt-6 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
<div class="bg-white shadow overflow-hidden rounded-md sm:rounded-lg px-4 py-5 sm:p-6">
<!-- Stat content -->
</div>
</div>
<%# Main content card %>
<div class="mt-6 bg-white shadow overflow-hidden rounded-md sm:rounded-lg">
<!-- Content -->
</div>
<%# Additional sections with consistent spacing %>
<div class="mt-8 bg-white shadow overflow-hidden rounded-md sm:rounded-lg">
<!-- Another section -->
</div>
<%# Danger Zone (always last, if applicable) %>
<div class="mt-8 bg-white shadow overflow-hidden rounded-md sm:rounded-lg border border-red-200">
<!-- Danger zone content -->
</div>
| Context | Spacing Class |
|---|---|
| Header to first card | mt-6 |
| Between cards | mt-8 |
| Between form sections | mt-6 |
| Mobile card list items | space-y-3 |
When someone says “this page needs to be mobile optimized,” use this checklist.
This is the definitive reference for what “mobile optimized” means in this project. The previous subsections (8.1–8.12) cover implementation patterns; this section provides the complete evaluation framework.
Before diving into the checklist, apply this simple test:
“Do one thing here, quickly, while distracted.”
Ask yourself:
If any answer is “no” or “maybe,” the page needs work.
Goal: Content flows naturally in a single column; nothing feels cramped or requires horizontal scrolling.
| ✅ Requirement | Notes |
|---|---|
| Single-column layout on mobile | No side-by-side columns that crunch content |
| No horizontal scrolling | Tables convert to cards; wide content wraps or scrolls within container |
| Logical vertical stacking | Most important content first; actions near related content |
| Adequate breathing room | py-4, space-y-4 minimum between sections |
| Full-bleed cards on mobile | Cards go edge-to-edge (no mx-4 margins creating gutters) |
🚩 Red flags:
Goal: Every interactive element is easy to tap accurately on the first try.
| ✅ Requirement | Notes |
|---|---|
| Minimum 44×44px tap targets | Buttons, links, checkboxes, dropdown triggers |
| 8px+ spacing between targets | Prevent accidental taps on adjacent elements |
| Primary actions in thumb zone | Bottom half of screen, reachable without stretching |
| No hover-dependent interactions | All hover states must have tap/touch equivalents |
| Clear active/pressed states | Visual feedback on tap (not just release) |
🚩 Red flags:
Goal: Users always know where they are and how to go back.
| ✅ Requirement | Notes |
|---|---|
| Clear page/section titles | Visible without scrolling |
| Back navigation consistent | Use "← Back to [Section]" pattern from §8.9 |
| Tab bars/pills scroll gracefully | Long tab lists wrap or scroll horizontally with indicators |
| Bottom nav doesn’t obscure content | Add pb-20 to main content when using fixed bottom nav |
| Current location highlighted | Active nav item visually distinct |
🚩 Red flags:
Goal: Text is comfortable to read on a phone screen.
| ✅ Requirement | Notes |
|---|---|
| Base font size 16px+ | Prevents iOS zoom on focus; comfortable reading |
| Line height 1.5+ for body text | leading-relaxed or leading-loose |
| Headings scale proportionally | text-xl mobile → text-2xl desktop, not fixed sizes |
| Max 80 characters per line | Use max-w-prose or similar constraint |
| Sufficient contrast | WCAG AA (4.5:1 for body, 3:1 for large text) |
🚩 Red flags:
Goal: The most important action/information is immediately obvious.
| ✅ Requirement | Notes |
|---|---|
| One clear primary action per screen | Button hierarchy: one primary, others secondary/ghost |
| Key info visible without scrolling | Name, status, primary action above the fold |
| Progressive disclosure for details | Expandable sections, “Show more” patterns |
| Metadata grouped and de-emphasized | Dates, counts, IDs in smaller/muted text |
| Empty states guide next action | Never just “No results” — always suggest what to do |
🚩 Red flags:
Goal: Forms are easy to complete on mobile without frustration.
| ✅ Requirement | Notes |
|---|---|
| One column layout for forms | Never side-by-side fields on mobile |
| Correct keyboard type | type="email", type="tel", inputmode="numeric" |
| Labels above inputs (not beside) | Labels stay visible when field is focused |
| Large, tappable checkboxes/radios | 44px touch target including label |
| Clear error states | Inline errors near the field, not just top of form |
| Submit button always visible | Consider sticky footer or position after last field |
🚩 Red flags:
Goal: Data is scannable and actionable without overwhelming the screen.
| ✅ Requirement | Notes |
|---|---|
| Tables convert to cards on mobile | Use sm:hidden / hidden sm:block pattern from §8.11 |
| Row actions accessible without scroll | Show on card, or in overflow menu |
| Pagination or infinite scroll | Don’t load 100+ items at once |
| Sort/filter controls accessible | Collapse into filter button if needed |
| Key columns visible | Hide low-priority columns on mobile, not primary data |
🚩 Red flags:
Goal: The page feels fast, even on slow connections.
| ✅ Requirement | Notes |
|---|---|
| Appropriate image sizes | Don’t send 2000px images for 80px thumbnails |
| Lazy loading for below-fold content | Images, cards, comments |
| Skeleton/placeholder states | Show structure while content loads |
| No layout shift on load | Reserve space for images, dynamic content |
| Turbo Frame/Stream for partial updates | Don’t reload full page for small changes |
🚩 Red flags:
Goal: The page works for users with different abilities and assistive technologies.
| ✅ Requirement | Notes |
|---|---|
| Focus order matches visual order | Tab through the page in logical sequence |
| Focus states visible | focus:ring-2 focus:ring-belmontblue minimum |
| Touch targets meet WCAG AA | 44×44px minimum |
| Screen reader announces state changes | Use ARIA live regions for dynamic updates |
| No essential info in color alone | Icons, text, or patterns supplement color |
🚩 Red flags:
Goal: The mobile experience feels warm and inviting, not cramped or sterile.
| ✅ Requirement | Notes |
|---|---|
| Adequate whitespace | Cards have padding; sections have margins |
| Warm neutrals, not cold grays | gray-50 backgrounds, not pure white everywhere |
| Rounded corners on cards | rounded-lg or rounded-xl for touch-friendly feel |
| Consistent iconography | Same icon set; icons aid comprehension |
| No “admin density” on user-facing pages | Champions aren’t power users; give them room |
🚩 Red flags:
When auditing a page for mobile optimization:
For implementation details on specific patterns, see:
| Requirement | Standard |
|---|---|
| Color contrast | WCAG AA minimum (4.5:1 for text) |
| Focus states | Visible keyboard focus indicators |
| Alt text | All images must have descriptive alt text |
| ARIA labels | Interactive elements need accessible names |
| Skip links | Navigation skip for keyboard users |
Note: The Belmont digital brand colors are designed with screen contrast in mind.
These are “defaults” to keep us consistent:
| Element | Default |
|---|---|
| Border radius | rounded-lg for inputs, sm:rounded-lg for admin cards |
| Card shadow | shadow for admin cards (not shadow-sm) |
| Borders | Low-contrast (border-gray-200), avoid heavy outlines |
| Page background | Warm neutral (bg-gray-50 or custom warm) |
| Card background | bg-white with shadow overflow-hidden sm:rounded-lg |
Note: Champion Portal (user-facing) may use rounded-xl shadow-sm for cards. Admin Portal uses sm:rounded-lg shadow for a more data-focused appearance.
Fix in this order:
Key Principle: Image variant dimensions must match CSS display dimensions to avoid fuzzy images.
When using variant(resize_to_fill: [width, height]), the pixel values should match the actual rendered size:
| Tailwind Class | Pixels | Variant Size |
|---|---|---|
w-14 h-14 |
56×56 | resize_to_fill: [56, 56] |
w-20 h-20 |
80×80 | resize_to_fill: [80, 80] |
w-24 h-24 |
96×96 | resize_to_fill: [96, 96] |
w-28 h-28 |
112×112 | resize_to_fill: [112, 112] |
w-32 h-32 |
128×128 | resize_to_fill: [128, 128] |
w-40 h-40 |
160×160 | resize_to_fill: [160, 160] |
w-48 h-48 |
192×192 | resize_to_fill: [192, 192] |
Rectangular images: For non-square displays, use rectangular variants:
w-32 h-24 (128×96) = resize_to_fill: [128, 96] (4:3 ratio)w-48 aspect-[8/5] (192×120) = resize_to_fill: [192, 120] (8:5 ratio)aspect-video (16:9) = resize_to_fill: [800, 450] for full-width heroesCommon mistakes:
| ❌ Wrong | ✅ Correct | Issue |
|———-|———–|——-|
| resize_to_fill: [128, 128] for w-28 | resize_to_fill: [112, 112] | Oversized → wasted bandwidth |
| resize_to_fill: [112, 112] for rectangular display | resize_to_fill: [128, 96] | Square for rectangle → fuzzy upscaling |
| resize_to_fill: [800, 500] for aspect-video | resize_to_fill: [800, 450] | 8:5 ratio for 16:9 container → cropping |
/* Configured in tailwind.config.js */
belmontblue: '#001D54' /* Primary anchor */
belmontred: '#B21029' /* Secondary emphasis */
admissionsblue: '#1D4289' /* Alt primary */
fountainblue: '#2874AF' /* Hover states */
skyblue: '#6AB3E7' /* Highlights */
towerred: '#862633' /* Legacy - prefer belmontred */
When building pages, always include explicit image placeholders with requests:
<!-- Example placeholder in ERB -->
<div class="relative bg-gray-200 rounded-lg overflow-hidden" style="aspect-ratio: 16/9;">
<div class="absolute inset-0 flex items-center justify-center text-gray-500">
<div class="text-center p-4">
<svg class="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p class="font-medium">IMAGE NEEDED</p>
<p class="text-sm">Hero: Campus bell tower at sunset</p>
<p class="text-xs">1920x600px, landscape, space for text overlay on left</p>
</div>
</div>
</div>
Image Request: Champion Portal Hero Image
Purpose: Landing page hero for alumnichampions.com
Dimensions: 1920×600px (will be cropped responsively)
Subject: Belmont campus with alumni, ideally near bell tower or lawn
Mood: Warm, welcoming, aspirational
Text overlay: Yes — need space on left side for headline
Notes: Avoid dated clothing or event-specific signage
| Document | Purpose |
|---|---|
| LANGUAGE_STYLE_GUIDE.md | Voice & tone for all copy — Required for any user-facing text |
| ../JOBS-TO-BE-DONE.md | User motivations (esp. Job C9: Feel Like I Belong) |
| ../STAKEHOLDER-OVERVIEW.md | High-level features and strategic context |
| ../phases/phase-1/1.9-pre-beta-polish.md | Visual refresh implementation |
| ../phases/phase-4/README.md | Mobile polish planning |
tailwind.config.js |
Color configuration |
| Belmont Brand Guidelines | Official brand guidelines |
This document should evolve as we build. Add patterns that work, remove patterns that don’t.