diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..8953c2b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,7 @@
+node_modules
+.git
+.gitignore
+.env
+.env.example
+*.md
+!internal_thought.md
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..68a3b56
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,27 @@
+GOOGLE_SERVICE_ACCOUNT=your_service_account_json_here
+GOOGLE_SHEET_ID=your_sheet_id_here
+GOOGLE_SHEET_NAME=Waitlist
+AI_GATEWAY_API_KEY=your_vercel_ai_gateway_key_here
+GAME_MODEL=mistral/mistral-large-latest
+
+# AI Gateway Configuration for Game Chat
+# Vercel AI Gateway API key (get from Vercel dashboard)
+AI_GATEWAY_API_KEY=your_vercel_ai_gateway_key_here
+
+# Model selection (optional, defaults to mistral/mistral-large-latest)
+# Format: provider/model-name
+# Examples:
+# mistral/mistral-large-latest
+# anthropic/claude-3-5-sonnet-20241022
+GAME_MODEL=mistral/mistral-large-latest
+
+# GitHub Configuration for Idea Sharing
+# GitHub Personal Access Token (fine-grained token with repo write access)
+# Create at: https://github.com/settings/tokens
+GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
+# GitHub Repository Configuration (optional, defaults shown)
+GITHUB_OWNER=understories
+GITHUB_REPO=votc
+GITHUB_BRANCH=main
+GITHUB_PATH=build_game/ideas
diff --git a/.gitignore b/.gitignore
index a6fc08a..5c0a362 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,25 +1,44 @@
-# Dependencies
-node_modules/
+refs/
-# Build output
-dist/
-build/
+# OS files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
-# Environment files
-.env
-.env.local
-.env.*.local
-
-# IDE
+# Editor files
.vscode/
.idea/
*.swp
*.swo
-
-# OS
-.DS_Store
-Thumbs.db
+*~
# Logs
*.log
npm-debug.log*
+
+# Temporary files
+*.tmp
+*.temp
+
+# Internal planning and research
+refs/
+
+# Google service account credentials (NEVER commit these!)
+*-service-account*.json
+service-account*.json
+google-credentials*.json
+credentials.json
+
+# Environment variables for local testing
+.env
+.env.local
+.env.*.local
+# But allow .env.example to be committed
+!.env.example
+
+.vercel
+.env*.local
diff --git a/Dockerfile b/Dockerfile
index 76666c1..4b3098a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,12 +1,21 @@
-FROM nginx:alpine
+FROM node:20-alpine
-# Copy static files
-COPY index.html /usr/share/nginx/html/
-COPY styles.css /usr/share/nginx/html/
+WORKDIR /app
-# Copy custom nginx config if needed
-COPY nginx.conf /etc/nginx/conf.d/default.conf
+# Copy package files
+COPY package*.json ./
-EXPOSE 80
+# Install dependencies
+RUN npm ci --only=production
-CMD ["nginx", "-g", "daemon off;"]
+# Install express for serving
+RUN npm install express
+
+# Copy application files
+COPY . .
+
+# Expose port
+EXPOSE 3000
+
+# Start server
+CMD ["node", "server.js"]
diff --git a/ENGINEERING_GUIDELINES.md b/ENGINEERING_GUIDELINES.md
new file mode 100644
index 0000000..4a35fb6
--- /dev/null
+++ b/ENGINEERING_GUIDELINES.md
@@ -0,0 +1,47 @@
+# Engineering Guidelines
+
+## Git & Version Control
+
+### Never Force Commit
+**CRITICAL RULE:** Never use `git add -f` or `git commit --force` to commit files that are intentionally ignored by `.gitignore`.
+
+**Rationale:**
+- Files in `.gitignore` are ignored for good reasons (security, privacy, internal planning)
+- Force committing bypasses these protections
+- If a file needs to be tracked, update `.gitignore` instead, don't force it
+
+**What to do instead:**
+1. If a file should be tracked: Remove it from `.gitignore` first
+2. If a file should remain ignored: Keep it local-only or use a different location
+3. If unsure: Ask before committing
+
+**Example of what NOT to do:**
+```bash
+# ❌ NEVER DO THIS
+git add -f refs/some-file.md
+git commit -m "Add file"
+```
+
+**Example of what TO do:**
+```bash
+# ✅ DO THIS INSTEAD
+# If file should be tracked:
+# 1. Edit .gitignore to remove the pattern
+# 2. Then commit normally
+git add refs/some-file.md
+git commit -m "Add file"
+```
+
+---
+
+## General Principles
+
+1. **Respect `.gitignore`** - Files are ignored for a reason
+2. **Ask before force operations** - Force operations can be destructive
+3. **Follow existing patterns** - Maintain consistency with project structure
+4. **Security first** - Never commit secrets, API keys, or sensitive data
+
+---
+
+**Last Updated:** January 2025
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..848f681
--- /dev/null
+++ b/README.md
@@ -0,0 +1,213 @@
+# Valley of the Commons
+
+Landing page and interactive game terminal for the Valley of the Commons project by Commons Hub.
+
+## Overview
+
+**Our valley is at a crossroads.** Once shaped by industry, it is now quietly deserting: businesses have closed, jobs are gone, young people leave. Yet beneath this surface of decline lies a rare constellation of opportunity. Just an hour from Vienna, with Europe's cleanest water flowing from the springs next door, surrounded by fertile land and affordable real estate, the Rax valley could become something else entirely: a prototype of future living.
+
+With the **commons hub** already established as a growing engine for community, events, and innovation, the immediate next step is expansion into an **Event Campus**. The bigger goal, however, is establishing a **Valley of the Commons**: a place where cooperative housing, cosmo-local production, and systemic resilience converge into a living laboratory of post-capitalism.
+
+## Why here, why now?
+
+Across Europe, villages like ours have been emptying out. Younger generations move to the cities, industries collapse, and what remains are elderly residents, shuttered factories, and a crumbling social fabric. Meanwhile, the digital nomad movement, the rise of smart villages, and the accelerating meta-crisis point towards a different trajectory: people are looking for ways out of the metropolis, towards self-sufficient and regenerative forms of life.
+
+Our setting is uniquely well-suited for this transition:
+
+- **Resilient resources:** The surrounding mountains cool the air and provide Vienna's drinking water, while the valleys in between contain fertile land.
+- **Space to grow:** An abundance of vacant houses, commercial spaces and agricultural lots makes expansion affordable and welcomed by locals.
+- **Connectivity:** One hour by train to Vienna and 90 minutes to the airport position us right at the center of Europe's rail and air connections.
+
+With local conditions ripe and global pressures rising, it's time to turn opportunity into action and start building. The future is shaped by the actions we take today.
+
+## From hub to valley
+
+The **commons hub** already anchors this process. Battle-tested as an event venue and guesthouse, it hosts everything from collaborative finance conferences, solarpunk meetups, and activist gatherings to academic workshops and prototyping sessions. To an emerging Valley of the Commons, it provides:
+
+- **An economic engine**: providing jobs, visibility, and a constant stream of curious people seeking alternatives to the status quo;
+- **A platform for entrepreneurship**: supporting new event series, research projects, product prototyping, and startup incubation;
+- **A beacon for metamodern thought and imagination**: holding space for narratives that orient us beyond collapse.
+
+## Housing the future
+
+A key pillar of the valley will be housing — not "smart homes" full of gadgets, but dwellings designed for the real challenges of the 21st century: climate change, resource constraints, and hyper-mobile lifestyles. We envision:
+
+- **Renovated houses** integrating state-of-the-art sustainable living concepts;
+- **Cooperative ownership**: from permanent co-housing to time-share models and shorter-term rentals;
+- **Distributed governance**: each house organized by its residents, following the principle of subsidiarity.
+
+The **commons hub GmbH** is a pragmatic bootstrap, but the valley itself will be **community-owned and self-governed**. Our role is to provide initial momentum — network, knowledge, infrastructure, and capital — then step back.
+
+## Production and self-sufficiency
+
+The Valley of the Commons won't just be a place to live — it will be a place to **collaboratively produce value** in diverse ways:
+
+- **Digital production** in communal co-working spaces;
+- **Physical production** in co-owned FabLabs seeking to develop innovative niche products and providing local manufacturing for community needs;
+- **Basic needs provision**: 100% water and energy self-sufficiency asap, while gradually building food resilience through community farming.
+
+In this way, production cycles will not only create a semi-autonomous local economy, but also build the capacity to provide for livelihoods and withstand future global disruptions.
+
+## Governance and community
+
+A key challenge in the political realm consists of balancing **local autonomy** with **collective coherence**, while also rebalancing the relationship between capital and labor through cooperative ownership and participatory governance. Guided by the principle of subsidiarity, we envision:
+
+- Co-owned and self-governed **houses and productive units**;
+- **Village-wide commons** coordinated through collectively chosen mechanisms;
+- **Integration with municipal politics** based on mutual recognition and complementarity.
+
+## Announcing: Popup Village 2026
+
+For now, the Valley of the Commons remains a vision, inspired by conversations among commons-oriented networks, deep adaptation thinkers, and p2p communities preparing for civilizational transition. To make it real, it needs future commons villagers to step in.
+
+In 2026, we plan a **4–6 week popup village** — exploring housing, governance, production, and the valley itself, with the goal of turning vision into concrete plans.
+
+**Michel Bauwens**, eminent Commons and P2P scholar, is on board, bringing his vision of a shift toward a commons-based civilization and experience with cosmo-local production. Veterans in mutual credit, community currencies, and housing coops are planning local research projects, while community leaders from Web3 co-livings and ecovillages provide guidance to **help lay the foundations of the Valley of the Commons**.
+
+The organizing team includes veterans of **Zuzalu** and other "zu-villages." Together, we will explore not only what is possible, but what it feels like to live in a valley oriented around the commons.
+
+## A growing campus, a growing commons
+
+The immediate next step on this journey is the expansion of the hub into **a monastery-like Event Campus** capable of hosting summits, exhibitions, and popups. To make this happen, we are running a **community lending campaign**, raising €200k in loans from our network to secure and renovate the buildings. **Over €80k have already been pledged**, with some lenders even donating their interest back to support future events.
+
+Supporting **the church expansion lays the groundwork** for the Valley of the Commons — the first step toward a campus that could grow into a vibrant village.
+
+## Get Involved
+
+🌱 **Lend to us** – help bridge the final stretch of funding.
+
+🙋♂️ **Pre-register** for the 2026 popup village.
+
+✍️ **Write to** f.fritsch@commons-hub.at **to get involved** in the popup organization.
+
+Together, we can turn vision into reality.
+
+## Project Structure
+
+```
+votc/
+├── index.html # Main landing page
+├── game.html # Game terminal interface
+├── privacy.html # Privacy FAQ page
+├── styles.css # Landing page stylesheet
+├── game.css # Game page stylesheet
+├── waitlist.js # Waitlist form handler (client-side)
+├── game.js # Game terminal logic
+├── package.json # Node.js dependencies
+├── vercel.json # Vercel configuration
+├── internal_thought.md # Game design context for AI
+├── api/
+│ ├── waitlist.js # Serverless function for Google Sheets
+│ └── share-to-github.js # Serverless function for GitHub sharing
+├── refs/ # Internal documentation (gitignored)
+├── README.md # This file
+├── commonshublogo.svg # Commons Hub logo
+└── valley.webp # Hero image
+```
+
+## Features
+
+- **Landing Page** - Event information and waitlist signup
+- **Game Terminal** - Interactive Socratic dialogue with AI game master
+- **Idea Sharing** - Share conversation excerpts to GitHub as structured ideas
+- **Privacy-First** - No tracking, no cookies, no user accounts
+
+## Setup
+
+### Prerequisites
+
+- Node.js 18.x+
+- Vercel account (for deployment)
+- Google Cloud account (for waitlist)
+- Vercel AI Gateway API key (for game chat)
+- GitHub Personal Access Token (for idea sharing)
+
+### Installation
+
+```bash
+git clone https://github.com/understories/votc.git
+cd votc
+npm install
+```
+
+### Environment Variables
+
+Create `.env` in the root directory (or set in Vercel dashboard):
+
+**Waitlist:**
+- `GOOGLE_SERVICE_ACCOUNT` - Google Service Account JSON (string)
+- `GOOGLE_SHEET_ID` - Google Sheets spreadsheet ID
+- `GOOGLE_SHEET_NAME` - Sheet name (default: 'Waitlist')
+
+**Game Chat:**
+- `GAME_INTELLIGENCE` or `AI_GATEWAY_API_KEY` - Vercel AI Gateway key (starts with `vck_`)
+- `GAME_MODEL` - Model name (default: `mistral/devstral-2`)
+
+**GitHub Sharing:**
+- `GITHUB_TOKEN` - GitHub Personal Access Token
+- `GITHUB_OWNER` - Repo owner (default: 'understories')
+- `GITHUB_REPO` - Repo name (default: 'votc')
+- `GITHUB_BRANCH` - Branch (default: 'main')
+- `GITHUB_PATH` - Ideas path (default: 'build_game/ideas')
+- `GITHUB_CONVERSATIONS_PATH` - Conversations path (default: 'build_game/conversations')
+
+### Local Development
+
+**Static pages only:**
+```bash
+python3 -m http.server 8000
+# or: npx http-server
+```
+
+**Full functionality (with serverless functions):**
+```bash
+npx vercel dev
+```
+
+### Deployment
+
+```bash
+npm i -g vercel
+vercel
+# Set environment variables in Vercel dashboard
+vercel --prod
+```
+
+## Waitlist Functionality
+
+The site includes a waitlist form that securely stores email addresses in a private Google Sheet. The integration uses:
+
+- **Frontend:** HTML form with JavaScript for submission handling
+- **Backend:** Vercel serverless function (`/api/waitlist.js`) that acts as a secure proxy
+- **Storage:** Google Sheets (private, accessible only to admins)
+
+**Setup Instructions:** See `refs/google-sheets-setup.md` for detailed configuration steps.
+
+**Security:** All Google Sheets credentials are stored in Vercel environment variables and never exposed to the client. The Google Sheet remains private even though the code is public.
+
+## Game Terminal
+
+The game terminal (`game.html`) provides an interactive Socratic dialogue interface where users can explore ideas about the Valley of the Commons with an AI game master. Features:
+
+- **Socratic questioning** - The AI guides conversations through thoughtful questions
+- **Idea sharing** - Users can share conversation excerpts or full conversations to GitHub
+- **Privacy-first** - Conversations are not saved unless explicitly shared
+- **Streaming responses** - Real-time text streaming for natural conversation flow
+
+**Game Chat:** Uses Vercel AI Gateway with Mistral models. See `refs/llm-integration-plan-v2.1.md` for architecture details.
+
+**Security:** All credentials stored in Vercel environment variables. No secrets in code.
+
+## Design Notes
+
+This landing page follows the design aesthetic of commons-hub.at:
+- Clean, minimal design
+- Uppercase headings
+- Sticky CTA button that scrolls with the page
+- Responsive layout
+- Modern typography
+
+## License
+
+© 2025 Commons Hub
+
diff --git a/api/game-chat.js b/api/game-chat.js
new file mode 100644
index 0000000..6acea93
--- /dev/null
+++ b/api/game-chat.js
@@ -0,0 +1,246 @@
+// Vercel serverless function for game chat
+// Uses Vercel AI Gateway with string model names
+// Implements Socratic game master moderator
+
+// CRITICAL: Set AI_GATEWAY_API_KEY before requiring 'ai' SDK
+// The SDK reads this at initialization time
+// Prefer GAME_INTELLIGENCE if it exists and is a valid key (starts with vck_)
+// Otherwise use AI_GATEWAY_API_KEY if it's a valid key
+const gatewayKey = process.env.GAME_INTELLIGENCE || process.env.AI_GATEWAY_API_KEY;
+if (gatewayKey && gatewayKey.startsWith('vck_')) {
+ process.env.AI_GATEWAY_API_KEY = gatewayKey;
+} else if (process.env.GAME_INTELLIGENCE && process.env.GAME_INTELLIGENCE.startsWith('vck_')) {
+ process.env.AI_GATEWAY_API_KEY = process.env.GAME_INTELLIGENCE;
+}
+
+const { streamText } = require('ai');
+const fs = require('fs');
+const path = require('path');
+
+// Load internal thoughts for context
+function loadInternalThoughts() {
+ try {
+ const thoughtsPath = path.join(process.cwd(), 'internal_thought.md');
+ if (fs.existsSync(thoughtsPath)) {
+ return fs.readFileSync(thoughtsPath, 'utf8');
+ }
+ } catch (error) {
+ console.warn('Could not load internal_thought.md:', error.message);
+ }
+ return '';
+}
+
+const INTERNAL_THOUGHTS = loadInternalThoughts();
+
+// Base system prompt for ongoing Socratic dialogue
+const SYSTEM_PROMPT = `You are a Socratic game master moderator at the Valley of the Commons.
+
+ROLE:
+- Ask ONE brief, open-ended question (1-2 sentences MAX)
+- NEVER provide answers, definitions, or solutions
+- If asked for a definition, respond with a question about meaning or context
+- Build on previous exchanges
+- Keep responses SHORT and terminal-friendly
+
+CONTEXT: "Valley of the Commons" is a decade-long game becoming a real village. Participants can propose tools, add rules, name places, create quests, document paths, bind myth to reality.
+
+DESIGN PRINCIPLES (reference subtly, never lecture):
+- Game as instrument for generating new operations
+- Physical-digital bridge: map, cards, projections
+- Community-driven: people mark places, add quests, surface tools
+- Ritualistic elements: mix of mythology and real life
+- Out of the box thinking: generate unconventional approaches
+- Commonalization: game becomes shared resource
+
+${INTERNAL_THOUGHTS ? `\nINTERNAL NOTES (inform questions, help participants discover):\n${INTERNAL_THOUGHTS}\n` : ''}
+
+CRITICAL: Be BRIEF. One question per response. Maximum 2 sentences. Probe, don't lecture.`;
+
+// Context-setting prompt for first response
+const FIRST_RESPONSE_PROMPT = `You are a game master moderator welcoming someone to the Valley of the Commons.
+
+This is the FIRST response after the user agreed to help create a game that shapes reality.
+
+TASK:
+- Brief context-setting (2-3 sentences) introducing the Valley
+- Use internal notes to paint what this game-village is becoming
+- End with ONE open-ended question inviting exploration
+- Do NOT respond to "yes" or "sure" - set context instead
+
+CONTEXT: "Valley of the Commons" is a decade-long game becoming a real village in the Austrian Alps. Participants can propose tools, add rules, name places, create quests, document paths, bind myth to reality.
+
+${INTERNAL_THOUGHTS ? `\nINTERNAL NOTES (inform context and question):\n${INTERNAL_THOUGHTS}\n` : ''}
+
+CRITICAL: Be BRIEF. Maximum 3-4 sentences total. One question at the end.`;
+
+module.exports = async function handler(req, res) {
+ console.log('[game-chat] Request received:', req.method, req.url);
+
+ // Only allow POST requests
+ if (req.method !== 'POST') {
+ console.log('[game-chat] Method not allowed:', req.method);
+ return res.status(405).json({ error: 'Method not allowed' });
+ }
+
+ // No CORS headers needed (same-origin only)
+ // If cross-origin needed later, restrict to specific domains
+
+ try {
+ const { messages } = req.body;
+ console.log('[game-chat] Messages received:', messages?.length || 0);
+ if (messages && messages.length > 0) {
+ console.log('[game-chat] First message:', {
+ role: messages[0]?.role,
+ content: messages[0]?.content?.substring(0, 100),
+ });
+ if (messages.length > 1) {
+ console.log('[game-chat] Last message:', {
+ role: messages[messages.length - 1]?.role,
+ content: messages[messages.length - 1]?.content?.substring(0, 100),
+ });
+ }
+ }
+
+ // Validate input
+ if (!messages || !Array.isArray(messages)) {
+ return res.status(400).json({ error: 'Invalid request format' });
+ }
+
+ // Sanitize and whitelist roles (CRITICAL: prevent system injection)
+ const sanitizedMessages = messages
+ .filter(msg => msg && typeof msg.content === 'string')
+ .map(msg => {
+ // Whitelist: only 'user' or 'assistant' roles allowed
+ const role = msg.role === 'assistant' ? 'assistant' : 'user';
+ return {
+ role: role,
+ content: msg.content.slice(0, 500), // Max 500 chars per message
+ };
+ })
+ .filter(msg => msg.content.length > 0); // Drop empty messages
+
+ console.log('[game-chat] Sanitized messages:', sanitizedMessages.length);
+ if (sanitizedMessages.length > 0) {
+ console.log('[game-chat] Sanitized first message:', {
+ role: sanitizedMessages[0]?.role,
+ content: sanitizedMessages[0]?.content?.substring(0, 100),
+ });
+ }
+
+ // Count user turns server-side (prevent gaming)
+ const userTurns = sanitizedMessages.filter(m => m.role === 'user').length;
+ if (userTurns > 12) {
+ return res.status(429).json({
+ error: 'Conversation limit reached. Please start a new session.'
+ });
+ }
+
+ // Detect if this is the first user response (context-setting phase)
+ const isFirstResponse = userTurns === 1 && sanitizedMessages.length === 1;
+
+ // Get API key (support both GAME_INTELLIGENCE and AI_GATEWAY_API_KEY)
+ // Vercel AI SDK automatically reads AI_GATEWAY_API_KEY from environment
+ // If GAME_INTELLIGENCE is set, use it as the API key
+ const apiKey = process.env.GAME_INTELLIGENCE || process.env.AI_GATEWAY_API_KEY;
+ console.log('[game-chat] API key check:', {
+ hasGAME_INTELLIGENCE: !!process.env.GAME_INTELLIGENCE,
+ hasAI_GATEWAY_API_KEY: !!process.env.AI_GATEWAY_API_KEY,
+ hasApiKey: !!apiKey,
+ apiKeyPrefix: apiKey ? apiKey.substring(0, 15) + '...' : 'none',
+ });
+
+ if (!apiKey) {
+ console.error('[game-chat] Missing GAME_INTELLIGENCE or AI_GATEWAY_API_KEY');
+ return res.status(500).json({ error: 'Server configuration error' });
+ }
+
+ // Ensure AI_GATEWAY_API_KEY is set for AI SDK (supports GAME_INTELLIGENCE alias)
+ // AI SDK reads AI_GATEWAY_API_KEY automatically when using string model names
+ // Must be set before streamText is called
+ if (!process.env.AI_GATEWAY_API_KEY) {
+ process.env.AI_GATEWAY_API_KEY = apiKey;
+ }
+
+ // Model selection - use string format for Vercel AI Gateway
+ // String format "provider/model" automatically routes through AI Gateway
+ // CRITICAL: AI_GATEWAY_API_KEY must be set in environment for this to work
+ const modelName = process.env.GAME_MODEL || 'mistral/devstral-2';
+ // Examples:
+ // - "mistral/devstral-2" (current - free tier)
+ // - "mistral/ministral-3b" (cost-effective)
+ // - "mistral/mistral-large-latest"
+ // - "anthropic/claude-3-5-sonnet-20241022"
+
+ console.log('[game-chat] Calling AI Gateway with string model:', modelName);
+ console.log('[game-chat] Message count:', sanitizedMessages.length);
+ console.log('[game-chat] AI_GATEWAY_API_KEY set:', !!process.env.AI_GATEWAY_API_KEY);
+
+ try {
+ // Use string model name - SDK automatically routes through AI Gateway
+ // when it sees the "provider/model" format and AI_GATEWAY_API_KEY is set
+
+ // Choose prompt based on whether this is the first response
+ const activePrompt = isFirstResponse ? FIRST_RESPONSE_PROMPT : SYSTEM_PROMPT;
+ console.log('[game-chat] Creating streamText with model:', modelName);
+ console.log('[game-chat] Using prompt type:', isFirstResponse ? 'FIRST_RESPONSE' : 'SOCRATIC');
+
+ const result = streamText({
+ model: modelName, // String format routes through Gateway
+ system: activePrompt, // System prompt (NOT in messages array)
+ messages: sanitizedMessages, // Only user/assistant messages
+ maxTokens: isFirstResponse ? 120 : 80, // Enforce brevity
+ temperature: 0.7,
+ });
+
+ console.log('[game-chat] StreamText result created');
+
+ // Set headers for streaming text response
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
+ res.setHeader('Cache-Control', 'no-cache');
+ res.setHeader('Connection', 'keep-alive');
+ res.setHeader('Transfer-Encoding', 'chunked');
+
+ // Stream text chunks directly to client
+ console.log('[game-chat] Starting to stream text to client');
+ try {
+ for await (const textPart of result.textStream) {
+ res.write(textPart);
+ }
+ res.end();
+ console.log('[game-chat] Stream completed and sent to client');
+ } catch (streamError) {
+ console.error('[game-chat] Error during streaming:', streamError.message);
+ if (!res.headersSent) {
+ res.status(500).json({ error: 'Stream error' });
+ } else {
+ res.end();
+ }
+ }
+ } catch (streamError) {
+ console.error('[game-chat] Error in streamText call:', {
+ message: streamError.message,
+ stack: streamError.stack,
+ });
+ throw streamError;
+ }
+
+ } catch (error) {
+ console.error('AI Gateway error:', {
+ message: error.message,
+ stack: error.stack,
+ model: modelName,
+ hasApiKey: !!apiKey,
+ apiKeyPrefix: apiKey ? apiKey.substring(0, 10) : 'none',
+ });
+
+ // Return more detailed error in development
+ const isDev = process.env.NODE_ENV !== 'production';
+ return res.status(500).json({
+ error: isDev
+ ? `AI Gateway error: ${error.message}`
+ : 'AI service temporarily unavailable. Please try again.',
+ ...(isDev && { details: error.stack })
+ });
+ }
+};
+
diff --git a/api/share-to-github.js b/api/share-to-github.js
new file mode 100644
index 0000000..84bbaf6
--- /dev/null
+++ b/api/share-to-github.js
@@ -0,0 +1,233 @@
+// Vercel serverless function to share conversation excerpts to GitHub
+// Creates files directly in the repository (NOT issues) using GitHub API
+// Uses GITHUB_TOKEN (github.com/vrnvrn account) to create files in:
+// - build_game/ideas/ for selected excerpts
+// - build_game/conversations/ for full chat history
+
+const { Octokit } = require('@octokit/rest');
+
+// Generate idea file template
+function generateIdeaTemplate(excerpt, context) {
+ const timestamp = new Date().toISOString();
+ const date = new Date().toLocaleString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ timeZoneName: 'short'
+ });
+
+ return `# Idea: [Auto-generated from conversation]
+
+**Source:** Valley of the Commons Game Master Dialogue
+**Date:** ${date}
+**Excerpt:**
+
+${excerpt}
+
+---
+
+**Context:** This idea emerged from a Socratic dialogue about game design in the Valley of the Commons.
+
+**Next Steps:**
+- [ ] Refine this idea
+- [ ] Connect to other ideas
+- [ ] Propose as a tool/rule/quest
+
+---
+
+*Generated from game master conversation*`;
+}
+
+// Generate unique filename based on timestamp
+function generateFilename(isFullChat = false) {
+ const now = new Date();
+ const year = now.getFullYear();
+ const month = String(now.getMonth() + 1).padStart(2, '0');
+ const day = String(now.getDate()).padStart(2, '0');
+ const hours = String(now.getHours()).padStart(2, '0');
+ const minutes = String(now.getMinutes()).padStart(2, '0');
+ const seconds = String(now.getSeconds()).padStart(2, '0');
+
+ const prefix = isFullChat ? 'conversation' : 'idea';
+ return `${prefix}-${year}-${month}-${day}-${hours}${minutes}${seconds}.md`;
+}
+
+module.exports = async function handler(req, res) {
+ // Only allow POST requests
+ if (req.method !== 'POST') {
+ return res.status(405).json({ error: 'Method not allowed' });
+ }
+
+ console.log('[share-to-github] Request received:', {
+ method: req.method,
+ hasBody: !!req.body,
+ bodyKeys: req.body ? Object.keys(req.body) : [],
+ });
+
+ try {
+ const { content, excerpt, context, isFullChat } = req.body;
+
+ console.log('[share-to-github] Request data:', {
+ hasContent: !!content,
+ contentLength: content ? content.length : 0,
+ hasExcerpt: !!excerpt,
+ isFullChat: isFullChat,
+ });
+
+ // Prefer full content if provided, otherwise use excerpt for backwards compatibility
+ let fileContent;
+ if (content && typeof content === 'string') {
+ // Use provided content (already includes template + chat history)
+ fileContent = content.slice(0, 50000).trim(); // Allow larger content for full chat history
+ if (fileContent.length === 0) {
+ return res.status(400).json({ error: 'Content cannot be empty' });
+ }
+ } else if (excerpt && typeof excerpt === 'string') {
+ // Fallback to generating template from excerpt (backwards compatibility)
+ const sanitizedExcerpt = excerpt.slice(0, 5000).trim();
+ if (sanitizedExcerpt.length === 0) {
+ return res.status(400).json({ error: 'Excerpt cannot be empty' });
+ }
+ fileContent = generateIdeaTemplate(sanitizedExcerpt, context || {});
+ } else {
+ return res.status(400).json({ error: 'Content or excerpt is required' });
+ }
+
+ // Get GitHub configuration
+ const token = process.env.GITHUB_TOKEN;
+ if (!token) {
+ console.error('[share-to-github] Missing GITHUB_TOKEN');
+ return res.status(500).json({ error: 'Server configuration error: Missing GitHub token' });
+ }
+
+ const owner = process.env.GITHUB_OWNER || 'understories';
+ const repo = process.env.GITHUB_REPO || 'votc';
+ const branch = process.env.GITHUB_BRANCH || 'main';
+
+ // Determine base path: conversations directory for full chats, ideas for excerpts
+ const isFullChatShare = isFullChat === true;
+ const basePath = isFullChatShare
+ ? (process.env.GITHUB_CONVERSATIONS_PATH || 'build_game/conversations')
+ : (process.env.GITHUB_PATH || 'build_game/ideas');
+
+ console.log('[share-to-github] GitHub config:', {
+ owner,
+ repo,
+ branch,
+ basePath,
+ isFullChatShare,
+ hasToken: !!token,
+ tokenPrefix: token ? token.substring(0, 10) + '...' : 'none',
+ });
+
+ // Initialize Octokit
+ const octokit = new Octokit({
+ auth: token,
+ });
+
+ // Generate filename and path
+ const filename = generateFilename(isFullChatShare);
+ const path = `${basePath}/${filename}`;
+
+ console.log('[share-to-github] Creating file:', {
+ filename,
+ path,
+ contentLength: fileContent.length,
+ });
+
+ // fileContent is already set above (either from content param or generated from excerpt)
+
+ // Create file in GitHub
+ const commitMessage = isFullChatShare
+ ? 'Add full conversation from game master dialogue'
+ : 'Add idea from game master conversation';
+
+ // Ensure content is properly encoded as base64
+ // GitHub API requires base64 encoding and the string must be valid
+ let base64Content;
+ try {
+ base64Content = Buffer.from(fileContent, 'utf8').toString('base64');
+ // Validate base64 encoding
+ if (!base64Content || base64Content.length === 0) {
+ throw new Error('Base64 encoding resulted in empty string');
+ }
+ } catch (encodeError) {
+ console.error('[share-to-github] Base64 encoding error:', encodeError);
+ return res.status(500).json({
+ error: 'Failed to encode content. Please try again.'
+ });
+ }
+
+ console.log('[share-to-github] Base64 content length:', base64Content.length);
+
+ const response = await octokit.repos.createOrUpdateFileContents({
+ owner: owner,
+ repo: repo,
+ path: path,
+ message: commitMessage,
+ content: base64Content,
+ branch: branch,
+ });
+
+ console.log('[share-to-github] File created successfully:', {
+ path: response.data.content.path,
+ sha: response.data.content.sha,
+ });
+
+ // Generate GitHub URL
+ const githubUrl = `https://github.com/${owner}/${repo}/blob/${branch}/${path}`;
+
+ return res.status(200).json({
+ success: true,
+ url: githubUrl,
+ filename: filename,
+ path: path,
+ });
+
+ } catch (error) {
+ console.error('[share-to-github] Error:', {
+ message: error.message,
+ status: error.status,
+ name: error.name,
+ response: error.response ? {
+ status: error.response.status,
+ statusText: error.response.statusText,
+ data: error.response.data,
+ } : null,
+ stack: error.stack,
+ });
+
+ // Handle specific GitHub API errors
+ if (error.status === 401) {
+ return res.status(500).json({
+ error: 'Authentication error. Please contact support.'
+ });
+ }
+
+ if (error.status === 403) {
+ return res.status(429).json({
+ error: 'Rate limit exceeded. Please try again later.'
+ });
+ }
+
+ if (error.status === 404) {
+ return res.status(500).json({
+ error: 'Repository not found. Please contact support.'
+ });
+ }
+
+ if (error.status === 422) {
+ return res.status(400).json({
+ error: 'Invalid file path. Please try again.'
+ });
+ }
+
+ // Generic error
+ return res.status(500).json({
+ error: 'Failed to share to GitHub. Please try again later.'
+ });
+ }
+};
+
diff --git a/api/waitlist.js b/api/waitlist.js
new file mode 100644
index 0000000..24a7c9e
--- /dev/null
+++ b/api/waitlist.js
@@ -0,0 +1,129 @@
+// Vercel serverless function to handle waitlist submissions
+// This keeps Google Sheets credentials private (server-side only)
+
+module.exports = async function handler(req, res) {
+ // Only allow POST requests
+ if (req.method !== 'POST') {
+ return res.status(405).json({ error: 'Method not allowed' });
+ }
+
+ // CORS headers for frontend access
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Methods', 'POST');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
+
+ // Handle preflight requests
+ if (req.method === 'OPTIONS') {
+ return res.status(200).end();
+ }
+
+ try {
+ const { email, name, involvement } = req.body;
+
+ // Validate email
+ if (!email || !email.includes('@')) {
+ return res.status(400).json({ error: 'Valid email is required' });
+ }
+
+ // Validate name
+ if (!name || name.trim() === '') {
+ return res.status(400).json({ error: 'Name is required' });
+ }
+
+ // Validate involvement
+ if (!involvement || involvement.trim() === '') {
+ return res.status(400).json({ error: 'Please describe your desired involvement' });
+ }
+
+ // Get credentials from environment variables
+ const credentials = process.env.GOOGLE_SERVICE_ACCOUNT;
+ const spreadsheetId = process.env.GOOGLE_SHEET_ID;
+ const sheetName = process.env.GOOGLE_SHEET_NAME || 'Waitlist';
+
+ if (!credentials || !spreadsheetId) {
+ console.error('Missing Google Sheets configuration');
+ return res.status(500).json({ error: 'Server configuration error' });
+ }
+
+ // Parse service account credentials
+ // Trim whitespace and handle multi-line JSON from environment variable
+ const credentialsTrimmed = credentials.trim();
+ let serviceAccount;
+ try {
+ serviceAccount = JSON.parse(credentialsTrimmed);
+ } catch (parseError) {
+ console.error('Failed to parse service account credentials:', parseError.message);
+ return res.status(500).json({ error: 'Server configuration error' });
+ }
+
+ // Import googleapis (will be installed as dependency)
+ const { google } = require('googleapis');
+
+ // Authenticate with Google Sheets API
+ const auth = new google.auth.GoogleAuth({
+ credentials: serviceAccount,
+ scopes: ['https://www.googleapis.com/auth/spreadsheets'],
+ });
+
+ const sheets = google.sheets({ version: 'v4', auth });
+
+ // Prepare the data to append
+ const timestamp = new Date().toISOString();
+ const values = [[timestamp, email, name || '', involvement || '']];
+
+ // Append to the sheet
+ await sheets.spreadsheets.values.append({
+ spreadsheetId,
+ range: `${sheetName}!A:D`,
+ valueInputOption: 'USER_ENTERED',
+ insertDataOption: 'INSERT_ROWS',
+ resource: {
+ values,
+ },
+ });
+
+ // Success response
+ return res.status(200).json({
+ success: true,
+ message: 'Successfully joined the waitlist!'
+ });
+
+ } catch (error) {
+ // Log detailed error for debugging (visible in Vercel logs)
+ console.error('Error adding to waitlist:', {
+ message: error.message,
+ code: error.code,
+ status: error.response?.status,
+ statusText: error.response?.statusText,
+ details: error.response?.data,
+ stack: error.stack
+ });
+
+ // Provide more specific error messages for common issues
+ if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
+ return res.status(500).json({
+ error: 'Network error connecting to Google Sheets. Please try again later.'
+ });
+ }
+
+ if (error.response?.status === 403) {
+ console.error('Permission denied - check service account has access to sheet');
+ return res.status(500).json({
+ error: 'Permission error. Please contact support.'
+ });
+ }
+
+ if (error.response?.status === 404) {
+ console.error('Sheet not found - check spreadsheet ID and sheet name');
+ return res.status(500).json({
+ error: 'Sheet configuration error. Please contact support.'
+ });
+ }
+
+ // Don't expose internal errors to client
+ return res.status(500).json({
+ error: 'Failed to join waitlist. Please try again later.'
+ });
+ }
+}
+
diff --git a/commonshublogo.svg b/commonshublogo.svg
new file mode 100644
index 0000000..2c7cd45
--- /dev/null
+++ b/commonshublogo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/community-partners.js b/community-partners.js
new file mode 100644
index 0000000..c43ea49
--- /dev/null
+++ b/community-partners.js
@@ -0,0 +1,84 @@
+// Dynamically load community partners
+document.addEventListener('DOMContentLoaded', function() {
+ const partnersContainer = document.getElementById('partners-container');
+ if (!partnersContainer) return;
+
+ // Define partners with placeholder links
+ // TODO: Replace placeholder URLs with actual partner websites
+ const partners = [
+ {
+ name: 'Hubs Network',
+ logo: 'hubs-network.jpg',
+ url: 'https://www.hubsnetwork.org'
+ },
+ {
+ name: 'Invisible Garden',
+ logo: 'invisible-garden.svg',
+ url: 'https://invisible.garden'
+ },
+ {
+ name: 'Understories',
+ logo: 'understories.png',
+ url: 'https://understories.github.io'
+ },
+ {
+ name: 'P2P Foundation',
+ logo: 'p2p.jpeg',
+ url: 'https://p2pfoundation.net/'
+ },
+ {
+ name: 'Crypto Commons Association',
+ logo: 'cca-logo.png',
+ url: 'https://www.crypto-commons.org'
+ },
+ {
+ name: 'Collaborative Finance',
+ logo: 'cofi_cropped.png',
+ url: 'https://www.collaborative-finance.net'
+ },
+ {
+ name: 'Akasha Hub',
+ logo: 'akasha.png',
+ url: 'https://akasha.barcelona'
+ },
+ {
+ name: 'dOrg',
+ logo: 'd0rg.png',
+ url: 'https://www.dorg.tech'
+ },
+ {
+ name: 'FarmLab',
+ logo: 'farmlab.png',
+ url: 'https://www.farmlab.at'
+ }
+ ];
+
+ // Load each partner
+ partners.forEach(partner => {
+ loadPartner(partner);
+ });
+
+ function loadPartner(partner) {
+ try {
+ // Create partner link
+ const partnerLink = document.createElement('a');
+ partnerLink.href = partner.url;
+ partnerLink.target = '_blank';
+ partnerLink.rel = 'noopener noreferrer';
+ partnerLink.className = 'partner-link';
+
+ // Create logo image
+ const img = document.createElement('img');
+ img.src = `community-partners/${partner.logo}`;
+ img.alt = partner.name;
+ img.className = 'partner-logo';
+ img.loading = 'lazy';
+
+ // Assemble link
+ partnerLink.appendChild(img);
+ partnersContainer.appendChild(partnerLink);
+ } catch (error) {
+ console.error(`Error loading partner ${partner.name}:`, error);
+ }
+ }
+});
diff --git a/community-partners/akasha.png b/community-partners/akasha.png
new file mode 100644
index 0000000..047cc74
Binary files /dev/null and b/community-partners/akasha.png differ
diff --git a/community-partners/cca-logo.png b/community-partners/cca-logo.png
new file mode 100644
index 0000000..8229de6
Binary files /dev/null and b/community-partners/cca-logo.png differ
diff --git a/community-partners/cofi_cropped.png b/community-partners/cofi_cropped.png
new file mode 100644
index 0000000..b78d5b3
Binary files /dev/null and b/community-partners/cofi_cropped.png differ
diff --git a/community-partners/d0rg.png b/community-partners/d0rg.png
new file mode 100644
index 0000000..a9f292d
Binary files /dev/null and b/community-partners/d0rg.png differ
diff --git a/community-partners/farmlab.png b/community-partners/farmlab.png
new file mode 100644
index 0000000..1031aae
Binary files /dev/null and b/community-partners/farmlab.png differ
diff --git a/community-partners/hubs-network.jpg b/community-partners/hubs-network.jpg
new file mode 100644
index 0000000..1462303
Binary files /dev/null and b/community-partners/hubs-network.jpg differ
diff --git a/community-partners/invisible-garden.svg b/community-partners/invisible-garden.svg
new file mode 100644
index 0000000..f19e85e
--- /dev/null
+++ b/community-partners/invisible-garden.svg
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/community-partners/p2p.jpeg b/community-partners/p2p.jpeg
new file mode 100644
index 0000000..227fc3e
Binary files /dev/null and b/community-partners/p2p.jpeg differ
diff --git a/community-partners/understories.png b/community-partners/understories.png
new file mode 100644
index 0000000..a5e8933
Binary files /dev/null and b/community-partners/understories.png differ
diff --git a/docker-compose.yml b/docker-compose.yml
index 6d4e1d1..cbb7b81 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,11 +1,14 @@
services:
- valley-commons:
+ votc:
build: .
+ container_name: votc
restart: unless-stopped
+ env_file:
+ - .env
labels:
- "traefik.enable=true"
- - "traefik.http.routers.valley-commons.rule=Host(`valleyofthecommons.org`) || Host(`www.valleyofthecommons.org`)"
- - "traefik.http.services.valley-commons.loadbalancer.server.port=80"
+ - "traefik.http.routers.votc.rule=Host(`votc.jeffemmett.com`)"
+ - "traefik.http.services.votc.loadbalancer.server.port=3000"
networks:
- traefik-public
diff --git a/game.css b/game.css
new file mode 100644
index 0000000..2d7157d
--- /dev/null
+++ b/game.css
@@ -0,0 +1,828 @@
+/* Terminal Game Page Styles */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ width: 100%;
+ height: 100vh;
+ background: #000;
+ color: #00ff00;
+ font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+ padding: 0;
+}
+
+/* GitHub Repo Link & Privacy FAQ */
+.top-links {
+ position: fixed;
+ top: 1rem;
+ left: 1rem;
+ z-index: 1000;
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+}
+
+.home-link,
+.github-link,
+.privacy-link {
+ color: #666;
+ opacity: 0.5;
+ transition: opacity 0.2s ease, color 0.2s ease;
+ text-decoration: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ /* Increase touch target size for mobile */
+ padding: 0.5rem;
+ min-width: 44px;
+ min-height: 44px;
+}
+
+.home-link:hover,
+.github-link:hover,
+.privacy-link:hover {
+ opacity: 1;
+ color: #00ff00;
+}
+
+.home-link svg {
+ width: 18px;
+ height: 18px;
+}
+
+.github-link svg,
+.privacy-link svg {
+ width: 20px;
+ height: 20px;
+}
+
+.terminal {
+ width: 100%;
+ flex: 1;
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ overflow: hidden;
+ padding: var(--spacing-lg, 2rem);
+ padding-top: 4rem; /* Space for top links */
+ padding-bottom: 0; /* Input line handles its own space */
+ position: relative;
+ min-height: 0; /* Allow flex shrinking */
+}
+
+.terminal-content {
+ width: 100%;
+ max-width: 800px;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ position: relative;
+ min-height: 0; /* Allow flex shrinking */
+}
+
+.terminal-line {
+ display: flex;
+ align-items: flex-start;
+ margin-bottom: 1rem;
+ font-size: 1.1rem;
+ line-height: 1.6;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+}
+
+.prompt {
+ color: #00ff00;
+ margin-right: 0.5rem;
+ font-weight: bold;
+ flex-shrink: 0; /* Prevent prompt from shrinking */
+}
+
+.question {
+ color: #fff;
+ font-size: clamp(1.2rem, 3vw, 1.5rem);
+}
+
+#input-line {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: #000;
+ padding: 1rem 0;
+ margin-top: 0;
+ z-index: 10;
+ border-top: 1px solid #333;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ flex-wrap: nowrap;
+ width: 100%;
+ box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.5); /* Subtle shadow to separate from content */
+}
+
+#terminal-input {
+ background: transparent;
+ border: none;
+ outline: none;
+ color: #00ff00;
+ font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
+ font-size: 1.1rem;
+ flex: 1;
+ padding: 0;
+ margin-left: 0.5rem;
+ min-width: 0; /* Allow flex shrinking */
+}
+
+.submit-button {
+ background: #00ff00;
+ color: #000;
+ border: 1px solid #00ff00;
+ padding: 0.5rem 1rem;
+ font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
+ font-size: 0.9rem;
+ font-weight: bold;
+ cursor: pointer;
+ white-space: nowrap;
+ flex-shrink: 0;
+ transition: background 0.2s ease, color 0.2s ease;
+}
+
+.submit-button:hover {
+ background: #00cc00;
+ border-color: #00cc00;
+}
+
+.submit-button:active {
+ background: #009900;
+ border-color: #009900;
+}
+
+.submit-button:disabled {
+ background: #333;
+ color: #666;
+ border-color: #333;
+ cursor: not-allowed;
+}
+
+#terminal-input::placeholder {
+ color: #555;
+}
+
+.cursor {
+ color: #00ff00;
+ animation: blink 1s infinite;
+ margin-left: 2px;
+}
+
+@keyframes blink {
+ 0%, 50% { opacity: 1; }
+ 51%, 100% { opacity: 0; }
+}
+
+.terminal-output {
+ margin-top: 2rem;
+ color: #fff;
+ font-size: 1rem;
+ line-height: 1.8;
+ white-space: pre-wrap;
+ opacity: 0;
+ animation: fadeIn 0.5s ease-in forwards;
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding-bottom: 6rem; /* Space for input line - increased to prevent overlap */
+ -webkit-overflow-scrolling: touch;
+ min-height: 0; /* Allow flex shrinking */
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+}
+
+.terminal-output.show {
+ opacity: 1;
+}
+
+.terminal-output p {
+ margin-bottom: 1rem;
+}
+
+.terminal-output ul {
+ list-style: none;
+ margin: 1.5rem 0;
+ padding-left: 0;
+}
+
+.terminal-output li {
+ margin-bottom: 0.8rem;
+ padding-left: 1.5rem;
+ position: relative;
+}
+
+.terminal-output li:before {
+ content: "→";
+ position: absolute;
+ left: 0;
+ color: #00ff00;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.ai-message-streaming {
+ animation: blink 1s infinite;
+}
+
+.user-message {
+ color: #00ff00;
+ flex: 1;
+ min-width: 0;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+}
+
+.ai-message {
+ color: #fff;
+ flex: 1;
+ min-width: 0;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+}
+
+.loading-indicator {
+ opacity: 0.7;
+}
+
+.loading-text {
+ color: #666;
+ font-style: italic;
+}
+
+.error-message {
+ margin-top: 1rem;
+}
+
+.error-text {
+ color: #ff4444;
+}
+
+/* Message selection and sharing */
+.message-line {
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+ padding: 0.5rem 0.75rem;
+ margin: 0.5rem 0;
+ border-radius: 2px;
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+}
+
+.message-line:hover {
+ background-color: rgba(0, 255, 0, 0.1);
+}
+
+.message-line.selected {
+ background-color: rgba(0, 255, 0, 0.2);
+ border-left: 2px solid #00ff00;
+ padding-left: 0.75rem;
+}
+
+/* User messages appear below AI messages with distinct styling */
+.message-line[data-role="user"] {
+ margin-top: 0.75rem;
+}
+
+.message-line[data-role="assistant"] {
+ margin-bottom: 0.25rem;
+}
+
+.share-buttons-container {
+ margin-top: 1rem;
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.full-chat-share-container {
+ margin-top: 1rem;
+ margin-bottom: 1rem;
+ display: flex;
+ justify-content: center;
+ position: absolute;
+ bottom: 4rem; /* Above input line */
+ left: 0;
+ right: 0;
+ background: transparent;
+ padding: 0.5rem 0;
+ z-index: 9;
+ pointer-events: none; /* Allow clicks to pass through to scrollable area */
+}
+
+.full-chat-share-container button {
+ pointer-events: auto; /* Re-enable clicks for the button */
+}
+
+/* Desktop: Keep input fixed at bottom like real CLI */
+@media (min-width: 769px) {
+ #input-line {
+ position: absolute;
+ bottom: 0;
+ }
+
+ .terminal-output {
+ padding-bottom: 6rem; /* Space for fixed input line - increased to prevent overlap */
+ }
+
+ .full-chat-share-container {
+ position: absolute;
+ bottom: 4rem;
+ }
+
+ /* Ensure desktop input is at least 16px to prevent zoom on some tablets */
+ #terminal-input {
+ font-size: max(1.1rem, 16px);
+ }
+}
+
+.share-button {
+ padding: 0.5rem 1rem;
+ background: transparent;
+ border: 2px solid #00ff00;
+ color: #00ff00;
+ font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
+ font-size: 0.85rem;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ flex: 1;
+ min-width: 150px;
+}
+
+.share-button:hover:not(:disabled) {
+ background: #00ff00;
+ color: #000;
+}
+
+.share-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.share-button-default {
+ border-color: #00ff00;
+}
+
+.share-button-own {
+ border-color: #666;
+ color: #999;
+}
+
+.share-button-own:hover:not(:disabled) {
+ background: #666;
+ color: #000;
+ border-color: #666;
+}
+
+.success-message {
+ margin-top: 1rem;
+}
+
+.success-text {
+ color: #00ff00;
+}
+
+.success-text a {
+ color: #00ff00;
+ text-decoration: underline;
+}
+
+.success-text a:hover {
+ text-decoration: none;
+}
+
+/* LLM Information Footer */
+.llm-info {
+ position: fixed;
+ bottom: 4rem; /* Above input line */
+ left: 0.5rem;
+ font-size: 0.6rem;
+ color: #666;
+ max-width: 300px;
+ line-height: 1.3;
+ z-index: 8;
+ pointer-events: none; /* Allow clicks to pass through unless on a link */
+}
+
+.llm-info a {
+ color: #00ff00;
+ text-decoration: none;
+ pointer-events: auto;
+}
+
+.llm-info a:hover {
+ text-decoration: underline;
+}
+
+.llm-info p {
+ margin: 0.25rem 0;
+}
+
+.model-note {
+ font-size: 0.55rem;
+ color: #555;
+ margin-top: 0.25rem;
+ font-style: italic;
+ line-height: 1.2;
+}
+
+/* Share Modal */
+.share-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.9);
+ z-index: 2000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+}
+
+.share-modal-content {
+ background: #000;
+ border: 2px solid #00ff00;
+ width: 100%;
+ max-width: 900px;
+ max-height: 90vh;
+ display: flex;
+ flex-direction: column;
+ color: #00ff00;
+ font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
+}
+
+.share-modal-header {
+ padding: 1rem;
+ border-bottom: 1px solid #00ff00;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.share-modal-header h2 {
+ margin: 0;
+ font-size: 1.2rem;
+ color: #00ff00;
+}
+
+.share-modal-close {
+ background: transparent;
+ border: none;
+ color: #00ff00;
+ font-size: 2rem;
+ cursor: pointer;
+ padding: 0;
+ width: 2rem;
+ height: 2rem;
+ line-height: 1;
+ transition: color 0.2s ease;
+}
+
+.share-modal-close:hover {
+ color: #fff;
+}
+
+.share-modal-body {
+ flex: 1;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ padding: 1rem;
+ gap: 1rem;
+}
+
+.share-template-section,
+.share-history-section {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.share-template-section {
+ flex: 1;
+ min-height: 0;
+}
+
+.share-template-section label,
+.share-history-section label {
+ font-size: 0.85rem;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.share-template-editor {
+ flex: 1;
+ min-height: 200px;
+ background: #111;
+ border: 1px solid #333;
+ color: #00ff00;
+ font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
+ font-size: 0.85rem;
+ padding: 1rem;
+ resize: vertical;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+.share-template-editor:focus {
+ outline: 2px solid #00ff00;
+ border-color: #00ff00;
+}
+
+.share-history-section {
+ flex: 0 0 200px;
+ min-height: 0;
+}
+
+.share-history-viewer {
+ flex: 1;
+ overflow-y: auto;
+ background: #111;
+ border: 1px solid #333;
+ padding: 1rem;
+ font-size: 0.8rem;
+ max-height: 200px;
+}
+
+.share-history-viewer .terminal-line {
+ margin-bottom: 0.5rem;
+ padding: 0.25rem 0;
+}
+
+.share-history-viewer .user-message {
+ color: #00ff00;
+}
+
+.share-history-viewer .assistant-message {
+ color: #fff;
+}
+
+.share-modal-footer {
+ padding: 1rem;
+ border-top: 1px solid #00ff00;
+ display: flex;
+ gap: 0.5rem;
+ justify-content: flex-end;
+}
+
+.share-modal-footer .share-button {
+ min-width: 150px;
+}
+
+@media (max-width: 768px) {
+ /* Mobile-first: Prevent body scroll, handle in terminal */
+ body {
+ overflow: hidden;
+ height: 100vh;
+ height: 100dvh; /* Dynamic viewport height for mobile browsers */
+ position: fixed;
+ width: 100%;
+ }
+
+ .terminal {
+ padding: 0;
+ padding-top: 3rem; /* Space for top links */
+ padding-left: 0.75rem;
+ padding-right: 0.75rem;
+ height: 100vh;
+ height: 100dvh;
+ max-height: 100vh;
+ max-height: 100dvh;
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+ /* Allow scrolling when keyboard appears */
+ }
+
+ .terminal-content {
+ max-width: 100%;
+ min-height: 100%;
+ padding: 0;
+ padding-bottom: 2rem; /* Space at bottom */
+ display: flex;
+ flex-direction: column;
+ }
+
+ /* Initial question - smaller on mobile */
+ .terminal-line:first-child {
+ font-size: 0.9rem;
+ margin-bottom: 1rem;
+ padding: 0.5rem 0;
+ }
+
+ .question {
+ font-size: 0.95rem;
+ line-height: 1.4;
+ }
+
+ /* Chat output - scrollable area, input appears below */
+ .terminal-output {
+ margin-top: 1rem;
+ font-size: 0.9rem;
+ line-height: 1.6;
+ padding-bottom: 0.5rem;
+ flex: 1;
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+ margin-bottom: 0;
+ /* Input will appear below in natural flow */
+ }
+
+ /* Input line - follows chat content, appears below messages */
+ #input-line {
+ position: relative;
+ padding: 0.75rem;
+ padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
+ margin-top: 1rem;
+ margin-bottom: 1rem;
+ background: #000;
+ border-top: 1px solid #333;
+ z-index: 10;
+ /* Ensure safe area on iOS */
+ padding-left: max(0.75rem, env(safe-area-inset-left));
+ padding-right: max(0.75rem, env(safe-area-inset-right));
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ /* Sticky at bottom when keyboard is closed, but follows content when keyboard opens */
+ position: sticky;
+ bottom: 0;
+ }
+
+ #terminal-input {
+ font-size: 16px; /* Prevent zoom on iOS */
+ padding: 0.75rem 0.5rem;
+ min-height: 44px; /* Minimum touch target size */
+ }
+
+ .submit-button {
+ padding: 0.75rem 1rem;
+ font-size: 0.9rem;
+ min-height: 44px; /* Minimum touch target size */
+ min-width: 70px;
+ }
+
+ /* Top links - smaller and positioned */
+ .top-links {
+ position: fixed;
+ top: 0.5rem;
+ left: 0.5rem;
+ z-index: 1000;
+ gap: 0.5rem;
+ padding-left: env(safe-area-inset-left);
+ }
+
+ .home-link svg,
+ .github-link svg,
+ .privacy-link svg {
+ width: 20px;
+ height: 20px;
+ }
+
+ /* Ensure touch targets are large enough on mobile */
+ .home-link,
+ .github-link,
+ .privacy-link {
+ min-width: 44px;
+ min-height: 44px;
+ padding: 0.5rem;
+ }
+
+ /* Full chat share button - appears above input in flow */
+ .full-chat-share-container {
+ position: relative;
+ margin-top: 1rem;
+ margin-bottom: 0.5rem;
+ padding: 0 0.75rem;
+ z-index: 9;
+ display: flex;
+ justify-content: center;
+ }
+
+ .full-chat-share-container button {
+ width: 100%;
+ padding: 0.75rem;
+ font-size: 0.85rem;
+ min-height: 44px;
+ }
+
+ /* LLM info - hide on mobile to save space */
+ .llm-info {
+ display: none !important; /* Hide on mobile to save space */
+ visibility: hidden !important;
+ opacity: 0 !important;
+ height: 0 !important;
+ overflow: hidden !important;
+ }
+
+ /* Share buttons - touch-friendly */
+ .share-button {
+ font-size: 0.9rem;
+ padding: 0.75rem 1rem;
+ min-height: 44px;
+ min-width: auto;
+ }
+
+ /* Message lines - better spacing on mobile */
+ .message-line {
+ padding: 0.75rem 0.5rem;
+ margin: 0.75rem 0;
+ font-size: 0.9rem;
+ line-height: 1.6;
+ }
+
+ /* Share modal - full screen on mobile */
+ .share-modal {
+ padding: 0;
+ align-items: flex-end;
+ }
+
+ .share-modal-content {
+ max-height: 90vh;
+ max-height: 90dvh;
+ border-radius: 1rem 1rem 0 0;
+ width: 100%;
+ max-width: 100%;
+ }
+
+ .share-modal-header {
+ padding: 1rem;
+ border-bottom: 1px solid #333;
+ }
+
+ .share-modal-body {
+ flex-direction: column;
+ padding: 1rem;
+ max-height: calc(90vh - 150px);
+ max-height: calc(90dvh - 150px);
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .share-template-section,
+ .share-history-section {
+ flex: none;
+ margin-bottom: 1rem;
+ }
+
+ .share-history-section {
+ max-height: 200px;
+ }
+
+ .share-modal-footer {
+ flex-direction: column;
+ padding: 1rem;
+ gap: 0.75rem;
+ }
+
+ .share-modal-footer .share-button {
+ width: 100%;
+ min-height: 48px;
+ }
+
+ /* Prompt styling */
+ .prompt {
+ font-size: 0.9rem;
+ margin-right: 0.4rem;
+ }
+
+ /* Prevent text selection on buttons for better touch */
+ .share-button,
+ .submit-button {
+ -webkit-tap-highlight-color: rgba(0, 255, 0, 0.2);
+ -webkit-touch-callout: none;
+ user-select: none;
+ }
+
+ /* Better scrolling */
+ .terminal-output {
+ -webkit-overflow-scrolling: touch;
+ scroll-behavior: smooth;
+ }
+}
diff --git a/game.html b/game.html
new file mode 100644
index 0000000..676dee6
--- /dev/null
+++ b/game.html
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+ Game - Valley of the Commons
+
+
+
+
+
+
+
+
+
+ $
+ Want to help create a game that shapes reality?
+
+
+ >
+
+ █
+ Enter
+
+
+
+
+
+
+
+
+
+ Powered by Mistral Devstral-2 via
+ Vercel AI Gateway
+
+
+ Note: Some models used in MVP are proprietary APIs.
+ Long-term roadmap includes migration to open-weights or self-hosted models.
+
+
+
+
+
+
diff --git a/game.js b/game.js
new file mode 100644
index 0000000..f3cd38b
--- /dev/null
+++ b/game.js
@@ -0,0 +1,895 @@
+// Game Terminal Chat Interface
+// Implements Socratic game master moderator with text streaming
+
+document.addEventListener('DOMContentLoaded', function() {
+ const input = document.getElementById('terminal-input');
+ const output = document.getElementById('output');
+ const cursor = document.querySelector('.cursor');
+
+ // Detect mobile device
+ const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
+ (window.matchMedia && window.matchMedia('(max-width: 768px)').matches);
+
+ let messageHistory = [];
+ let turnCount = 0;
+ const MAX_TURNS = 12;
+ let selectedText = null;
+ let selectedMessages = [];
+
+ // Auto-focus only on desktop (not mobile to avoid immediate keyboard)
+ if (!isMobile) {
+ input.focus();
+ }
+
+ // Initial question is set in HTML
+ // After first response, Socratic game master moderator takes over
+
+ async function sendMessage(userInput) {
+ // Check turn limit
+ if (turnCount >= MAX_TURNS) {
+ displayMessage('system', 'Conversation limit reached. The game master has stepped away.');
+ return;
+ }
+
+ // Add user message to history
+ messageHistory.push({ role: 'user', content: userInput });
+ turnCount++;
+
+ // Display user message
+ displayMessage('user', userInput);
+
+ // Show loading indicator
+ showLoadingIndicator('AI is thinking...');
+
+ try {
+ const response = await fetch('/api/game-chat', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ messages: messageHistory })
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text().catch(() => '');
+ let error;
+ try {
+ error = JSON.parse(errorText);
+ } catch {
+ error = { error: errorText || `API error: ${response.status}` };
+ }
+ throw new Error(error.error || `API error: ${response.status}`);
+ }
+
+ // Check if response body exists
+ if (!response.body) {
+ console.error('[game] No response body received');
+ throw new Error('No response body received');
+ }
+
+ console.log('[game] Response received, starting to read stream');
+
+ // Text stream (simple: just append chunks)
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let aiMessage = '';
+
+ // Create streaming message element
+ const streamingElement = createStreamingMessageElement();
+ console.log('[game] Streaming element created');
+
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) {
+ console.log('[game] Stream complete, received', aiMessage.length, 'characters');
+ break;
+ }
+
+ // Decode chunk and append directly (text stream, no parsing needed)
+ const chunk = decoder.decode(value, { stream: true });
+ console.log('[game] Chunk received:', chunk.substring(0, 50), 'length:', chunk.length);
+ if (chunk) {
+ aiMessage += chunk;
+ // Update UI (typewriter effect)
+ updateStreamingMessage(streamingElement, aiMessage);
+ }
+ }
+ } catch (streamError) {
+ console.error('[game] Stream reading error:', streamError);
+ throw streamError;
+ }
+
+ // Finalize streaming element
+ finalizeStreamingMessage(streamingElement, 'assistant', aiMessage);
+
+ // Add to history
+ messageHistory.push({ role: 'assistant', content: aiMessage });
+ hideLoadingIndicator();
+
+ // Show full chat share button if we have conversation history
+ if (messageHistory.length >= 2) {
+ showFullChatShareButton();
+ }
+
+ // Check if approaching limit
+ if (turnCount >= MAX_TURNS - 2) {
+ displayMessage('system', `[${MAX_TURNS - turnCount} exchanges remaining]`);
+ }
+
+ } catch (error) {
+ displayError('Connection error. Please try again.');
+ hideLoadingIndicator();
+ console.error('Chat error:', error);
+ }
+ }
+
+ function displayMessage(role, content) {
+ const output = document.getElementById('output');
+ const line = document.createElement('div');
+ line.className = 'terminal-line message-line';
+ line.dataset.role = role;
+ line.dataset.content = content;
+
+ const prompt = document.createElement('span');
+ prompt.className = 'prompt';
+ prompt.textContent = role === 'user' ? '>' : '$';
+ prompt.style.flexShrink = '0'; // Prevent prompt from shrinking
+
+ const text = document.createElement('span');
+ text.className = role === 'user' ? 'user-message' : 'ai-message';
+ text.textContent = content;
+
+ line.appendChild(prompt);
+ line.appendChild(text);
+ output.appendChild(line);
+ output.scrollTop = output.scrollHeight;
+
+ // Add click handler for selection
+ line.addEventListener('click', function() {
+ selectMessage(line, role, content);
+ });
+ }
+
+ function createStreamingMessageElement() {
+ const output = document.getElementById('output');
+ const line = document.createElement('div');
+ line.className = 'terminal-line';
+ line.style.display = 'flex';
+ line.style.alignItems = 'flex-start';
+ line.style.gap = '0.5rem';
+
+ const prompt = document.createElement('span');
+ prompt.className = 'prompt';
+ prompt.textContent = '$';
+ prompt.style.flexShrink = '0';
+
+ const text = document.createElement('span');
+ text.className = 'ai-message ai-message-streaming';
+ text.textContent = '';
+ text.style.flex = '1';
+
+ line.appendChild(prompt);
+ line.appendChild(text);
+ output.appendChild(line);
+
+ return text;
+ }
+
+ function updateStreamingMessage(element, content) {
+ if (!element) {
+ console.error('[game] updateStreamingMessage: element is null');
+ return;
+ }
+ element.textContent = content;
+ const output = document.getElementById('output');
+ if (output) {
+ output.scrollTop = output.scrollHeight;
+ }
+ console.log('[game] Updated streaming message, length:', content.length);
+ }
+
+ function finalizeStreamingMessage(element, role, content) {
+ element.classList.remove('ai-message-streaming');
+ // Add dataset attributes for message selection
+ const line = element.closest('.terminal-line');
+ if (line) {
+ line.classList.add('message-line');
+ const finalRole = role || 'assistant';
+ const finalContent = content || element.textContent;
+ line.dataset.role = finalRole;
+ line.dataset.content = finalContent;
+ // Ensure proper layout (flexbox already set in createStreamingMessageElement)
+ // Add click handler for selection
+ line.addEventListener('click', function() {
+ selectMessage(line, finalRole, finalContent);
+ });
+ }
+ }
+
+ function showLoadingIndicator(message) {
+ const output = document.getElementById('output');
+ const indicator = document.createElement('div');
+ indicator.className = 'terminal-line loading-indicator';
+ indicator.id = 'loading-indicator';
+
+ const prompt = document.createElement('span');
+ prompt.className = 'prompt';
+ prompt.textContent = '$';
+
+ const text = document.createElement('span');
+ text.className = 'loading-text';
+ text.textContent = message;
+
+ indicator.appendChild(prompt);
+ indicator.appendChild(text);
+ output.appendChild(indicator);
+ output.scrollTop = output.scrollHeight;
+ }
+
+ function hideLoadingIndicator() {
+ const indicator = document.getElementById('loading-indicator');
+ if (indicator) {
+ indicator.remove();
+ }
+ }
+
+ function displayError(message) {
+ const output = document.getElementById('output');
+ const line = document.createElement('div');
+ line.className = 'terminal-line error-message';
+
+ const prompt = document.createElement('span');
+ prompt.className = 'prompt';
+ prompt.textContent = '!';
+ prompt.style.color = '#ff4444';
+
+ const text = document.createElement('span');
+ text.className = 'error-text';
+ text.textContent = message;
+ text.style.color = '#ff4444';
+
+ line.appendChild(prompt);
+ line.appendChild(text);
+ output.appendChild(line);
+ output.scrollTop = output.scrollHeight;
+ }
+
+ // Handle Enter key and submit button
+ function handleSubmit() {
+ const userInput = input.value.trim();
+ if (userInput) {
+ sendMessage(userInput);
+ input.value = '';
+ // Only auto-focus on desktop (not mobile)
+ if (!isMobile) {
+ input.focus();
+ }
+ }
+ }
+
+ input.addEventListener('keydown', function(e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ handleSubmit();
+ }
+ });
+
+ // Submit button for mobile
+ const submitButton = document.getElementById('submit-button');
+ if (submitButton) {
+ submitButton.addEventListener('click', handleSubmit);
+ }
+
+ // Keep focus on input (only on desktop, not mobile to avoid interference)
+ if (!isMobile) {
+ input.addEventListener('blur', function() {
+ if (turnCount < MAX_TURNS) {
+ input.focus();
+ }
+ });
+ }
+
+ // Text selection functionality
+ function selectMessage(line, role, content) {
+ // Clear previous selection
+ clearSelection();
+
+ // Select this message
+ line.classList.add('selected');
+ selectedText = `${role === 'user' ? '>' : '$'} ${content}`;
+ selectedMessages = [{ role, content }];
+
+ // Show share button
+ showShareButton();
+ }
+
+ function clearSelection() {
+ document.querySelectorAll('.message-line.selected').forEach(line => {
+ line.classList.remove('selected');
+ });
+ selectedText = null;
+ selectedMessages = [];
+ hideShareButton();
+ }
+
+ function showShareButton() {
+ // Remove existing buttons if any
+ hideShareButton();
+
+ const output = document.getElementById('output');
+ const buttonContainer = document.createElement('div');
+ buttonContainer.id = 'share-buttons-container';
+ buttonContainer.className = 'share-buttons-container';
+
+ // Share button (opens modal)
+ const shareButton = document.createElement('button');
+ shareButton.id = 'share-btn';
+ shareButton.className = 'share-button share-button-default';
+ shareButton.textContent = 'share on github repo';
+ shareButton.addEventListener('click', () => showShareModal());
+
+ buttonContainer.appendChild(shareButton);
+ output.appendChild(buttonContainer);
+ }
+
+ function hideShareButton() {
+ const container = document.getElementById('share-buttons-container');
+ if (container) {
+ container.remove();
+ }
+ }
+
+ function showFullChatShareButton() {
+ // Remove existing button if any
+ hideFullChatShareButton();
+
+ const container = document.getElementById('full-chat-share-container');
+ if (!container) return;
+
+ const button = document.createElement('button');
+ button.id = 'full-chat-share-btn';
+ button.className = 'share-button share-button-own';
+ button.textContent = 'add whole chat history to github repo';
+ button.addEventListener('click', () => showShareFullChatModal());
+
+ container.appendChild(button);
+ }
+
+ function hideFullChatShareButton() {
+ const container = document.getElementById('full-chat-share-container');
+ if (container) {
+ container.innerHTML = '';
+ }
+ }
+
+ function generateIdeaTemplate(selectedText, selectedMessages, fullHistory) {
+ const timestamp = new Date().toISOString();
+ const date = new Date().toLocaleString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ timeZoneName: 'short'
+ });
+
+ // Extract key themes from conversation context
+ const assistantMessages = fullHistory.filter(m => m.role === 'assistant').map(m => m.content);
+
+ // Generate a title suggestion from selected text (first line or first 60 chars)
+ const firstLine = selectedText.split('\n')[0];
+ const titleSuggestion = firstLine.length > 60
+ ? firstLine.substring(0, 60).trim() + '...'
+ : firstLine.trim();
+
+ // Extract key themes from recent assistant messages
+ const keyThemes = assistantMessages.length > 0
+ ? assistantMessages.slice(-3).map(msg => {
+ // Extract first question or key phrase (first sentence or first 100 chars)
+ const firstSentence = msg.split(/[.!?]/)[0] || msg.substring(0, 100);
+ return firstSentence.trim();
+ }).filter(t => t.length > 0)
+ : ['Emerging from dialogue'];
+
+ return `# Idea: ${titleSuggestion}
+
+**Source:** Valley of the Commons Game Master Dialogue
+**Date:** ${date}
+**Selected Excerpt:**
+
+${selectedText}
+
+---
+
+## Context
+
+This idea emerged from a Socratic dialogue about game design in the Valley of the Commons.
+
+**Key Themes:**
+${keyThemes.map(theme => `- ${theme}`).join('\n')}
+
+---
+
+## Next Steps
+
+- [ ] Refine this idea
+- [ ] Connect to other ideas
+- [ ] Propose as a tool/rule/quest/place
+- [ ] Document implementation approach
+
+---
+
+## Full Conversation History
+
+${formatChatHistory(fullHistory)}
+
+---
+
+*Generated from game master conversation*`;
+ }
+
+ function generateFullChatTemplate(fullHistory) {
+ const timestamp = new Date().toISOString();
+ const date = new Date().toLocaleString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ timeZoneName: 'short'
+ });
+
+ // Generate title from first user message or first AI response
+ const firstUserMessage = fullHistory.find(m => m.role === 'user');
+ const firstAIMessage = fullHistory.find(m => m.role === 'assistant');
+ const titleSource = firstUserMessage?.content || firstAIMessage?.content || 'Conversation';
+ const titleSuggestion = titleSource.length > 60
+ ? titleSource.substring(0, 60).trim() + '...'
+ : titleSource.trim();
+
+ // Extract key themes from assistant messages
+ const assistantMessages = fullHistory.filter(m => m.role === 'assistant').map(m => m.content);
+ const keyThemes = assistantMessages.length > 0
+ ? assistantMessages.slice(0, 5).map(msg => {
+ const firstSentence = msg.split(/[.!?]/)[0] || msg.substring(0, 100);
+ return firstSentence.trim();
+ }).filter(t => t.length > 0)
+ : ['Emerging from dialogue'];
+
+ return `# Conversation: ${titleSuggestion}
+
+**Source:** Valley of the Commons Game Master Dialogue
+**Date:** ${date}
+**Message Count:** ${fullHistory.length} exchanges
+
+---
+
+## Context
+
+This is a complete Socratic dialogue about game design in the Valley of the Commons.
+
+**Key Themes:**
+${keyThemes.map(theme => `- ${theme}`).join('\n')}
+
+---
+
+## Full Conversation History
+
+${formatChatHistory(fullHistory)}
+
+---
+
+## Next Steps
+
+- [ ] Extract specific ideas from this conversation
+- [ ] Connect themes to other conversations
+- [ ] Propose tools/rules/quests/places based on insights
+- [ ] Document implementation approaches
+
+---
+
+*Generated from complete game master conversation*`;
+ }
+
+ function formatChatHistory(history) {
+ if (!history || history.length === 0) return 'No conversation history available.';
+
+ return history.map((msg, index) => {
+ const prefix = msg.role === 'user' ? '>' : '$';
+ const roleLabel = msg.role === 'user' ? 'User' : 'Game Master';
+ return `### ${roleLabel} (${index + 1})
+
+${prefix} ${msg.content}`;
+ }).join('\n\n---\n\n');
+ }
+
+ function showShareFullChatModal() {
+ if (!messageHistory || messageHistory.length === 0) {
+ displayError('No conversation history to share');
+ return;
+ }
+
+ // Get full conversation history
+ const fullHistory = messageHistory;
+
+ // Generate template for full chat
+ const template = generateFullChatTemplate(fullHistory);
+
+ // Create modal
+ const modal = document.createElement('div');
+ modal.id = 'share-modal';
+ modal.className = 'share-modal';
+ modal.setAttribute('role', 'dialog');
+ modal.setAttribute('aria-modal', 'true');
+ modal.setAttribute('aria-labelledby', 'share-modal-title-full');
+
+ modal.innerHTML = `
+
+
+
+
+ Full Conversation Template (editable):
+
+
+
+
Conversation Summary:
+
+
Total messages: ${fullHistory.length}
+
User messages: ${fullHistory.filter(m => m.role === 'user').length}
+
AI messages: ${fullHistory.filter(m => m.role === 'assistant').length}
+
+
+
+
+
+ `;
+
+ document.body.appendChild(modal);
+
+ // Focus management for accessibility
+ const closeButton = document.getElementById('share-modal-close');
+
+ // Focus the close button first (for keyboard navigation)
+ setTimeout(() => {
+ closeButton.focus();
+ }, 100);
+
+ // Event listeners
+ closeButton.addEventListener('click', () => hideShareModal());
+ document.getElementById('share-modal-cancel').addEventListener('click', () => hideShareModal());
+ document.getElementById('share-modal-share-default').addEventListener('click', () => {
+ const content = document.getElementById('share-template-editor').value;
+ shareToGitHub('default', content, fullHistory, true); // Full chat, save to conversations directory
+ });
+ document.getElementById('share-modal-share-own').addEventListener('click', () => {
+ const content = document.getElementById('share-template-editor').value;
+ shareToGitHub('own', content, fullHistory, true); // Full chat, save to conversations directory
+ });
+
+ // Close on outside click
+ modal.addEventListener('click', (e) => {
+ if (e.target === modal) {
+ hideShareModal();
+ }
+ });
+
+ // Close on Escape key
+ const handleEscape = (e) => {
+ if (e.key === 'Escape') {
+ hideShareModal();
+ document.removeEventListener('keydown', handleEscape);
+ }
+ };
+ document.addEventListener('keydown', handleEscape);
+ }
+
+ function showShareModal() {
+ if (!selectedText || selectedMessages.length === 0) {
+ displayError('No text selected');
+ return;
+ }
+
+ // Get full conversation history
+ const fullHistory = messageHistory;
+
+ // Generate template
+ const template = generateIdeaTemplate(selectedText, selectedMessages, fullHistory);
+
+ // Create modal
+ const modal = document.createElement('div');
+ modal.id = 'share-modal';
+ modal.className = 'share-modal';
+ modal.setAttribute('role', 'dialog');
+ modal.setAttribute('aria-modal', 'true');
+ modal.setAttribute('aria-labelledby', 'share-modal-title-idea');
+
+ modal.innerHTML = `
+
+
+
+
+ Idea Template (editable):
+
+
+
+
Full Conversation History:
+
+ ${formatChatHistoryForDisplay(fullHistory)}
+
+
+
+
+
+ `;
+
+ document.body.appendChild(modal);
+
+ // Focus management for accessibility
+ const closeButton = document.getElementById('share-modal-close');
+ const textarea = document.getElementById('share-template-editor');
+
+ // Focus the close button first (for keyboard navigation)
+ setTimeout(() => {
+ closeButton.focus();
+ }, 100);
+
+ // Event listeners
+ closeButton.addEventListener('click', () => hideShareModal());
+ document.getElementById('share-modal-cancel').addEventListener('click', () => hideShareModal());
+ document.getElementById('share-modal-share-default').addEventListener('click', () => {
+ const content = document.getElementById('share-template-editor').value;
+ shareToGitHub('default', content, fullHistory, false); // Selected excerpt, not full chat
+ });
+ document.getElementById('share-modal-share-own').addEventListener('click', () => {
+ const content = document.getElementById('share-template-editor').value;
+ shareToGitHub('own', content, fullHistory, false); // Selected excerpt, not full chat
+ });
+
+ // Close on outside click
+ modal.addEventListener('click', (e) => {
+ if (e.target === modal) {
+ hideShareModal();
+ }
+ });
+
+ // Close on Escape key
+ const handleEscape = (e) => {
+ if (e.key === 'Escape') {
+ hideShareModal();
+ document.removeEventListener('keydown', handleEscape);
+ }
+ };
+ document.addEventListener('keydown', handleEscape);
+ }
+
+ function formatChatHistoryForDisplay(history) {
+ if (!history || history.length === 0) return 'No conversation history available.
';
+
+ return history.map((msg) => {
+ const prefix = msg.role === 'user' ? '>' : '$';
+ const roleClass = msg.role === 'user' ? 'user-message' : 'assistant-message';
+ return `${prefix} ${escapeHtml(msg.content)}
`;
+ }).join('');
+ }
+
+ function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
+ function hideShareModal() {
+ const modal = document.getElementById('share-modal');
+ if (modal) {
+ // Return focus to the element that opened the modal (if available)
+ const previouslyFocused = document.activeElement;
+ modal.remove();
+ // Focus back to input if it was previously focused
+ if (input && document.activeElement === document.body) {
+ input.focus();
+ }
+ }
+ }
+
+ async function shareToGitHub(method, content, fullHistory, isFullChat = false) {
+ if (!content) {
+ displayError('No content to share');
+ return;
+ }
+
+ const shareButton = document.getElementById(`share-modal-share-${method === 'default' ? 'default' : 'own'}`);
+ const cancelButton = document.getElementById('share-modal-cancel');
+
+ if (shareButton) {
+ shareButton.disabled = true;
+ shareButton.textContent = 'Sharing...';
+ }
+ if (cancelButton) {
+ cancelButton.disabled = true;
+ }
+
+ if (method === 'own') {
+ // Redirect to GitHub to create file directly with user's account
+ shareWithOwnAccount(content, isFullChat);
+ hideShareModal();
+ return;
+ }
+
+ // Default: Use API with default account
+ try {
+ const response = await fetch('/api/share-to-github', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ content: content, // Full template with chat history
+ excerpt: selectedText, // Keep for backwards compatibility
+ isFullChat: isFullChat, // Flag to save to conversations directory
+ context: {
+ messages: selectedMessages,
+ fullHistory: fullHistory
+ }
+ })
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || 'Failed to share');
+ }
+
+ // Show success message
+ displaySuccess(data.url, data.filename);
+
+ // Clear selection and close modal
+ hideShareModal();
+ clearSelection();
+
+ } catch (error) {
+ displayError(`Failed to share: ${error.message}`);
+ if (shareButton) {
+ shareButton.disabled = false;
+ shareButton.textContent = method === 'default' ? 'Share (directly with dev github vrnvrn)' : 'Share (open github in new tab)';
+ }
+ if (cancelButton) {
+ cancelButton.disabled = false;
+ }
+ }
+ }
+
+ async function shareWithOwnAccount(content, isFullChat = false) {
+ // Determine directory and filename based on whether it's a full chat or excerpt
+ const basePath = isFullChat ? 'build_game/conversations' : 'build_game/ideas';
+ const prefix = isFullChat ? 'conversation' : 'idea';
+ const now = new Date();
+ const year = now.getFullYear();
+ const month = String(now.getMonth() + 1).padStart(2, '0');
+ const day = String(now.getDate()).padStart(2, '0');
+ const hours = String(now.getHours()).padStart(2, '0');
+ const minutes = String(now.getMinutes()).padStart(2, '0');
+ const seconds = String(now.getSeconds()).padStart(2, '0');
+ const filename = `${prefix}-${year}-${month}-${day}-${hours}${minutes}${seconds}.md`;
+ const filePath = `${basePath}/${filename}`;
+
+ const owner = 'understories';
+ const repo = 'votc';
+ const branch = 'main';
+
+ // Open GitHub's "Create new file" interface
+ // This will prompt user to fork if they don't have write access
+ // The filename is pre-filled, user just needs to paste content
+ const createFileUrl = `https://github.com/${owner}/${repo}/new/${branch}?filename=${encodeURIComponent(filePath)}`;
+
+ // Copy content to clipboard
+ let clipboardSuccess = false;
+ try {
+ await navigator.clipboard.writeText(content);
+ clipboardSuccess = true;
+ } catch (clipboardError) {
+ console.error('Clipboard API failed:', clipboardError);
+ }
+
+ // Open GitHub in new tab
+ window.open(createFileUrl, '_blank', 'noopener,noreferrer');
+
+ // Always show content in a copyable box for easy access
+ const output = document.getElementById('output');
+ const fileType = isFullChat ? 'conversation' : 'idea';
+
+ // Show message
+ if (clipboardSuccess) {
+ displayMessage('system', `Opened GitHub file creation page. Content copied to clipboard! Paste (Cmd/Ctrl+V) into the editor. If clipboard didn't work, copy from the box below:`);
+ } else {
+ displayMessage('system', `Opened GitHub file creation page. Copy the content from the box below and paste it into GitHub's editor:`);
+ }
+
+ // Display content in a copyable format
+ const contentBox = document.createElement('div');
+ contentBox.className = 'terminal-line';
+ contentBox.style.marginTop = '1rem';
+ contentBox.style.padding = '1rem';
+ contentBox.style.backgroundColor = '#111';
+ contentBox.style.border = '2px solid #00ff00';
+ contentBox.style.borderRadius = '4px';
+ contentBox.style.maxHeight = '400px';
+ contentBox.style.overflowY = 'auto';
+ contentBox.style.whiteSpace = 'pre-wrap';
+ contentBox.style.fontSize = '0.85rem';
+ contentBox.style.color = '#00ff00';
+ contentBox.style.fontFamily = 'Courier New, Monaco, Menlo, monospace';
+ contentBox.style.lineHeight = '1.6';
+ contentBox.textContent = content;
+ contentBox.style.cursor = 'text';
+ contentBox.setAttribute('contenteditable', 'true');
+ contentBox.setAttribute('spellcheck', 'false');
+
+ // Add a "Select All" button
+ const selectAllBtn = document.createElement('button');
+ selectAllBtn.textContent = 'Select All';
+ selectAllBtn.className = 'share-button share-button-default';
+ selectAllBtn.style.marginTop = '0.5rem';
+ selectAllBtn.style.fontSize = '0.8rem';
+ selectAllBtn.style.padding = '0.4rem 0.8rem';
+ selectAllBtn.addEventListener('click', () => {
+ const range = document.createRange();
+ range.selectNodeContents(contentBox);
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+ });
+
+ const container = document.createElement('div');
+ container.style.marginTop = '0.5rem';
+ container.appendChild(contentBox);
+ container.appendChild(selectAllBtn);
+
+ output.appendChild(container);
+ output.scrollTop = output.scrollHeight;
+ }
+
+ function displaySuccess(url, filename, accountInfo = '') {
+ const output = document.getElementById('output');
+ const line = document.createElement('div');
+ line.className = 'terminal-line success-message';
+
+ const prompt = document.createElement('span');
+ prompt.className = 'prompt';
+ prompt.textContent = '✓';
+ prompt.style.color = '#00ff00';
+
+ const text = document.createElement('span');
+ text.className = 'success-text';
+ const accountNote = accountInfo ? ` (${accountInfo})` : '';
+ text.innerHTML = `Shared! View on GitHub ${accountNote}`;
+ text.style.color = '#00ff00';
+
+ line.appendChild(prompt);
+ line.appendChild(text);
+ output.appendChild(line);
+ output.scrollTop = output.scrollHeight;
+
+ // Auto-dismiss after 5 seconds
+ setTimeout(() => {
+ line.remove();
+ }, 5000);
+ }
+
+ // Click outside to deselect
+ document.addEventListener('click', function(e) {
+ if (!e.target.closest('.message-line') && !e.target.closest('.share-button')) {
+ clearSelection();
+ }
+ });
+});
diff --git a/icon.svg b/icon.svg
new file mode 100644
index 0000000..58a8ac0
--- /dev/null
+++ b/icon.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/index.html b/index.html
index 3bc5dc9..46738ce 100644
--- a/index.html
+++ b/index.html
@@ -3,36 +3,271 @@
- Valley of the Commons
-
+
+
+ Valley of the Commons | Commons Hub
-
+
+
+
+
+
-
-
-
+
+
+
+
Skip
+
+
+
+
+
+
+
Valley of the Commons
-
Coming Soon
-
-
-
-
- A space for collaborative stewardship and shared abundance.
-
-
-
-
Stay updated on our progress:
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
Valley of the Commons
+
a village built on common ground
+
Valley of the Commons is a four-week pop-up village by a nascent network society envisioning life beyond extractive systems.
+
Rooted at commons hub and held by the forests, mountains, and open skies of the Austrian Alps, this gathering is a living commons shared in work and study, in making and care, in governance and everyday life.
+
Together, we lay the foundations for permanence by exploring housing, production, decision-making and ownership in community.
+
+
+
+
+
+
+
Pop-Up Event to Seed the Valley
+
24 August 2026 – 20 September 2026
+
+ APPLY NOW
+
+
+
+
+
+
+
+ Valley of the Commons emerges from years of hands-on experimentation with long-format community events. From co-organizing Zu-villages and Invisible Gardens to bootstrapping the commons hub, our team has ample experience in prototyping early nodes of future societies.
+ Over the last few years, popups have evolved into a cultural phenomenon: distributed communities gathering for multiple weeks to test new forms of living, governance, and collective purpose. These experiments have produced invaluable insights into technical innovations, governance mechanisms and financial tools enabling future network societies.
+ Building on that momentum, we believe it's time to shift gears - to make the social, productive, and infrastructural commons as tangible as the tech stack beneath.
+ The Popup is designed as a practice-oriented phase of preparation toward our own permanence. Over four weeks, participants will actively explore central aspects of a future settlement in theory and practice, while building relationships, clarifying shared priorities, and co-shaping the first real building blocks of the commons-centric network society waiting to be born.
+ The program will revolve around three guiding themes, positioned in the context of current global transformations and the renewed relevance of the commons:
+
+ Cosmo-local production & open value accounting
+ Nomad-friendly communal life in housing co-ops
+ Horizontal governance & funding mechanisms
+
+ Beyond these core tracks, participants are invited to introduce complementary topics they see relevant, and explore how their individual projects might integrate with the perspectives we weave as a community.
+ This popup is not a simulation. It is a real co-living community taking steps toward valley transformation. At the same time, its outcomes feed back into the broader conversation on transitioning network societies from temporary concentrations of talent into durable, regenerative forms of collective life.
+
+
+
+
+
+
+ The Return of the Commons
+ Across Europe and beyond, digital workers, post-corporate professionals, and remote-friendly entrepreneurs seek grounded, cooperative ways of living, beyond the isolation of the metropolis and the fragility of atomized nomadism.
+ This turn toward intentional communities is a structural process enabled by remote work, motivated by a longing for connection and nature, and driven by the need to hedge against rising volatility from climate shocks, fragile supply chains, precarious employment, and social or environmental degradation.
+ As corporate capitalism fails harder and harder, the urgent need for socio-technical systems, resilient enough to endure the coming decades, becomes apparent. The concepts are here, but we need real places, shared practices, and long-term structures to anchor them in daily life. This is why we build the Valley of the Commons.
+
+
+
+
+
+
+ As an experimental playground for regenerative systems design, the commons hub explores the liberatory potential of emerging technologies and social techniques that facilitate horizontal communities.
+ Drawing on years of experience in managing creative chaos, it provides the stability and freedom to let emerging communities bloom. Its facilities – versatile event spaces, a fablab, atelier, and outdoor gym – are complemented by self-organizing practices, gently guiding creative energy into productive flows.
+ In short, this village does not start from zero. It sprouts from a living, breathing center of the commons movement that offers an economic engine, a platform for entrepreneurship, and a beacon for metamodern thought and praxis.
+
+
+
+ Location: Höllental
+ Just an hour south of Vienna, Höllental closes the gentle Danube basin with a dramatic landscape of high peaks, steep cliffs, and cold rushing streams framed by dense forests. It feels remote, almost untouched, yet remains remarkably accessible: nearly 10 million people live within a three-hour drive, and Vienna’s international airport is under two hours away by rail.
+ Formerly a famed imperial summer retreat and wood-industry hub, the valley spent the last century dormant, its villages slowly decaying.
+ Recent signs of reversal point to a shift toward outdoor tourism, as hotels renovate and factories close for good. And yet, real estate remains affordable, with many houses and villas still awaiting new purpose.
+ Adding to its beauty and accessibility, the opportunities on the ground make this valley ideal for building for permanence.
+ Explore the Valley →
+
+
+
+
+ Schedule
+
+
The four-week popup follows a simple rhythm: mornings are structured learning paths, afternoons host workshops, field visits, and working groups, and evenings are reserved for shared life, reflection, and communal time.
+
Weekends remain open for exploration and connection with local initiatives. Life happens in between: shared meals, scenic hikes, river plunges, mushroom foraging, fire circles, late-night conversations under alpine stars, and much more.
+
We create these moments together, each contributing their own skills, energy, and quirks to the village.
+
+
+
+
+
+
READ MORE v
+
Week 1 Return of the Commons
+
24 – 30 August 2026
+
+ To set the scene, we begin with a five‑day course led by Michel Bauwens and Adam Arvidsson, two of the most insightful and engaging contemporary thinkers on the commons. Expect a wild ride through history, economic and sociological theory, peppered with anecdotes, examples, and insights.
+ Bauwens, founder of the P2P Foundation, brings a visionary lens, exploring the transformative potential of commons-based systems and their central role in post-capitalist collaboration. Arvidsson’s work instead turns the focus to how commons emerge in everyday production and social life where capitalism fails. Together, their perspectives offer both inspiration and grounded insight: a visionary account of the commons’ civilizational potential, and a practical, historically informed view of how commons-based social and productive forms can arise as capitalism’s reach and allure contract. Afternoons provide a practical counterpart: workshops, small-group exercises, and working sessions allow participants to translate theory into practice, exploring how commons ideas can be operationalized in daily life, collaborative projects, and the emerging Valley of the Commons.
+
+
+
+
+
READ MORE v
+
Week 2 Cosmo-local Production & Open Value Accounting (OVA)
+
31 August – 6 September 2026
+
+ In the second week, we shift from macro-narratives to the mechanics of how a village economy could actually function. Through a mix of theory sessions and hands-on workshops, participants explore how global knowledge commons and local production capacities can sustain livelihoods, small-scale industry, and community resilience.
+ We look at business models for cosmo-local manufacturing, mutual credit and community currencies, distributed supply chains, and the workflows that make small-scale fabrication viable. Alongside the economic layer, we dive into open value accounting: reviewing existing models, examining where they succeed (or fail), and collectively selecting a pragmatic framework that we can begin testing immediately. The emphasis throughout the week is on feasibility rather than perfection, focusing on economic structures that real people can actually use. Afternoons extend these concepts into practice: prototyping small production workflows, mapping local resources, drafting economic scenarios, and experimenting with simple Open Value Accounting tools that could underpin the emerging Valley of the Commons. This week is where abstract ideas meet tangible economic strategy.
+
+
+
+
+
READ MORE v
+
Week 3 Future Living
+
7 – 13 September 2026
+
+ Week three turns toward how we might actually live together – architecturally, legally, ecologically, and socially. We explore cooperative housing concepts, map local resources and unused buildings, and examine the ecological potentials of the valley as a site for long-term habitation.
+ Sessions cover sustainable renovation techniques, cooperative ownership structures (from co-housing to time-share to hybrid models), and governance frameworks for housing clusters grounded in subsidiarity and shared stewardship. The week’s aim is to converge on minimal, resilient living systems suited to the 21st century: homes that are affordable, adaptable, energy-efficient, and embedded in community. Afternoons translate ideas into grounded work: from site visits to sketches and spatial planning, we will develop early drafts of what a first housing cluster could look like. This is where speculative idealism meets architectural, legal, and economic reality.
+
+
+
+
+
READ MORE v
+
Week 4 Governance & Funding Models
+
14 – 20 September 2026
+
+ The fourth week brings everything into the domain of stewardship: how we organize, decide, invest, and protect what we build together. We explore participatory governance frameworks, cooperative legal structures, long-term investment models, and mechanisms for holding shared assets in trust.
+ Drawing on lessons from ecovillages, DAOs, co-ops, and community land trusts, participants map what actually works, and where idealistic models tend to fail in practice. Afternoons shift into applied design: drafting governance charters, testing decision-making protocols, sketching legal wrappers, and evaluating funding pathways that balance autonomy, flexibility, and long-term resilience. The week culminates in a coherent set of next steps: populating the Valley of the Commons association, defining its initial governance and funding architecture, and - potentially - outlining the first concrete co-living project. This final stretch lays the groundwork for the governance of a future permanent village.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Explore the Valley
+
+
+
+
+
+
+
+
+
Get involved
+
Join as a partner, theme curator, or sponsor.
+
+
+
+
+
Together, we can turn vision into reality.
+
+
+
+
+
+
+
diff --git a/internal_thought.md b/internal_thought.md
new file mode 100644
index 0000000..a4a41fe
--- /dev/null
+++ b/internal_thought.md
@@ -0,0 +1,94 @@
+# Internal Thoughts: Game Intelligence LLM Context
+## Valley of the Commons - Game Design Notes
+
+**Purpose:** Reference document for the Socratic game master moderator
+**Last Updated:** December 2025
+
+---
+
+## Core Concepts
+
+### Commoners -> New Faction
+- Not a faction yet as far as others are concerned
+- What can a Game-like approach add to this mix?
+- Generate out of the box thinking
+
+### World Context
+- World says - you can buy places, business negotiations… what else?
+- Mix of mythology and real life
+- Ritualistic elements
+- Game can be an instrument of coming up with different kinds of operations
+
+### Challenges & Opportunities
+- Lack cultural, social capital
+- Use the map and the instrument that it brings
+- With projections, pieces, take snapshots of people and locations
+- Turn them into physical cards (ahem)
+- Out of the box moves in the valley
+- Attracting games more permanently
+
+---
+
+## Map & Visualization
+
+### Map as Instrument
+- Map as visualization with real riddles, prompts to go out
+- Map view in map room
+- Projection numbers 1-50
+- Physical book with quests
+- Groups of people come here, mark places where things are playable
+- Add things as they come in into the game
+- War game ops do work like this
+
+---
+
+## Development Phases
+
+### MVP: Minimalist Place
+- Interesting game is developing
+- Interesting material -> the reason they engage
+- Hero's call
+- Game commonalized in some form
+- Take place in 3D printing from form
+
+### Phase of Collecting Tools
+- Gathering tools
+- People help us surface which tools we end up using
+- Open call to action
+- Get involved to help shape the game, and thus our village
+
+---
+
+## Key Questions
+
+**What can we offer to the village?**
+
+This question remains open and should be explored through Socratic dialogue.
+
+---
+
+## Design Principles
+
+1. **Game as Instrument:** The game is a tool for generating new forms of operations
+2. **Physical-Digital Bridge:** Map, cards, projections connect digital and physical
+3. **Community-Driven:** People mark places, add quests, surface tools
+4. **Ritualistic Elements:** Mix of mythology and real life
+5. **Out of the Box Thinking:** Generate unconventional approaches
+6. **Commonalization:** Game becomes shared resource, common good
+7. **One Question at a Time:** Always ask only one question per response to maintain focus and allow for deeper exploration
+
+---
+
+## Implementation Notes
+
+- Map room with projections
+- Physical cards from snapshots
+- Quest book (numbers 1-50)
+- 3D printing integration
+- Tool collection phase
+- Open call to action for participation
+
+---
+
+**Note for Game Master:** Use these concepts to guide Socratic questions, but never lecture. Probe with questions that help participants discover these connections themselves. **CRITICAL: Ask only one question at a time** - this maintains focus, allows for deeper exploration, and prevents overwhelming participants with multiple prompts.
+
diff --git a/nginx.conf b/nginx.conf
deleted file mode 100644
index e76d6b7..0000000
--- a/nginx.conf
+++ /dev/null
@@ -1,27 +0,0 @@
-server {
- listen 80;
- server_name localhost;
- root /usr/share/nginx/html;
- index index.html;
-
- # Gzip compression
- gzip on;
- gzip_types text/plain text/css application/javascript application/json;
- gzip_min_length 1000;
-
- # Cache static assets
- location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
- expires 1y;
- add_header Cache-Control "public, immutable";
- }
-
- # Main location
- location / {
- try_files $uri $uri/ /index.html;
- }
-
- # Security headers
- add_header X-Frame-Options "SAMEORIGIN" always;
- add_header X-Content-Type-Options "nosniff" always;
- add_header X-XSS-Protection "1; mode=block" always;
-}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..dc200a4
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,945 @@
+{
+ "name": "valley-of-the-commons",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "valley-of-the-commons",
+ "version": "1.0.0",
+ "dependencies": {
+ "@ai-sdk/anthropic": "^3.0.0",
+ "@ai-sdk/mistral": "^3.0.0",
+ "@octokit/rest": "^22.0.1",
+ "ai": "^6.0.1",
+ "googleapis": "^126.0.1"
+ },
+ "devDependencies": {
+ "dotenv": "^16.3.1"
+ },
+ "engines": {
+ "node": ">=18.x"
+ }
+ },
+ "node_modules/@ai-sdk/anthropic": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-3.0.0.tgz",
+ "integrity": "sha512-4BnxkXwRkvh+OB1ze0mHbskT90HL4MNrg6JUsRDkIsU9w5vitvGzxwc/XwlByUGMap/5I8/LZ3XZDzv6KViCuQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "3.0.0",
+ "@ai-sdk/provider-utils": "4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.76 || ^4.1.8"
+ }
+ },
+ "node_modules/@ai-sdk/gateway": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.0.tgz",
+ "integrity": "sha512-JcjePYVpbezv+XOxkxPemwnorjWpgDiiKWMYy6FXTCG2rFABIK2Co1bFxIUSDT4vYO6f1448x9rKbn38vbhDiA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "3.0.0",
+ "@ai-sdk/provider-utils": "4.0.0",
+ "@vercel/oidc": "3.0.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.76 || ^4.1.8"
+ }
+ },
+ "node_modules/@ai-sdk/mistral": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/mistral/-/mistral-3.0.0.tgz",
+ "integrity": "sha512-c7k/hLqXUdsbwco63kbM9mB7JA6c76kwPTtn9ulpsO8jvL+3n3+MKn0zWF7wjIZXc3kWAOBBK1zZB492hm+f3w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "3.0.0",
+ "@ai-sdk/provider-utils": "4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.76 || ^4.1.8"
+ }
+ },
+ "node_modules/@ai-sdk/provider": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.0.tgz",
+ "integrity": "sha512-m9ka3ptkPQbaHHZHqDXDF9C9B5/Mav0KTdky1k2HZ3/nrW2t1AgObxIVPyGDWQNS9FXT/FS6PIoSjpcP/No8rQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "json-schema": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@ai-sdk/provider-utils": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.0.tgz",
+ "integrity": "sha512-HyCyOls9I3a3e38+gtvOJOEjuw9KRcvbBnCL5GBuSmJvS9Jh9v3fz7pRC6ha1EUo/ZH1zwvLWYXBMtic8MTguA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "3.0.0",
+ "@standard-schema/spec": "^1.1.0",
+ "eventsource-parser": "^3.0.6"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.76 || ^4.1.8"
+ }
+ },
+ "node_modules/@octokit/auth-token": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
+ "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/core": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
+ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-token": "^6.0.0",
+ "@octokit/graphql": "^9.0.3",
+ "@octokit/request": "^10.0.6",
+ "@octokit/request-error": "^7.0.2",
+ "@octokit/types": "^16.0.0",
+ "before-after-hook": "^4.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/endpoint": {
+ "version": "11.0.2",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz",
+ "integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^16.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/graphql": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz",
+ "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request": "^10.0.6",
+ "@octokit/types": "^16.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/openapi-types": {
+ "version": "27.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
+ "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/plugin-paginate-rest": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
+ "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^16.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-request-log": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz",
+ "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-rest-endpoint-methods": {
+ "version": "17.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz",
+ "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^16.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/request": {
+ "version": "10.0.7",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.7.tgz",
+ "integrity": "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/endpoint": "^11.0.2",
+ "@octokit/request-error": "^7.0.2",
+ "@octokit/types": "^16.0.0",
+ "fast-content-type-parse": "^3.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/request-error": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz",
+ "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^16.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/rest": {
+ "version": "22.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz",
+ "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/core": "^7.0.6",
+ "@octokit/plugin-paginate-rest": "^14.0.0",
+ "@octokit/plugin-request-log": "^6.0.0",
+ "@octokit/plugin-rest-endpoint-methods": "^17.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/types": {
+ "version": "16.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
+ "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^27.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
+ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@vercel/oidc": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz",
+ "integrity": "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ai": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.1.tgz",
+ "integrity": "sha512-g/jPakC6h4vUJKDww0d6+VaJmfMC38UqH3kKsngiP+coT0uvCUdQ7lpFDJ0mNmamaOyRMaY2zwEB2RnTAaJU/w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/gateway": "3.0.0",
+ "@ai-sdk/provider": "3.0.0",
+ "@ai-sdk/provider-utils": "4.0.0",
+ "@opentelemetry/api": "1.9.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.76 || ^4.1.8"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/before-after-hook": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
+ "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/bignumber.js": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
+ "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
+ "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/fast-content-type-parse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
+ "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gaxios": {
+ "version": "6.7.1",
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
+ "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "extend": "^3.0.2",
+ "https-proxy-agent": "^7.0.1",
+ "is-stream": "^2.0.0",
+ "node-fetch": "^2.6.9",
+ "uuid": "^9.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/gcp-metadata": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
+ "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "gaxios": "^6.1.1",
+ "google-logging-utils": "^0.0.2",
+ "json-bigint": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/google-auth-library": {
+ "version": "9.15.1",
+ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
+ "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "base64-js": "^1.3.0",
+ "ecdsa-sig-formatter": "^1.0.11",
+ "gaxios": "^6.1.1",
+ "gcp-metadata": "^6.1.0",
+ "gtoken": "^7.0.0",
+ "jws": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/google-logging-utils": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
+ "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/googleapis": {
+ "version": "126.0.1",
+ "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-126.0.1.tgz",
+ "integrity": "sha512-4N8LLi+hj6ytK3PhE52KcM8iSGhJjtXnCDYB4fp6l+GdLbYz4FoDmx074WqMbl7iYMDN87vqD/8drJkhxW92mQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "google-auth-library": "^9.0.0",
+ "googleapis-common": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/googleapis-common": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz",
+ "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "extend": "^3.0.2",
+ "gaxios": "^6.0.3",
+ "google-auth-library": "^9.7.0",
+ "qs": "^6.7.0",
+ "url-template": "^2.0.8",
+ "uuid": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gtoken": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
+ "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
+ "license": "MIT",
+ "dependencies": {
+ "gaxios": "^6.0.0",
+ "jws": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/json-bigint": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+ "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bignumber.js": "^9.0.0"
+ }
+ },
+ "node_modules/json-schema": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
+ "license": "(AFL-2.1 OR BSD-3-Clause)"
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/universal-user-agent": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
+ "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
+ "license": "ISC"
+ },
+ "node_modules/url-template": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
+ "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
+ "license": "BSD"
+ },
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz",
+ "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..8d76b5d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "valley-of-the-commons",
+ "version": "1.0.0",
+ "description": "Landing page for Valley of the Commons",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "dependencies": {
+ "express": "^4.21.0",
+ "@ai-sdk/anthropic": "^3.0.0",
+ "@ai-sdk/mistral": "^3.0.0",
+ "@octokit/rest": "^22.0.1",
+ "ai": "^6.0.1",
+ "googleapis": "^126.0.1"
+ },
+ "devDependencies": {
+ "dotenv": "^16.3.1"
+ },
+ "engines": {
+ "node": ">=18.x"
+ }
+}
diff --git a/photos/campfire.png b/photos/campfire.png
new file mode 100644
index 0000000..7e65afa
Binary files /dev/null and b/photos/campfire.png differ
diff --git a/photos/gardenfront.jpg b/photos/gardenfront.jpg
new file mode 100644
index 0000000..ab7a81c
Binary files /dev/null and b/photos/gardenfront.jpg differ
diff --git a/photos/hubfront.jpg b/photos/hubfront.jpg
new file mode 100644
index 0000000..08c298d
Binary files /dev/null and b/photos/hubfront.jpg differ
diff --git a/photos/shortintro-poster.png b/photos/shortintro-poster.png
new file mode 100644
index 0000000..2870365
Binary files /dev/null and b/photos/shortintro-poster.png differ
diff --git a/photos/shortintro.mov b/photos/shortintro.mov
new file mode 100644
index 0000000..7e7ea0c
Binary files /dev/null and b/photos/shortintro.mov differ
diff --git a/privacy.html b/privacy.html
new file mode 100644
index 0000000..894a7a9
--- /dev/null
+++ b/privacy.html
@@ -0,0 +1,368 @@
+
+
+
+
+
+
+
+ Privacy & Open Source - Valley of the Commons
+
+
+
+
+
+
+
+
+
Model Choice & AI Infrastructure
+
+
Current Model: Mistral Devstral-2
+
+ We use Mistral Devstral-2 via
+ Vercel AI Gateway .
+ This is a cost-effective model suitable for MVP testing.
+
+
+
Why This Model?
+
+ Free tier: Available on Vercel AI Gateway free tier
+ Fast response times: Optimized for real-time dialogue
+ Open-source roadmap: We're committed to migrating to open-weights or self-hosted models
+
+
+
Long-term Vision
+
+ Note: Some models used in MVP are proprietary APIs.
+ Our long-term roadmap includes migration to open-weights or self-hosted models
+ to align with open commons principles.
+
+
+
+
+
Data Tracking & Privacy
+
+
What We Track
+
+ Server logs: Basic request metadata (method, timestamp, message count) for debugging and monitoring
+ Shared ideas: If you choose to share an idea or conversation to GitHub, it becomes part of the public repository
+
+
+
What We Do NOT Save
+
+ We do NOT save conversations: Your dialogue with the game master is processed temporarily to generate responses, but we do not store conversations in any database or persistent storage
+ We do NOT track conversations: Conversations exist only in your browser session and are not saved by us
+
+
+
AI Provider Tracking
+
+ Important: While we do not save your conversations , the AI provider
+ (Mistral via Vercel AI Gateway) may track conversations according to their own privacy policy.
+ We have no control over their data collection practices. For details, see
+ Mistral's Privacy Policy
+ and Vercel's Privacy Policy .
+
+
+
What We Do NOT Track
+
+ No user accounts: No registration, no login, no personal profiles
+ No cookies: No tracking cookies, no analytics cookies, no advertising trackers
+ No IP logging: IP addresses are not stored or logged
+ No third-party analytics: No Google Analytics, no Facebook Pixel, no tracking scripts
+ No email collection: The game interface does not collect email addresses
+
+
+
Conversation Handling
+
+ We do not save conversations. Conversations exist only in your browser session.
+ Messages are sent to the server for AI processing but are not stored persistently by us .
+ Each conversation is ephemeral and exists only during your active session. When you close your browser,
+ the conversation is gone from our systems.
+
+
+
When Conversations Are Saved
+
+ Conversations are only saved when you explicitly choose to share them to GitHub using
+ the "Share to GitHub" feature. This is an opt-in action that you control. Once shared, the conversation
+ becomes part of the public repository at build_game/conversations/.
+
+
+
Server-Side Processing
+
+ Messages are processed through Vercel serverless functions. Our server logs may contain:
+
+
+ Request metadata (timestamp, method)
+ Message count and length (for debugging)
+ First/last message previews (first 100 characters, for debugging only)
+
+
+ These logs are not publicly accessible and are used only for system monitoring and debugging.
+ They do not contain full conversation content and are automatically rotated by Vercel.
+
+
+
+
+
What Is Verifiable on GitHub
+
+
Open Source Repository
+
+ Our entire codebase is open source and available at
+ github.com/understories/votc .
+
+
+
You Can Verify:
+
+ All client-side code: HTML, CSS, JavaScript - everything runs in your browser
+ Serverless function code: All API endpoints are open source and auditable
+ No hidden tracking: Review the code yourself - no analytics, no trackers, no data collection
+ Data handling logic: See exactly how messages are processed, sanitized, and sent to AI
+ Security measures: Input sanitization, role whitelisting, turn limits - all visible in code
+ Shared ideas: Ideas shared to GitHub are publicly visible in build_game/ideas/
+
+
+
What's Not in the Repository
+
+ API keys: Stored securely in Vercel environment variables (never in code)
+ Internal thoughts: Game design notes are in the repo but don't contain user data
+ Server logs: Not committed to the repository
+
+
+
+
+
Open Source & Open Commons Best Practices
+
+
Our Commitment
+
+ Valley of the Commons follows open commons principles:
+
+
+ Transparency: All code is open source and auditable
+ No vendor lock-in: Using open standards and protocols
+ Community ownership: Ideas shared become part of the public commons
+ Minimal data collection: Only what's necessary for functionality
+ User control: You choose what to share, when to share it
+
+
+
Open Source License
+
+ The codebase is open source. Check the repository for the specific license terms.
+
+
+
Contributing
+
+ Contributions, improvements, and audits are welcome. The repository is public and open for
+ community participation.
+
+
+
Future Improvements
+
+ Migration to self-hosted or open-weights models
+ Enhanced privacy controls
+ Optional conversation export (user-controlled)
+ Local-first architecture where possible
+
+
+
+
+
Security Measures
+
+
Input Sanitization
+
+ Message content is limited to 500 characters per message
+ Only 'user' and 'assistant' roles are allowed (prevents system prompt injection)
+ Empty messages are filtered out
+
+
+
Rate Limiting
+
+ Maximum 12 user turns per conversation
+ Server-side turn counting (prevents client-side manipulation)
+
+
+
API Security
+
+ API keys stored in environment variables (never exposed to client)
+ Serverless functions handle all sensitive operations
+ No CORS for game chat (same-origin only)
+
+
+
+
+
Your Rights & Control
+
+
You Control:
+
+ What you share: Only share ideas you want to make public
+ When you share: Sharing is opt-in, not automatic
+ Your conversation: Conversations are ephemeral - close the browser to end the session
+
+
+
No Account Required
+
+ You can use the game interface without creating an account, providing an email, or any personal information.
+
+
+
Questions or Concerns?
+
+ If you have questions about privacy, data handling, or want to report a concern,
+ please open an issue on GitHub
+ or contact the project maintainers.
+
+
+
+
← Back to Game
+
+
+
+
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..5f0088a
--- /dev/null
+++ b/server.js
@@ -0,0 +1,58 @@
+const express = require('express');
+const path = require('path');
+
+const app = express();
+const PORT = process.env.PORT || 3000;
+
+// Middleware
+app.use(express.json());
+app.use(express.urlencoded({ extended: true }));
+
+// CORS middleware
+app.use((req, res, next) => {
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
+ if (req.method === 'OPTIONS') {
+ return res.status(200).end();
+ }
+ next();
+});
+
+// API routes - wrap Vercel serverless functions
+const waitlistHandler = require('./api/waitlist');
+const gameChatHandler = require('./api/game-chat');
+const shareToGithubHandler = require('./api/share-to-github');
+
+// Adapter to convert Vercel handler to Express
+const vercelToExpress = (handler) => async (req, res) => {
+ try {
+ await handler(req, res);
+ } catch (error) {
+ console.error('API Error:', error);
+ if (!res.headersSent) {
+ res.status(500).json({ error: 'Internal server error' });
+ }
+ }
+};
+
+app.all('/api/waitlist', vercelToExpress(waitlistHandler));
+app.all('/api/game-chat', vercelToExpress(gameChatHandler));
+app.all('/api/share-to-github', vercelToExpress(shareToGithubHandler));
+
+// Static files
+app.use(express.static(path.join(__dirname), {
+ extensions: ['html'],
+ index: 'index.html'
+}));
+
+// SPA fallback - serve index.html for non-API routes
+app.get('*', (req, res) => {
+ if (!req.path.startsWith('/api/')) {
+ res.sendFile(path.join(__dirname, 'index.html'));
+ }
+});
+
+app.listen(PORT, '0.0.0.0', () => {
+ console.log(`Valley of the Commons server running on port ${PORT}`);
+});
diff --git a/speakers.js b/speakers.js
new file mode 100644
index 0000000..e95f172
--- /dev/null
+++ b/speakers.js
@@ -0,0 +1,169 @@
+// Dynamically load featured speakers
+document.addEventListener('DOMContentLoaded', function() {
+ const speakersContainer = document.getElementById('speakers-container');
+ if (!speakersContainer) return;
+
+ // Define speakers based on folder structure
+ const speakers = [
+ {
+ name: 'Adam Arvidsson',
+ folder: 'Adam Arvidsson',
+ image: 'Arvidsson.png'
+ },
+ {
+ name: 'Charlie Fisher',
+ folder: 'Charlie Fisher',
+ image: 'charlie.jpeg'
+ },
+ {
+ name: 'Daniel Figueiredo',
+ folder: 'DANIEL RICHARD DE OLIVIERA FIGUEIREDO',
+ image: 'daniel.webp'
+ },
+ {
+ name: 'Emil Fritsch',
+ folder: 'Emil Fritsch',
+ image: 'emil.webp',
+ imagePosition: 'top'
+ },
+ {
+ name: 'Felix Fritsch',
+ folder: 'Felix Fritsch',
+ image: 'Fritsch.png'
+ },
+ {
+ name: 'Clara Gromaches',
+ folder: 'Clara Gromaches',
+ image: 'clara.jpg'
+ },
+ {
+ name: 'Koss',
+ folder: 'Koss',
+ image: 'koss.png'
+ },
+ {
+ name: 'Lorenzo Patuzzo',
+ folder: 'Lorenzo Patuzzo',
+ image: 'lorenzo.jpg'
+ },
+ {
+ name: 'Michel Bauwens',
+ folder: 'Michel Bauwens',
+ image: 'bauwens.jpeg'
+ },
+ {
+ name: 'Rashmi Abbigeri',
+ folder: 'Rashmi Abbigeri',
+ image: 'Abbigeri.png'
+ },
+ {
+ name: 'Una Wang',
+ folder: 'Una Wang',
+ image: 'una.jpg'
+ },
+ {
+ name: 'Veronica',
+ folder: 'Veronica',
+ image: 'veronica.png'
+ }
+ ];
+
+ // Sort by last name; single-name entries go last, sorted by first name
+ const withLastName = speakers.filter((speaker) => speaker.name.trim().split(/\s+/).length > 1);
+ const withoutLastName = speakers.filter((speaker) => speaker.name.trim().split(/\s+/).length === 1);
+
+ withLastName.sort((a, b) => {
+ const aParts = a.name.trim().split(/\s+/);
+ const bParts = b.name.trim().split(/\s+/);
+ const aLast = aParts[aParts.length - 1];
+ const bLast = bParts[bParts.length - 1];
+ const lastCompare = aLast.localeCompare(bLast, 'en', { sensitivity: 'base' });
+ if (lastCompare !== 0) return lastCompare;
+ return a.name.localeCompare(b.name, 'en', { sensitivity: 'base' });
+ });
+
+ withoutLastName.sort((a, b) => {
+ const aFirst = a.name.trim().split(/\s+/)[0];
+ const bFirst = b.name.trim().split(/\s+/)[0];
+ return aFirst.localeCompare(bFirst, 'en', { sensitivity: 'base' });
+ });
+
+ const sortedSpeakers = [...withLastName, ...withoutLastName];
+
+ // Load each speaker
+ sortedSpeakers.forEach(speaker => {
+ loadSpeaker(speaker);
+ });
+
+ async function loadSpeaker(speaker) {
+ // Create speaker card
+ const speakerCard = document.createElement('div');
+ speakerCard.className = 'speaker-card';
+
+ // Create image
+ const img = document.createElement('img');
+ img.src = `speakers/${speaker.folder}/${speaker.image}`;
+ img.alt = speaker.name;
+ img.className = 'speaker-image';
+ img.loading = 'lazy';
+ if (speaker.imagePosition) {
+ img.style.objectPosition = speaker.imagePosition;
+ }
+
+ // Create name
+ const name = document.createElement('h3');
+ name.className = 'speaker-name';
+ name.textContent = speaker.name;
+
+ // Create bio (filled after fetch to preserve order)
+ const bio = document.createElement('p');
+ bio.className = 'speaker-bio';
+
+ // Create toggle text for mobile
+ const toggleText = document.createElement('span');
+ toggleText.className = 'speaker-toggle-text';
+ toggleText.textContent = 'click to read more';
+
+ // Create read more indicator for desktop
+ const readMoreDesktop = document.createElement('span');
+ readMoreDesktop.className = 'speaker-read-more-desktop';
+ readMoreDesktop.textContent = 'read more v';
+
+ // Assemble card
+ speakerCard.appendChild(img);
+ speakerCard.appendChild(name);
+ speakerCard.appendChild(readMoreDesktop);
+ speakerCard.appendChild(toggleText);
+ speakerCard.appendChild(bio);
+
+ // Add click handler for mobile
+ speakerCard.addEventListener('click', function(e) {
+ if (window.innerWidth < 769) {
+ e.preventDefault();
+ speakerCard.classList.toggle('expanded');
+ // Update toggle text
+ if (speakerCard.classList.contains('expanded')) {
+ toggleText.textContent = 'click to collapse';
+ } else {
+ toggleText.textContent = 'click to read more';
+ }
+ }
+ });
+
+ // Append immediately to preserve sorted order
+ speakersContainer.appendChild(speakerCard);
+
+ try {
+ // Fetch bio
+ const bioResponse = await fetch(`speakers/${speaker.folder}/bio.md`);
+ if (!bioResponse.ok) {
+ console.error(`Failed to load bio for ${speaker.name}`);
+ return;
+ }
+ const bioText = await bioResponse.text();
+ bio.textContent = bioText.trim();
+ } catch (error) {
+ console.error(`Error loading speaker ${speaker.name}:`, error);
+ }
+ }
+});
diff --git a/speakers/Adam Arvidsson/Arvidsson.png b/speakers/Adam Arvidsson/Arvidsson.png
new file mode 100644
index 0000000..fbc9aa4
Binary files /dev/null and b/speakers/Adam Arvidsson/Arvidsson.png differ
diff --git a/speakers/Adam Arvidsson/bio.md b/speakers/Adam Arvidsson/bio.md
new file mode 100644
index 0000000..73fc173
--- /dev/null
+++ b/speakers/Adam Arvidsson/bio.md
@@ -0,0 +1 @@
+Adam Arvidsson is Professor of Sociology of Cultural and Communicative Processes at the Università Federico II di Napoli. His research explores emerging forms of cooperation enabled by digital media and their economic and political potential, with a particular focus on the evolving relationship between capitalism and the commons.
\ No newline at end of file
diff --git a/speakers/Charlie Fisher/bio.md b/speakers/Charlie Fisher/bio.md
new file mode 100644
index 0000000..9b9eed6
--- /dev/null
+++ b/speakers/Charlie Fisher/bio.md
@@ -0,0 +1 @@
+Charlie works on the practical demonstrations for new affordable housing and commons-based landholding approaches. As a researcher, founder, and project advisor, he ran an architecture practice for a decade, ran the Civic Tech studio at Dark Matter Labs, was a key advisor on large scale new housing developments in England, and was a co-founder of Oasa, a Swiss token-issuer for networked land projects.
\ No newline at end of file
diff --git a/speakers/Charlie Fisher/charlie.jpeg b/speakers/Charlie Fisher/charlie.jpeg
new file mode 100644
index 0000000..4adea06
Binary files /dev/null and b/speakers/Charlie Fisher/charlie.jpeg differ
diff --git a/speakers/Clara Gromaches/bio.md b/speakers/Clara Gromaches/bio.md
new file mode 100644
index 0000000..64e68dd
--- /dev/null
+++ b/speakers/Clara Gromaches/bio.md
@@ -0,0 +1 @@
+Clara Gromaches is an architect working on housing and land as commons. Her work combines fieldwork and emerging digital technologies to create experimental financial and governance mechanisms that protect and accelerate community-led models of collective ownership and stewardship through Komma.
\ No newline at end of file
diff --git a/speakers/Clara Gromaches/clara.jpg b/speakers/Clara Gromaches/clara.jpg
new file mode 100644
index 0000000..5f256d7
Binary files /dev/null and b/speakers/Clara Gromaches/clara.jpg differ
diff --git a/speakers/DANIEL RICHARD DE OLIVIERA FIGUEIREDO/bio.md b/speakers/DANIEL RICHARD DE OLIVIERA FIGUEIREDO/bio.md
new file mode 100644
index 0000000..8a5dc7e
--- /dev/null
+++ b/speakers/DANIEL RICHARD DE OLIVIERA FIGUEIREDO/bio.md
@@ -0,0 +1 @@
+Daniel manages hospitality at the Commons Hub with a unique blend of practicality, intelligence, and charm. His exceptional problem-solving skills and calm approach to tackling tough situations ground our community on a daily basis, and his positive energy lights our way as we transform our valley.
\ No newline at end of file
diff --git a/speakers/DANIEL RICHARD DE OLIVIERA FIGUEIREDO/daniel.webp b/speakers/DANIEL RICHARD DE OLIVIERA FIGUEIREDO/daniel.webp
new file mode 100644
index 0000000..754aa11
Binary files /dev/null and b/speakers/DANIEL RICHARD DE OLIVIERA FIGUEIREDO/daniel.webp differ
diff --git a/speakers/Emil Fritsch/bio.md b/speakers/Emil Fritsch/bio.md
new file mode 100644
index 0000000..69fe059
--- /dev/null
+++ b/speakers/Emil Fritsch/bio.md
@@ -0,0 +1,2 @@
+Emil, Co-Founder of the Commons Hub, comes from a civil engineering background and has worked in computer vision, started a green tech company, and built validator nodes for the hub. He entered IT during the 2017 neural network chess engine revolution. In 2020, he saw Hirschwangerhof’s potential and organized its first event—a 3-week hackathon and retreat—paving the way for the Commons Hub.
+
diff --git a/speakers/Emil Fritsch/emil.webp b/speakers/Emil Fritsch/emil.webp
new file mode 100644
index 0000000..bf6d3c2
Binary files /dev/null and b/speakers/Emil Fritsch/emil.webp differ
diff --git a/speakers/Felix Fritsch/Fritsch.png b/speakers/Felix Fritsch/Fritsch.png
new file mode 100644
index 0000000..537818e
Binary files /dev/null and b/speakers/Felix Fritsch/Fritsch.png differ
diff --git a/speakers/Felix Fritsch/bio.md b/speakers/Felix Fritsch/bio.md
new file mode 100644
index 0000000..68f17cd
--- /dev/null
+++ b/speakers/Felix Fritsch/bio.md
@@ -0,0 +1 @@
+Felix Fritsch, Co-Founder of the Commons Hub and Crypto Commons Association, combines expertise in political and economic theory with a passion for community organizing. Completed his PhD on The Emergence of the Crypto Commons.
\ No newline at end of file
diff --git a/speakers/Koss/bio.md b/speakers/Koss/bio.md
new file mode 100644
index 0000000..8932886
--- /dev/null
+++ b/speakers/Koss/bio.md
@@ -0,0 +1 @@
+ Koss is the Lead Gardener at Invisible Garden and runs Ecosystem & Community Growth at Swarm. With a mixed background across the arts, grassroots activism, policy/gov relations, Koss is generally curious about our expansion as a multi-planetary civilization and a long time supporter of the Commons Hub.
\ No newline at end of file
diff --git a/speakers/Koss/koss.png b/speakers/Koss/koss.png
new file mode 100644
index 0000000..709e57f
Binary files /dev/null and b/speakers/Koss/koss.png differ
diff --git a/speakers/Lorenzo Patuzzo/bio.md b/speakers/Lorenzo Patuzzo/bio.md
new file mode 100644
index 0000000..3446692
--- /dev/null
+++ b/speakers/Lorenzo Patuzzo/bio.md
@@ -0,0 +1 @@
+Lorenzo Patuzzo is a Creative Technologist and a Social Entrepreneur. He is part of AKASHA Foundation and Founder of AKASHA Hub, Hubs Network and Green City Lab, promoting collaboration, decentralization and open source philosophy, and facilitating local collaborative projects for social good and for the environment. He has been working in different sectors varying from industrial design, cinema, architecture and distributed global digital infrastructures since 2013. He is now focusing on a network of collaborative R+D places for society improvement as a path towards a coherent and self-managed society.
\ No newline at end of file
diff --git a/speakers/Lorenzo Patuzzo/lorenzo.jpg b/speakers/Lorenzo Patuzzo/lorenzo.jpg
new file mode 100644
index 0000000..9d59c31
Binary files /dev/null and b/speakers/Lorenzo Patuzzo/lorenzo.jpg differ
diff --git a/speakers/Michel Bauwens/bauwens.jpeg b/speakers/Michel Bauwens/bauwens.jpeg
new file mode 100644
index 0000000..3ba0cc6
Binary files /dev/null and b/speakers/Michel Bauwens/bauwens.jpeg differ
diff --git a/speakers/Michel Bauwens/bio.md b/speakers/Michel Bauwens/bio.md
new file mode 100644
index 0000000..6fb5704
--- /dev/null
+++ b/speakers/Michel Bauwens/bio.md
@@ -0,0 +1 @@
+Michel Bauwens, founder of p2pfoundation & FairCoop, is a Belgian political theorist, writer, and conference speaker on the subjects of technology, culture and business innovation.
\ No newline at end of file
diff --git a/speakers/Rashmi Abbigeri/Abbigeri.png b/speakers/Rashmi Abbigeri/Abbigeri.png
new file mode 100644
index 0000000..5491091
Binary files /dev/null and b/speakers/Rashmi Abbigeri/Abbigeri.png differ
diff --git a/speakers/Rashmi Abbigeri/bio.md b/speakers/Rashmi Abbigeri/bio.md
new file mode 100644
index 0000000..fd80800
--- /dev/null
+++ b/speakers/Rashmi Abbigeri/bio.md
@@ -0,0 +1 @@
+Rashmi Abbigeri is a research engineer at MetaGov working on grant analytics through Open Grants. Her work emphasizes the development of AI-ready datasets, a critical factor for the sustainable scaling of beneficial AI systems across different sectors.
\ No newline at end of file
diff --git a/speakers/Una Wang/bio.md b/speakers/Una Wang/bio.md
new file mode 100644
index 0000000..556ef10
--- /dev/null
+++ b/speakers/Una Wang/bio.md
@@ -0,0 +1 @@
+Una Wang researches machine agency, decentralized governance, and cyber-physical systems in the built environment as a Civil Engineering PhD candidate at ETH Zurich. Her work bridges blockchain technology, autonomous systems, and regenerative systems, exploring how these technologies can transform ownership and governance in physical infrastructure. She led research projects no1s1, co-founded DaoSuisse and Zuitzerland, and advises startups on blockchain integration and decentralized infrastructure.
\ No newline at end of file
diff --git a/speakers/Una Wang/una.jpg b/speakers/Una Wang/una.jpg
new file mode 100644
index 0000000..9ae82ca
Binary files /dev/null and b/speakers/Una Wang/una.jpg differ
diff --git a/speakers/Veronica/bio.md b/speakers/Veronica/bio.md
new file mode 100644
index 0000000..7237ddb
--- /dev/null
+++ b/speakers/Veronica/bio.md
@@ -0,0 +1 @@
+Veronica keeps organizing pop-up cities despite continuously declaring she is not an event organizer, and previously organized ZuVillage Georgia in 2024 and Zuitzerland in 2025. Her background and passions span education technology, artificial intelligence, decentralized technologies, individual and collective optimization, and truth seeking.
\ No newline at end of file
diff --git a/speakers/Veronica/veronica.png b/speakers/Veronica/veronica.png
new file mode 100644
index 0000000..6aa40c0
Binary files /dev/null and b/speakers/Veronica/veronica.png differ
diff --git a/styles.css b/styles.css
index 77e871b..20fdf71 100644
--- a/styles.css
+++ b/styles.css
@@ -1,122 +1,1328 @@
+/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
-:root {
- --color-bg: #f5f3ef;
- --color-text: #2d3a2d;
- --color-accent: #5a7247;
- --color-accent-hover: #4a6237;
- --color-muted: #6b7b6b;
- --font-display: 'Playfair Display', Georgia, serif;
- --font-body: 'Inter', system-ui, sans-serif;
-}
-
-body {
- font-family: var(--font-body);
- background: var(--color-bg);
- color: var(--color-text);
- min-height: 100vh;
+/* Loading Screen */
+.loading-screen {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #000 url('photos/shortintro-poster.png') center/cover no-repeat;
+ z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
- line-height: 1.6;
+ overflow: hidden;
+ opacity: 1;
+ transition: opacity 1.2s ease-out;
}
-.container {
- max-width: 600px;
- padding: 3rem 2rem;
- text-align: center;
+.loading-screen.hidden {
+ opacity: 0;
+ pointer-events: none;
}
-header {
- margin-bottom: 3rem;
-}
-
-h1 {
- font-family: var(--font-display);
- font-size: clamp(2.5rem, 8vw, 4rem);
- font-weight: 600;
- letter-spacing: -0.02em;
- margin-bottom: 0.5rem;
- color: var(--color-text);
-}
-
-.tagline {
- font-size: 1.125rem;
- color: var(--color-muted);
- font-weight: 300;
- letter-spacing: 0.1em;
- text-transform: uppercase;
-}
-
-main {
- margin-bottom: 4rem;
-}
-
-.description {
- font-size: 1.25rem;
- color: var(--color-muted);
- margin-bottom: 3rem;
- font-weight: 300;
-}
-
-.signup p {
- font-size: 0.9rem;
- color: var(--color-muted);
- margin-bottom: 1rem;
-}
-
-.email-form {
+.loading-image-container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
display: flex;
- flex-direction: column;
- gap: 0.75rem;
- max-width: 320px;
- margin: 0 auto;
+ align-items: center;
+ justify-content: center;
+ animation: none;
}
-@media (min-width: 480px) {
- .email-form {
- flex-direction: row;
+.loading-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transform: none;
+ animation: none;
+}
+
+.loading-title {
+ position: absolute;
+ bottom: 15%;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 10001;
+ text-align: center;
+ opacity: 0;
+ animation: fadeUpTitle 4s ease-out 2s forwards;
+}
+
+@media (max-width: 768px) {
+ .loading-title {
+ bottom: auto;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
}
}
-.email-form input {
- flex: 1;
- padding: 0.875rem 1rem;
- border: 1px solid #d4d0c8;
- border-radius: 4px;
- font-size: 1rem;
- font-family: var(--font-body);
- background: white;
- transition: border-color 0.2s ease;
+.skip-loading-button {
+ position: absolute;
+ top: var(--spacing-md);
+ right: var(--spacing-md);
+ z-index: 10002;
+ background: rgba(255, 255, 255, 0.2);
+ color: #fff;
+ border: 1px solid rgba(255, 255, 255, 0.5);
+ padding: var(--spacing-xs) var(--spacing-sm);
+ font-size: 14px;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ backdrop-filter: blur(4px);
}
-.email-form input:focus {
- outline: none;
+@media (max-width: 768px) {
+ .skip-loading-button {
+ left: 50%;
+ right: auto;
+ transform: translateX(-50%);
+ }
+}
+
+.skip-loading-button:hover {
+ background: rgba(255, 255, 255, 0.3);
+ border-color: rgba(255, 255, 255, 0.7);
+}
+
+.loading-title h1 {
+ font-family: 'Playfair Display', 'Cormorant Garamond', serif;
+ font-size: clamp(2.5rem, 6vw, 5rem);
+ font-weight: 300;
+ font-style: italic;
+ color: #fff;
+ text-shadow: 0 2px 20px rgba(0, 0, 0, 0.5), 0 4px 40px rgba(0, 0, 0, 0.3);
+ letter-spacing: 0.05em;
+ line-height: 1.2;
+ margin: 0;
+}
+
+@keyframes zoomIn {
+ 0% {
+ transform: scale(1);
+ }
+ 100% {
+ transform: scale(1.4);
+ }
+}
+
+@keyframes zoomInImage {
+ 0% {
+ transform: scale(1);
+ opacity: 0.7;
+ }
+ 100% {
+ transform: scale(1.6);
+ opacity: 1;
+ }
+}
+
+@keyframes fadeUpTitle {
+ 0% {
+ opacity: 0;
+ transform: translateX(-50%) translateY(30px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+}
+
+:root {
+ --color-text: #1a1a1a;
+ --color-text-light: #666;
+ --color-bg: #ffffff;
+ --color-accent: #000;
+ --color-link: #000;
+ --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ --font-serif: Georgia, serif;
+ --spacing-xs: 0.5rem;
+ --spacing-sm: 1rem;
+ --spacing-md: 2rem;
+ --spacing-lg: 4rem;
+ --spacing-xl: 6rem;
+ --max-width: 1200px;
+ --line-height: 1.6;
+ --speaker-name-size: 1.1rem;
+ --speaker-read-more-size: 0.85rem;
+ --speaker-toggle-size: 0.5rem;
+ --speaker-bio-size: 0.7rem;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+/* Ensure waitlist section has proper spacing for centering */
+#waitlist {
+ scroll-margin-top: 0;
+ scroll-margin-bottom: 0;
+}
+
+body {
+ font-family: var(--font-sans);
+ color: var(--color-text);
+ background-color: var(--color-bg);
+ line-height: var(--line-height);
+ font-size: 18px;
+}
+
+/* Header */
+.header {
+ position: sticky;
+ top: 0;
+ background: var(--color-bg);
+ border-bottom: 1px solid #e5e5e5;
+ z-index: 100;
+ padding: clamp(0.5rem, 1.5vw, var(--spacing-sm)) 0;
+}
+
+.header-container {
+ max-width: var(--max-width);
+ margin: 0 auto;
+ padding: 0 clamp(0.5rem, 2vw, var(--spacing-md));
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: clamp(0.5rem, 2vw, var(--spacing-sm));
+}
+
+.logo img {
+ height: clamp(24px, 3vw, 40px);
+ width: auto;
+ flex-shrink: 0;
+}
+
+.nav {
+ display: flex;
+ gap: clamp(0.5rem, 1.5vw, var(--spacing-md));
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.nav a {
+ color: var(--color-link);
+ text-decoration: none;
+ text-transform: uppercase;
+ font-size: clamp(10px, 1.2vw, 14px);
+ letter-spacing: 0.05em;
+ font-weight: 500;
+ white-space: nowrap;
+}
+
+.nav a:hover {
+ text-decoration: underline;
+}
+
+.nav-external {
+ color: #666;
+ opacity: 0.8;
+}
+
+.nav-external:hover {
+ color: #999;
+ opacity: 1;
+ text-decoration: underline;
+}
+
+.external-arrow {
+ display: inline-block;
+ margin-left: 4px;
+ font-size: 12px;
+ transition: transform 0.2s ease;
+}
+
+.nav-external:hover .external-arrow {
+ transform: translateX(2px);
+}
+
+.nav-rabbit {
+ font-size: 18px;
+ text-decoration: none;
+ line-height: 1;
+ transition: transform 0.2s ease;
+}
+
+.nav-rabbit:hover {
+ transform: scale(1.2);
+ text-decoration: none;
+}
+
+.nav-speakers-full {
+ display: none;
+}
+
+.nav-speakers-short {
+ display: inline;
+}
+
+.nav a.nav-cta-button {
+ display: inline-block;
+ background: var(--color-accent);
+ color: #ffffff !important;
+ padding: var(--spacing-xs) var(--spacing-sm);
+ text-decoration: none;
+ text-transform: uppercase;
+ font-weight: 600;
+ letter-spacing: 0.1em;
+ font-size: 14px;
+ border: 2px solid var(--color-accent);
+ transition: all 0.2s ease;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ white-space: nowrap;
+}
+
+.nav a.nav-cta-button:hover {
+ background: rgba(0, 0, 0, 0.8);
+ color: #ffffff !important;
+ text-decoration: none;
+ transform: translateY(-2px);
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
+}
+
+/* Sticky CTA Button */
+.cta-sticky {
+ position: fixed;
+ bottom: var(--spacing-md);
+ right: var(--spacing-md);
+ z-index: 1000;
+}
+
+.cta-button {
+ display: inline-block;
+ background: var(--color-accent);
+ color: var(--color-bg);
+ padding: var(--spacing-sm) var(--spacing-md);
+ text-decoration: none;
+ text-transform: uppercase;
+ font-weight: 600;
+ letter-spacing: 0.1em;
+ font-size: 14px;
+ border: 2px solid var(--color-accent);
+ transition: all 0.2s ease;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ white-space: nowrap;
+ margin-left: var(--spacing-md);
+}
+
+.cta-button:hover {
+ background: rgba(0, 0, 0, 0.8);
+ color: var(--color-bg);
+ transform: translateY(-2px);
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
+}
+
+/* Hero Section */
+.hero {
+ position: relative;
+ width: 100%;
+ height: calc(100vh - 210px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+.hero-image {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+}
+
+.hero-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.hero-content {
+ position: relative;
+ z-index: 1;
+ max-width: var(--max-width);
+ padding: var(--spacing-md) var(--spacing-md);
+ text-align: center;
+ color: var(--color-bg);
+ background: rgba(0, 0, 0, 0.4);
+ backdrop-filter: blur(2px);
+ width: 100%;
+ margin: 0 auto;
+}
+
+.hero-content h1 {
+ font-family: 'Playfair Display', 'Cormorant Garamond', serif;
+ font-size: clamp(2rem, 5vw, 4rem);
+ font-weight: 300;
+ font-style: italic;
+ margin-bottom: var(--spacing-sm);
+ line-height: 1.2;
+ letter-spacing: 0.05em;
+}
+
+.hero-tagline {
+ font-size: clamp(0.95rem, 1.8vw, 1.15rem);
+ font-style: italic;
+ margin-bottom: var(--spacing-sm);
+ opacity: 0.95;
+}
+
+.hero-subtitle {
+ font-size: clamp(0.95rem, 1.8vw, 1.1rem);
+ max-width: 800px;
+ margin: 0 auto;
+ line-height: 1.5;
+ margin-bottom: var(--spacing-sm);
+}
+
+.hero-subtitle a {
+ color: var(--color-bg);
+ text-decoration: underline;
+ transition: opacity 0.2s ease;
+}
+
+.hero-subtitle a:hover {
+ opacity: 0.8;
+}
+
+/* Event Dates Banner */
+.event-dates-banner {
+ background: var(--color-accent);
+ color: var(--color-bg);
+ padding: var(--spacing-md) var(--spacing-md);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ max-width: 100%;
+ margin-top: 0;
+}
+
+.event-dates-content {
+ max-width: var(--max-width);
+ text-align: center;
+ flex: 1;
+}
+
+.event-dates-banner .cta-button {
+ position: fixed;
+ right: var(--spacing-md);
+ bottom: var(--spacing-md);
+ transform: none;
+ margin-left: 0;
+ background: var(--color-accent);
+ color: var(--color-bg);
+ z-index: 1000;
+}
+
+.event-dates-banner .cta-button:hover {
+ background: rgba(0, 0, 0, 0.8);
+ color: var(--color-bg);
+ transform: translateY(-2px);
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
+}
+
+.event-title {
+ font-size: clamp(1.3rem, 2.5vw, 1.8rem);
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ margin-bottom: 0.25rem;
+ color: var(--color-bg);
+}
+
+.event-dates {
+ font-size: clamp(1rem, 1.8vw, 1.2rem);
+ font-weight: 400;
+ letter-spacing: 0.05em;
+ opacity: 0.95;
+}
+
+/* Main Content */
+.main-content {
+ max-width: var(--max-width);
+ margin: 0 auto;
+ padding: 3rem var(--spacing-md) 0;
+ position: relative;
+}
+
+.section {
+ margin-bottom: var(--spacing-xl);
+ scroll-margin-top: 80px; /* Account for sticky header */
+}
+
+.section h2 {
+ font-size: clamp(1.75rem, 3vw, 2.5rem);
+ font-weight: 700;
+ margin-bottom: var(--spacing-md);
+ line-height: 1.3;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.section h3 {
+ font-size: clamp(1.25rem, 2vw, 1.75rem);
+ font-weight: 600;
+ margin-top: var(--spacing-md);
+ margin-bottom: var(--spacing-sm);
+ line-height: 1.3;
+ text-transform: none;
+ letter-spacing: 0.02em;
+}
+
+.section h3 a {
+ color: var(--color-accent);
+ text-decoration: none;
+ transition: color 0.2s ease;
+}
+
+.section h3 a:hover {
+ color: var(--color-text);
+ text-decoration: underline;
+}
+
+.explore-link {
+ display: inline-block;
+ color: var(--color-accent);
+ text-decoration: none;
+ font-weight: 500;
+ font-size: 1.1rem;
+ margin-top: var(--spacing-sm);
+ transition: color 0.2s ease;
+}
+
+.explore-link:hover {
+ color: var(--color-text);
+ text-decoration: underline;
+}
+
+.section p {
+ margin-bottom: var(--spacing-md);
+ font-size: 1.2rem;
+ line-height: var(--line-height);
+}
+
+.section p:last-child {
+ margin-bottom: 0;
+}
+
+.feature-list {
+ list-style: none;
+ margin: var(--spacing-md) 0;
+}
+
+.feature-list li {
+ margin-bottom: var(--spacing-sm);
+ padding-left: var(--spacing-md);
+ position: relative;
+ font-size: 1.1rem;
+ line-height: var(--line-height);
+}
+
+.feature-list li::before {
+ content: "•";
+ position: absolute;
+ left: 0;
+ color: var(--color-accent);
+ font-weight: bold;
+}
+
+.feature-list li strong {
+ font-weight: 600;
+}
+
+.highlight-section {
+ background: #f8f8f8;
+ padding: var(--spacing-lg);
+ border-left: 4px solid var(--color-accent);
+ margin: var(--spacing-xl) 0;
+}
+
+/* CTA Section */
+.cta-section {
+ background: var(--color-accent);
+ color: var(--color-bg);
+ padding: var(--spacing-lg);
+ text-align: center;
+ margin-top: var(--spacing-lg);
+ margin-bottom: 0;
+}
+
+.cta-content h2 {
+ color: var(--color-bg);
+ margin-bottom: var(--spacing-sm);
+}
+
+.cta-content p {
+ margin-bottom: var(--spacing-xs);
+ font-size: 1.1rem;
+ line-height: 1.5;
+}
+
+.cta-content a {
+ color: var(--color-bg);
+ text-decoration: underline;
+}
+
+.cta-content a:hover {
+ text-decoration: none;
+}
+
+.cta-final {
+ margin-top: var(--spacing-sm);
+ font-size: 1.25rem;
+ font-weight: 600;
+}
+
+/* Schedule Section */
+.schedule-section {
+ background: #f8f8f8;
+ padding: var(--spacing-md);
+ border-left: 4px solid var(--color-accent);
+ position: relative;
+ overflow-x: visible;
+ width: 100vw;
+ max-width: 100vw;
+ margin-left: calc(-50vw + 50%);
+ margin-right: calc(-50vw + 50%);
+ padding-left: calc(50vw - 50% + var(--spacing-md));
+ padding-right: calc(50vw - 50% + var(--spacing-md));
+ box-sizing: border-box;
+}
+
+.schedule-intro {
+ margin-bottom: var(--spacing-md);
+}
+
+.schedule-intro p {
+ margin-bottom: var(--spacing-sm);
+}
+
+.schedule-photo {
+ display: block;
+ width: 100%;
+ max-width: 1000px;
+ margin: 0 auto var(--spacing-lg);
+ border-radius: 8px;
+ object-fit: cover;
+}
+
+.schedule-weeks {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+ margin-top: var(--spacing-md);
+ width: 100%;
+}
+
+.schedule-week {
+ background: var(--color-bg);
+ padding: var(--spacing-sm);
+ border: 1px solid #e5e5e5;
+ display: flex;
+ flex-direction: column;
+ min-height: auto;
+ position: relative;
+ width: 100%;
+ max-width: 100%;
+ box-sizing: border-box;
+ margin: 0;
+ transform: none;
+ contain: layout style;
+ transition: none !important;
+ animation: none !important;
+}
+
+.schedule-week > h3,
+.schedule-week > .week-theme {
+ display: inline;
+}
+
+.schedule-week > .week-theme {
+ margin-left: 0.5em;
+}
+
+.schedule-week:hover {
border-color: var(--color-accent);
}
-.email-form button {
- padding: 0.875rem 1.5rem;
- background: var(--color-accent);
- color: white;
- border: none;
- border-radius: 4px;
+@media (min-width: 769px) {
+ /* Hover behavior handled by visibility/opacity/max-height rules above */
+}
+
+.schedule-week h3 {
+ font-size: 1.3rem;
+ font-weight: 700;
+ margin-bottom: 0;
+ margin-right: 120px;
+ color: var(--color-accent);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ display: inline;
+}
+
+.schedule-week h3::after {
+ content: ": ";
+ margin-right: 0.25em;
+}
+
+.schedule-week .week-theme {
+ font-size: 1.3rem;
+ line-height: 1.4;
+ color: var(--color-accent);
+ margin: 0;
+ margin-left: 0;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ display: inline;
+}
+
+.schedule-week .week-dates {
+ font-size: 0.9rem;
+ color: var(--color-text-light);
+ margin-bottom: var(--spacing-xs);
+ margin-top: 2px;
+ font-weight: 400;
+ letter-spacing: 0.02em;
+ display: block;
+}
+
+.schedule-week .week-description {
+ margin-top: var(--spacing-xs);
+ position: relative;
+}
+
+.schedule-week .week-description-preview {
+ display: inline;
font-size: 1rem;
- font-family: var(--font-body);
+ line-height: 1.6;
+ color: var(--color-text);
+}
+
+.read-more-link {
+ position: absolute;
+ top: var(--spacing-md);
+ right: var(--spacing-md);
+ color: var(--color-accent);
+ text-decoration: none;
+ font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
- transition: background-color 0.2s ease;
+ z-index: 10;
}
-.email-form button:hover {
- background: var(--color-accent-hover);
+.read-more-link:hover {
+ color: var(--color-text);
+ text-decoration: underline;
}
-footer {
- color: var(--color-muted);
- font-size: 0.875rem;
+.schedule-week .week-description-full {
+ display: none;
+ font-size: 0.95rem;
+ line-height: 1.6;
+ color: var(--color-text);
+ margin-top: var(--spacing-sm);
+ padding-top: var(--spacing-sm);
+ border-top: 1px solid #e5e5e5;
}
+
+.schedule-week:hover .week-description-full,
+.schedule-week.expanded .week-description-full {
+ display: block;
+}
+
+/* Ensure collapsed state works on mobile */
+@media (max-width: 768px) {
+ .schedule-week:not(.expanded) .week-description-full {
+ display: none !important;
+ }
+}
+
+.schedule-week.expanded .read-more-link::after {
+ content: " (click to collapse)";
+ font-size: 0.85rem;
+ font-weight: normal;
+ color: var(--color-text-light);
+}
+
+@media (min-width: 769px) {
+ .read-more-link {
+ pointer-events: none;
+ }
+
+ .schedule-week.expanded .read-more-link::after {
+ display: none;
+ }
+}
+
+@media (max-width: 768px) {
+ .schedule-week.expanded .read-more-link {
+ pointer-events: auto;
+ }
+
+ .schedule-week.expanded .read-more-link {
+ font-size: 0.95rem;
+ }
+
+ .schedule-week.expanded .read-more-link::before {
+ content: "";
+ display: none;
+ }
+
+ /* Hide original text when expanded, show only collapse text */
+ .schedule-week.expanded .read-more-link {
+ color: transparent;
+ font-size: 0;
+ }
+
+ .schedule-week.expanded .read-more-link::after {
+ content: "click to collapse";
+ display: inline;
+ color: var(--color-accent);
+ font-size: 0.95rem;
+ }
+}
+
+.speakers-section {
+ margin-bottom: var(--spacing-xl);
+}
+
+.speakers-container {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: flex-start;
+ gap: var(--spacing-md);
+ margin-top: var(--spacing-md);
+ max-width: 1400px;
+ margin-left: auto;
+ margin-right: auto;
+ padding: 0 var(--spacing-md);
+}
+
+.speaker-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ text-align: center;
+ padding: var(--spacing-sm);
+ background: var(--color-bg);
+ border: 1px solid #e5e5e5;
+ border-radius: 8px;
+ transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
+ position: relative;
+ min-height: auto;
+ width: fit-content;
+ max-width: 200px;
+ align-self: flex-start;
+ overflow: visible;
+}
+
+.speaker-card:hover {
+ border-color: var(--color-accent);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ transform: translateY(-2px);
+ z-index: 1;
+}
+
+.speaker-image {
+ width: 120px;
+ height: 120px;
+ object-fit: cover;
+ border-radius: 50%;
+ display: block;
+ margin-bottom: -0.2rem;
+ border: 3px solid #e5e5e5;
+ transition: border-color 0.3s ease;
+ flex-shrink: 0;
+ filter: grayscale(100%);
+}
+
+.speaker-card:hover .speaker-image {
+ border-color: var(--color-accent);
+}
+
+.speaker-name {
+ font-size: var(--speaker-name-size);
+ font-weight: 600;
+ margin-top: 0;
+ margin-bottom: 0;
+ color: var(--color-accent);
+ text-transform: none;
+ letter-spacing: 0.02em;
+ line-height: 1.25;
+ width: auto;
+ max-width: 160px;
+ min-height: 2.6em;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ word-break: normal;
+ hyphens: none;
+ overflow-wrap: normal;
+}
+
+.speaker-read-more-desktop {
+ font-size: var(--speaker-read-more-size);
+ color: var(--color-text-light);
+ font-style: italic;
+ display: block;
+ width: auto;
+ text-align: center;
+}
+
+.speaker-toggle-text {
+ font-size: var(--speaker-toggle-size);
+ color: var(--color-text-light);
+ margin-top: var(--spacing-xs);
+ font-style: italic;
+ display: none;
+ width: auto;
+ text-align: center;
+}
+
+.speaker-bio {
+ font-size: var(--speaker-bio-size);
+ line-height: 1;
+ color: var(--color-text);
+ margin: 0;
+ margin-top: 0;
+ opacity: 0;
+ max-height: 0;
+ overflow: hidden;
+ text-align: left;
+ max-width: 100%;
+ transition: opacity 0.3s ease, max-height 0.3s ease, margin-top 0.3s ease;
+ visibility: hidden;
+ padding: 0;
+ width: 100%;
+}
+
+.speaker-card:hover .speaker-bio,
+.speaker-card.expanded .speaker-bio {
+ opacity: 1;
+ font-size: var(--speaker-bio-size);
+ max-height: none;
+ overflow: visible;
+ visibility: visible;
+ margin-top: var(--spacing-sm);
+ padding: 0;
+ display: block;
+}
+
+.community-partners-section {
+ margin-bottom: var(--spacing-xl);
+ background: #f8f8f8;
+ padding: var(--spacing-lg);
+ border-left: 4px solid var(--color-accent);
+}
+
+.partners-container {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ align-items: center;
+ gap: var(--spacing-md);
+ margin-top: var(--spacing-lg);
+ padding: var(--spacing-md) 0;
+ width: 100%;
+ max-width: 100%;
+}
+
+.partner-link {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: transform 0.2s ease, opacity 0.2s ease;
+ text-decoration: none;
+ flex: 1 1 auto;
+ padding: var(--spacing-sm) var(--spacing-md);
+ max-width: 300px;
+ min-width: 150px;
+}
+
+.partner-link:hover {
+ transform: scale(1.05);
+ opacity: 0.8;
+}
+
+.partner-logo {
+ max-width: 100%;
+ max-height: 100px;
+ width: auto;
+ height: auto;
+ object-fit: contain;
+ filter: grayscale(100%);
+ opacity: 0.7;
+ transition: filter 0.2s ease, opacity 0.2s ease;
+ display: block;
+}
+
+.partner-link:hover .partner-logo {
+ filter: grayscale(0%);
+ opacity: 1;
+}
+
+.explore-section {
+ margin-bottom: var(--spacing-xl);
+}
+
+.explore-embed {
+ width: 100%;
+ max-width: 100%;
+ margin-top: var(--spacing-md);
+ border-radius: 0;
+ overflow: hidden;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.explore-embed iframe {
+ width: 100%;
+ height: 450px;
+ border: 0;
+ display: block;
+}
+
+/* Waitlist Form */
+.waitlist-form {
+ max-width: 500px;
+ margin: var(--spacing-md) auto var(--spacing-md);
+ padding: 0;
+}
+
+.form-group {
+ margin-bottom: var(--spacing-sm);
+}
+
+.waitlist-form input[type="email"],
+.waitlist-form input[type="text"],
+.waitlist-form textarea {
+ width: 100%;
+ padding: var(--spacing-sm) var(--spacing-md);
+ font-size: 1rem;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ background: rgba(255, 255, 255, 0.1);
+ color: var(--color-bg);
+ border-radius: 0;
+ transition: all 0.2s ease;
+ font-family: var(--font-sans);
+ resize: vertical;
+}
+
+.waitlist-form input[type="email"]:focus,
+.waitlist-form input[type="text"]:focus,
+.waitlist-form textarea:focus {
+ outline: none;
+ border-color: var(--color-bg);
+ background: rgba(255, 255, 255, 0.2);
+}
+
+.waitlist-form input::placeholder,
+.waitlist-form textarea::placeholder {
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.waitlist-submit {
+ width: 100%;
+ padding: var(--spacing-sm) var(--spacing-md);
+ font-size: 1rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ background: var(--color-bg);
+ color: var(--color-accent);
+ border: 2px solid var(--color-bg);
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-family: var(--font-sans);
+ margin-top: var(--spacing-sm);
+}
+
+.waitlist-submit:hover:not(:disabled) {
+ background: transparent;
+ color: var(--color-bg);
+}
+
+.waitlist-submit:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.waitlist-message {
+ margin-top: var(--spacing-xs);
+ padding: var(--spacing-xs) var(--spacing-sm);
+ text-align: center;
+ font-size: 0.95rem;
+ min-height: 1.2rem;
+}
+
+.waitlist-message.success {
+ color: #90EE90;
+ background: rgba(144, 238, 144, 0.1);
+ border: 1px solid rgba(144, 238, 144, 0.3);
+}
+
+.waitlist-message.error {
+ color: #FFB6C1;
+ background: rgba(255, 182, 193, 0.1);
+ border: 1px solid rgba(255, 182, 193, 0.3);
+}
+
+/* Footer */
+.footer {
+ border-top: 1px solid #e5e5e5;
+ padding: var(--spacing-md) 0;
+ margin-top: 0;
+}
+
+.footer-container {
+ max-width: var(--max-width);
+ margin: 0 auto;
+ padding: 0 var(--spacing-md);
+ text-align: center;
+ color: var(--color-text-light);
+ font-size: 14px;
+}
+
+.footer-container a {
+ color: var(--color-link);
+ text-decoration: none;
+}
+
+.footer-container a:hover {
+ text-decoration: underline;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ :root {
+ --spacing-md: 1.5rem;
+ --spacing-lg: 2.5rem;
+ --spacing-xl: 3rem;
+ --speaker-name-size: 1rem;
+ --speaker-bio-size: 0.7rem;
+ }
+
+ .hero {
+ min-height: 50vh;
+ }
+
+ .hero-content {
+ padding: var(--spacing-lg) var(--spacing-md);
+ }
+
+ .event-dates-banner {
+ padding: var(--spacing-md);
+ flex-direction: column;
+ gap: var(--spacing-sm);
+ }
+
+ .event-dates-banner .cta-button {
+ position: static;
+ transform: none;
+ margin-top: var(--spacing-sm);
+ }
+
+ .event-title {
+ font-size: 1.25rem;
+ }
+
+ .event-dates {
+ font-size: 1rem;
+ }
+
+ .main-content {
+ padding: var(--spacing-lg) var(--spacing-md);
+ }
+
+ .section {
+ margin-bottom: var(--spacing-lg);
+ }
+
+ .highlight-section {
+ padding: var(--spacing-md);
+ }
+
+ .cta-section {
+ padding: var(--spacing-lg);
+ }
+
+ .schedule-section {
+ padding: var(--spacing-md);
+ }
+
+ .schedule-weeks {
+ gap: var(--spacing-sm);
+ }
+
+ .schedule-week {
+ min-height: auto;
+ }
+
+ .speakers-container {
+ flex-direction: column;
+ align-items: center;
+ gap: var(--spacing-sm);
+ padding: 0 var(--spacing-sm);
+ }
+
+ .speaker-card {
+ min-height: auto;
+ padding: var(--spacing-xs);
+ width: fit-content;
+ margin: 0 auto;
+ }
+
+ .speaker-image {
+ width: 90px;
+ height: 90px;
+ }
+
+ .speaker-name {
+ line-height: 1.15;
+ min-height: 2.2em;
+ justify-content: flex-start;
+ }
+
+ .speaker-read-more-desktop {
+ display: none;
+ }
+
+ .speaker-toggle-text {
+ display: block;
+ line-height: 1.1;
+ }
+
+ .speaker-bio {
+ line-height: .5;
+ }
+
+ .speaker-card.expanded .speaker-bio {
+ max-height: none;
+ }
+
+ /* Ensure collapsed state works on mobile - matching schedule fix */
+ .speaker-card:not(.expanded) .speaker-bio {
+ opacity: 0 !important;
+ max-height: 0 !important;
+ visibility: hidden !important;
+ margin-top: 0 !important;
+ overflow: hidden !important;
+ }
+
+ .partners-container {
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: var(--spacing-md);
+ padding: var(--spacing-sm) 0;
+ }
+
+ .partner-link {
+ padding: var(--spacing-xs) var(--spacing-sm);
+ max-width: 200px;
+ min-width: 120px;
+ flex: 1 1 auto;
+ }
+
+ .partner-logo {
+ max-width: 100%;
+ max-height: 70px;
+ }
+
+ .cta-sticky {
+ bottom: var(--spacing-sm);
+ right: var(--spacing-sm);
+ }
+
+ .cta-button {
+ padding: var(--spacing-xs) var(--spacing-sm);
+ font-size: 12px;
+ }
+}
+
+@media (max-width: 480px) {
+ .header-container {
+ padding: 0 clamp(0.25rem, 1vw, 0.5rem);
+ flex-wrap: nowrap;
+ gap: clamp(0.25rem, 1vw, 0.5rem);
+ }
+
+ .logo img {
+ height: clamp(18px, 3.5vw, 28px);
+ flex-shrink: 1;
+ }
+
+ .nav {
+ gap: 0;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ flex: 1;
+ min-width: 0;
+ justify-content: space-between;
+ }
+
+ .nav::-webkit-scrollbar {
+ display: none;
+ }
+
+ .nav-speakers-full {
+ display: none;
+ }
+
+ .nav-speakers-short {
+ display: inline;
+ }
+
+ .nav a[href="#explore"] {
+ display: none;
+ }
+
+ .nav a.nav-get-involved {
+ display: none;
+ }
+
+ .nav a {
+ font-size: clamp(10px, 2.5vw, 14px);
+ flex-shrink: 0;
+ white-space: nowrap;
+ padding: clamp(0.2rem, 1vw, 0.4rem) clamp(0.3rem, 1.2vw, 0.6rem);
+ }
+
+ .nav a:not(.nav-cta-button):not(.nav-rabbit) {
+ min-width: fit-content;
+ text-align: center;
+ }
+
+ .nav a.nav-rabbit {
+ font-size: clamp(14px, 3vw, 18px);
+ padding: 0 clamp(0.2rem, 0.8vw, 0.4rem);
+ }
+
+ .nav a.nav-cta-button {
+ background: var(--color-accent);
+ z-index: 10;
+ font-size: clamp(10px, 2.5vw, 14px);
+ padding: clamp(0.3rem, 1.2vw, 0.5rem) clamp(0.5rem, 2vw, 0.8rem);
+ flex-shrink: 0;
+ }
+}
+
diff --git a/test-waitlist.js b/test-waitlist.js
new file mode 100644
index 0000000..7ff3e3e
--- /dev/null
+++ b/test-waitlist.js
@@ -0,0 +1,150 @@
+// Test script for waitlist API
+// Run with: node test-waitlist.js
+// Make sure .env file exists with your credentials
+
+require('dotenv').config();
+const { google } = require('googleapis');
+
+async function testWaitlist() {
+ console.log('🧪 Testing Google Sheets Waitlist Integration...\n');
+
+ // Check environment variables
+ const credentials = process.env.GOOGLE_SERVICE_ACCOUNT;
+ const spreadsheetId = process.env.GOOGLE_SHEET_ID;
+ const sheetName = process.env.GOOGLE_SHEET_NAME || 'Waitlist';
+
+ console.log('📋 Configuration Check:');
+ console.log(' ✓ GOOGLE_SERVICE_ACCOUNT:', credentials ? `Set (${credentials.length} chars)` : '❌ MISSING');
+ console.log(' ✓ GOOGLE_SHEET_ID:', spreadsheetId || '❌ MISSING');
+ console.log(' ✓ GOOGLE_SHEET_NAME:', sheetName);
+ console.log('');
+
+ if (!credentials || !spreadsheetId) {
+ console.error('❌ Missing required environment variables!');
+ console.error(' Create a .env file with GOOGLE_SERVICE_ACCOUNT and GOOGLE_SHEET_ID');
+ process.exit(1);
+ }
+
+ try {
+ // Parse credentials
+ console.log('🔐 Parsing credentials...');
+ const credentialsTrimmed = credentials.trim();
+ let serviceAccount;
+ try {
+ serviceAccount = JSON.parse(credentialsTrimmed);
+ console.log(' ✓ Credentials parsed successfully');
+ console.log(' ✓ Service account email:', serviceAccount.client_email);
+ } catch (parseError) {
+ console.error(' ❌ Failed to parse credentials:', parseError.message);
+ process.exit(1);
+ }
+
+ // Authenticate
+ console.log('\n🔑 Authenticating with Google...');
+ const auth = new google.auth.GoogleAuth({
+ credentials: serviceAccount,
+ scopes: ['https://www.googleapis.com/auth/spreadsheets'],
+ });
+ console.log(' ✓ Authentication successful');
+
+ // Get sheets client
+ const sheets = google.sheets({ version: 'v4', auth });
+
+ // Test 1: Get spreadsheet metadata
+ console.log('\n📊 Test 1: Checking spreadsheet access...');
+ try {
+ const spreadsheet = await sheets.spreadsheets.get({
+ spreadsheetId,
+ });
+ console.log(' ✓ Spreadsheet found:', spreadsheet.data.properties.title);
+ console.log(' ✓ Spreadsheet ID:', spreadsheetId);
+ } catch (error) {
+ console.error(' ❌ Cannot access spreadsheet:', error.message);
+ if (error.code === 403) {
+ console.error(' → Permission denied. Make sure the service account email has Editor access to the sheet.');
+ console.error(' → Service account email:', serviceAccount.client_email);
+ } else if (error.code === 404) {
+ console.error(' → Spreadsheet not found. Check the GOOGLE_SHEET_ID.');
+ }
+ process.exit(1);
+ }
+
+ // Test 2: Check if sheet tab exists
+ console.log('\n📑 Test 2: Checking sheet tab...');
+ try {
+ const spreadsheet = await sheets.spreadsheets.get({
+ spreadsheetId,
+ });
+ const sheetTabs = spreadsheet.data.sheets.map(s => s.properties.title);
+ console.log(' ✓ Available sheet tabs:', sheetTabs.join(', '));
+
+ if (!sheetTabs.includes(sheetName)) {
+ console.error(` ❌ Sheet tab "${sheetName}" not found!`);
+ console.error(' → Available tabs:', sheetTabs.join(', '));
+ console.error(' → Update GOOGLE_SHEET_NAME or create a tab named "' + sheetName + '"');
+ process.exit(1);
+ }
+ console.log(` ✓ Sheet tab "${sheetName}" exists`);
+ } catch (error) {
+ console.error(' ❌ Error checking sheet tabs:', error.message);
+ process.exit(1);
+ }
+
+ // Test 3: Try to read from the sheet
+ console.log('\n📖 Test 3: Reading from sheet...');
+ try {
+ const response = await sheets.spreadsheets.values.get({
+ spreadsheetId,
+ range: `${sheetName}!A1:C10`,
+ });
+ console.log(' ✓ Can read from sheet');
+ if (response.data.values) {
+ console.log(' ✓ Current rows:', response.data.values.length);
+ }
+ } catch (error) {
+ console.error(' ❌ Cannot read from sheet:', error.message);
+ process.exit(1);
+ }
+
+ // Test 4: Try to append a test row
+ console.log('\n✍️ Test 4: Testing write access...');
+ try {
+ const testTimestamp = new Date().toISOString();
+ const testEmail = 'test@example.com';
+ const testName = 'Test User';
+ const values = [[testTimestamp, testEmail, testName]];
+
+ await sheets.spreadsheets.values.append({
+ spreadsheetId,
+ range: `${sheetName}!A:C`,
+ valueInputOption: 'USER_ENTERED',
+ insertDataOption: 'INSERT_ROWS',
+ resource: {
+ values,
+ },
+ });
+ console.log(' ✓ Successfully wrote test row to sheet');
+ console.log(' ✓ Check your Google Sheet - you should see a test entry');
+ } catch (error) {
+ console.error(' ❌ Cannot write to sheet:', error.message);
+ if (error.response?.status === 403) {
+ console.error(' → Permission denied. Service account needs Editor access.');
+ }
+ process.exit(1);
+ }
+
+ console.log('\n✅ All tests passed! Your configuration is correct.');
+ console.log('\n💡 Next steps:');
+ console.log(' 1. Make sure these same values are set in Vercel environment variables');
+ console.log(' 2. Deploy to Vercel');
+ console.log(' 3. Test the form on your live site');
+
+ } catch (error) {
+ console.error('\n❌ Unexpected error:', error.message);
+ console.error(' Stack:', error.stack);
+ process.exit(1);
+ }
+}
+
+testWaitlist();
+
diff --git a/valley.webp b/valley.webp
new file mode 100644
index 0000000..570ad76
Binary files /dev/null and b/valley.webp differ
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 0000000..07558a4
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,13 @@
+{
+ "version": 2,
+ "buildCommand": null,
+ "outputDirectory": ".",
+ "framework": null,
+ "rewrites": [
+ {
+ "source": "/((?!api/).*)",
+ "destination": "/index.html"
+ }
+ ]
+}
+
diff --git a/waitlist.js b/waitlist.js
new file mode 100644
index 0000000..5585319
--- /dev/null
+++ b/waitlist.js
@@ -0,0 +1,159 @@
+// Smooth scroll to section, ensuring title is visible
+function scrollToSection(sectionId, e) {
+ const section = document.getElementById(sectionId);
+ if (!section) return;
+
+ e.preventDefault();
+
+ // Get header height for offset
+ const header = document.querySelector('.header');
+ const headerHeight = header ? header.offsetHeight : 0;
+ const padding = 40; // Extra padding to ensure title is fully visible
+
+ // Calculate position to show section with title visible
+ const sectionRect = section.getBoundingClientRect();
+ const sectionTop = sectionRect.top + window.pageYOffset;
+ const viewportHeight = window.innerHeight;
+
+ // Scroll to position that shows the section title with proper spacing
+ // Position section so it starts below header with padding
+ const scrollPosition = sectionTop - headerHeight - padding;
+
+ // Smooth scroll to position
+ window.scrollTo({
+ top: Math.max(0, scrollPosition),
+ behavior: 'smooth'
+ });
+}
+
+// Smooth scroll to waitlist section, centered on page
+function scrollToWaitlist(e) {
+ scrollToSection('waitlist', e);
+}
+
+// Loading screen handler
+function initLoadingScreen() {
+ const loadingScreen = document.getElementById('loading-screen');
+ if (!loadingScreen) return;
+
+ const skipButton = document.getElementById('skip-loading');
+ if (skipButton) {
+ skipButton.addEventListener('click', function() {
+ loadingScreen.classList.add('hidden');
+ setTimeout(function() {
+ loadingScreen.remove();
+ }, 1200);
+ });
+ }
+
+ // Wait for page to fully load
+ window.addEventListener('load', function() {
+ // Add delay to allow title animation to complete (6s zoom + 4s title = ~6s total)
+ setTimeout(function() {
+ loadingScreen.classList.add('hidden');
+ // Remove from DOM after transition completes
+ setTimeout(function() {
+ loadingScreen.remove();
+ }, 1200);
+ }, 6500); // Wait for animations to complete
+ });
+}
+
+// Waitlist form submission handler and scroll setup
+document.addEventListener('DOMContentLoaded', function() {
+ // Initialize loading screen
+ initLoadingScreen();
+
+ // Set up scroll handlers for all anchor links
+ const waitlistLinks = document.querySelectorAll('a[href="#waitlist"]');
+ waitlistLinks.forEach(link => {
+ link.addEventListener('click', scrollToWaitlist);
+ });
+
+ const scheduleLinks = document.querySelectorAll('a[href="#schedule"]');
+ scheduleLinks.forEach(link => {
+ link.addEventListener('click', (e) => scrollToSection('schedule', e));
+ });
+
+ const form = document.getElementById('waitlist-form');
+ const emailInput = document.getElementById('waitlist-email');
+ const nameInput = document.getElementById('waitlist-name');
+ const involvementInput = document.getElementById('waitlist-involvement');
+ const messageDiv = document.getElementById('waitlist-message');
+ const submitButton = form.querySelector('button[type="submit"]');
+
+ if (!form) return;
+
+ form.addEventListener('submit', async function(e) {
+ e.preventDefault();
+
+ // Disable form during submission
+ submitButton.disabled = true;
+ submitButton.textContent = 'Submitting...';
+ messageDiv.textContent = '';
+ messageDiv.className = 'waitlist-message';
+
+ const email = emailInput.value.trim();
+ const name = nameInput.value.trim();
+
+ // Basic email validation
+ if (!email || !email.includes('@')) {
+ showMessage('Please enter a valid email address.', 'error');
+ submitButton.disabled = false;
+ submitButton.textContent = 'Submit';
+ return;
+ }
+
+ // Name is now required
+ if (!name || name.trim() === '') {
+ showMessage('Please enter your name.', 'error');
+ submitButton.disabled = false;
+ submitButton.textContent = 'Submit';
+ return;
+ }
+
+ const involvement = involvementInput.value.trim();
+
+ // Involvement is required
+ if (!involvement || involvement === '') {
+ showMessage('Please describe your desired involvement.', 'error');
+ submitButton.disabled = false;
+ submitButton.textContent = 'Submit';
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/waitlist', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ email, name, involvement }),
+ });
+
+ const data = await response.json();
+
+ if (response.ok && data.success) {
+ showMessage('Thank you! Your information has been submitted.', 'success');
+ form.reset();
+ } else {
+ showMessage(data.error || 'Something went wrong. Please try again.', 'error');
+ }
+ } catch (error) {
+ console.error('Error submitting form:', error);
+ showMessage('Network error. Please check your connection and try again.', 'error');
+ } finally {
+ submitButton.disabled = false;
+ submitButton.textContent = 'Submit';
+ }
+ });
+
+ function showMessage(message, type) {
+ messageDiv.textContent = message;
+ messageDiv.className = `waitlist-message ${type}`;
+
+ // Scroll message into view if needed
+ messageDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }
+});
+