Add Billing/Stripe integration and Landing Page (Phase 4+5)

Phase 4 - Billing/Stripe:
- src/Billing/StripeService.php: Stripe API wrapper
  - Checkout session creation
  - Customer management
  - Billing portal sessions
  - Webhook signature verification
- src/Billing/SubscriptionManager.php: Subscription logic
  - Plan management (CRUD)
  - Trial handling
  - Feature access checks
  - Invoice storage
- src/Billing/WebhookHandler.php: Stripe webhook processing
  - checkout.session.completed
  - customer.subscription.* events
  - invoice.paid / payment_failed
- api/stripe-webhook.php: Webhook endpoint
- dashboard/billing.php: Billing dashboard
  - Current plan display with features
  - Plan comparison grid
  - Upgrade buttons with Stripe Checkout
  - Invoice history

Phase 5 - Landing Page:
- landing/index.php: Marketing homepage
  - Hero section with CTA
  - Feature grid (6 features)
  - How it works (3 steps)
  - Final CTA section
  - Responsive design
- landing/pricing.php: Pricing page
  - Dynamic plan cards from DB
  - Monthly/yearly toggle (2 months free)
  - Feature comparison
  - FAQ accordion

All features respect saas_features toggles in settings.
This commit is contained in:
Claude
2026-01-23 19:16:18 +00:00
parent ac77e27089
commit 16673b91d3
7 changed files with 2082 additions and 0 deletions
+422
View File
@@ -0,0 +1,422 @@
<?php
/**
* Landing Page - Marketing Seite
*/
require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/SettingsManager.php';
$settingsManager = new SettingsManager();
// Prüfe ob Landing Page aktiviert
if (!$settingsManager->isLandingPageEnabled()) {
header('Location: /');
exit;
}
$trialDays = $settingsManager->getTrialDays();
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aurora Livecam - Ihre Webcam als Service</title>
<meta name="description" content="Erstellen Sie Ihre eigene Live-Webcam in wenigen Minuten. Wetter-Widget, Timelapse, Analytics und mehr. Jetzt kostenlos testen!">
<link rel="stylesheet" href="/dashboard/assets/dashboard.css">
<style>
:root {
--gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #1a202c;
}
/* Header */
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
background: rgba(255,255,255,0.95);
backdrop-filter: blur(10px);
z-index: 100;
border-bottom: 1px solid #e2e8f0;
}
.header-inner {
max-width: 1200px;
margin: 0 auto;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: 700;
background: var(--gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-decoration: none;
}
.nav-links {
display: flex;
gap: 2rem;
align-items: center;
}
.nav-links a {
color: #4a5568;
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
.nav-links a:hover {
color: #667eea;
}
/* Hero */
.hero {
padding: 8rem 2rem 6rem;
background: var(--gradient);
color: white;
text-align: center;
}
.hero h1 {
font-size: 3rem;
font-weight: 800;
margin-bottom: 1.5rem;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.hero p {
font-size: 1.25rem;
opacity: 0.9;
max-width: 600px;
margin: 0 auto 2rem;
}
.hero-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.btn-hero {
padding: 1rem 2rem;
border-radius: 0.5rem;
font-size: 1.1rem;
font-weight: 600;
text-decoration: none;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-hero-primary {
background: white;
color: #667eea;
}
.btn-hero-secondary {
background: rgba(255,255,255,0.2);
color: white;
border: 2px solid rgba(255,255,255,0.5);
}
.btn-hero:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
}
.trial-badge {
display: inline-block;
background: rgba(255,255,255,0.2);
padding: 0.5rem 1rem;
border-radius: 2rem;
margin-top: 2rem;
font-size: 0.9rem;
}
/* Features */
.features {
padding: 6rem 2rem;
background: #f7fafc;
}
.section-title {
text-align: center;
margin-bottom: 4rem;
}
.section-title h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.section-title p {
color: #718096;
font-size: 1.1rem;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.feature-card {
background: white;
padding: 2rem;
border-radius: 1rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
transition: transform 0.2s, box-shadow 0.2s;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.feature-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.feature-card h3 {
font-size: 1.25rem;
margin-bottom: 0.75rem;
}
.feature-card p {
color: #718096;
}
/* How it works */
.how-it-works {
padding: 6rem 2rem;
max-width: 1000px;
margin: 0 auto;
}
.steps {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
margin-top: 3rem;
}
.step {
text-align: center;
}
.step-number {
width: 60px;
height: 60px;
background: var(--gradient);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 700;
margin: 0 auto 1rem;
}
.step h4 {
margin-bottom: 0.5rem;
}
.step p {
color: #718096;
font-size: 0.9rem;
}
/* CTA */
.cta {
padding: 6rem 2rem;
background: var(--gradient);
color: white;
text-align: center;
}
.cta h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.cta p {
font-size: 1.1rem;
opacity: 0.9;
margin-bottom: 2rem;
}
/* Footer */
.footer {
background: #1a202c;
color: #a0aec0;
padding: 3rem 2rem;
}
.footer-inner {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 2rem;
}
.footer-links a {
color: #a0aec0;
text-decoration: none;
margin-right: 1.5rem;
}
.footer-links a:hover {
color: white;
}
/* Responsive */
@media (max-width: 768px) {
.hero h1 { font-size: 2rem; }
.nav-links { display: none; }
.features-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<!-- Header -->
<header class="header">
<div class="header-inner">
<a href="/" class="logo">Aurora Livecam</a>
<nav class="nav-links">
<a href="#features">Features</a>
<a href="/landing/pricing.php">Preise</a>
<a href="/dashboard/login.php">Login</a>
<a href="/onboarding/register.php" class="btn btn-primary btn-sm">Kostenlos starten</a>
</nav>
</div>
</header>
<!-- Hero -->
<section class="hero">
<h1>Ihre Webcam als Service - in 5 Minuten online</h1>
<p>Erstellen Sie Ihre eigene Live-Webcam-Website mit Wetter-Widget, Timelapse, Analytics und mehr. Keine Programmierkenntnisse erforderlich.</p>
<div class="hero-buttons">
<a href="/onboarding/register.php" class="btn-hero btn-hero-primary">
Jetzt starten
</a>
<a href="#features" class="btn-hero btn-hero-secondary">
Features ansehen
</a>
</div>
<div class="trial-badge">
<?php echo $trialDays; ?> Tage kostenlos testen - Keine Kreditkarte erforderlich
</div>
</section>
<!-- Features -->
<section class="features" id="features">
<div class="section-title">
<h2>Alles was Sie brauchen</h2>
<p>Professionelle Features für Ihre Live-Webcam</p>
</div>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">📹</div>
<h3>Live-Streaming</h3>
<p>HLS, RTMP oder WebRTC - verbinden Sie jeden Stream in Sekunden. Automatische Qualitätsanpassung inklusive.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🌤️</div>
<h3>Wetter-Widget</h3>
<p>Zeigen Sie Temperatur, Wind, Luftdruck und mehr an. Kostenlose Open-Meteo Integration ohne API-Key.</p>
</div>
<div class="feature-card">
<div class="feature-icon">⏱️</div>
<h3>Timelapse</h3>
<p>Automatische Zeitraffer-Erstellung. Scrubben Sie durch den ganzen Tag mit variabler Geschwindigkeit.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔍</div>
<h3>Zoom & Pan</h3>
<p>Lassen Sie Besucher in Ihren Stream hineinzoomen. Unterstützt Touch-Gesten und Maus-Steuerung.</p>
</div>
<div class="feature-card">
<div class="feature-icon">📊</div>
<h3>Analytics</h3>
<p>Sehen Sie wer Ihre Webcam besucht. Echtzeit-Zuschauerzähler und detaillierte Statistiken.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🎨</div>
<h3>Custom Branding</h3>
<p>Ihr Logo, Ihre Farben, Ihre Domain. Machen Sie die Webcam zu Ihrer eigenen.</p>
</div>
</div>
</section>
<!-- How it works -->
<section class="how-it-works">
<div class="section-title">
<h2>So einfach geht's</h2>
<p>In 3 Schritten zur eigenen Livecam</p>
</div>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
<h4>Registrieren</h4>
<p>Erstellen Sie in 30 Sekunden Ihr kostenloses Konto.</p>
</div>
<div class="step">
<div class="step-number">2</div>
<h4>Stream verbinden</h4>
<p>Fügen Sie Ihre Stream-URL ein. Wir unterstützen alle gängigen Formate.</p>
</div>
<div class="step">
<div class="step-number">3</div>
<h4>Anpassen & Teilen</h4>
<p>Personalisieren Sie Ihre Seite und teilen Sie den Link.</p>
</div>
</div>
</section>
<!-- CTA -->
<section class="cta">
<h2>Bereit loszulegen?</h2>
<p><?php echo $trialDays; ?> Tage kostenlos testen - keine Kreditkarte erforderlich</p>
<a href="/onboarding/register.php" class="btn-hero btn-hero-primary">
Jetzt kostenlos starten
</a>
</section>
<!-- Footer -->
<footer class="footer">
<div class="footer-inner">
<div>
© <?php echo date('Y'); ?> Aurora Livecam. Alle Rechte vorbehalten.
</div>
<div class="footer-links">
<a href="/terms">AGB</a>
<a href="/privacy">Datenschutz</a>
<a href="/imprint">Impressum</a>
<a href="mailto:support@aurora-livecam.com">Kontakt</a>
</div>
</div>
</footer>
</body>
</html>
+497
View File
@@ -0,0 +1,497 @@
<?php
/**
* Landing Page - Preise
*/
require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/SettingsManager.php';
if (file_exists(dirname(__DIR__) . '/src/bootstrap.php')) {
require_once dirname(__DIR__) . '/src/bootstrap.php';
}
use AuroraLivecam\Billing\SubscriptionManager;
$settingsManager = new SettingsManager();
// Pläne laden
$plans = [];
try {
$subscriptions = new SubscriptionManager();
$plans = $subscriptions->getPlans();
} catch (\Exception $e) {
// Fallback-Pläne
$plans = [
['name' => 'Free', 'slug' => 'free', 'price_monthly' => 0, 'features' => ['max_viewers' => 10, 'weather_widget' => true]],
['name' => 'Basic', 'slug' => 'basic', 'price_monthly' => 19, 'features' => ['max_viewers' => 50, 'weather_widget' => true, 'timelapse' => true, 'analytics' => true]],
['name' => 'Professional', 'slug' => 'professional', 'price_monthly' => 49, 'features' => ['max_viewers' => 200, 'custom_domain' => true, 'weather_widget' => true, 'timelapse' => true, 'analytics' => true, 'branding' => true]],
['name' => 'Enterprise', 'slug' => 'enterprise', 'price_monthly' => 149, 'features' => ['max_viewers' => -1, 'custom_domain' => true, 'weather_widget' => true, 'timelapse' => true, 'analytics' => true, 'branding' => true, 'priority_support' => true]],
];
}
$trialDays = $settingsManager->getTrialDays();
// Feature-Labels
$featureLabels = [
'max_viewers' => 'Gleichzeitige Zuschauer',
'storage_gb' => 'Speicherplatz',
'custom_domain' => 'Eigene Domain',
'weather_widget' => 'Wetter-Widget',
'timelapse' => 'Timelapse',
'analytics' => 'Analytics & Statistiken',
'branding' => 'Custom Branding',
'priority_support' => 'Priority Support',
];
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Preise - Aurora Livecam</title>
<link rel="stylesheet" href="/dashboard/assets/dashboard.css">
<style>
:root {
--gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #1a202c;
background: #f7fafc;
}
.header {
background: white;
border-bottom: 1px solid #e2e8f0;
padding: 1rem 2rem;
}
.header-inner {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: 700;
background: var(--gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-decoration: none;
}
.nav-links a {
color: #4a5568;
text-decoration: none;
margin-left: 1.5rem;
}
.page-header {
text-align: center;
padding: 4rem 2rem;
background: var(--gradient);
color: white;
}
.page-header h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.page-header p {
font-size: 1.1rem;
opacity: 0.9;
}
.pricing-toggle {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 2rem;
align-items: center;
}
.pricing-toggle span {
font-size: 0.9rem;
}
.pricing-toggle .active {
font-weight: 600;
}
.toggle-switch {
width: 60px;
height: 30px;
background: rgba(255,255,255,0.3);
border-radius: 15px;
position: relative;
cursor: pointer;
}
.toggle-switch::after {
content: '';
position: absolute;
width: 26px;
height: 26px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: 0.3s;
}
.toggle-switch.yearly::after {
left: 32px;
}
.save-badge {
background: #48bb78;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
}
.pricing-container {
max-width: 1200px;
margin: -3rem auto 4rem;
padding: 0 2rem;
}
.pricing-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.pricing-card {
background: white;
border-radius: 1rem;
padding: 2rem;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
position: relative;
display: flex;
flex-direction: column;
}
.pricing-card.featured {
border: 2px solid #667eea;
transform: scale(1.05);
}
.pricing-card.featured::before {
content: 'Beliebt';
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: var(--gradient);
color: white;
padding: 0.25rem 1rem;
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 600;
}
.pricing-card h3 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.pricing-card .price {
font-size: 3rem;
font-weight: 800;
margin: 1rem 0;
}
.pricing-card .price span {
font-size: 1rem;
font-weight: 400;
color: #718096;
}
.pricing-card .price-yearly {
display: none;
}
.yearly-mode .price-monthly { display: none; }
.yearly-mode .price-yearly { display: block; }
.pricing-card ul {
list-style: none;
flex: 1;
margin: 1.5rem 0;
}
.pricing-card li {
padding: 0.5rem 0;
color: #4a5568;
display: flex;
align-items: center;
gap: 0.5rem;
}
.pricing-card li.included::before {
content: '✓';
color: #48bb78;
font-weight: bold;
}
.pricing-card li.not-included {
color: #a0aec0;
text-decoration: line-through;
}
.pricing-card li.not-included::before {
content: '✗';
color: #e53e3e;
}
.pricing-card .btn {
width: 100%;
padding: 1rem;
border: none;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
text-decoration: none;
text-align: center;
transition: all 0.2s;
}
.pricing-card .btn-primary {
background: var(--gradient);
color: white;
}
.pricing-card .btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.pricing-card .btn-secondary {
background: #e2e8f0;
color: #4a5568;
}
.faq {
max-width: 800px;
margin: 0 auto 4rem;
padding: 0 2rem;
}
.faq h2 {
text-align: center;
margin-bottom: 2rem;
}
.faq-item {
background: white;
border-radius: 0.5rem;
margin-bottom: 1rem;
overflow: hidden;
}
.faq-question {
padding: 1.25rem;
font-weight: 600;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.faq-answer {
padding: 0 1.25rem 1.25rem;
color: #718096;
display: none;
}
.faq-item.open .faq-answer {
display: block;
}
.footer {
background: #1a202c;
color: #a0aec0;
padding: 2rem;
text-align: center;
}
@media (max-width: 768px) {
.pricing-card.featured {
transform: none;
}
.pricing-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<header class="header">
<div class="header-inner">
<a href="/landing/" class="logo">Aurora Livecam</a>
<nav class="nav-links">
<a href="/landing/">Home</a>
<a href="/dashboard/login.php">Login</a>
<a href="/onboarding/register.php" class="btn btn-primary btn-sm">Kostenlos starten</a>
</nav>
</div>
</header>
<section class="page-header">
<h1>Einfache, transparente Preise</h1>
<p><?php echo $trialDays; ?> Tage kostenlos testen - jederzeit kündbar</p>
<div class="pricing-toggle">
<span class="monthly-label active">Monatlich</span>
<div class="toggle-switch" id="billing-toggle"></div>
<span class="yearly-label">Jährlich</span>
<span class="save-badge">2 Monate gratis</span>
</div>
</section>
<div class="pricing-container" id="pricing-container">
<div class="pricing-grid">
<?php foreach ($plans as $index => $plan): ?>
<?php $isFeatured = $plan['slug'] === 'professional'; ?>
<div class="pricing-card <?php echo $isFeatured ? 'featured' : ''; ?>">
<h3><?php echo htmlspecialchars($plan['name']); ?></h3>
<div class="price price-monthly">
<?php if ($plan['price_monthly'] > 0): ?>
CHF <?php echo number_format($plan['price_monthly'], 0); ?><span>/Monat</span>
<?php else: ?>
Kostenlos
<?php endif; ?>
</div>
<div class="price price-yearly">
<?php if (isset($plan['price_yearly']) && $plan['price_yearly'] > 0): ?>
CHF <?php echo number_format($plan['price_yearly'] / 12, 0); ?><span>/Monat</span>
<div style="font-size: 0.875rem; color: #718096;">
CHF <?php echo number_format($plan['price_yearly'], 0); ?> jährlich
</div>
<?php elseif ($plan['price_monthly'] > 0): ?>
CHF <?php echo number_format($plan['price_monthly'] * 10 / 12, 0); ?><span>/Monat</span>
<div style="font-size: 0.875rem; color: #718096;">
CHF <?php echo number_format($plan['price_monthly'] * 10, 0); ?> jährlich
</div>
<?php else: ?>
Kostenlos
<?php endif; ?>
</div>
<ul>
<?php
$features = is_array($plan['features']) ? $plan['features'] : json_decode($plan['features'], true) ?? [];
$allFeatures = ['max_viewers', 'weather_widget', 'timelapse', 'analytics', 'custom_domain', 'branding', 'priority_support'];
foreach ($allFeatures as $feature):
$hasFeature = !empty($features[$feature]);
$value = $features[$feature] ?? null;
?>
<li class="<?php echo $hasFeature ? 'included' : 'not-included'; ?>">
<?php
if ($feature === 'max_viewers' && $value) {
echo $value === -1 ? 'Unbegrenzte Zuschauer' : "Bis $value Zuschauer";
} elseif ($feature === 'storage_gb' && $value) {
echo "$value GB Speicher";
} else {
echo $featureLabels[$feature] ?? ucfirst(str_replace('_', ' ', $feature));
}
?>
</li>
<?php endforeach; ?>
</ul>
<a href="/onboarding/register.php?plan=<?php echo $plan['slug']; ?>"
class="btn <?php echo $isFeatured || $plan['price_monthly'] > 0 ? 'btn-primary' : 'btn-secondary'; ?>">
<?php echo $plan['price_monthly'] > 0 ? 'Jetzt starten' : 'Kostenlos starten'; ?>
</a>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- FAQ -->
<section class="faq">
<h2>Häufige Fragen</h2>
<div class="faq-item">
<div class="faq-question">
Kann ich jederzeit wechseln oder kündigen?
<span>+</span>
</div>
<div class="faq-answer">
Ja! Sie können Ihren Plan jederzeit upgraden oder downgraden. Bei einer Kündigung bleibt Ihr Zugang bis zum Ende der Abrechnungsperiode aktiv.
</div>
</div>
<div class="faq-item">
<div class="faq-question">
Was passiert nach dem Trial?
<span>+</span>
</div>
<div class="faq-answer">
Nach Ablauf der <?php echo $trialDays; ?> Tage werden Sie automatisch auf den kostenlosen Plan umgestellt, sofern Sie kein Abo abschliessen. Keine Sorge, Ihre Daten bleiben erhalten.
</div>
</div>
<div class="faq-item">
<div class="faq-question">
Welche Zahlungsmethoden werden akzeptiert?
<span>+</span>
</div>
<div class="faq-answer">
Wir akzeptieren alle gängigen Kreditkarten (Visa, Mastercard, American Express) sowie TWINT und Banküberweisung bei Jahresabos.
</div>
</div>
<div class="faq-item">
<div class="faq-question">
Brauche ich technisches Wissen?
<span>+</span>
</div>
<div class="faq-answer">
Nein! Unser Onboarding-Wizard führt Sie Schritt für Schritt durch die Einrichtung. Sie benötigen lediglich eine Stream-URL (HLS/m3u8) von Ihrem Kamera-Anbieter.
</div>
</div>
</section>
<footer class="footer">
© <?php echo date('Y'); ?> Aurora Livecam. Alle Rechte vorbehalten.
</footer>
<script>
// Billing toggle
const toggle = document.getElementById('billing-toggle');
const container = document.getElementById('pricing-container');
toggle.addEventListener('click', () => {
toggle.classList.toggle('yearly');
container.classList.toggle('yearly-mode');
document.querySelector('.monthly-label').classList.toggle('active');
document.querySelector('.yearly-label').classList.toggle('active');
});
// FAQ accordion
document.querySelectorAll('.faq-question').forEach(q => {
q.addEventListener('click', () => {
q.parentElement.classList.toggle('open');
q.querySelector('span').textContent = q.parentElement.classList.contains('open') ? '' : '+';
});
});
</script>
</body>
</html>