# Lead-Capture & Content-Gate Engine — Implementierungsplan (Phase 1)

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Baue den Motor zum E-Mail-Sammeln: OTP-Verifizierung, Cookie-basiertes „Einmal eintragen reicht", wiederverwendbare Blade-Components, Magnet-Auslieferung, Admin-Übersicht — alles DSGVO-fest.

**Architecture:** `LeadService` kapselt alle Geschäftslogik (subscribe/verify/resend/deliverMagnet/unsubscribe). `LeadGate` ist ein stateless Helper (Cookie-Check via HMAC, kein DB-Hit). `<x-lead-capture>` + `<x-content-gate>` sind wiederverwendbare Blade-Components. Admin unter `/admin/leads` hinter bestehender `admin.auth`-Middleware.

**Tech Stack:** PHP 8.2, Laravel 12, SQLite (Dev/Test), Blade Components, HMAC-SHA256 für Cookie, `URL::temporarySignedRoute()` für Magnet-Downloads, `Mail::fake()` in Tests.

---

## Datei-Übersicht

| Datei | Neu/Mod | Zweck |
|---|---|---|
| `database/migrations/2026_07_01_000003_create_lead_tables.php` | Neu | leads/lead_otps/lead_events |
| `app/Models/Lead.php` | Neu | Eloquent-Modell, findByEmail, normalizeEmail |
| `app/Models/LeadOtp.php` | Neu | OTP-Logik: generate/activeFor/checkCode/consume |
| `app/Models/LeadEvent.php` | Neu | Audit-Log: static log() |
| `config/leads.php` | Neu | Cookie, OTP-TTL, Consent-Text |
| `config/lead_magnets.php` | Neu | Magnet-Slugs mit Titel/Datei/Mail-Text |
| `config/lead_pages.php` | Neu | SEO-Seiten /gratis/{slug} |
| `app/Mail/LeadOtpMail.php` | Neu | OTP-Code per Mail |
| `app/Mail/LeadMagnetMail.php` | Neu | Magnet-Auslieferung mit Download-Link |
| `resources/views/mail/leads/otp.blade.php` | Neu | OTP-Mail-Template |
| `resources/views/mail/leads/magnet.blade.php` | Neu | Magnet-Mail-Template |
| `app/Support/Leads/LeadService.php` | Neu | subscribe/verify/resend/deliverMagnet/unsubscribe |
| `app/Support/Leads/LeadGate.php` | Neu | isUnlocked(Request)/cookie()/cookieValue() |
| `app/Http/Controllers/LeadController.php` | Neu | POST subscribe/verify/resend, GET unsubscribe/download |
| `app/Http/Controllers/LeadPageController.php` | Neu | GET /gratis/{slug} |
| `app/Http/Controllers/Admin/AdminLeadController.php` | Neu | Admin index/export/destroy/resend |
| `resources/views/components/lead-capture.blade.php` | Neu | E-Mail-Formular + OTP-Schritt |
| `resources/views/components/content-gate.blade.php` | Neu | Teaser/Gated mit Paywall-Schema |
| `resources/views/components/partials/lead-form.blade.php` | Neu | Formular-Partial (shared) |
| `resources/views/gratis/seo-checkliste-2026.blade.php` | Neu | Beispiel-SEO-Seite 1 |
| `resources/views/gratis/ki-prompts-selbststaendige.blade.php` | Neu | Beispiel-SEO-Seite 2 |
| `resources/views/leads/unsubscribed.blade.php` | Neu | Abmelde-Bestätigung |
| `resources/views/admin/leads/index.blade.php` | Neu | Admin-View |
| `resources/views/admin/panel/layout.blade.php` | Mod | Sidebar „Marketing → Leads" |
| `routes/web.php` | Mod | Lead-Routen + Admin-Routen + Sitemap |
| `config/datenschutz.php` | Mod | Abschnitt für E-Mail-Liste |
| `tests/Feature/Leads/LeadSubscribeTest.php` | Neu | 7 Tests: subscribe/verify/dedupe/honeypot/abmelden |
| `tests/Feature/Leads/LeadGateTest.php` | Neu | 4 Tests: gratis-Seite, Gate offen/geschlossen |
| `tests/Feature/Leads/AdminLeadTest.php` | Neu | 5 Tests: Auth/Liste/CSV/Löschen/Resend |

---

### Task 1: Migration

**Files:**
- Create: `database/migrations/2026_07_01_000003_create_lead_tables.php`

- [ ] **Step 1.1: Migration schreiben**

```php
<?php
// database/migrations/2026_07_01_000003_create_lead_tables.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('leads', function (Blueprint $table) {
            $table->id();
            $table->string('email')->unique();
            $table->string('status')->default('pending'); // pending|confirmed|unsubscribed
            $table->timestamp('confirmed_at')->nullable();
            $table->timestamp('unsubscribed_at')->nullable();
            $table->string('unsubscribe_token', 64)->unique();
            $table->string('source')->nullable();
            $table->string('incentive')->nullable();
            $table->string('locale', 8)->default('de');
            $table->string('consent_version', 20);
            $table->text('consent_text');
            $table->string('ip_hash', 64)->nullable();
            $table->string('last_magnet_slug')->nullable();
            $table->timestamps();
        });

        Schema::create('lead_otps', function (Blueprint $table) {
            $table->id();
            $table->string('email')->index();
            $table->string('code_hash', 64);
            $table->timestamp('expires_at');
            $table->unsignedTinyInteger('attempts')->default(0);
            $table->timestamp('consumed_at')->nullable();
            $table->timestamp('created_at')->nullable();
        });

        Schema::create('lead_events', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('lead_id')->nullable()->index();
            $table->string('type', 40);
            $table->json('meta')->nullable();
            $table->timestamp('created_at')->nullable();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('lead_events');
        Schema::dropIfExists('lead_otps');
        Schema::dropIfExists('leads');
    }
};
```

- [ ] **Step 1.2: Migration ausführen**

```bash
php artisan config:clear && php artisan migrate
```

Expected: `Migrating: 2026_07_01_000003_create_lead_tables` → done.

- [ ] **Step 1.3: Commit**

```bash
git add database/migrations/2026_07_01_000003_create_lead_tables.php
git commit -m "feat(leads): Migration leads/lead_otps/lead_events"
```

---

### Task 2: Modelle

**Files:**
- Create: `app/Models/Lead.php`
- Create: `app/Models/LeadOtp.php`
- Create: `app/Models/LeadEvent.php`

- [ ] **Step 2.1: Lead Model**

```php
<?php
// app/Models/Lead.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Lead extends Model
{
    protected $fillable = [
        'email', 'status', 'confirmed_at', 'unsubscribed_at',
        'unsubscribe_token', 'source', 'incentive', 'locale',
        'consent_version', 'consent_text', 'ip_hash', 'last_magnet_slug',
    ];

    protected $casts = [
        'confirmed_at'    => 'datetime',
        'unsubscribed_at' => 'datetime',
    ];

    public static function normalizeEmail(string $email): string
    {
        return mb_strtolower(trim($email));
    }

    public static function generateUnsubscribeToken(): string
    {
        return bin2hex(random_bytes(32));
    }

    public static function findByEmail(string $email): ?self
    {
        return self::where('email', self::normalizeEmail($email))->first();
    }

    public function isConfirmed(): bool
    {
        return $this->status === 'confirmed';
    }

    public function isUnsubscribed(): bool
    {
        return $this->status === 'unsubscribed';
    }

    public function events(): HasMany
    {
        return $this->hasMany(LeadEvent::class);
    }
}
```

- [ ] **Step 2.2: LeadOtp Model**

```php
<?php
// app/Models/LeadOtp.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class LeadOtp extends Model
{
    public $timestamps = false;

    protected $fillable = ['email', 'code_hash', 'expires_at', 'attempts', 'consumed_at', 'created_at'];

    protected $casts = [
        'expires_at'  => 'datetime',
        'consumed_at' => 'datetime',
        'created_at'  => 'datetime',
    ];

    public function isValid(): bool
    {
        return $this->consumed_at === null
            && $this->expires_at->isFuture()
            && $this->attempts < 5;
    }

    public function checkCode(string $code): bool
    {
        return hash_equals($this->code_hash, hash('sha256', $code));
    }

    public function consume(): void
    {
        $this->update(['consumed_at' => now()]);
    }

    public static function invalidateFor(string $email): void
    {
        self::where('email', mb_strtolower(trim($email)))
            ->whereNull('consumed_at')
            ->update(['consumed_at' => now()]);
    }

    public static function generate(string $email): array
    {
        $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
        self::invalidateFor($email);
        self::create([
            'email'      => mb_strtolower(trim($email)),
            'code_hash'  => hash('sha256', $code),
            'expires_at' => now()->addMinutes(15),
            'attempts'   => 0,
            'created_at' => now(),
        ]);
        return ['code' => $code];
    }

    public static function activeFor(string $email): ?self
    {
        return self::where('email', mb_strtolower(trim($email)))
            ->whereNull('consumed_at')
            ->where('expires_at', '>', now())
            ->where('attempts', '<', 5)
            ->latest('created_at')
            ->first();
    }
}
```

- [ ] **Step 2.3: LeadEvent Model**

```php
<?php
// app/Models/LeadEvent.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class LeadEvent extends Model
{
    public $timestamps = false;

    protected $fillable = ['lead_id', 'type', 'meta', 'created_at'];

    protected $casts = ['meta' => 'array', 'created_at' => 'datetime'];

    public static function log(int|null $leadId, string $type, array $meta = []): void
    {
        self::create([
            'lead_id'    => $leadId,
            'type'       => $type,
            'meta'       => $meta ?: null,
            'created_at' => now(),
        ]);
    }
}
```

- [ ] **Step 2.4: Commit**

```bash
git add app/Models/Lead.php app/Models/LeadOtp.php app/Models/LeadEvent.php
git commit -m "feat(leads): Models Lead/LeadOtp/LeadEvent"
```

---

### Task 3: Config-Dateien

**Files:**
- Create: `config/leads.php`
- Create: `config/lead_magnets.php`
- Create: `config/lead_pages.php`

- [ ] **Step 3.1: config/leads.php**

```php
<?php
// config/leads.php
return [
    'cookie_name'      => 'kt_sub',
    'cookie_days'      => 365,
    'cookie_domain'    => env('APP_ENV') === 'production' ? '.kotsch.tech' : null,
    'otp_ttl_minutes'  => 15,
    'otp_max_attempts' => 5,
    'consent_version'  => '2026-07',
    'consent_text'     => 'Ich stimme zu, dass kotsch.tech meine E-Mail-Adresse zur Zusendung von Informationen, Angeboten und Neuigkeiten gemäß der Datenschutzerklärung nutzt. Ich kann meine Einwilligung jederzeit per Abmelde-Link in jeder Mail widerrufen.',
    'consent_text_en'  => 'I agree that kotsch.tech may use my email address to send me information, offers and news in accordance with the privacy policy. I can revoke my consent at any time via the unsubscribe link in any email.',
];
```

- [ ] **Step 3.2: config/lead_magnets.php**

```php
<?php
// config/lead_magnets.php
// file = Dateiname relativ zu storage/app/private/lead_magnets/ (oder null = nur Text-Mail).
return [

    'seo-checkliste-2026' => [
        'title'            => 'SEO-Checkliste 2026',
        'description'      => '20 konkrete Maßnahmen, die jede Website braucht.',
        'file'             => null,
        'delivery_subject' => 'Deine SEO-Checkliste 2026 von kotsch.tech',
        'delivery_body'    => 'Vielen Dank für dein Interesse! Hier sind deine 20 SEO-Maßnahmen für 2026. Viel Erfolg dabei!',
    ],

    'ki-prompt-starter' => [
        'title'            => 'KI-Prompt-Starter-Kit',
        'description'      => '15 sofort einsetzbare ChatGPT-Prompts für Selbstständige.',
        'file'             => null,
        'delivery_subject' => 'Dein KI-Prompt-Starter-Kit von kotsch.tech',
        'delivery_body'    => 'Hier sind deine 15 ChatGPT-Prompts für Freelancer und Gründer. Viel Spaß beim Ausprobieren!',
    ],

];
```

- [ ] **Step 3.3: config/lead_pages.php**

```php
<?php
// config/lead_pages.php
// SEO-Sammel-Seiten unter /gratis/{slug}.
// Neue Seite = Eintrag hier + View in resources/views/gratis/{slug}.blade.php + config:cache.
return [

    'seo-checkliste-2026' => [
        'title'       => 'Kostenlose SEO-Checkliste 2026',
        'description' => '20 Maßnahmen, die deine Website nach vorne bringen — als PDF zum Mitnehmen.',
        'magnet'      => 'seo-checkliste-2026',
        'source'      => 'landing:seo-checkliste-2026',
        'locale'      => 'de',
        'view'        => 'gratis.seo-checkliste-2026',
    ],

    'ki-prompts-fuer-selbststaendige' => [
        'title'       => 'KI-Prompts für Selbstständige — kostenlos',
        'description' => '15 sofort einsetzbare ChatGPT-Prompts für Freelancer, Gründer und kleine Unternehmen.',
        'magnet'      => 'ki-prompt-starter',
        'source'      => 'landing:ki-prompts-selbststaendige',
        'locale'      => 'de',
        'view'        => 'gratis.ki-prompts-selbststaendige',
    ],

];
```

- [ ] **Step 3.4: Commit**

```bash
git add config/leads.php config/lead_magnets.php config/lead_pages.php
git commit -m "feat(leads): Config leads/lead_magnets/lead_pages"
```

---

### Task 4: Mail-Klassen

**Files:**
- Create: `app/Mail/LeadOtpMail.php`
- Create: `app/Mail/LeadMagnetMail.php`
- Create: `resources/views/mail/leads/otp.blade.php`
- Create: `resources/views/mail/leads/magnet.blade.php`

- [ ] **Step 4.1: LeadOtpMail**

```php
<?php
// app/Mail/LeadOtpMail.php
namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class LeadOtpMail extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(
        public string $code,
        public string $email,
    ) {}

    public function envelope(): Envelope
    {
        return new Envelope(subject: 'Dein Code: ' . $this->code . ' — kotsch.tech');
    }

    public function content(): Content
    {
        return new Content(view: 'mail.leads.otp');
    }
}
```

- [ ] **Step 4.2: OTP-Mail-View**

Datei: `resources/views/mail/leads/otp.blade.php`

```html
<!DOCTYPE html>
<html lang="de">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
<body style="margin:0;padding:0;background:#f4f6fc;font-family:'Plus Jakarta Sans',system-ui,sans-serif;color:#0f172a;">
<table width="100%" cellpadding="0" cellspacing="0" style="padding:32px 16px;">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:20px;box-shadow:0 2px 32px rgba(37,99,235,.10);overflow:hidden;max-width:100%;">
    <tr><td style="background:linear-gradient(135deg,#2563eb,#7c3aed);padding:28px 36px;">
        <span style="color:#fff;font-size:1.25rem;font-weight:800;letter-spacing:-.02em;">kotsch.tech</span>
    </td></tr>
    <tr><td style="padding:36px;">
        <h1 style="margin:0 0 8px;font-size:1.6rem;font-weight:800;letter-spacing:-.02em;">Dein Bestätigungs-Code</h1>
        <p style="margin:0 0 28px;color:#64748b;font-size:1rem;line-height:1.6;">Bitte gib diesen Code auf der Website ein, um deine E-Mail-Adresse zu bestätigen.</p>
        <div style="background:#f4f6fc;border-radius:16px;padding:24px;text-align:center;margin-bottom:28px;">
            <span style="font-size:2.8rem;font-weight:800;letter-spacing:.25em;color:#2563eb;font-family:monospace;">{{ $code }}</span>
        </div>
        <p style="margin:0;color:#94a3b8;font-size:.86rem;line-height:1.6;">Dieser Code ist 15 Minuten gültig. Wenn du keine Anmeldung beantragt hast, ignoriere diese E-Mail.</p>
    </td></tr>
    <tr><td style="padding:20px 36px;border-top:1px solid #eceff5;">
        <p style="margin:0;color:#94a3b8;font-size:.78rem;">kotsch.tech · <a href="https://www.kotsch.tech/datenschutz" style="color:#64748b;">Datenschutz</a></p>
    </td></tr>
</table>
</td></tr>
</table>
</body>
</html>
```

- [ ] **Step 4.3: LeadMagnetMail**

```php
<?php
// app/Mail/LeadMagnetMail.php
namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class LeadMagnetMail extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(
        public string $magnetTitle,
        public string $downloadUrl,
        public string $unsubscribeUrl,
        public string $body,
    ) {}

    public function envelope(): Envelope
    {
        return new Envelope(subject: $this->magnetTitle . ' — kotsch.tech');
    }

    public function content(): Content
    {
        return new Content(view: 'mail.leads.magnet');
    }
}
```

- [ ] **Step 4.4: Magnet-Mail-View**

Datei: `resources/views/mail/leads/magnet.blade.php`

```html
<!DOCTYPE html>
<html lang="de">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
<body style="margin:0;padding:0;background:#f4f6fc;font-family:'Plus Jakarta Sans',system-ui,sans-serif;color:#0f172a;">
<table width="100%" cellpadding="0" cellspacing="0" style="padding:32px 16px;">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:20px;box-shadow:0 2px 32px rgba(37,99,235,.10);overflow:hidden;max-width:100%;">
    <tr><td style="background:linear-gradient(135deg,#2563eb,#7c3aed);padding:28px 36px;">
        <span style="color:#fff;font-size:1.25rem;font-weight:800;letter-spacing:-.02em;">kotsch.tech</span>
    </td></tr>
    <tr><td style="padding:36px;">
        <h1 style="margin:0 0 12px;font-size:1.5rem;font-weight:800;letter-spacing:-.02em;">{{ $magnetTitle }}</h1>
        <p style="margin:0 0 24px;color:#334155;font-size:1rem;line-height:1.7;">{{ $body }}</p>
        @if($downloadUrl)
        <a href="{{ $downloadUrl }}" style="display:inline-block;background:linear-gradient(135deg,#2563eb,#7c3aed);color:#fff;text-decoration:none;padding:14px 28px;border-radius:14px;font-weight:700;font-size:1rem;margin-bottom:24px;">
            ⬇ Jetzt herunterladen
        </a>
        <p style="margin:0 0 24px;color:#94a3b8;font-size:.82rem;">Der Download-Link ist 24 Stunden gültig.</p>
        @endif
    </td></tr>
    <tr><td style="padding:20px 36px;border-top:1px solid #eceff5;">
        <p style="margin:0;color:#94a3b8;font-size:.78rem;line-height:1.6;">
            kotsch.tech · <a href="https://www.kotsch.tech/datenschutz" style="color:#64748b;">Datenschutz</a>
            · <a href="{{ $unsubscribeUrl }}" style="color:#64748b;">Abmelden</a>
        </p>
    </td></tr>
</table>
</td></tr>
</table>
</body>
</html>
```

- [ ] **Step 4.5: Commit**

```bash
git add app/Mail/LeadOtpMail.php app/Mail/LeadMagnetMail.php resources/views/mail/leads/
git commit -m "feat(leads): Mail-Klassen OTP + Magnet mit Templates"
```

---

### Task 5: LeadService + LeadGate

**Files:**
- Create: `app/Support/Leads/LeadService.php`
- Create: `app/Support/Leads/LeadGate.php`

- [ ] **Step 5.1: LeadService**

```php
<?php
// app/Support/Leads/LeadService.php
namespace App\Support\Leads;

use App\Mail\LeadMagnetMail;
use App\Mail\LeadOtpMail;
use App\Models\Lead;
use App\Models\LeadEvent;
use App\Models\LeadOtp;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\URL;

class LeadService
{
    /**
     * Schritt 1: E-Mail eintragen.
     * @return array{step: 'otp'|'already_confirmed', lead: Lead}
     */
    public function subscribe(string $email, array $data = []): array
    {
        $email  = Lead::normalizeEmail($email);
        $locale = $data['locale'] ?? 'de';

        $lead = Lead::findByEmail($email);

        if (!$lead) {
            $lead = Lead::create([
                'email'             => $email,
                'status'            => 'pending',
                'unsubscribe_token' => Lead::generateUnsubscribeToken(),
                'source'            => $data['source'] ?? null,
                'incentive'         => $data['incentive'] ?? null,
                'locale'            => $locale,
                'consent_version'   => config('leads.consent_version'),
                'consent_text'      => config($locale === 'en' ? 'leads.consent_text_en' : 'leads.consent_text'),
                'ip_hash'           => !empty($data['ip']) ? hash('sha256', $data['ip']) : null,
            ]);
            LeadEvent::log($lead->id, 'signup', ['source' => $data['source'] ?? null]);
        }

        if ($lead->isConfirmed()) {
            return ['step' => 'already_confirmed', 'lead' => $lead];
        }

        $otp = LeadOtp::generate($email);
        Mail::to($email)->send(new LeadOtpMail($otp['code'], $email));
        LeadEvent::log($lead->id, 'otp_sent', []);

        return ['step' => 'otp', 'lead' => $lead];
    }

    /**
     * Schritt 2: OTP verifizieren.
     * @return array{ok: bool, error: string|null, lead: Lead|null}
     */
    public function verify(string $email, string $code): array
    {
        $email = Lead::normalizeEmail($email);
        $otp   = LeadOtp::activeFor($email);

        if (!$otp) {
            return ['ok' => false, 'error' => 'Code abgelaufen oder ungültig. Bitte neu anfordern.', 'lead' => null];
        }

        if (!$otp->checkCode($code)) {
            $otp->increment('attempts');
            $remaining = 5 - $otp->fresh()->attempts;
            if ($remaining <= 0) {
                $otp->update(['consumed_at' => now()]);
                return ['ok' => false, 'error' => 'Zu viele Fehlversuche. Bitte Code neu anfordern.', 'lead' => null];
            }
            return ['ok' => false, 'error' => "Falscher Code. Noch {$remaining} Versuch(e).", 'lead' => null];
        }

        $otp->consume();

        $lead = Lead::findByEmail($email);
        if (!$lead) {
            return ['ok' => false, 'error' => 'Lead nicht gefunden.', 'lead' => null];
        }

        $lead->update(['status' => 'confirmed', 'confirmed_at' => now()]);
        LeadEvent::log($lead->id, 'confirmed', []);

        return ['ok' => true, 'error' => null, 'lead' => $lead->fresh()];
    }

    /** OTP erneut senden. */
    public function resend(string $email): void
    {
        $email = Lead::normalizeEmail($email);
        $lead  = Lead::findByEmail($email);
        if (!$lead || $lead->isConfirmed()) {
            return;
        }

        $otp = LeadOtp::generate($email);
        Mail::to($email)->send(new LeadOtpMail($otp['code'], $email));
        LeadEvent::log($lead->id, 'resend', []);
    }

    /** Magnet per signiertem Link ausliefern. */
    public function deliverMagnet(Lead $lead, string $magnetSlug): void
    {
        $magnet = config("lead_magnets.{$magnetSlug}");
        if (!$magnet) {
            return;
        }

        $downloadUrl = '';
        if (!empty($magnet['file'])) {
            $downloadUrl = URL::temporarySignedRoute(
                'leads.download',
                now()->addHours(24),
                ['lead' => $lead->id, 'magnet' => $magnetSlug]
            );
        }

        $unsubscribeUrl = route('leads.unsubscribe', ['token' => $lead->unsubscribe_token]);

        Mail::to($lead->email)->send(new LeadMagnetMail(
            $magnet['title'],
            $downloadUrl,
            $unsubscribeUrl,
            $magnet['delivery_body'],
        ));

        $lead->update(['last_magnet_slug' => $magnetSlug]);
        LeadEvent::log($lead->id, 'magnet_sent', ['magnet' => $magnetSlug]);
    }

    /** Abmeldung per Token. */
    public function unsubscribe(string $token): ?Lead
    {
        $lead = Lead::where('unsubscribe_token', $token)->first();
        if (!$lead) {
            return null;
        }

        $lead->update(['status' => 'unsubscribed', 'unsubscribed_at' => now()]);
        LeadEvent::log($lead->id, 'unsubscribed', []);

        return $lead;
    }
}
```

- [ ] **Step 5.2: LeadGate**

```php
<?php
// app/Support/Leads/LeadGate.php
namespace App\Support\Leads;

use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Cookie;

class LeadGate
{
    /** Cookie-Wert: "{leadId}.{hmac}" — stateless, kein DB-Hit nötig. */
    public static function cookieValue(int $leadId): string
    {
        $mac = hash_hmac('sha256', (string) $leadId, config('app.key'));
        return $leadId . '.' . $mac;
    }

    /** Prüft ob ein gültiges kt_sub-Cookie gesetzt ist. */
    public static function isUnlocked(Request $request): bool
    {
        $value = $request->cookie(config('leads.cookie_name', 'kt_sub'));
        if (!$value) {
            return false;
        }

        $parts = explode('.', (string) $value, 2);
        if (count($parts) !== 2) {
            return false;
        }

        [$id, $mac] = $parts;
        if (!ctype_digit($id) || !$mac) {
            return false;
        }

        return hash_equals(
            hash_hmac('sha256', $id, config('app.key')),
            $mac
        );
    }

    /** Cookie-Objekt zum Setzen (HttpOnly, SameSite=Lax). */
    public static function cookie(int $leadId): Cookie
    {
        return cookie(
            config('leads.cookie_name', 'kt_sub'),
            self::cookieValue($leadId),
            config('leads.cookie_days', 365) * 24 * 60,
            '/',
            config('leads.cookie_domain'),
            config('app.env') === 'production',
            true,
            false,
            'Lax'
        );
    }
}
```

- [ ] **Step 5.3: Commit**

```bash
git add app/Support/Leads/LeadService.php app/Support/Leads/LeadGate.php
git commit -m "feat(leads): LeadService + LeadGate"
```

---

### Task 6: LeadController + Routen

**Files:**
- Create: `app/Http/Controllers/LeadController.php`
- Modify: `routes/web.php` (Lead-Routen einfügen)

- [ ] **Step 6.1: LeadController**

```php
<?php
// app/Http/Controllers/LeadController.php
namespace App\Http\Controllers;

use App\Models\Lead;
use App\Support\Leads\LeadGate;
use App\Support\Leads\LeadService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\View\View;

class LeadController extends Controller
{
    public function __construct(private LeadService $svc) {}

    /** POST /lead/subscribe */
    public function subscribe(Request $request): JsonResponse|RedirectResponse
    {
        if ($request->filled('website')) {
            return $this->ok($request, ['step' => 'otp']);
        }

        $key = 'lead_sub:' . $request->ip();
        if (RateLimiter::tooManyAttempts($key, 5)) {
            return $this->err($request, 'Zu viele Versuche. Bitte warte kurz.');
        }
        RateLimiter::hit($key, 60);

        $data = $request->validate([
            'email'     => ['required', 'email:rfc', 'max:254'],
            'consent'   => ['accepted'],
            'incentive' => ['nullable', 'string', 'max:80'],
            'source'    => ['nullable', 'string', 'max:120'],
        ]);

        $result = $this->svc->subscribe($data['email'], [
            'incentive' => $data['incentive'] ?? null,
            'source'    => $data['source'] ?? null,
            'ip'        => $request->ip(),
            'locale'    => app()->getLocale() === 'en' ? 'en' : 'de',
        ]);

        if ($result['step'] === 'already_confirmed') {
            $lead = $result['lead'];
            if (!empty($data['incentive'])) {
                $this->svc->deliverMagnet($lead, $data['incentive']);
            }
            return $this->ok($request, ['step' => 'already_confirmed'])
                ->cookie(LeadGate::cookie($lead->id));
        }

        return $this->ok($request, ['step' => 'otp', 'email' => $data['email']]);
    }

    /** POST /lead/verify */
    public function verify(Request $request): JsonResponse|RedirectResponse
    {
        $key = 'lead_verify:' . $request->ip();
        if (RateLimiter::tooManyAttempts($key, 10)) {
            return $this->err($request, 'Zu viele Versuche.');
        }
        RateLimiter::hit($key, 60);

        $data = $request->validate([
            'email' => ['required', 'email:rfc'],
            'code'  => ['required', 'string', 'size:6'],
        ]);

        $result = $this->svc->verify($data['email'], $data['code']);

        if (!$result['ok']) {
            return $this->err($request, $result['error']);
        }

        $lead = $result['lead'];
        if ($lead->incentive) {
            $this->svc->deliverMagnet($lead, $lead->incentive);
        }

        return $this->ok($request, ['step' => 'confirmed'])
            ->cookie(LeadGate::cookie($lead->id));
    }

    /** POST /lead/resend */
    public function resend(Request $request): JsonResponse|RedirectResponse
    {
        $key = 'lead_resend:' . $request->ip();
        if (RateLimiter::tooManyAttempts($key, 3)) {
            return $this->err($request, 'Zu viele Versuche. Bitte warte etwas.');
        }
        RateLimiter::hit($key, 120);

        $data = $request->validate(['email' => ['required', 'email:rfc']]);
        $this->svc->resend($data['email']);

        return $this->ok($request, ['step' => 'resent']);
    }

    /** GET /lead/abmelden/{token} */
    public function unsubscribe(string $token): View
    {
        $lead = $this->svc->unsubscribe($token);
        return view('leads.unsubscribed', ['found' => $lead !== null]);
    }

    /** GET /lead/download/{lead}/{magnet} (signed) */
    public function download(Request $request, Lead $lead, string $magnet)
    {
        abort_unless($request->hasValidSignature(), 403);

        $config = config("lead_magnets.{$magnet}");
        abort_unless($config && !empty($config['file']), 404);

        $basePath = storage_path('app/private/lead_magnets');
        $path     = $basePath . DIRECTORY_SEPARATOR . basename($config['file']);
        abort_unless(is_readable($path), 404);

        return response()->download($path, basename($config['file']));
    }

    // ── Helpers ──────────────────────────────────────────────────────────────

    private function ok(Request $request, array $data): JsonResponse|RedirectResponse
    {
        if ($request->expectsJson()) {
            return response()->json(['ok' => true] + $data);
        }
        return back()->with('lead_result', $data);
    }

    private function err(Request $request, string $message): JsonResponse|RedirectResponse
    {
        if ($request->expectsJson()) {
            return response()->json(['ok' => false, 'error' => $message], 422);
        }
        return back()->withErrors(['email' => $message]);
    }
}
```

- [ ] **Step 6.2: Lead-Routen in routes/web.php einfügen**

Direkt nach `Route::view('/', 'welcome')->name('home');` einfügen (vor der Sitemap-Route):

```php
// ── Lead-Capture & E-Mail-Sammlung ──────────────────────────────────────────
use App\Http\Controllers\LeadController;
use App\Http\Controllers\LeadPageController;

Route::post('/lead/subscribe',               [LeadController::class, 'subscribe'])->name('leads.subscribe');
Route::post('/lead/verify',                  [LeadController::class, 'verify'])->name('leads.verify');
Route::post('/lead/resend',                  [LeadController::class, 'resend'])->name('leads.resend');
Route::get('/lead/abmelden/{token}',         [LeadController::class, 'unsubscribe'])->name('leads.unsubscribe');
Route::get('/lead/download/{lead}/{magnet}', [LeadController::class, 'download'])->name('leads.download')->middleware('signed');
Route::get('/gratis/{slug}',                 [LeadPageController::class, '__invoke'])->name('leads.page');
```

- [ ] **Step 6.3: Commit**

```bash
git add app/Http/Controllers/LeadController.php routes/web.php
git commit -m "feat(leads): LeadController + öffentliche Routen"
```

---

### Task 7: LeadPageController + Views

**Files:**
- Create: `app/Http/Controllers/LeadPageController.php`
- Create: `resources/views/gratis/seo-checkliste-2026.blade.php`
- Create: `resources/views/gratis/ki-prompts-selbststaendige.blade.php`
- Create: `resources/views/leads/unsubscribed.blade.php`

- [ ] **Step 7.1: LeadPageController**

```php
<?php
// app/Http/Controllers/LeadPageController.php
namespace App\Http\Controllers;

use App\Support\Leads\LeadGate;
use Illuminate\Http\Request;
use Illuminate\View\View;

class LeadPageController extends Controller
{
    public function __invoke(Request $request, string $slug): View
    {
        $page = config("lead_pages.{$slug}");
        abort_if(!$page, 404);

        return view($page['view'], [
            'page'       => $page,
            'slug'       => $slug,
            'isUnlocked' => LeadGate::isUnlocked($request),
        ]);
    }
}
```

- [ ] **Step 7.2: Abmelde-View**

Datei: `resources/views/leads/unsubscribed.blade.php`

```blade
@extends('layouts.app')
@section('title', 'Abgemeldet — kotsch.tech')
@section('content')
<div style="max-width:560px;margin:80px auto;text-align:center;padding:0 20px;">
    @if($found)
        <div style="font-size:3rem;margin-bottom:16px;">✅</div>
        <h1 style="font-size:1.8rem;font-weight:800;margin-bottom:12px;">Erfolgreich abgemeldet</h1>
        <p style="color:#64748b;line-height:1.7;">Du erhältst keine weiteren E-Mails von uns. Deine Daten werden auf Anfrage vollständig gelöscht.</p>
    @else
        <div style="font-size:3rem;margin-bottom:16px;">❌</div>
        <h1 style="font-size:1.8rem;font-weight:800;margin-bottom:12px;">Link nicht gefunden</h1>
        <p style="color:#64748b;line-height:1.7;">Dieser Abmeldelink ist ungültig oder bereits verwendet. Schreib uns: <a href="mailto:arthurkotsch@kotsch.tech">arthurkotsch@kotsch.tech</a></p>
    @endif
    <a href="{{ route('home') }}" style="display:inline-block;margin-top:28px;color:#2563eb;font-weight:700;text-decoration:none;">← Zurück zur Startseite</a>
</div>
@endsection
```

- [ ] **Step 7.3: SEO-Seite 1 (SEO-Checkliste)**

Datei: `resources/views/gratis/seo-checkliste-2026.blade.php`

```blade
@extends('layouts.app')
@section('title', $page['title'] . ' — kotsch.tech')
@section('meta_description', $page['description'])
@push('head')
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "WebPage",
  "name": "Kostenlose SEO-Checkliste 2026",
  "description": "20 Maßnahmen, die deine Website nach vorne bringen",
  "isAccessibleForFree": "False",
  "hasPart": [{"@type": "WebPageElement","isAccessibleForFree": "False","cssSelector": ".gated-content"}]
}
</script>
@endpush
@section('content')
<div style="max-width:760px;margin:0 auto;padding:60px 20px 40px;">
    <h1 style="font-size:2.4rem;font-weight:800;letter-spacing:-.03em;line-height:1.2;margin-bottom:16px;">
        Kostenlose SEO-Checkliste 2026
    </h1>
    <p style="font-size:1.15rem;color:#475569;margin-bottom:36px;line-height:1.7;">
        20 konkrete Maßnahmen, die jede Website braucht — von technischen Grundlagen bis zu Content-Strategie. Als PDF zum Mitnehmen.
    </p>

    <h2 style="font-size:1.3rem;font-weight:700;margin:0 0 12px;">Was steckt drin?</h2>
    <ul style="line-height:2;color:#334155;padding-left:20px;margin-bottom:32px;">
        <li>✅ Core Web Vitals verstehen &amp; messen</li>
        <li>✅ Title-Tags &amp; Meta-Descriptions richtig schreiben</li>
        <li>✅ Interne Verlinkung strategisch aufbauen</li>
        <li>✅ Schema.org Markup für mehr Rich Snippets</li>
        <li>… und 16 weitere Punkte</li>
    </ul>

    <x-content-gate source="{{ $page['source'] }}" incentive="{{ $page['magnet'] }}"
        headline="PDF kostenlos herunterladen"
        subline="Trag deine E-Mail ein — wir schicken dir die Checkliste sofort zu.">
        <x-slot name="teaser"></x-slot>
        <x-slot name="gated">
            <div class="gated-content" style="background:#f0f9ff;border-radius:18px;padding:28px;margin-top:24px;">
                <h3 style="font-size:1.1rem;font-weight:700;margin:0 0 16px;">Die vollständige Checkliste:</h3>
                <ol style="line-height:2;color:#1e3a5f;padding-left:20px;">
                    <li>PageSpeed auf 90+ bringen (Google PageSpeed Insights)</li>
                    <li>HTTPS &amp; korrekte 301-Weiterleitungen</li>
                    <li>Mobile-First: Viewport + Touch-Targets</li>
                    <li>Canonical-Tags auf jeder Seite setzen</li>
                    <li>Sitemap.xml erstellen &amp; in Search Console einreichen</li>
                    <li>robots.txt prüfen — keine wichtigen Seiten gesperrt</li>
                    <li>Title-Tags optimieren (50–60 Zeichen, Keyword vorne)</li>
                    <li>Meta-Descriptions schreiben (145–160 Zeichen, Call-to-Action)</li>
                    <li>Überschriften-Hierarchie: genau 1× H1, H2–H3 strukturiert</li>
                    <li>Alt-Texte für alle inhaltlichen Bilder</li>
                    <li>Interne Links mit beschreibenden Ankertexten</li>
                    <li>Duplicate Content vermeiden (Parameter, Trailing-Slash)</li>
                    <li>Bildkomprimierung: WebP + lazy loading</li>
                    <li>Strukturierte Daten: FAQ, Artikel, BreadcrumbList</li>
                    <li>Google Search Console einrichten &amp; Fehler beheben</li>
                    <li>Backlink-Profil analysieren (Ahrefs / Search Console)</li>
                    <li>FAQ-Seiten für People Also Ask (PAA)</li>
                    <li>Lokale SEO: Google Business Profil &amp; NAP-Konsistenz</li>
                    <li>Content-Aktualität: veraltete Artikel aktualisieren</li>
                    <li>Core Web Vitals monatlich messen &amp; verbessern</li>
                </ol>
            </div>
        </x-slot>
    </x-content-gate>
</div>
@endsection
```

- [ ] **Step 7.4: SEO-Seite 2 (KI-Prompts)**

Datei: `resources/views/gratis/ki-prompts-selbststaendige.blade.php`

```blade
@extends('layouts.app')
@section('title', $page['title'] . ' — kotsch.tech')
@section('meta_description', $page['description'])
@push('head')
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "WebPage",
  "name": "KI-Prompts für Selbstständige",
  "description": "15 sofort einsetzbare ChatGPT-Prompts für Freelancer, Gründer und kleine Unternehmen",
  "isAccessibleForFree": "False",
  "hasPart": [{"@type": "WebPageElement","isAccessibleForFree": "False","cssSelector": ".gated-content"}]
}
</script>
@endpush
@section('content')
<div style="max-width:760px;margin:0 auto;padding:60px 20px 40px;">
    <h1 style="font-size:2.4rem;font-weight:800;letter-spacing:-.03em;line-height:1.2;margin-bottom:16px;">
        15 KI-Prompts für Selbstständige
    </h1>
    <p style="font-size:1.15rem;color:#475569;margin-bottom:36px;line-height:1.7;">
        ChatGPT &amp; Co. richtig nutzen als Freelancer, Gründer oder kleines Unternehmen. Diese 15 Prompts sparen dir sofort Zeit.
    </p>

    <h2 style="font-size:1.3rem;font-weight:700;margin:0 0 12px;">Was steckt drin?</h2>
    <ul style="line-height:2;color:#334155;padding-left:20px;margin-bottom:32px;">
        <li>📝 Angebote &amp; Rechnungstexte in Sekunden</li>
        <li>📧 Kundenanfragen professionell beantworten</li>
        <li>📱 Social-Media-Posts im eigenen Ton</li>
        <li>🔍 Marktrecherche beschleunigen</li>
        <li>… und 11 weitere fertige Prompts</li>
    </ul>

    <x-content-gate source="{{ $page['source'] }}" incentive="{{ $page['magnet'] }}"
        headline="Alle 15 Prompts kostenlos erhalten"
        subline="Trag deine E-Mail ein — du erhältst die Prompts sofort.">
        <x-slot name="teaser"></x-slot>
        <x-slot name="gated">
            <div class="gated-content" style="background:#faf5ff;border-radius:18px;padding:28px;margin-top:24px;">
                <h3 style="font-size:1.1rem;font-weight:700;margin:0 0 16px;">Alle 15 Prompts:</h3>
                <ol style="line-height:2.2;color:#4c1d95;padding-left:20px;font-size:.95rem;">
                    <li>"Schreib mir ein Angebot für [Leistung] an [Kundentyp]. Ton: professionell, aber persönlich."</li>
                    <li>"Erstelle eine höfliche Zahlungserinnerung für Rechnung #[Nummer]. Nicht zu streng."</li>
                    <li>"Formuliere eine Antwort auf diese Kundenanfrage: [Anfrage]. Kurz, klar, einladend."</li>
                    <li>"Schreib 5 LinkedIn-Posts zum Thema [Thema] in meinem Stil: [3 Beispiele]."</li>
                    <li>"Was sind die 5 häufigsten Einwände bei [Dienstleistung] und wie begegne ich ihnen?"</li>
                    <li>"Erstelle einen Projektzeitplan für [Projektbeschreibung] mit realistischen Pufferzeiten."</li>
                    <li>"Analysiere diese Wettbewerber-Website: [URL]. Was machen sie gut, was schlecht?"</li>
                    <li>"Formuliere eine Datenschutzerklärung für meine Website als [Freelancer-Typ]."</li>
                    <li>"Schreib eine kurze Intro-E-Mail, wenn ich mich bei einem neuen Kunden vorstelle."</li>
                    <li>"Erstelle 10 Ideen für Blogbeiträge, die meine Zielgruppe [Beschreibung] ansprechen."</li>
                    <li>"Vereinfache diesen Fachtext für Nicht-Experten: [Text]."</li>
                    <li>"Schreib eine XING/LinkedIn-Zusammenfassung für mein Profil. Ich mache: [Beschreibung]."</li>
                    <li>"Erkläre mir diesen Vertragsparagrafen in einfacher Sprache: [Paragraph]."</li>
                    <li>"Erstelle einen Monatsbericht für meinen Kunden basierend auf diesen Daten: [Daten]."</li>
                    <li>"Was sollte ich bei der Preisfindung für [Leistung] beachten? Branche: [Branche]."</li>
                </ol>
            </div>
        </x-slot>
    </x-content-gate>
</div>
@endsection
```

- [ ] **Step 7.5: Commit**

```bash
git add app/Http/Controllers/LeadPageController.php resources/views/gratis/ resources/views/leads/
git commit -m "feat(leads): LeadPageController + 2 Beispiel-SEO-Seiten + Abmelde-View"
```

---

### Task 8: Blade-Components

**Files:**
- Create: `resources/views/components/partials/lead-form.blade.php`
- Create: `resources/views/components/lead-capture.blade.php`
- Create: `resources/views/components/content-gate.blade.php`

- [ ] **Step 8.1: Lead-Form Partial (shared)**

Datei: `resources/views/components/partials/lead-form.blade.php`

```blade
@props([
    'showOtp'   => false,
    'incentive' => null,
    'source'    => null,
    'headline'  => 'Kostenlos anmelden',
    'subline'   => 'Kein Spam. Jederzeit abmelbar.',
    'variant'   => 'box',
])
<div data-lead-form style="
    @if($variant === 'box') background:#fff;border:1.5px solid #e2e8f0;border-radius:20px;padding:28px 26px;box-shadow:0 2px 24px rgba(37,99,235,.07); @endif
    max-width:520px;
">
@if(!$showOtp)
    <form method="POST" action="{{ route('leads.subscribe') }}">
        @csrf
        <input type="hidden" name="incentive" value="{{ $incentive }}">
        <input type="hidden" name="source"    value="{{ $source }}">
        {{-- Honeypot --}}
        <div aria-hidden="true" style="position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;">
            <input type="text" name="website" tabindex="-1" autocomplete="off">
        </div>

        <h3 style="margin:0 0 6px;font-size:1.25rem;font-weight:800;letter-spacing:-.02em;">{{ $headline }}</h3>
        <p style="margin:0 0 18px;color:#64748b;font-size:.92rem;line-height:1.5;">{{ $subline }}</p>

        @if($errors->has('email'))
            <p style="color:#dc2626;font-size:.84rem;font-weight:600;margin:0 0 12px;">{{ $errors->first('email') }}</p>
        @endif

        <div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:14px;">
            <input type="email" name="email" placeholder="deine@email.de" required autocomplete="email"
                style="flex:1;min-width:180px;padding:13px 16px;border:1.5px solid #e2e8f0;border-radius:12px;font-size:1rem;font-family:inherit;outline:none;transition:border-color .15s;"
                onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#e2e8f0'">
            <button type="submit"
                style="padding:13px 22px;background:linear-gradient(135deg,#2563eb,#7c3aed);color:#fff;border:none;border-radius:12px;font-weight:700;font-size:.95rem;cursor:pointer;white-space:nowrap;font-family:inherit;transition:filter .15s;"
                onmouseenter="this.style.filter='brightness(1.05)'" onmouseleave="this.style.filter=''">
                Kostenlos erhalten
            </button>
        </div>

        <label style="display:flex;align-items:flex-start;gap:10px;font-size:.82rem;color:#64748b;line-height:1.5;cursor:pointer;">
            <input type="checkbox" name="consent" value="1" required style="margin-top:3px;flex-shrink:0;accent-color:#2563eb;">
            <span>Ich stimme zu, dass kotsch.tech meine E-Mail gemäß der <a href="/datenschutz" target="_blank" style="color:#2563eb;">Datenschutzerklärung</a> nutzt. Abmeldung jederzeit möglich.</span>
        </label>
    </form>
@else
    <p style="margin:0 0 16px;font-size:1rem;color:#334155;line-height:1.6;">
        Wir haben dir einen 6-stelligen Code per E-Mail geschickt. Bitte gib ihn hier ein:
    </p>
    <form method="POST" action="{{ route('leads.verify') }}">
        @csrf
        <input type="hidden" name="email" value="{{ old('email', session('lead_result.email', '')) }}">
        @if($errors->has('email'))
            <p style="color:#dc2626;font-size:.84rem;font-weight:600;margin:0 0 12px;">{{ $errors->first('email') }}</p>
        @endif
        <div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px;">
            <input type="text" name="code" maxlength="6" placeholder="123456" required inputmode="numeric" autocomplete="one-time-code"
                style="flex:1;min-width:120px;padding:13px 16px;border:1.5px solid #e2e8f0;border-radius:12px;font-size:1.4rem;letter-spacing:.2em;text-align:center;font-family:monospace;outline:none;transition:border-color .15s;"
                onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#e2e8f0'">
            <button type="submit"
                style="padding:13px 22px;background:linear-gradient(135deg,#2563eb,#7c3aed);color:#fff;border:none;border-radius:12px;font-weight:700;font-size:.95rem;cursor:pointer;font-family:inherit;">
                Bestätigen
            </button>
        </div>
    </form>
    <form method="POST" action="{{ route('leads.resend') }}" style="margin-top:4px;">
        @csrf
        <input type="hidden" name="email" value="{{ old('email', session('lead_result.email', '')) }}">
        <button type="submit" style="background:none;border:none;color:#64748b;font-size:.84rem;cursor:pointer;padding:0;font-family:inherit;text-decoration:underline;">
            Code erneut senden
        </button>
    </form>
@endif
</div>
```

- [ ] **Step 8.2: x-lead-capture Component**

Datei: `resources/views/components/lead-capture.blade.php`

```blade
@props([
    'incentive' => null,
    'source'    => null,
    'headline'  => 'Kostenlos anmelden',
    'subline'   => 'Kein Spam. Jederzeit abmelbar.',
    'variant'   => 'box',
])
@php
    $isUnlocked = \App\Support\Leads\LeadGate::isUnlocked(request());
    $result     = session('lead_result', []);
    $step       = $result['step'] ?? null;
@endphp

@if($isUnlocked || $step === 'confirmed' || $step === 'already_confirmed')
    <div style="display:flex;align-items:center;gap:10px;padding:14px 18px;background:#f0fdf4;border:1.5px solid #bbf7d0;border-radius:14px;color:#15803d;font-weight:700;font-size:.92rem;">
        <span style="font-size:1.2rem;">✓</span>
        @if($step === 'confirmed') E-Mail bestätigt! Schau in dein Postfach.
        @else Du bist bereits dabei!
        @endif
    </div>
@elseif($step === 'otp' || $step === 'resent')
    @include('components.partials.lead-form', ['showOtp' => true, 'incentive' => $incentive, 'source' => $source])
@else
    @include('components.partials.lead-form', ['showOtp' => false, 'incentive' => $incentive, 'source' => $source, 'headline' => $headline, 'subline' => $subline, 'variant' => $variant])
@endif
```

- [ ] **Step 8.3: x-content-gate Component**

Datei: `resources/views/components/content-gate.blade.php`

```blade
@props([
    'source'    => null,
    'incentive' => null,
    'headline'  => 'Inhalt freischalten',
    'subline'   => 'Kostenlos, nur deine E-Mail ist nötig.',
])
@php
    $isUnlocked = \App\Support\Leads\LeadGate::isUnlocked(request());
@endphp

{{ $teaser }}

@if($isUnlocked)
    {{ $gated }}
@else
    <script type="application/ld+json">
    {"@context":"https://schema.org","@type":"WebPageElement","isAccessibleForFree":"False","cssSelector":".gated-content"}
    </script>

    <div style="position:relative;margin-top:24px;">
        <div aria-hidden="true" style="position:absolute;bottom:0;left:0;right:0;height:120px;background:linear-gradient(to bottom,transparent,#fff 75%);pointer-events:none;z-index:1;border-radius:0 0 18px 18px;"></div>
        <div style="max-height:160px;overflow:hidden;filter:blur(1.5px);opacity:.55;pointer-events:none;" aria-hidden="true">
            {{ $gated }}
        </div>
    </div>

    <div style="background:#fff;border:1.5px solid #e2e8f0;border-radius:20px;padding:28px;box-shadow:0 2px 24px rgba(37,99,235,.07);margin-top:-32px;position:relative;z-index:2;">
        <x-lead-capture :incentive="$incentive" :source="$source" :headline="$headline" :subline="$subline" variant="inline" />
    </div>
@endif
```

- [ ] **Step 8.4: Commit**

```bash
git add resources/views/components/lead-capture.blade.php resources/views/components/content-gate.blade.php resources/views/components/partials/lead-form.blade.php
git commit -m "feat(leads): Blade-Components x-lead-capture + x-content-gate"
```

---

### Task 9: Admin — Controller + View + Sidebar + Routen

**Files:**
- Create: `app/Http/Controllers/Admin/AdminLeadController.php`
- Create: `resources/views/admin/leads/index.blade.php`
- Modify: `resources/views/admin/panel/layout.blade.php` (Sidebar)
- Modify: `routes/web.php` (Admin-Routen)

- [ ] **Step 9.1: AdminLeadController**

```php
<?php
// app/Http/Controllers/Admin/AdminLeadController.php
namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Lead;
use App\Models\LeadEvent;
use App\Models\LeadOtp;
use App\Support\Leads\LeadService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;

class AdminLeadController extends Controller
{
    public function __construct(private LeadService $svc) {}

    public function index(Request $request): View
    {
        $q = Lead::query();
        if ($status = $request->query('status')) $q->where('status', $status);
        if ($source = $request->query('source'))  $q->where('source', 'like', "%{$source}%");
        if ($search = $request->query('search'))  $q->where('email', 'like', "%{$search}%");

        $leads = $q->latest()->paginate(50)->withQueryString();

        $stats = [
            'total'        => Lead::count(),
            'confirmed'    => Lead::where('status', 'confirmed')->count(),
            'pending'      => Lead::where('status', 'pending')->count(),
            'unsubscribed' => Lead::where('status', 'unsubscribed')->count(),
        ];
        $stats['rate'] = $stats['total'] > 0
            ? round($stats['confirmed'] / $stats['total'] * 100, 1)
            : 0;

        $topSources = Lead::selectRaw('source, COUNT(*) as n')
            ->whereNotNull('source')
            ->groupBy('source')
            ->orderByDesc('n')
            ->limit(10)
            ->pluck('n', 'source');

        return view('admin.leads.index', compact('leads', 'stats', 'topSources'));
    }

    public function export(Request $request): Response
    {
        $q = Lead::query();
        if ($status = $request->query('status')) $q->where('status', $status);
        if ($source = $request->query('source'))  $q->where('source', 'like', "%{$source}%");

        $rows  = $q->orderBy('id')->get();
        $lines = ["ID,E-Mail,Status,Quelle,Incentive,Locale,Bestätigt,Angemeldet\r\n"];

        foreach ($rows as $r) {
            $lines[] = implode(',', [
                $r->id,
                '"' . str_replace('"', '""', $r->email) . '"',
                $r->status,
                '"' . str_replace('"', '""', (string) $r->source) . '"',
                '"' . str_replace('"', '""', (string) $r->incentive) . '"',
                $r->locale,
                $r->confirmed_at?->toDateTimeString() ?? '',
                $r->created_at->toDateTimeString(),
            ]) . "\r\n";
        }

        return response(implode('', $lines), 200, [
            'Content-Type'        => 'text/csv; charset=UTF-8',
            'Content-Disposition' => 'attachment; filename="leads-export.csv"',
        ]);
    }

    public function destroy(Lead $lead): RedirectResponse
    {
        LeadEvent::where('lead_id', $lead->id)->delete();
        LeadOtp::where('email', $lead->email)->delete();
        $lead->delete();

        return back()->with('success', 'Lead und alle zugehörigen Daten gelöscht.');
    }

    public function resend(Lead $lead): RedirectResponse
    {
        if ($lead->isConfirmed()) {
            return back()->with('error', 'Lead ist bereits bestätigt.');
        }
        $this->svc->resend($lead->email);

        return back()->with('success', 'Code erneut gesendet an ' . $lead->email);
    }
}
```

- [ ] **Step 9.2: Admin-View**

Datei: `resources/views/admin/leads/index.blade.php`

```blade
@extends('admin.panel.layout')
@section('admin_title', 'Leads')
@section('admin_content')
<div class="admin-header">
    <div>
        <span class="shop-muted-label">Marketing</span>
        <h1>Leads &amp; E-Mail-Sammlung</h1>
        <p class="shop-copy">Alle gesammelten E-Mail-Adressen — bestätigt, ausstehend, abgemeldet.</p>
    </div>
    <a href="{{ route('admin.leads.export', request()->query()) }}" class="shop-button-light">
        <i class="fas fa-download"></i> CSV exportieren
    </a>
</div>

@if(session('success'))
    <div class="flash-banner flash-banner-success">{{ session('success') }}</div>
@endif
@if(session('error'))
    <div class="flash-banner flash-banner-error">{{ session('error') }}</div>
@endif

<div class="admin-card-grid">
    <div class="admin-metric-card"><span>Gesamt</span><strong>{{ $stats['total'] }}</strong></div>
    <div class="admin-metric-card"><span>Bestätigt</span><strong style="color:var(--ap-ok)">{{ $stats['confirmed'] }}</strong></div>
    <div class="admin-metric-card"><span>Ausstehend</span><strong style="color:var(--ap-warn)">{{ $stats['pending'] }}</strong></div>
    <div class="admin-metric-card"><span>Abgemeldet</span><strong style="color:var(--ap-err)">{{ $stats['unsubscribed'] }}</strong></div>
    <div class="admin-metric-card"><span>Conversion</span><strong>{{ $stats['rate'] }}%</strong></div>
</div>

<div style="display:grid;grid-template-columns:1fr 280px;gap:20px;align-items:start;">

<div class="admin-panel">
    <form method="GET" style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:20px;">
        <input type="text" name="search" placeholder="E-Mail suchen…" value="{{ request('search') }}"
            class="shop-input" style="flex:1;min-width:160px;padding:9px 13px;">
        <select name="status" class="shop-input" style="width:150px;padding:9px 13px;">
            <option value="">Alle Status</option>
            <option value="pending"      @selected(request('status')==='pending')>Ausstehend</option>
            <option value="confirmed"    @selected(request('status')==='confirmed')>Bestätigt</option>
            <option value="unsubscribed" @selected(request('status')==='unsubscribed')>Abgemeldet</option>
        </select>
        <button type="submit" class="shop-button-light" style="padding:9px 16px;"><i class="fas fa-filter"></i> Filtern</button>
        @if(request()->hasAny(['search','status','source']))
            <a href="{{ route('admin.leads.index') }}" class="shop-button-light" style="padding:9px 14px;">✕</a>
        @endif
    </form>

    <div class="admin-table-wrap">
        <table class="admin-table">
            <thead>
                <tr>
                    <th>E-Mail</th><th>Status</th><th>Quelle</th><th>Incentive</th><th>Datum</th><th>Aktionen</th>
                </tr>
            </thead>
            <tbody>
            @forelse($leads as $lead)
                <tr>
                    <td style="font-size:.9rem;">{{ $lead->email }}</td>
                    <td>
                        @php $sc = match($lead->status){ 'confirmed'=>'ok','pending'=>'warn','unsubscribed'=>'err', default=>'faint' } @endphp
                        <span class="admin-badge" style="background:rgba(0,0,0,.05);color:var(--ap-{{ $sc }})">{{ ucfirst($lead->status) }}</span>
                    </td>
                    <td style="font-size:.82rem;color:var(--ap-muted);max-width:160px;word-break:break-word;">{{ $lead->source ?: '—' }}</td>
                    <td style="font-size:.82rem;color:var(--ap-muted)">{{ $lead->incentive ?: '—' }}</td>
                    <td style="font-size:.8rem;white-space:nowrap">{{ $lead->created_at->format('d.m.Y') }}</td>
                    <td>
                        <div style="display:flex;gap:6px;flex-wrap:wrap;">
                            @if($lead->status === 'pending')
                                <form method="POST" action="{{ route('admin.leads.resend', $lead) }}">
                                    @csrf
                                    <button class="erledigt-btn" style="font-size:.76rem;padding:5px 10px;" title="OTP-Code erneut senden">Code senden</button>
                                </form>
                            @endif
                            <form method="POST" action="{{ route('admin.leads.destroy', $lead) }}"
                                onsubmit="return confirm('Lead {{ addslashes($lead->email) }} wirklich löschen (DSGVO-Löschung)?')">
                                @csrf @method('DELETE')
                                <button class="erledigt-btn" style="background:var(--ap-err);font-size:.76rem;padding:5px 10px;">Löschen</button>
                            </form>
                        </div>
                    </td>
                </tr>
            @empty
                <tr><td colspan="6" style="text-align:center;color:var(--ap-muted);padding:32px;">Keine Leads gefunden.</td></tr>
            @endforelse
            </tbody>
        </table>
    </div>
    <div style="margin-top:16px;">{{ $leads->links() }}</div>
</div>

<div class="admin-panel">
    <h3>Top-Quellen</h3>
    @forelse($topSources as $src => $n)
        <div style="display:flex;justify-content:space-between;align-items:center;padding:9px 0;border-bottom:1px solid var(--ap-line);font-size:.86rem;">
            <span style="color:var(--ap-ink);word-break:break-all;line-height:1.4;">{{ $src }}</span>
            <span style="font-weight:800;color:var(--ap-brand);flex-shrink:0;margin-left:10px;">{{ $n }}</span>
        </div>
    @empty
        <p style="color:var(--ap-muted);font-size:.88rem;">Noch keine Quellen.</p>
    @endforelse
</div>

</div>
@endsection
```

- [ ] **Step 9.3: Sidebar-Eintrag in layout.blade.php**

In `resources/views/admin/panel/layout.blade.php`, direkt vor `<span class="ap-nav-label">Extern</span>`:

```blade
            <span class="ap-nav-label">Marketing</span>
            <a href="{{ route('admin.leads.index') }}" class="{{ Request::routeIs('admin.leads.*') ? 'is-active' : '' }}">
                <i class="fas fa-envelope-open-text"></i> Leads
            </a>
```

- [ ] **Step 9.4: Admin-Routen in routes/web.php**

Direkt vor dem bestehenden `// Admin: Apps`-Block einfügen:

```php
// Admin: Leads
use App\Http\Controllers\Admin\AdminLeadController;
Route::middleware('admin.auth')->prefix('admin/leads')->name('admin.leads.')->group(function () {
    Route::get('/',               [AdminLeadController::class, 'index'])->name('index');
    Route::get('/export',         [AdminLeadController::class, 'export'])->name('export');
    Route::delete('/{lead}',      [AdminLeadController::class, 'destroy'])->name('destroy');
    Route::post('/{lead}/resend', [AdminLeadController::class, 'resend'])->name('resend');
});
```

- [ ] **Step 9.5: Commit**

```bash
git add app/Http/Controllers/Admin/AdminLeadController.php resources/views/admin/leads/ resources/views/admin/panel/layout.blade.php routes/web.php
git commit -m "feat(leads): Admin /admin/leads (Liste/Filter/Stats/CSV/DSGVO-Löschen)"
```

---

### Task 10: Datenschutz-Abschnitt

**Files:**
- Modify: `config/datenschutz.php`

- [ ] **Step 10.1: Leads-Abschnitt in datenschutz.php**

Im `areas`-Array nach dem letzten bestehenden Eintrag einen neuen Eintrag `'leads'` hinzufügen:

```php
'leads' => [
    'label'       => ['E-Mail-Liste & Lead-Erfassung', 'Email list & lead capture'],
    'icon'        => 'fa-envelope-open-text',
    'accent'      => '#2563eb',
    'intro'       => [
        'Wenn du auf einer unserer Seiten deine E-Mail-Adresse eingibst, um kostenlose Inhalte, Checklisten oder Anleitungen zu erhalten, verarbeiten wir deine Daten auf Grundlage deiner ausdrücklichen Einwilligung (Art. 6 Abs. 1 lit. a DSGVO).',
        'When you enter your email address on one of our pages to receive free content, checklists or guides, we process your data based on your explicit consent (Art. 6 (1) lit. a GDPR).',
    ],
    'data' => [
        [
            'label'  => ['E-Mail-Adresse', 'Email address'],
            'detail' => ['Gespeichert normalisiert (lowercase). Zur OTP-Verifizierung und zum Versand von Inhalten/Neuigkeiten.', 'Stored normalised (lowercase). Used for OTP verification and sending content/updates.'],
        ],
        [
            'label'  => ['IP-Adresse (gehasht)', 'IP address (hashed)'],
            'detail' => ['Nur als SHA-256-Hash — kein Rückschluss auf die Klartext-IP möglich. Nachweis für den Einwilligungsvorgang.', 'As SHA-256 hash only — no reverse lookup possible. Proof of the consent process.'],
        ],
        [
            'label'  => ['Einwilligungstext & -version', 'Consent text & version'],
            'detail' => ['Der zum Anmeldezeitpunkt gültige Text wird gespeichert (DSGVO-Nachweis).', 'The text valid at registration time is stored (GDPR proof of consent).'],
        ],
        [
            'label'  => ['Einmalig-Code (OTP)', 'One-time code (OTP)'],
            'detail' => ['Nur der SHA-256-Hash des Codes wird gespeichert. Code ist 15 Minuten gültig, danach automatisch invalidiert.', 'Only the SHA-256 hash of the code is stored. Valid for 15 minutes, then automatically invalidated.'],
        ],
    ],
    'purpose'     => ['Auslieferung kostenloser Inhalte (Lead-Magnets), ggf. weitere Informationen und Angebote von kotsch.tech.', 'Delivery of free content (lead magnets) and optionally further information and offers from kotsch.tech.'],
    'legal_basis' => ['Einwilligung (Art. 6 Abs. 1 lit. a DSGVO). Abmeldung jederzeit über den Link in jeder E-Mail.', 'Consent (Art. 6 (1) lit. a GDPR). Unsubscription possible at any time via the link in any email.'],
    'retention'   => ['Bis zur Abmeldung oder Löschanfrage.', 'Until unsubscription or deletion request.'],
    'third_parties' => [
        ['Zoho Mail', 'E-Mail-Zustellung (OTP + Magnet)', 'Zoho Mail', 'Email delivery (OTP + magnet)'],
    ],
    'cookies' => false,
],
```

- [ ] **Step 10.2: Commit**

```bash
git add config/datenschutz.php
git commit -m "feat(leads): Datenschutz-Abschnitt E-Mail-Liste"
```

---

### Task 11: Sitemap-Integration

**Files:**
- Modify: `routes/web.php` (Sitemap-Closure)

- [ ] **Step 11.1: /gratis/{slug} in Sitemap aufnehmen**

In der Sitemap-Closure in `routes/web.php`, nach dem Wissen-Abschnitt und vor dem abschließenden XML-Response, einfügen:

```php
// ── Lead-Capture Seiten (/gratis/{slug}) ────────────────────────────────────
foreach (array_keys(config('lead_pages', [])) as $lpSlug) {
    if (preg_match('/^[a-z0-9][a-z0-9\-]*$/i', $lpSlug)) {
        $add('/gratis/' . $lpSlug, '0.7', 'monthly');
    }
}
```

- [ ] **Step 11.2: Commit**

```bash
git add routes/web.php
git commit -m "feat(leads): /gratis/{slug} in Sitemap"
```

---

### Task 12: Feature-Tests

**Files:**
- Create: `tests/Feature/Leads/LeadSubscribeTest.php`
- Create: `tests/Feature/Leads/LeadGateTest.php`
- Create: `tests/Feature/Leads/AdminLeadTest.php`

- [ ] **Step 12.1: LeadSubscribeTest**

```php
<?php
// tests/Feature/Leads/LeadSubscribeTest.php
namespace Tests\Feature\Leads;

use App\Mail\LeadMagnetMail;
use App\Mail\LeadOtpMail;
use App\Models\Lead;
use App\Models\LeadEvent;
use App\Models\LeadOtp;
use App\Support\Leads\LeadGate;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;

class LeadSubscribeTest extends TestCase
{
    use RefreshDatabase;

    public function test_subscribe_creates_pending_lead_and_sends_otp(): void
    {
        Mail::fake();

        $this->postJson('/lead/subscribe', [
            'email'   => 'test@example.com',
            'consent' => '1',
            'source'  => 'test:unit',
        ])->assertOk()->assertJsonFragment(['step' => 'otp']);

        $this->assertDatabaseHas('leads', ['email' => 'test@example.com', 'status' => 'pending']);
        $this->assertDatabaseHas('lead_events', ['type' => 'otp_sent']);
        Mail::assertSent(LeadOtpMail::class);
    }

    public function test_subscribe_deduplicates_email(): void
    {
        Mail::fake();

        $this->postJson('/lead/subscribe', ['email' => 'dup@example.com', 'consent' => '1']);
        $this->postJson('/lead/subscribe', ['email' => 'dup@example.com', 'consent' => '1']);

        $this->assertDatabaseCount('leads', 1);
    }

    public function test_verify_confirms_lead_sets_cookie_and_delivers_magnet(): void
    {
        Mail::fake();

        Lead::create([
            'email'             => 'verify@example.com',
            'status'            => 'pending',
            'unsubscribe_token' => bin2hex(random_bytes(32)),
            'incentive'         => 'seo-checkliste-2026',
            'consent_version'   => '2026-07',
            'consent_text'      => 'Test',
        ]);

        ['code' => $code] = LeadOtp::generate('verify@example.com');

        $response = $this->postJson('/lead/verify', [
            'email' => 'verify@example.com',
            'code'  => $code,
        ]);

        $response->assertOk()->assertJsonFragment(['step' => 'confirmed']);
        $this->assertDatabaseHas('leads', ['email' => 'verify@example.com', 'status' => 'confirmed']);

        Mail::assertSent(LeadMagnetMail::class);
        $this->assertDatabaseHas('lead_events', ['type' => 'confirmed']);
        $this->assertDatabaseHas('lead_events', ['type' => 'magnet_sent']);
    }

    public function test_verify_wrong_code_increments_attempts(): void
    {
        Mail::fake();

        Lead::create([
            'email'             => 'wrong@example.com',
            'status'            => 'pending',
            'unsubscribe_token' => bin2hex(random_bytes(32)),
            'consent_version'   => '2026-07',
            'consent_text'      => 'Test',
        ]);
        LeadOtp::generate('wrong@example.com');

        $this->postJson('/lead/verify', ['email' => 'wrong@example.com', 'code' => '000000'])
            ->assertStatus(422);

        $this->assertDatabaseHas('leads', ['email' => 'wrong@example.com', 'status' => 'pending']);
        $this->assertEquals(1, LeadOtp::where('email', 'wrong@example.com')->first()->attempts);
    }

    public function test_already_confirmed_skips_otp_and_sets_cookie(): void
    {
        Mail::fake();

        Lead::create([
            'email'             => 'already@example.com',
            'status'            => 'confirmed',
            'confirmed_at'      => now(),
            'unsubscribe_token' => bin2hex(random_bytes(32)),
            'consent_version'   => '2026-07',
            'consent_text'      => 'Test',
        ]);

        $this->postJson('/lead/subscribe', [
            'email'   => 'already@example.com',
            'consent' => '1',
        ])->assertOk()->assertJsonFragment(['step' => 'already_confirmed']);

        Mail::assertNotSent(LeadOtpMail::class);
    }

    public function test_unsubscribe_via_token(): void
    {
        Lead::create([
            'email'             => 'unsub@example.com',
            'status'            => 'confirmed',
            'unsubscribe_token' => 'test-token-abc',
            'consent_version'   => '2026-07',
            'consent_text'      => 'Test',
        ]);

        $this->get('/lead/abmelden/test-token-abc')
            ->assertOk()
            ->assertSee('abgemeldet');

        $this->assertDatabaseHas('leads', ['email' => 'unsub@example.com', 'status' => 'unsubscribed']);
    }

    public function test_honeypot_silently_succeeds_without_creating_lead(): void
    {
        Mail::fake();

        $this->postJson('/lead/subscribe', [
            'email'   => 'bot@example.com',
            'consent' => '1',
            'website' => 'http://spam.com',
        ])->assertOk();

        $this->assertDatabaseMissing('leads', ['email' => 'bot@example.com']);
        Mail::assertNothingSent();
    }
}
```

- [ ] **Step 12.2: LeadGateTest**

```php
<?php
// tests/Feature/Leads/LeadGateTest.php
namespace Tests\Feature\Leads;

use App\Models\Lead;
use App\Support\Leads\LeadGate;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class LeadGateTest extends TestCase
{
    use RefreshDatabase;

    public function test_gratis_page_renders(): void
    {
        $this->get('/gratis/seo-checkliste-2026')
            ->assertOk()
            ->assertSee('SEO-Checkliste');
    }

    public function test_gratis_page_returns_404_for_unknown_slug(): void
    {
        $this->get('/gratis/gibt-es-nicht')->assertNotFound();
    }

    public function test_content_gate_shows_paywall_schema_without_cookie(): void
    {
        $this->get('/gratis/seo-checkliste-2026')
            ->assertOk()
            ->assertSee('isAccessibleForFree')
            ->assertSee('lead/subscribe');
    }

    public function test_content_gate_unlocks_gated_content_with_valid_cookie(): void
    {
        $lead = Lead::create([
            'email'             => 'gate@example.com',
            'status'            => 'confirmed',
            'unsubscribe_token' => bin2hex(random_bytes(32)),
            'consent_version'   => '2026-07',
            'consent_text'      => 'Test',
        ]);

        $response = $this->withCookie(config('leads.cookie_name', 'kt_sub'), LeadGate::cookieValue($lead->id))
            ->get('/gratis/seo-checkliste-2026');

        $response->assertOk()
            ->assertDontSee('lead/subscribe')
            ->assertSee('vollständige Checkliste');
    }
}
```

- [ ] **Step 12.3: AdminLeadTest**

```php
<?php
// tests/Feature/Leads/AdminLeadTest.php
namespace Tests\Feature\Leads;

use App\Models\Lead;
use App\Models\LeadEvent;
use App\Models\LeadOtp;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;

class AdminLeadTest extends TestCase
{
    use RefreshDatabase;

    private function actingAsAdmin(): static
    {
        $raw  = 'test-admin-token-leads';
        $hash = hash('sha256', $raw);
        DB::table('admin_sessions')->insert([
            'token_hash' => $hash,
            'expires_at' => now()->addDays(1),
            'created_at' => now(),
        ]);
        return $this->withCookie('admin_auth_token', $raw);
    }

    public function test_admin_leads_requires_auth(): void
    {
        $this->get('/admin/leads')->assertRedirectToRoute('admin.login');
    }

    public function test_admin_leads_index_loads_and_shows_email(): void
    {
        Lead::create([
            'email'             => 'show@example.com',
            'status'            => 'confirmed',
            'unsubscribe_token' => bin2hex(random_bytes(32)),
            'consent_version'   => '2026-07',
            'consent_text'      => 'Test',
        ]);

        $this->actingAsAdmin()
            ->get('/admin/leads')
            ->assertOk()
            ->assertSee('show@example.com');
    }

    public function test_csv_export_contains_lead_email(): void
    {
        Lead::create([
            'email'             => 'csv@example.com',
            'status'            => 'confirmed',
            'unsubscribe_token' => bin2hex(random_bytes(32)),
            'consent_version'   => '2026-07',
            'consent_text'      => 'Test',
        ]);

        $response = $this->actingAsAdmin()->get('/admin/leads/export');
        $response->assertOk();
        $this->assertStringContainsString('csv@example.com', $response->getContent());
    }

    public function test_destroy_deletes_lead_events_and_otps(): void
    {
        $lead = Lead::create([
            'email'             => 'del@example.com',
            'status'            => 'pending',
            'unsubscribe_token' => bin2hex(random_bytes(32)),
            'consent_version'   => '2026-07',
            'consent_text'      => 'Test',
        ]);
        LeadEvent::log($lead->id, 'signup');
        LeadOtp::generate('del@example.com');

        $this->actingAsAdmin()
            ->delete('/admin/leads/' . $lead->id)
            ->assertRedirect();

        $this->assertDatabaseMissing('leads',       ['id' => $lead->id]);
        $this->assertDatabaseMissing('lead_events', ['lead_id' => $lead->id]);
        $this->assertDatabaseMissing('lead_otps',   ['email' => 'del@example.com']);
    }

    public function test_resend_sends_otp_mail(): void
    {
        Mail::fake();

        $lead = Lead::create([
            'email'             => 'resend@example.com',
            'status'            => 'pending',
            'unsubscribe_token' => bin2hex(random_bytes(32)),
            'consent_version'   => '2026-07',
            'consent_text'      => 'Test',
        ]);

        $this->actingAsAdmin()
            ->post('/admin/leads/' . $lead->id . '/resend')
            ->assertRedirect();

        Mail::assertSent(\App\Mail\LeadOtpMail::class);
    }
}
```

- [ ] **Step 12.4: Commit**

```bash
git add tests/Feature/Leads/
git commit -m "feat(leads): Feature-Tests LeadSubscribe/LeadGate/AdminLead (16 Tests)"
```

---

### Task 13: Migration ausführen + Tests grün

- [ ] **Step 13.1: Config leeren + Migration ausführen**

```bash
php artisan config:clear && php artisan migrate
```

Expected: `Migrated: 2026_07_01_000003_create_lead_tables` — OK.

- [ ] **Step 13.2: Tests ausführen**

```bash
composer test
```

Expected: Alle bisherigen Tests grün + mindestens 16 neue (LeadSubscribeTest × 7 + LeadGateTest × 4 + AdminLeadTest × 5).

**Häufige Fehler:**
- `Class 'App\Support\Leads\LeadGate' not found` → `composer dump-autoload` ausführen.
- `Route [admin.login] not defined` in AdminLeadTest → Route-Name prüfen: muss `admin.login` heißen (in routes/web.php, `->name('admin.login')`).
- `Cookie not decrypted in test` → Laravel verschlüsselt Cookies; Test mit `$this->withCookie()` liefert den Wert direkt (unverschlüsselt). `LeadGate::isUnlocked()` liest über `request()->cookie()`, das in Tests ohne Encryption-Middleware läuft — korrekt.
- `assertSee('vollständige Checkliste') fails` → Prüfe ob der View-Name in `config/lead_pages.php` genau `'gratis.seo-checkliste-2026'` lautet.

- [ ] **Step 13.3: Config-Cache wiederherstellen**

```bash
php artisan config:cache
```

- [ ] **Step 13.4: Abschluss-Commit**

```bash
git add -A
git commit -m "feat(leads): Phase 1 Lead-Capture-Engine — komplett, Tests grün"
```
