From a402cbaef5a1a1dbfd68c4a97ce0a803534fc4f0 Mon Sep 17 00:00:00 2001 From: Atlas SHB Date: Tue, 23 Jun 2026 13:37:25 +0200 Subject: [PATCH] =?UTF-8?q?Initial=20community=20site=20=E2=80=94=20AI=20H?= =?UTF-8?q?UB=20Tilburg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .claude/launch.json | 11 ++ README.md | 112 ++++++++++++++++ config.json | 40 ++++++ docker-compose.yml | 19 +++ evolution/.env.example | 5 + evolution/welcome_bot.py | 90 +++++++++++++ nginx.conf | 25 ++++ site/app.js | 86 ++++++++++++ site/config.js | 5 + site/index.html | 174 ++++++++++++++++++++++++ site/style.css | 277 +++++++++++++++++++++++++++++++++++++++ 11 files changed, 844 insertions(+) create mode 100644 .claude/launch.json create mode 100644 README.md create mode 100644 config.json create mode 100644 docker-compose.yml create mode 100644 evolution/.env.example create mode 100644 evolution/welcome_bot.py create mode 100644 nginx.conf create mode 100644 site/app.js create mode 100644 site/config.js create mode 100644 site/index.html create mode 100644 site/style.css diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..720cbb2 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "tilburg-hub", + "runtimeExecutable": "python", + "runtimeArgs": ["-m", "http.server", "3456", "--directory", "site"], + "port": 3456 + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..34ff4bf --- /dev/null +++ b/README.md @@ -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 +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. diff --git a/config.json b/config.json new file mode 100644 index 0000000..1c4d447 --- /dev/null +++ b/config.json @@ -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:00–14: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 + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..502b884 --- /dev/null +++ b/docker-compose.yml @@ -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] diff --git a/evolution/.env.example b/evolution/.env.example new file mode 100644 index 0000000..a6a14ee --- /dev/null +++ b/evolution/.env.example @@ -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 diff --git a/evolution/welcome_bot.py b/evolution/welcome_bot.py new file mode 100644 index 0000000..bae4dd9 --- /dev/null +++ b/evolution/welcome_bot.py @@ -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:00–14: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() diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..7e2c044 --- /dev/null +++ b/nginx.conf @@ -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; +} diff --git a/site/app.js b/site/app.js new file mode 100644 index 0000000..c57816c --- /dev/null +++ b/site/app.js @@ -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 '
  • ' + r + '
  • '; + }).join(''); + } + + // Projects grid + var grid = document.getElementById('projects-grid'); + if (grid && cfg.projects && cfg.projects.length) { + grid.innerHTML = cfg.projects.map(function (p) { + return [ + '
    ', + '
    ' + (p.icon || '🔧') + '
    ', + '

    ' + p.name + '

    ', + '

    ' + p.description + '

    ', + p.url ? ' Bekijk project →' : '', + '
    ' + (p.author || '') + '
    ', + '
    ' + ].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'); + }); + }); +})(); diff --git a/site/config.js b/site/config.js new file mode 100644 index 0000000..5437c49 --- /dev/null +++ b/site/config.js @@ -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") diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..fdc800a --- /dev/null +++ b/site/index.html @@ -0,0 +1,174 @@ + + + + + + AI HUB Tilburg + + + + + + + + + + + + + +
    +
    +
    Opgericht 2026 · Tilburg
    +

    Builders,
    thinkers,
    makers.

    +

    + 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. +

    + +
    + 🟢 Actief + · + 5048 AB Tilburg + · + Open voor iedereen +
    +
    +
    + + +
    +
    +
    + Wat is dit? +

    Geen club, geen bedrijf.
    Een verzamelpunt.

    +

    + 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. +

    +

    + Wat je hier vindt: mensen die vragen stellen, kennis delen, resources samenvoegen en soms gewoon laten zien waar ze mee bezig zijn. +

    +
    +
    +
    + 01 +

    Jouw project, jouw tempo

    +

    Niemand stuurt jou. Je deelt wat je wil, wanneer je wil.

    +
    +
    + 02 +

    Middelen samenvoegen

    +

    Kennis, netwerk, tools — als je iets kunt bieden, bied je het aan. Vrijwillig.

    +
    +
    + 03 +

    Compound groei

    +

    Samen sneller dan alleen. Door te zien wat anderen doen, groei je zelf.

    +
    +
    +
    +
    + + +
    +
    + Bijeenkomsten +

    Elke week. Geen agenda.

    +
    +
    +
    📅
    +

    Elke zaterdag

    +

    12:00–14:00

    +

    Inloop — kom als je kan, ga als je wil

    +
    +
    +
    📍
    +

    Business Centre Tilburg

    +

    Kraaivenstraat 36-14

    +

    5048 AB Tilburg

    + Open in Maps → +
    +
    +
    💻
    +

    Online check-in

    +

    Kan je er niet bij zijn?

    +

    Check WhatsApp voor actuele locatie en updates

    +
    +
    +
    +
    + + +
    +
    + Spelregels +

    Vijf afspraken. Niet meer.

    +
      +
    1. Iedereen heeft zijn eigen project. Niemand beslist voor een ander.
    2. +
    3. Geen commerciële pitch zonder toestemming van de groep.
    4. +
    5. Opbouwend — vragen stellen is beter dan oordelen.
    6. +
    7. Aanwezig zijn als je kan. Online check-in altijd welkom.
    8. +
    9. Middelen en kennis delen als je wil — niet verplicht.
    10. +
    +
    +
    + + +
    +
    + Projecten +

    Wat leden bouwen.

    +

    Leden kunnen hier hun project toevoegen. Geen pitch — gewoon: wat ben je aan het doen?

    +
    + +
    +
    🚧
    +

    Jouw project hier? Deel het in de WhatsApp groep.

    +
    +
    +
    +
    + + +
    +
    + Doe mee +

    Klaar om mee te bouwen?

    +

    Sluit je aan via WhatsApp. Geen sollicitatie, geen formulier. Gewoon joinen en aanwezig zijn.

    + + 📱 Word lid via WhatsApp + +

    Stuur een berichtje wie je bent en wat je bouwt.
    Dat is alles.

    +
    +
    + + + + + + diff --git a/site/style.css b/site/style.css new file mode 100644 index 0000000..eb831e9 --- /dev/null +++ b/site/style.css @@ -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; } +}