Initial community site — AI HUB Tilburg

Zero-dependency static site. config.json-driven.
Deploy: docker compose up -d
Publish: GitHub Pages / Cloudflare Pages / Tailscale Serve

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Atlas SHB 2026-06-23 13:37:25 +02:00
commit a402cbaef5
11 changed files with 844 additions and 0 deletions

11
.claude/launch.json Normal file
View file

@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "tilburg-hub",
"runtimeExecutable": "python",
"runtimeArgs": ["-m", "http.server", "3456", "--directory", "site"],
"port": 3456
}
]
}

112
README.md Normal file
View file

@ -0,0 +1,112 @@
# AI HUB Tilburg — Community Site
Open-source community website template. Clone it, configure it, deploy it in 5 minutes.
## Quick start
```bash
git clone <this-repo>
cd tilburg-hub
# 1. Edit your community details
nano config.json # or open in any editor
# 2. Run
docker compose up -d
# 3. Open http://localhost:3456
```
That's it. No database, no build step, no accounts.
## Configuration
All community settings live in `config.json`:
| Key | What it does |
|---|---|
| `community.name` | Site title and nav logo |
| `community.tagline` | Short description |
| `join.whatsapp_invite` | The WhatsApp group invite link |
| `location.*` | Physical address shown on the site |
| `meetings.*` | Meeting schedule and format |
| `rules[]` | Community rules (array of strings) |
| `projects[]` | Member projects to showcase |
| `founding_members[]` | Listed in the footer |
## Rename for your community
```bash
# Replace all default placeholders in one command:
grep -rl 'AI HUB Tilburg' . | xargs sed -i 's/AI HUB Tilburg/Your Community Name/g'
```
Then update `config.json` with your details.
## Deploy publicly (free, no server required)
### Option A — GitHub Pages
1. Push this repo to GitHub
2. Go to Settings → Pages → Deploy from `main` branch `/site` folder
3. Done. Your community is live at `https://YOUR_ORG.github.io/tilburg-hub`
### Option B — Cloudflare Pages
1. Connect your GitHub repo to Cloudflare Pages
2. Root: `site/`, build command: *(none — static)*
3. Free HTTPS, global CDN
### Option C — MET Desktop (Docker, local network / Tailscale)
```bash
docker compose up -d
```
Then expose via Tailscale Serve or Cloudflare Tunnel:
```bash
# Tailscale Serve (one-liner, works on Windows/Mac/Linux)
tailscale serve http://localhost:3456
# Or use the Cloudflare Tunnel block in docker-compose.yml (uncomment + add your token)
```
## Evolution API integration (optional WhatsApp bot)
If you have an Evolution API instance connected to WhatsApp, you can wire a webhook to:
- Welcome new members when they join the group
- Post project updates to the group from the website
See `evolution/` folder for webhook scripts. Set your API key in `.env`:
```
EVOLUTION_API_URL=http://YOUR_SERVER:8120
EVOLUTION_API_KEY=your-key-here
EVOLUTION_INSTANCE=your-instance-name
WA_GROUP_JID=123456789@g.us
```
## Project structure
```
tilburg-hub/
├── config.json ← ALL community settings (edit this)
├── docker-compose.yml ← one-command deploy
├── nginx.conf ← webserver config
├── site/
│ ├── index.html ← the page
│ ├── style.css ← styles (warm, neutral, not Atlas)
│ ├── app.js ← reads config.json, populates page
│ └── config.js ← placeholder (overwritten by app.js)
├── evolution/ ← optional WhatsApp webhook scripts
└── README.md
```
## Philosophy
This site was built to be:
- **Independent** — no central server, no Atlas branding required
- **Forkable** — anyone can clone and run their own community
- **Zero-dependency** — pure HTML/CSS/JS, no framework, no npm
- **Config-first** — change everything without touching HTML
## License
MIT — do what you want with it.

40
config.json Normal file
View file

@ -0,0 +1,40 @@
{
"community": {
"name": "AI HUB Tilburg",
"tagline": "Builders, thinkers, makers — geen leider, wel richting.",
"description": "Een open community voor iedereen in de regio Tilburg die bouwt, onderzoekt of experimenteert met AI, technologie en ondernemen. Geen agenda, geen hiërarchie. Je project is van jou.",
"founded": "2026",
"language": "nl"
},
"location": {
"name": "Business Centre Tilburg",
"address": "Kraaivenstraat 36-14",
"city": "5048 AB Tilburg",
"country": "Nederland",
"maps_query": "Kraaivenstraat+36+Tilburg"
},
"meetings": {
"schedule": "Elke zaterdag",
"time": "12:0014:00",
"format": "Inloop — kom als je kan, ga als je wil",
"note": "Check WhatsApp voor actuele locatie en updates"
},
"join": {
"whatsapp_invite": "https://chat.whatsapp.com/REPLACE_WITH_YOUR_LINK",
"cta_text": "Word lid via WhatsApp"
},
"rules": [
"Iedereen heeft zijn eigen project. Niemand beslist voor een ander.",
"Geen commerciële pitch zonder toestemming van de groep.",
"Opbouwend — vragen stellen is beter dan oordelen.",
"Aanwezig zijn als je kan. Online check-in altijd welkom.",
"Middelen en kennis delen als je wil — niet verplicht."
],
"founding_members": [
{ "name": "Atlas Corporation", "role": "Founding member · sponsor · facilitator", "url": "https://atlascorporation.nl" }
],
"projects": [],
"social": {
"whatsapp": true
}
}

19
docker-compose.yml Normal file
View file

@ -0,0 +1,19 @@
services:
web:
image: nginx:alpine
restart: unless-stopped
ports:
- "3456:80" # change 3456 to any free port on your machine
volumes:
- ./site:/usr/share/nginx/html:ro
- ./config.json:/usr/share/nginx/html/config.json:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
labels:
- "community=tilburg-hub"
# Optional: Cloudflare Tunnel for public HTTPS without port forwarding
# tunnel:
# image: cloudflare/cloudflared:latest
# restart: unless-stopped
# command: tunnel --no-autoupdate run --token YOUR_TUNNEL_TOKEN
# depends_on: [web]

5
evolution/.env.example Normal file
View file

@ -0,0 +1,5 @@
EVOLUTION_API_URL=http://YOUR_SERVER:8120
EVOLUTION_API_KEY=your-api-key-here
EVOLUTION_INSTANCE=atlas
WA_GROUP_JID=123456789@g.us
BOT_PORT=8765

90
evolution/welcome_bot.py Normal file
View file

@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""
AI HUB Tilburg WhatsApp welcome bot via Evolution API.
Sends a welcome message when a new member joins the group.
Setup:
cp .env.example .env
# fill in your values
python3 welcome_bot.py --serve # starts a webhook server on :8765
"""
import os, json, sys, http.server, urllib.request, urllib.parse
EVO_URL = os.getenv("EVOLUTION_API_URL", "http://localhost:8120")
EVO_KEY = os.getenv("EVOLUTION_API_KEY", "")
INSTANCE = os.getenv("EVOLUTION_INSTANCE", "atlas")
GROUP_JID = os.getenv("WA_GROUP_JID", "") # 123456789@g.us
WELCOME_MESSAGE = """👋 Welkom bij AI HUB Tilburg!
We zijn een open community van builders, makers en denkers in de regio Tilburg. Geen leider, geen verplichtingen.
📍 We komen samen op zaterdag 12:0014:00 bij Business Centre Tilburg.
Vertel gerust wie je bent en wat je bouwt. Niemand beslist voor jou jouw project is van jou.
Fijn dat je erbij bent! 🙌"""
def send_message(to_jid: str, text: str):
payload = json.dumps({"number": to_jid, "text": text}).encode()
req = urllib.request.Request(
f"{EVO_URL}/message/sendText/{INSTANCE}",
data=payload,
headers={"Content-Type": "application/json", "apikey": EVO_KEY},
method="POST"
)
try:
with urllib.request.urlopen(req, timeout=10) as r:
print(f"[bot] sent to {to_jid}: HTTP {r.status}")
return True
except Exception as e:
print(f"[bot] error: {e}")
return False
class WebhookHandler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
self.send_response(200)
self.end_headers()
try:
event = json.loads(body)
except Exception:
return
# Evolution API group participant update event
event_type = event.get("event", "")
if event_type == "group-participants.update":
data = event.get("data", {})
action = data.get("action", "")
jid = data.get("id", "")
if action in ("add", "invite") and jid:
print(f"[bot] new member: {jid}")
# Welcome in group
send_message(GROUP_JID, WELCOME_MESSAGE)
# Or DM the new member directly:
# send_message(jid, WELCOME_MESSAGE)
def log_message(self, *args):
pass # suppress access logs
def main():
if "--serve" in sys.argv:
port = int(os.getenv("BOT_PORT", "8765"))
server = http.server.HTTPServer(("0.0.0.0", port), WebhookHandler)
print(f"[bot] listening on :{port}")
print(f"[bot] register this URL as Evolution webhook for instance '{INSTANCE}'")
server.serve_forever()
else:
print("Usage: python3 welcome_bot.py --serve")
print(" Then register http://YOUR_IP:8765 as a webhook in Evolution API")
if __name__ == "__main__":
main()

25
nginx.conf Normal file
View file

@ -0,0 +1,25 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# serve config.json with correct MIME type + CORS for fetch()
location = /config.json {
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
}
# SPA fallback (not needed for static, but safe to have)
location / {
try_files $uri $uri/ /index.html;
}
# Security headers
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header Referrer-Policy no-referrer-when-downgrade;
gzip on;
gzip_types text/html text/css application/javascript application/json image/svg+xml;
}

86
site/app.js Normal file
View file

@ -0,0 +1,86 @@
// AI HUB Tilburg — community site runtime
// Reads config.json and populates the page.
(function () {
function get(obj, path) {
return path.split('.').reduce(function (o, k) {
return o && o[k] !== undefined ? o[k] : null;
}, obj);
}
function applyConfig(cfg) {
// Text nodes
document.querySelectorAll('[data-config]').forEach(function (el) {
var val = get(cfg, el.dataset.config);
if (val !== null) el.textContent = val;
});
// href → direct config path
document.querySelectorAll('[data-config-href]').forEach(function (el) {
var val = get(cfg, el.dataset.configHref);
if (val) el.href = val;
});
// href → Google Maps query
document.querySelectorAll('[data-config-href-maps]').forEach(function (el) {
var val = get(cfg, el.dataset.configHrefMaps);
if (val) el.href = 'https://www.google.com/maps/search/' + encodeURIComponent(val);
});
// Rules list
var rulesEl = document.getElementById('rules-list');
if (rulesEl && cfg.rules && cfg.rules.length) {
rulesEl.innerHTML = cfg.rules.map(function (r) {
return '<li>' + r + '</li>';
}).join('');
}
// Projects grid
var grid = document.getElementById('projects-grid');
if (grid && cfg.projects && cfg.projects.length) {
grid.innerHTML = cfg.projects.map(function (p) {
return [
'<div class="project-card">',
' <div class="project-icon">' + (p.icon || '🔧') + '</div>',
' <h3>' + p.name + '</h3>',
' <p>' + p.description + '</p>',
p.url ? ' <a class="link" href="' + p.url + '" target="_blank" rel="noopener">Bekijk project →</a>' : '',
' <div class="project-meta" style="margin-top:12px;font-size:12px;color:var(--text-4);font-family:var(--font-mono)">' + (p.author || '') + '</div>',
'</div>'
].join('');
}).join('');
}
// Founding members in footer
var fLink = document.getElementById('founding-link');
if (fLink && cfg.founding_members && cfg.founding_members.length) {
var fm = cfg.founding_members[0];
fLink.textContent = fm.name;
if (fm.url) fLink.href = fm.url;
}
// Language
if (cfg.community && cfg.community.language) {
document.documentElement.lang = cfg.community.language;
}
// Page title
if (cfg.community && cfg.community.name) {
document.title = cfg.community.name;
}
}
// Try to load from config.json (works when served via HTTP)
fetch('../config.json')
.then(function (r) { return r.json(); })
.then(applyConfig)
.catch(function () {
// Fallback: try same directory
fetch('config.json')
.then(function (r) { return r.json(); })
.then(applyConfig)
.catch(function () {
console.info('[tilburg-hub] config.json not found — using HTML defaults');
});
});
})();

5
site/config.js Normal file
View file

@ -0,0 +1,5 @@
// Auto-generated from config.json by build.js
// Or served directly if no build step is used
// Edit config.json — not this file.
window.HUB_CONFIG = null; // populated by app.js via fetch("../config.json")

174
site/index.html Normal file
View file

@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI HUB Tilburg</title>
<meta name="description" content="Een open community voor builders, thinkers en makers in de regio Tilburg. Geen leider, wel richting.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap">
<link rel="stylesheet" href="style.css">
<script src="config.js" defer></script>
<script src="app.js" defer></script>
</head>
<body>
<!-- NAV -->
<nav>
<div class="container nav-inner">
<div class="logo">
<span class="logo-dot"></span>
<span class="logo-text" data-config="community.name">AI HUB Tilburg</span>
</div>
<a href="#join" class="btn btn-sm">Word lid →</a>
</div>
</nav>
<!-- HERO -->
<section class="hero">
<div class="container">
<div class="badge" data-config="community.founded">Opgericht 2026 · Tilburg</div>
<h1>Builders,<br>thinkers,<br><em>makers.</em></h1>
<p class="lead" data-config="community.description">
Een open community voor iedereen in de regio Tilburg die bouwt, onderzoekt of experimenteert met AI, technologie en ondernemen. Geen agenda, geen hiërarchie. Je project is van jou.
</p>
<div class="hero-actions">
<a href="#join" class="btn btn-primary" data-config-href="join.whatsapp_invite" data-config-text="join.cta_text">Word lid via WhatsApp</a>
<a href="#bijeenkomsten" class="btn btn-ghost">Wanneer & Waar →</a>
</div>
<div class="hero-meta">
<span>🟢 Actief</span>
<span>·</span>
<span data-config="location.city">5048 AB Tilburg</span>
<span>·</span>
<span>Open voor iedereen</span>
</div>
</div>
</section>
<!-- WHAT -->
<section class="section" id="over">
<div class="container grid-2">
<div>
<span class="kicker">Wat is dit?</span>
<h2>Geen club, geen bedrijf.<br>Een verzamelpunt.</h2>
<p>
AI HUB Tilburg is een informele community. We hebben geen bestuur, geen contributie en geen gemeenschappelijke agenda. Iedereen werkt aan zijn eigen project — samen in dezelfde ruimte, met dezelfde energie.
</p>
<p>
Wat je hier vindt: mensen die vragen stellen, kennis delen, resources samenvoegen en soms gewoon laten zien waar ze mee bezig zijn.
</p>
</div>
<div class="card-stack">
<div class="principle-card">
<span class="number">01</span>
<h3>Jouw project, jouw tempo</h3>
<p>Niemand stuurt jou. Je deelt wat je wil, wanneer je wil.</p>
</div>
<div class="principle-card">
<span class="number">02</span>
<h3>Middelen samenvoegen</h3>
<p>Kennis, netwerk, tools — als je iets kunt bieden, bied je het aan. Vrijwillig.</p>
</div>
<div class="principle-card">
<span class="number">03</span>
<h3>Compound groei</h3>
<p>Samen sneller dan alleen. Door te zien wat anderen doen, groei je zelf.</p>
</div>
</div>
</div>
</section>
<!-- MEETINGS -->
<section class="section section-tinted" id="bijeenkomsten">
<div class="container">
<span class="kicker">Bijeenkomsten</span>
<h2>Elke week. Geen agenda.</h2>
<div class="meetings-grid">
<div class="meeting-card">
<div class="meeting-icon">📅</div>
<h3 data-config="meetings.schedule">Elke zaterdag</h3>
<p data-config="meetings.time">12:0014:00</p>
<p class="sub" data-config="meetings.format">Inloop — kom als je kan, ga als je wil</p>
</div>
<div class="meeting-card">
<div class="meeting-icon">📍</div>
<h3 data-config="location.name">Business Centre Tilburg</h3>
<p data-config="location.address">Kraaivenstraat 36-14</p>
<p class="sub" data-config="location.city">5048 AB Tilburg</p>
<a href="#" class="link" id="maps-link" data-config-href-maps="location.maps_query">Open in Maps →</a>
</div>
<div class="meeting-card">
<div class="meeting-icon">💻</div>
<h3>Online check-in</h3>
<p>Kan je er niet bij zijn?</p>
<p class="sub" data-config="meetings.note">Check WhatsApp voor actuele locatie en updates</p>
</div>
</div>
</div>
</section>
<!-- RULES -->
<section class="section" id="spelregels">
<div class="container container-narrow">
<span class="kicker">Spelregels</span>
<h2>Vijf afspraken. Niet meer.</h2>
<ol class="rules-list" id="rules-list">
<li>Iedereen heeft zijn eigen project. Niemand beslist voor een ander.</li>
<li>Geen commerciële pitch zonder toestemming van de groep.</li>
<li>Opbouwend — vragen stellen is beter dan oordelen.</li>
<li>Aanwezig zijn als je kan. Online check-in altijd welkom.</li>
<li>Middelen en kennis delen als je wil — niet verplicht.</li>
</ol>
</div>
</section>
<!-- PROJECTS -->
<section class="section section-tinted" id="projecten">
<div class="container">
<span class="kicker">Projecten</span>
<h2>Wat leden bouwen.</h2>
<p class="section-lead">Leden kunnen hier hun project toevoegen. Geen pitch — gewoon: wat ben je aan het doen?</p>
<div class="projects-grid" id="projects-grid">
<!-- Populated from config.json or submitted by members -->
<div class="project-card project-placeholder">
<div class="project-icon">🚧</div>
<p>Jouw project hier? Deel het in de WhatsApp groep.</p>
</div>
</div>
</div>
</section>
<!-- JOIN -->
<section class="section join-section" id="join">
<div class="container container-narrow text-center">
<span class="kicker">Doe mee</span>
<h2>Klaar om mee te bouwen?</h2>
<p>Sluit je aan via WhatsApp. Geen sollicitatie, geen formulier. Gewoon joinen en aanwezig zijn.</p>
<a class="btn btn-primary btn-lg" href="#" data-config-href="join.whatsapp_invite" id="join-btn">
📱 Word lid via WhatsApp
</a>
<p class="join-sub">Stuur een berichtje wie je bent en wat je bouwt.<br>Dat is alles.</p>
</div>
</section>
<!-- FOOTER -->
<footer>
<div class="container footer-inner">
<div class="footer-left">
<span class="logo-text" data-config="community.name">AI HUB Tilburg</span>
<span class="footer-sub" data-config="location.city">Tilburg · Nederland</span>
</div>
<div class="footer-right">
<span class="footer-sub">Founding member:</span>
<a href="https://atlascorporation.nl" target="_blank" rel="noopener" id="founding-link">Atlas Corporation</a>
</div>
<div class="footer-copy">
<span>Open community · Geen commercieel belang · </span>
<a href="https://github.com/YOUR_ORG/tilburg-hub" target="_blank" rel="noopener">Broncode op GitHub</a>
</div>
</div>
</footer>
</body>
</html>

277
site/style.css Normal file
View file

@ -0,0 +1,277 @@
/* AI HUB Tilburg Community Site
Deliberately NOT Atlas dark/space theme.
Warm, neutral, maker-space energy. */
:root {
--bg: #FAFAF8;
--bg-tint: #F4F2EE;
--surface: #FFFFFF;
--border: #E8E4DE;
--text-1: #1A1714;
--text-2: #4A4540;
--text-3: #7A736C;
--text-4: #A8A098;
--accent: #2B6CB0;
--accent-warm: #C05621;
--accent-green:#276749;
--font-sans: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'Courier New', monospace;
--r: 8px;
--r-lg: 16px;
--shadow: 0 1px 4px rgba(0,0,0,0.06), 0 4px 16px rgba(0,0,0,0.04);
--shadow-lg: 0 8px 32px rgba(0,0,0,0.08);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; font-size: 16px; }
body {
font-family: var(--font-sans);
background: var(--bg);
color: var(--text-1);
line-height: 1.65;
-webkit-font-smoothing: antialiased;
}
/* LAYOUT */
.container { max-width: 1080px; margin: 0 auto; padding: 0 24px; }
.container-narrow { max-width: 680px; }
.text-center { text-align: center; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 64px; align-items: start; }
@media (max-width: 720px) { .grid-2 { grid-template-columns: 1fr; gap: 40px; } }
/* TYPOGRAPHY */
h1 { font-size: clamp(52px, 8vw, 96px); font-weight: 800; line-height: 1.0; letter-spacing: -0.03em; color: var(--text-1); }
h1 em { font-style: normal; color: var(--accent); }
h2 { font-size: clamp(28px, 4vw, 44px); font-weight: 700; line-height: 1.15; letter-spacing: -0.02em; margin-bottom: 20px; }
h3 { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
p { color: var(--text-2); font-size: 16px; line-height: 1.75; margin-bottom: 16px; }
p:last-child { margin-bottom: 0; }
.lead { font-size: 19px; line-height: 1.7; color: var(--text-2); max-width: 56ch; margin-top: 24px; margin-bottom: 36px; }
.section-lead { font-size: 18px; color: var(--text-2); max-width: 52ch; margin-bottom: 40px; }
.sub { color: var(--text-3); font-size: 14px; margin-top: 4px; }
.link { color: var(--accent); font-size: 14px; font-weight: 500; text-decoration: none; display: inline-block; margin-top: 12px; }
.link:hover { text-decoration: underline; }
.kicker {
display: inline-block;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-3);
margin-bottom: 16px;
}
/* BUTTONS */
.btn {
display: inline-flex; align-items: center; gap: 8px;
padding: 12px 22px; border-radius: var(--r);
font-size: 15px; font-weight: 600; text-decoration: none;
cursor: pointer; border: none; transition: all 0.15s ease;
line-height: 1;
}
.btn-sm { padding: 8px 16px; font-size: 13px; }
.btn-lg { padding: 16px 32px; font-size: 17px; }
.btn-primary {
background: var(--accent); color: #fff;
box-shadow: 0 2px 8px rgba(43,108,176,0.25);
}
.btn-primary:hover { background: #2558a0; transform: translateY(-1px); box-shadow: 0 4px 16px rgba(43,108,176,0.3); }
.btn-ghost {
background: transparent; color: var(--text-2);
border: 1px solid var(--border);
}
.btn-ghost:hover { border-color: var(--text-3); color: var(--text-1); }
/* NAV */
nav {
position: sticky; top: 0; z-index: 100;
background: rgba(250,250,248,0.92);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
}
.nav-inner {
display: flex; align-items: center; justify-content: space-between;
height: 60px;
}
.logo { display: flex; align-items: center; gap: 10px; }
.logo-dot {
width: 10px; height: 10px; border-radius: 50%;
background: var(--accent);
animation: pulse 2.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(0.85); }
}
.logo-text { font-size: 15px; font-weight: 700; color: var(--text-1); }
/* HERO */
.hero {
padding: 96px 0 80px;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: -40%; right: -20%;
width: 600px; height: 600px;
border-radius: 50%;
background: radial-gradient(circle, rgba(43,108,176,0.06) 0%, transparent 70%);
pointer-events: none;
}
.hero h1 { margin-bottom: 0; }
.hero-actions { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; }
.hero-meta {
display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
margin-top: 32px;
font-family: var(--font-mono); font-size: 12px;
color: var(--text-3); letter-spacing: 0.04em;
}
.badge {
display: inline-block;
font-family: var(--font-mono); font-size: 11px;
color: var(--accent); background: rgba(43,108,176,0.08);
border: 1px solid rgba(43,108,176,0.2);
border-radius: 999px; padding: 4px 12px;
margin-bottom: 24px; letter-spacing: 0.06em;
}
/* SECTIONS */
.section { padding: 96px 0; }
.section-tinted { background: var(--bg-tint); }
/* PRINCIPLE CARDS */
.card-stack { display: flex; flex-direction: column; gap: 16px; }
.principle-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 24px 28px;
position: relative;
transition: box-shadow 0.2s;
}
.principle-card:hover { box-shadow: var(--shadow-lg); }
.number {
font-family: var(--font-mono); font-size: 11px; font-weight: 600;
color: var(--text-4); letter-spacing: 0.1em;
display: block; margin-bottom: 10px;
}
.principle-card h3 { font-size: 16px; margin-bottom: 6px; }
.principle-card p { font-size: 14px; margin: 0; }
/* MEETINGS */
.meetings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-top: 40px;
}
.meeting-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 28px 24px;
transition: box-shadow 0.2s;
}
.meeting-card:hover { box-shadow: var(--shadow-lg); }
.meeting-icon { font-size: 28px; margin-bottom: 14px; }
.meeting-card h3 { font-size: 17px; margin-bottom: 6px; }
.meeting-card p { font-size: 15px; margin-bottom: 2px; }
/* RULES */
.rules-list {
list-style: none;
counter-reset: rule;
display: flex; flex-direction: column; gap: 16px;
margin-top: 32px;
}
.rules-list li {
counter-increment: rule;
display: flex; align-items: flex-start; gap: 16px;
padding: 20px 24px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r);
font-size: 16px; color: var(--text-1); line-height: 1.6;
}
.rules-list li::before {
content: counter(rule, decimal-leading-zero);
font-family: var(--font-mono); font-size: 11px; font-weight: 600;
color: var(--text-4); white-space: nowrap; margin-top: 3px;
}
/* PROJECTS */
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 20px;
}
.project-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 28px;
transition: box-shadow 0.2s;
}
.project-card:hover { box-shadow: var(--shadow-lg); }
.project-icon { font-size: 32px; margin-bottom: 14px; }
.project-placeholder {
border-style: dashed;
display: flex; flex-direction: column; align-items: center;
justify-content: center; text-align: center;
min-height: 160px; color: var(--text-3);
}
.project-placeholder p { color: var(--text-3); font-size: 14px; }
/* JOIN */
.join-section {
background: var(--text-1);
color: #fff;
padding: 96px 0;
}
.join-section .kicker { color: rgba(255,255,255,0.4); }
.join-section h2 { color: #fff; }
.join-section p { color: rgba(255,255,255,0.65); max-width: 44ch; margin: 0 auto 36px; }
.join-sub { font-size: 14px; color: rgba(255,255,255,0.4); margin-top: 20px; text-align: center; }
/* FOOTER */
footer {
background: var(--bg-tint);
border-top: 1px solid var(--border);
padding: 40px 0;
}
.footer-inner {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
align-items: center;
}
.footer-copy {
grid-column: 1 / -1;
font-size: 12px; color: var(--text-4);
font-family: var(--font-mono);
}
.footer-copy a { color: var(--accent); text-decoration: none; }
.footer-left { display: flex; flex-direction: column; gap: 4px; }
.footer-right { text-align: right; display: flex; flex-direction: column; gap: 4px; align-items: flex-end; }
.footer-sub { font-size: 12px; color: var(--text-3); }
.footer-right a { color: var(--accent); font-size: 14px; font-weight: 500; text-decoration: none; }
@media (max-width: 600px) {
.footer-inner { grid-template-columns: 1fr; }
.footer-right { text-align: left; align-items: flex-start; }
.footer-copy { grid-column: 1; }
.hero { padding: 60px 0 56px; }
.section { padding: 64px 0; }
.join-section { padding: 64px 0; }
}