Migrate design system from ethereal/cool to warm/craft/guild theme

Replace ghost/whisper/sparkle color palettes with guild/candlelight/parchment/ember/earth tokens.
Switch typography from NB Television Pro to Quietism serif. Update all 25 Vue components,
layouts, and pages to new design system. Add circle color tokens, typography scale, prose-guild
class, and warm texture effects. Clean up stale documentation files.
This commit is contained in:
Jennie Robinson Faber 2026-02-24 20:01:11 +00:00
parent d588c49946
commit a62e167876
39 changed files with 1300 additions and 2087 deletions

2
.gitignore vendored
View file

@ -22,5 +22,5 @@ logs
.env
.env.*
!.env.example
/*.md
/docs/
scripts/*.js

367
CLAUDE.md
View file

@ -1,329 +1,94 @@
## 2. Member Features
# CLAUDE.md
### Member Profiles
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
**Core Fields:**
## Project Overview
- Name, pronouns, time zone
- Avatar/photo - choose from ghosts
- Studio/organization affiliation
- Bio (rich text)
- Skills tags (searchable)
- Location (city/region)
- Social links (Mastodon, LinkedIn, etc.)
- **Offering:** What I can contribute
- **Looking For:** What I need support with
Ghost Guild is a membership community platform for game developers exploring cooperative business models. Built with Nuxt 4, Vue 3, MongoDB, and Nuxt UI 4.
**Privacy Controls:**
## Commands
- Public/members-only/private toggle per field
- Opt-in to member directory
### Member Updates/Mini Blog
- Post updates about projects, learnings, questions
- Rich text with image support
- Comments enabled
- Filter by circle or topic tags
## 3. Events System
### Core Features
- RSVP with capacity limits
- Waitlist management
- Add to calendar (.ics download)
- Pre-event discussion threads
- Post-event recordings archive
- Speaker/facilitator profiles
### Member-Proposed Events
**Proposal Flow:**
1. Member submits event idea via form
2. Include: Topic, format, target circle, time commitment
3. Admin quick review (spam check only)
4. Published to "Proposed Events" board
5. Members can express interest (like feature upvote pages)
6. If threshold met (e.g., 5 interested), event is scheduled
7. Proposer gets facilitator support if needed
## 4. Resources Integration
### Consolidating Existing Assets
**Import and organize from:**
- learn.weirdghosts.ca content
- Existing tools and templates
- PA curriculum materials (where appropriate)
- Case studies and examples
**Organization Structure:**
```
Resources/
├── Start Here/
│ ├── Welcome Letter from Jennie & Eileen
│ ├── How Ghost Guild Works
│ └── Solidarity Economics Explained
├── Learning Paths/
│ ├── Community Track → links to learn.weirdghosts.ca
│ ├── Founder Track → practical tools
│ └── Practitioner Track → advanced resources
├── Templates & Tools/
│ ├── Governance/
│ ├── Financial/
│ ├── Operations/
│ └── Legal/
├── Case Studies/
│ └── Member stories and examples
└── External Resources/
└── Curated links and recommendations
```bash
npm run dev # Start dev server at http://localhost:3000
npm run build # Production build
npm run preview # Preview production build
```
### Resource Features
No test framework is currently configured.
- Tag by circle relevance (but accessible to all)
- Download tracking for impact metrics
- Version control for templates
- Comment threads on resources
- "Request a resource" feature
## Architecture
## 5. Peer Support System
### Stack
### Cal.com Integration for 1:1s
- **Framework:** Nuxt 4 (Vue 3 + Nitro server)
- **UI:** Nuxt UI 4 (`@nuxt/ui@^4`) with Tailwind CSS
- **Database:** MongoDB via Mongoose
- **Auth:** JWT magic link (email-only, no passwords)
- **Payments:** Helcim (recurring subscriptions + ticket sales)
- **Email:** Resend
- **Slack:** `@slack/web-api` for member invitations and notifications
- **Images:** Cloudinary
- **Analytics:** Plausible (`ghostguild.org`)
**Setup:**
### Key Directories
- Each member can enable peer support availability
- Set their own hours/frequency
- Cal.com handles scheduling
- Types of sessions:
- Peer support (30 min)
- Co-founder check-in (45 min)
- Practitioner office hours (60 min)
- `app/composables/` — State management via `useState()` (no Pinia/Vuex). Key composables: `useAuth`, `useHelcim`, `useMemberPayment`, `useMemberStatus`
- `app/config/` — Circle definitions (`circles.js`) and contribution tiers (`contributions.js`) used across frontend and forms
- `app/middleware/` — Route guards: `auth.js` (member pages), `admin.js` (admin pages), `coming-soon.global.js` (launch gate)
- `app/layouts/``default`, `admin`, `landing`, `coming-soon`
- `server/api/` — Nitro API routes organized by feature: `auth/`, `events/`, `members/`, `helcim/`, `series/`, `updates/`, `admin/`, `slack/`
- `server/models/` — Mongoose schemas: `Member`, `Event`, `Series`, `Update`
- `server/utils/` — Service integrations: `mongoose.js`, `helcim.js`, `resend.js`, `slack.ts`, `tickets.js`
**Matching System:**
### Domain Model
- Simple questionnaire about current needs
- Suggest 3 potential peers based on:
- Complementary skills/needs
- Time zone compatibility
- Circle alignment (optional)
- Book directly via Cal.com links
Three membership **circles**: Community, Founder, Practitioner — each with different access and context. Five **contribution tiers**: $0, $5, $15, $30, $50/month via Helcim subscriptions.
## 6. Dashboard Design
Member statuses: `pending_payment`, `active`, `suspended`, `cancelled`.
### Personalized Sections
Events support ticketing with circle-specific pricing overrides and can be grouped into Series with bundled passes.
**Welcome Block:**
### Design System
- "Welcome back, [Name]"
- Your circle: [Circle] | Your contribution: $X/month
- Quick stats: Days as member, events attended, peers met
- **Colors:** `guild-*` (warm neutral), `candlelight-*` (amber/gold accent), `parchment-*` (cream surfaces), `ember-*` (rust accent), `earth-*` (brown/ochre) — defined in `app/assets/css/main.css`
- **Circle tokens:** `--color-circle-community`, `--color-circle-founder`, `--color-circle-practitioner` with `-light`, `-dark`, `-bg` variants
- **Typography:** Inter (body), Quietism (display/headers, self-hosted from `public/fonts/`), Ubuntu Mono (code)
- **Theme:** `primary: amber`, `neutral: stone` — configured in `app/app.config.ts`
- **Effects:** `.candlelight-glow`, `.warm-text`, `.ink-grain`, `.paper-texture`, `.woodcut-border`, `.guild-stamp`, `.halftone-texture`, `.dithered-bg`, `.dithered-warm`
- **Content:** `.prose-guild` class for wiki/long-form content with warm palette and Quietism headings
**Community Pulse:**
### Environment
- Recent member updates (mini blog posts)
- Upcoming events this week
- New resources added
- New members to welcome
Copy `.env.example` to `.env`. Required: `MONGODB_URI`, `JWT_SECRET`, `RESEND_API_KEY`, `HELCIM_API_TOKEN`, `SLACK_BOT_TOKEN`. Public vars are prefixed `NUXT_PUBLIC_`. The `NUXT_PUBLIC_COMING_SOON` flag gates access behind a launch page.
**Your Activity:**
## Conventions
- Your upcoming events
- Scheduled peer sessions
- Recent discussions you're in
- Resources you've bookmarked
- All frontend code is plain JavaScript (not TypeScript), using Vue 3 Composition API
- Server utilities auto-imported by Nitro — no explicit imports needed in API routes
- Use `USwitch` (not `UToggle`) — this is the correct Nuxt UI 3+ component name
- No fallback/placeholder data — always use real data
- Follow Nuxt 4 file-based routing conventions for route naming
- Always check Nuxt UI 3 latest documentation on the web when implementing UI components
**Take Action:**
## Product Spec
- Post an update
- Propose an event
- Book a peer session
- Browse resources
- Update profile
The sections below describe planned and in-progress features for reference.
**Impact Metrics:**
### Member Features
- Profiles with privacy controls (public/members-only/private per field)
- Member updates/mini blog with rich text and images
- Peer support system with Cal.com integration for 1:1 scheduling
- Total solidarity spots funded
- Events hosted this month
- Active members this week
- Resources shared
### Events System
- RSVP with capacity limits and waitlist management
- Calendar export (.ics), ticketing, series passes
- Member-proposed events with interest threshold
## 7. Collaborative Tools
### Resources (Planned)
- Learning paths by circle, templates and tools, case studies
- Tag by circle relevance, download tracking, version control
### Etherpad Integration
**Use Cases:**
- Meeting notes templates
- Collaborative resource creation
- Event planning documents
- Shared learning notes
**Implementation:**
- Self-hosted Etherpad instance
- SSO with Ghost Guild accounts
- Auto-save and version history
- Export to multiple formats
- Embed in event pages for notes
### Living Documents
- Community-maintained guides
- Glossaries and definitions
- Frequently asked questions
- Best practices collections
## 8. Technical Infrastructure
### Notification System
**Channels:**
- Email (via Resend)
- In-app notifications
- Slack integration via bot
**Configurable Preferences:**
- Event reminders
- New resources in your area
- Peer session invitations
- Member updates digest
- Community announcements
### Search & Discovery
- Full-text search across:
- Resources
- Member profiles
- Event descriptions
- Member updates
- Filter by circle, tags, date
- Save searches for alerts
### Analytics & Reporting
- Member engagement metrics
- Resource usage stats
- Event attendance patterns
- Contribution distribution
- Circle movement tracking
## 9. Content for Launch
### Essential Content Pieces
1. **Welcome Video** - Jennie & Eileen introduce Ghost Guild
2. **How This Works** - Clear explanation of circles and contributions
3. **Circle Guides** - What to expect in each circle
4. **Solidarity Economics** - Practical examples from gaming
5. **Getting Started Checklist** - First week actions
### Pre-Populated Content
- 10-15 essential resources per circle
- 3-5 upcoming events scheduled
- Sample member updates to show activity
- FAQ based on pre-registration questions
## 10. Launch Strategy
### Soft Launch (Week Before)
- Invite 10-15 friendly testers
- Each from different backgrounds/circles
- Gather feedback on:
- Onboarding flow
- Resource organization
- Event system
- Profile creation
### Launch Week
**Day 1-2:** PA alumni and close network
- Personal invitations
- Extra support available
- Gather testimonials
**Day 3-4:** Gamma Space announcement
- Post in relevant channels
- Host info session
**Day 5-7:** Public launch
- Email pre-registration list
- Social media announcement
- Open registration
### Success Metrics
**Week 1:**
- 30 members across all circles
- 80% complete profiles
- 50% attend first event
**Month 1:**
- 75 active members
- 5 member-proposed events
- 20 peer sessions booked
- 90% Slack participation
## 11. Ongoing Operations
### Weekly Tasks
- Review member proposals for events
- Process Gamma Space channel access
- Update resource library
- Send member spotlight
### Monthly Tasks
- Impact report to members
- Review and adjust contribution distribution
- Plan next month's events
- Gather member feedback
### Quarterly Reviews
- Assess circle definitions
- Evaluate pricing model
- Review platform features
- Plan new initiatives
---
## Implementation Priority Order
### Must Have for Launch
1. Payment processing (Helcim)
2. Basic Slack automation
3. Member dashboard
4. Simple resource library
5. Event listing and RSVP
### Nice to Have for Launch
7. Member profiles
8. Peer matching system
9. Cal.com integration
10. Member updates/blog
### Can Build Post-Launch
11. Etherpad integration
12. Member-proposed events
13. Advanced search
14. Analytics dashboard
15. Monthly themes
### Implementation Priority
**Must have:** Payment processing, Slack automation, member dashboard, resource library, event listing/RSVP
**Nice to have:** Member profiles, peer matching, Cal.com, member updates
**Post-launch:** Etherpad integration, member-proposed events, advanced search, analytics dashboard

View file

@ -1,149 +0,0 @@
# Helcim Payment Flow Fix
## Issue
The initial implementation had a mismatch in the payment flow:
- **Error**: "Customer ID is required" when attempting to purchase event tickets
- **Cause**: The payment initialization endpoint required a `customerId`, but event tickets are one-time purchases that don't need customer accounts
## Solution
### 1. Updated Payment Initialization (`server/api/helcim/initialize-payment.post.js`)
**Changed from:**
- Always requiring `customerId`
- Always using `verify` payment type (for card verification)
- Amount fixed at 0
**Changed to:**
- `customerId` is now optional
- Detects event ticket purchases via `metadata.type === 'event_ticket'`
- Uses `purchase` type for event tickets with amount > 0
- Uses `verify` type for subscription setup (card verification)
```javascript
// For event tickets: HelcimPay.js completes the purchase immediately
const paymentType = isEventTicket && amount > 0 ? 'purchase' : 'verify'
```
### 2. Updated Ticket Purchase Endpoint (`server/api/events/[id]/tickets/purchase.post.js`)
**Changed from:**
- Expecting `paymentToken` from client
- Calling `processHelcimPayment()` to process the payment
- Payment happens server-side after modal closes
**Changed to:**
- Expecting `transactionId` from client
- Payment already completed by HelcimPay.js modal
- Server just records the transaction
**Why?**
When using HelcimPay.js with `purchase` type, the payment is processed inside the modal and we get a completed transaction back. We don't need to make a second API call to charge the card.
### 3. Updated Frontend Component (`app/components/EventTicketPurchase.vue`)
**Changed from:**
- Getting `cardToken` from payment modal
- Sending `paymentToken` to purchase endpoint
**Changed to:**
- Getting `transactionId` from payment modal
- Sending `transactionId` to purchase endpoint
- Added validation to ensure transaction ID exists
## Payment Flow Comparison
### Old Flow (Subscriptions)
```
1. Initialize payment session (verify mode, amount: 0)
2. User enters card in modal
3. Modal returns cardToken
4. Send cardToken to server
5. Server calls Helcim API to charge card
6. Create registration
```
### New Flow (Event Tickets)
```
1. Initialize payment session (purchase mode, amount: actual price)
2. User enters card in modal
3. Helcim charges card immediately
4. Modal returns transactionId
5. Send transactionId to server
6. Server records transaction and creates registration
```
## Benefits
1. **Simpler**: One API call instead of two
2. **Faster**: Payment completes in the modal
3. **More Secure**: No need to handle card tokens server-side
4. **PCI Compliant**: Card data never touches our server
5. **Better UX**: User sees immediate payment confirmation
## Testing
### Free Member Tickets
```bash
# Should work without payment modal
1. Member logs in
2. Views event with member.isFree: true
3. Fills name/email
4. Clicks "Complete Registration"
5. ✓ Registers immediately (no payment)
```
### Paid Public Tickets
```bash
# Should trigger Helcim modal
1. Non-member views event
2. Sees public ticket price
3. Fills name/email
4. Clicks "Pay $XX.XX"
5. Helcim modal opens
6. Enters test card: 4242 4242 4242 4242
7. Payment processes
8. ✓ Modal closes with success
9. ✓ Registration created with transaction ID
```
## Environment Variables
Ensure these are set in `.env`:
```bash
# Public (client-side)
NUXT_PUBLIC_HELCIM_TOKEN=your_helcim_api_token
NUXT_PUBLIC_HELCIM_ACCOUNT_ID=your_account_id
# Private (server-side)
HELCIM_API_TOKEN=your_helcim_api_token
```
## Common Issues
### "Customer ID is required"
- ✅ **Fixed** - This error should no longer occur for event tickets
- If you still see it, check that `metadata.type: 'event_ticket'` is being passed
### "No transaction ID received"
- Check browser console for HelcimPay.js errors
- Verify Helcim credentials are correct
- Ensure test mode is enabled for testing
### Payment modal doesn't open
- Check that HelcimPay.js script loaded (see console)
- Verify `NUXT_PUBLIC_HELCIM_TOKEN` is set
- Check browser console for initialization errors
## Files Changed
1. `server/api/helcim/initialize-payment.post.js` - Smart payment type detection
2. `server/api/events/[id]/tickets/purchase.post.js` - Accept transactionId instead of token
3. `app/components/EventTicketPurchase.vue` - Pass transactionId instead of cardToken
---
**Status**: ✅ Fixed
**Date**: 2025-10-14
**Impact**: Event ticket purchases now work correctly with Helcim

View file

@ -1,120 +0,0 @@
# Helcim Integration Testing Guide
## Setup Complete
The Helcim Recurring API integration has been set up with the following components:
### 1. Composables
- `/app/composables/useHelcim.js` - Server-side Helcim API interactions
- `/app/composables/useHelcimPay.js` - Client-side HelcimPay.js integration
### 2. Server API Endpoints
- `/server/api/helcim/customer.post.js` - Creates Helcim customer and member record
- `/server/api/helcim/subscription.post.js` - Creates subscription for paid tiers
- `/server/api/helcim/verify-payment.post.js` - Verifies payment token
### 3. Updated Pages
- `/app/pages/join.vue` - Multi-step signup flow with payment integration
### 4. Database Schema
- Updated `/server/models/member.js` with subscription fields
## Testing Instructions
### Prerequisites
1. Ensure your `.env` file has the test Helcim token:
```
NUXT_PUBLIC_HELCIM_TOKEN=your_test_token_here
```
2. Ensure you have test payment plans created in Helcim dashboard matching these IDs:
- `supporter-monthly-5`
- `member-monthly-15`
- `advocate-monthly-30`
- `champion-monthly-50`
### Test Flow
#### 1. Start the Development Server
```bash
npm run dev
```
#### 2. Test Free Tier Signup
1. Navigate to `/join`
2. Fill in name and email
3. Select any circle
4. Choose "$0 - I need support right now"
5. Click "Complete Registration"
6. Should go directly to confirmation without payment
#### 3. Test Paid Tier Signup
1. Navigate to `/join`
2. Fill in test details:
- Name: Test User
- Email: test@example.com
3. Select any circle
4. Choose a paid contribution tier (e.g., "$15 - I can sustain the community")
5. Click "Continue to Payment"
6. On the payment step, use Helcim test card numbers:
- **Success**: 4111 1111 1111 1111
- **Decline**: 4000 0000 0000 0002
- CVV: Any 3 digits
- Expiry: Any future date
7. Click "Complete Payment"
8. Should see confirmation with member details
### Test Card Numbers (Helcim Test Mode)
- **Visa Success**: 4111 1111 1111 1111
- **Mastercard Success**: 5500 0000 0000 0004
- **Amex Success**: 3400 0000 0000 009
- **Decline**: 4000 0000 0000 0002
- **Insufficient Funds**: 4000 0000 0000 0051
### Debugging
#### Check API Responses
Open browser DevTools Network tab to monitor:
- `/api/helcim/customer` - Should return customer ID and token
- `/api/helcim/verify-payment` - Should return card details
- `/api/helcim/subscription` - Should return subscription ID
#### Common Issues
1. **HelcimPay.js not loading**
- Check console for script loading errors
- Verify token is correctly set in environment
2. **Customer creation fails**
- Check API token permissions in Helcim dashboard
- Verify MongoDB connection
3. **Payment verification fails**
- Ensure you're using test card numbers
- Check that Helcim account is in test mode
4. **Subscription creation fails**
- Verify payment plan IDs exist in Helcim
- Check that card token was successfully captured
### Database Verification
Check MongoDB for created records:
```javascript
// In MongoDB shell or client
db.members.findOne({ email: "test@example.com" })
```
Should see:
- `helcimCustomerId` populated
- `helcimSubscriptionId` for paid tiers
- `status: "active"` after successful payment
- `paymentMethod: "card"` for paid tiers
## Next Steps
Once testing is successful:
1. Switch to production Helcim token
2. Create production payment plans in Helcim
3. Update plan IDs in `/app/config/contributions.js` if needed
4. Test with real payment card (small amount)
5. Set up webhook endpoints for subscription events (renewals, failures, cancellations)

View file

@ -1,284 +0,0 @@
# Helcim Event Ticketing Integration - Implementation Summary
## Overview
Successfully integrated Helcim payment processing with Ghost Guild's event ticketing system to support paid events, member pricing, early bird discounts, and capacity management.
## What Was Built
### 1. Enhanced Event Model (`server/models/event.js`)
Added comprehensive ticket schema with:
- **Member tickets**: Free/discounted pricing for members with circle-specific overrides
- **Public tickets**: Standard pricing with early bird support
- **Capacity management**: Overall event capacity tracking
- **Waitlist system**: Queue management when events sell out
- **Registration tracking**: Enhanced with ticket type, price paid, payment status, and refund info
### 2. Ticket Business Logic (`server/utils/tickets.js`)
Created utility functions for:
- `calculateTicketPrice()` - Determines applicable price based on member status and early bird
- `checkTicketAvailability()` - Real-time availability checking
- `validateTicketPurchase()` - Pre-purchase validation
- `reserveTicket()` - Temporary reservation during checkout (prevents race conditions)
- `releaseTicket()` - Release abandoned reservations
- `completeTicketPurchase()` - Finalize purchase and update counts
- `addToWaitlist()` - Waitlist management
- `formatPrice()` - Consistent price formatting
### 3. API Endpoints (`server/api/events/[id]/tickets/`)
Four new REST endpoints:
#### `GET available.get.js`
- Check ticket availability and pricing for a user
- Returns: ticket type, price, availability, remaining spots
- Supports both authenticated (members) and public users
#### `POST check-eligibility.post.js`
- Verify if user qualifies for member pricing
- Returns: member status and circle information
#### `POST purchase.post.js`
- Complete ticket purchase with Helcim payment
- Validates availability, processes payment, creates registration
- Handles both free (member) and paid (public) tickets
#### `POST reserve.post.js`
- Temporarily reserve ticket during checkout
- Prevents overselling during payment processing
- 10-minute TTL on reservations
### 4. Frontend Components
#### `EventTicketCard.vue`
Reusable ticket display component showing:
- Ticket name and description
- Price with early bird indicator
- Member savings comparison
- Availability status
- Waitlist option when sold out
#### `EventTicketPurchase.vue`
Main ticket purchase flow component:
- Fetches ticket availability on load
- Displays appropriate ticket card
- Registration form (name, email)
- Integrated Helcim payment for paid tickets
- Success/error handling with toast notifications
- Shows "already registered" state
### 5. Enhanced Composable (`app/composables/useHelcimPay.js`)
Added `initializeTicketPayment()` method:
- Ticket-specific payment initialization
- Includes event metadata for tracking
- Uses email as customer code for one-time purchases
### 6. Updated Event Detail Page (`app/pages/events/[id].vue`)
- Detects if event has tickets enabled
- Shows new ticket system OR legacy registration form
- Maintains backward compatibility
- Handles ticket purchase success/error events
### 7. Enhanced Email Templates (`server/utils/resend.js`)
Updated registration confirmation emails to include:
- Ticket type (Member/Public)
- Amount paid
- Transaction ID
- "Member Benefit" callout for free member tickets
## How It Works
### Free Member Event Flow
```
1. Member views event → Sees "Free for Members" ticket
2. Fills in name/email → Click "Complete Registration"
3. System verifies membership → Creates registration
4. Sends confirmation email → Shows success message
```
### Paid Public Event Flow
```
1. Public user views event → Sees ticket price
2. Fills in name/email → Clicks "Pay $XX.XX"
3. Helcim modal opens → User enters payment info
4. Payment processes → System creates registration
5. Sends confirmation with receipt → Shows success
```
### Early Bird Pricing
```
- Before deadline: Shows early bird price + countdown
- After deadline: Automatically switches to regular price
- Calculated server-side for security
```
## Configuration
### Event Setup
To enable ticketing for an event, set in the event document:
```javascript
{
tickets: {
enabled: true,
currency: "CAD",
member: {
available: true,
isFree: true, // or set price
name: "Member Ticket",
description: "Free for Ghost Guild members"
},
public: {
available: true,
price: 25.00,
quantity: 50, // or null for unlimited
earlyBirdPrice: 20.00,
earlyBirdDeadline: "2025-11-01T00:00:00Z",
name: "Public Ticket"
},
capacity: {
total: 75 // Total capacity across all ticket types
}
}
}
```
### Circle-Specific Pricing Example
```javascript
{
tickets: {
member: {
isFree: false, // Not free by default
price: 15.00, // Default member price
circleOverrides: {
community: {
isFree: true // Free for community circle
},
founder: {
price: 10.00 // Discounted for founders
},
practitioner: {
price: 5.00 // Heavily discounted for practitioners
}
}
}
}
}
```
## Migration Strategy
### Backward Compatibility
- Legacy `pricing` field still supported
- Events without `tickets.enabled` use old registration system
- Existing registrations work with new system
### Converting Events to New System
```javascript
// Old format
{
pricing: {
isFree: false,
publicPrice: 25,
paymentRequired: true
}
}
// New format
{
tickets: {
enabled: true,
member: {
isFree: true
},
public: {
available: true,
price: 25
}
}
}
```
## Testing Checklist
### Member Ticket Flow
- [ ] Member can see free ticket
- [ ] Email pre-fills if logged in
- [ ] Registration completes without payment
- [ ] Confirmation email shows member benefit
- [ ] "Already registered" state shows correctly
### Public Ticket Flow
- [ ] Public user sees correct price
- [ ] Helcim modal opens on submit
- [ ] Payment processes successfully
- [ ] Transaction ID saved to registration
- [ ] Confirmation email includes receipt
### Early Bird Pricing
- [ ] Early bird price shows before deadline
- [ ] Countdown timer displays correctly
- [ ] Regular price shows after deadline
- [ ] Price calculation is server-side
### Capacity Management
- [ ] Ticket count decrements on purchase
- [ ] Sold out message shows when full
- [ ] Waitlist option appears if enabled
- [ ] No overselling (test concurrent purchases)
### Edge Cases
- [ ] Already registered users see status
- [ ] Cancelled events show cancellation message
- [ ] Past events don't allow registration
- [ ] Member-only events gate non-members
- [ ] Payment failures don't create registrations
## Future Enhancements
### Phase 2 Features
1. **Waitlist Notifications**: Auto-email when spots open
2. **Refund Processing**: Handle ticket cancellations with refunds
3. **Ticket Types**: Multiple ticket tiers per event
4. **Group Tickets**: Purchase multiple tickets at once
5. **Promo Codes**: Discount code support
6. **Admin Dashboard**: View sales, export attendee lists
### Phase 3 Features
1. **Recurring Events**: Auto-apply tickets to series
2. **Transfer Tickets**: Allow users to transfer registrations
3. **PDF Tickets**: Generate printable/QR code tickets
4. **Revenue Analytics**: Track ticket sales and revenue
5. **Dynamic Pricing**: Adjust prices based on demand
## Files Created
- `server/utils/tickets.js` (420 lines)
- `server/api/events/[id]/tickets/available.get.js` (150 lines)
- `server/api/events/[id]/tickets/purchase.post.js` (180 lines)
- `server/api/events/[id]/tickets/check-eligibility.post.js` (50 lines)
- `server/api/events/[id]/tickets/reserve.post.js` (80 lines)
- `app/components/EventTicketCard.vue` (195 lines)
- `app/components/EventTicketPurchase.vue` (330 lines)
## Files Modified
- `server/models/event.js` - Enhanced ticket schema + registration fields
- `app/pages/events/[id].vue` - Integrated ticket UI
- `app/composables/useHelcimPay.js` - Added ticket payment method
- `server/utils/resend.js` - Enhanced email with ticket info
## Total Implementation
- **~1,400 lines of code** across 11 files
- **4 new API endpoints**
- **2 new Vue components**
- **10+ utility functions**
- **Fully backward compatible**
## Support
For questions or issues:
- Check Helcim API docs: https://docs.helcim.com
- Review CLAUDE.md for project context
- Test in development with Helcim test credentials
---
**Status**: ✅ Implementation Complete
**Last Updated**: 2025-10-14
**Developer**: Claude (Anthropic)

View file

@ -1,75 +0,0 @@
# Ghost Guild is a Nuxt 4 Site
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

View file

@ -1,400 +0,0 @@
# Series Ticketing Implementation
## Overview
Comprehensive ticketing system for event series that allows users to purchase a single pass for all events in a series, or optionally register for individual events (drop-in model).
## Two Ticketing Models Supported
### 1. Series-Level Ticketing (Default)
**Intent:** Registrants attend all sessions in the series
**Use cases:**
- Workshop series (e.g., 4-week game dev workshop)
- Courses
- Multi-day events
- Any series where commitment to all sessions is expected
**How it works:**
- Purchase ONE series pass that grants access to ALL events
- Single payment covers the entire series
- Automatic registration for all events upon purchase
- User receives single confirmation email with full schedule
### 2. Event-Level Ticketing (Drop-in)
**Intent:** Each event can be attended independently
**Use cases:**
- Recurring meetups (e.g., monthly community calls)
- Tournaments where sessions are standalone
- Drop-in events where flexibility is key
**How it works:**
- Each event has its own ticket configuration
- Users register per event, not for the series
- Can attend just one, some, or all events
- Standard event ticketing applies
## What Was Built
### 1. Data Models
#### New Series Model (`server/models/series.js`)
Dedicated model for managing event series with:
- Series metadata (title, description, type, dates)
- Full ticket configuration (member/public pricing, capacity)
- Circle-specific pricing overrides
- Early bird pricing support
- Waitlist functionality
- Series registrations tracking
- Event registration references
#### Enhanced Event Model
Added fields to `server/models/event.js`:
- `tickets.requiresSeriesTicket` - Flag to indicate series pass is required
- `tickets.seriesTicketReference` - Reference to Series model
- `registrations.ticketType` - Added "series_pass" option
- `registrations.isSeriesTicketHolder` - Boolean flag
- `registrations.seriesTicketId` - Reference to series registration
### 2. Backend Logic
#### Series Ticket Utilities (`server/utils/tickets.js`)
Added 8 new functions:
- `calculateSeriesTicketPrice()` - Pricing with member/circle overrides
- `checkSeriesTicketAvailability()` - Real-time availability checking
- `validateSeriesTicketPurchase()` - Pre-purchase validation
- `reserveSeriesTicket()` - Temporary reservation during checkout
- `releaseSeriesTicket()` - Release abandoned reservations
- `completeSeriesTicketPurchase()` - Finalize purchase and update counts
- `checkUserSeriesPass()` - Verify if user has valid pass
- `registerForAllSeriesEvents()` - Bulk registration across all events
### 3. API Endpoints
#### Series Ticket Endpoints
- `GET /api/series/[id]/tickets/available` - Check pass availability and pricing
- `POST /api/series/[id]/tickets/purchase` - Complete series pass purchase
- `POST /api/series/[id]/tickets/check-eligibility` - Verify member status
- `GET /api/events/[id]/check-series-access` - Verify series pass ownership for event
### 4. Frontend Components
#### EventSeriesTicketCard.vue
Beautiful purple-themed card displaying:
- Series pass name and pricing
- What's included (all events listed)
- Member savings comparison
- Event schedule preview (first 3 events)
- Availability status
- Member benefit callouts
#### SeriesPassPurchase.vue
Complete purchase flow component:
- Loads series pass information
- Registration form (name/email)
- Helcim payment integration for paid passes
- Success/error handling with toast notifications
- Automatic registration for all events
- Email confirmation
#### Updated EventTicketPurchase.vue
Enhanced to detect series pass requirements:
- Checks if event requires series pass
- Shows "Series Pass Required" message with link to series
- Displays "Registered via Series Pass" for pass holders
- Graceful fallback to regular ticketing
### 5. Email Templates
#### Series Pass Confirmation Email
Professional HTML email featuring:
- Purple gradient header
- Full series pass details
- Complete event schedule with dates/times
- Member benefit callout (if applicable)
- Transaction ID (if paid)
- "What's Next" guidance
- Dashboard link
### 6. UI Integration
#### Updated Series Detail Page (`/series/[id].vue`)
- New "Get Your Series Pass" section
- Displays SeriesPassPurchase component
- Shows only if `series.tickets.enabled` is true
- Refreshes data after successful purchase
- User session integration
## Configuration Examples
### Example 1: Free Member Series, Paid Public
```javascript
const series = {
id: "coop-game-dev-2025",
title: "Cooperative Game Development Workshop Series",
type: "workshop_series",
tickets: {
enabled: true,
requiresSeriesTicket: true, // Must buy series pass
allowIndividualEventTickets: false,
currency: "CAD",
member: {
available: true,
isFree: true,
name: "Member Series Pass",
description: "Free for Ghost Guild members"
},
public: {
available: true,
price: 100.00,
earlyBirdPrice: 80.00,
earlyBirdDeadline: "2025-11-01T00:00:00Z",
name: "Public Series Pass",
description: "Access to all 4 workshops"
},
capacity: {
total: 30 // Total capacity across all ticket types
}
}
}
```
### Example 2: Circle-Specific Pricing
```javascript
const series = {
tickets: {
enabled: true,
member: {
isFree: false,
price: 50.00, // Default member price
circleOverrides: {
community: {
isFree: true // Free for community circle
},
founder: {
price: 25.00 // Discounted for founders
},
practitioner: {
price: 10.00 // Heavily discounted
}
}
}
}
}
```
### Example 3: Drop-in Series (Individual Event Tickets)
```javascript
const series = {
id: "monthly-meetup-2025",
title: "Monthly Community Meetup",
type: "recurring_meetup",
tickets: {
enabled: false, // No series-level tickets
requiresSeriesTicket: false,
allowIndividualEventTickets: true
}
}
// Each event in series has its own tickets config
const event = {
series: {
id: "monthly-meetup-2025",
isSeriesEvent: true
},
tickets: {
enabled: true,
member: {
isFree: true
},
public: {
available: true,
price: 10.00
}
}
}
```
## User Flows
### Flow 1: Member Purchases Free Series Pass
1. Member visits series page (`/series/coop-game-dev-2025`)
2. Sees "Free for Members" series pass card
3. Fills in name/email (pre-filled from session)
4. Clicks "Complete Registration"
5. System creates series registration
6. System registers user for all 4 events automatically
7. User receives series pass confirmation email with full schedule
8. Page refreshes showing "You're Registered!" status
### Flow 2: Public User Purchases Paid Series Pass
1. Public user visits series page
2. Sees "$100 Series Pass" (or $80 early bird)
3. Fills in name/email
4. Clicks "Pay $100.00"
5. Helcim payment modal opens
6. User completes payment
7. System processes payment and creates registrations
8. User receives confirmation email with transaction ID
9. Success toast shows "You're registered for all 4 events"
### Flow 3: User with Series Pass Views Individual Event
1. User has series pass for "Coop Game Dev Series"
2. Visits individual event page (`/events/workshop-1`)
3. EventTicketPurchase component checks series access
4. Sees "You're Registered!" with message:
"You have access to this event via your series pass for Cooperative Game Development Workshop Series"
5. No payment or registration required
### Flow 4: User Without Series Pass Views Required Event
1. User visits event that requires series pass
2. EventTicketPurchase component detects requirement
3. Sees purple banner: "Series Pass Required"
4. Message explains event is part of series
5. "View Series & Purchase Pass" button
6. Clicking redirects to series page to buy pass
## Database Schema
### Series Document Structure
```javascript
{
_id: ObjectId,
id: "coop-game-dev-2025", // String identifier
slug: "cooperative-game-development-workshop-series",
title: "Cooperative Game Development Workshop Series",
description: "Learn to build co-op games...",
type: "workshop_series",
isVisible: true,
isActive: true,
startDate: ISODate("2025-11-15"),
endDate: ISODate("2025-12-06"),
totalEvents: 4,
tickets: {
enabled: true,
requiresSeriesTicket: true,
currency: "CAD",
member: { /* ... */ },
public: { /* ... */ },
capacity: { total: 30, reserved: 2 }
},
registrations: [
{
memberId: ObjectId,
name: "Jane Doe",
email: "jane@example.com",
ticketType: "member",
ticketPrice: 0,
paymentStatus: "not_required",
registeredAt: ISODate,
eventRegistrations: [
{ eventId: ObjectId, registrationId: ObjectId },
{ eventId: ObjectId, registrationId: ObjectId }
]
}
],
targetCircles: ["founder", "practitioner"],
createdBy: "admin",
createdAt: ISODate,
updatedAt: ISODate
}
```
## Backward Compatibility
✅ **Fully backward compatible**
- Events without series tickets work as before
- Legacy pricing model still supported
- Existing registrations unaffected
- Series can exist without ticket system enabled
## Testing Checklist
### Series Pass Purchase
- [ ] Member can see free series pass
- [ ] Public user sees correct pricing
- [ ] Early bird pricing displays correctly
- [ ] Helcim payment modal opens for paid passes
- [ ] Payment processes successfully
- [ ] All events get registrations created
- [ ] Confirmation email sends with all events listed
- [ ] Capacity decrements correctly
### Event Access Control
- [ ] User with series pass sees "Registered" on event pages
- [ ] User without pass sees "Series Pass Required" message
- [ ] Link to series page works correctly
- [ ] Individual event tickets disabled when series pass required
### Edge Cases
- [ ] Already registered users see correct message
- [ ] Sold out series shows waitlist option
- [ ] Cancelled events don't allow registration
- [ ] Payment failures don't create registrations
- [ ] Concurrent purchases don't oversell
## Future Enhancements
### Phase 2 (Planned)
- Series pass transfers between users
- Partial series passes ("Any 3 of 5 workshops")
- Upgrade from individual ticket to series pass
- Series pass refunds and cancellations
- Admin dashboard for series pass holders
- Export attendee lists by series
### Phase 3 (Nice to Have)
- Gift series passes
- Group/team series passes
- Installment payment plans
- Series completion certificates
- Recurring series subscriptions
## Files Created/Modified
### New Files (9)
- `server/models/series.js` - Series data model
- `server/api/series/[id]/tickets/available.get.js` - Check availability
- `server/api/series/[id]/tickets/purchase.post.js` - Purchase series pass
- `server/api/series/[id]/tickets/check-eligibility.post.js` - Member check
- `server/api/events/[id]/check-series-access.get.js` - Verify pass ownership
- `app/components/EventSeriesTicketCard.vue` - Series pass display card
- `app/components/SeriesPassPurchase.vue` - Purchase flow component
- `SERIES_TICKETING_IMPLEMENTATION.md` - This documentation
### Modified Files (4)
- `server/models/event.js` - Added series ticket fields
- `server/utils/tickets.js` - Added 8 series ticket functions (~360 lines)
- `server/utils/resend.js` - Added series pass confirmation email
- `app/components/EventTicketPurchase.vue` - Series pass detection
- `app/pages/series/[id].vue` - Integrated purchase UI
### Total Implementation
- **~1,200 lines of new code**
- **9 new files**
- **4 modified files**
- **4 new API endpoints**
- **2 new Vue components**
- **8 new utility functions**
- **1 new data model**
## Support
For questions or issues:
- Review this documentation
- Check existing ticketing docs: `HELCIM_TICKET_INTEGRATION.md`
- Reference project context: `CLAUDE.md`
---
**Status**: ✅ Implementation Complete
**Last Updated**: 2025-10-14
**Developer**: Claude (Anthropic)

View file

@ -1,57 +0,0 @@
# Helcim Integration - Issues Fixed
## Problem
The API was returning 401 Unauthorized when trying to create customers.
## Root Cause
The runtime config wasn't properly accessing the Helcim token in server-side endpoints.
## Solution Applied
### 1. Fixed Runtime Config Access
Updated all server endpoints to:
- Pass the `event` parameter to `useRuntimeConfig(event)`
- Fallback to `process.env.NUXT_PUBLIC_HELCIM_TOKEN` if config doesn't load
### 2. Files Updated
- `/server/api/helcim/customer.post.js`
- `/server/api/helcim/subscription.post.js`
- `/server/api/helcim/verify-payment.post.js`
- `/server/api/helcim/test-connection.get.js`
### 3. Fixed Import Path
Created `/server/config/contributions.js` to re-export the contributions config for server-side imports.
### 4. Verified Token Works
Created `test-helcim-direct.js` which successfully:
- Connected to Helcim API
- Created a test customer (ID: 32854583, Code: CST1000)
## Testing Instructions
1. Restart your development server:
```bash
npm run dev
```
2. Test the connection:
```bash
curl http://localhost:3000/api/helcim/test-connection
```
3. Try the signup flow at `/join`
## Important Notes
- The token in your `.env` file is working correctly
- The Helcim API is accessible and responding
- Customer creation is functional when called directly
- The issue was specifically with how Nuxt's runtime config was being accessed in server endpoints
## Next Steps
Once you confirm the signup flow works:
1. Test with different contribution tiers
2. Verify payment capture with test cards
3. Check that subscriptions are created correctly
4. Consider adding webhook endpoints for subscription events

View file

@ -1,7 +1,7 @@
export default defineAppConfig({
ui: {
colors: {
primary: "emerald",
primary: "amber",
neutral: "stone",
},
formField: {

View file

@ -1,2 +1,43 @@
/* Font declarations are now handled by @nuxt/fonts module */
/* See nuxt.config.ts for font configuration */
/*
* Font declarations for Ghost Guild
*
* Quietism: Display/heading font (serif)
* Place woff2 files in public/fonts/
* Expected files: Quietism-Regular.woff2, Quietism-Medium.woff2,
* Quietism-Bold.woff2, Quietism-Italic.woff2
*
* Inter and Ubuntu Mono are loaded via @nuxt/fonts module.
* See nuxt.config.ts for configuration.
*/
@font-face {
font-family: "Quietism";
src: url("/fonts/Quietism-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Quietism";
src: url("/fonts/Quietism-Medium.woff2") format("woff2");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Quietism";
src: url("/fonts/Quietism-Bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Quietism";
src: url("/fonts/Quietism-Italic.woff2") format("woff2");
font-weight: 400;
font-style: italic;
font-display: swap;
}

View file

@ -1,3 +1,43 @@
/*
* Ghost Guild Design System
* ========================
*
* Token naming convention:
* --color-{palette}-{shade} Color palettes (50-950 scale)
* --color-circle-{name}-{variant} Circle-specific tokens
* --font-{role} Font family tokens
* --text-{scale} Typographic scale
* --leading-{role} Line heights
* --tracking-{role} Letter spacing
*
* Palettes:
* guild-* Warm neutral ground (dark hall / workshop)
* candlelight-* Primary warm accent (amber/gold/ochre)
* parchment-* Light content surfaces (cream/vellum)
* ember-* Secondary warm accent (deeper amber/rust)
* earth-* Tertiary muted tones (brown/ochre)
*
* Usage guidelines:
* Backgrounds: guild-900/950 (dark), parchment-50/100 (light surfaces)
* Text primary: guild-100 (dark mode), guild-900 (light mode)
* Text secondary: guild-300/400
* Borders: guild-700 (dark), guild-300 (light)
* Accents: candlelight-* for links, highlights, interactive
* Surfaces: parchment-* for readable content areas
*
* Effect classes:
* .ink-grain Subtle noise overlay (mix-blend-mode: overlay)
* .paper-texture Paper fiber overlay
* .woodcut-border Irregular line border via border-image
* .candlelight-glow Warm box-shadow (replaces .ethereal-glow)
* .warm-text Warm text-shadow (replaces .ethereal-text)
* .guild-stamp Circular decorative double-border frame
* .halftone-texture Dot pattern overlay (updated for warm palette)
* .dithered-bg Cross-hatch dither pattern
* .dithered-warm Amber-tinted dither variant
* .prose-guild Wiki/long-form content styling
*/
@import "./fonts.css";
@import "tailwindcss";
@import "@nuxt/ui";
@ -9,132 +49,229 @@
--font-sans: "Inter", sans-serif;
--font-body: "Inter", sans-serif;
--font-mono: "Ubuntu Mono", monospace;
--font-display: "NB Television Pro", monospace;
--font-display: "Quietism", serif;
--font-serif: "Quietism", serif;
/* Ethereal color palette - light mode (inverted for light backgrounds) */
--color-ghost-50: #0a0a0a;
--color-ghost-100: #1a1a1a;
--color-ghost-200: #2a2a2a;
--color-ghost-300: #3a3a3a;
--color-ghost-400: #4a4a4a;
--color-ghost-500: #6a6a6a;
--color-ghost-600: #8a8a8a;
--color-ghost-700: #b0b0b0;
--color-ghost-800: #d0d0d0;
--color-ghost-900: #f0f0f0;
/* Guild - warm neutral ground (light mode: dark values for text on light bg) */
--color-guild-50: #1a1510;
--color-guild-100: #2a241c;
--color-guild-200: #3a332a;
--color-guild-300: #524939;
--color-guild-400: #6b5f4d;
--color-guild-500: #8a7c68;
--color-guild-600: #a49585;
--color-guild-700: #bfb3a2;
--color-guild-800: #d9d0c3;
--color-guild-900: #f0ebe4;
--color-guild-950: #f8f5f0;
/* Subtle accent - barely visible blue-gray (light mode) */
--color-whisper-50: #0f1419;
--color-whisper-100: #1a1f2e;
--color-whisper-200: #252d40;
--color-whisper-300: #2f3b52;
--color-whisper-400: #3a4964;
--color-whisper-500: #4f5d7a;
--color-whisper-600: #687291;
--color-whisper-700: #8491a8;
--color-whisper-800: #a8b3c7;
--color-whisper-900: #d4dae6;
/* Candlelight - primary warm accent (amber/gold/ochre) */
--color-candlelight-50: #1f1708;
--color-candlelight-100: #3d2c0f;
--color-candlelight-200: #5c4118;
--color-candlelight-300: #7b5822;
--color-candlelight-400: #9a6f2c;
--color-candlelight-500: #b8873a;
--color-candlelight-600: #d09e4e;
--color-candlelight-700: #e0b86e;
--color-candlelight-800: #ecd09a;
--color-candlelight-900: #f5e6c5;
--color-candlelight-950: #faf2e0;
/* Sparkle accent (light mode) */
--color-sparkle-50: #202020;
--color-sparkle-100: #404040;
--color-sparkle-200: #606060;
--color-sparkle-300: #808080;
--color-sparkle-400: #a0a0a0;
--color-sparkle-500: #c0c0c0;
--color-sparkle-600: #d0d0d0;
--color-sparkle-700: #e8e8e8;
--color-sparkle-800: #f0f0f0;
--color-sparkle-900: #fafafa;
/* Parchment - light content surfaces (cream/vellum) */
--color-parchment-50: #1d1a14;
--color-parchment-100: #332e24;
--color-parchment-200: #4a4234;
--color-parchment-300: #635844;
--color-parchment-400: #7d6e56;
--color-parchment-500: #97866c;
--color-parchment-600: #afa088;
--color-parchment-700: #c8baa5;
--color-parchment-800: #ddd3c3;
--color-parchment-900: #f0ebe0;
--color-parchment-950: #f9f6f0;
/* Ember - secondary warm accent (deeper amber/rust) */
--color-ember-50: #1e120b;
--color-ember-100: #3b2215;
--color-ember-200: #58321f;
--color-ember-300: #76432a;
--color-ember-400: #945535;
--color-ember-500: #b26840;
--color-ember-600: #c87e55;
--color-ember-700: #da9a72;
--color-ember-800: #e8b899;
--color-ember-900: #f3d6c0;
--color-ember-950: #f9ebe0;
/* Earth - tertiary muted tones (brown/ochre) */
--color-earth-50: #17140e;
--color-earth-100: #2d271c;
--color-earth-200: #433a2a;
--color-earth-300: #5a4d39;
--color-earth-400: #726148;
--color-earth-500: #8a7658;
--color-earth-600: #a08c6d;
--color-earth-700: #b7a487;
--color-earth-800: #cebda4;
--color-earth-900: #e5d7c2;
--color-earth-950: #f2ece1;
}
.dark {
/* Ethereal color palette - dark mode (original values) */
--color-ghost-50: #f0f0f0;
--color-ghost-100: #d0d0d0;
--color-ghost-200: #b0b0b0;
--color-ghost-300: #8a8a8a;
--color-ghost-400: #6a6a6a;
--color-ghost-500: #4a4a4a;
--color-ghost-600: #3a3a3a;
--color-ghost-700: #2a2a2a;
--color-ghost-800: #1a1a1a;
--color-ghost-900: #0a0a0a;
/* Guild - warm neutral (dark mode: light values) */
--color-guild-50: #f0ebe4;
--color-guild-100: #d9d0c3;
--color-guild-200: #bfb3a2;
--color-guild-300: #a49585;
--color-guild-400: #8a7c68;
--color-guild-500: #6b5f4d;
--color-guild-600: #524939;
--color-guild-700: #3a332a;
--color-guild-800: #2a241c;
--color-guild-900: #1a1510;
--color-guild-950: #110e0a;
/* Subtle accent - barely visible blue-gray (dark mode) */
--color-whisper-50: #d4dae6;
--color-whisper-100: #a8b3c7;
--color-whisper-200: #8491a8;
--color-whisper-300: #687291;
--color-whisper-400: #4f5d7a;
--color-whisper-500: #3a4964;
--color-whisper-600: #2f3b52;
--color-whisper-700: #252d40;
--color-whisper-800: #1a1f2e;
--color-whisper-900: #0f1419;
/* Candlelight - primary warm accent (dark mode) */
--color-candlelight-50: #faf2e0;
--color-candlelight-100: #f5e6c5;
--color-candlelight-200: #ecd09a;
--color-candlelight-300: #e0b86e;
--color-candlelight-400: #d09e4e;
--color-candlelight-500: #b8873a;
--color-candlelight-600: #9a6f2c;
--color-candlelight-700: #7b5822;
--color-candlelight-800: #5c4118;
--color-candlelight-900: #3d2c0f;
--color-candlelight-950: #1f1708;
/* Sparkle accent (dark mode) */
--color-sparkle-50: #fafafa;
--color-sparkle-100: #f0f0f0;
--color-sparkle-200: #e8e8e8;
--color-sparkle-300: #d0d0d0;
--color-sparkle-400: #c0c0c0;
--color-sparkle-500: #a0a0a0;
--color-sparkle-600: #808080;
--color-sparkle-700: #606060;
--color-sparkle-800: #404040;
--color-sparkle-900: #202020;
/* Parchment - light content surfaces (dark mode) */
--color-parchment-50: #f9f6f0;
--color-parchment-100: #f0ebe0;
--color-parchment-200: #ddd3c3;
--color-parchment-300: #c8baa5;
--color-parchment-400: #afa088;
--color-parchment-500: #97866c;
--color-parchment-600: #7d6e56;
--color-parchment-700: #635844;
--color-parchment-800: #4a4234;
--color-parchment-900: #332e24;
--color-parchment-950: #1d1a14;
/* Ember - secondary warm accent (dark mode) */
--color-ember-50: #f9ebe0;
--color-ember-100: #f3d6c0;
--color-ember-200: #e8b899;
--color-ember-300: #da9a72;
--color-ember-400: #c87e55;
--color-ember-500: #b26840;
--color-ember-600: #945535;
--color-ember-700: #76432a;
--color-ember-800: #58321f;
--color-ember-900: #3b2215;
--color-ember-950: #1e120b;
/* Earth - tertiary muted tones (dark mode) */
--color-earth-50: #f2ece1;
--color-earth-100: #e5d7c2;
--color-earth-200: #cebda4;
--color-earth-300: #b7a487;
--color-earth-400: #a08c6d;
--color-earth-500: #8a7658;
--color-earth-600: #726148;
--color-earth-700: #5a4d39;
--color-earth-800: #433a2a;
--color-earth-900: #2d271c;
--color-earth-950: #17140e;
}
/* Global ethereal background - light mode */
/* Circle-specific tokens */
:root {
--ethereal-bg:
/* Community - warm hearth amber */
--color-circle-community: #b8873a;
--color-circle-community-light: #e0b86e;
--color-circle-community-dark: #7b5822;
--color-circle-community-bg: rgba(184, 135, 58, 0.1);
/* Founder - forge copper/orange */
--color-circle-founder: #b26840;
--color-circle-founder-light: #da9a72;
--color-circle-founder-dark: #76432a;
--color-circle-founder-bg: rgba(178, 104, 64, 0.1);
/* Practitioner - deep gold/ochre */
--color-circle-practitioner: #8a7658;
--color-circle-practitioner-light: #b7a487;
--color-circle-practitioner-dark: #5a4d39;
--color-circle-practitioner-bg: rgba(138, 118, 88, 0.1);
/* Typographic scale */
--text-display-xl: 3.5rem;
--text-display-lg: 2.5rem;
--text-display: 2rem;
--text-display-sm: 1.5rem;
--text-body-lg: 1.125rem;
--text-body: 1rem;
--text-body-sm: 0.875rem;
--text-caption: 0.75rem;
--text-overline: 0.6875rem;
/* Line heights */
--leading-display: 1.15;
--leading-body: 1.65;
/* Letter spacing */
--tracking-display: -0.02em;
--tracking-wide: 0.05em;
/* Ambient background - warm radial gradients */
--ambient-bg:
radial-gradient(
circle at 20% 80%,
rgba(40, 40, 40, 0.03) 0%,
rgba(184, 135, 58, 0.03) 0%,
transparent 50%
),
radial-gradient(
circle at 80% 20%,
rgba(40, 40, 40, 0.02) 0%,
rgba(178, 104, 64, 0.02) 0%,
transparent 50%
),
radial-gradient(
circle at 40% 40%,
rgba(40, 40, 40, 0.01) 0%,
rgba(138, 118, 88, 0.01) 0%,
transparent 50%
);
--halftone-pattern: radial-gradient(
circle,
rgba(0, 0, 0, 0.1) 1px,
rgba(42, 36, 28, 0.1) 1px,
transparent 1px
);
--halftone-size: 8px 8px;
}
/* Dark mode background */
/* Dark mode */
.dark:root {
--ethereal-bg:
--ambient-bg:
radial-gradient(
circle at 20% 80%,
rgba(232, 232, 232, 0.03) 0%,
rgba(224, 184, 110, 0.03) 0%,
transparent 50%
),
radial-gradient(
circle at 80% 20%,
rgba(232, 232, 232, 0.02) 0%,
rgba(218, 154, 114, 0.02) 0%,
transparent 50%
),
radial-gradient(
circle at 40% 40%,
rgba(232, 232, 232, 0.01) 0%,
rgba(183, 164, 135, 0.01) 0%,
transparent 50%
);
--halftone-pattern: radial-gradient(
circle,
rgba(255, 255, 255, 0.1) 1px,
rgba(240, 235, 228, 0.1) 1px,
transparent 1px
);
}
@ -144,12 +281,41 @@ html {
}
body {
background: var(--ethereal-bg), #f0f0f0;
background: var(--ambient-bg), #f0ebe4;
background-attachment: fixed;
}
.dark body {
background: var(--ethereal-bg), #0a0a0a;
background: var(--ambient-bg), #1a1510;
}
/* Display typography utilities */
.text-display-xl {
font-family: var(--font-display);
font-size: var(--text-display-xl);
line-height: var(--leading-display);
letter-spacing: var(--tracking-display);
}
.text-display-lg {
font-family: var(--font-display);
font-size: var(--text-display-lg);
line-height: var(--leading-display);
letter-spacing: var(--tracking-display);
}
.text-display {
font-family: var(--font-display);
font-size: var(--text-display);
line-height: var(--leading-display);
letter-spacing: var(--tracking-display);
}
.text-display-sm {
font-family: var(--font-display);
font-size: var(--text-display-sm);
line-height: var(--leading-display);
letter-spacing: var(--tracking-display);
}
/* Halftone texture overlay */
@ -170,106 +336,93 @@ body {
pointer-events: none;
}
/* Sparkle effects */
@keyframes sparkle {
0%,
100% {
opacity: 0.3;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1.2);
}
/* Craft/Material Effects */
/* Warm box-shadow (replaces .ethereal-glow) */
.candlelight-glow {
box-shadow:
0 0 20px rgba(184, 135, 58, 0.12),
0 0 40px rgba(184, 135, 58, 0.06),
inset 0 1px 0 rgba(240, 235, 228, 0.08);
}
@keyframes twinkle {
0%,
100% {
opacity: 0.2;
}
25% {
opacity: 0.8;
}
75% {
opacity: 0.4;
}
/* Warm text-shadow (replaces .ethereal-text) */
.warm-text {
text-shadow: 0 0 10px rgba(224, 184, 110, 0.25);
}
.sparkle-field {
/* Subtle noise overlay */
.ink-grain {
position: relative;
overflow: hidden;
}
.sparkle-field::after {
.ink-grain::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
radial-gradient(
circle at 10% 20%,
var(--color-sparkle-200) 1px,
transparent 1px
),
radial-gradient(
circle at 90% 80%,
var(--color-sparkle-400) 1px,
transparent 1px
),
radial-gradient(
circle at 30% 70%,
var(--color-sparkle-200) 0.5px,
transparent 0.5px
),
radial-gradient(
circle at 70% 30%,
var(--color-sparkle-400) 0.5px,
transparent 0.5px
),
radial-gradient(
circle at 50% 10%,
var(--color-sparkle-200) 1px,
transparent 1px
),
radial-gradient(
circle at 20% 90%,
var(--color-sparkle-400) 0.5px,
transparent 0.5px
);
background-size:
200px 200px,
300px 300px,
150px 150px,
250px 250px,
180px 180px,
220px 220px;
animation: twinkle 4s infinite ease-in-out;
background-image: url("/textures/grain.png");
background-repeat: repeat;
background-size: 128px 128px;
mix-blend-mode: overlay;
opacity: 0.04;
pointer-events: none;
opacity: 0.6;
}
/* Ethereal glow effects */
.ethereal-glow {
box-shadow:
0 0 20px rgba(232, 232, 232, 0.1),
0 0 40px rgba(232, 232, 232, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
/* Paper fiber overlay */
.paper-texture {
position: relative;
}
.ethereal-text {
text-shadow: 0 0 10px rgba(232, 232, 232, 0.3);
.paper-texture::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: url("/textures/paper.png");
background-repeat: repeat;
background-size: 256px 256px;
opacity: 0.04;
pointer-events: none;
}
/* Irregular line border */
.woodcut-border {
border-image: repeating-linear-gradient(
90deg,
var(--color-guild-700) 0px,
var(--color-guild-600) 2px,
var(--color-guild-700) 3px,
transparent 3px,
transparent 6px
) 4;
}
/* Circular decorative frame */
.guild-stamp {
border: 2px solid var(--color-candlelight-600);
outline: 2px solid var(--color-candlelight-700);
outline-offset: 3px;
border-radius: 50%;
}
/* Warm interior feel for content areas */
.guild-interior {
background: var(--ambient-bg);
box-shadow: inset 0 2px 8px rgba(26, 21, 16, 0.06);
}
/* Dithered gradients */
.dithered-bg {
background:
linear-gradient(45deg, var(--color-ghost-800) 25%, transparent 25%),
linear-gradient(-45deg, var(--color-ghost-800) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--color-ghost-700) 75%),
linear-gradient(-45deg, transparent 75%, var(--color-ghost-700) 75%);
linear-gradient(45deg, var(--color-guild-800) 25%, transparent 25%),
linear-gradient(-45deg, var(--color-guild-800) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--color-guild-700) 75%),
linear-gradient(-45deg, transparent 75%, var(--color-guild-700) 75%);
background-size: 4px 4px;
background-position:
0 0,
@ -278,6 +431,58 @@ body {
-2px 0px;
}
/* Amber-tinted dither variant */
.dithered-warm {
background:
linear-gradient(45deg, var(--color-candlelight-800) 25%, transparent 25%),
linear-gradient(-45deg, var(--color-candlelight-800) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--color-candlelight-700) 75%),
linear-gradient(-45deg, transparent 75%, var(--color-candlelight-700) 75%);
background-size: 4px 4px;
background-position:
0 0,
0 2px,
2px -2px,
-2px 0px;
}
/* Prose overrides for guild wiki/long-form content */
.prose-guild {
max-width: 72ch;
}
.prose-guild h1,
.prose-guild h2,
.prose-guild h3,
.prose-guild h4 {
font-family: var(--font-display);
letter-spacing: var(--tracking-display);
}
.prose-guild a {
color: var(--color-candlelight-600);
text-decoration-color: var(--color-candlelight-700);
text-underline-offset: 3px;
}
.prose-guild a:hover {
color: var(--color-candlelight-500);
text-decoration-color: var(--color-candlelight-500);
}
.prose-guild blockquote {
background-color: var(--color-parchment-900);
border-left-color: var(--color-candlelight-600);
border-left-width: 3px;
padding: 1rem 1.5rem;
border-radius: 0 0.5rem 0.5rem 0;
}
.dark .prose-guild blockquote {
background-color: var(--color-parchment-900);
border-left-color: var(--color-candlelight-500);
}
/* Mobile responsive utilities */
@media (max-width: 1023px) {
/* Prevent horizontal scroll on mobile */

View file

@ -7,7 +7,7 @@
>
<!-- Left: Copyright and minimal info -->
<div>
<p class="text-ghost-500 text-xs mb-2">
<p class="text-guild-500 text-xs mb-2">
© {{ currentYear }} Ghost Guild
</p>
</div>
@ -16,7 +16,7 @@
<div class="flex flex-wrap gap-6 text-xs">
<a
href="mailto:hello@ghostguild.org"
class="text-ghost-500 hover:text-ghost-300 transition-colors"
class="text-guild-500 hover:text-guild-300 transition-colors"
>
Contact
</a>

View file

@ -3,13 +3,13 @@
:class="[
isMobile
? 'w-full flex flex-col bg-transparent'
: 'w-64 lg:w-80 backdrop-blur-sm h-screen sticky top-0 flex flex-col bg-ghost-900 border-r border-ghost-700',
: 'w-64 lg:w-80 backdrop-blur-sm h-screen sticky top-0 flex flex-col bg-guild-900 border-r border-guild-700',
]"
>
<!-- Logo/Brand at top (desktop only) -->
<div v-if="!isMobile" class="p-8 border-b border-ghost-700 bg-primary-500">
<div v-if="!isMobile" class="p-8 border-b border-guild-700 bg-primary-500">
<NuxtLink to="/" class="flex flex-col items-center gap-3 group">
<span class="text-xl font-bold text-white ethereal-text tracking-wider"
<span class="text-xl font-bold text-white warm-text tracking-wider"
>Ghost Guild Logo</span
>
</NuxtLink>
@ -31,8 +31,8 @@
<!-- Hover indicator -->
<span
class="text-ghost-200 hover:text-ghost-100 transition-all duration-300 text-lg tracking-wide block py-2 hover:ethereal-text"
active-class="text-ghost-100 ethereal-text translate-x-2"
class="text-guild-200 hover:text-guild-100 transition-all duration-300 text-lg tracking-wide block py-2 hover:warm-text"
active-class="text-guild-100 warm-text translate-x-2"
>
{{ item.label }}
</span>
@ -41,12 +41,12 @@
</ul>
<!-- Contact Email -->
<div class="mt-8 pt-6 border-t border-ghost-800/50">
<p class="text-ghost-500 text-xs">
<div class="mt-8 pt-6 border-t border-guild-800/50">
<p class="text-guild-500 text-xs">
Contact us:
<a
href="mailto:hello@ghostguild.org"
class="text-ghost-400 hover:text-ghost-300 transition-colors"
class="text-guild-400 hover:text-guild-300 transition-colors"
>hello@ghostguild.org</a
>
</p>
@ -61,30 +61,30 @@
<div
:class="
isMobile
? 'mt-8 pt-6 border-t border-ghost-800/50'
: 'mt-12 pt-8 border-t border-ghost-800/50'
? 'mt-8 pt-6 border-t border-guild-800/50'
: 'mt-12 pt-8 border-t border-guild-800/50'
"
>
<div v-if="isAuthenticated" class="space-y-4">
<NuxtLink
to="/member/dashboard"
class="block text-ghost-300 hover:text-ghost-100 hover:ethereal-text transition-all duration-300 py-2"
class="block text-guild-300 hover:text-guild-100 hover:warm-text transition-all duration-300 py-2"
@click="handleNavigate"
>
<span class="block text-sm text-whisper-400 mb-1">{{
<span class="block text-sm text-candlelight-400 mb-1">{{
memberData?.name || "Member"
}}</span>
Dashboard
</NuxtLink>
<button
@click="handleLogout"
class="text-ghost-500 hover:text-ghost-300 transition-all duration-300 text-sm"
class="text-guild-500 hover:text-guild-300 transition-all duration-300 text-sm"
>
Logout
</button>
</div>
<div v-else class="space-y-4">
<p class="text-ghost-400 text-sm mb-4">
<p class="text-guild-400 text-sm mb-4">
Enter your email to receive a login link
</p>

View file

@ -1,18 +1,18 @@
<template>
<div
class="series-badge p-4 bg-ghost-800/50 dark:bg-ghost-700/30 rounded-xl border border-ghost-600 dark:border-ghost-600"
class="series-badge p-4 bg-guild-800/50 dark:bg-guild-700/30 rounded-xl border border-guild-600 dark:border-guild-600"
>
<div class="flex items-start justify-between gap-6">
<div class="flex-1 min-w-0">
<div class="flex flex-wrap items-center gap-2 mb-2">
<span
class="series-badge__label text-sm font-semibold text-ghost-300 dark:text-ghost-300"
class="series-badge__label text-sm font-semibold text-guild-300 dark:text-guild-300"
>
Part of a Series
</span>
<span
v-if="totalEvents"
class="series-badge__count inline-flex items-center px-2 py-0.5 rounded-md bg-ghost-700/50 dark:bg-ghost-600/50 text-sm font-medium text-ghost-200 dark:text-ghost-200"
class="series-badge__count inline-flex items-center px-2 py-0.5 rounded-md bg-guild-700/50 dark:bg-guild-600/50 text-sm font-medium text-guild-200 dark:text-guild-200"
>
<template v-if="position">
Event {{ position }} of {{ totalEvents }}
@ -21,13 +21,13 @@
</span>
</div>
<h3
class="series-badge__title text-lg font-semibold text-ghost-100 dark:text-ghost-100 mb-2"
class="series-badge__title text-lg font-semibold text-guild-100 dark:text-guild-100 mb-2"
>
{{ title }}
</h3>
<p
v-if="description"
class="series-badge__description text-sm text-ghost-300 dark:text-ghost-300"
class="series-badge__description text-sm text-guild-300 dark:text-guild-300"
>
{{ description }}
</p>

View file

@ -1,6 +1,6 @@
<template>
<div
class="series-ticket-card border border-ghost-600 dark:border-ghost-600 rounded-xl overflow-hidden"
class="series-ticket-card border border-guild-600 dark:border-guild-600 rounded-xl overflow-hidden"
>
<!-- Header -->
<div
@ -39,27 +39,27 @@
</div>
<!-- Body -->
<div class="p-6 bg-ghost-800/50 dark:bg-ghost-700/30">
<div class="p-6 bg-guild-800/50 dark:bg-guild-700/30">
<!-- What's Included -->
<div class="mb-6">
<h4 class="text-sm font-semibold text-ghost-200 dark:text-ghost-200 mb-3 uppercase tracking-wide">
<h4 class="text-sm font-semibold text-guild-200 dark:text-guild-200 mb-3 uppercase tracking-wide">
What's Included
</h4>
<div class="space-y-2">
<div class="flex items-center gap-2 text-ghost-300 dark:text-ghost-300">
<div class="flex items-center gap-2 text-guild-300 dark:text-guild-300">
<Icon name="heroicons:check-circle" class="w-5 h-5 text-green-400" />
<span>Access to all {{ totalEvents }} events in the series</span>
</div>
<div
v-if="ticket.isFree && !isMember"
class="flex items-center gap-2 text-ghost-300 dark:text-ghost-300"
class="flex items-center gap-2 text-guild-300 dark:text-guild-300"
>
<Icon name="heroicons:check-circle" class="w-5 h-5 text-green-400" />
<span>Automatic registration for all sessions</span>
</div>
<div
v-if="memberSavings > 0"
class="flex items-center gap-2 text-ghost-300 dark:text-ghost-300"
class="flex items-center gap-2 text-guild-300 dark:text-guild-300"
>
<Icon name="heroicons:check-circle" class="w-5 h-5 text-green-400" />
<span>Save {{ formatPrice(memberSavings, ticket.currency) }} as a member</span>
@ -69,14 +69,14 @@
<!-- Events List Preview -->
<div v-if="events && events.length > 0" class="mb-6">
<h4 class="text-sm font-semibold text-ghost-200 dark:text-ghost-200 mb-3 uppercase tracking-wide">
<h4 class="text-sm font-semibold text-guild-200 dark:text-guild-200 mb-3 uppercase tracking-wide">
Series Schedule
</h4>
<div class="space-y-2">
<div
v-for="(event, index) in events.slice(0, 3)"
:key="event.id"
class="flex items-start gap-3 p-3 bg-ghost-700/50 dark:bg-ghost-600/30 rounded-lg"
class="flex items-start gap-3 p-3 bg-guild-700/50 dark:bg-guild-600/30 rounded-lg"
>
<div
class="w-8 h-8 rounded-full bg-purple-600/20 border border-purple-500/30 flex items-center justify-center flex-shrink-0"
@ -84,17 +84,17 @@
<span class="text-sm font-bold text-purple-300">{{ index + 1 }}</span>
</div>
<div class="flex-1 min-w-0">
<div class="font-medium text-ghost-100 dark:text-ghost-100 text-sm">
<div class="font-medium text-guild-100 dark:text-guild-100 text-sm">
{{ event.title }}
</div>
<div class="text-xs text-ghost-400 dark:text-ghost-400 mt-1">
<div class="text-xs text-guild-400 dark:text-guild-400 mt-1">
{{ formatEventDate(event.startDate) }}
</div>
</div>
</div>
<div
v-if="events.length > 3"
class="text-center text-sm text-ghost-400 dark:text-ghost-400 pt-2"
class="text-center text-sm text-guild-400 dark:text-guild-400 pt-2"
>
+ {{ events.length - 3 }} more event{{ events.length - 3 !== 1 ? 's' : '' }}
</div>

View file

@ -4,7 +4,7 @@
:class="[
isSelected
? 'border-primary bg-primary/5'
: 'border-ghost-600 bg-ghost-800/50',
: 'border-guild-600 bg-guild-800/50',
isAvailable && !alreadyRegistered
? 'hover:border-primary/50 cursor-pointer'
: 'opacity-60 cursor-not-allowed',
@ -14,10 +14,10 @@
<!-- Ticket Header -->
<div class="flex items-start justify-between mb-4">
<div>
<h3 class="text-lg font-semibold text-ghost-100">
<h3 class="text-lg font-semibold text-guild-100">
{{ ticketInfo.name }}
</h3>
<p v-if="ticketInfo.description" class="text-sm text-ghost-300 mt-1">
<p v-if="ticketInfo.description" class="text-sm text-guild-300 mt-1">
{{ ticketInfo.description }}
</p>
</div>
@ -37,7 +37,7 @@
<div class="flex items-baseline gap-2">
<span
class="text-3xl font-bold"
:class="ticketInfo.isFree ? 'text-green-400' : 'text-ghost-100'"
:class="ticketInfo.isFree ? 'text-green-400' : 'text-guild-100'"
>
{{ ticketInfo.formattedPrice }}
</span>
@ -56,7 +56,7 @@
v-if="ticketInfo.isEarlyBird && ticketInfo.formattedRegularPrice"
class="mt-1"
>
<span class="text-sm text-ghost-400 line-through">
<span class="text-sm text-guild-400 line-through">
Regular: {{ ticketInfo.formattedRegularPrice }}
</span>
</div>
@ -80,7 +80,7 @@
<Icon name="heroicons:check-circle" class="w-4 h-4 inline mr-1" />
You save {{ formatPrice(ticketInfo.memberSavings) }} as a member!
</p>
<p class="text-xs text-ghost-400 mt-1">
<p class="text-xs text-guild-400 mt-1">
Public price: {{ ticketInfo.publicTicket.formattedPrice }}
</p>
</div>
@ -102,10 +102,10 @@
<Icon name="heroicons:x-circle-solid" class="w-4 h-4" />
Sold Out
</span>
<span v-else-if="ticketInfo.remaining !== null" class="text-ghost-300">
<span v-else-if="ticketInfo.remaining !== null" class="text-guild-300">
{{ ticketInfo.remaining }} remaining
</span>
<span v-else class="text-ghost-300"> Unlimited availability </span>
<span v-else class="text-guild-300"> Unlimited availability </span>
</div>
<!-- Selection Indicator -->
@ -117,7 +117,7 @@
<!-- Waitlist Option -->
<div
v-if="!isAvailable && ticketInfo.waitlistAvailable && !alreadyRegistered"
class="mt-4 pt-4 border-t border-ghost-600"
class="mt-4 pt-4 border-t border-guild-600"
>
<UButton
color="gray"

View file

@ -5,7 +5,7 @@
<div
class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"
></div>
<p class="text-ghost-300">Loading ticket information...</p>
<p class="text-guild-300">Loading ticket information...</p>
</div>
<!-- Error State -->
@ -35,7 +35,7 @@
<strong>{{ ticketInfo.series?.title }}</strong> and requires a series
pass to attend.
</p>
<p class="text-sm text-ghost-300 mb-6">
<p class="text-sm text-guild-300 mb-6">
Purchase a series pass to get access to all events in this series.
</p>
<UButton
@ -70,7 +70,7 @@
details.
</template>
</p>
<p class="text-sm text-ghost-300">
<p class="text-sm text-guild-300">
See you on {{ formatEventDate(eventStartDate) }}!
</p>
</div>
@ -89,7 +89,7 @@
<!-- Registration Form -->
<div v-if="ticketInfo.available && !ticketInfo.alreadyRegistered">
<h3 class="text-xl font-bold text-ghost-100 mb-4">
<h3 class="text-xl font-bold text-guild-100 mb-4">
{{ ticketInfo.isFree ? "Register" : "Purchase Ticket" }}
</h3>
@ -98,7 +98,7 @@
<div>
<label
for="name"
class="block text-sm font-medium text-ghost-200 mb-2"
class="block text-sm font-medium text-guild-200 mb-2"
>
Full Name
</label>
@ -116,7 +116,7 @@
<div>
<label
for="email"
class="block text-sm font-medium text-ghost-200 mb-2"
class="block text-sm font-medium text-guild-200 mb-2"
>
Email Address
</label>
@ -128,7 +128,7 @@
placeholder="Enter your email"
:disabled="processing || isLoggedIn"
/>
<p v-if="isLoggedIn" class="text-xs text-ghost-400 mt-1">
<p v-if="isLoggedIn" class="text-xs text-guild-400 mt-1">
Using your member email
</p>
</div>
@ -185,10 +185,10 @@
>
<Icon
name="heroicons:ticket"
class="w-16 h-16 text-ghost-400 mx-auto mb-4"
class="w-16 h-16 text-guild-400 mx-auto mb-4"
/>
<h3 class="text-xl font-bold text-ghost-100 mb-2">Event Sold Out</h3>
<p class="text-ghost-300 mb-6">
<h3 class="text-xl font-bold text-guild-100 mb-2">Event Sold Out</h3>
<p class="text-guild-300 mb-6">
This event is currently at capacity. Join the waitlist to be notified
if spots become available.
</p>
@ -203,8 +203,8 @@
name="heroicons:x-circle"
class="w-16 h-16 text-red-400 mx-auto mb-4"
/>
<h3 class="text-xl font-bold text-ghost-100 mb-2">Event Sold Out</h3>
<p class="text-ghost-300">
<h3 class="text-xl font-bold text-guild-100 mb-2">Event Sold Out</h3>
<p class="text-guild-300">
Unfortunately, this event is at capacity and no longer accepting
registrations.
</p>

View file

@ -5,12 +5,12 @@
:description="description"
:dismissible="dismissible"
:ui="{
content: 'bg-ghost-900 border border-ghost-700',
header: 'bg-ghost-900 border-b border-ghost-700',
body: 'bg-ghost-900',
footer: 'bg-ghost-900 border-t border-ghost-700',
title: 'text-ghost-100',
description: 'text-ghost-400',
content: 'bg-guild-900 border border-guild-700',
header: 'bg-guild-900 border-b border-guild-700',
body: 'bg-guild-900',
footer: 'bg-guild-900 border-t border-guild-700',
title: 'text-guild-100',
description: 'text-guild-400',
}"
>
<template #body>
@ -20,9 +20,9 @@
<div class="w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
<Icon name="heroicons:check-circle" class="w-10 h-10 text-green-400" />
</div>
<h3 class="text-lg font-semibold text-ghost-100 mb-2">Check your email</h3>
<p class="text-ghost-300">
We've sent a magic link to <strong class="text-ghost-100">{{ loginForm.email }}</strong>.
<h3 class="text-lg font-semibold text-guild-100 mb-2">Check your email</h3>
<p class="text-guild-300">
We've sent a magic link to <strong class="text-guild-100">{{ loginForm.email }}</strong>.
Click the link to sign in.
</p>
</div>
@ -37,16 +37,16 @@
class="w-full"
placeholder="your.email@example.com"
:ui="{
root: 'bg-ghost-800 border-ghost-600 text-ghost-100 placeholder-ghost-500',
root: 'bg-guild-800 border-guild-600 text-guild-100 placeholder-guild-500',
}"
/>
</UFormField>
<!-- Info Box -->
<div class="bg-ghost-800 border border-ghost-600 p-4 rounded-lg mb-6">
<div class="bg-guild-800 border border-guild-600 p-4 rounded-lg mb-6">
<div class="flex items-start gap-3">
<Icon name="heroicons:envelope" class="w-5 h-5 text-whisper-400 flex-shrink-0 mt-0.5" />
<p class="text-sm text-ghost-300">
<Icon name="heroicons:envelope" class="w-5 h-5 text-candlelight-400 flex-shrink-0 mt-0.5" />
<p class="text-sm text-guild-300">
We'll send you a secure magic link. No password needed!
</p>
</div>
@ -73,12 +73,12 @@
</UForm>
<!-- Join Link -->
<div v-if="!loginSuccess" class="text-center pt-2 border-t border-ghost-700">
<p class="text-ghost-400 text-sm pt-4">
<div v-if="!loginSuccess" class="text-center pt-2 border-t border-guild-700">
<p class="text-guild-400 text-sm pt-4">
Don't have an account?
<NuxtLink
to="/join"
class="text-whisper-400 hover:text-whisper-300 font-medium"
class="text-candlelight-400 hover:text-candlelight-300 font-medium"
@click="close"
>
Join Ghost Guild

View file

@ -49,7 +49,7 @@
<button
v-if="dismissible"
@click="isDismissed = true"
class="text-ghost-400 hover:text-ghost-200 transition-colors"
class="text-guild-400 hover:text-guild-200 transition-colors"
:aria-label="`Dismiss ${statusConfig.label} banner`"
>
<Icon name="heroicons:x-mark" class="w-5 h-5" />
@ -119,7 +119,7 @@ const getActionButtonClass = (color) => {
const colorClasses = {
orange: "bg-orange-600 text-white hover:bg-orange-700",
blue: "bg-blue-600 text-white hover:bg-blue-700",
gray: "bg-ghost-700 text-ghost-100 hover:bg-ghost-600",
gray: "bg-guild-700 text-guild-100 hover:bg-guild-600",
};
return `${baseClass} ${colorClasses[color] || colorClasses.blue}`;
};

View file

@ -26,8 +26,8 @@
v-if="showInteractiveArea"
:class="[
'rounded-2xl p-6 md:p-8 mb-12 backdrop-blur-sm',
props.theme === 'ethereal'
? 'bg-ghost-800/60 border border-ghost-700 ethereal-glow halftone-texture'
props.theme === 'guild'
? 'bg-guild-800/60 border border-guild-700 candlelight-glow halftone-texture'
: 'bg-[--ui-bg-elevated] shadow-xl border border-blue-200',
]"
>
@ -35,8 +35,8 @@
<button
:class="[
'p-2 md:p-3 rounded-full transition-all duration-300 flex-shrink-0',
props.theme === 'ethereal'
? 'bg-whisper-600/80 text-ghost-100 hover:bg-whisper-500 ethereal-glow'
props.theme === 'guild'
? 'bg-candlelight-600/80 text-guild-100 hover:bg-candlelight-500 candlelight-glow'
: 'bg-blue-500 text-white hover:bg-blue-600',
]"
@click="$emit('prev')"
@ -61,8 +61,8 @@
<p
:class="[
'text-base md:text-lg',
props.theme === 'ethereal'
? 'text-ghost-200'
props.theme === 'guild'
? 'text-guild-200'
: 'text-[--ui-text-muted]',
]"
>
@ -77,8 +77,8 @@
<button
:class="[
'p-2 md:p-3 rounded-full transition-all duration-300 flex-shrink-0',
props.theme === 'ethereal'
? 'bg-whisper-600/80 text-ghost-100 hover:bg-whisper-500 ethereal-glow'
props.theme === 'guild'
? 'bg-candlelight-600/80 text-guild-100 hover:bg-candlelight-500 candlelight-glow'
: 'bg-blue-500 text-white hover:bg-blue-600',
]"
@click="$emit('next')"
@ -137,7 +137,7 @@ const props = defineProps({
type: String,
default: "blue",
validator: (value) =>
["blue", "purple", "emerald", "gray", "ethereal"].includes(value),
["blue", "purple", "emerald", "gray", "guild"].includes(value),
},
size: {
type: String,
@ -182,8 +182,8 @@ const backgroundClass = computed(() => {
purple: "bg-gradient-to-br from-purple-50 to-violet-100",
emerald: "bg-gradient-to-br from-emerald-50 to-teal-100",
gray: "bg-neutral-100",
ethereal:
"bg-gradient-to-br from-ghost-900 via-ghost-800 to-whisper-900 halftone-texture",
guild:
"bg-gradient-to-br from-guild-900 via-guild-800 to-candlelight-900 halftone-texture",
};
return themes[props.theme] || themes.blue;
});

View file

@ -1,6 +1,6 @@
<template>
<div class="flex items-center gap-3 text-sm">
<span class="text-gray-700 dark:text-ghost-400 text-xs font-medium"
<span class="text-gray-700 dark:text-guild-400 text-xs font-medium"
>{{ label }}:</span
>
<div class="flex items-center gap-2">
@ -8,7 +8,7 @@
class="text-xs transition-colors"
:class="
isPrivate
? 'text-gray-500 dark:text-ghost-500'
? 'text-gray-500 dark:text-guild-500'
: 'text-blue-600 dark:text-blue-400 font-semibold'
"
>
@ -25,7 +25,7 @@
:class="
isPrivate
? 'text-blue-600 dark:text-blue-400 font-semibold'
: 'text-gray-500 dark:text-ghost-500'
: 'text-gray-500 dark:text-guild-500'
"
>
Private

View file

@ -38,7 +38,7 @@
<!-- Registration Form -->
<div
v-if="passInfo.available && !passInfo.alreadyRegistered"
class="bg-ghost-800/50 dark:bg-ghost-700/30 rounded-xl border border-ghost-600 dark:border-ghost-600 p-6"
class="bg-guild-800/50 dark:bg-guild-700/30 rounded-xl border border-guild-600 dark:border-guild-600 p-6"
>
<h3 class="text-xl font-bold text-[--ui-text] mb-6">
{{

View file

@ -12,7 +12,7 @@
/>
<div
v-else
class="w-12 h-12 rounded-full bg-ghost-700 flex items-center justify-center text-ghost-300 font-bold"
class="w-12 h-12 rounded-full bg-guild-700 flex items-center justify-center text-guild-300 font-bold"
>
{{ update.author?.name?.charAt(0)?.toUpperCase() || "?" }}
</div>
@ -23,30 +23,30 @@
<!-- Header -->
<div class="flex items-start justify-between gap-4 mb-2">
<div>
<h3 class="font-semibold text-ghost-100">
<h3 class="font-semibold text-guild-100">
<NuxtLink
v-if="update.author?._id"
:to="`/updates/user/${update.author._id}`"
class="hover:text-ghost-300 transition-colors"
class="hover:text-guild-300 transition-colors"
>
{{ update.author.name }}
</NuxtLink>
<span v-else>Unknown Member</span>
</h3>
<div class="flex items-center gap-2 text-sm text-ghost-400">
<div class="flex items-center gap-2 text-sm text-guild-400">
<time :datetime="update.createdAt">
{{ formatDate(update.createdAt) }}
</time>
<span v-if="isEdited" class="text-ghost-500">(edited)</span>
<span v-if="isEdited" class="text-guild-500">(edited)</span>
<span
v-if="update.privacy === 'private'"
class="px-2 py-0.5 bg-ghost-700 text-ghost-300 rounded text-xs"
class="px-2 py-0.5 bg-guild-700 text-guild-300 rounded text-xs"
>
Private
</span>
<span
v-if="update.privacy === 'public'"
class="px-2 py-0.5 bg-ghost-700 text-ghost-300 rounded text-xs"
class="px-2 py-0.5 bg-guild-700 text-guild-300 rounded text-xs"
>
Public
</span>
@ -73,12 +73,12 @@
</div>
<!-- Content -->
<div class="text-ghost-200 whitespace-pre-wrap break-words mb-3">
<div class="text-guild-200 whitespace-pre-wrap break-words mb-3">
<template v-if="showPreview && update.content.length > 300">
{{ update.content.substring(0, 300) }}...
<NuxtLink
:to="`/updates/${update._id}`"
class="text-ghost-400 hover:text-ghost-300 ml-1"
class="text-guild-400 hover:text-guild-300 ml-1"
>
Read more
</NuxtLink>
@ -100,14 +100,14 @@
</div>
<!-- Footer actions -->
<div class="flex items-center gap-4 text-sm text-ghost-400">
<div class="flex items-center gap-4 text-sm text-guild-400">
<NuxtLink
:to="`/updates/${update._id}`"
class="hover:text-ghost-300 transition-colors"
class="hover:text-guild-300 transition-colors"
>
View full update
</NuxtLink>
<span v-if="update.commentsEnabled" class="text-ghost-500">
<span v-if="update.commentsEnabled" class="text-guild-500">
Comments (coming soon)
</span>
</div>
@ -175,15 +175,15 @@ const formatDate = (date) => {
<style scoped>
.update-card {
background-color: rgb(41 37 36);
border-color: rgb(87 83 78);
background-color: var(--color-guild-800);
border-color: var(--color-guild-600);
}
.update-card:hover {
border-color: rgb(120 113 108);
border-color: var(--color-guild-500);
}
:deep(.card) {
background-color: rgb(41 37 36);
background-color: var(--color-guild-800);
}
</style>

View file

@ -11,19 +11,19 @@
</UFormField>
<!-- Privacy Settings -->
<div class="border border-ghost-700 rounded-lg p-4 bg-ghost-800/30">
<h3 class="text-sm font-medium text-ghost-200 mb-4">Privacy Settings</h3>
<div class="border border-guild-700 rounded-lg p-4 bg-guild-800/30">
<h3 class="text-sm font-medium text-guild-200 mb-4">Privacy Settings</h3>
<div class="space-y-3">
<label class="flex items-center gap-3 cursor-pointer">
<input
v-model="formData.privacy"
type="radio"
value="public"
class="w-4 h-4 text-ghost-400"
class="w-4 h-4 text-guild-400"
/>
<div>
<div class="text-ghost-200 font-medium">Public</div>
<div class="text-sm text-ghost-400">
<div class="text-guild-200 font-medium">Public</div>
<div class="text-sm text-guild-400">
Visible to everyone, including non-members
</div>
</div>
@ -34,11 +34,11 @@
v-model="formData.privacy"
type="radio"
value="members"
class="w-4 h-4 text-ghost-400"
class="w-4 h-4 text-guild-400"
/>
<div>
<div class="text-ghost-200 font-medium">Members Only</div>
<div class="text-sm text-ghost-400">
<div class="text-guild-200 font-medium">Members Only</div>
<div class="text-sm text-guild-400">
Only visible to Ghost Guild members
</div>
</div>
@ -49,11 +49,11 @@
v-model="formData.privacy"
type="radio"
value="private"
class="w-4 h-4 text-ghost-400"
class="w-4 h-4 text-guild-400"
/>
<div>
<div class="text-ghost-200 font-medium">Private</div>
<div class="text-sm text-ghost-400">Only visible to you</div>
<div class="text-guild-200 font-medium">Private</div>
<div class="text-sm text-guild-400">Only visible to you</div>
</div>
</label>
</div>
@ -66,8 +66,8 @@
<div class="flex items-center gap-3">
<USwitch v-model="formData.commentsEnabled" />
<div>
<div class="text-ghost-200 font-medium">Enable Comments</div>
<div class="text-sm text-ghost-400">
<div class="text-guild-200 font-medium">Enable Comments</div>
<div class="text-sm text-guild-400">
Allow members to comment on this update
</div>
</div>
@ -75,7 +75,7 @@
<!-- Actions -->
<div
class="flex justify-between items-center pt-4 border-t border-ghost-700"
class="flex justify-between items-center pt-4 border-t border-guild-700"
>
<UButton variant="ghost" color="neutral" @click="$emit('cancel')">
Cancel
@ -157,28 +157,28 @@ watch(
<style scoped>
/* Field labels */
:deep(label) {
color: rgb(231 229 228) !important;
color: var(--color-guild-200) !important;
font-weight: 500;
}
/* Textarea styling */
:deep(textarea) {
background-color: rgb(41 37 36) !important;
color: rgb(231 229 228) !important;
border-color: rgb(87 83 78) !important;
background-color: var(--color-guild-800) !important;
color: var(--color-guild-200) !important;
border-color: var(--color-guild-600) !important;
}
:deep(textarea::placeholder) {
color: rgb(120 113 108) !important;
color: var(--color-guild-500) !important;
}
:deep(textarea:focus) {
border-color: rgb(168 162 158) !important;
background-color: rgb(44 40 39) !important;
border-color: var(--color-guild-400) !important;
background-color: var(--color-guild-700) !important;
}
/* Radio buttons */
input[type="radio"] {
accent-color: rgb(168 162 158);
accent-color: var(--color-candlelight-600);
}
</style>

View file

@ -1,36 +1,48 @@
// Central configuration for Ghost Guild Circles
export const CIRCLES = {
COMMUNITY: {
value: 'community',
label: 'Community Circle',
description: 'For individuals interested in learning about cooperative principles',
value: "community",
label: "Community",
shortDescription: "Learning about co-ops",
description:
"For individuals interested in learning about cooperative principles",
features: [
'Game workers curious about co-ops, researchers, industry allies',
'People exploring alternative work models',
'Access to community forums and resources'
]
"Game workers curious about coops",
"People exploring alternative work models, including researchers and industry allies",
"Access to community forums and resources",
],
colorToken: "circle-community",
metaphor: "Reading Room",
icon: null,
},
FOUNDER: {
value: 'founder',
label: 'Founder Circle',
description: 'For those actively establishing or growing their cooperative studio',
value: "founder",
label: "Founders",
shortDescription: "Building your studio",
description: "For those actively establishing or growing their coop",
features: [
'Has two tracks: Peer Accelerator Prep Track and Indie Track',
'Teams working toward PA application',
'Early-stage co-op studios',
'Studios transitioning to co-op model'
]
"Teams working toward applying for the Peer Accelerator",
"Early-stage coop studios",
"Studios transitioning to coop model",
],
colorToken: "circle-founder",
metaphor: "Workshop",
icon: null,
},
PRACTITIONER: {
value: 'practitioner',
label: 'Practitioner Circle',
description: 'For Peer Accelerator alumni and experienced studio leaders',
value: "practitioner",
label: "Practitioners",
shortDescription: "Leading and mentoring",
description: "For Peer Accelerator alumni and experienced studio founders",
features: [
'Those implementing cooperative models',
'Industry mentors and advisors',
'Expert-level workshops and mentorship opportunities'
]
}
"Those implementing cooperative models",
"Industry mentors and advisors",
"Expert-level workshops and mentorship opportunities",
],
colorToken: "circle-practitioner",
metaphor: "Alcove",
icon: null,
},
};
// Get all circle options as an array (useful for forms)
@ -40,15 +52,15 @@ export const getCircleOptions = () => {
// Get valid circle values for validation
export const getValidCircleValues = () => {
return Object.values(CIRCLES).map(circle => circle.value);
return Object.values(CIRCLES).map((circle) => circle.value);
};
// Get circle by value
export const getCircleByValue = (value) => {
return Object.values(CIRCLES).find(circle => circle.value === value);
return Object.values(CIRCLES).find((circle) => circle.value === value);
};
// Check if a circle value is valid
export const isValidCircleValue = (value) => {
return getValidCircleValues().includes(value);
};
};

View file

@ -1,5 +1,5 @@
<template>
<div class="min-h-screen bg-ghost-900">
<div class="min-h-screen bg-guild-900">
<slot />
</div>
</template>

View file

@ -1,5 +1,5 @@
<template>
<div class="min-h-screen bg-ghost-900 relative">
<div class="min-h-screen bg-guild-900 relative">
<!-- Background image at top - full page width -->
<div
class="absolute inset-x-0 pointer-events-none z-0"
@ -23,12 +23,12 @@
<!-- Mobile Header -->
<div
class="lg:hidden fixed top-0 left-0 right-0 z-50 bg-ghost-900/95 backdrop-blur-md border-b border-ghost-700"
class="lg:hidden fixed top-0 left-0 right-0 z-50 bg-guild-900/95 backdrop-blur-md border-b border-guild-700"
>
<div class="flex items-center justify-between p-4">
<NuxtLink
to="/"
class="text-lg font-bold text-white ethereal-text tracking-wider"
class="text-lg font-bold text-white warm-text tracking-wider"
>
Ghost Guild
</NuxtLink>
@ -46,7 +46,7 @@
<!-- Container to center content and sidebar together -->
<div class="lg:flex lg:justify-center lg:gap-0">
<!-- Main Content Column -->
<div class="lg:w-[800px] overflow-y-auto relative z-[5]">
<div class="lg:w-[800px] overflow-y-auto relative z-[5] guild-interior">
<div class="relative">
<slot />
</div>

131
app/layouts/landing.vue Normal file
View file

@ -0,0 +1,131 @@
<template>
<div class="min-h-screen bg-guild-900">
<!-- Horizontal Navigation -->
<nav class="w-full px-6 md:px-8 py-4">
<div class="max-w-6xl mx-auto flex items-center justify-between">
<!-- Logo/Wordmark -->
<NuxtLink to="/" class="text-xl font-bold text-primary-400 tracking-wide">
Ghost Guild
</NuxtLink>
<!-- Desktop Navigation Links -->
<div class="hidden md:flex items-center gap-8">
<NuxtLink
to="/about"
class="text-guild-300 hover:text-guild-100 transition-colors text-sm"
>
About
</NuxtLink>
<NuxtLink
to="/events"
class="text-guild-300 hover:text-guild-100 transition-colors text-sm"
>
Events
</NuxtLink>
<template v-if="isAuthenticated">
<NuxtLink
to="/member/dashboard"
class="text-primary-400 hover:text-primary-300 transition-colors text-sm font-medium"
>
Dashboard
</NuxtLink>
</template>
<template v-else>
<button
@click="openLoginModal"
class="text-primary-400 hover:text-primary-300 transition-colors text-sm font-medium"
>
Sign In
</button>
</template>
</div>
<!-- Mobile Menu Button -->
<UButton
icon="i-lucide-menu"
color="neutral"
variant="ghost"
size="md"
class="md:hidden"
@click="isMobileMenuOpen = true"
aria-label="Open menu"
/>
</div>
</nav>
<!-- Main Content -->
<main>
<slot />
</main>
<!-- Footer -->
<footer class="border-t border-guild-800 py-8 px-6 md:px-8">
<div class="max-w-6xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
<p class="text-guild-500 text-sm">
&copy; {{ currentYear }} Ghost Guild
</p>
<a
href="mailto:hello@ghostguild.org"
class="text-guild-500 hover:text-guild-300 transition-colors text-sm"
>
hello@ghostguild.org
</a>
</div>
</footer>
<!-- Mobile Navigation Drawer -->
<USlideover v-model:open="isMobileMenuOpen" side="right">
<template #body>
<div class="p-6 space-y-6">
<NuxtLink
to="/about"
class="block text-guild-200 hover:text-guild-100 transition-colors text-lg"
@click="isMobileMenuOpen = false"
>
About
</NuxtLink>
<NuxtLink
to="/events"
class="block text-guild-200 hover:text-guild-100 transition-colors text-lg"
@click="isMobileMenuOpen = false"
>
Events
</NuxtLink>
<template v-if="isAuthenticated">
<NuxtLink
to="/member/dashboard"
class="block text-primary-400 hover:text-primary-300 transition-colors text-lg font-medium"
@click="isMobileMenuOpen = false"
>
Dashboard
</NuxtLink>
</template>
<template v-else>
<button
@click="handleMobileSignIn"
class="block text-primary-400 hover:text-primary-300 transition-colors text-lg font-medium"
>
Sign In
</button>
</template>
</div>
</template>
</USlideover>
<!-- Login Modal -->
<LoginModal />
</div>
</template>
<script setup>
const { isAuthenticated } = useAuth()
const { openLoginModal } = useLoginModal()
const isMobileMenuOpen = ref(false)
const currentYear = new Date().getFullYear()
const handleMobileSignIn = () => {
isMobileMenuOpen.value = false
openLoginModal()
}
</script>

View file

@ -11,37 +11,116 @@
<!-- Main Content -->
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-3xl">
<!-- TODO: Add copy about history and connection to Baby Ghosts -->
<p class="text-lg text-[--ui-text-muted]">
Ghost Guild is a community of learning and practice for anyone,
anywhere interested in a video game industry-wide shift to
worker-owned studio models. It is also the membership program of
Baby Ghosts, a Canadian nonprofit that provides resources and
support for worker-owned studios. After running our Peer Accelerator
program for three years, we are now scaling up our operations to
support more studios and expand our reach. As we build our knowledge
commons, more folks unable to participate in the program can benefit
from collectively compiled knowledge and find community.
</p>
<p>
something here about the work to make Slack integration smooth and
safe; more about purpose??
</p>
<p>
We are pretty interested in saying _fuck you_ to hierarchy however
it shows up in our work. So the Ghost Guild membership program is
tier-less but peer-full. We've loosely named some circles you can
join that will help us connect you with folks at the same stage of
development as you, and with resources that are in line with your
needs and interests. But none of these circles is superior, and
there's no harm or shame in sticking with one for a while, or moving
between them to find the best fit. Choosing your financial
contribution level is also not about paying for access to additional
resources - everything is available to every member, no matter their
circle or contribution level. Rather, it's about finding a dues
level that's meaningful to you but not a burden.
</p>
<div class="max-w-3xl prose prose-lg dark:prose-invert">
<!-- directives:[] -->
<div id="content">
<p>
Ghost Guild is a community of learning and practice for anyone,
anywhere interested in a video game industry-wide shift to
worker-owned studio models.
</p>
<p>
We reject hierarchy wherever it shows up. Ghost Guild is
tier-less, but peer-full.
</p>
<h2 id="our-story">Our Story</h2>
<p>
Ghost Guild is the membership program of Baby Ghosts, a Canadian
nonprofit that provides resources and support for worker-owned
game studios.
</p>
<p>
For three years, Baby Ghosts has run the Peer Accelerator - an
intensive program pairing early-stage studios with mentorship,
peer learning, and grants. Twenty-five studios have graduated. The
model works! Peer learning builds stronger foundations than
top-down advice ever could.
</p>
<p>
But not everyone can commit to a six-month cohort. Some folks are
still exploring. Others are already running established
cooperatives and want to give back. Many are scattered across the
world and just need to know they're not alone in wanting to build
something different.
</p>
<p><em>Ghost Guild is how we open the doors wider.</em></p>
<p>
As we build our knowledge commons, more folks can benefit from
collectively compiled wisdom and find community - whether or not
they ever apply to the Peer Accelerator. The intensive program
continues. Ghost Guild expands access to everything around it.
</p>
<h2 id="the-circles">The Circles</h2>
<p>
We've loosely named some circles you can join. This is not to rank
you, but to connect you with folks at a similar stage and with
resources that fit where you are right now.
</p>
<p>
No circle is superior. There's no shame in sticking with one for a
while, or moving between them to find the best fit.
</p>
<p>
The Community Circle is for individuals exploring cooperative
principles. Whether you're working in the industry or in academia,
you'll get access to the knowledge commons, workshops, resources,
guides, community Slack and peer support, and social events and
networking.
</p>
<p>
The Founder Circle is for those actively building a worker-owned
studio. You'll have access to everything within the platform, just
like any other member, but you might be particularly interested in
peer matching with studios at similar stages and Peer Accelerator
alumni, and templates for governance, financial modelling, and
decision-making.
</p>
<p>
The Practitioner Circle is for Peer Accelerator alumni and
experienced cooperative studio leaders. You'll hopefully find
yourself providing paid support to other members, as well as
engaging in collaborative research opportunities with academic
partners, connecting to other coops for business development, and
helping build a platform for changing industry practices.
</p>
<h2 id="how-contribution-works">How Contribution Works</h2>
<p>
Choosing your financial contribution is also not about paying for
access. Everything is available to every member, no matter their
circle or contribution level.
</p>
<p>
Rather, it's about finding a dues level that's meaningful to you
without being a burden.
</p>
<p>
The knowledge commons is open to all Ghosties. Your contribution
sustains a community you believe in.
</p>
<p>
If dues are a barrier, that's okay. Members who are able to
contribute more can direct additional funds to the Solidarity
Fund, which covers dues for those who need support.
</p>
<h2 id="community">Community</h2>
<p>
When you join Ghost Guild, you join a community of Ghosties -
folks at every stage of the journey, learning from each other.
</p>
<p>
Our Slack community is built with care. New members are welcomed
thoughtfully, channels are structured to help you find your
people, and we grow at a pace that protects what makes this space
special.
</p>
<p>
This is a cascading mentorship structure where everyone is both
learning and teaching. Practitioners mentor Founders. Founders
mentor Community members. And Community members bring fresh
perspectives that keep everyone honest.
</p>
<p>Welcome, Ghostie! 👻</p>
</div>
</div>
</UContainer>
</section>

View file

@ -1,23 +1,23 @@
<template>
<div
v-if="pending"
class="min-h-screen bg-ghost-900 flex items-center justify-center"
class="min-h-screen bg-guild-900 flex items-center justify-center"
>
<div class="text-center">
<div
class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"
></div>
<p class="text-ghost-200">Loading event details...</p>
<p class="text-guild-200">Loading event details...</p>
</div>
</div>
<div
v-else-if="error"
class="min-h-screen bg-ghost-900 flex items-center justify-center"
class="min-h-screen bg-guild-900 flex items-center justify-center"
>
<div class="text-center">
<h2 class="text-2xl font-bold text-ghost-100 mb-2">Event Not Found</h2>
<p class="text-ghost-300 mb-6">
<h2 class="text-2xl font-bold text-guild-100 mb-2">Event Not Found</h2>
<p class="text-guild-300 mb-6">
The event you're looking for doesn't exist.
</p>
<NuxtLink to="/events" class="text-blue-400 hover:underline">
@ -74,35 +74,35 @@
<PageHeader v-else :title="event.title" theme="blue" size="medium" />
<!-- Event Details Section -->
<section class="py-16 bg-ghost-900">
<section class="py-16 bg-guild-900">
<UContainer>
<div class="max-w-4xl mx-auto">
<!-- Event Meta Info -->
<div class="mb-8">
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
<div>
<p class="text-sm text-ghost-400">Date</p>
<p class="font-semibold text-ghost-100">
<p class="text-sm text-guild-400">Date</p>
<p class="font-semibold text-guild-100">
{{ formatDate(event.startDate) }}
</p>
</div>
<div>
<p class="text-sm text-ghost-400">Time</p>
<p class="font-semibold text-ghost-100">
<p class="text-sm text-guild-400">Time</p>
<p class="font-semibold text-guild-100">
{{ formatTime(event.startDate, event.endDate) }}
</p>
</div>
<div>
<p class="text-sm text-ghost-400">Location</p>
<p class="font-semibold text-ghost-100">
<p class="text-sm text-guild-400">Location</p>
<p class="font-semibold text-guild-100">
{{ event.location }}
</p>
</div>
<div>
<p class="text-sm text-ghost-400">Calendar</p>
<p class="text-sm text-guild-400">Calendar</p>
<UButton
:href="`/api/events/${route.params.id}/calendar`"
download
@ -150,7 +150,7 @@
>
<div class="flex items-center space-x-2">
<span
class="text-sm font-medium text-gray-800 dark:text-ghost-200"
class="text-sm font-medium text-gray-800 dark:text-guild-200"
>Recommended for:</span
>
<div class="flex flex-wrap gap-2">
@ -167,31 +167,31 @@
<!-- Event Description -->
<div class="prose prose-lg dark:prose-invert max-w-none mb-12">
<h2 class="text-2xl font-bold text-ghost-100 mb-4">
<h2 class="text-2xl font-bold text-guild-100 mb-4">
About This Event
</h2>
<!-- Series Description -->
<div
v-if="event.series?.isSeriesEvent && event.series.description"
class="event-series-description mb-6 p-4 bg-ghost-800/30 dark:bg-ghost-700/20 rounded-lg border border-ghost-600 dark:border-ghost-600"
class="event-series-description mb-6 p-4 bg-guild-800/30 dark:bg-guild-700/20 rounded-lg border border-guild-600 dark:border-guild-600"
>
<h3
class="event-series-description__title text-lg font-semibold text-ghost-100 dark:text-ghost-100 mb-2"
class="event-series-description__title text-lg font-semibold text-guild-100 dark:text-guild-100 mb-2"
>
About the {{ event.series.title }} Series
</h3>
<p class="event-series-description__text text-ghost-200">
<p class="event-series-description__text text-guild-200">
{{ event.series.description }}
</p>
</div>
<p class="text-ghost-200">
<p class="text-guild-200">
{{ event.description }}
</p>
<div v-if="event.agenda && event.agenda.length > 0" class="mt-8">
<h3 class="text-xl font-semibold text-ghost-100 mb-4">
<h3 class="text-xl font-semibold text-guild-100 mb-4">
Event Agenda
</h3>
<ul class="space-y-3">
@ -205,7 +205,7 @@
>
{{ index + 1 }}
</span>
<span class="text-ghost-200">{{ item }}</span>
<span class="text-guild-200">{{ item }}</span>
</li>
</ul>
</div>
@ -214,7 +214,7 @@
v-if="event.speakers && event.speakers.length > 0"
class="mt-8"
>
<h3 class="text-xl font-semibold text-ghost-100 mb-4">
<h3 class="text-xl font-semibold text-guild-100 mb-4">
Speakers
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@ -224,13 +224,13 @@
class="flex items-start space-x-4"
>
<div>
<p class="font-semibold text-ghost-100">
<p class="font-semibold text-guild-100">
{{ speaker.name }}
</p>
<p class="text-sm text-ghost-300">
<p class="text-sm text-guild-300">
{{ speaker.role }}
</p>
<p class="text-sm text-ghost-400 mt-1">
<p class="text-sm text-guild-400 mt-1">
{{ speaker.bio }}
</p>
</div>
@ -367,7 +367,7 @@
v-else-if="memberData && (!event.membersOnly || isMember)"
class="text-center"
>
<p class="text-lg text-ghost-200 mb-6">
<p class="text-lg text-guild-200 mb-6">
You are logged in, {{ memberData.name }}.
</p>
<UButton
@ -383,14 +383,14 @@
<!-- Not Logged In - Show Registration Form -->
<div v-else>
<h3 class="text-xl font-bold text-ghost-100 mb-6">
<h3 class="text-xl font-bold text-guild-100 mb-6">
Register for This Event
</h3>
<form @submit.prevent="handleRegistration" class="space-y-4">
<div>
<label
for="name"
class="block text-sm font-medium text-ghost-200 mb-2"
class="block text-sm font-medium text-guild-200 mb-2"
>
Full Name
</label>
@ -406,7 +406,7 @@
<div>
<label
for="email"
class="block text-sm font-medium text-ghost-200 mb-2"
class="block text-sm font-medium text-guild-200 mb-2"
>
Email Address
</label>
@ -422,7 +422,7 @@
<div>
<label
for="membershipLevel"
class="block text-sm font-medium text-ghost-200 mb-2"
class="block text-sm font-medium text-guild-200 mb-2"
>
Membership Status
</label>
@ -452,17 +452,17 @@
<!-- Event Capacity -->
<div
v-if="event.maxAttendees"
class="mt-6 pt-6 border-t border-ghost-700"
class="mt-6 pt-6 border-t border-guild-700"
>
<div class="flex items-center justify-between">
<span class="text-sm text-ghost-300">Event Capacity</span>
<span class="text-sm text-guild-300">Event Capacity</span>
<div class="flex items-center space-x-2">
<span class="text-sm font-semibold text-ghost-100">
<span class="text-sm font-semibold text-guild-100">
{{ event.registeredCount || 0 }} /
{{ event.maxAttendees }}
</span>
<div
class="w-24 h-2 bg-ghost-700 rounded-full overflow-hidden"
class="w-24 h-2 bg-guild-700 rounded-full overflow-hidden"
>
<div
class="h-full bg-blue-500 rounded-full"
@ -476,7 +476,7 @@
<!-- Waitlist Section -->
<div
v-if="event.tickets?.waitlist?.enabled && isEventFull"
class="mt-6 pt-6 border-t border-ghost-700"
class="mt-6 pt-6 border-t border-guild-700"
>
<!-- Already on Waitlist -->
<div v-if="isOnWaitlist" class="text-center">
@ -534,9 +534,9 @@
</div>
<!-- Additional Information -->
<div class="mt-8 p-6 rounded-xl border border-ghost-700">
<h4 class="font-semibold text-ghost-100 mb-3">Questions?</h4>
<p class="text-sm text-ghost-200 mb-3">
<div class="mt-8 p-6 rounded-xl border border-guild-700">
<h4 class="font-semibold text-guild-100 mb-3">Questions?</h4>
<p class="text-sm text-guild-200 mb-3">
If you have any questions about this event please drop us a line.
</p>
<a

View file

@ -8,7 +8,7 @@
/>
<!-- Events Section with Tabs -->
<section class="bg-ghost-900 dark:bg-ghost-950">
<section class="bg-guild-900 dark:bg-guild-950">
<UContainer>
<UTabs
v-model="activeTab"
@ -51,7 +51,7 @@
<div
class="flex flex-col items-center md:items-start gap-2"
>
<label class="text-sm font-medium text-ghost-200">
<label class="text-sm font-medium text-guild-200">
Show past events?
</label>
<USwitch v-model="includePastEvents" />
@ -79,10 +79,10 @@
<template #panel="{ close }">
<div
class="bg-ghost-800 dark:bg-ghost-900 p-6 rounded-md shadow-lg w-80 space-y-4"
class="bg-guild-800 dark:bg-guild-900 p-6 rounded-md shadow-lg w-80 space-y-4"
>
<div class="space-y-2">
<label class="text-sm font-medium text-ghost-200">
<label class="text-sm font-medium text-guild-200">
Filter by category
</label>
<USelectMenu
@ -107,7 +107,7 @@
<!-- Search Status Message -->
<div v-if="isSearching" class="mt-6 text-center">
<p class="text-ghost-300 text-sm">
<p class="text-guild-300 text-sm">
{{
filteredEvents.length > 0
? `Found ${filteredEvents.length} event${
@ -129,10 +129,10 @@
class="group flex items-start gap-4 py-2 hover:opacity-80 transition-opacity"
>
<div class="flex-shrink-0 text-center">
<div class="text-2xl font-bold text-ghost-100">
<div class="text-2xl font-bold text-guild-100">
{{ event.start.getDate() }}
</div>
<div class="text-xs text-ghost-400 uppercase">
<div class="text-xs text-guild-400 uppercase">
{{
event.start.toLocaleDateString("en-US", {
month: "short",
@ -144,7 +144,7 @@
<div class="flex-1 min-w-0">
<div class="flex items-start gap-2 mb-1">
<h3
class="text-lg font-semibold text-ghost-100 group-hover:text-primary transition-colors"
class="text-lg font-semibold text-guild-100 group-hover:text-primary transition-colors"
>
{{ event.title }}
</h3>
@ -155,7 +155,7 @@
/>
</div>
<p class="text-sm text-ghost-300 mb-2 line-clamp-2">
<p class="text-sm text-guild-300 mb-2 line-clamp-2">
{{ event.content }}
</p>
@ -171,7 +171,7 @@
<Icon
name="heroicons:arrow-right"
class="w-5 h-5 text-ghost-400 group-hover:text-primary group-hover:translate-x-1 transition-all flex-shrink-0 mt-1"
class="w-5 h-5 text-guild-400 group-hover:text-primary group-hover:translate-x-1 transition-all flex-shrink-0 mt-1"
/>
</NuxtLink>
</div>
@ -183,13 +183,13 @@
<ClientOnly>
<div
v-if="pending"
class="min-h-[400px] bg-ghost-800 rounded-xl flex items-center justify-center"
class="min-h-[400px] bg-guild-800 rounded-xl flex items-center justify-center"
>
<div class="text-center">
<div
class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"
></div>
<p class="text-ghost-200">Loading calendar...</p>
<p class="text-guild-200">Loading calendar...</p>
</div>
</div>
<div v-else style="min-height: 600px">
@ -198,7 +198,7 @@
:events="calendarEvents"
:time="false"
active-view="month"
class="ghost-calendar"
class="guild-calendar"
:disable-views="['years', 'year']"
:hide-view-selector="false"
events-on-month-view="short"
@ -209,13 +209,13 @@
</div>
<template #fallback>
<div
class="min-h-[400px] bg-ghost-800 rounded-xl flex items-center justify-center"
class="min-h-[400px] bg-guild-800 rounded-xl flex items-center justify-center"
>
<div class="text-center">
<div
class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"
></div>
<p class="text-ghost-200">Loading calendar...</p>
<p class="text-guild-200">Loading calendar...</p>
</div>
</div>
</template>
@ -229,7 +229,7 @@
<!-- Event Series -->
<div v-if="activeSeries.length > 0" class="text-center my-12">
<h2 class="text-3xl font-bold text-ghost-100 mb-8">
<h2 class="text-3xl font-bold text-guild-100 mb-8">
Current Event Series
</h2>
</div>
@ -242,17 +242,17 @@
v-for="series in activeSeries.slice(0, 6)"
:key="series.id"
:to="`/series/${series.id}`"
class="series-list-item block bg-ghost-800/50 dark:bg-ghost-700/30 rounded-xl p-6 border border-ghost-600 dark:border-ghost-600 hover:border-ghost-500 hover:bg-ghost-800/70 dark:hover:bg-ghost-700/50 transition-all duration-300"
class="series-list-item block bg-guild-800/50 dark:bg-guild-700/30 rounded-xl p-6 border border-guild-600 dark:border-guild-600 hover:border-guild-500 hover:bg-guild-800/70 dark:hover:bg-guild-700/50 transition-all duration-300"
>
<div class="flex items-start justify-between mb-4">
<div class="flex items-center gap-2">
<span
class="series-list-item__label text-sm font-semibold text-ghost-300 dark:text-ghost-300"
class="series-list-item__label text-sm font-semibold text-guild-300 dark:text-guild-300"
>
Event Series
</span>
<span
class="series-list-item__count inline-flex items-center px-2 py-0.5 rounded-md bg-ghost-700/50 dark:bg-ghost-600/50 text-sm font-medium text-ghost-200 dark:text-ghost-200"
class="series-list-item__count inline-flex items-center px-2 py-0.5 rounded-md bg-guild-700/50 dark:bg-guild-600/50 text-sm font-medium text-guild-200 dark:text-guild-200"
>
{{ series.eventCount }} events
</span>
@ -272,13 +272,13 @@
</div>
<h3
class="series-list-item__title text-lg font-semibold text-ghost-100 dark:text-ghost-100 mb-2"
class="series-list-item__title text-lg font-semibold text-guild-100 dark:text-guild-100 mb-2"
>
{{ series.title }}
</h3>
<p
class="series-list-item__description text-sm text-ghost-300 dark:text-ghost-300 mb-4 line-clamp-2"
class="series-list-item__description text-sm text-guild-300 dark:text-guild-300 mb-4 line-clamp-2"
>
{{ series.description }}
</p>
@ -291,31 +291,31 @@
>
<div class="flex items-center gap-2">
<div
class="series-list-item__event-number w-6 h-6 bg-ghost-700/50 dark:bg-ghost-600/50 text-ghost-200 dark:text-ghost-200 rounded-full flex items-center justify-center text-xs font-medium border border-ghost-600 dark:border-ghost-500"
class="series-list-item__event-number w-6 h-6 bg-guild-700/50 dark:bg-guild-600/50 text-guild-200 dark:text-guild-200 rounded-full flex items-center justify-center text-xs font-medium border border-guild-600 dark:border-guild-500"
>
{{ event.series?.position || index + 1 }}
</div>
<span
class="series-list-item__event-title text-ghost-200 dark:text-ghost-200 truncate"
class="series-list-item__event-title text-guild-200 dark:text-guild-200 truncate"
>{{ event.title }}</span
>
</div>
<span
class="series-list-item__event-date text-ghost-300 dark:text-ghost-300"
class="series-list-item__event-date text-guild-300 dark:text-guild-300"
>
{{ formatEventDate(event.startDate) }}
</span>
</div>
<div
v-if="series.events.length > 3"
class="series-list-item__more-events text-xs text-ghost-300 dark:text-ghost-300 text-center pt-1"
class="series-list-item__more-events text-xs text-guild-300 dark:text-guild-300 text-center pt-1"
>
+{{ series.events.length - 3 }} more events
</div>
</div>
<div
class="series-list-item__date-range text-sm text-ghost-300 dark:text-ghost-300"
class="series-list-item__date-range text-sm text-guild-300 dark:text-guild-300"
>
{{ formatDateRange(series.startDate, series.endDate) }}
</div>
@ -323,27 +323,27 @@
</div>
<!-- Attend Our Events -->
<section class="py-20 bg-ghost-800 dark:bg-ghost-900">
<section class="py-20 bg-guild-800 dark:bg-guild-900">
<UContainer>
<div class="text-center mb-16">
<h2 class="text-3xl font-bold text-ghost-100 mb-8">
<h2 class="text-3xl font-bold text-guild-100 mb-8">
Attend Our Events
</h2>
</div>
<div class="max-w-4xl mx-auto">
<div
class="bg-ghost-900 rounded-2xl p-8 border border-ghost-700 mb-12"
class="bg-guild-900 rounded-2xl p-8 border border-guild-700 mb-12"
>
<div class="prose prose-lg dark:prose-invert max-w-none">
<p class="text-lg leading-relaxed text-ghost-200 mb-6">
<p class="text-lg leading-relaxed text-guild-200 mb-6">
Our events bring together game developers, founders, and practitioners
who are building more equitable workplaces. From hands-on workshops
about governance and finance to casual co-working sessions and game nights,
there's something for every stage of your journey.
</p>
<p class="text-lg leading-relaxed text-ghost-200 mb-6">
<p class="text-lg leading-relaxed text-guild-200 mb-6">
All events are designed to be accessible, with most offered free to members
and sliding-scale pricing for non-members. Can't make it live?
Many sessions are recorded and shared in our resource library.
@ -353,31 +353,31 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="text-center">
<h3 class="text-lg font-semibold text-ghost-100 mb-2">
<h3 class="text-lg font-semibold text-guild-100 mb-2">
Monthly Meetups
</h3>
<p class="text-sm text-ghost-300">
<p class="text-sm text-guild-300">
Casual knowledge sharing sessions
</p>
</div>
<div class="text-center">
<h3 class="text-lg font-semibold text-ghost-100 mb-2">
<h3 class="text-lg font-semibold text-guild-100 mb-2">
Workshops
</h3>
<p class="text-sm text-ghost-300">
<p class="text-sm text-guild-300">
Hands-on learning about cooperative and worker-centric business
models
</p>
</div>
<div class="text-center">
<h3 class="text-lg font-semibold text-ghost-100 mb-2">
<h3 class="text-lg font-semibold text-guild-100 mb-2">
Social Events
</h3>
<p class="text-sm text-ghost-300">
<p class="text-sm text-guild-300">
Game nights, socials, and more
</p>
</div>
@ -638,47 +638,47 @@ const getSeriesTypeBadgeClass = (type) => {
}
/* Calendar styling based on tranzac design principles */
.ghost-calendar :deep(.vuecal__event) {
.guild-calendar :deep(.vuecal__event) {
border-radius: 0.5rem;
border: 2px solid #292524;
transform: translateZ(0);
transition: transform 300ms;
}
.ghost-calendar :deep(.vuecal__event:hover) {
.guild-calendar :deep(.vuecal__event:hover) {
transform: scale(1.05);
}
.ghost-calendar :deep(.vuecal__event-title) {
.guild-calendar :deep(.vuecal__event-title) {
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ghost-calendar :deep(.vuecal__event-time) {
.guild-calendar :deep(.vuecal__event-time) {
display: none;
}
.ghost-calendar :deep(.vuecal__event.event-community) {
.guild-calendar :deep(.vuecal__event.event-community) {
background-color: #2563eb;
color: #f5f5f4;
border-color: #1d4ed8;
}
.ghost-calendar :deep(.vuecal__event.event-workshop) {
.guild-calendar :deep(.vuecal__event.event-workshop) {
background-color: #059669;
color: #f5f5f4;
border-color: #047857;
}
.ghost-calendar :deep(.vuecal__event.event-social) {
.guild-calendar :deep(.vuecal__event.event-social) {
background-color: #7c3aed;
color: #f5f5f4;
border-color: #6d28d9;
}
.ghost-calendar :deep(.vuecal__event.event-showcase) {
.guild-calendar :deep(.vuecal__event.event-showcase) {
background-color: #d97706;
color: #f5f5f4;
border-color: #b45309;
@ -829,7 +829,7 @@ const getSeriesTypeBadgeClass = (type) => {
}
/* Ghost calendar theme */
.ghost-calendar {
.guild-calendar {
--vuecal-primary-color: #fff;
--vuecal-text-color: #e7e5e4;
--vuecal-border-color: #57534e;
@ -838,120 +838,120 @@ const getSeriesTypeBadgeClass = (type) => {
background-color: #292524;
}
.ghost-calendar :deep(.vuecal__bg) {
.guild-calendar :deep(.vuecal__bg) {
background-color: #292524;
}
.ghost-calendar :deep(.vuecal__header) {
.guild-calendar :deep(.vuecal__header) {
background-color: #1c1917;
border-bottom: 1px solid #57534e;
}
.ghost-calendar :deep(.vuecal__title-bar) {
.guild-calendar :deep(.vuecal__title-bar) {
background-color: #1c1917;
}
.ghost-calendar :deep(.vuecal__title) {
.guild-calendar :deep(.vuecal__title) {
color: #e7e5e4;
font-weight: 600;
}
.ghost-calendar :deep(.vuecal__weekdays-headings) {
.guild-calendar :deep(.vuecal__weekdays-headings) {
background-color: #1c1917;
border-bottom: 1px solid #57534e;
}
.ghost-calendar :deep(.vuecal__heading) {
.guild-calendar :deep(.vuecal__heading) {
color: #a8a29e;
font-weight: 500;
}
.ghost-calendar :deep(.vuecal__cell) {
.guild-calendar :deep(.vuecal__cell) {
background-color: #292524;
border-color: #57534e;
color: #e7e5e4;
}
.ghost-calendar :deep(.vuecal__cell:hover) {
.guild-calendar :deep(.vuecal__cell:hover) {
background-color: #44403c;
border-color: #78716c;
}
.ghost-calendar :deep(.vuecal__cell-content) {
.guild-calendar :deep(.vuecal__cell-content) {
color: #e7e5e4;
}
.ghost-calendar :deep(.vuecal__cell--today) {
.guild-calendar :deep(.vuecal__cell--today) {
background-color: rgba(59, 130, 246, 0.1);
border: 2px solid #3b82f6;
}
.ghost-calendar :deep(.vuecal__cell--out-of-scope) {
.guild-calendar :deep(.vuecal__cell--out-of-scope) {
background-color: #1c1917;
color: #78716c;
}
.ghost-calendar :deep(.vuecal__arrow) {
.guild-calendar :deep(.vuecal__arrow) {
color: #a8a29e;
}
.ghost-calendar :deep(.vuecal__arrow:hover) {
.guild-calendar :deep(.vuecal__arrow:hover) {
background-color: #44403c;
}
.ghost-calendar :deep(.vuecal__today-btn) {
.guild-calendar :deep(.vuecal__today-btn) {
background-color: #44403c;
color: #fff;
border: 1px solid #78716c;
font-weight: 600;
}
.ghost-calendar :deep(.vuecal__today-btn:hover) {
.guild-calendar :deep(.vuecal__today-btn:hover) {
background-color: #57534e;
border-color: #a8a29e;
}
.ghost-calendar :deep(.vuecal__view-btn),
.ghost-calendar :deep(button[class*="view"]) {
.guild-calendar :deep(.vuecal__view-btn),
.guild-calendar :deep(button[class*="view"]) {
background-color: #44403c !important;
color: #ffffff !important;
border: 1px solid #78716c !important;
font-weight: 600 !important;
}
.ghost-calendar :deep(.vuecal__view-btn:hover),
.ghost-calendar :deep(button[class*="view"]:hover) {
.guild-calendar :deep(.vuecal__view-btn:hover),
.guild-calendar :deep(button[class*="view"]:hover) {
background-color: #57534e !important;
border-color: #a8a29e !important;
color: #ffffff !important;
}
.ghost-calendar :deep(.vuecal__view-btn--active),
.ghost-calendar :deep(button[class*="view"][class*="active"]) {
.guild-calendar :deep(.vuecal__view-btn--active),
.guild-calendar :deep(button[class*="view"][class*="active"]) {
background-color: #0c0a09 !important;
color: #ffffff !important;
border-color: #a8a29e !important;
}
.ghost-calendar :deep(.vuecal__view-btn--active:hover),
.ghost-calendar :deep(button[class*="view"][class*="active"]:hover) {
.guild-calendar :deep(.vuecal__view-btn--active:hover),
.guild-calendar :deep(button[class*="view"][class*="active"]:hover) {
background-color: #1c1917 !important;
border-color: #d6d3d1 !important;
color: #ffffff !important;
}
.ghost-calendar :deep(.vuecal__title-bar button) {
.guild-calendar :deep(.vuecal__title-bar button) {
color: #ffffff !important;
font-weight: 600 !important;
}
.ghost-calendar :deep(.vuecal__title-bar .default-view-btn) {
.guild-calendar :deep(.vuecal__title-bar .default-view-btn) {
background-color: #44403c !important;
color: #ffffff !important;
border: 1px solid #78716c !important;
}
.ghost-calendar :deep(.vuecal__title-bar .default-view-btn.active) {
.guild-calendar :deep(.vuecal__title-bar .default-view-btn.active) {
background-color: #0c0a09 !important;
border-color: #a8a29e !important;
}

View file

@ -1,109 +1,139 @@
<template>
<div class="relative">
<!-- Experimental Hero Section -->
<section class="mb-24">
<div class="relative">
<!-- Large artistic title -->
<div class="max-w-6xl mx-auto px-6 md:px-8">
<!-- Hero Section -->
<section class="py-16 md:py-24">
<div class="max-w-2xl">
<h1
class="text-6xl md:text-8xl font-bold text-ghost-100 ethereal-text leading-tight mb-8"
class="text-4xl md:text-5xl font-light text-guild-100 leading-tight mb-2"
>
Become a Ghostie
Build your co-op studio
</h1>
<p
class="text-4xl md:text-5xl font-light text-guild-500 leading-tight mb-8"
>
with people who get it.
</p>
<!-- Floating subtitle -->
<div class="mb-16">
<p class="text-ghost-100 text-lg max-w-md">
A peer community for creatives and game devs<br />
exploring cooperative models
<p class="text-lg text-guild-400 leading-relaxed mb-8 max-w-xl">
Ghost Guild is a peer community for game developers exploring
cooperative models. Find support, share knowledge, grow together.
</p>
<!-- Signup Form -->
<form @submit.prevent="handleJoinSubmit" class="mb-4">
<div class="flex flex-col sm:flex-row gap-3">
<UInput
v-model="joinEmail"
type="email"
placeholder="your.email@example.com"
size="lg"
class="flex-1"
:disabled="isSubmitting"
/>
<UButton
type="submit"
size="lg"
:loading="isSubmitting"
:disabled="!isEmailValid"
>
Join Us
</UButton>
</div>
</form>
<p class="text-sm text-guild-600">Free to join. Pay what you can.</p>
<!-- Success/Error Messages -->
<div
v-if="submitSuccess"
class="mt-4 p-3 bg-primary-500/10 border border-primary-500/30 rounded-lg"
>
<p class="text-primary-400 text-sm">
Check your email to complete signup!
</p>
</div>
<!-- Decorative elements -->
<div
class="absolute top-0 right-0 w-32 h-32 border border-ghost-800 rounded-full opacity-20"
/>
<div
class="absolute top-20 -left-8 w-16 h-px bg-whisper-500 opacity-40"
/>
v-if="submitError"
class="mt-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg"
>
<p class="text-red-400 text-sm">{{ submitError }}</p>
</div>
</div>
</section>
<!-- Join Section - Offset Layout -->
<section class="mb-32 relative">
<div>
<!-- Value Props Section -->
<section class="py-16 border-t border-guild-800">
<div class="grid md:grid-cols-3 gap-8 md:gap-12">
<div>
<p class="text-sm font-medium text-primary-400 mb-3">Peer Support</p>
<p class="text-guild-400 leading-relaxed">
Connect with founders at your stage and practitioners who've been
there. Real conversations, real help.
</p>
</div>
<div>
<p class="text-sm font-medium text-primary-400 mb-3">
Shared Knowledge
</p>
<p class="text-guild-400 leading-relaxed">
Templates, governance docs, financial modelstools built by co-ops,
for co-ops. All members get full access.
</p>
</div>
<div>
<p class="text-sm font-medium text-primary-400 mb-3">
Solidarity Economics
</p>
<p class="text-guild-400 leading-relaxed">
Those who can, support those who can't. No tiers, no gatekeeping.
Everyone gets everything.
</p>
</div>
</div>
</section>
<!-- Circles Section -->
<section class="py-16 border-t border-guild-800">
<p class="text-sm text-guild-600 mb-8">Find your people</p>
<div class="space-y-4 mb-8">
<NuxtLink
to="/join"
class="inline-block px-8 py-3 border border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 hover:ethereal-text transition-all duration-500"
v-for="circle in circles"
:key="circle.value"
to="/about/circles"
class="flex items-baseline gap-8 group py-2"
>
Join Us Today
<span
class="text-guild-300 group-hover:text-guild-100 transition-colors w-32 md:w-40"
>
{{ circle.label }}
</span>
<span class="text-guild-600">
{{ circle.shortDescription }}
</span>
</NuxtLink>
</div>
<!-- Decorative corner element -->
<div
class="absolute -right-4 top-0 w-20 h-20 border-t border-r border-ghost-800 opacity-30"
/>
<p class="text-sm text-guild-600 italic">
These reflect your journey, not your status. Move between them as you
grow.
</p>
</section>
<!-- Circles - Asymmetrical Grid -->
<section class="mb-32">
<div class="space-y-8">
<div
v-for="(circle, index) in circles"
:key="circle.value"
class="flex gap-8 items-start"
>
<!-- Content -->
<div class="flex-1 max-w-lg">
<h3 class="text-xl text-ghost-100 mb-3">{{ circle.label }}</h3>
<p class="text-ghost-200 text-sm leading-relaxed mb-4">
{{ circle.description }}
</p>
<!-- Features as inline text -->
<div class="text-sm text-ghost-400">
<span v-for="(feature, i) in circle.features" :key="feature">
{{ feature
}}<span v-if="i < circle.features.length - 1"> </span>
</span>
</div>
</div>
<!-- Side accent -->
</div>
</div>
</section>
<!-- Why Join? - Diagonal Layout -->
<section class="mb-32 relative">
<div class="transform -rotate-1">
<h2 class="text-3xl font-light text-ghost-200 mb-12">Why Join?</h2>
</div>
<div class="ml-12 relative">
<div
class="absolute -left-4 top-0 w-32 h-px bg-whisper-500 opacity-30 transform rotate-12"
/>
<div class="max-w-2xl">
<p class="text-ghost-300 leading-loose text-lg mb-8">
Ghost Guild is Baby Ghosts' membership program, and a community of
game makers building studios that center workers, not just profits.
</p>
<p class="text-ghost-400 leading-relaxed ml-8">
There's space for you no matter where you are in your cooperative
journey and no matter where in the world you are! You'll find peers,
resources, and support here.
</p>
</div>
<div
class="absolute -bottom-8 right-0 text-6xl text-ghost-800 opacity-20 font-bold"
>
?
</div>
</div>
<!-- Bottom CTA Section -->
<section class="py-24 border-t border-guild-800 text-center">
<p class="text-sm text-guild-600 mb-4">Part of the Baby Ghosts family</p>
<h2 class="text-3xl md:text-4xl font-light text-guild-200 mb-8">
Ready to find your people?
</h2>
<UButton
to="/join"
variant="outline"
size="lg"
class="hover:bg-primary-500/10"
>
Become a Ghostie
</UButton>
</section>
</div>
</template>
@ -111,5 +141,40 @@
<script setup>
import { getCircleOptions } from "~/config/circles";
definePageMeta({
layout: "landing",
});
const circles = getCircleOptions();
// Join form state
const joinEmail = ref("");
const isSubmitting = ref(false);
const submitSuccess = ref(false);
const submitError = ref("");
const isEmailValid = computed(() => {
return joinEmail.value && joinEmail.value.includes("@");
});
const handleJoinSubmit = async () => {
if (!isEmailValid.value || isSubmitting.value) return;
isSubmitting.value = true;
submitSuccess.value = false;
submitError.value = "";
try {
// Redirect to join page with email pre-filled
await navigateTo({
path: "/join",
query: { email: joinEmail.value },
});
} catch (err) {
console.error("Join error:", err);
submitError.value = "Something went wrong. Please try again.";
} finally {
isSubmitting.value = false;
}
};
</script>

View file

@ -16,9 +16,9 @@
>
<div class="text-center">
<div
class="w-8 h-8 border-4 border-whisper-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
class="w-8 h-8 border-4 border-candlelight-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
/>
<p class="text-ghost-300">Loading your dashboard...</p>
<p class="text-guild-300">Loading your dashboard...</p>
</div>
</div>
@ -28,11 +28,11 @@
class="flex justify-center items-center py-20"
>
<div class="text-center max-w-md">
<div class="w-16 h-16 bg-ghost-800 border border-ghost-600 rounded-full flex items-center justify-center mx-auto mb-4">
<Icon name="heroicons:lock-closed" class="w-8 h-8 text-ghost-400" />
<div class="w-16 h-16 bg-guild-800 border border-guild-600 rounded-full flex items-center justify-center mx-auto mb-4">
<Icon name="heroicons:lock-closed" class="w-8 h-8 text-guild-400" />
</div>
<h2 class="text-xl font-semibold text-ghost-100 mb-2">Sign in required</h2>
<p class="text-ghost-400 mb-6">Please sign in to access your member dashboard.</p>
<h2 class="text-xl font-semibold text-guild-100 mb-2">Sign in required</h2>
<p class="text-guild-400 mb-6">Please sign in to access your member dashboard.</p>
<UButton @click="openLoginModal({ title: 'Sign in to your dashboard', description: 'Enter your email to access your member dashboard' })">
Sign In
</UButton>
@ -45,23 +45,23 @@
<MemberStatusBanner :dismissible="true" />
<!-- Welcome Card -->
<UCard
class="sparkle-field"
:ui="{
root: 'bg-ghost-900 border border-ghost-700',
header: 'border-b border-ghost-700',
body: 'bg-ghost-900',
root: 'bg-guild-900 border border-guild-700',
header: 'border-b border-guild-700',
body: 'bg-guild-900',
}"
>
<template #header>
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<h1 class="text-2xl font-bold text-ghost-100 ethereal-text">
<h1 class="text-2xl font-bold text-guild-100 warm-text">
Welcome to Ghost Guild, {{ memberData?.name }}!
</h1>
<p
:class="[
'mt-2',
isActive ? 'text-ghost-300' : statusConfig.textColor,
isActive ? 'text-guild-300' : statusConfig.textColor,
]"
>
{{
@ -78,7 +78,7 @@
</div>
<div v-else class="flex-shrink-0">
<div
class="w-16 h-16 bg-ghost-700 border border-ghost-600 flex items-center justify-center text-ghost-200 font-bold text-xl"
class="w-16 h-16 bg-guild-700 border border-guild-600 flex items-center justify-center text-guild-200 font-bold text-xl"
>
{{ memberData?.name?.charAt(0)?.toUpperCase() }}
</div>
@ -87,14 +87,14 @@
</template>
<div class="flex flex-wrap gap-4 text-sm">
<div class="bg-ghost-800 border border-ghost-600 px-4 py-2">
<span class="text-ghost-200">Circle:</span>
<div class="bg-guild-800 border border-guild-600 px-4 py-2">
<span class="text-guild-200">Circle:</span>
<span class="font-medium text-stone-50 ml-1 capitalize">{{
memberData?.circle
}}</span>
</div>
<div class="bg-ghost-800 border border-ghost-600 px-4 py-2">
<span class="text-ghost-200">Contribution:</span>
<div class="bg-guild-800 border border-guild-600 px-4 py-2">
<span class="text-guild-200">Contribution:</span>
<span class="font-medium text-stone-50 ml-1"
>${{ memberData?.contributionTier }} CAD/month</span
>
@ -105,13 +105,13 @@
<!-- Quick Links -->
<UCard
:ui="{
root: 'bg-ghost-900 border border-ghost-700',
header: 'border-b border-ghost-700 bg-ghost-900',
body: 'bg-ghost-900',
root: 'bg-guild-900 border border-guild-700',
header: 'border-b border-guild-700 bg-guild-900',
body: 'bg-guild-900',
}"
>
<template #header>
<h2 class="text-xl font-bold text-ghost-100 ethereal-text">
<h2 class="text-xl font-bold text-guild-100 warm-text">
Quick Links
</h2>
</template>
@ -122,9 +122,9 @@
variant="outline"
:disabled="!canPeerSupport"
:class="[
'border-ghost-600 text-ghost-200 justify-start',
'border-guild-600 text-guild-200 justify-start',
canPeerSupport
? 'hover:bg-ghost-800 hover:border-whisper-500'
? 'hover:bg-guild-800 hover:border-candlelight-500'
: 'opacity-50 cursor-not-allowed',
]"
block
@ -141,7 +141,7 @@
to="https://wiki.ghostguild.org"
target="_blank"
variant="outline"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
class="border-guild-600 text-guild-200 hover:bg-guild-800 hover:border-candlelight-500 justify-start"
block
>
Browse Resources
@ -150,7 +150,7 @@
<UButton
to="/member/profile"
variant="outline"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
class="border-guild-600 text-guild-200 hover:bg-guild-800 hover:border-candlelight-500 justify-start"
block
>
Update Profile
@ -159,7 +159,7 @@
<UButton
to="/events"
variant="outline"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
class="border-guild-600 text-guild-200 hover:bg-guild-800 hover:border-candlelight-500 justify-start"
block
>
View Events
@ -168,7 +168,7 @@
<UButton
to="/members"
variant="outline"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
class="border-guild-600 text-guild-200 hover:bg-guild-800 hover:border-candlelight-500 justify-start"
block
>
Browse Members
@ -177,7 +177,7 @@
<UButton
to="/member/profile#account"
variant="outline"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
class="border-guild-600 text-guild-200 hover:bg-guild-800 hover:border-candlelight-500 justify-start"
block
>
Manage Account
@ -188,14 +188,14 @@
<!-- Your Registered Events -->
<UCard
:ui="{
root: 'bg-ghost-900 border border-ghost-700',
header: 'border-b border-ghost-700 bg-ghost-900',
body: 'bg-ghost-900',
root: 'bg-guild-900 border border-guild-700',
header: 'border-b border-guild-700 bg-guild-900',
body: 'bg-guild-900',
}"
>
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold text-ghost-100 ethereal-text">
<h2 class="text-xl font-bold text-guild-100 warm-text">
Your Upcoming Events
</h2>
<div class="flex items-center gap-2">
@ -204,7 +204,7 @@
@click="copyCalendarLink"
variant="ghost"
size="sm"
class="text-ghost-300 hover:text-ghost-100"
class="text-guild-300 hover:text-guild-100"
icon="heroicons:calendar"
>
{{
@ -215,7 +215,7 @@
to="/events"
variant="ghost"
size="sm"
class="text-ghost-300 hover:text-ghost-100"
class="text-guild-300 hover:text-guild-100"
>
Browse All Events
</UButton>
@ -225,7 +225,7 @@
<div v-if="loadingEvents" class="text-center py-8">
<div
class="w-6 h-6 border-2 border-whisper-500 border-t-transparent rounded-full animate-spin mx-auto"
class="w-6 h-6 border-2 border-candlelight-500 border-t-transparent rounded-full animate-spin mx-auto"
></div>
</div>
@ -234,7 +234,7 @@
v-for="evt in registeredEvents"
:key="evt._id"
:to="`/events/${evt.slug || evt._id}`"
class="block p-4 border border-ghost-700 hover:border-whisper-500 transition-colors"
class="block p-4 border border-guild-700 hover:border-candlelight-500 transition-colors"
>
<div class="flex items-start gap-4">
<div
@ -252,18 +252,18 @@
</div>
<div
v-else
class="flex-shrink-0 w-20 h-20 bg-ghost-800 border border-ghost-600 flex items-center justify-center"
class="flex-shrink-0 w-20 h-20 bg-guild-800 border border-guild-600 flex items-center justify-center"
>
<Icon
name="heroicons:calendar-days"
class="w-8 h-8 text-whisper-400"
class="w-8 h-8 text-candlelight-400"
/>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-ghost-100 mb-1">
<h3 class="font-semibold text-guild-100 mb-1">
{{ evt.title }}
</h3>
<div class="flex items-center gap-4 text-sm text-ghost-400">
<div class="flex items-center gap-4 text-sm text-guild-400">
<span class="flex items-center gap-1">
<Icon name="heroicons:calendar" class="w-4 h-4" />
{{ formatEventDate(evt.startDate) }}
@ -277,7 +277,7 @@
<div class="flex-shrink-0">
<Icon
name="heroicons:chevron-right"
class="w-5 h-5 text-ghost-500"
class="w-5 h-5 text-guild-500"
/>
</div>
</div>
@ -287,15 +287,15 @@
<div v-else class="text-center py-8">
<Icon
name="heroicons:calendar-days"
class="w-12 h-12 text-ghost-600 mx-auto mb-3"
class="w-12 h-12 text-guild-600 mx-auto mb-3"
/>
<p class="text-ghost-400 mb-4">
<p class="text-guild-400 mb-4">
You haven't registered for any upcoming events
</p>
<UButton
to="/events"
size="sm"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500"
class="border-guild-600 text-guild-200 hover:bg-guild-800 hover:border-candlelight-500"
>
Browse Events
</UButton>
@ -304,15 +304,15 @@
<!-- Calendar subscription instructions -->
<div
v-if="registeredEvents.length > 0 && showCalendarInstructions"
class="mt-4 p-4 bg-ghost-800 border border-ghost-600"
class="mt-4 p-4 bg-guild-800 border border-guild-600"
>
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<h4 class="text-sm font-semibold text-ghost-100 mb-2">
<h4 class="text-sm font-semibold text-guild-100 mb-2">
How to Subscribe to Your Calendar
</h4>
<ul
class="text-xs text-ghost-300 space-y-1 list-disc list-inside"
class="text-xs text-guild-300 space-y-1 list-disc list-inside"
>
<li>
<strong>Google Calendar:</strong> Click "+" "From URL"
@ -327,14 +327,14 @@
Paste the link
</li>
</ul>
<p class="text-xs text-ghost-400 mt-2">
<p class="text-xs text-guild-400 mt-2">
Your calendar will automatically update when you register or
unregister from events.
</p>
</div>
<button
@click="showCalendarInstructions = false"
class="text-ghost-400 hover:text-ghost-200"
class="text-guild-400 hover:text-guild-200"
>
<Icon name="heroicons:x-mark" class="w-5 h-5" />
</button>

View file

@ -11,8 +11,8 @@
<UContainer class="px-4">
<!-- Stats -->
<div v-if="isAuthenticated && !pending" class="mb-8 flex items-center justify-between">
<div class="text-ghost-300">
<span class="text-2xl font-bold text-ghost-100">{{ total }}</span>
<div class="text-guild-300">
<span class="text-2xl font-bold text-guild-100">{{ total }}</span>
{{ total === 1 ? "update" : "updates" }} posted
</div>
<UButton to="/updates/new" icon="i-lucide-plus"> New Update </UButton>
@ -25,9 +25,9 @@
>
<div class="text-center">
<div
class="w-8 h-8 border-4 border-ghost-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
class="w-8 h-8 border-4 border-guild-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
></div>
<p class="text-ghost-400">Loading your updates...</p>
<p class="text-guild-400">Loading your updates...</p>
</div>
</div>
@ -37,11 +37,11 @@
class="flex justify-center items-center py-20"
>
<div class="text-center max-w-md">
<div class="w-16 h-16 bg-ghost-800 border border-ghost-600 rounded-full flex items-center justify-center mx-auto mb-4">
<Icon name="heroicons:lock-closed" class="w-8 h-8 text-ghost-400" />
<div class="w-16 h-16 bg-guild-800 border border-guild-600 rounded-full flex items-center justify-center mx-auto mb-4">
<Icon name="heroicons:lock-closed" class="w-8 h-8 text-guild-400" />
</div>
<h2 class="text-xl font-semibold text-ghost-100 mb-2">Sign in required</h2>
<p class="text-ghost-400 mb-6">Please sign in to view your updates.</p>
<h2 class="text-xl font-semibold text-guild-100 mb-2">Sign in required</h2>
<p class="text-guild-400 mb-6">Please sign in to view your updates.</p>
<UButton @click="openLoginModal({ title: 'Sign in to view your updates', description: 'Enter your email to access your updates' })">
Sign In
</UButton>
@ -79,7 +79,7 @@
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
class="text-ghost-600"
class="text-guild-600"
>
<path
stroke-linecap="round"
@ -89,10 +89,10 @@
/>
</svg>
</div>
<h3 class="text-lg font-medium text-ghost-300 mb-2">
<h3 class="text-lg font-medium text-guild-300 mb-2">
No updates yet
</h3>
<p class="text-ghost-400 mb-6">
<p class="text-guild-400 mb-6">
Share your first update with the community
</p>
<UButton to="/updates/new" icon="i-lucide-plus">

View file

@ -18,7 +18,7 @@
<div
class="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
></div>
<p class="text-gray-600 dark:text-ghost-400">
<p class="text-gray-600 dark:text-guild-400">
Loading your profile...
</p>
</div>
@ -30,11 +30,11 @@
class="flex justify-center items-center py-20"
>
<div class="text-center max-w-md">
<div class="w-16 h-16 bg-ghost-800 border border-ghost-600 rounded-full flex items-center justify-center mx-auto mb-4">
<Icon name="heroicons:lock-closed" class="w-8 h-8 text-ghost-400" />
<div class="w-16 h-16 bg-guild-800 border border-guild-600 rounded-full flex items-center justify-center mx-auto mb-4">
<Icon name="heroicons:lock-closed" class="w-8 h-8 text-guild-400" />
</div>
<h2 class="text-xl font-semibold text-ghost-100 mb-2">Sign in required</h2>
<p class="text-ghost-400 mb-6">Please sign in to access your profile settings.</p>
<h2 class="text-xl font-semibold text-guild-100 mb-2">Sign in required</h2>
<p class="text-guild-400 mb-6">Please sign in to access your profile settings.</p>
<UButton @click="openLoginModal({ title: 'Sign in to your profile', description: 'Enter your email to manage your profile settings' })">
Sign In
</UButton>
@ -52,7 +52,7 @@
<!-- Basic Information -->
<div>
<h2
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-guild-100 warm-text"
>
Basic Information
</h2>
@ -124,7 +124,7 @@
:class="
formData.avatar === ghost.value
? 'border-blue-400 bg-blue-500/20'
: 'border-ghost-700 bg-ghost-800/50 hover:border-ghost-600'
: 'border-guild-700 bg-guild-800/50 hover:border-guild-600'
"
@click="formData.avatar = ghost.value"
>
@ -153,7 +153,7 @@
<!-- Professional Info -->
<div>
<h2
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-guild-100 warm-text"
>
Professional Information
</h2>
@ -222,7 +222,7 @@
<!-- Community Connections -->
<div>
<h2
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-guild-100 warm-text"
>
Community Connections
</h2>
@ -238,7 +238,7 @@
<!-- Tags input -->
<div>
<label
class="block text-sm font-medium text-gray-800 dark:text-ghost-200 mb-2"
class="block text-sm font-medium text-gray-800 dark:text-guild-200 mb-2"
>
Skills & Topics
</label>
@ -270,7 +270,7 @@
<!-- Description textarea -->
<div>
<label
class="block text-sm font-medium text-gray-800 dark:text-ghost-200 mb-2"
class="block text-sm font-medium text-gray-800 dark:text-guild-200 mb-2"
>
Details
</label>
@ -300,7 +300,7 @@
<!-- Tags input -->
<div>
<label
class="block text-sm font-medium text-gray-800 dark:text-ghost-200 mb-2"
class="block text-sm font-medium text-gray-800 dark:text-guild-200 mb-2"
>
Skills & Topics
</label>
@ -332,7 +332,7 @@
<!-- Description textarea -->
<div>
<label
class="block text-sm font-medium text-gray-800 dark:text-ghost-200 mb-2"
class="block text-sm font-medium text-gray-800 dark:text-guild-200 mb-2"
>
Details
</label>
@ -357,7 +357,7 @@
<!-- Peer Support -->
<div>
<h2
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-guild-100 warm-text"
>
Peer Support
</h2>
@ -368,12 +368,12 @@
<USwitch v-model="formData.peerSupportEnabled" />
<div>
<p
class="font-medium text-gray-800 dark:text-ghost-200"
class="font-medium text-gray-800 dark:text-guild-200"
>
Offer Peer Support
</p>
<p
class="text-sm text-gray-600 dark:text-ghost-400 mt-1"
class="text-sm text-gray-600 dark:text-guild-400 mt-1"
>
Make yourself available to support other members
</p>
@ -478,7 +478,7 @@
/>
<template #hint>
<span
class="text-xs text-gray-500 dark:text-ghost-500"
class="text-xs text-gray-500 dark:text-guild-500"
>
{{ formData.peerSupportMessage?.length || 0 }}/200
characters
@ -506,7 +506,7 @@
<!-- Directory Settings -->
<div>
<h2
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-guild-100 warm-text"
>
Directory Visibility
</h2>
@ -514,10 +514,10 @@
<div class="flex items-start gap-4">
<USwitch v-model="formData.showInDirectory" />
<div>
<p class="font-medium text-gray-800 dark:text-ghost-200">
<p class="font-medium text-gray-800 dark:text-guild-200">
Show in Member Directory
</p>
<p class="text-sm text-gray-600 dark:text-ghost-400 mt-1">
<p class="text-sm text-gray-600 dark:text-guild-400 mt-1">
Allow other members to discover and connect with you
through the directory
</p>
@ -544,7 +544,7 @@
<!-- Actions -->
<div
class="flex justify-between items-center pt-4 border-t border-ghost-800/50"
class="flex justify-between items-center pt-4 border-t border-guild-800/50"
>
<UButton
type="button"
@ -574,20 +574,20 @@
<!-- Current Membership -->
<div>
<h2
class="text-2xl font-semibold mb-6 text-gray-900 dark:text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-6 text-gray-900 dark:text-guild-100 warm-text"
>
Current Membership
</h2>
<div
class="backdrop-blur-sm bg-white/80 dark:bg-ghost-800/50 border border-gray-200 dark:border-ghost-700 rounded-lg p-6 space-y-4"
class="backdrop-blur-sm bg-white/80 dark:bg-guild-800/50 border border-gray-200 dark:border-guild-700 rounded-lg p-6 space-y-4"
>
<!-- Status Badge -->
<div
class="flex items-center justify-between pb-4 border-b border-gray-200 dark:border-ghost-700"
class="flex items-center justify-between pb-4 border-b border-gray-200 dark:border-guild-700"
>
<div>
<p class="text-sm text-gray-600 dark:text-ghost-400">
<p class="text-sm text-gray-600 dark:text-guild-400">
Membership Status
</p>
<div class="flex items-center gap-2 mt-1">
@ -620,21 +620,21 @@
<div class="flex items-start justify-between">
<div>
<p class="text-sm text-gray-600 dark:text-ghost-400">
<p class="text-sm text-gray-600 dark:text-guild-400">
Circle
</p>
<p
class="text-lg font-medium text-gray-900 dark:text-ghost-100 capitalize"
class="text-lg font-medium text-gray-900 dark:text-guild-100 capitalize"
>
{{ memberData.circle }}
</p>
</div>
<div>
<p class="text-sm text-gray-600 dark:text-ghost-400">
<p class="text-sm text-gray-600 dark:text-guild-400">
Contribution Level
</p>
<p
class="text-lg font-medium text-gray-900 dark:text-ghost-100"
class="text-lg font-medium text-gray-900 dark:text-guild-100"
>
${{ contributionTierDetails?.amount }}/month
</p>
@ -642,10 +642,10 @@
</div>
<div v-if="memberData.subscriptionStartDate">
<p class="text-sm text-gray-600 dark:text-ghost-400">
<p class="text-sm text-gray-600 dark:text-guild-400">
Member Since
</p>
<p class="text-gray-900 dark:text-ghost-100">
<p class="text-gray-900 dark:text-guild-100">
{{ formatDate(memberData.subscriptionStartDate) }}
</p>
</div>
@ -656,10 +656,10 @@
memberData.contributionTier !== '0'
"
>
<p class="text-sm text-gray-600 dark:text-ghost-400">
<p class="text-sm text-gray-600 dark:text-guild-400">
Next Billing Date
</p>
<p class="text-gray-900 dark:text-ghost-100">
<p class="text-gray-900 dark:text-guild-100">
{{ formatDate(memberData.nextBillingDate) }}
</p>
</div>
@ -669,15 +669,15 @@
<!-- Change Contribution Level -->
<div>
<h2
class="text-2xl font-semibold mb-6 text-gray-900 dark:text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-6 text-gray-900 dark:text-guild-100 warm-text"
>
Change Contribution Level
</h2>
<div
class="backdrop-blur-sm bg-white/80 dark:bg-ghost-800/50 border border-gray-200 dark:border-ghost-700 rounded-lg p-6"
class="backdrop-blur-sm bg-white/80 dark:bg-guild-800/50 border border-gray-200 dark:border-guild-700 rounded-lg p-6"
>
<p class="text-gray-700 dark:text-ghost-300 mb-6">
<p class="text-gray-700 dark:text-guild-300 mb-6">
Choose a new contribution level that works for you.
Changes will take effect on your next billing cycle.
</p>
@ -691,14 +691,14 @@
'w-full text-left p-4 rounded-lg border-2 transition-all',
selectedContributionTier === tier.value
? 'border-blue-400 bg-blue-500/20'
: 'border-gray-300 dark:border-ghost-600 bg-gray-50 dark:bg-ghost-900/30 hover:border-blue-300 dark:hover:border-ghost-500',
: 'border-gray-300 dark:border-guild-600 bg-gray-50 dark:bg-guild-900/30 hover:border-blue-300 dark:hover:border-guild-500',
]"
@click="selectedContributionTier = tier.value"
>
<div class="flex items-center justify-between">
<div>
<p
class="font-medium text-gray-900 dark:text-ghost-100"
class="font-medium text-gray-900 dark:text-guild-100"
>
{{ tier.label }}
</p>
@ -745,20 +745,20 @@
<!-- Cancel Membership -->
<div>
<h2
class="text-2xl font-semibold mb-6 text-gray-900 dark:text-ghost-100 ethereal-text"
class="text-2xl font-semibold mb-6 text-gray-900 dark:text-guild-100 warm-text"
>
Cancel Membership
</h2>
<div
class="backdrop-blur-sm bg-white/80 dark:bg-ghost-800/50 border border-gray-200 dark:border-ghost-700 rounded-lg p-6"
class="backdrop-blur-sm bg-white/80 dark:bg-guild-800/50 border border-gray-200 dark:border-guild-700 rounded-lg p-6"
>
<p class="text-gray-700 dark:text-ghost-300 mb-4">
<p class="text-gray-700 dark:text-guild-300 mb-4">
We're sorry to see you go. If you cancel, you'll lose
access to member benefits at the end of your current
billing period.
</p>
<p class="text-sm text-gray-600 dark:text-ghost-400 mb-6">
<p class="text-sm text-gray-600 dark:text-guild-400 mb-6">
Need a break? Consider switching to the free tier instead.
</p>

View file

@ -43,7 +43,7 @@
<!-- Skills Filter -->
<div v-if="availableSkills && availableSkills.length > 0">
<div class="flex flex-wrap gap-2">
<span class="text-sm text-ghost-400 mr-2 self-center"
<span class="text-sm text-guild-400 mr-2 self-center"
>Filter by skill:</span
>
<button
@ -57,7 +57,7 @@
:class="
selectedSkills.includes(skill)
? 'bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-300 dark:border-purple-500/50'
: 'bg-gray-100 dark:bg-ghost-800/50 text-gray-700 dark:text-ghost-400 border-gray-300 dark:border-ghost-700 hover:border-gray-400 dark:hover:border-ghost-600'
: 'bg-gray-100 dark:bg-guild-800/50 text-gray-700 dark:text-guild-400 border-gray-300 dark:border-guild-700 hover:border-gray-400 dark:hover:border-guild-600'
"
@click="toggleSkill(skill)"
>
@ -81,7 +81,7 @@
<!-- Peer Support Topics Filter -->
<div v-if="availableTopics && availableTopics.length > 0">
<div class="flex flex-wrap gap-2">
<span class="text-sm text-ghost-400 mr-2 self-center"
<span class="text-sm text-guild-400 mr-2 self-center"
>Filter by peer support topic:</span
>
<button
@ -95,7 +95,7 @@
:class="
selectedTopics.includes(topic)
? 'bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-300 dark:border-purple-500/50'
: 'bg-gray-100 dark:bg-ghost-800/50 text-gray-700 dark:text-ghost-400 border-gray-300 dark:border-ghost-700 hover:border-gray-400 dark:hover:border-ghost-600'
: 'bg-gray-100 dark:bg-guild-800/50 text-gray-700 dark:text-guild-400 border-gray-300 dark:border-guild-700 hover:border-gray-400 dark:hover:border-guild-600'
"
@click="toggleTopic(topic)"
>
@ -126,7 +126,7 @@
"
class="flex items-center gap-2 text-sm flex-wrap"
>
<span class="text-ghost-400">Active filters:</span>
<span class="text-guild-400">Active filters:</span>
<span
v-if="selectedCircle && selectedCircle !== 'all'"
class="px-2 py-1 bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 rounded-full border border-purple-300 dark:border-purple-500/30 flex items-center gap-1"
@ -173,13 +173,13 @@
<div
class="w-8 h-8 border-4 border-purple-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
></div>
<p class="text-ghost-400">Loading members...</p>
<p class="text-guild-400">Loading members...</p>
</div>
</div>
<!-- Members List -->
<div v-else-if="members.length > 0">
<div class="mb-4 text-ghost-400 text-sm">
<div class="mb-4 text-guild-400 text-sm">
{{ totalCount }} {{ totalCount === 1 ? "member" : "members" }} found
</div>
@ -187,7 +187,7 @@
<div
v-for="member in members"
:key="member._id"
class="relative backdrop-blur-sm bg-ghost-900/50 border border-ghost-700/50 rounded-lg p-6 hover:border-purple-500/50 transition-all group"
class="relative backdrop-blur-sm bg-guild-900/50 border border-guild-700/50 rounded-lg p-6 hover:border-purple-500/50 transition-all group"
>
<!-- Peer Support Sticker Badge -->
<PeerSupportBadge
@ -199,7 +199,7 @@
<div class="flex items-start gap-4 mb-4">
<!-- Avatar -->
<div
class="w-16 h-16 rounded-lg bg-ghost-800 border border-ghost-700 flex items-center justify-center flex-shrink-0 group-hover:border-purple-500/50 transition-colors"
class="w-16 h-16 rounded-lg bg-guild-800 border border-guild-700 flex items-center justify-center flex-shrink-0 group-hover:border-purple-500/50 transition-colors"
>
<img
v-if="member.avatar"
@ -207,16 +207,16 @@
:alt="member.name"
class="w-12 h-12 object-contain"
/>
<span v-else class="text-2xl text-ghost-600">👻</span>
<span v-else class="text-2xl text-guild-600">👻</span>
</div>
<!-- Name and Meta Info -->
<div class="flex-1 min-w-0">
<div class="flex items-baseline gap-2 flex-wrap mb-2">
<h3 class="font-semibold text-lg text-ghost-100">
<h3 class="font-semibold text-lg text-guild-100">
{{ member.name }}
</h3>
<span v-if="member.pronouns" class="text-sm text-ghost-400">
<span v-if="member.pronouns" class="text-sm text-guild-400">
{{ member.pronouns }}
</span>
</div>
@ -227,13 +227,13 @@
>
{{ circleLabels[member.circle] }}
</span>
<span v-if="member.studio" class="text-sm text-ghost-400">
<span v-if="member.studio" class="text-sm text-guild-400">
{{ member.studio }}
</span>
<span v-if="member.location" class="text-sm text-ghost-500">
<span v-if="member.location" class="text-sm text-guild-500">
📍 {{ member.location }}
</span>
<span v-if="member.timeZone" class="text-sm text-ghost-500">
<span v-if="member.timeZone" class="text-sm text-guild-500">
🕐 {{ member.timeZone }}
</span>
</div>
@ -243,7 +243,7 @@
<!-- Bio -->
<div
v-if="member.bio"
class="mb-4 text-ghost-300 text-sm leading-relaxed prose prose-invert prose-sm max-w-none"
class="mb-4 text-guild-300 text-sm leading-relaxed prose prose-invert prose-sm max-w-none"
v-html="renderMarkdown(member.bio)"
></div>
@ -280,7 +280,7 @@
<!-- Personal Message -->
<div
v-if="member.peerSupport.personalMessage"
class="text-sm text-ghost-300 italic mb-2"
class="text-sm text-guild-300 italic mb-2"
>
"{{ member.peerSupport.personalMessage }}"
</div>
@ -288,7 +288,7 @@
<!-- Availability -->
<div
v-if="member.peerSupport.availability"
class="text-xs text-ghost-400 mb-2"
class="text-xs text-guild-400 mb-2"
>
Availability: {{ member.peerSupport.availability }}
</div>
@ -326,7 +326,7 @@
</h5>
<p
v-if="member.offering.description"
class="text-ghost-300 text-sm"
class="text-guild-300 text-sm"
>
{{ member.offering.description }}
</p>
@ -353,7 +353,7 @@
</h5>
<p
v-if="member.lookingFor.description"
class="text-ghost-300 text-sm"
class="text-guild-300 text-sm"
>
{{ member.lookingFor.description }}
</p>
@ -386,7 +386,7 @@
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
class="text-ghost-600"
class="text-guild-600"
>
<path
stroke-linecap="round"
@ -396,10 +396,10 @@
/>
</svg>
</div>
<h3 class="text-lg font-medium text-ghost-300 mb-2">
<h3 class="text-lg font-medium text-guild-300 mb-2">
No members found
</h3>
<p class="text-ghost-400 mb-6">
<p class="text-guild-400 mb-6">
Try adjusting your search or filters
</p>
<UButton variant="outline" @click="clearAllFilters">

View file

@ -7,7 +7,7 @@
size="large"
/>
<section class="py-16 bg-ghost-900">
<section class="py-16 bg-guild-900">
<UContainer>
<div class="max-w-4xl mx-auto">
<!-- Welcome Message -->
@ -26,17 +26,17 @@
class="w-full h-full object-contain"
/>
</div>
<h2 class="text-2xl font-bold text-ghost-100 mb-4">
<h2 class="text-2xl font-bold text-guild-100 mb-4">
Hey {{ memberData?.name || "there" }}!
</h2>
<p class="text-lg text-ghost-300 max-w-2xl mx-auto">
<p class="text-lg text-guild-300 max-w-2xl mx-auto">
You've joined a an awesome community!!👻 Welcome to Ghost guild
</p>
</div>
<!-- Getting Started Steps -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-16">
<div class="p-6 bg-ghost-800/50 rounded-xl border border-ghost-700">
<div class="p-6 bg-guild-800/50 rounded-xl border border-guild-700">
<div
class="w-12 h-12 bg-purple-500/20 rounded-lg flex items-center justify-center mb-4"
>
@ -45,10 +45,10 @@
class="w-6 h-6 text-purple-400"
/>
</div>
<h3 class="font-semibold text-ghost-100 mb-2">
<h3 class="font-semibold text-guild-100 mb-2">
1. Complete Your Profile
</h3>
<p class="text-sm text-ghost-400 mb-4">
<p class="text-sm text-guild-400 mb-4">
Tell the community about yourself, your skills, and what you're
looking for.
</p>
@ -57,7 +57,7 @@
</UButton>
</div>
<div class="p-6 bg-ghost-800/50 rounded-xl border border-ghost-700">
<div class="p-6 bg-guild-800/50 rounded-xl border border-guild-700">
<div
class="w-12 h-12 bg-blue-500/20 rounded-lg flex items-center justify-center mb-4"
>
@ -66,10 +66,10 @@
class="w-6 h-6 text-blue-400"
/>
</div>
<h3 class="font-semibold text-ghost-100 mb-2">
<h3 class="font-semibold text-guild-100 mb-2">
2. Join an Event
</h3>
<p class="text-sm text-ghost-400 mb-4">
<p class="text-sm text-guild-400 mb-4">
From workshops to game nights, events are the heart of our
community.
</p>
@ -78,16 +78,16 @@
</UButton>
</div>
<div class="p-6 bg-ghost-800/50 rounded-xl border border-ghost-700">
<div class="p-6 bg-guild-800/50 rounded-xl border border-guild-700">
<div
class="w-12 h-12 bg-green-500/20 rounded-lg flex items-center justify-center mb-4"
>
<Icon name="heroicons:users" class="w-6 h-6 text-green-400" />
</div>
<h3 class="font-semibold text-ghost-100 mb-2">
<h3 class="font-semibold text-guild-100 mb-2">
3. Meet the Community
</h3>
<p class="text-sm text-ghost-400 mb-4">
<p class="text-sm text-guild-400 mb-4">
Connect with other members and find peers for support and
collaboration.
</p>
@ -99,12 +99,12 @@
<!-- About Circles -->
<div
class="p-8 bg-ghost-800/30 rounded-2xl border border-ghost-700 mb-16"
class="p-8 bg-guild-800/30 rounded-2xl border border-guild-700 mb-16"
>
<h3 class="text-xl font-bold text-ghost-100 mb-4">
<h3 class="text-xl font-bold text-guild-100 mb-4">
Understanding Circles
</h3>
<p class="text-ghost-300 mb-6">
<p class="text-guild-300 mb-6">
Ghost Guild is organized into three circles based on where you are
in your journey. Your circle helps us tailor events and resources
to your needs.
@ -116,10 +116,10 @@
:class="
memberData?.circle === 'community'
? 'bg-purple-500/20 border border-purple-500/50'
: 'bg-ghost-800/50'
: 'bg-guild-800/50'
"
>
<h4 class="font-semibold text-ghost-100 mb-2">
<h4 class="font-semibold text-guild-100 mb-2">
Community Circle
<span
v-if="memberData?.circle === 'community'"
@ -127,7 +127,7 @@
> You're here</span
>
</h4>
<p class="text-sm text-ghost-400">
<p class="text-sm text-guild-400">
For those exploring solidarity economics and alternative
studio models.
</p>
@ -138,10 +138,10 @@
:class="
memberData?.circle === 'founder'
? 'bg-purple-500/20 border border-purple-500/50'
: 'bg-ghost-800/50'
: 'bg-guild-800/50'
"
>
<h4 class="font-semibold text-ghost-100 mb-2">
<h4 class="font-semibold text-guild-100 mb-2">
Founder Circle
<span
v-if="memberData?.circle === 'founder'"
@ -149,7 +149,7 @@
> You're here</span
>
</h4>
<p class="text-sm text-ghost-400">
<p class="text-sm text-guild-400">
For those actively building or running a cooperative or
solidarity-based studio.
</p>
@ -160,10 +160,10 @@
:class="
memberData?.circle === 'practitioner'
? 'bg-purple-500/20 border border-purple-500/50'
: 'bg-ghost-800/50'
: 'bg-guild-800/50'
"
>
<h4 class="font-semibold text-ghost-100 mb-2">
<h4 class="font-semibold text-guild-100 mb-2">
Practitioner Circle
<span
v-if="memberData?.circle === 'practitioner'"
@ -171,7 +171,7 @@
> You're here</span
>
</h4>
<p class="text-sm text-ghost-400">
<p class="text-sm text-guild-400">
For consultants, advisors, and professionals supporting
cooperative game studios.
</p>
@ -181,9 +181,9 @@
<!-- Resources -->
<div
class="p-8 bg-ghost-800/30 rounded-2xl border border-ghost-700 mb-16"
class="p-8 bg-guild-800/30 rounded-2xl border border-guild-700 mb-16"
>
<h3 class="text-xl font-bold text-ghost-100 mb-4">
<h3 class="text-xl font-bold text-guild-100 mb-4">
Resources & Support
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@ -197,10 +197,10 @@
/>
</div>
<div>
<h4 class="font-semibold text-ghost-100 mb-1">
<h4 class="font-semibold text-guild-100 mb-1">
Resource Library
</h4>
<p class="text-sm text-ghost-400 mb-2">
<p class="text-sm text-guild-400 mb-2">
Templates, guides, and tools for building solidarity-based
studios.
</p>
@ -226,10 +226,10 @@
/>
</div>
<div>
<h4 class="font-semibold text-ghost-100 mb-1">
<h4 class="font-semibold text-guild-100 mb-1">
Peer Support
</h4>
<p class="text-sm text-ghost-400 mb-2">
<p class="text-sm text-guild-400 mb-2">
Connect 1:1 with community members for advice and support.
</p>
<UButton

0
public/fonts/.gitkeep Normal file
View file

0
public/textures/.gitkeep Normal file
View file