Compare commits

...

2 commits

Author SHA1 Message Date
9b47296cea Rebrand Hub+Flux
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
2026-05-20 15:44:15 +02:00
0a3e52d603 19-05-2026 Rebrand Pressekonto, Hub-Flux UI und Legacy-Media-Migration
Umbenennung presseportale → pressekonto in Domains, Themes und Dokumentation.
Design-Tokens, Portal-Shell, Customer-Dashboard, Auth- und Admin-PM-Views.
Artisan-Befehl migrate:legacy-media mit Tests und Hub-Flux-Entwicklungsdocs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 16:36:13 +00:00
229 changed files with 17648 additions and 4821 deletions

View file

@ -17,6 +17,6 @@ args = [
command = "npx"
[mcp_servers.laravel-boost]
command = "vendor/bin/sail"
command = "php"
args = ["artisan", "boost:mcp"]
cwd = "/var/www/html"

View file

@ -47,7 +47,7 @@ Das Dockerfile wurde angepasst um:
Falls die automatische Installation fehlschlägt, können Sie den Container manuell bauen:
```bash
cd /Users/pandora/Sites/presseportale.com
cd /Users/pandora/Sites/pressekonto.de
docker build --build-arg WWWUSER=501 --build-arg WWWGROUP=20 -f docker/8.4/Dockerfile -t sail-8.4/app docker/8.4
```

View file

@ -1,5 +1,5 @@
{
"name": "Presseportale (Dev Container)",
"name": "Pressekonto (Dev Container)",
"dockerComposeFile": [
"../docker-compose.yml"
],

3
.gitignore vendored
View file

@ -56,4 +56,5 @@ Icon
_static/
_work/
_storage/
_businessportal24.com/
_businessportal24.com/
dev/migration/

View file

@ -4,11 +4,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
This is a multi-domain Laravel application ("Presseportale") that supports different domains with distinct themes and styling. The application uses Laravel with Livewire, Volt, and Fortify for authentication, along with Flux UI components.
This is a multi-domain Laravel application ("Pressekonto") that supports different domains with distinct themes and styling. The application uses Laravel with Livewire, Volt, and Fortify for authentication, along with Flux UI components.
### Supported Domains
- **Main Portal**: presseportale.test (local) / presseportale.com (live) Main admin portal page
- **Main Portal**: pressekonto.test (local) / pressekonto.de (live) Main admin portal page
- **Presseecho**: presseecho.test - Landing page with presseecho theme
- **Business Portal**: businessportal24.test - Landing page with business portal theme

View file

@ -4,7 +4,7 @@
Diese Laravel-Anwendung unterstützt verschiedene Domains mit unterschiedlichen Styles:
- **Haupt-Website**: https://presseportale.test (lokal) / https://presseportale.com (live) Haupt-Portal Admin-Page
- **Haupt-Website**: https://pressekonto.test (lokal) / https://pressekonto.de (live) Haupt-Portal Admin-Page
- **APP_PRESSEECHO**: https://presseecho.test
- **APP_BUSINESSPORTAL**: https://businessportal24.test

View file

@ -8,18 +8,18 @@ Für dein Multi-Domain-Setup empfehle ich folgende Asset-URLs:
| Bereich | Domain | Asset-URL | Port | Verwendung |
|---------|--------|-----------|------|------------|
| **Backend** | presseportale.test | `assets.presseportale.test` | 5177 | Portal + FluxUI |
| **Frontend** | presseecho.test<br>businessportal24.test | `assets-web.presseportale.test` | 5178 | Beide Frontend-Domains |
| **Backend** | pressekonto.test | `assets.pressekonto.test` | 5177 | Portal + FluxUI |
| **Frontend** | presseecho.test<br>businessportal24.test | `assets-web.pressekonto.test` | 5178 | Beide Frontend-Domains |
### Warum diese URLs?
#### 1. **assets.presseportale.test** (Portal/Backend)
#### 1. **assets.pressekonto.test** (Portal/Backend)
- ✅ Kurz und prägnant
- ✅ Eindeutig dem Portal zugeordnet
- ✅ Keine zusätzliche Subdomain-Tiefe
- ✅ Folgt gängiger Konvention
#### 2. **assets-web.presseportale.test** (Web/Frontend)
#### 2. **assets-web.pressekonto.test** (Web/Frontend)
- ✅ Klar als "Web" (Frontend) gekennzeichnet
- ✅ Ein Asset-Server für beide Frontend-Domains
- ✅ Gute Trennung zu Portal-Assets
@ -31,23 +31,23 @@ Falls du andere URLs bevorzugst, hier sind Alternativen:
### Option A: Mit Suffix-Präfix
```
portal-assets.presseportale.test → Port 5177
web-assets.presseportale.test → Port 5178
portal-assets.pressekonto.test → Port 5177
web-assets.pressekonto.test → Port 5178
```
- ⚠️ Etwas länger
- ✅ Sehr explizit
### Option B: Mit "vite" im Namen
```
vite.presseportale.test → Port 5177
vite-web.presseportale.test → Port 5178
vite.pressekonto.test → Port 5177
vite-web.pressekonto.test → Port 5178
```
- ⚠️ Technologie-spezifisch (was wenn du später zu einem anderen Build-Tool wechselst?)
- ⚠️ Weniger klar was geladen wird
### Option C: Separate Domains pro Frontend
```
assets.presseportale.test → Port 5177 (Portal)
assets.pressekonto.test → Port 5177 (Portal)
assets.presseecho.test → Port 5178 (Presseecho)
assets.businessportal24.test → Port 5178 (Businessportal24)
```
@ -61,7 +61,7 @@ assets.businessportal24.test → Port 5178 (Businessportal24)
```yaml
# Portal Assets (Backend)
- "traefik.http.routers.assets-portal.rule=Host(`assets.presseportale.test`)"
- "traefik.http.routers.assets-portal.rule=Host(`assets.pressekonto.test`)"
- "traefik.http.routers.assets-portal.entrypoints=websecure"
- "traefik.http.routers.assets-portal.tls=true"
- "traefik.http.routers.assets-portal.service=assets-portal-service"
@ -69,7 +69,7 @@ assets.businessportal24.test → Port 5178 (Businessportal24)
- "traefik.http.services.assets-portal-service.loadbalancer.server.scheme=http"
# Web Assets (Frontend)
- "traefik.http.routers.assets-web.rule=Host(`assets-web.presseportale.test`)"
- "traefik.http.routers.assets-web.rule=Host(`assets-web.pressekonto.test`)"
- "traefik.http.routers.assets-web.entrypoints=websecure"
- "traefik.http.routers.assets-web.tls=true"
- "traefik.http.routers.assets-web.service=assets-web-service"
@ -89,8 +89,8 @@ ports:
```env
# Vite Asset Domains
ASSET_URL_PORTAL=https://assets.presseportale.test
ASSET_URL_WEB=https://assets-web.presseportale.test
ASSET_URL_PORTAL=https://assets.pressekonto.test
ASSET_URL_WEB=https://assets-web.pressekonto.test
# Vite Development Ports
VITE_PORT_PORTAL=5177
@ -102,11 +102,11 @@ VITE_PORT_WEB=5178
Füge folgende Einträge zu deiner `/etc/hosts` (Linux/Mac) oder `C:\Windows\System32\drivers\etc\hosts` (Windows) hinzu:
```
127.0.0.1 presseportale.test
127.0.0.1 pressekonto.test
127.0.0.1 presseecho.test
127.0.0.1 businessportal24.test
127.0.0.1 assets.presseportale.test
127.0.0.1 assets-web.presseportale.test
127.0.0.1 assets.pressekonto.test
127.0.0.1 assets-web.pressekonto.test
```
## Vite-Konfigurationen
@ -121,7 +121,7 @@ export default defineConfig({
host: "0.0.0.0",
port: 5177,
hmr: {
host: "assets.presseportale.test", // ← Asset-URL
host: "assets.pressekonto.test", // ← Asset-URL
protocol: "wss",
},
},
@ -139,7 +139,7 @@ export default defineConfig({
host: "0.0.0.0",
port: 5178,
hmr: {
host: "assets-web.presseportale.test", // ← Asset-URL
host: "assets-web.pressekonto.test", // ← Asset-URL
protocol: "wss",
},
},
@ -156,16 +156,16 @@ Browser-Request
2. Laravel lädt View mit: @vite(['resources/css/web/theme-presseecho.css', ...])
3. Vite-Helper generiert: <script src="https://assets-web.presseportale.test/@vite/client"></script>
<link href="https://assets-web.presseportale.test/resources/css/web/theme-presseecho.css">
3. Vite-Helper generiert: <script src="https://assets-web.pressekonto.test/@vite/client"></script>
<link href="https://assets-web.pressekonto.test/resources/css/web/theme-presseecho.css">
4. Browser requested: assets-web.presseportale.test
4. Browser requested: assets-web.pressekonto.test
5. Traefik routet zu: Container Port 5178
6. Vite Web Server antwortet
7. HMR WebSocket öffnet: wss://assets-web.presseportale.test
7. HMR WebSocket öffnet: wss://assets-web.pressekonto.test
8. ✅ Hot Module Replacement funktioniert!
```
@ -175,8 +175,8 @@ Browser-Request
### 1. DNS-Auflösung testen
```bash
# Sollte zu 127.0.0.1 auflösen
ping assets.presseportale.test
ping assets-web.presseportale.test
ping assets.pressekonto.test
ping assets-web.pressekonto.test
```
### 2. Vite-Server starten
@ -195,15 +195,15 @@ Du solltest sehen:
### 3. Browser-Test
Öffne:
- https://presseportale.test (sollte Assets von assets.presseportale.test laden)
- https://presseecho.test (sollte Assets von assets-web.presseportale.test laden)
- https://businessportal24.test (sollte Assets von assets-web.presseportale.test laden)
- https://pressekonto.test (sollte Assets von assets.pressekonto.test laden)
- https://presseecho.test (sollte Assets von assets-web.pressekonto.test laden)
- https://businessportal24.test (sollte Assets von assets-web.pressekonto.test laden)
### 4. HMR-Test
1. Öffne Browser DevTools (F12)
2. Gehe zu "Network" Tab
3. Filter auf "WS" (WebSocket)
4. Du solltest Verbindungen zu `wss://assets.*.presseportale.test` sehen
4. Du solltest Verbindungen zu `wss://assets.*.pressekonto.test` sehen
5. Ändere eine CSS-Datei
6. Browser sollte automatisch neu laden (ohne vollständigen Page-Refresh)
@ -256,8 +256,8 @@ docker compose logs laravel.test | grep traefik
### ✅ Verwende diese Asset-URLs:
```
assets.presseportale.test → Port 5177 (Portal/Backend)
assets-web.presseportale.test → Port 5178 (Web/Frontend)
assets.pressekonto.test → Port 5177 (Portal/Backend)
assets-web.pressekonto.test → Port 5178 (Web/Frontend)
```
### ✅ Vorteile:

View file

@ -9,14 +9,14 @@ Füge die folgenden Variablen zu deiner `.env`-Datei hinzu:
```env
# Domain-Konfigurationen
APP_NAME=presseportale
APP_URL=https://presseportale.test
APP_NAME=pressekonto
APP_URL=https://pressekonto.test
APP_PRIMARY="#3ea3dc"
APP_ACCENT="#5c5c60"
# Entwicklungseinstellungen für Domains
DEV_SIMULATE_DOMAIN=false
DEV_SIMULATED_DOMAIN=presseportale.test
DEV_SIMULATED_DOMAIN=pressekonto.test
```
## Entwicklungsmodus
@ -34,10 +34,10 @@ unabhängig von der tatsächlichen URL.
Jede Domain kann eigene Einstellungen haben:
### Haupt-Website (presseportale.test)
### Haupt-Website (pressekonto.test)
- `APP_URL`: Die Domain für die Haupt-Website (https://presseportale.test)
- `APP_NAME`: Der Name der Haupt-Website (presseportale)
- `APP_URL`: Die Domain für die Haupt-Website (https://pressekonto.test)
- `APP_NAME`: Der Name der Haupt-Website (pressekonto)
- `APP_PRIMARY`: Die primäre Farbe im HEX-Format (#3ea3dc)
- `APP_ACCENT`: Die Akzentfarbe im HEX-Format (#5c5c60)
@ -62,7 +62,7 @@ Jede Domain kann eigene Einstellungen haben:
Um die verschiedenen Domains lokal zu testen, füge folgende Zeilen zu deiner Hosts-Datei hinzu:
```
127.0.0.1 presseportale.test
127.0.0.1 pressekonto.test
127.0.0.1 presseecho.test
127.0.0.1 businessportal24.test
```
@ -101,7 +101,7 @@ Im Code kannst du auf die Domain-Konfiguration zugreifen:
Das Projekt verwendet bereits eine Multi-Domain-Architektur mit:
- **Hauptwebsite:** `presseportale.test` - Hauptwebsite mit blauem Theme (#3ea3dc)
- **Hauptwebsite:** `pressekonto.test` - Hauptwebsite mit blauem Theme (#3ea3dc)
- **Presseecho:** `presseecho.test` - Presseecho-Website mit rotem Theme (#e94a3c)
- **Business Portal:** `businessportal24.test` - Business Portal mit orangem Theme (#f69f0f)

View file

@ -2,7 +2,7 @@
## 🔍 Ursprüngliches Problem
Auf https://presseportale.test erschien der Fehler:
Auf https://pressekonto.test erschien der Fehler:
```
[Error] Not allowed to use restricted network host "0.0.0.0":
https://0.0.0.0:5178/@vite/client
@ -53,7 +53,7 @@ npm run dev:all
```bash
# Portal CSS wird korrekt geladen:
curl -Iks https://assets.presseportale.test/resources/css/portal.css
curl -Iks https://assets.pressekonto.test/resources/css/portal.css
# → HTTP/2 200 ✅
# Web Assets funktionieren:
@ -65,13 +65,13 @@ curl -Iks https://assets.businessportal24.test/resources/css/web/theme-businessp
| Domain | Asset-Domain | Port | Build-Dir | CSS-Datei |
|--------|-------------|------|-----------|-----------|
| presseportale.test | assets.presseportale.test | 5177 | build/portal | portal.css |
| pressekonto.test | assets.pressekonto.test | 5177 | build/portal | portal.css |
| presseecho.test | assets.presseecho.test | 5178 | build/web | theme-presseecho.css |
| businessportal24.test | assets.businessportal24.test | 5178 | build/web | theme-businessportal24.css |
## 🚀 Nächste Schritte
1. **Browser testen**: Öffne https://presseportale.test und mache einen Hard-Refresh (`Ctrl+Shift+R`)
1. **Browser testen**: Öffne https://pressekonto.test und mache einen Hard-Refresh (`Ctrl+Shift+R`)
2. **Keine Fehler mehr**: Die "0.0.0.0" Fehler sollten verschwunden sein
3. **Assets laden über HTTPS**: Alle CSS/JS-Dateien werden über die korrekten Asset-Subdomains geladen
@ -87,7 +87,7 @@ Falls du die Docker Container neu gestartet hast, stelle sicher dass:
1. ✅ DNS-Einträge in `/etc/hosts` vorhanden sind:
```
127.0.0.1 assets.presseportale.test
127.0.0.1 assets.pressekonto.test
127.0.0.1 assets.presseecho.test
127.0.0.1 assets.businessportal24.test
```

View file

@ -4,7 +4,7 @@
### 1. `config/domains.php`
Jede Domain hat jetzt eine dedizierte `asset_url`:
- `portal`: `https://assets.presseportale.test`
- `portal`: `https://assets.pressekonto.test`
- `presseecho`: `https://assets.presseecho.test`
- `businessportal24`: `https://assets.businessportal24.test`
@ -39,7 +39,7 @@ sleep 5 && tail -30 /tmp/vite-server.log
### 2. Im Browser testen
Öffne mit Hard-Refresh (`Ctrl+Shift+R`):
- ✅ https://presseportale.test
- ✅ https://pressekonto.test
- ✅ https://presseecho.test
- ✅ https://businessportal24.test
@ -52,7 +52,7 @@ https://0.0.0.0:5178/@vite/client
**NACHHER (✅)**:
```
https://assets.presseportale.test/@vite/client
https://assets.pressekonto.test/@vite/client
https://assets.presseecho.test/@vite/client
https://assets.businessportal24.test/@vite/client
```

View file

@ -54,7 +54,7 @@ composer require laravel/fortify laravel/sanctum
## Routen
### Web-Authentifizierung (presseportale.test)
### Web-Authentifizierung (pressekonto.test)
- `GET /login` - Anmeldeseite (Livewire)
- `POST /login` - Anmeldung (Livewire)
@ -68,7 +68,7 @@ composer require laravel/fortify laravel/sanctum
- `GET /verify-email` - E-Mail-Verifizierung (Livewire)
- `GET /confirm-password` - Passwort bestätigen (Livewire)
### API-Routen (api.presseportale.test)
### API-Routen (api.pressekonto.test)
- `GET /api/user` - Aktueller Benutzer (geschützt)
- `GET /api/profile` - Benutzerprofil (geschützt)
@ -78,7 +78,7 @@ composer require laravel/fortify laravel/sanctum
### Web-Authentifizierung
1. Besuchen Sie `http://portal.presseportale.test/login`
1. Besuchen Sie `http://portal.pressekonto.test/login`
2. Registrieren Sie sich oder melden Sie sich an
3. Nutzen Sie die verschiedenen Authentifizierungsfeatures
@ -87,7 +87,7 @@ composer require laravel/fortify laravel/sanctum
1. **Token erstellen**:
```bash
curl -X POST http://api.presseportale.test/login \
curl -X POST http://api.pressekonto.test/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"password"}'
```
@ -95,7 +95,7 @@ curl -X POST http://api.presseportale.test/login \
2. **Geschützte Route aufrufen**:
```bash
curl -X GET http://api.presseportale.test/api/user \
curl -X GET http://api.pressekonto.test/api/user \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
```
@ -150,7 +150,7 @@ Bearbeiten Sie die Komponenten in `resources/views/livewire/auth/`:
```php
// Beispiel: Login-Komponente anpassen
new #[Layout('components.layouts.auth')] class extends Component {
new #[Layout('components.layouts.auth.pressekonto')] class extends Component {
#[Validate('required|string|email')]
public string $email = '';

View file

@ -6,7 +6,7 @@ Das Projekt nutzt ein dynamisches Theme-System mit 3 Domains:
| Domain | Theme | Primary Color | Secondary Color | CSS-Datei |
|--------|-------|---------------|-----------------|-----------|
| **presseportale.test** | portal | #526266 | #82a0a7 | `resources/css/portal.css` |
| **pressekonto.test** | portal | #526266 | #82a0a7 | `resources/css/portal.css` |
| **presseecho.test** | presseecho | #345636 (Grün) | #6b8f71 | `resources/css/web/theme-presseecho.css` |
| **businessportal24.test** | businessportal24 | #cf3628 (Rot) | #f0834a | `resources/css/web/theme-businessportal24.css` |
@ -276,8 +276,8 @@ npm run build:web
Der `ThemeServiceProvider` unterstützt einen `?theme=` URL-Parameter zum Testen:
```
https://presseportale.test?theme=presseecho
https://presseportale.test?theme=businessportal24
https://pressekonto.test?theme=presseecho
https://pressekonto.test?theme=businessportal24
```
### Via Host
@ -285,7 +285,7 @@ https://presseportale.test?theme=businessportal24
Einfach die entsprechende Domain aufrufen:
```
https://presseportale.test → Portal Theme
https://pressekonto.test → Portal Theme
https://presseecho.test → Presseecho Theme
https://businessportal24.test → Businessportal24 Theme
```

View file

@ -6,7 +6,7 @@ Jede Domain hat jetzt ihre eigene dedizierte Asset-Subdomain:
| Domain | Asset-Subdomain | Port | Vite-Config |
|--------|----------------|------|-------------|
| `presseportale.test` | `assets.presseportale.test` | 5177 | `vite.portal.config.js` |
| `pressekonto.test` | `assets.pressekonto.test` | 5177 | `vite.portal.config.js` |
| `presseecho.test` | `assets.presseecho.test` | 5178 | `vite.web.config.js` |
| `businessportal24.test` | `assets.businessportal24.test` | 5178 | `vite.web.config.js` |
@ -20,7 +20,7 @@ Füge folgende Einträge zu deiner Hosts-Datei hinzu (lokal auf deinem Host-Syst
**Windows**: `C:\Windows\System32\drivers\etc\hosts`
```
127.0.0.1 assets.presseportale.test
127.0.0.1 assets.pressekonto.test
127.0.0.1 assets.presseecho.test
127.0.0.1 assets.businessportal24.test
```
@ -47,7 +47,7 @@ npm run dev:all
### 1. `docker-compose.yml`
Neue Traefik-Routen hinzugefügt:
- `assets.presseportale.test` → Port 5177 (Portal)
- `assets.pressekonto.test` → Port 5177 (Portal)
- `assets.presseecho.test` → Port 5178 (Presseecho)
- `assets.businessportal24.test` → Port 5178 (Businessportal24)
@ -66,7 +66,7 @@ Nach dem Neustart kannst du testen:
```bash
# Im DevContainer:
curl -Ik https://assets.presseportale.test/@vite/client
curl -Ik https://assets.pressekonto.test/@vite/client
curl -Ik https://assets.presseecho.test/@vite/client
curl -Ik https://assets.businessportal24.test/@vite/client
```

View file

@ -6,7 +6,7 @@ Dieses Projekt verwendet **2 separate Vite-Ports** für unterschiedliche Domain-
| Bereich | Port | Vite Config | Tailwind Config | Domains | FluxUI |
|---------|------|-------------|-----------------|---------|--------|
| **Backend (Portal)** | 5177 | `vite.portal.config.js` | `tailwind.portal.config.js` | `presseportale.test` | ✅ Ja |
| **Backend (Portal)** | 5177 | `vite.portal.config.js` | `tailwind.portal.config.js` | `pressekonto.test` | ✅ Ja |
| **Frontend (Web)** | 5178 | `vite.web.config.js` | `tailwind.web.config.js` | `presseecho.test`, `businessportal24.test` | ❌ Nein |
## Warum 2 Ports?
@ -35,15 +35,15 @@ Startet beide Vite-Server parallel mit `concurrently`
npm run dev:portal
```
- Port: 5177
- HMR-Host: assets.presseportale.test
- Domain: presseportale.test
- HMR-Host: assets.pressekonto.test
- Domain: pressekonto.test
### Option 3: Nur Frontend (Web)
```bash
npm run dev:web
```
- Port: 5178
- HMR-Host: assets-web.presseportale.test
- HMR-Host: assets-web.pressekonto.test
- Domains: presseecho.test, businessportal24.test
## Production Build
@ -86,7 +86,7 @@ public/
## Theme-System
### Backend (Portal)
- **Domain:** presseportale.test
- **Domain:** pressekonto.test
- **Theme:** `portal`
- **CSS:** `resources/css/portal.css`
- **Views:** `resources/views/portal/**`
@ -110,8 +110,8 @@ public/
Beide Vite-Server laufen intern auf HTTP (`https: false`), Traefik übernimmt SSL-Terminierung:
- **Portal HMR:** `wss://assets.presseportale.test` → Port 5177
- **Web HMR:** `wss://assets-web.presseportale.test` → Port 5178
- **Portal HMR:** `wss://assets.pressekonto.test` → Port 5177
- **Web HMR:** `wss://assets-web.pressekonto.test` → Port 5178
## Troubleshooting

View file

@ -37,7 +37,7 @@ sudo nano /etc/hosts
**Einträge:**
```
127.0.0.1 assets.presseportale.test
127.0.0.1 assets.pressekonto.test
127.0.0.1 assets.presseecho.test
127.0.0.1 assets.businessportal24.test
```
@ -51,7 +51,7 @@ Zurück im DevContainer:
tail -20 /tmp/vite-server.log
# Teste die Asset-URLs:
curl -Ik https://assets.presseportale.test/@vite/client
curl -Ik https://assets.pressekonto.test/@vite/client
curl -Ik https://assets.presseecho.test/@vite/client
curl -Ik https://assets.businessportal24.test/@vite/client
@ -63,7 +63,7 @@ curl -Ik https://assets.businessportal24.test/@vite/client
Öffne:
- https://businessportal24.test
- https://presseecho.test
- https://presseportale.test
- https://pressekonto.test
Die Assets sollten nun korrekt über HTTPS von den jeweiligen Asset-Subdomains geladen werden!
@ -71,7 +71,7 @@ Die Assets sollten nun korrekt über HTTPS von den jeweiligen Asset-Subdomains g
| Hauptdomain | Asset-Domain | Port | Build-Dir |
|------------|-------------|------|-----------|
| presseportale.test | assets.presseportale.test | 5177 | public/build/portal |
| pressekonto.test | assets.pressekonto.test | 5177 | public/build/portal |
| presseecho.test | assets.presseecho.test | 5178 | public/build/web |
| businessportal24.test | assets.businessportal24.test | 5178 | public/build/web |

View file

@ -65,7 +65,7 @@ Du benötigst **mindestens 2 Vite-Ports**:
│ └──────────────────┘ └──────────────────┘ │
│ ↓ ↓ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ presseportale.test │ │ presseecho.test │ │
│ │ pressekonto.test │ │ presseecho.test │ │
│ │ │ │ businessp24.test│ │
│ └──────────────────┘ └──────────────────┘ │
│ │
@ -80,14 +80,14 @@ Du benötigst **mindestens 2 Vite-Ports**:
- Port: **5177**
- Input: `resources/css/portal.css`
- Build: `public/build/portal`
- HMR: `assets.presseportale.test`
- HMR: `assets.pressekonto.test`
- FluxUI: ✅ Ja
#### ✅ `vite.web.config.js`
- Port: **5178** (geändert von 5177!)
- Input: `theme-presseecho.css`, `theme-businessportal24.css`
- Build: `public/build/web`
- HMR: `assets-web.presseportale.test`
- HMR: `assets-web.pressekonto.test`
- FluxUI: ❌ Nein
#### ❌ `vite.config.js` (deprecated)
@ -190,14 +190,14 @@ npm run build:web
| URL | Vite-Port | Theme | FluxUI |
|-----|-----------|-------|--------|
| https://presseportale.test | 5177 | portal | ✅ |
| https://pressekonto.test | 5177 | portal | ✅ |
| https://presseecho.test | 5178 | presseecho | ❌ |
| https://businessportal24.test | 5178 | businessportal24 | ❌ |
## HMR (Hot Module Replacement)
- **Portal:** `wss://assets.presseportale.test` → Port 5177
- **Web:** `wss://assets-web.presseportale.test` → Port 5178
- **Portal:** `wss://assets.pressekonto.test` → Port 5177
- **Web:** `wss://assets-web.pressekonto.test` → Port 5178
⚠️ **Wichtig:** Traefik muss beide HMR-Hosts routen!
@ -210,12 +210,12 @@ Stelle sicher, dass Traefik beide Vite-Ports routet:
# docker-compose.yml oder traefik.yml
labels:
# Portal Assets
- "traefik.http.routers.vite-portal.rule=Host(`assets.presseportale.test`)"
- "traefik.http.routers.vite-portal.rule=Host(`assets.pressekonto.test`)"
- "traefik.http.routers.vite-portal.service=vite-portal"
- "traefik.http.services.vite-portal.loadbalancer.server.port=5177"
# Web Assets
- "traefik.http.routers.vite-web.rule=Host(`assets-web.presseportale.test`)"
- "traefik.http.routers.vite-web.rule=Host(`assets-web.pressekonto.test`)"
- "traefik.http.routers.vite-web.service=vite-web"
- "traefik.http.services.vite-web.loadbalancer.server.port=5178"
```
@ -223,11 +223,11 @@ labels:
### 2. DNS/Hosts-Datei aktualisieren
```
127.0.0.1 presseportale.test
127.0.0.1 pressekonto.test
127.0.0.1 presseecho.test
127.0.0.1 businessportal24.test
127.0.0.1 assets.presseportale.test
127.0.0.1 assets-web.presseportale.test
127.0.0.1 assets.pressekonto.test
127.0.0.1 assets-web.pressekonto.test
```
### 3. Blade-Templates aktualisieren
@ -258,7 +258,7 @@ Diese Dateien werden nicht mehr benötigt:
npm run dev:all
# 2. Browser öffnen
# - https://presseportale.test (Backend)
# - https://pressekonto.test (Backend)
# - https://presseecho.test (Frontend Grün)
# - https://businessportal24.test (Frontend Rot)

View file

@ -1,6 +1,6 @@
openapi: 3.1.0
info:
title: Presseportale API
title: Pressekonto API
version: 1.0.0
description: >
REST API for customer integrations after the 2026 migration. Legacy

View file

@ -0,0 +1,472 @@
<?php
namespace App\Console\Commands;
use App\Enums\Portal;
use App\Models\Company;
use App\Models\PressRelease;
use App\Models\PressReleaseImage;
use App\Services\Image\ImageService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class MigrateLegacyMedia extends Command
{
protected $signature = 'legacy:migrate-media
{--portal=all : Portal (presseecho|businessportal24|all)}
{--type=all : Medientyp (company-logos|press-release-images|all)}
{--base-path=dev/migration : Lokaler Ordner mit den Legacy-Dateien}
{--dry-run : Nur prüfen, nichts kopieren oder aktualisieren}
{--force : Bereits migrierte Dateien erneut überschreiben}
{--limit=0 : Maximal N Legacy-Datensätze pro Typ und Portal verarbeiten (0 = alle)}';
protected $description = 'Migriert Legacy-Firmenlogos und Pressemitteilungsbilder anhand der Legacy-DB in den finalen Storage.';
private const PORTAL_CONNECTIONS = [
'presseecho' => 'mysql_presseecho',
'businessportal24' => 'mysql_businessportal',
];
/**
* @var array<string, list<string>>
*/
private const SOURCE_DIRECTORIES = [
'company-logos' => [
'uploads/company',
'thumbnails/company',
],
'press-release-images' => [
'uploads/pressreleaseimage',
'uploads/pressreleaseimage_',
'uploads/_pressreleaseimage',
'thumbnails/pressreleaseimage',
],
];
public function __construct(private readonly ImageService $imageService)
{
parent::__construct();
}
public function handle(): int
{
$portals = $this->selectedPortals();
$types = $this->selectedTypes();
if ($portals === []) {
$this->error('Ungültiges Portal. Erlaubt: presseecho, businessportal24, all.');
return self::FAILURE;
}
if ($types === []) {
$this->error('Ungültiger Medientyp. Erlaubt: company-logos, press-release-images, all.');
return self::FAILURE;
}
$configuredBasePath = (string) $this->option('base-path');
$basePath = Str::startsWith($configuredBasePath, '/')
? rtrim($configuredBasePath, '/')
: base_path(trim($configuredBasePath, '/'));
$dryRun = (bool) $this->option('dry-run');
$force = (bool) $this->option('force');
$limit = max(0, (int) $this->option('limit'));
$totals = $this->emptyStats();
foreach ($portals as $portal) {
foreach ($types as $type) {
$stats = match ($type) {
'company-logos' => $this->migrateCompanyLogos($portal, $basePath, $dryRun, $force, $limit),
'press-release-images' => $this->migratePressReleaseImages($portal, $basePath, $dryRun, $force, $limit),
};
foreach ($totals as $key => $value) {
$totals[$key] = $value + $stats[$key];
}
$this->line(sprintf(
'%s/%s: Legacy %d, migriert %d, Thumbnail-Fallback %d, DB-Updates %d, bereits synchron %d, Ziel fehlt %d, Datei fehlt %d',
$portal,
$type,
$stats['legacy_rows'],
$stats['migrated'],
$stats['thumbnail_fallback'],
$stats['updated'],
$stats['already_synced'],
$stats['missing_target'],
$stats['missing_file'],
));
}
}
$this->newLine();
$this->info(sprintf(
'Gesamt: Legacy %d, migriert %d, Thumbnail-Fallback %d, DB-Updates %d, bereits synchron %d, Ziel fehlt %d, Datei fehlt %d%s',
$totals['legacy_rows'],
$totals['migrated'],
$totals['thumbnail_fallback'],
$totals['updated'],
$totals['already_synced'],
$totals['missing_target'],
$totals['missing_file'],
$dryRun ? ' (Dry-Run)' : '',
));
return ($totals['missing_target'] + $totals['missing_file']) > 0
? self::FAILURE
: self::SUCCESS;
}
/**
* @return array{legacy_rows:int,migrated:int,thumbnail_fallback:int,updated:int,already_synced:int,missing_target:int,missing_file:int}
*/
private function migrateCompanyLogos(string $portal, string $basePath, bool $dryRun, bool $force, int $limit): array
{
$stats = $this->emptyStats();
$sourceIndex = $this->sourceIndex($basePath, $portal, 'company-logos');
$processed = 0;
DB::connection(self::PORTAL_CONNECTIONS[$portal])
->table('company')
->whereNotNull('logo')
->where('logo', '!=', '')
->orderBy('id')
->chunk(500, function ($rows) use ($portal, $sourceIndex, $dryRun, $force, $limit, &$processed, &$stats): bool {
foreach ($rows as $row) {
if ($limit > 0 && $processed >= $limit) {
return false;
}
$processed++;
$stats['legacy_rows']++;
$company = Company::withoutGlobalScopes()
->where('legacy_portal', $portal)
->where('legacy_id', $row->id)
->first(['id', 'logo_path', 'logo_variants']);
if (! $company) {
$stats['missing_target']++;
$this->warn("Ziel fehlt: {$portal}/company/{$row->id}");
continue;
}
$sourceFilename = $this->legacyFilename((string) $row->logo);
$sourcePath = $sourceIndex[$sourceFilename] ?? $sourceIndex[Str::lower($sourceFilename)] ?? null;
$destinationPath = "company-logos/{$portal}/{$company->id}/{$sourceFilename}";
if ($this->isAlreadySynced($company->logo_path, $destinationPath, $force)) {
$stats['already_synced']++;
if (! $dryRun && (! is_array($company->logo_variants) || $company->logo_variants === [])) {
$variants = $this->imageService->generateMissingCompanyLogoVariants($destinationPath);
if ($variants !== []) {
$company->forceFill(['logo_variants' => $variants])->save();
$stats['updated']++;
}
}
continue;
}
if (! $sourcePath) {
$stats['missing_file']++;
$this->warn("Datei fehlt: {$portal}/company/{$sourceFilename} (Company #{$company->id})");
continue;
}
if (! $dryRun) {
$this->copyToPublicStorage($sourcePath, $destinationPath, $force);
$company->forceFill([
'logo_path' => $destinationPath,
'logo_variants' => $this->imageService->generateMissingCompanyLogoVariants($destinationPath),
])->save();
}
$stats['migrated']++;
$stats['updated']++;
}
return true;
});
return $stats;
}
/**
* @return array{legacy_rows:int,migrated:int,thumbnail_fallback:int,updated:int,already_synced:int,missing_target:int,missing_file:int}
*/
private function migratePressReleaseImages(string $portal, string $basePath, bool $dryRun, bool $force, int $limit): array
{
$stats = $this->emptyStats();
$sourceIndex = $this->sourceIndex($basePath, $portal, 'press-release-images');
$thumbnailIndex = $this->thumbnailIndex($basePath, $portal);
$processed = 0;
DB::connection(self::PORTAL_CONNECTIONS[$portal])
->table('press_release_image')
->orderBy('id')
->chunk(500, function ($rows) use ($portal, $sourceIndex, $thumbnailIndex, $dryRun, $force, $limit, &$processed, &$stats): bool {
foreach ($rows as $row) {
if ($limit > 0 && $processed >= $limit) {
return false;
}
if (blank($row->image)) {
continue;
}
$processed++;
$stats['legacy_rows']++;
$pressRelease = PressRelease::withoutGlobalScopes()
->where('legacy_portal', $portal)
->where('legacy_id', $row->press_release_id)
->first(['id']);
if (! $pressRelease) {
$stats['missing_target']++;
$this->warn("Ziel fehlt: {$portal}/press_release/{$row->press_release_id}");
continue;
}
$sourceFilename = $this->legacyFilename((string) $row->image);
$sourcePath = $sourceIndex[$sourceFilename] ?? $sourceIndex[Str::lower($sourceFilename)] ?? null;
$usedThumbnailFallback = false;
if (! $sourcePath) {
$sourcePath = $thumbnailIndex[(int) $row->id] ?? null;
$usedThumbnailFallback = $sourcePath !== null;
}
$destinationFilename = $usedThumbnailFallback ? basename($sourcePath) : $sourceFilename;
$destinationPath = "press-releases/{$pressRelease->id}/images/{$destinationFilename}";
$image = PressReleaseImage::withTrashed()
->where('legacy_portal', $portal)
->where('legacy_id', $row->id)
->first();
if ($image && $this->isAlreadySynced($image->path, $destinationPath, $force)) {
$stats['already_synced']++;
if (! $dryRun && (! is_array($image->variants) || $image->variants === [])) {
$variants = $this->imageService->generateMissingPressReleaseVariants($destinationPath);
if ($variants !== []) {
$image->forceFill(['variants' => $variants])->save();
$stats['updated']++;
}
}
continue;
}
if (! $sourcePath) {
$stats['missing_file']++;
$this->warn("Datei fehlt: {$portal}/press_release_image/{$sourceFilename} (Image #{$row->id})");
continue;
}
if ($usedThumbnailFallback) {
$stats['thumbnail_fallback']++;
}
$variants = [];
$size = [null, null];
$mime = null;
if (! $dryRun) {
$this->copyToPublicStorage($sourcePath, $destinationPath, $force);
$variants = $this->imageService->generateMissingPressReleaseVariants($destinationPath);
$size = @getimagesize(Storage::disk('public')->path($destinationPath)) ?: [null, null];
$mime = File::mimeType($sourcePath) ?: null;
$image = $image ?: new PressReleaseImage;
$image->forceFill([
'press_release_id' => $pressRelease->id,
'disk' => 'public',
'path' => $destinationPath,
'variants' => $variants,
'title' => $row->title ?: null,
'description' => $row->description ?: null,
'copyright' => $row->copyright ?: null,
'is_preview' => (bool) $row->is_preview_image,
'sort_order' => 0,
'width' => is_int($size[0] ?? null) ? $size[0] : null,
'height' => is_int($size[1] ?? null) ? $size[1] : null,
'mime' => $mime,
'legacy_portal' => $portal,
'legacy_id' => $row->id,
]);
if ($image->trashed()) {
$image->restore();
}
$image->save();
}
$stats['migrated']++;
$stats['updated']++;
}
return true;
});
return $stats;
}
/**
* @return list<string>
*/
private function selectedPortals(): array
{
$portal = (string) $this->option('portal');
return match ($portal) {
'all' => [Portal::Presseecho->value, Portal::Businessportal24->value],
Portal::Presseecho->value, Portal::Businessportal24->value => [$portal],
default => [],
};
}
/**
* @return list<string>
*/
private function selectedTypes(): array
{
$type = (string) $this->option('type');
return match ($type) {
'all' => ['company-logos', 'press-release-images'],
'company-logos', 'press-release-images' => [$type],
default => [],
};
}
/**
* @return array{legacy_rows:int,migrated:int,thumbnail_fallback:int,updated:int,already_synced:int,missing_target:int,missing_file:int}
*/
private function emptyStats(): array
{
return [
'legacy_rows' => 0,
'migrated' => 0,
'thumbnail_fallback' => 0,
'updated' => 0,
'already_synced' => 0,
'missing_target' => 0,
'missing_file' => 0,
];
}
/**
* @return array<string, string>
*/
private function sourceIndex(string $basePath, string $portal, string $type): array
{
$index = [];
foreach (self::SOURCE_DIRECTORIES[$type] as $relativeDirectory) {
$directory = "{$basePath}/{$portal}/{$relativeDirectory}";
if (! File::isDirectory($directory)) {
continue;
}
foreach (File::allFiles($directory) as $file) {
$index[$file->getFilename()] ??= $file->getPathname();
$index[Str::lower($file->getFilename())] ??= $file->getPathname();
}
}
return $index;
}
/**
* @return array<int, string>
*/
private function thumbnailIndex(string $basePath, string $portal): array
{
$directory = "{$basePath}/{$portal}/thumbnails/pressreleaseimage";
if (! File::isDirectory($directory)) {
return [];
}
$index = [];
$priorities = [
'press_image_preview' => 50,
'pressrelease_form' => 40,
'backend_list' => 30,
'press_image_list' => 20,
'press_list_image' => 10,
];
foreach (File::allFiles($directory) as $file) {
if (! preg_match('/-(\d+)\.[^.]+$/', $file->getFilename(), $matches)) {
continue;
}
$legacyImageId = (int) $matches[1];
$thumbnailType = Str::after($file->getPathname(), "{$directory}/");
$thumbnailType = Str::before($thumbnailType, DIRECTORY_SEPARATOR);
$priority = $priorities[$thumbnailType] ?? 0;
$current = $index[$legacyImageId] ?? null;
if (! $current || $priority > ($current['priority'] ?? 0)) {
$index[$legacyImageId] = [
'path' => $file->getPathname(),
'priority' => $priority,
];
}
}
return collect($index)
->map(fn (array $entry): string => $entry['path'])
->all();
}
private function legacyFilename(string $path): string
{
$urlPath = parse_url($path, PHP_URL_PATH);
$filename = basename(rawurldecode((string) ($urlPath ?: $path)));
return Str::of($filename)->trim()->toString();
}
private function isAlreadySynced(?string $currentPath, string $destinationPath, bool $force): bool
{
return ! $force
&& $currentPath === $destinationPath
&& Storage::disk('public')->exists($destinationPath);
}
private function copyToPublicStorage(string $sourcePath, string $destinationPath, bool $force): void
{
if (! $force && Storage::disk('public')->exists($destinationPath)) {
return;
}
$stream = fopen($sourcePath, 'rb');
if ($stream === false) {
throw new \RuntimeException("Quelldatei kann nicht gelesen werden: {$sourcePath}");
}
try {
Storage::disk('public')->put($destinationPath, $stream, 'public');
} finally {
fclose($stream);
}
}
}

View file

@ -16,7 +16,7 @@ class ThemeHelper
'positive' => 'img/logos/portal-logo-positive.svg',
'negative' => 'img/logos/portal-logo-negative.svg',
],
'presseportale' => [
'pressekonto' => [
'positive' => 'img/logos/portal-logo-positive.svg',
'negative' => 'img/logos/portal-logo-negative.svg',
],
@ -100,7 +100,7 @@ class ThemeHelper
{
$config = self::getDomainConfig();
return $config['domain_name'] ?? 'presseportale.test';
return $config['domain_name'] ?? 'pressekonto.test';
}
public static function getDomainUrl(): string
@ -118,12 +118,12 @@ class ThemeHelper
$theme = config('app.theme', 'portal');
$assetUrlMap = [
'portal' => 'https://assets.presseportale.test',
'presseportale' => 'https://assets.presseportale.test',
'portal' => 'https://assets.pressekonto.test',
'pressekonto' => 'https://assets.pressekonto.test',
'presseecho' => 'https://assets.presseecho.test',
'businessportal24' => 'https://assets.businessportal24.test',
];
return $assetUrlMap[$theme] ?? 'https://assets.presseportale.test';
return $assetUrlMap[$theme] ?? 'https://assets.pressekonto.test';
}
}

View file

@ -22,7 +22,7 @@ class MagicLoginLink extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: 'Ihr Login-Link fuer presseportale'
subject: 'Ihr Login-Link fuer pressekonto'
);
}

View file

@ -80,7 +80,7 @@ class AppServiceProvider extends ServiceProvider
config(['app.asset_url' => $assetUrl]);
} catch (\Exception $e) {
// Fallback to default if theme detection fails
config(['app.asset_url' => 'https://assets.presseportale.test']);
config(['app.asset_url' => 'https://assets.pressekonto.test']);
}
}
}

View file

@ -97,7 +97,7 @@ class ThemeServiceProvider extends ServiceProvider
if (app()->environment('local')) {
// Entwicklung: Vite Dev Server mit HMR
$viteDevServerUrl = env('VITE_DEV_SERVER_URL', 'https://assets.presseportale.test');
$viteDevServerUrl = env('VITE_DEV_SERVER_URL', 'https://assets.pressekonto.test');
Vite::useHotFile(public_path('hot'));
config(['app.vite_dev_server_url' => $viteDevServerUrl]);
View::share('viteDevServerUrl', $viteDevServerUrl);

View file

@ -177,6 +177,24 @@ class ImageService
);
}
/**
* Generates and persists missing variants for an existing company logo.
*
* @return array<string, string>
*/
public function generateMissingCompanyLogoVariants(string $relativePath): array
{
$disk = $this->disk();
if (! $disk->exists($relativePath)) {
return [];
}
$extension = strtolower(pathinfo($relativePath, PATHINFO_EXTENSION) ?: 'jpg');
return $this->generateLogoVariants($disk, $relativePath, $extension);
}
private function deleteWithVariants(?string $relativePath, ?array $variants, ?string $diskName = null): void
{
$disk = $diskName ? Storage::disk($diskName) : $this->disk();

View file

@ -13,23 +13,23 @@ return [
*/
'protocol' => env('APP_PROTOCOL', 'https://'),
'domain_portal' => env('APP_PORTAL_NAME', 'presseportale.test'),
'domain_portal' => env('APP_PORTAL_NAME', 'pressekonto.test'),
'domain_presseecho' => env('APP_PRESSEECHO_NAME', 'presseecho.test'),
'domain_businessportal' => env('APP_BUSINESSPORTAL_NAME', 'businessportal24.test'),
'domain_portal_url' => env('APP_PORTAL_URL', 'https://presseportale.test'),
'domain_portal_url' => env('APP_PORTAL_URL', 'https://pressekonto.test'),
'domain_presseecho_url' => env('APP_PRESSEECHO_URL', 'https://presseecho.test'),
'domain_businessportal_url' => env('APP_BUSINESSPORTAL_URL', 'https://businessportal24.test'),
'domains' => [
'portal' => [
'domain_name' => env('APP_PORTAL_NAME', 'presseportale.test'),
'url' => env('APP_PORTAL_URL', 'https://presseportale.test'),
'asset_url' => env('APP_PORTAL_ASSET_URL', 'https://assets.presseportale.test'),
'domain_name' => env('APP_PORTAL_NAME', 'pressekonto.test'),
'url' => env('APP_PORTAL_URL', 'https://pressekonto.test'),
'asset_url' => env('APP_PORTAL_ASSET_URL', 'https://assets.pressekonto.test'),
'theme' => 'main',
'view_prefix' => 'portal',
'assets_dir' => 'build/portal',
'description' => 'Backend Presseportale',
'description' => 'Backend Pressekonto',
'color_scheme' => [
'primary' => env('APP_PORTAL_PRIMARY', '#526266'), //
'secondary' => env('APP_PORTAL_SECONDARY', '#82a0a7'), //
@ -37,14 +37,14 @@ return [
'font' => 'Montserrat',
],
'presseportale' => [
'domain_name' => env('APP_PORTAL_NAME', 'presseportale.test'),
'url' => env('APP_PORTAL_URL', 'https://presseportale.test'),
'asset_url' => env('APP_PORTAL_ASSET_URL', 'https://assets.presseportale.test'),
'theme' => 'presseportale',
'pressekonto' => [
'domain_name' => env('APP_PORTAL_NAME', 'pressekonto.test'),
'url' => env('APP_PORTAL_URL', 'https://pressekonto.test'),
'asset_url' => env('APP_PORTAL_ASSET_URL', 'https://assets.pressekonto.test'),
'theme' => 'pressekonto',
'view_prefix' => 'web',
'assets_dir' => 'build/web',
'description' => 'Hub-Landing presseportale.com (öffentlicher Publisher-Bereich)',
'description' => 'Hub-Landing pressekonto.de (öffentlicher Publisher-Bereich)',
'color_scheme' => [
'primary' => '#1A2540',
'secondary' => '#B07A3A',
@ -52,12 +52,12 @@ return [
'font' => 'Inter Tight',
'brand' => [
'name' => 'presse',
'accent' => 'portale',
'accent' => 'konto',
'tagline_short' => 'Publisher · Hub',
'tagline_long' => 'Der gemeinsame Publisher-Bereich für presseecho und businessportal24 Pressemitteilungen schreiben, redaktionell prüfen lassen, auf beiden Reichweiten veröffentlichen.',
'footer_legal' => '© :year presseportale · Alle Rechte vorbehalten',
'about_label' => 'Über presseportale',
'meta_title' => 'presseportale Publisher-Hub für presseecho und businessportal24',
'footer_legal' => '© :year pressekonto · Alle Rechte vorbehalten',
'about_label' => 'Über pressekonto',
'meta_title' => 'pressekonto Publisher-Hub für presseecho und businessportal24',
'meta_description' => 'Ein Konto, zwei Reichweiten: Pressemitteilungen redaktionell geprüft auf presseecho und businessportal24 gleichzeitig veröffentlichen.',
],
],

View file

@ -25,7 +25,7 @@ return [
|
*/
'dev_url' => env('ASSET_URL', env('VITE_DEV_SERVER_URL', 'https://assets.presseportale.test')),
'dev_url' => env('ASSET_URL', env('VITE_DEV_SERVER_URL', 'https://assets.pressekonto.test')),
/*
|--------------------------------------------------------------------------

View file

@ -20,11 +20,11 @@ class DatabaseSeeder extends Seeder
RolesAndPermissionsSeeder::class,
AdminPresetSeeder::class,
PaymentOptionSeeder::class,
CategorySeeder::class,
// CategorySeeder::class,
]);
$adminUser = User::firstOrCreate([
'email' => 'admin@presseportale.test',
'email' => 'admin@pressekonto.test',
], [
'name' => 'Portal Admin',
'password' => Hash::make('password'),

View file

@ -1,12 +1,12 @@
# Entwicklungskonzept BusinessPortal24, Presseecho & presseportale-Hub Frontend
# Entwicklungskonzept BusinessPortal24, Presseecho & pressekonto-Hub Frontend
> **Stand:** 13. Mai 2026
> **Domains:** `businessportal24.test` / `.com` · `presseecho.test` / `.de` · `presseportale.test` / `.com` (Hub)
> **Theme-Slugs:** `businessportal24` (warm-rotes Editorial) · `presseecho` (grünes Editorial) · `presseportale` (Hub-Blau · Publisher-Landing)
> **Domains:** `businessportal24.test` / `.com` · `presseecho.test` / `.de` · `pressekonto.test` / `.de` (Hub)
> **Theme-Slugs:** `businessportal24` (warm-rotes Editorial) · `presseecho` (grünes Editorial) · `pressekonto` (Hub-Blau · Publisher-Landing)
> **Assets-Dir (geteilt):** `public/build/web/`
> **Ziel:** Editoriales DACH-Pressemitteilungs-Ökosystem mit 1:1-Mockup-Umsetzung. Presseecho nutzt die **gleiche Komponenten-Architektur** wie BP24, der Hub `presseportale.com` ist eine **eigenständige Publisher-Landing** mit klar abgegrenztem Charakter (Hub-Blau + Bernstein, kein Editorial-Feed).
> **Ziel:** Editoriales DACH-Pressemitteilungs-Ökosystem mit 1:1-Mockup-Umsetzung. Presseecho nutzt die **gleiche Komponenten-Architektur** wie BP24, der Hub `pressekonto.de` ist eine **eigenständige Publisher-Landing** mit klar abgegrenztem Charakter (Hub-Blau + Bernstein, kein Editorial-Feed).
Dieses Dokument beschreibt den aktuellen Stand und die wichtigsten Architektur­entscheidungen der BusinessPortal24-, Presseecho- und presseportale-Hub-Frontend-Entwicklung. Es ist die zentrale Anlaufstelle für alle, die im Frontend weiterarbeiten oder neue Seiten ergänzen.
Dieses Dokument beschreibt den aktuellen Stand und die wichtigsten Architektur­entscheidungen der BusinessPortal24-, Presseecho- und pressekonto-Hub-Frontend-Entwicklung. Es ist die zentrale Anlaufstelle für alle, die im Frontend weiterarbeiten oder neue Seiten ergänzen.
---
@ -67,9 +67,9 @@ Request (Host: presseecho.test)
Lokale Domain-Simulation:
- `.env`: `DEV_SIMULATE_DOMAIN=true`, `DEV_SIMULATED_DOMAIN=businessportal24.test|presseecho.test`
- Alternativ: `?theme=businessportal24|presseecho|presseportale` als Query-Parameter
- Alternativ: `?theme=businessportal24|presseecho|pressekonto` als Query-Parameter
> **Hub-Sonderfall (`presseportale.test`):** Diese Domain ist gleichzeitig **Admin-Backend** (Auth/Admin/Customer-Routen, theme = `main`, Build-Dir `build/portal/`) **und** öffentliche **Hub-Landing** (theme = `presseportale`, Build-Dir `build/web/`). In `config/domains.php` existieren beide Einträge (`portal` und `presseportale`) für dieselbe `domain_name`. Der `ThemeServiceProvider` matcht zuerst `portal` (Backend-Standard); für die öffentliche Landing schaltet **`routes/web.php` per `$applyWebDomainConfig('presseportale')` explizit auf das Hub-Theme** um. Auth- und Admin-Routen bleiben unbeeinflusst.
> **Hub-Sonderfall (`pressekonto.test`):** Diese Domain ist gleichzeitig **Admin-Backend** (Auth/Admin/Customer-Routen, theme = `main`, Build-Dir `build/portal/`) **und** öffentliche **Hub-Landing** (theme = `pressekonto`, Build-Dir `build/web/`). In `config/domains.php` existieren beide Einträge (`portal` und `pressekonto`) für dieselbe `domain_name`. Der `ThemeServiceProvider` matcht zuerst `portal` (Backend-Standard); für die öffentliche Landing schaltet **`routes/web.php` per `$applyWebDomainConfig('pressekonto')` explizit auf das Hub-Theme** um. Auth- und Admin-Routen bleiben unbeeinflusst.
### 3.1 Generischer Daten-Provider
@ -138,7 +138,7 @@ Damit das Theme für Presseecho dokumentiert ist, hier der **verbindliche Token-
| Erste Variante | `#1f4d3a → #163a2c` | **zu hell** |
| **Final** | **`#1a3d2e → #122d22`** | **abgenommen** ✅ |
### 3.1.2 presseportale-Hub-Palette (Stand 13.05.2026)
### 3.1.2 pressekonto-Hub-Palette (Stand 13.05.2026)
Der **Hub** ist bewusst eigenständig positioniert: er ist **kein Editorial-Feed**, sondern eine reine Publisher-Landing („Ein Konto zwei Reichweiten"). Er bekommt daher einen ganz eigenen Charakter:
@ -147,7 +147,7 @@ Der **Hub** ist bewusst eigenständig positioniert: er ist **kein Editorial-Feed
* **Akzent:** gedecktes Bernstein `#B07A3A` **bewusst gewählt**, weil weder Orange (BP24) noch Grün (Presseecho). Der Hub steht visuell „zwischen" den beiden Brands.
* **Schrift:** Inter Tight (Standardtext) + JetBrains Mono (Mono) + Source Serif 4 (**nur für Marken-Mentions** der Tochter-Portale, damit typografische Konsistenz zur jeweiligen Brand-Landing erhalten bleibt; im Hub-Fließtext nicht verwendet).
Token-Snapshot aus `resources/css/web/theme-presseportale.css`:
Token-Snapshot aus `resources/css/web/theme-pressekonto.css`:
```css
@theme {
@ -225,12 +225,12 @@ In `config/domains.php` liegt pro Domain ein **`brand`-Block**, der Komponenten
...
],
],
'presseportale' => [ // Hub-Variante (web)
'theme' => 'presseportale',
'pressekonto' => [ // Hub-Variante (web)
'theme' => 'pressekonto',
'brand' => [
'name' => 'presse', // hub-blau
'accent' => 'portale', // bernstein
'footer_legal' => '© :year presseportale · Alle Rechte vorbehalten',
'accent' => 'konto', // bernstein
'footer_legal' => '© :year pressekonto · Alle Rechte vorbehalten',
...
],
],
@ -246,12 +246,12 @@ Die Schreibweise der drei Marken folgt einer einheitlichen Regel: **keine TLD-En
| ------------------ | ------------------------------------------ | ---------------------- | --------------------- |
| `presseecho` | **presse**·*echo* | `#345636` (Forest) | `#9BD5B2` |
| `businessportal24` | **businessportal**·*24* | `#C84A1E` (Orange) | `#F4B098` |
| `presseportale` | **presse**·*portale* | `#B07A3A` (Bernstein) | `#B07A3A` |
| `pressekonto` | **presse**·*konto* | `#B07A3A` (Bernstein) | `#B07A3A` |
**Single Source of Truth:** Die Komponente `<x-web.brand-mark brand="…" />` rendert die Markenschreibung zentral inkl. Span-Splitting, Schriftart und Akzent­farbe. Sie wird überall verwendet, wo eine Marke als Fließtext-Mention erscheint:
* Hub-Komponenten (`hub/top-utility-bar`, `hub/site-header`, `hub/site-footer`, `hub/brand-context-banner`)
* Hub-View `presseportale.blade.php` (Hero-Headline, Architektur-Diagramm, Tarif-Subline, Plattform-Familie, FAQ)
* Hub-View `pressekonto.blade.php` (Hero-Headline, Architektur-Diagramm, Tarif-Subline, Plattform-Familie, FAQ)
* Cross-Brand-Mentions auf BP24-/Presseecho-Landings, falls ergänzt
```blade
@ -417,22 +417,22 @@ Alle Komponenten haben **konsistente Konventionen**:
### 5.4 Hub-Komponenten (`components/web/hub/`)
Der Hub `presseportale.com` hat einen **eigenen, deutlich anderen Charakter** als die beiden Brand-Portale (kein Editorial-Feed, sondern Publisher-Landing) und bekommt daher einen eigenen Komponenten-Namespace. Die Sektionen selbst (Hero, Features, How-it-works, Tarife, Plattform-Familie, Social-Proof, FAQ, CTA) sind als **inline-Markup** in `resources/views/web/presseportale.blade.php` umgesetzt, weil sie page-spezifisch sind.
Der Hub `pressekonto.de` hat einen **eigenen, deutlich anderen Charakter** als die beiden Brand-Portale (kein Editorial-Feed, sondern Publisher-Landing) und bekommt daher einen eigenen Komponenten-Namespace. Die Sektionen selbst (Hero, Features, How-it-works, Tarife, Plattform-Familie, Social-Proof, FAQ, CTA) sind als **inline-Markup** in `resources/views/web/pressekonto.blade.php` umgesetzt, weil sie page-spezifisch sind.
| Datei | Rolle |
| --- | --- |
| `hub/top-utility-bar.blade.php` | Schmale Hub-Blau-Topbar mit Datum, „Publisher-Hub für …"-Brand-Family-Links (rendert `<x-web.brand-mark variant="on-dark" :serif="false">`), Status/Doku/Kontakt. Props: `date`, `siblingPortals` (jetzt Liste mit `brand`-Key statt fixer Strings). |
| `hub/site-header.blade.php` | Wortmark `presse`·`portale` (über `<x-web.brand-mark brand="presseportale" :serif="false">`) + Untertitel „Publisher · Hub", zentrale Primary-Nav (Tarife, So funktioniert es, …), Anmelden + Konto erstellen CTAs. Routes: `login`, `register`. |
| `hub/site-header.blade.php` | Wortmark `presse`·`konto` (über `<x-web.brand-mark brand="pressekonto" :serif="false">`) + Untertitel „Publisher · Hub", zentrale Primary-Nav (Tarife, So funktioniert es, …), Anmelden + Konto erstellen CTAs. Routes: `login`, `register`. |
| `hub/brand-context-banner.blade.php` | **Conditional Banner** unter dem Header greift nur bei `?from=presseecho` oder `?from=businessportal24` und zeigt: „Sie kommen von … Ihr Konto hier funktioniert für beide Portale". Markenname über Brand-Mark (font-serif), „Zurück zu …"-Link nutzt sans-Variante. |
| `hub/site-footer.blade.php` | 4-Spaltiger Hub-Footer (Konto / Plattform / Rechtliches + Brand-Spalte mit Plattform-Familie-Links über Brand-Mark `variant="on-dark"`), Hub-Gradient `linear-gradient(180deg,#1A2540,#0F1729)`. Brand-Block aus `config/domains.php`. |
**Hub-Sektionen als inline-Blade** (in `presseportale.blade.php`):
**Hub-Sektionen als inline-Blade** (in `pressekonto.blade.php`):
1. **Hero** mit Architektur-Diagramm rechts (zentraler Hub-Knoten + Brand-Portal-Karten + Output-Boxen, alles SVG-only).
2. **Was Sie hier können** 3-Karten-Grid (Veröffentlichen / Newsrooms / Reichweite).
3. **So funktioniert es** 4-Step-Ol mit Differenzierungs-Highlight in Schritt 3 (Bernstein-Akzent für „Unsere Qualitätssicherung").
4. **Tarife** 3 Karten (Starter / Standard / Pro mit `.ribbon-recommend`) + breiter Enterprise-Streifen in Hub-Blau.
5. **Hinter presseportale.com** 2-Spalten-Plattform-Familie mit den **Original-Brand-Gradients** der Tochter-Portale (`#1F4D3A→#163A2C` für Presseecho, `#1A1F26→#232A33` für BP24).
5. **Hinter pressekonto.de** 2-Spalten-Plattform-Familie mit den **Original-Brand-Gradients** der Tochter-Portale (`#1F4D3A→#163A2C` für Presseecho, `#1A1F26→#232A33` für BP24).
6. **Aktive Newsrooms** Prose-Auflistung statt Logo-Wall + kompakte Stats-Sidebar.
7. **FAQ** CSS-only-Accordion (`<details>` + `.faq-chev`) mit 8 Fragen, eine offen by default.
8. **CTA-Wiederholung** + Footer.
@ -496,12 +496,12 @@ Drei spiegelbildliche Test-Szenarien:
2. **`feed only shows published presseecho content`** Portal-Trennung gespiegelt: Presseecho + Both sichtbar, BP24-Only und Drafts nicht.
3. **`shows most read releases in the sidebar`** Hits-Sortierung.
### Datei: `tests/Feature/Web/PresseportaleHubHomeTest.php`
### Datei: `tests/Feature/Web/PressekontoHubHomeTest.php`
Fünf Test-Szenarien rund um die Hub-Landing:
1. **`renders the publisher landing shell`** prüft alle Hauptsektionen sind sichtbar (Publisher-Hub, Was Sie hier können, So funktioniert es, Vier Schritte, Tarife, Starter, Standard, Pro, Enterprise, Hinter presseportale.com, Plattform im Überblick, Häufige Fragen, Loslegen, Alle Systeme betriebsbereit).
2. **`loads the hub theme assets, not portal admin`** stellt sicher, dass `theme-presseportale` aus dem Manifest geladen wird (nicht `theme-businessportal24` oder `theme-presseecho`).
1. **`renders the publisher landing shell`** prüft alle Hauptsektionen sind sichtbar (Publisher-Hub, Was Sie hier können, So funktioniert es, Vier Schritte, Tarife, Starter, Standard, Pro, Enterprise, Hinter pressekonto.de, Plattform im Überblick, Häufige Fragen, Loslegen, Alle Systeme betriebsbereit).
2. **`loads the hub theme assets, not portal admin`** stellt sicher, dass `theme-pressekonto` aus dem Manifest geladen wird (nicht `theme-businessportal24` oder `theme-presseecho`).
3. **`hides the brand-context banner without a from parameter`** Default-Aufruf zeigt keinen „Sie kommen von …"-Banner.
4. **`shows the brand-context banner when arriving from presseecho`** `?from=presseecho` triggert den Banner inkl. Link „Zurück zu presseecho.de".
5. **`shows the brand-context banner when arriving from businessportal24`** `?from=businessportal24` triggert den Banner für BP24.
@ -563,9 +563,9 @@ vendor/bin/pint --dirty --format agent
| 13 | 12.05.2026 | Brand-Konfiguration in `config/domains.php` pro Domain (`brand.name`, `brand.accent`, `brand.tagline_*`, `brand.newsletter_topics`, `brand.footer_legal`, `brand.about_label`) `site-header`, `site-footer`, `newsletter-strip` lesen daraus | ✅ |
| 14 | 12.05.2026 | `press-release-feed`-Volt-Component portal-agnostisch (Prop `:portal`); Aufruf vom View-Layer aus | ✅ |
| 15 | 12.05.2026 | `PresseechoHomeTest` analog zu `Businessportal24HomeTest` (3 Szenarien, RefreshDatabase) | ✅ |
| 16 | 13.05.2026 | **presseportale-Hub-Landing live**: neues Web-Theme `presseportale` (Hub-Blau + Bernstein, `theme-presseportale.css`), eigener Komponenten-Namespace `components/web/hub/` (Top-Bar, Site-Header, Brand-Context-Banner, Site-Footer), Hub-View `web/presseportale.blade.php` mit Hero/Architektur-Diagramm, Features, How-it-works, Tarife (Starter/Standard/Pro+Ribbon, Enterprise-Streifen), Plattform-Familie, Aktive-Newsrooms, FAQ-Accordion, CTA. `routes/web.php` schaltet für `presseportale.test` auf das Hub-Theme um. Root-Route in `routes/admin.php` entfernt (Layout referenziert jetzt `route('dashboard')`). | ✅ |
| 17 | 13.05.2026 | `PresseportaleHubHomeTest` (5 Szenarien inkl. Brand-Context-Banner-Conditional). Vite-Config + ThemeHelper + `web-master`-Fonts (Inter Tight + JetBrains Mono ohne Serif) für `presseportale` ergänzt. | ✅ |
| 18 | 13.05.2026 | **Brand-Mark-Konvention etabliert** (Feintuning Marken-Schreibweise): keine TLD am Marken­schriftzug, Akzent farblich vom Basis-Wort abgesetzt. Single Source of Truth `<x-web.brand-mark>` (Marken-Tabelle inkl. Standard- und On-Dark-Akzentfarben, Serif/Sans-Switch). `config/domains.php` umgestellt (`presseecho`: `name=presse`/`accent=echo`; `presseportale`: `name=presse`/`accent=portale`; Footer-Legal & Meta-Texte ohne TLD). Hub-Komponenten und Hub-View durchgehend auf Brand-Mark migriert (Top-Utility-Bar, Site-Header, Brand-Context-Banner, Site-Footer, Hero-Headline, Architektur-Diagramm, Tarif-Subline, Plattform-Familie, FAQ). Hub-Theme bekommt Source Serif 4 als `--font-serif` (für Marken-Mentions) Bunny-Font-Loader erweitert. **+1 neuer Test `uses the brand-mark splitting without TLDs`**; alle 12 Web-Tests grün. | ✅ |
| 16 | 13.05.2026 | **pressekonto-Hub-Landing live**: neues Web-Theme `pressekonto` (Hub-Blau + Bernstein, `theme-pressekonto.css`), eigener Komponenten-Namespace `components/web/hub/` (Top-Bar, Site-Header, Brand-Context-Banner, Site-Footer), Hub-View `web/pressekonto.blade.php` mit Hero/Architektur-Diagramm, Features, How-it-works, Tarife (Starter/Standard/Pro+Ribbon, Enterprise-Streifen), Plattform-Familie, Aktive-Newsrooms, FAQ-Accordion, CTA. `routes/web.php` schaltet für `pressekonto.test` auf das Hub-Theme um. Root-Route in `routes/admin.php` entfernt (Layout referenziert jetzt `route('dashboard')`). | ✅ |
| 17 | 13.05.2026 | `PressekontoHubHomeTest` (5 Szenarien inkl. Brand-Context-Banner-Conditional). Vite-Config + ThemeHelper + `web-master`-Fonts (Inter Tight + JetBrains Mono ohne Serif) für `pressekonto` ergänzt. | ✅ |
| 18 | 13.05.2026 | **Brand-Mark-Konvention etabliert** (Feintuning Marken-Schreibweise): keine TLD am Marken­schriftzug, Akzent farblich vom Basis-Wort abgesetzt. Single Source of Truth `<x-web.brand-mark>` (Marken-Tabelle inkl. Standard- und On-Dark-Akzentfarben, Serif/Sans-Switch). `config/domains.php` umgestellt (`presseecho`: `name=presse`/`accent=echo`; `pressekonto`: `name=presse`/`accent=konto`; Footer-Legal & Meta-Texte ohne TLD). Hub-Komponenten und Hub-View durchgehend auf Brand-Mark migriert (Top-Utility-Bar, Site-Header, Brand-Context-Banner, Site-Footer, Hero-Headline, Architektur-Diagramm, Tarif-Subline, Plattform-Familie, FAQ). Hub-Theme bekommt Source Serif 4 als `--font-serif` (für Marken-Mentions) Bunny-Font-Loader erweitert. **+1 neuer Test `uses the brand-mark splitting without TLDs`**; alle 12 Web-Tests grün. | ✅ |
| 19 | 12.05.2026 | **Aktuell offen:** Detailseite, Branchenseite, Veröffentlichen-Landing für BP24 + Presseecho. Hub-Folgeseiten (Konto-Erstellen-Flow als Landing, Tarif-Detail, Doku-Hub) ebenfalls offen. | 🟡 |
---

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,216 @@
# Phase 0 — Design-Tokens vereinheitlichen
> **Ziel**: Single Source of Truth für alle Design-Tokens (Farben, Fonts,
> Radii, Schatten). Sowohl Hub-Build als auch Portal-Build beziehen ihre
> Werte aus derselben Datei. **Visuell ändert sich noch nichts.**
**Status**: ✅ abgeschlossen am 2026-05-19
**Risiko**: sehr niedrig
**Aufwand (tatsächlich)**: ~½ Tag
## Ergebnis-Check (2026-05-19)
- Single Source of Truth liegt in `resources/css/shared/design-tokens.css`.
- Web-Build und Portal-Build importieren sie beide.
- **Visuelle Unverändertheit verifiziert**:
- Hub (`pressekonto.test/`, `/login`, `/register`) — unverändert.
- Portal (`/dashboard`) — FluxUI-Defaults bleiben dominant
(`--font-sans: "Instrument Sans"`, Zinc-Palette, `#3ea3dc`-Akzent).
- Build-Sizes:
- `theme-pressekonto: 193 kB` (vorher 189 kB · +4 kB für neue Tokens)
- `theme-presseecho`, `theme-businessportal24`: praktisch unverändert
- `portal: 408 kB` (vorher 397 kB · +12 kB für zusätzlich bereitgestellte Token-Vars im `:root`)
- Details: `PROGRESS.md` (Eintrag vom 2026-05-19).
## Warum
Heute leben Tokens an zwei Orten:
- `resources/css/web/theme-pressekonto.css` — Hub-Tokens (Hub-Blau,
Bernstein, Buchpapier, Inter Tight)
- `resources/css/portal.css` — Portal-Tokens (Zinc, `#3ea3dc`, Instrument
Sans)
Solange diese parallel gepflegt werden, **driften** sie auseinander. Wir
ziehen die gemeinsame Wahrheit in eine eigene Datei und referenzieren
sie aus beiden Welten.
## Liefergegenstand
```
resources/css/shared/
└── design-tokens.css ← NEU
```
Inhalt: alle `--color-*`, `--font-*`, `--radius-*`, `--shadow-*` Tokens,
die in Hub und Portal gleichermaßen gelten. Strukturiert als
`@theme`-Block, sodass Tailwind v4 die Variablen sowohl als
CSS-Custom-Properties als auch als Tailwind-Utility-Klassen erkennt.
## Schritte
### 1. Token-Inventur aus `theme-pressekonto.css`
Die folgenden Tokens werden aus `theme-pressekonto.css` extrahiert und
nach `shared/design-tokens.css` verschoben:
#### Surfaces
- `--color-bg`, `--color-bg-elev`, `--color-bg-rule`, `--color-bg-rule-strong`
- `--color-bg-dark`, `--color-bg-card`, `--color-bg-card-warm`,
`--color-bg-card-warm-border`, `--color-bg-card-warm-hover`,
`--color-bg-card-warm-rule`
#### Hub-Palette
- `--color-hub`, `--color-hub-2`, `--color-hub-3`
- `--color-hub-soft`, `--color-hub-soft-2`, `--color-hub-line`
- `--color-topbar`, `--color-topbar2`, `--color-topbar-deep`
#### Akzent (Bernstein)
- `--color-accent`, `--color-accent-deep`, `--color-accent-soft`,
`--color-accent-warm`
#### Ink (Anthrazit)
- `--color-ink`, `--color-ink-2`, `--color-ink-3`, `--color-ink-4`
- `--color-ink-on-dark`, `--color-ink-on-dark-2`, `--color-ink-on-dark-3`,
`--color-ink-on-dark-muted`, `--color-ink-on-dark-rule`
#### Brand-Aliase + Status
- `--color-brand`, `--color-brand-deep`, `--color-brand-soft`
- `--color-live`, `--color-gain`, `--color-loss`, `--color-ok`
#### Editorial / Cards
- `--color-card-warm-cat`, `--color-card-warm-title`,
`--color-feature-line`, `--color-feature-dot`
#### Status (für KPI-Cards / Badges — laut Mockup ergänzen)
- `--color-warn` `#A87A1F`, `--color-warn-soft` `#F6EAC8`
- `--color-err` `#A8331F`, `--color-err-soft` `#F4DAD2`
- `--color-ok-soft` `#E2F1E5`
#### Fonts
- `--font-sans` (Inter Tight)
- `--font-serif` (Source Serif 4 — nur für Brand-Mark)
- `--font-mono` (JetBrains Mono)
#### Layout
- `--container-layout: 1280px`
#### Radii (laut Mockup)
- `--radius-xs: 3px`, `--radius-sm: 4px`, `--radius-md: 6px`,
`--radius-lg: 8px`
#### Schatten (laut Mockup + Hub-Login)
- `--shadow-soft`: leicht warm, für Cards
- `--shadow-card`: Standard-Card-Schatten
- `--shadow-card-hover`: Hover-Stufe
- `--shadow-auth`: weiche Glocke unter Auth-Card
### 2. Datei `resources/css/shared/design-tokens.css` anlegen
Aufbau:
```css
/**
* Hub × FluxUI — Gemeinsame Design-Tokens
*
* Single Source of Truth für Hub-Frontend (build/web) und Portal-Backend
* (build/portal). Beide CSS-Builds @import diese Datei.
*
* Token-Names sind STABIL — Werte können sich ändern (z.B. Dark Mode in
* Phase 5), Namen nicht.
*/
@theme {
/* Surfaces */
--color-bg: #f6f4ef;
--color-bg-elev: #fbfaf6;
/* … alle Tokens aus der Inventur … */
}
/* Dark Mode (vorbereitet, in Phase 5 finalisiert) */
@media (prefers-color-scheme: dark) {
@theme {
/* Spätere Dark-Werte */
}
}
```
### 3. `theme-pressekonto.css` refactoren
Die `@theme {}`-Definitionen werden durch einen
`@import "../shared/design-tokens.css";` ersetzt. Nur die `@layer
components {}`-Klassen (`.eyebrow`, `.auth-card`, `.field-input`, etc.)
bleiben in `theme-pressekonto.css`.
`:root { … --background, --primary … }`-HSL-Variablen für Legacy-Komponenten
bleiben ebenfalls hier (sind Portal-unspezifisch).
### 4. `portal.css` minimal vorbereiten (noch keine Werte übernehmen)
In Phase 0 importieren wir die Token-Datei in `portal.css`, **aber lassen
das alte `@theme`-Setup mit Zinc/Accent zunächst stehen**. Damit:
- Beide Welten greifen technisch auf die gleiche Datei zu
- Aber Portal bleibt visuell unverändert (Zinc-Palette gewinnt durch
Reihenfolge im `@theme`)
In Phase 1 wird das Zinc-Setup dann **gezielt durch Hub-Werte ersetzt**.
### 5. Build & Verifikation
```bash
npm run build:web # → erzeugt theme-pressekonto.css ohne Drift
npm run build:portal # → erzeugt portal.css unverändert
```
Erwartung:
- Hub-Landing rendert visuell **identisch** wie vorher
- Hub-Auth-Pages rendern visuell **identisch** wie vorher
- Portal rendert visuell **identisch** wie vorher
Smoke-Test (kein neues Test-Schreiben nötig):
```bash
php artisan tinker --execute '
$urls = [
"https://pressekonto.test/",
"https://pressekonto.test/login",
"https://pressekonto.test/dashboard",
];
foreach ($urls as $u) {
echo $u . " => " .
app(\Illuminate\Contracts\Http\Kernel::class)
->handle(\Illuminate\Http\Request::create($u, "GET"))
->getStatusCode() . "\n";
}'
```
Alle 3 URLs müssen weiterhin `200` liefern (für `/dashboard` ggf.
`302`-Redirect, je nach Auth-Status — beides ist okay, solange kein `500`).
## Akzeptanzkriterien
- [ ] `resources/css/shared/design-tokens.css` existiert mit allen Hub-Tokens
- [ ] `theme-pressekonto.css` importiert die Token-Datei und enthält
keine doppelten `--color-*`-Definitionen mehr
- [ ] `portal.css` importiert die Token-Datei (Werte werden in Phase 1 genutzt)
- [ ] `npm run build:web` und `npm run build:portal` laufen ohne Fehler durch
- [ ] Hub-Landing, Hub-Auth und Portal-Login visuell **unverändert**
- [ ] Pint passed (`vendor/bin/sail bin pint --dirty --format agent`)
## Risiken & Fallstricke
- **Tailwind v4 + `@theme`**: Mehrere `@theme {}`-Blöcke in importierten
Dateien werden zusammengeführt. Das funktioniert, solange die
Token-Namen eindeutig sind.
- **Reihenfolge der Imports**: Tokens müssen **vor** den
`@layer components {}`-Definitionen importiert werden, sonst
greifen die Variablen in den Komponenten nicht.
- **Portal-Tailwind-Config**: `tailwind.portal.config.js` darf die
Token-Datei nicht ausschließen. `@source`-Direktiven prüfen.
## Was Phase 0 NICHT macht
- Portal sieht **noch nicht** wie der Hub aus — das ist Phase 1
- Keine Änderung am Sidebar-Layout, am Logo oder am Dashboard
- Keine Dark-Mode-Aktivierung (nur vorbereitet)

View file

@ -0,0 +1,290 @@
# Phase 1 — Portal-Shell ans Hub-Design angleichen
> **Ziel**: Sidebar, Topbar und Layout-Container des Portals sehen aus
> wie das Mockup `User Dashboard presseportale.html`. Inhalt der
> einzelnen Pages bleibt unverändert — wir tauschen nur die Shell.
**Status**: ✅ abgeschlossen am 2026-05-19
**Risiko (tatsächlich)**: niedrig — keine Test-Regressionen
**Aufwand (tatsächlich)**: ~½ Tag (kleiner als geschätzt, weil Topbar
auf Phase 2 verschoben wurde)
## Ergebnis-Check (2026-05-19)
**Umgesetzt**:
- `portal.css` mit Hub-Token-Bridge, Inter-Tight-Font, Zinc→Hub-Mapping,
FluxUI-Overrides für Sidebar / Navlist / Buttons / Cards.
- Sidebar mit Brand-Mark + Eyebrow „Publisher · Hub", neuem Hub-Stil-
Testmodus-Block, ohne Starter-Kit-Resources-Block.
- Customer-Banner (`app.blade.php`) im Hub-Soft-Look mit Hub-Pille.
- `class="dark"` aus Sidebar-Layout entfernt — Light Mode ist Default.
- Font-Wechsel auf Bunny: `inter-tight + jetbrains-mono + source-serif-4`.
**Verschoben auf Phase 2**:
- Eigene Topbar mit Breadcrumb + Bridge-Row + Search + „Neue Mitteilung"-CTA
(lebt sinnvoller im Customer-Dashboard-Kontext).
- Konto-Switcher als Sidebar-Header oben (statt User-Menü unten).
**Build-Sizes**:
- `portal.css: 409.03 kB` (vorher 408.89 kB · +0.14 kB · weit unter dem
10 %-Limit aus den Akzeptanzkriterien).
**Tests**:
- Auth-Test-Suite Vergleich (Stash vs Phase 1): `8 failed, 15 passed`
in beiden Ständen — 0 zusätzliche Regressionen durch Phase 1.
- Zwei Tests im Zuge des Login-Fixes angepasst (Admin-Rolle bzw.
`terms_accepted: true`), siehe `PROGRESS.md` Eintrag „Phase 1".
**Details**: `PROGRESS.md` (Eintrag vom 2026-05-19, Abschnitt „Phase 1").
## Sichtbarer Mehrwert
Nach Phase 1 sieht der eingeloggte User:
- Eine **warme Sidebar** im Hub-Stil mit Brand-Wortmarke statt
Starter-Kit-Logo
- Eine **schlanke Topbar** mit Breadcrumb, Bridge-Row, Search, Notification,
"Neue Mitteilung"-Button
- Den **Testmodus-Block** (Impersonation) als Hub-Karten-Element
- Den **Konto-Switcher** als oberen Sidebar-Header
Innenleben (Tabellen, Formulare, Cards) bleiben FluxUI — wirken aber
durch Token-Anpassung **automatisch passender**.
## Liefergegenstand
### Geänderte Dateien
| Datei | Änderung |
|-------|----------|
| `resources/css/portal.css` | Zinc → Hub-Palette, Font auf Inter Tight, `--color-accent` auf Hub-Blau, FluxUI-Overrides |
| `resources/views/components/layouts/app/sidebar.blade.php` | Brand-Mark statt App-Logo, Eyebrow „Publisher · Hub", Sidebar-Design am Mockup orientiert, Testmodus-Block neu |
| `resources/views/partials/head.blade.php` | Font-Wechsel (Bunny: inter-tight + jetbrains-mono statt instrument-sans) |
| `resources/views/components/layouts/app.blade.php` | Customer-Banner ggf. an neues Design anpassen |
### Vermutlich gelöscht
| Datei | Begründung |
|-------|------------|
| `resources/views/components/layouts/app/header.blade.php` | Wird laut Inventur nirgends referenziert |
| `resources/views/partials/admin-head.blade.php` | Legacy, im Code nicht eingebunden |
| `resources/views/components/app-logo.blade.php` | Wird durch `brand-mark` ersetzt |
| `resources/views/components/app-logo-icon.blade.php` | Wird durch `brand-mark` ersetzt |
Vor Löschung: per `rg "x-app-logo"` und `rg "auth.split|auth.card"`
prüfen, dass nichts mehr referenziert wird.
## Schritte
### 1. `portal.css` — Token-Bridge zu FluxUI
```css
@import "tailwindcss";
@import "../../vendor/livewire/flux/dist/flux.css";
@import "./shared/design-tokens.css"; /* aus Phase 0 */
@source '../views';
@source '../../vendor/livewire/flux-pro/stubs/**/*.blade.php';
@source '../../vendor/livewire/flux/stubs/**/*.blade.php';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* FluxUI-Akzent auf Hub-Blau */
--color-accent: var(--color-hub);
--color-accent-content: var(--color-hub);
--color-accent-foreground: #ffffff;
/* Zinc auf warmes Buchpapier mappen */
--color-zinc-50: var(--color-bg-elev); /* #FBFAF6 */
--color-zinc-100: var(--color-bg); /* #F6F4EF */
--color-zinc-200: var(--color-bg-rule); /* #E2DDD0 */
--color-zinc-300: #cfc8b5;
--color-zinc-400: var(--color-ink-4); /* #8A918D */
--color-zinc-500: var(--color-ink-3); /* #5A6360 */
--color-zinc-600: var(--color-ink-2); /* #3A413D */
--color-zinc-700: var(--color-ink); /* #1A1F1C */
--color-zinc-800: var(--color-hub-2); /* #243152 */
--color-zinc-900: var(--color-hub); /* #1A2540 */
--color-zinc-950: var(--color-topbar-deep); /* #0F1729 */
}
```
#### FluxUI-spezifische Overrides
```css
/* Navlist — Hub-Stil mit Active-Strip links */
[data-flux-navlist-item][data-active="true"] {
background: var(--color-hub-soft);
color: var(--color-hub);
font-weight: 600;
position: relative;
}
[data-flux-navlist-item][data-active="true"]::before {
content: "";
position: absolute;
left: 0;
top: 6px;
bottom: 6px;
width: 2px;
background: var(--color-hub);
border-radius: 0 2px 2px 0;
}
/* Buttons — Hub-Primär */
[data-flux-button][data-variant="primary"] {
background: var(--color-hub);
color: #ffffff;
}
[data-flux-button][data-variant="primary"]:hover {
background: var(--color-hub-2);
}
/* Sidebar-Section-Headings */
[data-flux-navlist] [data-flux-navlist-group-heading] {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-ink-4);
}
/* Cards bekommen Hub-Buchpapier statt Zinc */
[data-flux-card] {
background: var(--color-bg-card);
border-color: var(--color-bg-rule);
}
```
> **Hinweis**: Die exakten `[data-flux-*]`-Attribute werden beim Bauen
> per Dev-Tools verifiziert. Die hier gezeigten sind die wahrscheinlichsten
> laut FluxUI-Doku.
### 2. `partials/head.blade.php` — Font wechseln
```diff
- <link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600"
+ <link href="https://fonts.bunny.net/css?family=inter-tight:400,500,600,700|jetbrains-mono:400,500,600|source-serif-4:400,500,600,700"
rel="stylesheet" />
```
### 3. Sidebar neu — `components/layouts/app/sidebar.blade.php`
Aufbau **strikt am Mockup** orientiert (s. `dev/frontend/tailwind_v3/User Dashboard presseportale.html`):
#### Sidebar-Aufbau (oben → unten)
1. **Brand-Block** (`px-5 pt-6 pb-5`):
- `<x-web.brand-mark brand="pressekonto" :serif="false" />` (19 px, bold)
- Eyebrow „Publisher · Hub" darunter
- **Konto-Switcher-Button** mit Avatar (Initialen), Name, Firma — als
`<flux:dropdown>`-Trigger mit Custom-Stil
2. **Navigation** (`px-3 flex-1`):
- Section „Mein Bereich": Übersicht, Meine Pressemitteilungen (mit Counter-Badge),
Firmen, Buchungen & Add-ons, Statistiken (disabled, „bald")
- Section „Finanzen": Credits & Tarif (bald), Rechnungen, Zahlungsarten (bald)
- Section „Konto": Profil, Sicherheit, API & Integrationen, Benachrichtigungen (bald)
- Section „Administration" (nur für Admins/Editoren): Press-Releases, Companies, Users, Roles, etc.
3. **Testmodus-Block** (`px-4 pb-4`) — wenn Impersonation aktiv:
- Hub-blaues Panel mit Bernstein-Eyebrow „Testmodus aktiv"
- „Zurück zum Admin"-Button (weiß auf Hub-Blau)
4. **Resources-Block** (`px-3 pb-5 border-t`):
- Optional: Tailwind CSS, Hero Icons, Flux UI, Repository
- Im Live-Portal vermutlich weglassen oder durch eigene Hilfe-Links ersetzen
#### Komponenten-Strategie
Wo immer möglich **FluxUI-Komponenten** verwenden:
- `<flux:sidebar>` als Wrapper
- `<flux:navlist>` für Section-Gruppen
- `<flux:navlist.item>` für Einträge, `data-active`-Markierung übernimmt CSS-Override
- `<flux:dropdown>` für Konto-Switcher
- `<flux:badge>` für Counter
Wo FluxUI nicht passt (z.B. Konto-Switcher-Header mit Avatar+Name+Firma+Chevron):
**Custom Blade** in `<x-portal.account-switcher>` als wiederverwendbare Komponente.
### 4. `app.blade.php` — Customer-Banner Hub-Stil
Der Banner „User Backend" (für Customer-Rolle) wird visuell ans Hub-Design
angeglichen — Hub-Soft-Hintergrund, Hub-Blau-Eyebrow, Firma-Switcher als
Pille.
### 5. `class="dark"` entfernen
In allen Auth- und App-Layouts:
```diff
- <html lang="..." class="dark">
+ <html lang="...">
```
FluxUI Appearance-Switcher in den Settings übernimmt die Steuerung.
Dark-Mode-Werte landen in Phase 5 in `design-tokens.css`.
### 6. Build & Visual-Check
```bash
npm run build:portal
```
Öffnen und visuell prüfen:
- `https://pressekonto.test/dashboard` (Admin-Dashboard)
- `https://pressekonto.test/admin/me` (Customer-Dashboard)
- `https://pressekonto.test/settings/profile`
- `https://pressekonto.test/admin/companies`
- `https://pressekonto.test/admin/press-releases`
Erwartung:
- Sidebar wie Mockup
- Topbar mit Breadcrumb + Aktionen
- Inhalt unverändert, aber Tabellen/Cards/Buttons in Hub-Tonart
- Keine kaputten Layouts
## Akzeptanzkriterien
- [ ] Sidebar nutzt `<x-web.brand-mark brand="pressekonto" />` statt `x-app-logo`
- [ ] Sidebar-Sections und Active-Item entsprechen visuell dem Mockup
- [ ] Topbar hat Breadcrumb, Search, Notification, „Neue Mitteilung"-CTA
- [ ] Font im Portal ist **Inter Tight** (sichtbar im DevTools)
- [ ] FluxUI-Buttons (Primary) sind **Hub-Blau**, nicht mehr `#3ea3dc`
- [ ] FluxUI-Tabellen sehen sauber aus mit Buchpapier-Hintergrund
- [ ] `class="dark"` ist aus allen Layouts entfernt
- [ ] Alle bestehenden Routen `/dashboard`, `/admin/*`, `/admin/me/*`,
`/settings/*` rendern Status 200
- [ ] Pint & vorhandene Tests bleiben grün
- [ ] Page-Last-Vergleich: `portal.css`-Größe darf um max. 10 % wachsen
## Risiken & Fallstricke
- **FluxUI-Selektoren ändern sich**: `[data-flux-*]`-Attribute sind nicht
öffentlich dokumentierte API, sondern Implementation-Detail. Bei
FluxUI-Update kann ein Override brechen. Mitigation: Selektoren so
spezifisch wie nötig, so generisch wie möglich; gut kommentieren.
- **Zinc-Remapping kann Side-Effects haben**: Stellen, die hardcoded
`zinc-700` für Text-Farben verwenden, werden plötzlich Hub-Blau.
Mitigation: nach dem Build kritische Pages durchklicken; gegebenenfalls
einzelne Stellen explizit auf `text-ink` umstellen.
- **Tailwind v4 Custom-Properties**: Reihenfolge im `@theme`-Block ist
wichtig — Tokens müssen vor Overrides definiert sein.
- **Mobile Sidebar**: Das Mockup zeigt nur Desktop. `flux:sidebar` hat
einen eingebauten Mobile-Toggle — der bleibt erhalten und wird visuell
angeglichen.
## Was Phase 1 NICHT macht
- Dashboards (`admin.dashboard`, `customer.dashboard`) bekommen noch
**kein** Stat-Card-Strip-Redesign — das ist Phase 2 + 3
- Listen-Pages werden nicht überarbeitet — passt automatisch durch
Token-Anpassung „gut genug" bis Phase 4
- Dark Mode wird nicht aktiv ausgeliefert — Token-Werte werden vorbereitet,
aber bleiben in Phase 5
## Review-Punkt
Nach Phase 1 wird Frank/Du visuell drüberschauen und entscheiden:
- Welche Detail-Pages priorisiert werden (Phase 4)
- Ob Customer-Dashboard (Phase 2) direkt danach kommt
- Ob das Auth-Layout im Portal (`auth.split`, `auth.card`) entfernt werden kann

View file

@ -0,0 +1,244 @@
# Weitere Phasen — Outline
> Übersicht über Phasen 26. Diese werden detailliert ausgearbeitet,
> sobald Phase 0+1 abgenommen sind. Jede Phase bekommt ein eigenes
> `0X-PHASE-N-NAME.md`, wenn sie aktiv wird.
---
## Phase 2 — Customer-Dashboard auf Mockup-Stil
**Status**: ✅ **abgeschlossen** (de facto in Phase 1 +
verfeinert in 4J) · **Aufwand**: ~½ Tag · **Risiko**: niedrig
> Während der Phase-1-Migration wurden Page-Header, KPI-Reihe,
> 2-Spalten-Grids, `<x-portal.stat-card>`,
> `<x-portal.hint-card>` und Brand-Bridge-Dark-Card bereits
> umgesetzt. Phase 4J hat die Recent-PM-Liste mit Portal-Pills
> + PM-ID-Sub auf den finalen 4H/4I-Pattern-Stand gebracht.
> Match zum Mockup ≥ 95 %.
### Ziel
`livewire/customer/dashboard.blade.php` matched das Mockup
`User Dashboard presseportale.html` zu ≥ 90 %.
### Bausteine
- **Page-Header** mit „Mein Dashboard" + Begrüßung + Firma-zugeordnet-Pille
- **KPI-Reihe** (4 Stat-Cards mit linkem Farb-Strip)
- Gesamt (Hub-Blau-Strip)
- Veröffentlicht (Grün-Strip, `is-ok`)
- In Prüfung (Bernstein-Strip, `is-warn`)
- Entwürfe (Grau-Strip, `is-muted`)
- **2-Spalten-Grid**:
- Links: Empty-State / Liste „Meine letzten Pressemitteilungen"
- Rechts: „Datenqualität" mit Progress-Bars (Profil-Vollständigkeit,
Rechnungsadresse)
- **Unten**:
- Links: Firmen-Slots
- Rechts: Brand-Bridge-Dark-Card (presseecho + businessportal24)
### Neue Blade-Components
- `<x-portal.stat-card>` — Stat-Card mit Slot + Strip-Variante
- `<x-portal.hint-card>` — Datenqualitäts-Hint mit Icon + Progress-Bar
- `<x-portal.bridge-card>` — Dunkle Brand-Bridge-Card
→ Diese landen in `resources/views/components/portal/`.
---
## Phase 3 — Admin-Dashboard konsistent
**Status**: ✅ **abgeschlossen** (in Phase 1 + 4J)
· **Aufwand**: ~½ Tag · **Risiko**: niedrig
> Admin-Dashboard nutzt seit Phase 1 dasselbe Vokabular wie
> das Customer-Dashboard: Page-Header, 5-KPI-Reihe mit
> `<x-portal.stat-card>`, 2-Spalten-Grid (Recent + Pending
> Reviews), Newsletter `panel-warm`, Quick-Actions-Grid,
> Footer. Phase 4J hat Portal-Pills, PM-ID-Sub und
> Inline-Action „Prüfen →" mit `is-row-warn`-Tinting für
> die Review-Queue ergänzt.
---
## Phase 4 — Listen-/Detail-Pages durchgehen
**Status**: ✅ **komplett abgeschlossen** (4A4J)
· **Aufwand**: ~35 Tage gesamt · **Risiko**: niedrig
> Alle 10 Sub-Päckchen (4A4J) sind grün, Build + Pint
> sauber, alle relevanten Tests bestehen
> (230/231 gesamt — der eine Fail ist der pre-existing
> `ApiDocumentationTest` ohne Bezug zur UI-Migration).
> Das gesamte Backend (Admin + Customer) nutzt damit
> dieselbe Design-Sprache: Page-Header mit
> Eyebrow/Badge/Subtitle, `<x-portal.stat-card>`-KPI-Reihen,
> `article.panel` als Container, `flux:table` mit Hub-Padding
> + Sortierung, `flux:card` nur wo Test-Verträge sie noch
> brauchen, sowie die Mockup-Patterns aus 4H/4I/4J
> (Saved-Views-Tabs, Active-Chips, Portal-Pills,
> Inline-Actions, Row-Tinting, 3-stufige Empty-States).
### Ziel
Alle Volt-Pages im Admin- und Customer-Bereich nutzen denselben Hub-Stil.
### Vorgehen
- Pro Page: 1530 min
- In Päckchen geteilt:
- **4A** = Press-Releases Listen (Admin + Customer) — ✅ **abgeschlossen**
siehe `07-PHASE-4A-PRESS-RELEASES-LISTEN.md`
- **4B** = Press-Releases Detail/Show (Admin + Customer) — ✅ **abgeschlossen**
siehe `08-PHASE-4B-PRESS-RELEASES-DETAIL.md`
- **4C** = Press-Releases Forms (create/edit, Admin + Customer) — ✅ **abgeschlossen**
siehe `09-PHASE-4C-PRESS-RELEASES-FORMS.md`
- **4D** = Companies (`admin.companies.*`) — ✅ **abgeschlossen**
siehe `10-PHASE-4D-COMPANIES.md`
- **4E** = Profile/Settings (`settings.*`, `customer.profile`, `customer.security`) — ✅ **abgeschlossen**
siehe `11-PHASE-4E-PROFILE-SETTINGS.md`
- **4F** = Restliche Admin-Bereiche (contacts, categories, presets, footer-codes, users, roles, newsletter, reports, invoices, coupons, payments, portal-switcher) — ✅ **abgeschlossen**
siehe `13-PHASE-4F-ADMIN-REST.md`
- **4G** = Restliche Customer-Bereiche (invoices, tokens, bookings, company-switcher, me/press-kits/*) — ✅ **abgeschlossen**
siehe `12-PHASE-4G-CUSTOMER-PORTAL.md`
- **4H** = Customer „Meine Pressemitteilungen" auf Mockup-Stil (Counter-Strip, Saved-Views-Tabs, Filter-Chips, Active-Chips, Portal-Pills, Inline-Actions, Row-Tint, 3-fach-Empty-State, Status-Aktionen-Legende) — ✅ **abgeschlossen**
siehe `14-PHASE-4H-PRESS-RELEASES-MOCKUP.md`
- **4I** = Admin „Pressemitteilungen" — Mockup-Patterns übertragen (Saved-Views-Tabs, Active-Chips, Portal-Pills, Row-Tint, Inline-Actions als Modal-Trigger, reduzierte Spalten, 2-stufiger Empty-State) — ✅ **abgeschlossen**
siehe `15-PHASE-4I-ADMIN-PRESS-RELEASES.md`
- **4J** = Dashboard-PM-Listen mit 4H/4I-Patterns (Portal-Pills, PM-ID-Sub, Customer + Admin Dashboard, Inline-Action „Prüfen →" für Admin-Review-Queue mit `is-row-warn` Tinting) — ✅ **abgeschlossen**
siehe `16-PHASE-4J-DASHBOARD-LISTS.md`
### Was geändert wird
- Page-Header-Struktur (Eyebrow + H1 + Subtitle + Aktions-Bar)
- Filter-Bars werden Hub-konform gestylt
- `flux:table`-Header bekommen Eyebrow-Optik
- Form-Felder bleiben FluxUI, aber mit Hub-Tokens
### Was nicht geändert wird
- Logik / Volt-Methoden
- Datenbank / Models
---
## Phase 5 — Dark Mode konsistent
**Status**: ✅ **abgeschlossen** · **Aufwand**: ~½ Tag (Großteil
der Arbeit war Vorbereitung in Phase 04) · **Risiko**: niedrig
> Tokens, Switcher und alle Custom-CSS-Komponenten waren
> bereits token-basiert vorbereitet — `shared/design-tokens.css`
> hat einen kompletten `.dark { … }`-Block (Z. 182248) mit allen
> Surfaces, Hub/Bernstein-Akzenten, Ink-Skala, Status-Farben,
> Schatten und `color-scheme: dark`. `hub-components.css` ist
> zu 100 % token-basiert und schaltet automatisch um. FluxUI's
> `@fluxAppearance` ist in beiden Head-Partials integriert,
> Switcher liegt im User-Menü (Desktop + Mobile) sowie in
> `settings/appearance.blade.php`. Bei der Inventur fiel nur
> ein latenter Bug auf (`--color-ink-deep`-Token existiert
> nicht, in `customer/tokens.blade.php` auf
> `--color-panel-dark-2` umgestellt). QR-Code in
> `customer/security.blade.php` behält bewusst `bg-white` in
> beiden Modi (Scan-Tauglichkeit), klarstellender Kommentar
> ergänzt.
>
> Siehe `17-PHASE-5-DARK-MODE.md` für Details.
### Quelle
`dev/frontend/tailwind_v3/User Dashboard presseportale Dark.html`
hat alle Dark-Tokens bereits vorgegeben — alle Werte sind in
`design-tokens.css` übernommen.
### Was geliefert wird
- Settings → Erscheinung → „Dunkel" schaltet Portal um
- Settings → Erscheinung → „System" folgt OS-Präferenz
- Switcher zusätzlich direkt im User-Menü (Sidebar + Mobile)
- Sidebar, Topbar, Dashboards, Listen, Forms — alles schaltet
automatisch über Token-Cascade um
- `panel-dark` bleibt KONSTANT dunkel (Brand-Bridge-Card)
- `--color-accent-warm` bleibt KONSTANT Bernstein (Hint-Cards)
- Hub-Landing und Hub-Auth bleiben Light (kein
`@fluxAppearance` im Web-Build)
---
## Phase 6 — Auth-Cleanup
**Status**: ✅ **abgeschlossen** · **Aufwand**: ~30 min · **Risiko**: niedrig
> Hub-Auth bleibt eigenständig (auf Web-Build mit dem `pressekonto`-Layout),
> weil Hub-Atmosphäre + Light-Bundle als Trade-off bewusst gewollt sind.
> Die ungenutzten Starter-Kit-Layouts wurden entfernt: `auth/simple`,
> `auth/split`, `auth/card`, der Wrapper `auth.blade.php`, das alternative
> `app/header.blade.php` sowie die Debug-Views `livewire/auth/login-simple`
> und `test-simple`. Damit gibt es keine hardcoded `class="dark"`-Reste
> mehr, die Phase 5 hätten torpedieren können. Doku-Beispiel in
> `_docs/FORTIFY-SANCTUM-SETUP.md` auf `components.layouts.auth.pressekonto`
> umgestellt. CSS-Bundle ist dabei sogar ~2.4 KB kleiner geworden.
>
> Siehe `18-PHASE-6-AUTH-CLEANUP.md` für die Inventur.
### Optional für später (NICHT Teil von Phase 6)
Eine echte **Auth-Konsolidierung** (Hub-Auth-CSS in den Portal-Build
ziehen, Web-Build nur für die Landing-Pages behalten) wäre denkbar,
aber:
- Hub-Atmosphäre (Konzentrische Kreise, Hub-Grid) ist visuell stark
- Hub-Auth ist leichtgewichtig (kein FluxUI im Bundle)
- Aktueller Stand funktioniert sauber
→ keine Action notwendig, bleibt offen.
---
## Gesamt-Status (Stand 2026-05-20)
| Phase | Inhalt | Status |
|---|---|---|
| 0 | Hub-Auth (Login/Register) im Hub-Stil | ✅ |
| 1 | Portal-Migration (Tokens, FluxUI-Overrides, Sidebar/Topbar, App-Shell, Dark-Switch im User-Menü) | ✅ |
| 2 | Customer-Dashboard auf Mockup-Stil | ✅ (in P1, verfeinert in 4J) |
| 3 | Admin-Dashboard konsistent | ✅ (in P1, verfeinert in 4J) |
| 4 | Listen/Detail durchgehen (4A4J) | ✅ **komplett** |
| 5 | Dark Mode konsistent | ✅ **abgeschlossen** |
| 6 | Auth-Cleanup | ✅ **abgeschlossen** |
### Phase 4 — Sub-Päckchen im Detail
| ID | Bereich | Plan-Doc |
|---|---|---|
| 4A | Press-Releases Listen (Admin + Customer) | `07-PHASE-4A-…` |
| 4B | Press-Releases Detail/Show | `08-PHASE-4B-…` |
| 4C | Press-Releases Forms (create/edit) | `09-PHASE-4C-…` |
| 4D | Companies (Admin) | `10-PHASE-4D-…` |
| 4E | Profile/Settings (Admin + Customer) | `11-PHASE-4E-…` |
| 4F | Restliche Admin-Bereiche (contacts, categories, presets, footer-codes, users, roles, newsletter, reports, invoices, coupons, payments, portal-switcher) | `13-PHASE-4F-…` |
| 4G | Restliche Customer-Bereiche (invoices, tokens, bookings, company-switcher, press-kits) | `12-PHASE-4G-…` |
| 4H | Customer „Meine Pressemitteilungen" Mockup-Stil (Counter-Strip, Saved-Views, Filter-/Active-Chips, Portal-Pills, Inline-Actions, Row-Tint, 3-stufiger Empty-State, Aktionen-Legende) | `14-PHASE-4H-…` |
| 4I | Admin „Pressemitteilungen" — Mockup-Patterns übertragen | `15-PHASE-4I-…` |
| 4J | Dashboard-PM-Listen mit 4H/4I-Patterns | `16-PHASE-4J-…` |
### Nächste sinnvolle Schritte (Priorität)
> Die hub-flux-Roadmap ist mit Phase 6 **vollständig** abgeschlossen.
> Alle weiteren Themen sind eigene Initiativen.
1. **Manueller Dark-Mode Smoke-Test**: Im Browser User-Menü →
Erscheinung → „Dunkel" und durch die Hauptseiten klicken
(Dashboard, Listen, Detail, Security mit QR, Tokens). Erwartung:
Lesbar + konsistent. Kleine Polish-Runde, falls visuelle
Auffälligkeiten.
2. **PM-Form-Wizard-Refactor**: Mockup
`User Neue Mitteilung presseportale.html` auf den bestehenden
Press-Release-Create/Edit-Flow übertragen. Größere Aktion mit
eigener Phase.
3. **Web-Frontend-Block** (eigenständig, NICHT Teil von Phase 16):
Die noch ungenutzten Mockups
`Startseite Tailwind.html`,
`Startseite presseecho Tailwind.html`,
`Branchenseite Tailwind.html`,
`Detailseite Tailwind.html`,
`Veröffentlichen Tailwind.html`
sind Public-Facing-Pages der beiden Portale (businessportal24 +
presseecho), nicht Portal/Backend. Würde eine **eigene Roadmap**
bekommen, weil andere Build-Pipeline (`vite.web.config.js`), andere
Komponenten und ein ganz anderer Stil-Stack
(Source-Serif-Headlines, Brand-orange für businessportal24).

View file

@ -0,0 +1,177 @@
# Phase 2 — Customer-Dashboard auf Mockup-Stil
> Detail-Plan analog zu `01-PHASE-0-TOKENS.md` und
> `02-PHASE-1-PORTAL-SHELL.md`. Wird beim Abschluss auf Status `✅ abgeschlossen`
> gesetzt; Lessons learned wandern in `PROGRESS.md`.
**Status**: ✅ abgeschlossen · **Aufwand**: ~½ Tag · **Risiko**: niedrig
---
## Ziel
`resources/views/livewire/customer/dashboard.blade.php` matched das Mockup
`dev/frontend/tailwind_v3/User Dashboard presseportale.html` zu ≥ 90 %.
Sichtbarer Wow-Moment für den Kunden, ohne neue Business-Logik.
## Was sich ändert
### Visuell
| Bereich | Heute | Mockup |
| ----------------- | ------------------------------------------------ | --------------------------------------------------------------------- |
| Page-Header | `<flux:card>` mit `<flux:heading>` „Willkommen“ | Hub-Badge + Eyebrow + großes H1 + Untertitel + rechts Status-Pille |
| KPI-Reihe | 4 schmale `<flux:card>` mit `<flux:text>`-Zahl | 4 `stat-card`s mit Strip links, Mono-Zahl groß, Sub-Info, Sparkline |
| Pressemitteilungen-Block | `<flux:card>` mit Liste / Empty-State | `panel` mit `section-eyebrow`, Empty-State mit Schritt-Karten 01/02/03 |
| Datenqualität | Grid aus `<flux:badge>`-Karten | `panel` rechts mit 2 `hint-card`s (Progress-Bar + Action-Link) |
| Firmen-Slot | `<flux:card>` mit Firmen-Liste | `panel` mit gestrichelten Empty-Slot-Karten + Hinweis-Box |
| Brand-Bridge | (nicht vorhanden) | `panel-dark` rechts: presseecho + businessportal24 Status |
| Footer | (nicht vorhanden) | Subtle-Footer mit Tastenkürzel / Changelog / Status / Support |
### Datenmodell
Heute liefert das Volt-`with()`:
- `stats.total | published | review | draft`
- `qualityHints[]`
- `recent` (PressRelease-Collection, max. 5)
- `companies`
Mockup verlangt **zusätzlich**:
- `stats.deltaMonth` — Veränderung ggü. Vormonat (Total)
- `profileCompleteness` — Prozentwert für Profil-Progress-Bar
- `billingCompleteness` — Prozentwert für Rechnungsadresse
- `bridgeStatus``['presseecho' => 'connected'|'pending', 'businessportal24' => …]`
(vorerst `connected` fest verdrahtet, später aus echtem Sync-Status)
## Was NICHT geändert wird
- Logik / Volt-Methoden — Layout-only.
- Statistik-Backend (`PressRelease::where(...)`) bleibt 1:1.
- `qualityHints`-Logik bleibt — wird nur anders gerendert.
- Topbar (eigene Phase, später).
---
## Implementierungs-Schritte
### 1. Shared Hub-Components-CSS
Klassen wandern in **`resources/css/shared/hub-components.css`** (neue Datei).
Importiert in:
- `resources/css/portal.css` (nach `flux.css`)
- `resources/css/web/shared-styles.css` (nach `design-tokens.css`)
Damit haben **Portal-Build und Web-Build** dieselbe Hub-Komponenten-Sprache —
DRY für späteres Wiederverwenden (z. B. Admin-Dashboard in Phase 3).
Folgende Klassen kommen rein (alle mit `var(--color-*)`-Tokens, keine Hex-Literale):
- `.panel`, `.panel-warm`, `.panel-dark`, `.panel-head`
- `.stat-card` + `.stat-strip` + `.stat-label` + `.stat-num`
+ Varianten: `.is-primary`, `.is-ok`, `.is-warn`, `.is-muted`
- `.hint-card` + `.hint-card .hint-ico`
- `.badge` + `.badge.warn` + `.badge.ok` + `.badge.hub` + `.badge.dot`
- `.bridge-row`, `.dot-pe`, `.dot-bp`
### 2. Blade-Components in `resources/views/components/portal/`
#### `stat-card.blade.php`
```blade
<x-portal.stat-card variant="primary|ok|warn|muted" label="Gesamt" :value="$stats['total']">
<x-slot:meta>2026</x-slot:meta> {{-- rechts oben --}}
<x-slot:trend>0 ggü. Vormonat</x-slot:trend> {{-- unten --}}
</x-portal.stat-card>
```
Render: `<article class="stat-card is-{variant}">` + Strip + Label + Mono-Zahl
+ Meta-Slot oben rechts + Trend-Slot unten. Sparkline (SVG) erstmal weggelassen
— optionales Detail; lässt sich nachschieben, wenn Daten dafür da sind.
#### `hint-card.blade.php`
```blade
<x-portal.hint-card :icon="$hint['icon']" :title="$hint['title']" :percent="60" :href="$hint['href']">
{{ $hint['description'] }}
<x-slot:action>Profil öffnen</x-slot:action>
</x-portal.hint-card>
```
Render: `.hint-card`-Grid mit Icon-Square + Progress-Bar + Text + Action-Link
in `text-accent-deep`.
#### `bridge-card.blade.php` (optional, Phase 2 minimal)
Bleibt erstmal **inline** im Dashboard (sehr spezifisch, eine Stelle).
Wird in Phase 3/4 extrahiert, wenn klar ist wie weit der Brand-Bridge-Pattern
sich verbreitet.
### 3. `customer/dashboard.blade.php` umbauen
Layout-Skelett (alles im `<div class="space-y-8">`-Container):
1. **Page-Header**`<header>` mit Grid `1fr auto`:
- Links: Hub-Badge `User Backend` + Eyebrow + `<h1>Mein Dashboard</h1>`
+ Subtitle (Name + Reichweiten-Hinweis)
- Rechts: Status-Pille
- Wenn `$selectedCompany`: Hub-Badge mit Firma-Name (grün/ok-Stil)
- Wenn nicht: Warn-Pille „Keine Firma zugeordnet → zuordnen“
2. **KPI-Reihe** — 4 `<x-portal.stat-card>` (Gesamt/Veröffentlicht/Prüfung/Entwürfe)
3. **Zweispalten-Grid** (`2fr 1fr`):
- Links: `<article class="panel">` mit Pressemitteilungen — Liste **oder**
Empty-State (3 Schritt-Karten)
- Rechts: `<article class="panel">` mit `<x-portal.hint-card>`s
4. **Unteres Grid** (`2fr 1fr`):
- Links: `<article class="panel">` Firmen — Liste **oder** Empty-Slot-Karten
- Rechts: `<article class="panel-dark">` Brand-Bridge (inline)
5. **Footer** — kleine Link-Reihe in `text-ink-3`
### 4. Volt `with()` ergänzen
- `stats.deltaMonth` via zweiter Query (Vormonats-Counts vs. Aktuell)
- `profileCompleteness` als simpler Heuristik-Wert (firstname/lastname/phone/etc.)
- `billingCompleteness` analog (Vorhanden = 100, sonst 0; oder Ist-Felder/Soll-Felder)
- `bridgeStatus` vorerst hardcoded `['presseecho' => 'connected', 'businessportal24' => 'connected']`
→ später aus echtem Sync-Service
### 5. Tests
`tests/Feature/Customer/DashboardTest.php` (oder bestehender Test, falls vorhanden):
- Rendert ohne Fehler bei eingeloggtem Customer
- Stat-Zahlen werden korrekt ausgegeben
- Empty-State wird angezeigt, wenn keine PRs existieren
- Quality-Hints werden angezeigt, wenn `profile()` fehlt
---
## Akzeptanzkriterien
- [x] Phase-2-Plan-Dokument geschrieben
- [x] `shared/hub-components.css` existiert, importiert in beiden Builds
- [x] `<x-portal.stat-card>` und `<x-portal.hint-card>` rendern wie Mockup
- [x] `/admin/me` zeigt das neue Dashboard ohne Console-Errors
- [x] Empty-State für Pressemitteilungen ist sichtbar, wenn keine vorhanden
- [x] Quality-Hints rendern mit Progress-Bar
- [x] Brand-Bridge-Dark-Card unten rechts zeigt presseecho + businessportal24
- [x] Neuer Smoke-Test `tests/Feature/Customer/DashboardTest.php` mit 5 Cases
(Core-Sections, Empty-State, PR-Liste, Profil-Hint, vollständiges Profil
blendet Hints aus). Cross-Check: alle 18 verwandten Tests bleiben grün.
- [x] Pint clean
- [x] `PROGRESS.md`-Eintrag
---
## Risiken & Mitigation
- **FluxUI-Klassen vs. Custom-CSS-Kollisionen** — wir mischen `<flux:badge>`
weiterhin **nicht** im Dashboard (für Status-Pillen nehmen wir
`.badge.hub|.warn|.ok|.dot`). Auf den Detail-Seiten (Phase 4) bleibt
FluxUI dominant.
- **Sparklines weggelassen** — minimaler Stilverlust, wird in Phase 4 mit
echten Trend-Daten nachgereicht. Mockup-Match weiterhin ≥ 90 %.
- **`stats.deltaMonth`-Performance** — zweite Query auf gleicher Tabelle;
bei wachsendem Datensatz später cachen. Heute irrelevant.

View file

@ -0,0 +1,159 @@
# Phase 5 — Dark Mode konsistent + Appearance-Switcher im User-Menü
> **Vorzugsphase**: Eigentlich Phase 5, vorgezogen vor Phase 3/4 nach
> User-Wunsch. Grund: Der FluxUI-Appearance-Switcher hat in Phase 1 den
> Dark-Mode-Bug ausgelöst (`#6d8ad3` statt Hub-Blau); statt Symptom-Fix
> wollen wir den vollen Dark Mode jetzt richtig.
**Status**: ✅ abgeschlossen · **Aufwand**: ~½ Tag · **Risiko**: niedrig
---
## Ziele
1. Dark Mode funktioniert sauber auf allen Portal-Seiten (Customer-Dashboard,
Sidebar, FluxUI-Komponenten, Hub-Components).
2. Switcher (Light / Dark / System) **direkt im User-Menü** verfügbar — kein
Umweg über `/settings/appearance` mehr nötig.
3. Hub-Frontend (Landing + Auth) bleibt **bewusst Light-Only** — Hub-Atmosphäre
ist Buchpapier.
4. Der Notfall-Hack aus Phase 1 (`portal.css: .dark { --color-accent: var(--color-hub) }`)
wird durch das echte Dark-Token-Mapping ersetzt.
## Quelle der Wahrheit
`dev/frontend/tailwind_v3/User Dashboard presseportale Dark.html`
liefert alle Dark-Werte. Wir spiegeln die Token-Namen aus dem Light-Mode 1:1.
---
## Was sich ändert
### 1. `shared/design-tokens.css`
Der seit Phase 0 vorbereitete `.dark`-Block wird **aktiviert** und um die seit
Phase 2 nachgezogenen Tokens (`--color-bg-rule-2`, `--color-gain-deep`) ergänzt.
Wichtige Umwidmungen im Dark Mode (gleiche Namen, andere Werte):
| Token | Light | Dark | Bedeutung |
| ----------------------- | ----------- | ----------- | ------------------------------------------ |
| `--color-bg` | `#f6f4ef` | `#0e1218` | Page-Background |
| `--color-bg-elev` | `#fbfaf6` | `#14181f` | Sidebar / leicht hervorgehoben |
| `--color-bg-card` | `#ffffff` | `#181d27` | Card-Body |
| `--color-bg-rule` | `#e2ddd0` | `#2a3142` | Trennlinien |
| `--color-bg-rule-2` | `#ede7d7` | `#232838` | Progress-Track |
| `--color-hub` | `#1a2540` | `#5a78c2` | Primary-Akzent (heller im Dark) |
| `--color-hub-soft` | `#e5e9f1` | `#1f2a47` | Hint-Hintergrund |
| `--color-accent` | `#b07a3a` | `#d9a560` | Bernstein-Akzent (heller im Dark) |
| `--color-accent-warm` | `#b07a3a` | `#b07a3a` | **bleibt gleich** — für Border-Akzente |
| `--color-accent-deep` | `#8a5e27` | `#b07a3a` | Action-Link-Color (heller im Dark) |
| `--color-accent-soft` | `#f1e6d3` | `#2a2418` | Hint-Icon-BG |
| `--color-ink` | `#1a1f1c` | `#ece9e0` | Text-Primary |
| `--color-ink-2` | `#3a413d` | `#c9c5b8` | Text-Secondary |
| `--color-ok / -soft` | `#2e8540 / #e2f1e5` | `#4dc076 / #1a2d22` | Status grün |
`--color-accent-warm` als **konstanter** Bernstein-Token (gleicher Wert in
beiden Modi) ist ein bewusster Trick: Im Portal mapt `--color-accent` auf
`var(--color-hub)` (Primary-Akzent), aber Bernstein-Borders (Hint-Card,
Schritt-Karten-Eyebrow) brauchen weiterhin den ungeänderten Bernstein-Wert.
`--color-accent-warm` ist der Token dafür.
### 2. `portal.css`
- Den `.dark { --color-accent: var(--color-hub); … }`-Notfall-Block aus
Phase 1 entfernen — er ist mit echtem Dark-Mapping überflüssig.
- Stattdessen einen kurzen Kommentar setzen, dass das Token-Mapping aus
`design-tokens.css` greift.
- Primary-Button-Hover-Override: heute hartcodiert auf `--color-hub-2`
(`#243152`). Im Dark Mode ist `--color-hub-2` = `#6d8ad3` (heller),
was richtig ist (Hover = noch heller als Default-Button). Override bleibt
via Variable korrekt, kein Eingriff nötig.
- Button-Shadows: `rgba(26, 37, 64, …)` ist Light-Mode-spezifisch. Im Dark
Mode wirkt der bläuliche Shadow auf dunklem Card-BG falsch. Lösung:
per `@media (prefers-color-scheme: dark)` ODER `.dark`-Selektor die
Shadow-Farben auf transparenten Schwarz tönen.
### 3. `shared/hub-components.css`
- Bernstein-Stellen umstellen: `var(--color-accent)`
`var(--color-accent-warm)` für **Hint-Card-Border-Left** und
**Progress-Bar-Fill** (sonst wird's im Portal Hub-Blau gemapt).
- Wegen Tokens-Architektur (`hub-soft` etc. werden im Dark Mode dunkler)
funktioniert der Großteil **automatisch** — keine `.dark`-Overrides nötig.
- Eine Ausnahme: `.panel-dark` und die Brand-Bridge-Boxes — im Light Mode
ist `panel-dark` Hub-Blau (#1a2540). Im Dark Mode bleibt das auch
`var(--color-hub)` — und da ist `--color-hub` heller (#5a78c2). Wir
brauchen also einen **konstanten dunklen Token** für `panel-dark` — sonst
wird die „dunkle“ Bridge-Card im Dark Mode plötzlich hellblau.
→ Neuer Token `--color-panel-dark` (immer #0f1729, in beiden Modi)
oder direkt `var(--color-topbar-deep)` (existiert bereits).
### 4. User-Menü-Switcher
Beide Dropdowns (Desktop in `sidebar.blade.php`, Mobile in derselben Datei)
bekommen vor dem Logout einen Block:
```blade
<flux:menu.separator />
<div class="px-2 py-1.5">
<flux:radio.group x-data variant="segmented" x-model="$flux.appearance" size="sm">
<flux:radio value="light" icon="sun" />
<flux:radio value="dark" icon="moon" />
<flux:radio value="system" icon="computer-desktop" />
</flux:radio.group>
</div>
```
Das ist exakt das Pattern aus `livewire/settings/appearance.blade.php`, nur
kompakt mit Icons-only. `$flux.appearance` ist FluxUI's Magic-Object,
LocalStorage-persistent, von `@fluxAppearance` injiziert.
### 5. Hub-Auth bleibt Light
`auth/pressekonto.blade.php` lädt **kein** `@fluxAppearance` und **kein**
`partials/head` — die Hub-Auth-Pipeline ist eigenständig. Damit wird `class="dark"`
dort nie gesetzt, selbst wenn das LocalStorage `dark` enthält. Hub-Auth bleibt
also automatisch Light. **Kein Eingriff nötig.**
---
## Implementierungs-Schritte (Reihenfolge)
1. `design-tokens.css`: Dark-Block aktivieren, `--color-accent-warm` definieren,
`--color-panel-dark` einführen (oder Re-Use `--color-topbar-deep`).
2. `portal.css`: Hack rausnehmen, Button-Shadow für Dark Mode tönen.
3. `hub-components.css`: Bernstein-Stellen auf `--color-accent-warm`,
`.panel-dark` auf `--color-panel-dark`.
4. `sidebar.blade.php`: Switcher in beide Dropdowns einbauen.
5. Build + Smoke-Test im Browser.
6. Bestehende Tests laufen lassen (sollten alle weiter grün sein, weil
keine Test-Assertion farbbezogen ist).
7. Pint + PROGRESS + Plan-Status.
## Akzeptanzkriterien
- [x] Plan-Dokument geschrieben
- [x] Switcher Light/Dark/System im User-Menü (Desktop + Mobile) sichtbar,
Änderung wirkt sofort + persistent (LocalStorage)
- [x] Dark Mode: Customer-Dashboard rendert mit allen Sektionen korrekt
(Tokens schalten um, kein hellblauer Akzent-Bug)
- [x] Brand-Bridge-Karte bleibt im Dark Mode dunkel (`--color-panel-dark-2`
konstant in beiden Modi)
- [x] Hub-Auth bleibt im Dark Mode Light (lädt kein `@fluxAppearance`)
- [x] Tests grün (18/18)
- [x] Pint clean
- [x] PROGRESS-Eintrag
## Risiken & Mitigation
- **FluxUI-Default-Komponenten im Dark Mode** (z.B. Modals, Dropdowns):
unsere Zinc→Hub-Mapping in `portal.css` bridgt die Skala. Im Dark Mode
müssen die Zinc-Werte auch umgekippt werden — passiert automatisch über
die Token-Vars, weil wir `var(--color-bg-*)` nutzen. Restrisiko: einzelne
FluxUI-Komponenten könnten harte Tailwind-Klassen wie `bg-white` haben,
die im Dark unverändert weiß bleiben. Smoke-Test deckt das auf.
- **Hub-Components-Schatten** sind in Hub-Blau-Alpha definiert
(`rgba(26, 37, 64, …)`). Im Dark Mode auf dunklem Bg wirken sie evtl.
„falsch warm“. Akzeptabel im ersten Wurf; Feintuning per `prefers-color-scheme`
Media-Query falls visuell stört.

View file

@ -0,0 +1,55 @@
# Phase 3 — Admin-Dashboard im Hub-Vokabular
**Status**: ✅ abgeschlossen · **Aufwand**: ~¼ Tag · **Risiko**: niedrig
---
## Ziel
`resources/views/admin/dashboard.blade.php` nutzt dieselbe Designsprache wie
das Customer-Dashboard (Phase 2). Keine neuen Komponenten, keine neue Logik
— nur Vokabular-Umbau auf die DRY-Schicht.
## Was sich ändert
### Layout
- Page-Header: Hub-Badge „Admin Backend" + Eyebrow + großes H1
„Admin Dashboard" + Subtitle. Rechts: Status-Pille „Alle Systeme operational"
(ok-Style mit Dot).
- KPI-Reihe: weiter **5 Stat-Cards** (wie heute), aber als
`<x-portal.stat-card>` mit Strip-Variante:
- Pressemitteilungen → `primary` (mit Sub: `X pub · Y prüf · Z entwurf`)
- In Prüfung → `warn` (eigene KPI, war vorher in der PM-Card versteckt)
- Firmen → `muted`
- Kontakte → `muted`
- Benutzer → `muted`
- 2-Spalten-Grid (`2fr 1fr`) — wie heute:
- Links: Panel „Letzte Pressemitteilungen" (Liste + Status-Badges
in `.badge.ok|warn|err|hub`).
- Rechts: Panel „Zur Prüfung" mit warn-Pille (Count) + Liste +
„+ N weitere"-Link.
- Bottom-Reihe (`1fr 2fr`) — **neu**:
- Links: `panel-warm` Newsletter-Block (Mono-Zahl + Subline).
- Rechts: `panel` Quick-Actions mit Section-Eyebrow + Schnellzugriff
auf Invoices/Payments/Coupons/Presets.
- Footer: subtle Link-Reihe analog Customer-Dashboard.
### Was NICHT geändert wird
- Controller-Logik / Datenform.
- Newsletter-Count bleibt erhalten, wandert nur in einen eigenen Block.
- Bestehende Tests (`DashboardTest`) bleiben grün — alle geprüften
Strings (`3`, `1 pub`, `1 prüf`, `1 entwurf`, `Review Dashboard PM`)
bleiben im Output.
## Akzeptanzkriterien
- [x] Plan geschrieben
- [x] Admin-Dashboard verwendet `<x-portal.stat-card>`, `.panel`,
`.section-eyebrow`, `.badge`
- [x] Customer-Dashboard und Admin-Dashboard sind visuell aus einem Guss
- [x] Dark Mode greift automatisch (alle Tokens)
- [x] `DashboardTest` bleibt grün ohne Anpassung (alle 3 Cases +
Wortlaut „1 pub / 1 prüf / 1 entwurf / Review Dashboard PM")
- [x] Pint clean, PROGRESS-Eintrag

View file

@ -0,0 +1,58 @@
# Phase 4A — Press-Releases Listen-Pages
> Erstes Päckchen aus Phase 4 (Listen/Detail-Pages iterativ). Wegen
> Umfang teilen wir Phase 4 in Päckchen — A = Listen, B = Detail/Show,
> C = Forms (create/edit), D = Companies, E = Settings/Profile, F = Rest.
**Status**: ✅ abgeschlossen · **Aufwand**: ~¼ Tag · **Risiko**: niedrig
---
## Scope
In diesem Päckchen NUR:
- `resources/views/livewire/admin/press-releases/index.blade.php`
- `resources/views/livewire/customer/press-releases/index.blade.php`
NICHT in diesem Päckchen:
- `show.blade.php`, `create.blade.php`, `edit.blade.php` (Päckchen B/C)
- Companies, Settings, Profile (Päckchen D/E)
## Ziel
Beide Listen nutzen das Hub-Vokabular:
- **Page-Header** wie im Dashboard (Hub-Badge + Eyebrow + H1 + Subtitle,
CTA rechts).
- **KPI-Reihe** (nur Admin) als `<x-portal.stat-card>` — exakt
wie auf dem Admin-Dashboard (DRY: vier Cards mit primary/ok/warn/muted).
- **Filter-Bar** als `.panel` mit `.panel-head` „Filter & Suche".
- **Tabelle** in `.panel` (kein FluxUI-Card-Wrapper mehr). FluxUI-Table
bleibt — wir bridgen über Zinc-→Hub-Mapping.
- **Status-Badges** auf eigene `.badge.ok|warn|err|hub`-Pillen
(statt `<flux:badge color="…">`) — visuelle Konsistenz zum Dashboard.
- **Empty-State** in Hub-Stil mit Icon-Box.
## Was NICHT angefasst wird
- **Confirm-Modals** für Publish/Reject/Archive — Tests assertieren
Wortlaut („Pressemitteilung veröffentlichen?" etc.).
- **Volt-Logik** (Sort, Filter, Methods) — Layout-only.
- **FluxUI-Form-Inputs** (Search, Select, Combobox) — bleiben, weil
Hub kein eigenes Combobox-Pendant hat.
## Akzeptanzkriterien
- [x] Plan
- [x] Admin-Liste: Page-Header + 4 Stat-Cards + Filter-Panel + Tabelle-Panel + Hub-Badges
- [x] Customer-Liste: Page-Header + Filter-Panel + Tabelle-Panel + Hub-Badges + Empty-State
- [x] Visuell aus einem Guss mit Dashboard
- [x] `PressReleaseWorkflowTest` + `AdminPressReleaseActionsTest` bleiben grün
- [x] Build + Pint + PROGRESS
## Bonus-Fix
- `customer/dashboard.blade.php` Subtitle ergänzt um „Übersicht für {Firma}",
wenn `$selectedCompany` gesetzt ist. Korrigiert eine Phase-2-Regression
bei `CustomerCompanyContextTest > customer dashboard is filtered by …`.

View file

@ -0,0 +1,56 @@
# Phase 4B — Press-Releases Detail/Show-Pages
> Zweites Päckchen aus Phase 4. Folgt auf 4A (Listen).
**Status**: ✅ abgeschlossen · **Aufwand**: ~⅓ Tag · **Risiko**: niedrig
---
## Scope
- `resources/views/livewire/admin/press-releases/show.blade.php`
- `resources/views/livewire/customer/press-releases/show.blade.php`
NICHT in diesem Päckchen:
- `create.blade.php`, `edit.blade.php` (Päckchen 4C — Forms)
- Companies, Settings, Profile (Päckchen 4D/4E)
## Ziel
Beide Detail-Pages im Hub-Vokabular:
- **Page-Header** wie auf den Listen (Hub-Badge + Eyebrow + H1 +
Subtitle), Status-Pill direkt unter dem Eyebrow oder im Header-Meta,
Aktions-Buttons (Bearbeiten / Zurück / Vorschau-Link) rechts.
- **Status-Workflow-Aktionsbar** als `.panel` mit klarer Optik
je nach Status (review = warn, published = ok, draft = neutral).
- **Content-Hauptbereich** (PM-Text) als `.panel` (kein FluxUI-Card-Wrapper).
- **Sidebar / Side-Cards** als kleine `.panel` mit `panel-head`.
- **Status-Verlauf-Timeline** als `.panel` mit Hub-Badges
(`.badge.ok|warn|err|hub`).
- **Rejection-Hinweis** (Customer) als Hub-Style-Error-Panel mit
Linker Akzent-Border (statt `<flux:callout>`).
- **Share-Link-Erfolgsbox** (Customer) als Hub-Style-Success-Block.
- **Pressekontakte-Liste** (Customer) als Hub-Items in `.panel`.
## Was explizit NICHT angefasst wird
- **Confirm-Modals** (Publish / Reject / Archive) — Tests in
`AdminPressReleaseActionsTest` assertieren Wortlaute.
- **Wortlaute** `Werbliche Sprache wurde markiert.` und
`Erneut einreichen` aus `PressReleaseWorkflowTest`.
- **Volt-Logik** (publish/reject/archive/submitForReview/
generateShareLink/with) — Layout-only.
## Akzeptanzkriterien
- [x] Plan
- [x] Admin-Show: Page-Header + Status-Workflow-Bar + Text-Panel +
Sidebar-Panels + Timeline + Hub-Badges. Modals unverändert.
- [x] Customer-Show: Page-Header + Status-Workflow-Bar +
Rejection-Panel + Share-Erfolgsblock + Contacts-Panel +
Verlauf-Panel + Text-Panel + Hub-Badges. „Erneut einreichen" +
„Werbliche Sprache wurde markiert." Wortlaute bleiben.
- [x] `PressReleaseWorkflowTest` + `AdminPressReleaseActionsTest`
bleiben grün (16 passed, 52 assertions).
- [x] Build + Pint + PROGRESS.

View file

@ -0,0 +1,69 @@
# Phase 4C — Press-Releases Forms (create / edit)
> Drittes Päckchen aus Phase 4. Folgt auf 4A (Listen) und 4B (Show).
**Status**: ✅ abgeschlossen · **Aufwand**: ~⅓–½ Tag · **Risiko**: niedrig
---
## Scope
- `resources/views/livewire/admin/press-releases/create.blade.php`
- `resources/views/livewire/admin/press-releases/edit.blade.php`
- `resources/views/livewire/customer/press-releases/create.blade.php`
- `resources/views/livewire/customer/press-releases/edit.blade.php`
NICHT in diesem Päckchen:
- Companies (Päckchen 4D)
- Settings / Profile (Päckchen 4E)
- Restliche Admin-Bereiche (Päckchen 4F)
## Ziel
Alle vier Forms im Hub-Vokabular:
- **Page-Header** wie auf Listen und Detail-Pages: Hub-Badge +
Eyebrow + H1 + Subtitle. Bei Edit zusätzlich Status-Pille im
Header-Meta. Aktions-Buttons rechts.
- **Form-Sektionen** als `.panel` mit `.panel-head` und
`section-eyebrow` ("Inhalt", "SEO & Links", "Metadaten",
"Status-Aktionen", "Aktionen").
- **Form-Felder** bleiben FluxUI (`<flux:field>`, `<flux:label>`,
`<flux:input>`, `<flux:textarea>`, `<flux:select>` inkl.
Combobox-Variant, `<flux:checkbox>`, `<flux:error>`) — das
Token-Bridging aus Phase 1 zieht. Required-Marker werden auf
Hub-Error-Token umgestellt.
- **Action-Buttons** ("Speichern", "Zur Prüfung einreichen",
"Als Entwurf speichern", "Status wechseln") in einem `.panel`
mit `.panel-head` "Aktionen".
- **Flash-Boxen** auf Hub-Token-Pillen.
## Was explizit NICHT angefasst wird
- **Volt-Logik** (`save`, `submitForReview`, `publish`, `reject`,
`backToDraft`, `archive`, `changeStatus`, `deletePressRelease`,
`mount`, `with`, `selectedCompany`) — Layout-only.
- **Confirm-Modals** der Admin-Edit-Page (`confirm-status-change`,
`confirm-delete-press-release`) — Tests assertieren Wortlaute.
- **Wortlaute** aus `AdminPressReleaseActionsTest`:
`Neuer Status`, `Status wechseln`, `Status wirklich wechseln?`,
`Pressemitteilung löschen?`, `Status wurde auf`.
- **Image-Manager-Komponente** (`<livewire:components.press-release-images-manager>`).
- **Wortlaute** für Customer-Create-Page: `Beta GmbH` (Firma
aus Faktur via `companyId`) — Combobox/Select bleibt funktional.
## Akzeptanzkriterien
- [x] Plan
- [x] Admin-Edit: Page-Header + 5 Panels + Hub-Status-Badge,
beide Modals unverändert, alle Wortlaut-Assertions grün.
- [x] Admin-Create: Page-Header + 3 Panels (Inhalt, SEO, Metadaten)
+ Aktions-Panel.
- [x] Customer-Create: Page-Header + 2 Panels (Inhalt, Metadaten)
+ Aktions-Panel.
- [x] Customer-Edit: Page-Header + 2 Panels (Inhalt mit Image-Manager,
Metadaten) + Aktions-Panel.
- [x] `AdminPressReleaseActionsTest`,
`CustomerCompanyContextTest > customer press releases create …`,
`CustomerProfileSecurityTest` bleiben grün (72 Tests, 310 assertions).
- [x] Build + Pint + PROGRESS.

View file

@ -0,0 +1,79 @@
# Phase 4D — Companies (admin)
> Viertes Päckchen aus Phase 4. Folgt auf 4A/4B/4C
> (Press-Releases-Strecke komplett).
**Status**: ✅ abgeschlossen · **Aufwand**: ~½ Tag · **Risiko**: niedrigmittel
---
## Scope
- `resources/views/livewire/admin/companies/index.blade.php` (573 Z.)
- `resources/views/livewire/admin/companies/show.blade.php` (400 Z.)
- `resources/views/livewire/admin/companies/edit.blade.php` (412 Z.)
- `resources/views/livewire/admin/companies/create.blade.php` (281 Z.)
NICHT in diesem Päckchen:
- `admin/contacts/*` — eigener Bereich, Päckchen 4F.
- `me/press-kits/*` — Customer-Sicht auf Firmen, eigener Päckchen-Anteil.
- Settings / Profile (Päckchen 4E).
## Ziel
Alle vier Companies-Pages im Hub-Vokabular — gleiches Muster wie
die Press-Releases-Strecke:
- **Page-Header** mit Hub-Badge „Admin Backend" + Eyebrow + H1 +
Subtitle. Bei Show/Edit zusätzlich Portal-Pille (Presseecho /
Businessportal24 / Both) und ID/Slug-Hinweis.
- **KPI-Reihe** auf Index als `<x-portal.stat-card>` falls Stats
vorhanden (Gesamt, je Portal, ggf. mit/ohne PMs).
- **Filter-Bar** als `.panel` mit `.panel-head` „Filter & Suche".
- **Tabelle/Listen** in `.panel` mit Hub-Badges
(`.badge.hub|ok|warn`).
- **Form-Sektionen** (Edit, Create) als `.panel` mit `.panel-head`
und `section-eyebrow`.
- **Tabs** (Show: „contacts", andere?) bleiben als FluxUI-Tabs —
optisch über das Token-Bridging genug Hub-Look.
- **Confirm-Modals** unverändert lassen (z.B. delete-confirm).
## Was explizit NICHT angefasst wird
- **Volt-Logik** in allen 4 Files — Layout-only.
- **`deleteCompany`**-Methode + Confirm-Modal — Test
`UserManagementTest` assertiert Redirect-Verhalten.
- **Wortlaute, die Tests prüfen**:
- `Firmen` (Sidebar-Link), `Portal`, `Alle Portale`,
`Firmen PM Zaehler GmbH` (Faktur-Name).
- Portal-Labels `Presseecho`, `Businessportal24`.
- URL-Pfade `/admin/companies/{id}`.
- **FluxUI Form-Felder, Combobox, Tab-Groups** bleiben.
## Akzeptanzkriterien
- [x] Plan
- [x] Index: Page-Header + Stats + Filter-Panel + Tabellen-Panel +
Hub-Badges.
- [x] Show: Page-Header (mit Portal-Pille) + Logo-/Meta-Block +
Tabs + Hub-Badges.
- [x] Edit: Page-Header (mit Portal-Pille) + Form-Panels +
Aktions-Panel. Delete-Modal unverändert.
- [x] Create: Page-Header + Form-Panels + Aktions-Panel.
- [x] `UserManagementTest`, `PortalAssetManifestTest` grün
(25 Tests, 227 Assertions). Volle Suite: 230 grün
(Vorhandener `ApiDocumentationTest`-Fail nicht von uns).
- [x] Build + Pint + PROGRESS.
## Notes
- Tabs als schlankes Bottom-Border-Pattern statt FluxUI-Tabs:
spart Layout-Komplexität und ist optisch näher am Hub.
- Portal-Badges bewusst alle als `badge hub` vereinheitlicht —
Portal-spezifische Farben (purple/blue) brachen aus dem
Tokensystem aus. Portal-Label bleibt sichtbar im Text.
- Logo-Boxen folgen jetzt dem gleichen Token-Pattern wie auf den
Press-Release-Detail-Seiten (`var(--color-bg-elev)` +
`var(--color-bg-rule)` border).
- Delete-Confirm-Modal komplett unverändert (Test prüft Redirect
nach `deleteCompany`, nicht UI).

View file

@ -0,0 +1,103 @@
# Phase 4E — Profile & Settings
> Fünftes Päckchen aus Phase 4. Folgt auf 4D Companies.
**Status**: ✅ abgeschlossen · **Aufwand**: ~½ Tag · **Risiko**: niedrig
---
## Scope
**Admin / „/settings/*" (alle Rollen)**
- `resources/views/livewire/settings/profile.blade.php` (117 Z.)
- `resources/views/livewire/settings/password.blade.php` (82 Z.)
- `resources/views/livewire/settings/appearance.blade.php` (21 Z.)
- `resources/views/livewire/settings/delete-user-form.blade.php`
(59 Z., wird in profile.blade.php als Komponente eingebunden)
**Customer („Mein Bereich")**
- `resources/views/livewire/customer/profile.blade.php` (451 Z.)
- `resources/views/livewire/customer/security.blade.php` (295 Z.)
NICHT in diesem Päckchen:
- `customer/invoices.blade.php`, `customer/tokens.blade.php`,
`customer/bookings.blade.php`, `customer/company-switcher`
eigenes Päckchen 4F.
- 2FA-Pages aus Fortify-Standard-Setup — bleiben FluxUI bis sie
separat angepackt werden.
## Ziel
Beide Settings-Strecken im Hub-Vokabular wie Press-Releases /
Companies:
- **Page-Header** mit Rolle-Pille („Admin Backend" oder
„User Backend") + Eyebrow + H1 + Subtitle.
- **Form-Sektionen** als `.panel` mit `.panel-head` und
`section-eyebrow`.
- **FluxUI-Form-Felder** bleiben (`flux:field`, `flux:input`,
`flux:label`, `flux:error`, `flux:checkbox`, `flux:radio`,
`flux:textarea`, `flux:button`).
- **Required-Marker** auf Hub-Token (`text-[color:var(--color-err)]`).
- **Save-Action** in eigenem Panel-Footer mit Save-Indicator-Span.
- **Flash-/Success-Messages** auf Hub-Token-Pillen.
- **Danger-Zone** (Delete-Account, Sessions löschen) als
`.panel` mit `is-danger`-Akzent (linker Roter Strip).
## Was explizit NICHT angefasst wird
- Volt-Logik in allen Dateien.
- `<x-settings.layout>` Wrapper-Komponente (falls vorhanden) —
Layout-only-Änderungen, keine Hülle.
- 2FA-/Confirm-Password-Modals.
- Test-relevante Strings auf customer/profile + customer/security:
- **profile**: „Rechnungsadresse"
- **security**: „Konto-Sicherheit", „Letzter Login",
„Aktive Sessions", „Passwort ändern", „E-Mail-Adresse ändern",
„Zwei-Faktor-Authentifizierung"
## Akzeptanzkriterien
- [x] Plan
- [x] `partials/settings-heading` zentral auf Hub-Page-Header.
- [x] `components/settings/layout` zentral auf Hub-Sidebar +
Content-Panel.
- [x] `settings/appearance` Hub-styled (profitiert vom neuen
Wrapper, kein Body-Touch nötig).
- [x] `settings/profile` Hub-styled + Verification-Hinweis als
Hub-Warn-Box + Save-Button mit Token-„Saved."-Pill.
- [x] `settings/delete-user-form` als Hub-Danger-Box mit
linkem Roten Strip. Modal-Markup unverändert.
- [x] `settings/password` Hub-styled (Save-Bar mit Border-Top).
- [x] `customer/profile` Hub-styled: Header, 3 Panels (Konto,
Profil, Rechnungsadresse), Aktionen-Panel, Zugeordnete-Firmen-
Panel mit Hub-Badges.
- [x] `customer/security` Hub-styled: Header, 4-spaltige
KPI-Reihe, Passwort+E-Mail 2-Col-Grid, 2FA-Panel mit
Hub-Recovery-Codes, Sessions-Panel mit Hub-Empty-State.
- [x] Tests grün:
- `Settings|CustomerProfileSecurity|CustomerCompanyContext`
→ 33 passed (146 assertions).
- Volle Suite → 230 passed, 3 skipped, 1 pre-existing
`ApiDocumentationTest`-Fail (nicht von 4E).
- [x] Build + Pint + PROGRESS.
## Notes
- Der zentrale Umbau der beiden Wrapper (`settings-heading`,
`settings.layout`) hat sich bezahlt gemacht: die drei
`settings/*`-Pages sind weitgehend identisch klein geblieben,
Page-Header und Side-Nav kommen automatisch im Hub-Look.
- `<x-action-message>` für „Saved."-Toast bleibt drin, aber
der Slot ist auf einen Hub-getönten `<span>` umgestellt.
- Empty-State für „Aktive Sessions" nutzt das gleiche
Hub-Icon-Box-Pattern wie auf den Press-Releases-Listen.
- 2FA-QR-Container behält explizit weißen Hintergrund —
QR-Codes brauchen hohen Kontrast unabhängig vom Mode.
- Alle Test-Pflicht-Strings unverändert: „Rechnungsadresse",
„Konto-Sicherheit", „Letzter Login", „Aktive Sessions",
„Passwort ändern", „E-Mail-Adresse ändern",
„Zwei-Faktor-Authentifizierung".

View file

@ -0,0 +1,78 @@
# Phase 4G — Customer Portal (Mein Bereich)
> Sechstes Päckchen aus Phase 4. Folgt auf 4E
> (Profile/Settings).
**Status**: ✅ abgeschlossen · **Aufwand**: ~½ Tag · **Risiko**: niedrigmittel
---
## Scope
**Liste / Detail / Helper**:
- `resources/views/livewire/customer/invoices.blade.php` (194 Z.)
- `resources/views/livewire/customer/tokens.blade.php` (212 Z.)
- `resources/views/livewire/customer/bookings.blade.php` (52 Z.)
- `resources/views/livewire/customer/company-switcher.blade.php`
(91 Z., sitzt in der Sidebar)
- `resources/views/livewire/customer/press-kits/index.blade.php`
(119 Z.)
- `resources/views/livewire/customer/press-kits/show.blade.php`
(734 Z., großes Detail-Cockpit)
NICHT in diesem Päckchen:
- `customer/press-releases/*` — schon in 4A/4B/4C.
- `customer/dashboard.blade.php` — schon in Phase 2.
- `customer/profile.blade.php`, `customer/security.blade.php`
schon in 4E.
- Admin-Bereich (Päckchen 4F).
## Ziel
Alle Customer-Pages im Hub-Vokabular:
- **Page-Header** „User Backend"-Pille + Eyebrow
„Mein Bereich · …" + H1 + Subtitle.
- **KPI/Summary-Reihe** wo Stats existieren
(`<x-portal.stat-card>`).
- **Tabellen, Listen, Forms** in `.panel` mit `.panel-head`.
- **Flash-Boxen** (success/info/warn/error) auf Hub-Token.
- **Hub-Badges** statt `flux:badge` (außer dort wo speziell
begründet).
- **Empty-States** als Hub-Icon-Box.
- **Company-Switcher**: bleibt funktional, optisch Hub-Akzent.
## Was explizit NICHT angefasst wird
- Volt-Logik in allen Dateien.
- `<flux:select variant="combobox">`, `<flux:input>` usw.
- Konfirm-/Edit-Modals in `press-kits/show.blade.php` (falls
vorhanden).
- Bestimmte Strings, die Tests assertieren:
- **tokens**: „API-Tokens" (Heading),
„API-Tokens werden erst freigeschaltet" (Hinweis).
- **invoices**: „Hinweis zu Rechnungen",
„Rechnungsadresse im Profil pflegen", „Öffnen".
- **company-switcher**: „Firma öffnen",
`route('me.press-kits.show', …)`.
- **press-kits.show**: „Abrechnung", „Statistik" (Tab-Labels
bzw. Section-Headings).
## Akzeptanzkriterien
- [x] Plan
- [x] `customer/invoices`
- [x] `customer/tokens`
- [x] `customer/bookings` (Coming-Soon-Stub)
- [x] `customer/company-switcher` (Sidebar-Komponente)
- [x] `customer/press-kits/index`
- [x] `customer/press-kits/show` (Stammdaten/Kontakte/PMs/
Abrechnung/Statistik im Hub-Stil; Forms inline behalten)
- [x] `CustomerPortalTest`, `CustomerCompanyContextTest`,
`PanelConsolidationTest` grün (29 Tests, 131 Assertions).
- [x] Volle Suite: 230 passed, 3 skipped, 1 pre-existing fail
(`ApiDocumentationTest`, fehlende `docs/api/v1.yml`).
- [x] Build (portal: 418 kB CSS, 44 kB JS) + Pint clean +
PROGRESS.

View file

@ -0,0 +1,84 @@
# Phase 4F — Restliche Admin-Bereiche
> Siebtes Päckchen aus Phase 4. Folgt auf 4G
> (Customer Portal).
**Status**: ✅ abgeschlossen · **Aufwand**: ~1,52 Tage · **Risiko**: niedrig
---
## Scope
Insgesamt ~7.500 Z. Blade. Aufgeteilt in 4 Sub-Päckchen:
### 4F-1 — Stammdaten & Switcher (~1.250 Z.)
- `admin/presets/{index,create,edit}.blade.php` (361 Z.)
- `admin/presets/partials/form-fields.blade.php`
- `admin/categories/{index,create,edit}.blade.php` (813 Z.)
- `admin/portal-switcher.blade.php` (68 Z., Sidebar-Komponente)
### 4F-2 — Pressekontakte (~1.360 Z.)
- `admin/contacts/index.blade.php` (729 Z.)
- `admin/contacts/create.blade.php` (275 Z.)
- `admin/contacts/edit.blade.php` (352 Z.)
### 4F-3 — Operations & Finance (~1.870 Z.)
- `admin/footer-codes/{index,create,edit}.blade.php` (673 Z.)
- `admin/reports/slow-requests.blade.php` + table (288 Z.)
- `admin/invoices/index.blade.php` (314 Z.)
- `admin/coupons/index.blade.php` (43 Z., Stub)
- `admin/payments/index.blade.php` (53 Z., Stub)
### 4F-4 — User-Verwaltung (~3.020 Z.)
- `admin/users.blade.php` (939 Z.) +
`admin/users/{create,edit,show,table}.blade.php`
- `admin/roles/{index,create,edit}.blade.php` (393 Z.)
- `admin/newsletter/sync.blade.php` (171 Z.)
## Ziel
Alle Admin-Pages im Hub-Vokabular:
- **Page-Header** „Admin Backend"-Pille + Eyebrow
„Administration · …" + H1 + Subtitle.
- **KPI/Summary-Reihe** wo Stats existieren
(`<x-portal.stat-card>`).
- **Tabellen, Listen, Forms** in `.panel` mit `.panel-head`.
- **Flash-Boxen** (success/info/warn/error) auf Hub-Token.
- **Hub-Badges** statt `flux:badge` (außer in begründeten
Fällen).
- **Empty-States** als Hub-Icon-Box.
- **Danger-Zones** mit linkem roten Strip.
## Was explizit NICHT angefasst wird
- Volt-Logik in allen Dateien.
- `<flux:input>`, `<flux:select>`, `<flux:checkbox>`,
`<flux:textarea>`, `<flux:field>`, `<flux:error>`,
`<flux:table.*>`, `<flux:button>`, `<flux:modal>` etc.
- Modals und ihre Bestätigungs-Flows.
- Test-relevante Strings — vor jedem Sub-Päckchen werden die
betreffenden Test-Assertions geprüft.
## Tests pro Sub-Päckchen
| Sub | Tests |
|---|---|
| 4F-1 | `CategoryIndexPerformanceTest`, `AdminCategoryManagementTest`, `AdminPresetManagementTest` |
| 4F-2 | (keine spezifischen Contact-Tests gefunden, evtl. via `AdminAssetManifestTest` indirekt) |
| 4F-3 | `AdminFooterCodeManagementTest`, `AdminSlowRequestReportTest`, `AdminSlowRequestLoggingTest`, `AdminLegacyInvoiceArchiveTest` |
| 4F-4 | `UserManagementTest`, `UserImpersonationTest`, `RoleManagementTest` |
## Akzeptanzkriterien
- [x] Plan
- [x] 4F-1 Stammdaten & Switcher
- [x] 4F-2 Pressekontakte
- [x] 4F-3 Operations & Finance
- [x] 4F-4 User-Verwaltung
- [x] Tests pro Sub-Päckchen grün
- [x] Build + Pint + PROGRESS.

View file

@ -0,0 +1,171 @@
# Phase 4H — Customer „Meine Pressemitteilungen" auf Mockup-Stil
> Nachgelagertes Päckchen aus Phase 4. Hebelt das wichtigste
> Customer-Arbeitstool auf das volle Mockup-Vokabular.
**Status**: ✅ abgeschlossen · **Aufwand**: ~1 Tag · **Risiko**: niedrig
---
## Anlass
`customer/press-releases/index.blade.php` ist heute auf
~50 % Mockup-Match: Page-Header, Filter-Panel, Tabelle und
Empty-State sind Hub-styled, aber alle hochwirksamen Patterns
aus dem Mockup fehlen. Diese Seite ist die meistbesuchte des
Customer-Bereichs (primäres Arbeitstool).
## Vorlage
`dev/frontend/tailwind_v3/User Pressemitteilungen presseportale.html`
## Match-Ziel
≥ 90 % zum Mockup, ohne Backend-API-Erweiterungen.
---
## Scope — was umgesetzt wird
### 1. Counter-Strip im Header
Statt langer Subzeile: Inline-Counter pro Status direkt
unter H1.
`24 Mitteilungen · 18 veröffentlicht · 1 in Prüfung · 4 Entwürfe · 1 abgelehnt`
Zahlen kommen aus aggregiertem `withCount()` über die Status-Enum.
### 2. Saved-Views-Tabs
Tab-Leiste statt Status-Dropdown:
`Alle (24) · Veröffentlicht (18) · Entwürfe (4) · In Prüfung (1) · Abgelehnt (1) · Archiv (0)`
Aktive Tab unterstrichen mit Hub-Blau. Klick → `statusFilter`-Volt-Property.
### 3. Filter-Chips mit Caret
Status/Portal/Firma/Zeitraum als Chips mit Dropdown-Caret.
Aktive Chip in Hub-Blau, inaktive auf weißem Background.
**Hinweis**: Status-Chip ist im UI redundant mit Saved-Views-Tabs;
wir lassen ihn weg. Bleiben: Portal, Firma, Zeitraum (`erstellt` ab Datum X).
### 4. Active-Chips (entfernbare Filter)
Unter den Filter-Chips: Sichtbarmachung aktiver Filter mit
`×`-Button zum Entfernen.
### 5. Bulk-Selection (nur UI-Vorbereitung)
Checkbox in Header und pro Zeile. Header-Checkbox kann
indeterminate-State haben. **Keine Bulk-Aktionen** im Scope —
das wird erst später ein eigenes Päckchen (Bulk-Approve etc.).
### 6. Portal-Pills mit farbigem Dot
Pro Zeile in Spalte „Portal": je nach `portal`-Enum:
- `presseecho` → grüner Dot
- `businessportal24` → orangener Dot
- `both` → beide Pills nebeneinander
### 7. Inline-Actions neben Status
Statusspalte bekommt **zusätzlich** zur Badge eine kleine
Inline-Action je nach Status:
| Status | Inline-Action |
|---|---|
| Draft | `Zur Prüfung →` |
| Review | `Zurückziehen →` (nur wenn `withdrawFromReview` existiert, sonst weglassen) |
| Published | — (keine Inline-Action, nur Hauptaktion „Vorschau") |
| Rejected | `Grund ansehen →` (Modal/Tooltip mit `rejection_reason`) |
| Archived | — |
### 8. Row-Tinting
Zeilen mit Status `review` bekommen `is-row-warn` (sehr
dezent gelb beim Hover), `rejected` bekommen `is-row-err`
(rot beim Hover).
### 9. Reichere Zeilen-Sub
Sub-Zeile unter Titel: `PM-Nummer · Datum · Hinweis`
Wir nutzen vorhandene Felder: `id` (als „PM-{id}"), `published_at`,
`rejection_reason`-Existenz.
### 10. Datum mit Sub
Datumsspalte zweizeilig: Datum (Mono-Font, tabular-nums) +
Sub (Status-Kontext z.B. „veröffentlicht · 09:12").
### 11. 3-fach Empty-State
Drei Varianten je nach Kontext:
- **`empty-filter`** — wenn Filter aktiv: „Keine Mitteilungen mit diesen Filtern" + Reset-Button
- **`empty-search`** — wenn `search` gesetzt: „Keine Treffer für `…`" + Suche-Reset
- **`empty-none`** — wenn gar keine PMs überhaupt: „Noch keine Pressemitteilungen" + 3-Schritt-Onboarding
### 12. Status-Aktionen-Legende
`panel-warm` unter der Tabelle: 5-Spalten-Grid mit allen
Aktionen pro Status. Reduziert Support-Anfragen.
### 13. Spalten-/Export-Buttons (rechts im Header)
„Spalten" (jetzt non-functional als `bald`-Badge) und „Export"
(`bald`-Badge) als sekundäre Buttons. Optional — können auch
weggelassen werden, da wir keine Spalten-Konfiguration haben.
**Entscheidung**: weglassen. Kein UX-Mehrwert ohne Backend.
---
## Was NICHT angefasst wird
- Volt-Logik (`submitForReview`, `sort`, `with`) — nur erweitert
um Aggregate-Counts.
- Backend-Endpoints / API.
- Detail-/Show-Seite und Create/Edit-Forms.
- Routen.
## Neue / erweiterte CSS-Klassen in `hub-components.css`
- `.counter-strip`, `.counter-strip .seg`, `.counter-strip .sep`
- `.view-tab` (mit `.is-active`, `.cnt`)
- `.filter-chip` (mit `.is-active`, `.caret`)
- `.active-chip` (mit `.x`)
- `.portal-pill` (mit `.pe`, `.bp`, `.pdot`)
- `.inline-action` (mit `.warn`, `.err`)
- `.is-row-warn`, `.is-row-err` (Row-Tinting)
- `.empty-stage`, `.empty-ico`, `.empty-title`, `.empty-sub`
- `.badge.muted` (für Draft / Archiv)
- Optional: `.checkbox.indeterminate` (für Bulk-Header)
## Volt-Erweiterungen
- `statusCounts` (assoc-Array Status-Value → int) — aggregiert
über `withoutGlobalScopes()->where('user_id', …)->groupBy('status')`
- `totalCount` (int) — Gesamt-Counter unabhängig vom Filter
- Saved-View-Klick mappt auf `statusFilter` (kein eigener Property nötig)
## Tests
Keine bestehenden Tests für `customer/press-releases/index`
gefunden. Akzeptanz:
- `php artisan test --compact` läuft sauber (modulo
pre-existing `ApiDocumentationTest`)
- Build (`npm run build:portal`) clean
- Pint clean
## Akzeptanzkriterien
- [x] Plan
- [x] CSS-Klassen ergänzt
- [x] Volt-Counts ergänzt
- [x] Markup auf Mockup-Stil
- [x] Build + Pint + Tests grün
- [x] PROGRESS.md + 03-WEITERE-PHASEN.md aktualisiert

View file

@ -0,0 +1,136 @@
# Phase 4I — Admin „Pressemitteilungen" Mockup-Patterns
> Folgepäckchen zu 4H. Überträgt die in der Customer-PM-Liste
> entwickelten Mockup-Patterns auf die Admin-Seite. Die
> CSS-Komponenten existieren bereits.
**Status**: ✅ abgeschlossen · **Aufwand**: ~½ Tag · **Risiko**: niedrig
---
## Anlass
Nach 4H matched `customer/press-releases/index.blade.php` das
Mockup zu ~90 %. Die gleichen Patterns gehören in die
Admin-Liste — das ist der zentrale Editorial-Workflow.
## Aktueller Stand
`admin/press-releases/index.blade.php` ist bei ~60 % Match:
- ✅ Page-Header (Hub-Stil)
- ✅ 4-KPI-Reihe (`<x-portal.stat-card>`)
- ✅ Filter-Panel (search/status/portal/language/category +
3 entity-comboboxes)
- ✅ Tabellen-Panel + Empty-State (Hub-styled)
- ✅ Modals (publish/reject/archive) — test-kritisch
- ❌ Saved-Views-Tabs
- ❌ Active-Chips für aktive Filter
- ❌ Portal-Pills (statt Text)
- ❌ Row-Tinting (review/rejected)
- ❌ Inline-Actions („Veröffentlichen →", „Ablehnen →" neben Status)
- ❌ Reichere Sub-Zeilen (PM-{id} + Datum + Status-Kontext)
## Scope
### 1. Saved-Views-Tabs
Tab-Leiste zwischen KPI-Reihe und Filter:
`Alle · Veröffentlicht · In Prüfung · Entwürfe · Abgelehnt · Archiv`
Counter-Pille pro Tab basierend auf erweiterten `stats`.
### 2. Active-Chips
Sichtbarkeit aller aktiven Filter (Status, Portal, Sprache,
Kategorie, User, Firma, Kontakt, Suche), jeweils mit `×`.
### 3. Portal-Pills
Statt „Presseecho"/„Businessportal24"/„Beide" als Text:
Portal-Pills mit farbigem Dot. Bei `Portal::Both` zwei
Pills nebeneinander.
### 4. Row-Tinting
`is-row-warn` für review, `is-row-err` für rejected.
### 5. Inline-Actions
**Zusätzlich** zu den bestehenden Action-Buttons + Modals:
| Status | Inline-Action |
|---|---|
| Review | `Freigeben →` + `Ablehnen →` (öffnet die Modals) |
| Published | `Archivieren →` (öffnet Modal) |
Die Inline-Actions sind **nur** Trigger für die bestehenden
Modals (keine direkte Wire-Methode), damit die existierenden
Test-Modal-Strings („Pressemitteilung veröffentlichen?")
weiter angezeigt werden. Action-Buttons in der `Aktionen`-
Spalte bleiben als Backup für Power-User.
### 6. Reichere Sub-Zeilen
In Titel-Zelle: `PM-{id}` als Sub. In Datum-Zelle: Hauptdatum
+ Status-Kontext (`erstellt · 11:02`, `veröffentlicht · 14:30`, …).
### 7. Counter-Strip ausgelassen
KPI-Reihe ist bereits 4-spaltig (Gesamt, Veröffentlicht, In
Prüfung, Entwürfe) und voll funktional. Counter-Strip wäre
redundant. Bleibt bei KPI-Cards.
## Was bleibt unangetastet
- **Volt-Methoden** (`publish`, `reject`, `archive`, `sort`,
`with`, `clearUserFilter`, …)
- **Bestehende Modals** mit Strings „Pressemitteilung
veröffentlichen?", „Pressemitteilung ablehnen?",
„Pressemitteilung archivieren?" (Test-kritisch)
- **Filter-Panel-Struktur** (Entity-Comboboxes erhalten,
kein Reduktionsexperiment)
- **`stats`-Cache-Methode** (`AdminPerformanceCache`)
## Erweiterung
- `pressReleaseStats()` erweitert um `rejected` und `archived`
Counter, damit alle 6 Saved-Views-Tabs ihre Counter haben
## Tests
- `AdminPressReleaseActionsTest` (3 Tests, prüft auf Modal-Strings)
- `PressReleaseWorkflowTest` (mehrere Tests, prüft auf
`publish`/`reject`-Methoden + Audit-Logs)
- Volle Suite muss weiter sauber laufen (modulo
pre-existing `ApiDocumentationTest`)
## Akzeptanzkriterien
- [x] Plan
- [x] Volt: `stats` um `rejected` + `archived` erweitert
- [x] Volt: `setView()` + `resetFilters()` ergänzt
- [x] Saved-Views-Tabs eingebaut (6 Stück inkl. Counter)
- [x] Active-Chips eingebaut (8 Filter-Typen)
- [x] Portal-Pills statt Text (incl. Both → 2 Pills)
- [x] Row-Tinting (review → warn, rejected → err)
- [x] Inline-Actions als Modal-Trigger
(Freigeben/Ablehnen/Archivieren) zusätzlich neben Badge
- [x] Sub-Zeilen: PM-ID + Firma + Sprache, Datum + Status-Kontext
- [x] Empty-State 2-stufig (mit/ohne Filter)
- [x] Build + Pint + Tests grün (16/16 PR + 230/231 voll)
- [x] PROGRESS.md + 03-WEITERE-PHASEN.md aktualisiert
## Lessons Learned
- `@php(...)` Inline-Form führte beim Compile zu einem
Syntax-Fehler im kompilierten View (führte zu
`<?php($var = ...)` ohne Whitespace), obwohl alle Klammern
balanced waren. Vermutung: Konflikt der Klammer-Zählung
mit Methoden-Calls. Lösung: alle drei `@php(...)`
Statements zu `@php ... @endphp` umgewandelt.
- Tabellen-Spalten reduziert (8 → 7), weil die rechte
„Aktionen"-Spalte mit den Status-Icons inhaltlich
redundant zur neuen Inline-Action in der Status-Spalte
war. Die linke „Aktionen"-Spalte (view/edit) blieb.

View file

@ -0,0 +1,63 @@
# Phase 4J — Dashboard-PM-Listen mit 4H/4I-Patterns
> Mini-Folgepäckchen: Die in 4H/4I entwickelten
> Mockup-Patterns (Portal-Pills, Status-Sub-Zeilen,
> Inline-Actions) werden auf die kompakten PM-Listen
> in den Dashboards übertragen.
**Status**: ✅ abgeschlossen · **Aufwand**: ~30 min · **Risiko**: niedrig
---
## Anlass
Nach 4H/4I sehen die Voll-Listen exakt wie das Mockup aus —
die Dashboard-Listen ("Meine letzten Pressemitteilungen",
"Letzte Pressemitteilungen", "Zur Prüfung") sind aber noch
auf dem alten Stand (Badge rechts, Sub-Zeile ohne Portal).
Das wirkt inkonsistent.
## Scope
### Customer-Dashboard (Volt `customer/dashboard.blade.php`)
- **Volt-Component**: `recent` lädt aktuell
`['id', 'title', 'status', 'company_id', 'created_at']`.
`portal` ergänzen.
- **Markup `recent`-Liste**:
- Portal-Pills (presseecho/businessportal24) neben dem Badge
- Sub-Zeile mit PM-ID + Firma + Datum (statt nur Firma + Datum)
### Admin-Dashboard (Controller-View `admin/dashboard.blade.php`)
- **`recentPRs`-Liste**:
- Portal-Pills neben dem Badge
- Sub-Zeile mit PM-ID + Firma + User + Datum
- **`pendingReviews`-Liste**:
- Portal-Pills neben Titel
- Inline-Action „Prüfen →" als Link zur Show-Page
(im Dashboard keine direkte `publish`/`reject`-Methode
möglich, da Controller+Blade statt Volt)
## Was bleibt unangetastet
- Backend-Logik (Controller, Volt-`with()`)
- Customer-Tests (`DashboardTest`) — die geprüften
Strings („Phase 2 Demo Release", „Alle anzeigen",
„Meine letzten Pressemitteilungen", …) bleiben
- Quick-Actions, Newsletter, Brand-Bridge — bereits gut
## Akzeptanzkriterien
- [x] Plan
- [x] Customer-Volt: `portal` + `published_at` in
Recent-Select-Liste
- [x] Customer-Dashboard: Portal-Pills + PM-ID-Sub
+ published_at als Primärdatum bei Published
- [x] Admin-Dashboard recentPRs: Portal-Pills + PM-ID-Sub,
`badgeClass` mit `muted`-Variante (statt `hub` für
archived/draft)
- [x] Admin-Dashboard pendingReviews: Portal-Pills +
Inline-Action „Prüfen →" + `is-row-warn` Tinting
- [x] Build + Pint + Tests grün (5/5 Dashboard, 230/231 voll)
- [x] PROGRESS.md + 03-WEITERE-PHASEN.md aktualisiert

View file

@ -0,0 +1,152 @@
# Phase 5 — Dark Mode
> Dark Mode für das Portal (Customer + Admin), gesteuert
> über den FluxUI Appearance-Switcher. Hub-Frontend bleibt
> Light-Only.
**Status**: ✅ abgeschlossen · **Aufwand**: ~½ Tag
(meiste Arbeit war Vorbereitung in Phase 04) · **Risiko**: niedrig
---
## Befund
Beim Start in Phase 5 war faktisch ~95 % der Arbeit schon
in den vorherigen Phasen erledigt worden. Konkret:
### Was bereits da war
1. **`shared/design-tokens.css`** hat einen kompletten
`.dark { … }`-Block (Z. 182248) mit:
- Surfaces (bg, bg-elev, bg-rule, bg-card, bg-card-warm)
- Hub-Blau (heller im Dark Mode: `#5a78c2`)
- Bernstein (heller: `#d9a560`), `--color-accent-warm`
bewusst konstant
- Ink-Skala (invertiert)
- Status-Farben (ok/warn/err in helleren Varianten)
- Bridge-Dots (heller)
- Schatten (neutral schwarz statt warm-blau)
- `color-scheme: dark` für native Controls
2. **`portal.css`** hat:
- `@custom-variant dark (&:where(.dark, .dark *))` für
Tailwind-`dark:`-Variants
- Dark-Mode Shadow-Overrides für FluxUI-Primary-Buttons
- Klare Erläuterung, warum kein Notfall-`.dark { … }`-Hack
mehr nötig ist
3. **`shared/hub-components.css`** ist **zu 100 %
token-basiert** (panels, badges, view-tabs, filter-chips,
active-chips, portal-pills, inline-actions, empty-stage,
counter-strip). Sobald die Tokens umschalten, schalten
alle Komponenten automatisch um.
4. **`panel-dark`** nutzt `--color-panel-dark-2` /
`--color-panel-dark` — die sind in beiden Modi
identisch. Die Brand-Bridge-Card bleibt damit immer
dunkel.
5. **`@fluxAppearance`** ist in beiden Head-Partials
eingebunden (`partials/head.blade.php` +
`partials/admin-head.blade.php`).
6. **Switcher** ist an drei Stellen verfügbar:
- Sidebar Desktop User-Menü
(`components/layouts/app/sidebar.blade.php`)
- Sidebar Mobile User-Menü (gleiche Datei)
- `settings/appearance.blade.php` (volle Card mit
Light/Dark/System)
Alle drei nutzen `x-model="$flux.appearance"`
FluxUI's Magic-Object, das LocalStorage-persistent ist
und automatisch `class="dark"` auf `<html>` setzt.
### Was zu fixen war
Bei der Inventur fielen nur zwei Stellen auf, die im
Dark Mode brechen würden:
1. **`customer/tokens.blade.php` Z. 137**: Token-Anzeige
nach Generierung nutzte ein nicht existentes
`--color-ink-deep`-Token. Damit war der Hintergrund
bisher schlicht transparent (Light: kaum sichtbar,
Dark: ebenso). Außerdem würde `--color-ink` im Dark
Mode hell werden — weißer Text auf hellem Bg wäre
unlesbar. Fix: auf `--color-panel-dark-2` (konstant
dunkel) umstellen.
2. **`customer/security.blade.php` Z. 270**: 2FA-QR-Code
in `bg-white`-Block. Das ist **bewusst** so — QR-Codes
brauchen schwarz-auf-weiß, sonst werden sie nicht
eingescannt. Kein Fix, nur Kommentar zur Klarstellung.
### Was unkritisch ist
- `customer/dashboard.blade.php`: 8 Treffer für
`bg-white` / `text-white` — alle im `panel-dark`
Brand-Bridge Block (konstant dunkel, weiße Texte
korrekt) und im Empty-State Counter-Badge auf
`--color-accent` (Bernstein-Bg, beides Modi).
- `admin/dashboard.blade.php` Z. 235: Quick-Action
Hover-State `group-hover:bg-hub group-hover:text-white`
— Hub-Bg ist sowohl Light (dunkles Hub-Blau) als auch
Dark (helleres Hub-Blau) okay für weißen Text.
- Sidebar-`dark:bg-zinc-…` Klassen aus dem Starter-Kit:
Die Zinc-Skala ist in `portal.css` auf warmes
Buchpapier gemapped, das funktioniert weiter — die
Avatar-Bg's sind sowieso nur 8×8-Pixel-Spots.
- `customer/press-kits/show.blade.php` Z. 440: Logo-Bg
`bg-white` — bewusst, weil Firmen-Logos einen weißen
Hintergrund für korrekte Darstellung brauchen.
### Was außerhalb von Phase 5 ist
- **`shared-styles.css`** hat `.dark .card`, `.dark
.slider-*`, `.dark .highlight-*`, `.dark .section-*`
Regeln. Diese sind für den **Web-Bereich**
(Hub-Frontend, presseecho, businessportal24), nicht
fürs Portal. `portal.css` importiert
`shared-styles.css` **nicht** — die Regeln landen also
nicht im Portal-Bundle.
- **Web-Frontend** bleibt Light-Only — wie in der
Roadmap definiert.
## Akzeptanzkriterien
- [x] Plan
- [x] Recherche aller hardcoded Farbklassen
- [x] `--color-ink-deep``--color-panel-dark-2` in
`customer/tokens.blade.php`
- [x] `bg-white` für 2FA-QR-Code in `security.blade.php`
mit erklärendem Kommentar versehen
- [x] Build + Pint + Tests grün
- [x] PROGRESS.md + 03-WEITERE-PHASEN.md auf ✅
## Lessons Learned
- Konsequente Token-Disziplin (kein Hex außer in
`design-tokens.css`) zahlt sich beim Dark Mode massiv
aus: Statt 15 Pages × dutzende Klassen anzufassen,
schaltet das gesamte Portal mit einem `.dark {…}` Block
um.
- Die wenigen Ausnahmen (panel-dark, QR-Code-Bg,
Logo-Bg) sind dokumentierte Bewusst-Entscheidungen,
keine Schuld.
- FluxUI's `$flux.appearance` Magic-Object spart eine
eigene Alpine-Component und persistiert über
LocalStorage.
## Manueller Smoke-Test (Empfehlung)
Da Pest keine echte Browser-Rendering-Pipeline hat:
- Im Browser: User-Menü → Erscheinung → „Dunkel"
- Folgende Seiten besuchen + visuell prüfen:
- `/dashboard` (Admin) + `/me/dashboard` (Customer)
- `/me/press-releases` (Customer-Liste mit
Saved-Views-Tabs, Filter-Chips, Portal-Pills,
Row-Tints)
- `/admin/press-releases` (Admin-Liste mit Inline-Actions)
- `/me/security` (2FA-QR mit `bg-white`)
- `/me/tokens` (Token-Anzeige nach Generierung)
- `/admin/companies/{id}` (Detail-View mit allen Panels)
- Erwartung: Lesbar, konsistent, keine grellen
Kontraste, keine „weißen Inseln" auf dunklem Bg.

View file

@ -0,0 +1,54 @@
# Phase 6 — Auth-Cleanup
> Ungenutzte Starter-Kit-Layouts und Debug-Views entfernen,
> die seit der Hub-Auth-Umstellung (Phase 0) keine Verwendung
> mehr haben.
**Status**: ✅ abgeschlossen · **Aufwand**: ~30 min · **Risiko**: niedrig
---
## Inventur
### Aktiv genutzt (BEHALTEN)
| Datei | Genutzt von |
|---|---|
| `components/layouts/app.blade.php` | Wrapper → `app.sidebar` |
| `components/layouts/app/sidebar.blade.php` | primäres Portal-Layout (alle Volt-Pages) |
| `components/layouts/auth/pressekonto.blade.php` | login, register, forgot-password, verify-email, confirm-password, reset-password |
### Ungenutzt (ENTFERNT)
| Datei | Grund |
|---|---|
| `components/layouts/auth.blade.php` | dünner Wrapper auf `auth.simple` — nirgends als Layout referenziert (außer `login-simple` + `test-simple` Debug, die ebenfalls weg sind) |
| `components/layouts/auth/simple.blade.php` | Starter-Kit-Rest, hardcoded `class="dark"` |
| `components/layouts/auth/split.blade.php` | Starter-Kit-Rest, hardcoded `class="dark"` |
| `components/layouts/auth/card.blade.php` | Starter-Kit-Rest, hardcoded `class="dark"` |
| `components/layouts/app/header.blade.php` | alternatives App-Layout (Top-Bar statt Sidebar), wird vom Wrapper nie aufgerufen, hardcoded `class="dark"` |
| `livewire/auth/login-simple.blade.php` | Debug-Volt-Login („Log in to your account"-Englisch) ohne Route, nutzte `<x-layouts.auth>` |
| `test-simple.blade.php` | Debug-Page „Diese Seite funktioniert ohne Volt/Livewire" ohne Route |
### Aktualisiert
- `_docs/FORTIFY-SANCTUM-SETUP.md`: Code-Beispiel von
`components.layouts.auth` auf `components.layouts.auth.pressekonto`
umgestellt (alter Wert verwies auf die soeben gelöschte
Wrapper-Datei).
## Verifikation
- `rg "auth\.(simple|split|card)"` und
`rg "app\.header"` → 0 Treffer in `resources/` und `routes/`
- Build, Pint, alle relevanten Tests grün
## Lessons Learned
- Mit der Anti-Flash-Bridge aus Phase 5 wären die hardcoded
`class="dark"`-Layouts sowieso zur Stolperfalle für
Light-Mode-User geworden, falls sie je reaktiviert werden.
Lösung: weg damit.
- Hub-Auth-Pages laufen seit Phase 0 ausschließlich über
`auth/pressekonto.blade.php`. Die Starter-Kit-Layouts waren
schon damals tote Last.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,143 @@
# Hub × FluxUI — Visuelle Vereinheitlichung Portal ↔ Hub
> **Ziel**: Das Portal-Backend (User-Panel, Admin-Bereich) wird visuell an den
> Hub-Stil (`pressekonto.test`-Landing + Auth-Seiten) angeglichen, **ohne
> FluxUI aufzugeben**. Die FluxUI-Komponenten werden über Design-Tokens und
> CSS-Overrides ans Hub-Design adaptiert.
## Status
| Phase | Beschreibung | Status |
|-------|--------------|--------|
| 0 | Design-Tokens vereinheitlichen | **✅ abgeschlossen** (2026-05-19) |
| 1 | Portal-Shell (Sidebar, Layout, Brand-Mark) | **✅ abgeschlossen** (2026-05-19) |
| 2 | Customer-Dashboard auf Mockup-Stil (inkl. Topbar) | 🟡 wartet auf Freigabe |
| 3 | Admin-Dashboard konsistent | ⚪ später |
| 4 | Listen-/Detail-Pages | ⚪ iterativ |
| 5 | Dark Mode konsistent | ⚪ später |
| 6 | Auth-Konsolidierung (optional) | ⚪ optional |
→ Tagesaktueller Fortschritt: [`PROGRESS.md`](./PROGRESS.md)
## Dokumente in diesem Verzeichnis
| Datei | Zweck |
|-------|-------|
| [`README.md`](./README.md) | Diese Übersicht: Entscheidung, Architektur, Status |
| [`01-PHASE-0-TOKENS.md`](./01-PHASE-0-TOKENS.md) | Phase 0: Tokens vereinheitlichen (aktiv) |
| [`02-PHASE-1-PORTAL-SHELL.md`](./02-PHASE-1-PORTAL-SHELL.md) | Phase 1: Portal-Shell ans Hub-Design angleichen |
| [`03-WEITERE-PHASEN.md`](./03-WEITERE-PHASEN.md) | Outline für Phasen 26 |
| [`PROGRESS.md`](./PROGRESS.md) | Fortschritts-Log + Notizen |
## Visuelle Vorlagen
Maßgebliche Mockups für diese Arbeit:
- `dev/frontend/tailwind_v3/User Dashboard presseportale.html` — Light-Variante
- `dev/frontend/tailwind_v3/User Dashboard presseportale Dark.html` — Dark-Variante
- `dev/frontend/tailwind_v3/Login pressekonto A3 Tailwind.html` — Hub-Auth-Vorlage (bereits umgesetzt)
- `dev/frontend/tailwind_v3/Hub Landing pressekonto-2.html` — Hub-Landing-Vorlage (bereits umgesetzt)
## Architektur-Entscheidung
### Plan B (gewählt) — Tokens teilen, Komponenten getrennt lassen
```
┌─────────────────────────────────────────────────────────────────┐
│ shared/design-tokens.css │
│ (Hub-Blau, Bernstein, Buchpapier, Inter Tight, JetBrains Mono) │
└────────────────┬────────────────────────┬───────────────────────┘
│ │
┌────────▼─────────┐ ┌─────────▼──────────┐
│ portal.css │ │ theme-pressekonto │
│ + FluxUI │ │ + shared-styles │
│ (Backend) │ │ (Hub-Landing/Auth)│
└──────────────────┘ └────────────────────┘
build/portal build/web
```
**Warum nicht alles in FluxUI?**
- Die Hub-Landing lebt von Atmosphäre (konzentrische Kreise, Hub-Grid, eigene
Eyebrows, Bridge-Cards), die mit FluxUI-Komponenten nicht 1:1 abbildbar ist
- FluxUI würde im Hub-Frontend zur unnötigen Bundle-Last
- Der gerade gebaute Hub-Login wäre verloren
**Warum nicht alles Custom?**
- 77 Blade-Dateien nutzen FluxUI ein Rewrite wäre Aufwand ohne Mehrwert
- FluxUI-Tabellen, -Forms, -Modals, -Date-Picker sind im Backend Gold wert
- FluxUI ist sehr gut **customizable** über CSS-Custom-Properties
und `[data-flux-*]`-Selectoren
### Verworfen — Plan A (alles FluxUI) & Plan C (alles im Web-Build)
Siehe Chat-Historie für die Begründung. Kurz: zu viel Aufwand, zu wenig
Mehrwert, würde bereits gelieferte Hub-Atmosphäre verschlechtern.
## Branding & Tokens
### Brand
- **Marke**: pressekonto (verbindlich, für Portal und Hub)
- **Wortmarke**: `<x-web.brand-mark brand="pressekonto" />` — auch im Portal
- **Logo-Komponente** `<x-app-logo>` wird im Portal **abgelöst**
### Farben (Light)
| Token | Wert | Rolle |
|-------|------|-------|
| `--color-hub` | `#1A2540` | **Primärer Akzent** (Sidebar-Active, Primary-Buttons, Eyebrows) |
| `--color-hub-2` | `#243152` | Hover-Stufe von `--color-hub` |
| `--color-hub-3` | `#2E3D66` | Tertiäre Stufe (Dekoration auf dunklem Grund) |
| `--color-hub-soft` | `#E5E9F1` | Active-Pill-Hintergrund in der Sidebar |
| `--color-accent` | `#B07A3A` | **Sekundärer Akzent** (Bernstein Notifications, Datenqualität, Empfehlungs-Ribbon) |
| `--color-accent-deep` | `#8A5E27` | Akzent-Hover/Text |
| `--color-bg` | `#F6F4EF` | Warmes Buchpapier — Haupt-Hintergrund |
| `--color-bg-elev` | `#FBFAF6` | Elevation 1 (Sidebar, Topbar, leichte Cards) |
| `--color-bg-card` | `#FFFFFF` | Reine Cards |
| `--color-bg-rule` | `#E2DDD0` | Standard-Trennlinien |
| `--color-ink` | `#1A1F1C` | Primärtext |
| `--color-ink-2` | `#3A413D` | Sekundärtext (Begleittexte) |
| `--color-ink-3` | `#5A6360` | Meta, Labels |
| `--color-ink-4` | `#8A918D` | Disabled, Hintergrund-Meta |
### Farben (Dark) — laut Mockup
Wird in Phase 5 ausgearbeitet. Hub-Blau wird heller (`#5A78C2`), Akzent
wärmer (`#D9A560`), Hintergrund tief Anthrazit (`#0E1218`). Wichtig: die
Token-Namen bleiben gleich, nur die Werte tauschen — keine doppelte
UI-Pflege.
### Typografie
| Token | Wert |
|-------|------|
| `--font-sans` | `"Inter Tight", Inter, system-ui, sans-serif` |
| `--font-mono` | `"JetBrains Mono", "SF Mono", ui-monospace, monospace` |
| `--font-serif` | `"Source Serif 4", Georgia, serif` (nur für Brand-Mark) |
Aktuell im Portal: **Instrument Sans** → wird in Phase 1 abgelöst.
## Konventionen
- **Light Mode** ist Default. `class="dark"` auf `<html>` wird entfernt.
- Dark Mode wird via `prefers-color-scheme` + Flux Appearance-Switcher
gesteuert. Token-Layer übernimmt die Umschaltung.
- Token-Names sind **identisch** zwischen Hub-Theme und Portal-Theme.
Sowohl `bg-bg-elev` als auch `bg-hub-soft` funktionieren in beiden Welten.
- FluxUI-Komponenten werden **nicht überschrieben**, sondern über
`[data-flux-*]`-Selectoren in `portal.css` ergänzt — minimaler Eingriff,
maximale Kompatibilität mit FluxUI-Updates.
## Was wir bewusst NICHT machen
- **Kein FluxUI-Rewrite**. Nur Token-Anpassung + Selektor-Overrides.
- **Keine zweite UI-Pflege für Dark Mode**. Tokens-only-Ansatz.
- **Kein Verschmelzen der Builds** (Phase 6 optional).
- **Keine Änderung an Volt-/Livewire-Logik** in dieser Arbeit.
## Wer ändert was
- **Diese Doku** wird mit jeder Phase aktualisiert. `PROGRESS.md` enthält
die Tages-Notes.
- Code-Änderungen sind kleinteilig und werden in eigenen Commits gebündelt
(`hub-flux: phase 0 — tokens vereinheitlicht`, etc.).
- Bei Unklarheiten/Entscheidungen: kurz in `PROGRESS.md` festhalten,
damit nachvollziehbar bleibt warum etwas so und nicht anders.

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>presseportale.com — Publisher-Hub</title>
<title>pressekonto.de — Publisher-Hub</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@ -163,7 +163,7 @@
<!-- Wortmark: schlicht, kein Symbol — wirkt im Verlags-Segment seriöser -->
<a href="#" class="flex items-baseline gap-2.5">
<span class="text-[24px] font-bold tracking-[-0.5px] text-hub leading-none">presseportale<span class="text-accent">.com</span></span>
<span class="text-[24px] font-bold tracking-[-0.5px] text-hub leading-none">pressekonto<span class="text-accent">.com</span></span>
<span class="hidden md:inline-block w-px h-[18px] bg-bg-rule"></span>
<span class="eyebrow muted text-[9.5px] tracking-[0.22em]">Publisher · Hub</span>
</a>
@ -230,7 +230,7 @@
</h1>
<p class="text-[17px] leading-[1.55] text-ink-2 mt-7 m-0 max-w-[560px]">
presseportale.com ist der gemeinsame Publisher-Bereich für unsere beiden Pressefachportale. Pressemitteilungen schreiben, redaktionell prüfen lassen, auf beiden Reichweiten veröffentlichen — und Reichweite, Empfänger und Abrechnung an einem Ort verwalten.
pressekonto.de ist der gemeinsame Publisher-Bereich für unsere beiden Pressefachportale. Pressemitteilungen schreiben, redaktionell prüfen lassen, auf beiden Reichweiten veröffentlichen — und Reichweite, Empfänger und Abrechnung an einem Ort verwalten.
</p>
<div class="mt-8 flex items-center gap-3">
@ -290,7 +290,7 @@
<div class="absolute" style="top:50%;left:50%;transform:translate(-50%,-50%);">
<div class="w-[140px] h-[140px] rounded-full bg-hub-grad flex flex-col items-center justify-center text-white shadow-lg shadow-hub/30">
<div class="text-[10px] font-bold tracking-[0.22em] uppercase text-hub-line">Hub</div>
<div class="text-[15px] font-bold tracking-[-0.3px] mt-1">presseportale</div>
<div class="text-[15px] font-bold tracking-[-0.3px] mt-1">pressekonto</div>
<div class="text-[10px] text-white/55 tracking-[0.18em] uppercase mt-0.5">.com</div>
</div>
</div>
@ -767,15 +767,15 @@
</div>
</section>
<!-- ============== HINTER PRESSEPORTALE — PLATTFORM-FAMILIE ============== -->
<!-- ============== HINTER PRESSEKONTO — PLATTFORM-FAMILIE ============== -->
<section id="familie" class="max-w-layout mx-auto px-8 pt-20 pb-16">
<header class="mb-10">
<div class="section-eyebrow mb-4">Hinter presseportale.com</div>
<div class="section-eyebrow mb-4">Hinter pressekonto.de</div>
<h2 class="text-[32px] font-bold m-0 tracking-[-0.6px] text-ink leading-[1.18] max-w-[820px]">
Zwei eigenständige Pressefachportale. Eine kuratierte Verlags-Familie.
</h2>
<p class="text-[14.5px] text-ink-2 leading-[1.55] m-0 mt-4 max-w-[760px]">
presseportale.com ist nicht „irgendein Tool" — es ist die zentrale Plattform für unsere beiden redaktionell geführten Pressefachportale. Jedes Portal hat einen eigenen Charakter, eigene Leserschaft und eigene Themen-Schwerpunkte.
pressekonto.de ist nicht „irgendein Tool" — es ist die zentrale Plattform für unsere beiden redaktionell geführten Pressefachportale. Jedes Portal hat einen eigenen Charakter, eigene Leserschaft und eigene Themen-Schwerpunkte.
</p>
</header>
@ -856,7 +856,7 @@
<path d="M9 12h6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="text-[13px] leading-[1.55]">
Pressemitteilungen, die Sie über presseportale.com einreichen, erscheinen auf <strong class="text-white font-semibold">beiden Portalen</strong> — ohne Aufpreis, ohne doppelte Eingabe. Sie haben eine zentrale Mitteilungs-Verwaltung und eine zentrale Reichweiten-Statistik.
Pressemitteilungen, die Sie über pressekonto.de einreichen, erscheinen auf <strong class="text-white font-semibold">beiden Portalen</strong> — ohne Aufpreis, ohne doppelte Eingabe. Sie haben eine zentrale Mitteilungs-Verwaltung und eine zentrale Reichweiten-Statistik.
</div>
</div>
</section>
@ -876,7 +876,7 @@
<!-- Fließtext-Nennung — wie auf den Brand-Portalen, kein Logo-Grid -->
<div>
<p class="text-[16px] leading-[1.7] text-ink-2 m-0">
Über presseportale.com veröffentlichen unter anderem
Über pressekonto.de veröffentlichen unter anderem
<a class="text-hub font-semibold underline underline-offset-[3px] decoration-hub/25 hover:decoration-hub" href="#">Siemens AG</a>,
<a class="text-hub font-semibold underline underline-offset-[3px] decoration-hub/25 hover:decoration-hub" href="#">BASF SE</a>,
<a class="text-hub font-semibold underline underline-offset-[3px] decoration-hub/25 hover:decoration-hub" href="#">Deutsche Bank</a>,
@ -1069,7 +1069,7 @@
<div>
<div class="text-[24px] font-bold leading-none tracking-[-0.5px] text-white">
presseportale<span style="color:#B07A3A;">.com</span>
pressekonto<span style="color:#B07A3A;">.com</span>
</div>
<div class="eyebrow on-dark mt-2 text-[9.5px] tracking-[0.22em]" style="color:#7B8FBF;">
Publisher · Hub
@ -1121,7 +1121,7 @@
<div class="border-t border-white/15">
<div class="max-w-layout mx-auto px-8 py-5 flex items-center justify-between gap-4 text-[11.5px] text-white/55">
<span>© 2026 presseportale.com · Alle Rechte vorbehalten</span>
<span>© 2026 pressekonto.de · Alle Rechte vorbehalten</span>
<span class="flex items-center gap-2 font-mono text-[10.5px]">
<span class="inline-block w-1.5 h-1.5 rounded-full bg-ok"></span>
Alle Systeme betriebsbereit

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>presseportale.com — Anmelden</title>
<title>pressekonto.de — Anmelden</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@ -160,7 +160,7 @@
<header class="relative z-10 px-10 py-[22px] flex items-center justify-between">
<a href="#" class="flex items-baseline gap-2.5 no-underline">
<span class="text-[19px] font-bold tracking-[-0.4px] text-hub leading-none">
presseportale<span class="text-accent">.com</span>
pressekonto<span class="text-accent">.com</span>
</span>
<span class="inline-block w-px h-[14px] bg-bg-rule"></span>
<span class="text-[9.5px] font-bold tracking-[0.22em] uppercase text-ink-3">

View file

@ -0,0 +1,767 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>presseportale.com — Mein Dashboard · Dark</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
/* DARK MODE — Tokens umgewidmet, gleiche Namen wie Light-Variante */
bg: "#0E1218",
"bg-elev": "#14181F",
"bg-rule": "#2A3142",
"bg-rule-2": "#232838",
"bg-card": "#181D27",
"bg-card-warm": "#1F1A12",
"hub": "#5A78C2",
"hub-2": "#6D8AD3",
"hub-3": "#4A65A8",
"hub-soft": "#1F2A47",
"hub-soft-2":"#2C3A5D",
"hub-line": "#7B8FBF",
"accent": "#D9A560",
"accent-deep": "#B07A3A",
"accent-soft": "#2A2418",
ink: "#ECE9E0",
"ink-2":"#C9C5B8",
"ink-3":"#8E8B82",
"ink-4":"#5D5C57",
"ink-on-dark": "#F6F4EF",
"ink-on-dark-2": "#B2B9C7",
"ink-on-dark-3": "#7B8FBF",
ok: "#4DC076",
"ok-soft":"#1A2D22",
warn: "#D9A560",
"warn-soft":"#2D2418",
err: "#E07664",
"err-soft":"#2E1715",
},
fontFamily: {
sans: ['"Inter Tight"','Inter','system-ui','sans-serif'],
mono: ['"JetBrains Mono"','"SF Mono"','ui-monospace','monospace'],
},
backgroundImage: {
"hub-grad": "linear-gradient(135deg,#1A2540 0%,#243152 100%)",
"hub-grad-2": "linear-gradient(180deg,#1A2540 0%,#0F1729 100%)",
"accent-grad": "linear-gradient(135deg,#B07A3A 0%,#8A5E27 100%)",
}
}
}
};
</script>
<style>
html { color-scheme: dark; }
html,body { margin:0; padding:0; }
body { background:#0A0D12; font-family:"Inter Tight",system-ui,sans-serif; }
.eyebrow {
font-size: 10.5px; font-weight: 700;
letter-spacing: 0.20em; text-transform: uppercase;
color: #B2B9C7;
}
.eyebrow.muted { color:#8E8B82; letter-spacing:0.16em; font-weight:600; font-size:10px; }
.eyebrow.accent { color:#D9A560; }
.eyebrow.on-dark { color:#7B8FBF; }
.section-eyebrow {
display:inline-flex; align-items:center; gap:10px;
font-size: 10.5px; font-weight: 700;
letter-spacing: 0.22em; text-transform: uppercase;
color:#B2B9C7;
}
.section-eyebrow::after {
content:""; display:block; width:30px; height:1px;
background:#B2B9C7; opacity:.35;
}
.rule { height:1px; background:#2A3142; border:0; margin:0; }
.rule-strong { height:1px; background:#ECE9E0; border:0; margin:0; }
/* Sidebar */
.nav-item {
display:flex; align-items:center; gap:11px;
padding:8px 12px;
border-radius:4px;
font-size:13px; font-weight:500;
color:#C9C5B8;
transition: background .12s, color .12s;
position:relative;
}
.nav-item:hover { background:#1F2A47; color:#ECE9E0; }
.nav-item.active {
background:#1F2A47;
color:#ECE9E0;
font-weight:600;
}
.nav-item.active::before {
content:""; position:absolute; left:-1px; top:6px; bottom:6px;
width:2px; background:#7B8FBF; border-radius:0 2px 2px 0;
}
.nav-item.disabled { color:#5D5C57; cursor:default; }
.nav-item.disabled:hover { background:transparent; color:#5D5C57; }
.nav-item .ico {
width:16px; height:16px; flex-shrink:0;
color: currentColor; opacity:.8;
}
.nav-item.active .ico { opacity:1; }
.nav-section {
font-size:10px; font-weight:700; letter-spacing:0.18em;
text-transform:uppercase; color:#5D5C57;
padding: 0 12px 6px;
}
/* Card / panel headers */
.panel {
background:#181D27; border:1px solid #2A3142; border-radius:6px;
}
.panel-warm { background:#14181F; border:1px solid #2A3142; border-radius:6px; }
.panel-dark { background:#0F1729; border:1px solid #1A2540; border-radius:6px; color:#F6F4EF; }
.panel-head {
display:flex; align-items:center; justify-content:space-between;
padding:14px 20px;
border-bottom:1px solid #2A3142;
}
.panel-dark .panel-head { border-bottom-color: rgba(255,255,255,.08); }
/* Stat cards */
.stat-card {
position:relative;
background:#181D27; border:1px solid #2A3142; border-radius:6px;
padding:18px 20px;
transition: border-color .12s, box-shadow .12s;
}
.stat-card .stat-strip {
position:absolute; left:0; top:0; bottom:0; width:3px;
background:#2C3A5D; border-radius:6px 0 0 6px;
}
.stat-card.is-primary .stat-strip { background:#5A78C2; }
.stat-card.is-ok .stat-strip { background:#4DC076; }
.stat-card.is-warn .stat-strip { background:#D9A560; }
.stat-card.is-muted .stat-strip { background:#5D5C57; }
.stat-label {
font-size:10.5px; font-weight:700;
letter-spacing:0.16em; text-transform:uppercase;
color:#8E8B82;
}
.stat-card.is-ok .stat-label { color:#4DC076; }
.stat-card.is-warn .stat-label { color:#D9A560; }
.stat-card.is-muted .stat-label { color:#8E8B82; }
.stat-num {
font-family:"JetBrains Mono","SF Mono",ui-monospace,monospace;
font-variant-numeric:tabular-nums;
font-size:36px; font-weight:600; color:#ECE9E0;
letter-spacing:-0.5px; line-height:1;
margin-top:14px;
}
/* Hint chips (Datenqualität) */
.hint-card {
display:grid; gap:14px;
grid-template-columns: auto 1fr;
align-items:start;
background:#1F1A12; border:1px solid #2D2418; border-left:3px solid #B07A3A;
border-radius:5px;
padding:16px 18px;
}
.hint-card .hint-ico {
width:36px; height:36px; border-radius:4px;
background:#2D2418; color:#D9A560;
display:flex; align-items:center; justify-content:center;
flex-shrink:0;
}
/* FluxUI-style buttons */
.btn-primary {
display:inline-flex; align-items:center; gap:8px; justify-content:center;
padding:9px 16px;
background:#5A78C2; color:#FFFFFF;
border-radius:4px; font-size:13px; font-weight:600;
transition: background .15s;
}
.btn-primary:hover { background:#6D8AD3; }
.btn-secondary {
display:inline-flex; align-items:center; gap:8px; justify-content:center;
padding:8px 14px;
background:#181D27; color:#ECE9E0;
border:1px solid #2A3142; border-radius:4px;
font-size:12.5px; font-weight:600;
transition: border-color .15s, background .15s;
}
.btn-secondary:hover { border-color:#5A78C2; background:#1F2A47; }
.badge {
display:inline-flex; align-items:center; gap:6px;
padding:3px 9px; border-radius:99px;
font-size:10.5px; font-weight:700;
letter-spacing:0.10em; text-transform:uppercase;
}
.badge.warn { background:#2D2418; color:#D9A560; }
.badge.ok { background:#1A2D22; color:#4DC076; }
.badge.hub { background:#1F2A47; color:#B2B9C7; }
.badge.dot::before {
content:""; width:6px; height:6px; border-radius:99px; background:currentColor;
}
/* Bridge ribbon - sehr subtil */
.bridge-row {
display:inline-flex; align-items:center; gap:6px;
font-family:"JetBrains Mono","SF Mono",ui-monospace,monospace;
font-size:10.5px; letter-spacing:0.10em; text-transform:uppercase;
color:#8E8B82;
}
.dot-pe { width:6px; height:6px; border-radius:99px; background:#4DA37A; }
.dot-bp { width:6px; height:6px; border-radius:99px; background:#E36340; }
</style>
</head>
<body class="bg-bg text-ink font-sans antialiased">
<!-- ============== ARTBOARD ============== -->
<div class="mx-auto bg-bg" style="width:1440px;">
<div class="flex" style="min-height:980px;">
<!-- =============================================
SIDEBAR — schmal, warm, klare Hierarchie
(FluxUI <flux:navlist> + Slot-Header-Styles)
============================================== -->
<aside class="bg-bg-elev border-r border-bg-rule flex flex-col" style="width:260px;">
<!-- Brand-Block -->
<div class="px-5 pt-6 pb-5">
<a href="Hub Landing presseportale.html" class="flex items-baseline gap-2">
<span class="text-[19px] font-bold tracking-[-0.4px] text-hub leading-none">presseportale<span class="text-accent">.com</span></span>
</a>
<div class="eyebrow muted mt-2">Publisher · Hub</div>
<!-- Aktive Firma / Konto-Switcher (FluxUI-Slot: header before nav) -->
<button class="mt-4 w-full grid items-center gap-2.5 px-3 py-2.5 bg-bg-card border border-bg-rule rounded-[4px] hover:border-hub/40 text-left"
style="grid-template-columns:auto 1fr auto;">
<span class="w-7 h-7 rounded-[3px] bg-hub-soft border border-hub-soft-2 flex items-center justify-center text-hub text-[11px] font-bold">TU</span>
<span class="min-w-0">
<span class="block text-[12.5px] font-semibold text-ink leading-tight truncate">Test User</span>
<span class="block text-[10.5px] text-ink-3 leading-tight mt-0.5 truncate">Keine Firma zugeordnet</span>
</span>
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" class="text-ink-3">
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 7.5l3-3 3 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" opacity="0.4"/>
</svg>
</button>
</div>
<nav class="px-3 flex-1">
<div class="nav-section">Mein Bereich</div>
<div class="space-y-0.5 mb-5">
<a class="nav-item active" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M2 7l6-5 6 5v7H2z" stroke="currentColor" stroke-width="1.4"/><path d="M6 14V9h4v5" stroke="currentColor" stroke-width="1.4"/></svg>
Übersicht
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="2.5" width="9" height="11" stroke="currentColor" stroke-width="1.4"/><path d="M11.5 5h2v8.5H4" stroke="currentColor" stroke-width="1.4"/><path d="M5 5.5h4M5 8h4M5 10.5h2.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
Meine Pressemitteilungen
<span class="badge hub ml-auto" style="font-size:9.5px;padding:1px 6px;letter-spacing:0.08em;">0</span>
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.4"/><path d="M2.5 6h11" stroke="currentColor" stroke-width="1.4"/><path d="M6 9h1M9 9h1M6 11.5h1M9 11.5h1" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Firmen
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 5h10l-1 9H4z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6 5V3.5a2 2 0 014 0V5" stroke="currentColor" stroke-width="1.4"/></svg>
Buchungen &amp; Add-ons
</a>
<span class="nav-item disabled">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 13V8M7 13V5M11 13V9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Statistiken
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
</span>
</div>
<div class="nav-section">Finanzen</div>
<div class="space-y-0.5 mb-5">
<span class="nav-item disabled">
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="5" stroke="currentColor" stroke-width="1.4"/><path d="M8 5.5v5M6 8h4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Credits &amp; Tarif
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
</span>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 2.5h7l3 3v8H3z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M10 2.5V5.5h3" stroke="currentColor" stroke-width="1.4"/><path d="M5.5 8h5M5.5 10.5h5M5.5 6h2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
Rechnungen
</a>
<span class="nav-item disabled">
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="4.5" width="11" height="7.5" stroke="currentColor" stroke-width="1.4"/><path d="M2.5 7h11" stroke="currentColor" stroke-width="1.4"/></svg>
Zahlungsarten
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
</span>
</div>
<div class="nav-section">Konto</div>
<div class="space-y-0.5 mb-5">
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M3 13.5c.7-2.4 2.7-4 5-4s4.3 1.6 5 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Profil
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M8 2l5 2v4c0 3-2 5-5 6-3-1-5-3-5-6V4z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6 8l1.5 1.5L10.5 6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Sicherheit
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="6" cy="8" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M8.5 8h5M11 8v2.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
API &amp; Integrationen
</a>
<span class="nav-item disabled">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3.5 7a4.5 4.5 0 119 0v3.5l1 1.5H2.5l1-1.5z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6.5 13a1.5 1.5 0 003 0" stroke="currentColor" stroke-width="1.4"/></svg>
Benachrichtigungen
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
</span>
</div>
</nav>
<!-- Testmodus-Block (war im Original „Zurück zum Admin") -->
<div class="px-4 pb-4">
<div class="text-ink-on-dark rounded-[5px] p-4 relative overflow-hidden" style="background:linear-gradient(135deg,#1A2540 0%,#243152 100%); border:1px solid #2A3142;">
<div class="absolute -top-6 -right-6 w-16 h-16 rounded-full opacity-50" style="background:#2E3D66;"></div>
<div class="absolute -bottom-8 -left-8 w-20 h-20 rounded-full opacity-30" style="background:#2E3D66;"></div>
<div class="relative">
<div class="flex items-center gap-2 mb-2">
<span class="w-1.5 h-1.5 rounded-full animate-pulse" style="background:#D9A560;"></span>
<span class="eyebrow on-dark" style="color:#F4D89C;">Testmodus aktiv</span>
</div>
<div class="text-[12px] leading-[1.5] text-ink-on-dark-2">
Angemeldet als <strong class="text-white font-semibold">Test User</strong>.<br/>
Admin: <strong class="text-white font-semibold">Portal Admin</strong>
</div>
<button class="mt-3 w-full px-3 py-2 text-[12px] font-semibold rounded-[3px] transition-colors flex items-center justify-center gap-1.5" style="background:#F6F4EF;color:#1A2540;">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none">
<path d="M9 3L3 9M3 9H8M3 9V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
Zurück zum Admin
</button>
</div>
</div>
</div>
<!-- Resources -->
<div class="px-3 pb-5 border-t border-bg-rule pt-4">
<div class="nav-section">Resources</div>
<div class="space-y-0.5">
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 11l4 2 6-9-4-1z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>Tailwind CSS</a>
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M8 2l5 2v4c0 3-2 5-5 6-3-1-5-3-5-6V4z" stroke="currentColor" stroke-width="1.3"/></svg>Hero Icons</a>
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M9 2L4 9h3l-1 5 5-7H8z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>Flux UI</a>
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 4l5-2 5 2v8l-5 2-5-2z" stroke="currentColor" stroke-width="1.3"/><path d="M3 4l5 2 5-2M8 6v8" stroke="currentColor" stroke-width="1.3"/></svg>Repository</a>
</div>
</div>
</aside>
<!-- =============================================
MAIN — Inhalt
============================================== -->
<main class="flex-1 min-w-0">
<!-- Topbar — sehr schlank; Brücken-Kontext + Kontoinfo rechts -->
<div class="bg-bg-elev border-b border-bg-rule">
<div class="px-10 py-3 flex items-center gap-6">
<!-- Breadcrumb -->
<div class="flex items-center gap-2 text-[12px] text-ink-3 font-medium">
<a href="Hub Landing presseportale.html" class="hover:text-hub">Hub</a>
<span class="text-ink-4">/</span>
<span class="text-ink-2">User Backend</span>
<span class="text-ink-4">/</span>
<span class="text-hub font-semibold">Übersicht</span>
</div>
<span class="flex-1"></span>
<!-- Reichweiten-Kontext (Brücke zur Hub-CI) -->
<span class="bridge-row">
<span class="dot-pe"></span> presseecho
<span class="text-ink-4 mx-1">·</span>
<span class="dot-bp"></span> businessportal24
</span>
<span class="w-px h-5 bg-bg-rule"></span>
<!-- Search (FluxUI input slot) -->
<div class="relative">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" class="absolute left-2.5 top-1/2 -translate-y-1/2 text-ink-3">
<circle cx="7" cy="7" r="4.5" stroke="currentColor" stroke-width="1.3"/>
<path d="M10.5 10.5L13 13" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg>
<input class="pl-8 pr-3 py-1.5 w-[220px] bg-bg-card border border-bg-rule rounded-[4px] text-[12.5px] text-ink placeholder:text-ink-4 focus:outline-none focus:border-hub" placeholder="Suchen…" />
<span class="absolute right-2 top-1/2 -translate-y-1/2 text-[10px] font-mono text-ink-4 border border-bg-rule rounded px-1">⌘K</span>
</div>
<button class="relative w-8 h-8 flex items-center justify-center rounded-[4px] hover:bg-bg-card border border-transparent hover:border-bg-rule">
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" class="text-ink-2">
<path d="M3.5 7a4.5 4.5 0 119 0v3.5l1 1.5H2.5l1-1.5z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/>
<path d="M6.5 13a1.5 1.5 0 003 0" stroke="currentColor" stroke-width="1.3"/>
</svg>
<span class="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-accent"></span>
</button>
<a href="#" class="btn-primary text-[12px] py-1.5 px-3.5">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none">
<path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
</svg>
Neue Mitteilung
</a>
</div>
</div>
<!-- Inhalt -->
<div class="px-10 py-8 space-y-6">
<!-- ============== PAGE HEADER ============== -->
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">User Backend</span>
<span class="eyebrow muted">Mein Bereich · A · 01</span>
</div>
<h1 class="text-[34px] font-bold tracking-[-0.7px] text-ink leading-[1.1] m-0">Mein Dashboard</h1>
<p class="text-[13.5px] text-ink-2 leading-[1.55] mt-2 m-0 max-w-[640px]">
Willkommen zurück, <strong class="font-semibold text-ink">Test User</strong>. Hier sehen Sie Status und Reichweite Ihres Kundenkontos für presseecho und businessportal24.
</p>
</div>
<div class="flex items-center gap-3 flex-shrink-0">
<!-- Zeitraum-Filter (FluxUI dropdown-style) -->
<button class="btn-secondary whitespace-nowrap">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.3"/><path d="M2.5 6h11M5 2v3M11 2v3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
Diese Woche
<svg width="10" height="10" viewBox="0 0 12 12" fill="none"><path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<!-- Firma-zuordnen-Status — als Banner-Pille, statt im Karte-Header eingesperrt -->
<span class="inline-flex items-center gap-2 px-3 py-1.5 bg-warn-soft border border-warn/30 text-accent rounded-[4px] text-[12px] font-semibold whitespace-nowrap">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" class="flex-shrink-0"><path d="M8 2l6 11H2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M8 7v3M8 11.5v.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Keine Firma zugeordnet
<a href="#" class="underline underline-offset-[3px] decoration-accent/40 hover:decoration-accent">zuordnen →</a>
</span>
</div>
</header>
<!-- ============== STAT-CARDS — KPI-Reihe ============== -->
<section class="grid gap-4" style="grid-template-columns:repeat(4,1fr);">
<article class="stat-card is-primary">
<span class="stat-strip"></span>
<div class="flex items-baseline justify-between">
<div class="stat-label" style="color:#7B8FBF;">Gesamt</div>
<span class="text-[10.5px] font-mono text-ink-3 tracking-[0.14em] uppercase">2026</span>
</div>
<div class="stat-num">0</div>
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
<span class="flex items-center gap-1">
<svg width="9" height="9" viewBox="0 0 10 10" fill="none"><path d="M5 8V2M2 5l3-3 3 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
0 ggü. Vormonat
</span>
</div>
<!-- Mini Sparkline placeholder -->
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
<path d="M0 18 L10 17 L20 18 L30 16 L40 18 L50 15 L60 17 L70 14 L80 16" stroke="#1A2540" stroke-width="1.2" fill="none"/>
<circle cx="80" cy="16" r="1.8" fill="#1A2540"/>
</svg>
</article>
<article class="stat-card is-ok">
<span class="stat-strip"></span>
<div class="flex items-baseline justify-between">
<div class="stat-label">Veröffentlicht</div>
<span class="badge ok" style="font-size:9.5px;padding:1px 6px;">live</span>
</div>
<div class="stat-num" style="color:#6BD498;">0</div>
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
auf beiden Portalen
</div>
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
<path d="M0 19 L10 18 L20 19 L30 17 L40 16 L50 14 L60 13 L70 11 L80 10" stroke="#2E8540" stroke-width="1.2" fill="none"/>
<circle cx="80" cy="10" r="1.8" fill="#2E8540"/>
</svg>
</article>
<article class="stat-card is-warn">
<span class="stat-strip"></span>
<div class="flex items-baseline justify-between">
<div class="stat-label">In Prüfung</div>
<span class="badge warn" style="font-size:9.5px;padding:1px 6px;">Ø 4 h</span>
</div>
<div class="stat-num" style="color:#D9A560;">0</div>
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
redaktionelle Prüfung
</div>
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
<path d="M0 16 L10 17 L20 14 L30 18 L40 13 L50 17 L60 12 L70 15 L80 11" stroke="#A87A1F" stroke-width="1.2" fill="none"/>
<circle cx="80" cy="11" r="1.8" fill="#A87A1F"/>
</svg>
</article>
<article class="stat-card is-muted">
<span class="stat-strip"></span>
<div class="flex items-baseline justify-between">
<div class="stat-label">Entwürfe</div>
<span class="text-[10.5px] font-mono text-ink-3 tracking-[0.14em] uppercase">privat</span>
</div>
<div class="stat-num" style="color:#8E8B82;">0</div>
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
gespeichert, nicht eingereicht
</div>
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
<path d="M0 12 L10 12 L20 12 L30 12 L40 12 L50 12 L60 12 L70 12 L80 12" stroke="#8A918D" stroke-width="1.2" stroke-dasharray="2 2" fill="none"/>
</svg>
</article>
</section>
<!-- ============== ZWEISPALTEN-GRID ============== -->
<section class="grid gap-6" style="grid-template-columns:2fr 1fr;">
<!-- ============== LINKS: Pressemitteilungen-Liste / Empty ============== -->
<article class="panel">
<div class="panel-head">
<div class="flex items-center gap-3">
<span class="section-eyebrow">Meine letzten Pressemitteilungen</span>
</div>
<div class="flex items-center gap-3">
<span class="text-[11.5px] text-ink-3">0 von 0</span>
<a href="#" class="text-[12px] font-semibold text-hub hover:underline underline-offset-[3px] decoration-hub/30">Alle anzeigen →</a>
</div>
</div>
<!-- Empty State — sauber, ohne überlappende Skelett-Zeilen -->
<div class="px-10 py-14 flex flex-col items-center text-center">
<!-- Dezentes Tabellen-Icon -->
<div class="w-16 h-16 rounded-[6px] bg-hub-soft border border-hub-soft-2 flex items-center justify-center text-hub mb-5 relative">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="14" height="16" stroke="currentColor" stroke-width="1.5"/>
<path d="M17 7h4v13H6" stroke="currentColor" stroke-width="1.5"/>
<path d="M6 8h8M6 11h8M6 14h5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg>
<span class="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full bg-accent text-white text-[10px] font-bold flex items-center justify-center font-mono">0</span>
</div>
<div class="text-[16px] font-semibold text-ink m-0">Noch keine Pressemitteilungen</div>
<p class="text-[13px] text-ink-3 leading-[1.55] mt-2 m-0 max-w-[460px]">
Starten Sie mit einer ersten Mitteilung für die aktive Firma oder Ihr Kundenkonto. Veröffentlichung erfolgt nach redaktioneller Prüfung auf beiden Portalen.
</p>
<div class="mt-6 flex items-center gap-2.5">
<a href="#" class="btn-primary">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
Erste Pressemitteilung erstellen
</a>
<a href="#" class="btn-secondary">Vorlage öffnen</a>
</div>
<!-- Vorgeschlagene Schritte -->
<div class="mt-9 grid gap-3 w-full max-w-[560px]" style="grid-template-columns:repeat(3,1fr);">
<div class="text-left px-3 py-2.5 bg-bg-elev border border-bg-rule rounded-[3px]">
<div class="font-mono text-[9.5px] tracking-[0.16em] text-accent font-bold mb-1">01</div>
<div class="text-[11.5px] font-semibold text-ink leading-tight">Firma zuordnen</div>
</div>
<div class="text-left px-3 py-2.5 bg-bg-elev border border-bg-rule rounded-[3px]">
<div class="font-mono text-[9.5px] tracking-[0.16em] text-accent font-bold mb-1">02</div>
<div class="text-[11.5px] font-semibold text-ink leading-tight">Mitteilung verfassen</div>
</div>
<div class="text-left px-3 py-2.5 bg-bg-elev border border-bg-rule rounded-[3px]">
<div class="font-mono text-[9.5px] tracking-[0.16em] text-accent font-bold mb-1">03</div>
<div class="text-[11.5px] font-semibold text-ink leading-tight">Zur Prüfung senden</div>
</div>
</div>
</div>
<div class="px-5 py-3 bg-bg-elev border-t border-bg-rule flex items-center gap-2.5 text-[11.5px] text-ink-3">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" class="text-hub flex-shrink-0">
<path d="M8 1.5l5.5 2.5v4c0 3.3-2.2 5.8-5.5 6.5C4.7 13.8 2.5 11.3 2.5 8V4z" stroke="currentColor" stroke-width="1.3" fill="none"/>
<path d="M5.5 8l2 2 3.5-4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Tipp: Geprüfte Mitteilungen erscheinen i. d. R. binnen <strong class="text-ink-2 font-semibold">4 Stunden</strong> werktags auf beiden Portalen.
</div>
</article>
<!-- ============== RECHTS: Datenqualität ============== -->
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">Datenqualität</span>
<span class="badge warn dot">2 offen</span>
</div>
<div class="px-5 py-5">
<p class="text-[12px] text-ink-3 leading-[1.55] m-0 mb-4">
Diese Hinweise helfen, Ihr User Backend vollständig und sauber zu halten.
</p>
<div class="space-y-3">
<div class="hint-card">
<span class="hint-ico">
<svg width="18" height="18" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M3 13.5c.7-2.4 2.7-4 5-4s4.3 1.6 5 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
</span>
<div class="min-w-0">
<div class="flex items-baseline justify-between gap-3">
<div class="text-[13px] font-semibold text-ink leading-tight">Profil unvollständig</div>
<span class="font-mono text-[10px] text-ink-3 tracking-[0.10em] uppercase whitespace-nowrap flex-shrink-0">60 %</span>
</div>
<div class="mt-2 h-1 w-full rounded-full bg-bg-rule-2 overflow-hidden">
<div class="h-full rounded-full bg-accent" style="width:60%;"></div>
</div>
<p class="text-[11.5px] text-ink-3 leading-[1.5] mt-2 m-0">
Vorname, Telefon und Pressekontakt fehlen für saubere Kundenakte.
</p>
<a href="#" class="inline-flex items-center gap-1 text-[11.5px] font-semibold text-accent mt-2 hover:underline underline-offset-[3px] decoration-accent/40">Profil öffnen →</a>
</div>
</div>
<div class="hint-card">
<span class="hint-ico">
<svg width="18" height="18" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.4"/><path d="M2.5 6h11" stroke="currentColor" stroke-width="1.4"/></svg>
</span>
<div class="min-w-0">
<div class="flex items-baseline justify-between gap-3">
<div class="text-[13px] font-semibold text-ink leading-tight">Rechnungsadresse fehlt</div>
<span class="font-mono text-[10px] text-ink-3 tracking-[0.10em] uppercase whitespace-nowrap flex-shrink-0">0 %</span>
</div>
<div class="mt-2 h-1 w-full rounded-full bg-bg-rule-2 overflow-hidden">
<div class="h-full rounded-full bg-accent" style="width:5%;"></div>
</div>
<p class="text-[11.5px] text-ink-3 leading-[1.5] mt-2 m-0">
Hinterlegen Sie eine Rechnungsadresse, damit spätere Buchungen sauber abgerechnet werden können.
</p>
<a href="#" class="inline-flex items-center gap-1 text-[11.5px] font-semibold text-accent mt-2 hover:underline underline-offset-[3px] decoration-accent/40">Rechnungsadresse ergänzen →</a>
</div>
</div>
</div>
</div>
</article>
</section>
<!-- ============== UNTERER GRID: FIRMEN + REICHWEITE / AKTIVITÄT ============== -->
<section class="grid gap-6" style="grid-template-columns:2fr 1fr;">
<!-- Firmen Card -->
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">Meine Firmen</span>
<div class="flex items-center gap-3">
<span class="badge hub" style="font-size:9.5px;padding:1px 6px;">0 zugeordnet</span>
<a href="#" class="text-[12px] font-semibold text-hub hover:underline underline-offset-[3px] decoration-hub/30">Profil &amp; Firma verwalten →</a>
</div>
</div>
<div class="p-6">
<div class="grid gap-3" style="grid-template-columns:1fr 1fr;">
<!-- Empty-Slot Firma 1 -->
<div class="relative border border-dashed border-bg-rule rounded-[5px] p-5 hover:border-hub/50 hover:bg-bg-elev transition-colors">
<div class="flex items-center gap-3 mb-3">
<span class="w-10 h-10 rounded-[4px] border border-bg-rule bg-bg-elev flex items-center justify-center text-ink-4">
<svg width="18" height="18" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.3"/><path d="M2.5 6h11M6 9h1M9 9h1M6 11.5h1M9 11.5h1" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
</span>
<div>
<div class="text-[13.5px] font-semibold text-ink-2">Firma hinzufügen</div>
<div class="text-[11px] text-ink-3 mt-0.5">Slot frei · 1 von 3</div>
</div>
</div>
<p class="text-[11.5px] text-ink-3 leading-[1.5] m-0">
Pressestellen, für die Sie Mitteilungen erstellen — mit eigenem Logo, Kontaktperson und Themen-Tags.
</p>
<a href="#" class="absolute top-3 right-3 inline-flex items-center justify-center w-7 h-7 rounded-[3px] bg-bg-card border border-bg-rule text-hub hover:border-hub">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
</a>
</div>
<!-- Hinweis -->
<div class="bg-bg-elev border border-bg-rule rounded-[5px] p-5">
<div class="eyebrow muted mb-2">Hinweis</div>
<div class="text-[13px] text-ink-2 leading-[1.55] m-0">
Keine Firmen zugeordnet. Wenn hier eine Firma fehlen sollte, prüfen Sie bitte Ihr Profil oder wenden Sie sich an den Support.
</div>
<div class="mt-3 flex items-center gap-2">
<a href="#" class="btn-secondary" style="padding:6px 10px;font-size:11.5px;">Profil prüfen</a>
<a href="/cdn-cgi/l/email-protection#e5969095958a9791a5959780969680958a9791848980cb868a88" class="text-[11.5px] font-semibold text-ink-3 hover:text-hub">Support kontaktieren</a>
</div>
</div>
</div>
</div>
</article>
<!-- Aktivität / Bridge -->
<article class="panel-dark">
<div class="panel-head">
<span class="eyebrow on-dark">Brand-Bridge</span>
<span class="font-mono text-[10px] text-ink-on-dark-3 tracking-[0.14em] uppercase">A · B</span>
</div>
<div class="px-5 py-5">
<div class="text-[12.5px] leading-[1.55] text-ink-on-dark-2 m-0 mb-4">
Ein Konto, beide Portale — Veröffentlichungen werden parallel auf presseecho und businessportal24 ausgespielt.
</div>
<div class="grid gap-3" style="grid-template-columns:1fr 1fr;">
<div class="rounded-[4px] px-3.5 py-3 border border-white/5" style="background:#1A2540;">
<div class="flex items-center gap-2 mb-1.5">
<span class="dot-pe"></span>
<span class="text-[11px] font-bold tracking-[0.14em] uppercase text-white/85">presseecho</span>
</div>
<div class="font-mono text-[15px] font-semibold text-white" style="font-variant-numeric:tabular-nums;">verbunden</div>
<div class="text-[10.5px] text-ink-on-dark-3 mt-0.5">Archiv · Branchen-Tiefe</div>
</div>
<div class="rounded-[4px] px-3.5 py-3 border border-white/5" style="background:#1A2540;">
<div class="flex items-center gap-2 mb-1.5">
<span class="dot-bp"></span>
<span class="text-[11px] font-bold tracking-[0.14em] uppercase text-white/85">businessportal24</span>
</div>
<div class="font-mono text-[15px] font-semibold text-white" style="font-variant-numeric:tabular-nums;">verbunden</div>
<div class="text-[10.5px] text-ink-on-dark-3 mt-0.5">Wirtschaft · Live</div>
</div>
</div>
<hr class="mt-5 mb-4" style="border:0;height:1px;background:rgba(255,255,255,.10);" />
<div class="space-y-2 text-[11.5px] text-ink-on-dark-2">
<div class="flex items-center justify-between">
<span>API-Status</span>
<span class="flex items-center gap-1.5 text-white"><span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>operational</span>
</div>
<div class="flex items-center justify-between">
<span>Letzte Synchronisation</span>
<span class="font-mono text-white">vor 2 min</span>
</div>
<div class="flex items-center justify-between">
<span>Tarif</span>
<span class="font-mono text-white">Starter</span>
</div>
</div>
<a href="Hub Landing presseportale.html#tarife" class="mt-5 inline-flex items-center gap-1.5 text-[11.5px] font-semibold text-white hover:underline underline-offset-[3px] decoration-white/30">
Tarife &amp; Add-ons ansehen →
</a>
</div>
</article>
</section>
<!-- ============== FUSSZEILE ============== -->
<footer class="flex items-center justify-between pt-4 pb-2 text-[11px] text-ink-3 border-t border-bg-rule">
<span>© 2026 presseportale.com · Publisher-Hub</span>
<span class="flex items-center gap-5">
<a href="#" class="hover:text-hub">Tastenkürzel</a>
<a href="#" class="hover:text-hub">Changelog</a>
<a href="#" class="hover:text-hub">Statusseite</a>
<a href="/cdn-cgi/l/email-protection#21525451514e535561515344525244514e5355404d440f424e4c" class="hover:text-hub">Support</a>
</span>
</footer>
</div>
</main>
</div>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script></body>
</html>

View file

@ -0,0 +1,764 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>presseportale.com — Mein Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
bg: "#F6F4EF",
"bg-elev": "#FBFAF6",
"bg-rule": "#E2DDD0",
"bg-rule-2": "#EDE7D7",
"bg-card": "#FFFFFF",
"bg-card-warm": "#EFEADC",
"hub": "#1A2540",
"hub-2": "#243152",
"hub-3": "#2E3D66",
"hub-soft": "#E5E9F1",
"hub-soft-2":"#CFD6E4",
"hub-line": "#7B8FBF",
"accent": "#B07A3A",
"accent-deep": "#8A5E27",
"accent-soft": "#F1E6D3",
ink: "#1A1F1C",
"ink-2":"#3A413D",
"ink-3":"#5A6360",
"ink-4":"#8A918D",
"ink-on-dark": "#F6F4EF",
"ink-on-dark-2": "#B2B9C7",
"ink-on-dark-3": "#7B8FBF",
ok: "#2E8540",
"ok-soft":"#E2F1E5",
warn: "#A87A1F",
"warn-soft":"#F6EAC8",
err: "#A8331F",
"err-soft":"#F4DAD2",
},
fontFamily: {
sans: ['"Inter Tight"','Inter','system-ui','sans-serif'],
mono: ['"JetBrains Mono"','"SF Mono"','ui-monospace','monospace'],
},
backgroundImage: {
"hub-grad": "linear-gradient(135deg,#1A2540 0%,#243152 100%)",
"hub-grad-2": "linear-gradient(180deg,#1A2540 0%,#0F1729 100%)",
"accent-grad": "linear-gradient(135deg,#B07A3A 0%,#8A5E27 100%)",
}
}
}
};
</script>
<style>
html,body { margin:0; padding:0; }
body { background:#E8E4DA; font-family:"Inter Tight",system-ui,sans-serif; }
.eyebrow {
font-size: 10.5px; font-weight: 700;
letter-spacing: 0.20em; text-transform: uppercase;
color: #1A2540;
}
.eyebrow.muted { color:#5A6360; letter-spacing:0.16em; font-weight:600; font-size:10px; }
.eyebrow.accent { color:#8A5E27; }
.eyebrow.on-dark { color:#7B8FBF; }
.section-eyebrow {
display:inline-flex; align-items:center; gap:10px;
font-size: 10.5px; font-weight: 700;
letter-spacing: 0.22em; text-transform: uppercase;
color:#1A2540;
}
.section-eyebrow::after {
content:""; display:block; width:30px; height:1px;
background:#1A2540; opacity:.45;
}
.rule { height:1px; background:#E2DDD0; border:0; margin:0; }
.rule-strong { height:1px; background:#1A1F1C; border:0; margin:0; }
/* Sidebar */
.nav-item {
display:flex; align-items:center; gap:11px;
padding:8px 12px;
border-radius:4px;
font-size:13px; font-weight:500;
color:#3A413D;
transition: background .12s, color .12s;
position:relative;
}
.nav-item:hover { background:#F6F4EF; color:#1A2540; }
.nav-item.active {
background:#E5E9F1;
color:#1A2540;
font-weight:600;
}
.nav-item.active::before {
content:""; position:absolute; left:-1px; top:6px; bottom:6px;
width:2px; background:#1A2540; border-radius:0 2px 2px 0;
}
.nav-item.disabled { color:#8A918D; cursor:default; }
.nav-item.disabled:hover { background:transparent; color:#8A918D; }
.nav-item .ico {
width:16px; height:16px; flex-shrink:0;
color: currentColor; opacity:.8;
}
.nav-item.active .ico { opacity:1; }
.nav-section {
font-size:10px; font-weight:700; letter-spacing:0.18em;
text-transform:uppercase; color:#8A918D;
padding: 0 12px 6px;
}
/* Card / panel headers */
.panel {
background:#FFFFFF; border:1px solid #E2DDD0; border-radius:6px;
}
.panel-warm { background:#FBFAF6; border:1px solid #E2DDD0; border-radius:6px; }
.panel-dark { background:#1A2540; border:1px solid #0F1729; border-radius:6px; color:#F6F4EF; }
.panel-head {
display:flex; align-items:center; justify-content:space-between;
padding:14px 20px;
border-bottom:1px solid #E2DDD0;
}
.panel-dark .panel-head { border-bottom-color: rgba(255,255,255,.10); }
/* Stat cards */
.stat-card {
position:relative;
background:#FFFFFF; border:1px solid #E2DDD0; border-radius:6px;
padding:18px 20px;
transition: border-color .12s, box-shadow .12s;
}
.stat-card .stat-strip {
position:absolute; left:0; top:0; bottom:0; width:3px;
background:#CFD6E4; border-radius:6px 0 0 6px;
}
.stat-card.is-primary .stat-strip { background:#1A2540; }
.stat-card.is-ok .stat-strip { background:#2E8540; }
.stat-card.is-warn .stat-strip { background:#A87A1F; }
.stat-card.is-muted .stat-strip { background:#8A918D; }
.stat-label {
font-size:10.5px; font-weight:700;
letter-spacing:0.16em; text-transform:uppercase;
color:#5A6360;
}
.stat-card.is-ok .stat-label { color:#2E8540; }
.stat-card.is-warn .stat-label { color:#A87A1F; }
.stat-card.is-muted .stat-label { color:#8A918D; }
.stat-num {
font-family:"JetBrains Mono","SF Mono",ui-monospace,monospace;
font-variant-numeric:tabular-nums;
font-size:36px; font-weight:600; color:#1A1F1C;
letter-spacing:-0.5px; line-height:1;
margin-top:14px;
}
/* Hint chips (Datenqualität) */
.hint-card {
display:grid; gap:14px;
grid-template-columns: auto 1fr;
align-items:start;
background:#FBFAF6; border:1px solid #E2DDD0; border-left:3px solid #B07A3A;
border-radius:5px;
padding:16px 18px;
}
.hint-card .hint-ico {
width:36px; height:36px; border-radius:4px;
background:#F1E6D3; color:#8A5E27;
display:flex; align-items:center; justify-content:center;
flex-shrink:0;
}
/* FluxUI-style standard button (the "blue" one in screenshots) — kept, but harmonised */
.btn-primary {
display:inline-flex; align-items:center; gap:8px; justify-content:center;
padding:9px 16px;
background:#1A2540; color:#FFFFFF;
border-radius:4px; font-size:13px; font-weight:600;
transition: background .15s;
}
.btn-primary:hover { background:#243152; }
.btn-secondary {
display:inline-flex; align-items:center; gap:8px; justify-content:center;
padding:8px 14px;
background:#FFFFFF; color:#1A2540;
border:1px solid #CFD6E4; border-radius:4px;
font-size:12.5px; font-weight:600;
transition: border-color .15s, background .15s;
}
.btn-secondary:hover { border-color:#1A2540; background:#F6F4EF; }
.badge {
display:inline-flex; align-items:center; gap:6px;
padding:3px 9px; border-radius:99px;
font-size:10.5px; font-weight:700;
letter-spacing:0.10em; text-transform:uppercase;
}
.badge.warn { background:#F6EAC8; color:#8A5E27; }
.badge.ok { background:#E2F1E5; color:#1F5E2E; }
.badge.hub { background:#E5E9F1; color:#1A2540; }
.badge.dot::before {
content:""; width:6px; height:6px; border-radius:99px; background:currentColor;
}
/* Bridge ribbon - sehr subtil */
.bridge-row {
display:inline-flex; align-items:center; gap:6px;
font-family:"JetBrains Mono","SF Mono",ui-monospace,monospace;
font-size:10.5px; letter-spacing:0.10em; text-transform:uppercase;
color:#5A6360;
}
.dot-pe { width:6px; height:6px; border-radius:99px; background:#1F4D3A; }
.dot-bp { width:6px; height:6px; border-radius:99px; background:#C84A1E; }
</style>
</head>
<body class="bg-bg text-ink font-sans antialiased">
<!-- ============== ARTBOARD ============== -->
<div class="mx-auto bg-bg" style="width:1440px;">
<div class="flex" style="min-height:980px;">
<!-- =============================================
SIDEBAR — schmal, warm, klare Hierarchie
(FluxUI <flux:navlist> + Slot-Header-Styles)
============================================== -->
<aside class="bg-bg-elev border-r border-bg-rule flex flex-col" style="width:260px;">
<!-- Brand-Block -->
<div class="px-5 pt-6 pb-5">
<a href="Hub Landing presseportale.html" class="flex items-baseline gap-2">
<span class="text-[19px] font-bold tracking-[-0.4px] text-hub leading-none">presseportale<span class="text-accent">.com</span></span>
</a>
<div class="eyebrow muted mt-2">Publisher · Hub</div>
<!-- Aktive Firma / Konto-Switcher (FluxUI-Slot: header before nav) -->
<button class="mt-4 w-full grid items-center gap-2.5 px-3 py-2.5 bg-white border border-bg-rule rounded-[4px] hover:border-hub/40 text-left"
style="grid-template-columns:auto 1fr auto;">
<span class="w-7 h-7 rounded-[3px] bg-hub-soft border border-hub-soft-2 flex items-center justify-center text-hub text-[11px] font-bold">TU</span>
<span class="min-w-0">
<span class="block text-[12.5px] font-semibold text-ink leading-tight truncate">Test User</span>
<span class="block text-[10.5px] text-ink-3 leading-tight mt-0.5 truncate">Keine Firma zugeordnet</span>
</span>
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" class="text-ink-3">
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 7.5l3-3 3 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" opacity="0.4"/>
</svg>
</button>
</div>
<nav class="px-3 flex-1">
<div class="nav-section">Mein Bereich</div>
<div class="space-y-0.5 mb-5">
<a class="nav-item active" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M2 7l6-5 6 5v7H2z" stroke="currentColor" stroke-width="1.4"/><path d="M6 14V9h4v5" stroke="currentColor" stroke-width="1.4"/></svg>
Übersicht
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="2.5" width="9" height="11" stroke="currentColor" stroke-width="1.4"/><path d="M11.5 5h2v8.5H4" stroke="currentColor" stroke-width="1.4"/><path d="M5 5.5h4M5 8h4M5 10.5h2.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
Meine Pressemitteilungen
<span class="badge hub ml-auto" style="font-size:9.5px;padding:1px 6px;letter-spacing:0.08em;">0</span>
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.4"/><path d="M2.5 6h11" stroke="currentColor" stroke-width="1.4"/><path d="M6 9h1M9 9h1M6 11.5h1M9 11.5h1" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Firmen
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 5h10l-1 9H4z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6 5V3.5a2 2 0 014 0V5" stroke="currentColor" stroke-width="1.4"/></svg>
Buchungen &amp; Add-ons
</a>
<span class="nav-item disabled">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 13V8M7 13V5M11 13V9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Statistiken
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
</span>
</div>
<div class="nav-section">Finanzen</div>
<div class="space-y-0.5 mb-5">
<span class="nav-item disabled">
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="5" stroke="currentColor" stroke-width="1.4"/><path d="M8 5.5v5M6 8h4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Credits &amp; Tarif
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
</span>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 2.5h7l3 3v8H3z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M10 2.5V5.5h3" stroke="currentColor" stroke-width="1.4"/><path d="M5.5 8h5M5.5 10.5h5M5.5 6h2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
Rechnungen
</a>
<span class="nav-item disabled">
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="4.5" width="11" height="7.5" stroke="currentColor" stroke-width="1.4"/><path d="M2.5 7h11" stroke="currentColor" stroke-width="1.4"/></svg>
Zahlungsarten
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
</span>
</div>
<div class="nav-section">Konto</div>
<div class="space-y-0.5 mb-5">
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M3 13.5c.7-2.4 2.7-4 5-4s4.3 1.6 5 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Profil
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M8 2l5 2v4c0 3-2 5-5 6-3-1-5-3-5-6V4z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6 8l1.5 1.5L10.5 6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Sicherheit
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="6" cy="8" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M8.5 8h5M11 8v2.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
API &amp; Integrationen
</a>
<span class="nav-item disabled">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3.5 7a4.5 4.5 0 119 0v3.5l1 1.5H2.5l1-1.5z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6.5 13a1.5 1.5 0 003 0" stroke="currentColor" stroke-width="1.4"/></svg>
Benachrichtigungen
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
</span>
</div>
</nav>
<!-- Testmodus-Block (war im Original „Zurück zum Admin") -->
<div class="px-4 pb-4">
<div class="bg-hub text-ink-on-dark rounded-[5px] p-4 relative overflow-hidden">
<div class="absolute -top-6 -right-6 w-16 h-16 rounded-full bg-hub-3 opacity-50"></div>
<div class="absolute -bottom-8 -left-8 w-20 h-20 rounded-full bg-hub-3 opacity-30"></div>
<div class="relative">
<div class="flex items-center gap-2 mb-2">
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
<span class="eyebrow on-dark" style="color:#F4D89C;">Testmodus aktiv</span>
</div>
<div class="text-[12px] leading-[1.5] text-ink-on-dark-2">
Angemeldet als <strong class="text-white font-semibold">Test User</strong>.<br/>
Admin: <strong class="text-white font-semibold">Portal Admin</strong>
</div>
<button class="mt-3 w-full px-3 py-2 bg-white text-hub text-[12px] font-semibold rounded-[3px] hover:bg-bg transition-colors flex items-center justify-center gap-1.5">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none">
<path d="M9 3L3 9M3 9H8M3 9V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
Zurück zum Admin
</button>
</div>
</div>
</div>
<!-- Resources -->
<div class="px-3 pb-5 border-t border-bg-rule pt-4">
<div class="nav-section">Resources</div>
<div class="space-y-0.5">
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 11l4 2 6-9-4-1z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>Tailwind CSS</a>
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M8 2l5 2v4c0 3-2 5-5 6-3-1-5-3-5-6V4z" stroke="currentColor" stroke-width="1.3"/></svg>Hero Icons</a>
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M9 2L4 9h3l-1 5 5-7H8z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>Flux UI</a>
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 4l5-2 5 2v8l-5 2-5-2z" stroke="currentColor" stroke-width="1.3"/><path d="M3 4l5 2 5-2M8 6v8" stroke="currentColor" stroke-width="1.3"/></svg>Repository</a>
</div>
</div>
</aside>
<!-- =============================================
MAIN — Inhalt
============================================== -->
<main class="flex-1 min-w-0">
<!-- Topbar — sehr schlank; Brücken-Kontext + Kontoinfo rechts -->
<div class="bg-bg-elev border-b border-bg-rule">
<div class="px-10 py-3 flex items-center gap-6">
<!-- Breadcrumb -->
<div class="flex items-center gap-2 text-[12px] text-ink-3 font-medium">
<a href="Hub Landing presseportale.html" class="hover:text-hub">Hub</a>
<span class="text-ink-4">/</span>
<span class="text-ink-2">User Backend</span>
<span class="text-ink-4">/</span>
<span class="text-hub font-semibold">Übersicht</span>
</div>
<span class="flex-1"></span>
<!-- Reichweiten-Kontext (Brücke zur Hub-CI) -->
<span class="bridge-row">
<span class="dot-pe"></span> presseecho
<span class="text-ink-4 mx-1">·</span>
<span class="dot-bp"></span> businessportal24
</span>
<span class="w-px h-5 bg-bg-rule"></span>
<!-- Search (FluxUI input slot) -->
<div class="relative">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" class="absolute left-2.5 top-1/2 -translate-y-1/2 text-ink-3">
<circle cx="7" cy="7" r="4.5" stroke="currentColor" stroke-width="1.3"/>
<path d="M10.5 10.5L13 13" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg>
<input class="pl-8 pr-3 py-1.5 w-[220px] bg-white border border-bg-rule rounded-[4px] text-[12.5px] placeholder:text-ink-4 focus:outline-none focus:border-hub" placeholder="Suchen…" />
<span class="absolute right-2 top-1/2 -translate-y-1/2 text-[10px] font-mono text-ink-4 border border-bg-rule rounded px-1">⌘K</span>
</div>
<button class="relative w-8 h-8 flex items-center justify-center rounded-[4px] hover:bg-bg border border-transparent hover:border-bg-rule">
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" class="text-ink-2">
<path d="M3.5 7a4.5 4.5 0 119 0v3.5l1 1.5H2.5l1-1.5z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/>
<path d="M6.5 13a1.5 1.5 0 003 0" stroke="currentColor" stroke-width="1.3"/>
</svg>
<span class="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-accent"></span>
</button>
<a href="#" class="btn-primary text-[12px] py-1.5 px-3.5">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none">
<path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
</svg>
Neue Mitteilung
</a>
</div>
</div>
<!-- Inhalt -->
<div class="px-10 py-8 space-y-6">
<!-- ============== PAGE HEADER ============== -->
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">User Backend</span>
<span class="eyebrow muted">Mein Bereich · A · 01</span>
</div>
<h1 class="text-[34px] font-bold tracking-[-0.7px] text-ink leading-[1.1] m-0">Mein Dashboard</h1>
<p class="text-[13.5px] text-ink-2 leading-[1.55] mt-2 m-0 max-w-[640px]">
Willkommen zurück, <strong class="font-semibold text-ink">Test User</strong>. Hier sehen Sie Status und Reichweite Ihres Kundenkontos für presseecho und businessportal24.
</p>
</div>
<div class="flex items-center gap-3 flex-shrink-0">
<!-- Zeitraum-Filter (FluxUI dropdown-style) -->
<button class="btn-secondary whitespace-nowrap">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.3"/><path d="M2.5 6h11M5 2v3M11 2v3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
Diese Woche
<svg width="10" height="10" viewBox="0 0 12 12" fill="none"><path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<!-- Firma-zuordnen-Status — als Banner-Pille, statt im Karte-Header eingesperrt -->
<span class="inline-flex items-center gap-2 px-3 py-1.5 bg-warn-soft border border-warn/30 text-accent-deep rounded-[4px] text-[12px] font-semibold whitespace-nowrap">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" class="flex-shrink-0"><path d="M8 2l6 11H2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M8 7v3M8 11.5v.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Keine Firma zugeordnet
<a href="#" class="underline underline-offset-[3px] decoration-accent-deep/40 hover:decoration-accent-deep">zuordnen →</a>
</span>
</div>
</header>
<!-- ============== STAT-CARDS — KPI-Reihe ============== -->
<section class="grid gap-4" style="grid-template-columns:repeat(4,1fr);">
<article class="stat-card is-primary">
<span class="stat-strip"></span>
<div class="flex items-baseline justify-between">
<div class="stat-label" style="color:#1A2540;">Gesamt</div>
<span class="text-[10.5px] font-mono text-ink-3 tracking-[0.14em] uppercase">2026</span>
</div>
<div class="stat-num">0</div>
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
<span class="flex items-center gap-1">
<svg width="9" height="9" viewBox="0 0 10 10" fill="none"><path d="M5 8V2M2 5l3-3 3 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
0 ggü. Vormonat
</span>
</div>
<!-- Mini Sparkline placeholder -->
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
<path d="M0 18 L10 17 L20 18 L30 16 L40 18 L50 15 L60 17 L70 14 L80 16" stroke="#1A2540" stroke-width="1.2" fill="none"/>
<circle cx="80" cy="16" r="1.8" fill="#1A2540"/>
</svg>
</article>
<article class="stat-card is-ok">
<span class="stat-strip"></span>
<div class="flex items-baseline justify-between">
<div class="stat-label">Veröffentlicht</div>
<span class="badge ok" style="font-size:9.5px;padding:1px 6px;">live</span>
</div>
<div class="stat-num" style="color:#1F5E2E;">0</div>
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
auf beiden Portalen
</div>
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
<path d="M0 19 L10 18 L20 19 L30 17 L40 16 L50 14 L60 13 L70 11 L80 10" stroke="#2E8540" stroke-width="1.2" fill="none"/>
<circle cx="80" cy="10" r="1.8" fill="#2E8540"/>
</svg>
</article>
<article class="stat-card is-warn">
<span class="stat-strip"></span>
<div class="flex items-baseline justify-between">
<div class="stat-label">In Prüfung</div>
<span class="badge warn" style="font-size:9.5px;padding:1px 6px;">Ø 4 h</span>
</div>
<div class="stat-num" style="color:#8A5E27;">0</div>
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
redaktionelle Prüfung
</div>
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
<path d="M0 16 L10 17 L20 14 L30 18 L40 13 L50 17 L60 12 L70 15 L80 11" stroke="#A87A1F" stroke-width="1.2" fill="none"/>
<circle cx="80" cy="11" r="1.8" fill="#A87A1F"/>
</svg>
</article>
<article class="stat-card is-muted">
<span class="stat-strip"></span>
<div class="flex items-baseline justify-between">
<div class="stat-label">Entwürfe</div>
<span class="text-[10.5px] font-mono text-ink-3 tracking-[0.14em] uppercase">privat</span>
</div>
<div class="stat-num" style="color:#5A6360;">0</div>
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
gespeichert, nicht eingereicht
</div>
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
<path d="M0 12 L10 12 L20 12 L30 12 L40 12 L50 12 L60 12 L70 12 L80 12" stroke="#8A918D" stroke-width="1.2" stroke-dasharray="2 2" fill="none"/>
</svg>
</article>
</section>
<!-- ============== ZWEISPALTEN-GRID ============== -->
<section class="grid gap-6" style="grid-template-columns:2fr 1fr;">
<!-- ============== LINKS: Pressemitteilungen-Liste / Empty ============== -->
<article class="panel">
<div class="panel-head">
<div class="flex items-center gap-3">
<span class="section-eyebrow">Meine letzten Pressemitteilungen</span>
</div>
<div class="flex items-center gap-3">
<span class="text-[11.5px] text-ink-3">0 von 0</span>
<a href="#" class="text-[12px] font-semibold text-hub hover:underline underline-offset-[3px] decoration-hub/30">Alle anzeigen →</a>
</div>
</div>
<!-- Empty State — sauber, ohne überlappende Skelett-Zeilen -->
<div class="px-10 py-14 flex flex-col items-center text-center">
<!-- Dezentes Tabellen-Icon -->
<div class="w-16 h-16 rounded-[6px] bg-hub-soft border border-hub-soft-2 flex items-center justify-center text-hub mb-5 relative">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="14" height="16" stroke="currentColor" stroke-width="1.5"/>
<path d="M17 7h4v13H6" stroke="currentColor" stroke-width="1.5"/>
<path d="M6 8h8M6 11h8M6 14h5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg>
<span class="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full bg-accent text-white text-[10px] font-bold flex items-center justify-center font-mono">0</span>
</div>
<div class="text-[16px] font-semibold text-ink m-0">Noch keine Pressemitteilungen</div>
<p class="text-[13px] text-ink-3 leading-[1.55] mt-2 m-0 max-w-[460px]">
Starten Sie mit einer ersten Mitteilung für die aktive Firma oder Ihr Kundenkonto. Veröffentlichung erfolgt nach redaktioneller Prüfung auf beiden Portalen.
</p>
<div class="mt-6 flex items-center gap-2.5">
<a href="#" class="btn-primary">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
Erste Pressemitteilung erstellen
</a>
<a href="#" class="btn-secondary">Vorlage öffnen</a>
</div>
<!-- Vorgeschlagene Schritte -->
<div class="mt-9 grid gap-3 w-full max-w-[560px]" style="grid-template-columns:repeat(3,1fr);">
<div class="text-left px-3 py-2.5 bg-bg-elev border border-bg-rule rounded-[3px]">
<div class="font-mono text-[9.5px] tracking-[0.16em] text-accent font-bold mb-1">01</div>
<div class="text-[11.5px] font-semibold text-ink leading-tight">Firma zuordnen</div>
</div>
<div class="text-left px-3 py-2.5 bg-bg-elev border border-bg-rule rounded-[3px]">
<div class="font-mono text-[9.5px] tracking-[0.16em] text-accent font-bold mb-1">02</div>
<div class="text-[11.5px] font-semibold text-ink leading-tight">Mitteilung verfassen</div>
</div>
<div class="text-left px-3 py-2.5 bg-bg-elev border border-bg-rule rounded-[3px]">
<div class="font-mono text-[9.5px] tracking-[0.16em] text-accent font-bold mb-1">03</div>
<div class="text-[11.5px] font-semibold text-ink leading-tight">Zur Prüfung senden</div>
</div>
</div>
</div>
<div class="px-5 py-3 bg-bg-elev border-t border-bg-rule flex items-center gap-2.5 text-[11.5px] text-ink-3">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" class="text-hub flex-shrink-0">
<path d="M8 1.5l5.5 2.5v4c0 3.3-2.2 5.8-5.5 6.5C4.7 13.8 2.5 11.3 2.5 8V4z" stroke="currentColor" stroke-width="1.3" fill="none"/>
<path d="M5.5 8l2 2 3.5-4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Tipp: Geprüfte Mitteilungen erscheinen i. d. R. binnen <strong class="text-ink-2 font-semibold">4 Stunden</strong> werktags auf beiden Portalen.
</div>
</article>
<!-- ============== RECHTS: Datenqualität ============== -->
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">Datenqualität</span>
<span class="badge warn dot">2 offen</span>
</div>
<div class="px-5 py-5">
<p class="text-[12px] text-ink-3 leading-[1.55] m-0 mb-4">
Diese Hinweise helfen, Ihr User Backend vollständig und sauber zu halten.
</p>
<div class="space-y-3">
<div class="hint-card">
<span class="hint-ico">
<svg width="18" height="18" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M3 13.5c.7-2.4 2.7-4 5-4s4.3 1.6 5 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
</span>
<div class="min-w-0">
<div class="flex items-baseline justify-between gap-3">
<div class="text-[13px] font-semibold text-ink leading-tight">Profil unvollständig</div>
<span class="font-mono text-[10px] text-ink-3 tracking-[0.10em] uppercase whitespace-nowrap flex-shrink-0">60 %</span>
</div>
<div class="mt-2 h-1 w-full rounded-full bg-bg-rule-2 overflow-hidden">
<div class="h-full rounded-full bg-accent" style="width:60%;"></div>
</div>
<p class="text-[11.5px] text-ink-3 leading-[1.5] mt-2 m-0">
Vorname, Telefon und Pressekontakt fehlen für saubere Kundenakte.
</p>
<a href="#" class="inline-flex items-center gap-1 text-[11.5px] font-semibold text-accent-deep mt-2 hover:underline underline-offset-[3px] decoration-accent-deep/40">Profil öffnen →</a>
</div>
</div>
<div class="hint-card">
<span class="hint-ico">
<svg width="18" height="18" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.4"/><path d="M2.5 6h11" stroke="currentColor" stroke-width="1.4"/></svg>
</span>
<div class="min-w-0">
<div class="flex items-baseline justify-between gap-3">
<div class="text-[13px] font-semibold text-ink leading-tight">Rechnungsadresse fehlt</div>
<span class="font-mono text-[10px] text-ink-3 tracking-[0.10em] uppercase whitespace-nowrap flex-shrink-0">0 %</span>
</div>
<div class="mt-2 h-1 w-full rounded-full bg-bg-rule-2 overflow-hidden">
<div class="h-full rounded-full bg-accent" style="width:5%;"></div>
</div>
<p class="text-[11.5px] text-ink-3 leading-[1.5] mt-2 m-0">
Hinterlegen Sie eine Rechnungsadresse, damit spätere Buchungen sauber abgerechnet werden können.
</p>
<a href="#" class="inline-flex items-center gap-1 text-[11.5px] font-semibold text-accent-deep mt-2 hover:underline underline-offset-[3px] decoration-accent-deep/40">Rechnungsadresse ergänzen →</a>
</div>
</div>
</div>
</div>
</article>
</section>
<!-- ============== UNTERER GRID: FIRMEN + REICHWEITE / AKTIVITÄT ============== -->
<section class="grid gap-6" style="grid-template-columns:2fr 1fr;">
<!-- Firmen Card -->
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">Meine Firmen</span>
<div class="flex items-center gap-3">
<span class="badge hub" style="font-size:9.5px;padding:1px 6px;">0 zugeordnet</span>
<a href="#" class="text-[12px] font-semibold text-hub hover:underline underline-offset-[3px] decoration-hub/30">Profil &amp; Firma verwalten →</a>
</div>
</div>
<div class="p-6">
<div class="grid gap-3" style="grid-template-columns:1fr 1fr;">
<!-- Empty-Slot Firma 1 -->
<div class="relative border border-dashed border-bg-rule rounded-[5px] p-5 hover:border-hub/50 hover:bg-bg-elev transition-colors">
<div class="flex items-center gap-3 mb-3">
<span class="w-10 h-10 rounded-[4px] border border-bg-rule bg-bg-elev flex items-center justify-center text-ink-4">
<svg width="18" height="18" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.3"/><path d="M2.5 6h11M6 9h1M9 9h1M6 11.5h1M9 11.5h1" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
</span>
<div>
<div class="text-[13.5px] font-semibold text-ink-2">Firma hinzufügen</div>
<div class="text-[11px] text-ink-3 mt-0.5">Slot frei · 1 von 3</div>
</div>
</div>
<p class="text-[11.5px] text-ink-3 leading-[1.5] m-0">
Pressestellen, für die Sie Mitteilungen erstellen — mit eigenem Logo, Kontaktperson und Themen-Tags.
</p>
<a href="#" class="absolute top-3 right-3 inline-flex items-center justify-center w-7 h-7 rounded-[3px] bg-white border border-bg-rule text-hub hover:border-hub">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
</a>
</div>
<!-- Hinweis -->
<div class="bg-bg-elev border border-bg-rule rounded-[5px] p-5">
<div class="eyebrow muted mb-2">Hinweis</div>
<div class="text-[13px] text-ink-2 leading-[1.55] m-0">
Keine Firmen zugeordnet. Wenn hier eine Firma fehlen sollte, prüfen Sie bitte Ihr Profil oder wenden Sie sich an den Support.
</div>
<div class="mt-3 flex items-center gap-2">
<a href="#" class="btn-secondary" style="padding:6px 10px;font-size:11.5px;">Profil prüfen</a>
<a href="/cdn-cgi/l/email-protection#d1a2a4a1a1bea3a591a1a3b4a2a2b4a1bea3a5b0bdb4ffb2bebc" class="text-[11.5px] font-semibold text-ink-3 hover:text-hub">Support kontaktieren</a>
</div>
</div>
</div>
</div>
</article>
<!-- Aktivität / Bridge -->
<article class="panel-dark">
<div class="panel-head">
<span class="eyebrow on-dark">Brand-Bridge</span>
<span class="font-mono text-[10px] text-ink-on-dark-3 tracking-[0.14em] uppercase">A · B</span>
</div>
<div class="px-5 py-5">
<div class="text-[12.5px] leading-[1.55] text-ink-on-dark-2 m-0 mb-4">
Ein Konto, beide Portale — Veröffentlichungen werden parallel auf presseecho und businessportal24 ausgespielt.
</div>
<div class="grid gap-3" style="grid-template-columns:1fr 1fr;">
<div class="bg-hub-2 rounded-[4px] px-3.5 py-3 border border-white/5">
<div class="flex items-center gap-2 mb-1.5">
<span class="dot-pe"></span>
<span class="text-[11px] font-bold tracking-[0.14em] uppercase text-white/85">presseecho</span>
</div>
<div class="font-mono text-[15px] font-semibold text-white" style="font-variant-numeric:tabular-nums;">verbunden</div>
<div class="text-[10.5px] text-ink-on-dark-3 mt-0.5">Archiv · Branchen-Tiefe</div>
</div>
<div class="bg-hub-2 rounded-[4px] px-3.5 py-3 border border-white/5">
<div class="flex items-center gap-2 mb-1.5">
<span class="dot-bp"></span>
<span class="text-[11px] font-bold tracking-[0.14em] uppercase text-white/85">businessportal24</span>
</div>
<div class="font-mono text-[15px] font-semibold text-white" style="font-variant-numeric:tabular-nums;">verbunden</div>
<div class="text-[10.5px] text-ink-on-dark-3 mt-0.5">Wirtschaft · Live</div>
</div>
</div>
<hr class="mt-5 mb-4" style="border:0;height:1px;background:rgba(255,255,255,.10);" />
<div class="space-y-2 text-[11.5px] text-ink-on-dark-2">
<div class="flex items-center justify-between">
<span>API-Status</span>
<span class="flex items-center gap-1.5 text-white"><span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>operational</span>
</div>
<div class="flex items-center justify-between">
<span>Letzte Synchronisation</span>
<span class="font-mono text-white">vor 2 min</span>
</div>
<div class="flex items-center justify-between">
<span>Tarif</span>
<span class="font-mono text-white">Starter</span>
</div>
</div>
<a href="Hub Landing presseportale.html#tarife" class="mt-5 inline-flex items-center gap-1.5 text-[11.5px] font-semibold text-white hover:underline underline-offset-[3px] decoration-white/30">
Tarife &amp; Add-ons ansehen →
</a>
</div>
</article>
</section>
<!-- ============== FUSSZEILE ============== -->
<footer class="flex items-center justify-between pt-4 pb-2 text-[11px] text-ink-3 border-t border-bg-rule">
<span>© 2026 presseportale.com · Publisher-Hub</span>
<span class="flex items-center gap-5">
<a href="#" class="hover:text-hub">Tastenkürzel</a>
<a href="#" class="hover:text-hub">Changelog</a>
<a href="#" class="hover:text-hub">Statusseite</a>
<a href="/cdn-cgi/l/email-protection#04777174746b767044747661777761746b76706568612a676b69" class="hover:text-hub">Support</a>
</span>
</footer>
</div>
</main>
</div>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script></body>
</html>

View file

@ -0,0 +1,976 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>presseportale.com — Neue Pressemitteilung</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700&family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600;8..60,700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
bg:"#F6F4EF","bg-elev":"#FBFAF6","bg-rule":"#E2DDD0","bg-rule-2":"#EDE7D7",
"bg-card":"#FFFFFF","bg-card-warm":"#EFEADC",
hub:"#1A2540","hub-2":"#243152","hub-3":"#2E3D66",
"hub-soft":"#E5E9F1","hub-soft-2":"#CFD6E4","hub-line":"#7B8FBF",
accent:"#B07A3A","accent-deep":"#8A5E27","accent-soft":"#F1E6D3",
ink:"#1A1F1C","ink-2":"#3A413D","ink-3":"#5A6360","ink-4":"#8A918D",
"ink-on-dark":"#F6F4EF","ink-on-dark-2":"#B2B9C7","ink-on-dark-3":"#7B8FBF",
ok:"#2E8540","ok-soft":"#E2F1E5",
warn:"#A87A1F","warn-soft":"#F6EAC8",
err:"#A8331F","err-soft":"#F4DAD2",
},
fontFamily:{
sans:['"Inter Tight"','Inter','system-ui','sans-serif'],
serif:['"Source Serif 4"','Georgia','serif'],
mono:['"JetBrains Mono"','"SF Mono"','ui-monospace','monospace'],
},
}
}
};
</script>
<style>
html,body{margin:0;padding:0;}
body{background:#E8E4DA;font-family:"Inter Tight",system-ui,sans-serif;}
.eyebrow{font-size:10.5px;font-weight:700;letter-spacing:.20em;text-transform:uppercase;color:#1A2540;}
.eyebrow.muted{color:#5A6360;letter-spacing:.16em;font-weight:600;font-size:10px;}
.eyebrow.accent{color:#8A5E27;}
.eyebrow.on-dark{color:#7B8FBF;}
.rule{height:1px;background:#E2DDD0;border:0;margin:0;}
/* Sidebar */
.nav-item{display:flex;align-items:center;gap:11px;padding:8px 12px;border-radius:4px;font-size:13px;font-weight:500;color:#3A413D;transition:background .12s,color .12s;position:relative;}
.nav-item:hover{background:#F6F4EF;color:#1A2540;}
.nav-item.active{background:#E5E9F1;color:#1A2540;font-weight:600;}
.nav-item.active::before{content:"";position:absolute;left:-1px;top:6px;bottom:6px;width:2px;background:#1A2540;border-radius:0 2px 2px 0;}
.nav-item.disabled{color:#8A918D;cursor:default;}
.nav-item.disabled:hover{background:transparent;color:#8A918D;}
.nav-item .ico{width:16px;height:16px;flex-shrink:0;color:currentColor;opacity:.8;}
.nav-item.active .ico{opacity:1;}
.nav-section{font-size:10px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:#8A918D;padding:0 12px 6px;}
/* Panels */
.panel{background:#FFFFFF;border:1px solid #E2DDD0;border-radius:6px;}
.panel-warm{background:#FBFAF6;border:1px solid #E2DDD0;border-radius:6px;}
/* Buttons */
.btn-primary{display:inline-flex;align-items:center;gap:8px;justify-content:center;padding:10px 16px;background:#1A2540;color:#fff;border-radius:4px;font-size:13px;font-weight:600;transition:background .15s;}
.btn-primary:hover{background:#243152;}
.btn-primary.full{width:100%;padding:11px 16px;}
.btn-secondary{display:inline-flex;align-items:center;gap:8px;justify-content:center;padding:8px 14px;background:#fff;color:#1A2540;border:1px solid #CFD6E4;border-radius:4px;font-size:12.5px;font-weight:600;transition:border-color .15s,background .15s;}
.btn-secondary:hover{border-color:#1A2540;background:#F6F4EF;}
.btn-secondary.full{width:100%;}
.btn-ghost{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;color:#3A413D;border-radius:4px;font-size:12px;font-weight:500;transition:background .12s,color .12s;}
.btn-ghost:hover{background:#F6F4EF;color:#1A2540;}
/* Badges */
.badge{display:inline-flex;align-items:center;gap:6px;padding:3px 9px;border-radius:99px;font-size:10.5px;font-weight:700;letter-spacing:.10em;text-transform:uppercase;}
.badge.warn{background:#F6EAC8;color:#8A5E27;}
.badge.ok{background:#E2F1E5;color:#1F5E2E;}
.badge.err{background:#F4DAD2;color:#8E2A19;}
.badge.hub{background:#E5E9F1;color:#1A2540;}
.badge.muted{background:#EFEADC;color:#5A6360;}
.badge.dot::before{content:"";width:6px;height:6px;border-radius:99px;background:currentColor;}
/* BALD-Badge */
.badge-bald{display:inline-flex;align-items:center;gap:5px;padding:2px 8px;border-radius:99px;font-size:9.5px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;background:#F1E6D3;color:#8A5E27;border:1px dashed #E1C883;}
.badge-bald::before{content:"";width:4px;height:4px;border-radius:99px;background:#B07A3A;}
/* Bridge ribbon */
.bridge-row{display:inline-flex;align-items:center;gap:6px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:10.5px;letter-spacing:.10em;text-transform:uppercase;color:#5A6360;}
.dot-pe{width:6px;height:6px;border-radius:99px;background:#1F4D3A;}
.dot-bp{width:6px;height:6px;border-radius:99px;background:#C84A1E;}
/* Editor wrapper */
.field-label{display:flex;align-items:center;gap:8px;font-size:10.5px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:#5A6360;margin-bottom:8px;}
.field-label .req{color:#A8331F;font-weight:700;letter-spacing:0;}
.field-help{font-size:11.5px;color:#8A918D;margin-top:6px;line-height:1.5;}
.field-help.warn{color:#8A5E27;}
.field-counter{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:10.5px;color:#8A918D;font-variant-numeric:tabular-nums;}
/* Inputs */
.inp{
width:100%;padding:10px 12px;background:#fff;border:1px solid #CFD6E4;border-radius:4px;
font-size:13px;color:#1A1F1C;transition:border-color .15s,box-shadow .15s;font-family:inherit;
}
.inp:focus{outline:none;border-color:#1A2540;box-shadow:0 0 0 3px rgba(26,37,64,.07);}
.inp::placeholder{color:#8A918D;}
.inp.title{font-size:24px;font-weight:700;letter-spacing:-.5px;line-height:1.2;padding:14px 16px;color:#1A1F1C;font-family:"Source Serif 4",Georgia,serif;}
.inp.title::placeholder{color:#B5BCB9;font-weight:400;}
.inp.subtitle{font-size:15px;font-weight:500;line-height:1.4;padding:11px 14px;color:#3A413D;font-family:"Source Serif 4",Georgia,serif;}
.inp.subtitle::placeholder{color:#B5BCB9;font-weight:400;}
.inp.small{padding:7px 10px;font-size:12.5px;}
.select{
width:100%;padding:8px 30px 8px 12px;background:#fff;border:1px solid #CFD6E4;border-radius:4px;
font-size:12.5px;color:#1A1F1C;appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 4.5l3 3 3-3' stroke='%235A6360' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat:no-repeat;background-position:right 10px center;cursor:pointer;
}
.select:focus{outline:none;border-color:#1A2540;}
/* Toolbar / editor */
.toolbar{display:flex;align-items:center;gap:2px;padding:7px 10px;border-bottom:1px solid #E2DDD0;background:#FBFAF6;border-radius:4px 4px 0 0;flex-wrap:wrap;}
.tb-btn{display:inline-flex;align-items:center;justify-content:center;min-width:28px;height:28px;padding:0 7px;border-radius:3px;font-size:12.5px;font-weight:600;color:#3A413D;border:1px solid transparent;transition:background .12s,color .12s,border-color .12s;}
.tb-btn:hover{background:#fff;border-color:#CFD6E4;color:#1A2540;}
.tb-btn.is-active{background:#1A2540;color:#fff;}
.tb-btn .lbl{font-size:11px;letter-spacing:.04em;}
.tb-sep{width:1px;height:18px;background:#E2DDD0;margin:0 4px;}
.editor-body{padding:18px 20px;min-height:340px;font-family:"Source Serif 4",Georgia,serif;font-size:15px;line-height:1.65;color:#1A1F1C;}
.editor-body h2{font-family:"Source Serif 4",Georgia,serif;font-size:18px;font-weight:700;color:#1A1F1C;margin:18px 0 8px;letter-spacing:-.2px;}
.editor-body p{margin:0 0 12px;}
.editor-body p.lede{font-weight:500;font-size:16px;color:#1A1F1C;}
.editor-body strong{color:#1A1F1C;font-weight:700;}
.editor-body em{font-style:italic;}
.editor-body ul{margin:6px 0 14px;padding-left:22px;}
.editor-body ul li{margin:4px 0;}
.editor-body a{color:#8A5E27;text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:1px;text-decoration-color:rgba(138,94,39,.45);}
.editor-cursor{display:inline-block;width:1px;height:18px;background:#1A2540;vertical-align:-3px;animation:blink 1s steps(2) infinite;margin-left:1px;}
@keyframes blink{50%{opacity:0;}}
/* Media tiles */
.media-tile{position:relative;border:1px solid #E2DDD0;border-radius:5px;background:#FBFAF6;overflow:hidden;}
.media-thumb{aspect-ratio:16/10;background:#EDE7D7;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;}
.media-meta{padding:9px 11px;}
.media-cap{font-size:11.5px;color:#3A413D;line-height:1.4;}
.media-alt{font-size:10.5px;color:#8A918D;margin-top:3px;font-style:italic;line-height:1.4;}
.media-cover-flag{position:absolute;top:8px;left:8px;}
.media-actions{position:absolute;top:8px;right:8px;display:flex;gap:4px;}
.media-act{width:24px;height:24px;border-radius:3px;background:rgba(26,31,28,.62);color:#fff;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px);}
.media-act:hover{background:rgba(26,37,64,.85);}
/* Drop zone */
.dropzone{border:1.5px dashed #CFD6E4;border-radius:5px;background:#FBFAF6;padding:18px 16px;display:flex;align-items:center;justify-content:center;gap:14px;text-align:left;transition:border-color .15s,background .15s;}
.dropzone:hover{border-color:#1A2540;background:#F6F4EF;}
.dropzone .dz-ico{width:42px;height:42px;border-radius:5px;background:#E5E9F1;color:#1A2540;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
/* Right sidebar — meta blocks */
.meta-card{background:#fff;border:1px solid #E2DDD0;border-radius:5px;padding:14px 16px;}
.meta-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;}
.meta-title{font-size:10.5px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:#1A2540;}
.meta-title .num{font-family:"JetBrains Mono",ui-monospace,monospace;color:#8A918D;margin-right:6px;letter-spacing:.06em;}
/* Checklist */
.check-row{display:flex;align-items:flex-start;gap:10px;padding:7px 0;font-size:12.5px;}
.check-row .ic{width:16px;height:16px;border-radius:99px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px;}
.check-row.ok .ic{background:#E2F1E5;color:#1F5E2E;}
.check-row.warn .ic{background:#F6EAC8;color:#8A5E27;}
.check-row.err .ic{background:#F4DAD2;color:#8E2A19;}
.check-row .lbl{flex:1;color:#1A1F1C;line-height:1.4;}
.check-row .lbl .sub{display:block;font-size:11px;color:#5A6360;margin-top:1px;}
.check-row.ok .lbl{color:#3A413D;}
/* Portal chooser */
.portal-opt{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border:1px solid #E2DDD0;border-radius:4px;background:#FBFAF6;cursor:pointer;transition:border-color .12s,background .12s;}
.portal-opt:hover{border-color:#1A2540;}
.portal-opt.is-checked{border-color:#1A2540;background:#fff;box-shadow:inset 0 0 0 1px #1A2540;}
.portal-opt .pcheck{width:14px;height:14px;border:1.5px solid #CFD6E4;border-radius:3px;background:#fff;flex-shrink:0;margin-top:2px;position:relative;}
.portal-opt.is-checked .pcheck{background:#1A2540;border-color:#1A2540;}
.portal-opt.is-checked .pcheck::after{content:"";position:absolute;left:3px;top:0;width:4px;height:8px;border:solid #fff;border-width:0 1.5px 1.5px 0;transform:rotate(45deg);}
.portal-opt .ptitle{font-size:12.5px;font-weight:600;color:#1A1F1C;display:flex;align-items:center;gap:7px;}
.portal-opt .psub{font-size:11px;color:#5A6360;margin-top:2px;line-height:1.4;}
/* Tag chip */
.tag-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 4px 3px 9px;background:#E5E9F1;color:#1A2540;border-radius:3px;font-size:11.5px;font-weight:500;}
.tag-chip .x{width:16px;height:16px;border-radius:2px;display:inline-flex;align-items:center;justify-content:center;color:#1A2540;transition:background .12s;}
.tag-chip .x:hover{background:#CFD6E4;}
/* Boilerplate panel */
.boiler{background:#FBFAF6;border:1px dashed #CFD6E4;border-radius:5px;padding:14px 16px;}
/* Autosave row */
.autosave{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;color:#5A6360;}
.autosave .dot{width:6px;height:6px;border-radius:99px;background:#2E8540;box-shadow:0 0 0 3px rgba(46,133,64,.15);}
/* Tabs (für Pressekontakt) */
.tab-mini{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;font-size:11.5px;font-weight:600;color:#5A6360;border-radius:3px;cursor:pointer;}
.tab-mini:hover{color:#1A2540;}
.tab-mini.is-active{background:#E5E9F1;color:#1A2540;}
/* Collapsible */
.collapse-head{display:flex;align-items:center;justify-content:space-between;cursor:pointer;}
.collapse-head:hover .meta-title{color:#243152;}
.chev{width:16px;height:16px;color:#5A6360;transition:transform .15s;}
.meta-card.is-open .chev{transform:rotate(180deg);}
/* AI inline hint (BALD) */
.ai-hint{display:flex;align-items:center;gap:9px;padding:8px 11px;background:#FBF6EB;border:1px dashed #E1C883;border-radius:4px;font-size:11.5px;color:#8A5E27;line-height:1.45;}
.ai-hint .ai-ico{width:22px;height:22px;border-radius:4px;background:#F1E6D3;color:#8A5E27;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.ai-hint .ai-cta{margin-left:auto;font-size:10.5px;font-weight:700;letter-spacing:.10em;text-transform:uppercase;color:#5A6360;padding:3px 7px;border:1px solid #E2DDD0;border-radius:99px;background:#fff;cursor:not-allowed;}
/* Char counter pill */
.meter{display:inline-flex;align-items:center;gap:6px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:10.5px;color:#5A6360;font-variant-numeric:tabular-nums;}
.meter .bar{position:relative;width:60px;height:4px;background:#EDE7D7;border-radius:99px;overflow:hidden;}
.meter .bar i{position:absolute;left:0;top:0;bottom:0;background:#1A2540;border-radius:99px;}
.meter.good .bar i{background:#2E8540;}
.meter.warn .bar i{background:#A87A1F;}
/* Sticky aside helper */
@media (min-width: 1280px){
.sticky-aside{position:sticky;top:18px;}
}
/* mark / highlight in editor */
.editor-body mark.suggest{background:linear-gradient(transparent 55%, #F6EAC8 55%);padding:0 1px;border-radius:1px;}
</style>
</head>
<body class="bg-bg text-ink font-sans antialiased">
<!-- ============== ARTBOARD ============== -->
<div class="mx-auto bg-bg" style="width:1440px;">
<div class="flex" style="min-height:1500px;">
<!-- ============== SIDEBAR ============== -->
<aside class="bg-bg-elev border-r border-bg-rule flex flex-col" style="width:260px;">
<div class="px-5 pt-6 pb-5">
<a href="Hub Landing presseportale.html" class="flex items-baseline gap-2">
<span class="text-[19px] font-bold tracking-[-0.4px] text-hub leading-none">presseportale<span class="text-accent">.com</span></span>
</a>
<div class="eyebrow muted mt-2">Publisher · Hub</div>
<button class="mt-4 w-full grid items-center gap-2.5 px-3 py-2.5 bg-white border border-bg-rule rounded-[4px] hover:border-hub/40 text-left" style="grid-template-columns:auto 1fr auto;">
<span class="w-7 h-7 rounded-[3px] bg-hub-soft border border-hub-soft-2 flex items-center justify-center text-hub text-[11px] font-bold">TU</span>
<span class="min-w-0">
<span class="block text-[12.5px] font-semibold text-ink leading-tight truncate">Test User</span>
<span class="block text-[10.5px] text-ink-3 leading-tight mt-0.5 truncate">Tegernseer Brauerei AG +1</span>
</span>
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" class="text-ink-3">
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 7.5l3-3 3 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" opacity="0.4"/>
</svg>
</button>
</div>
<nav class="px-3 flex-1">
<div class="nav-section">Mein Bereich</div>
<div class="space-y-0.5 mb-5">
<a class="nav-item" href="User Dashboard presseportale.html">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M2 7l6-5 6 5v7H2z" stroke="currentColor" stroke-width="1.4"/><path d="M6 14V9h4v5" stroke="currentColor" stroke-width="1.4"/></svg>
Übersicht
</a>
<a class="nav-item active" href="User Pressemitteilungen presseportale.html">
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="2.5" width="9" height="11" stroke="currentColor" stroke-width="1.4"/><path d="M11.5 5h2v8.5H4" stroke="currentColor" stroke-width="1.4"/><path d="M5 5.5h4M5 8h4M5 10.5h2.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
Meine Pressemitteilungen
<span class="badge hub ml-auto" style="font-size:9.5px;padding:1px 6px;letter-spacing:0.08em;">25</span>
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.4"/><path d="M2.5 6h11" stroke="currentColor" stroke-width="1.4"/><path d="M6 9h1M9 9h1M6 11.5h1M9 11.5h1" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Firmen
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 5h10l-1 9H4z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6 5V3.5a2 2 0 014 0V5" stroke="currentColor" stroke-width="1.4"/></svg>
Buchungen &amp; Add-ons
</a>
<span class="nav-item disabled">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 13V8M7 13V5M11 13V9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Statistiken
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
</span>
</div>
<div class="nav-section">Finanzen</div>
<div class="space-y-0.5 mb-5">
<span class="nav-item disabled">
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="5" stroke="currentColor" stroke-width="1.4"/><path d="M8 5.5v5M6 8h4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Credits &amp; Tarif
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
</span>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 2.5h7l3 3v8H3z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M10 2.5V5.5h3" stroke="currentColor" stroke-width="1.4"/><path d="M5.5 8h5M5.5 10.5h5M5.5 6h2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
Rechnungen
</a>
</div>
<div class="nav-section">Konto</div>
<div class="space-y-0.5 mb-5">
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M3 13.5c.7-2.4 2.7-4 5-4s4.3 1.6 5 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Profil
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M8 2l5 2v4c0 3-2 5-5 6-3-1-5-3-5-6V4z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6 8l1.5 1.5L10.5 6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Sicherheit
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="6" cy="8" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M8.5 8h5M11 8v2.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
API &amp; Integrationen
</a>
</div>
</nav>
<div class="px-4 pb-4">
<div class="bg-hub text-ink-on-dark rounded-[5px] p-4 relative overflow-hidden">
<div class="absolute -top-6 -right-6 w-16 h-16 rounded-full bg-hub-3 opacity-50"></div>
<div class="absolute -bottom-8 -left-8 w-20 h-20 rounded-full bg-hub-3 opacity-30"></div>
<div class="relative">
<div class="flex items-center gap-2 mb-2">
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
<span class="eyebrow on-dark" style="color:#F4D89C;">Testmodus aktiv</span>
</div>
<div class="text-[12px] leading-[1.5] text-ink-on-dark-2">
Angemeldet als <strong class="text-white font-semibold">Test User</strong>.<br/>
Admin: <strong class="text-white font-semibold">Portal Admin</strong>
</div>
<button class="mt-3 w-full px-3 py-2 bg-white text-hub text-[12px] font-semibold rounded-[3px] hover:bg-bg transition-colors flex items-center justify-center gap-1.5">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none">
<path d="M9 3L3 9M3 9H8M3 9V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Zurück zum Admin
</button>
</div>
</div>
</div>
</aside>
<!-- ============== MAIN ============== -->
<main class="flex-1 min-w-0" data-screen-label="01 Neue Mitteilung erstellen">
<!-- Topbar -->
<div class="bg-bg-elev border-b border-bg-rule">
<div class="px-10 py-3 flex items-center gap-6">
<div class="flex items-center gap-2 text-[12px] text-ink-3 font-medium">
<a href="Hub Landing presseportale.html" class="hover:text-hub">Hub</a>
<span class="text-ink-4">/</span>
<a href="User Dashboard presseportale.html" class="hover:text-hub">User Backend</a>
<span class="text-ink-4">/</span>
<a href="User Pressemitteilungen presseportale.html" class="hover:text-hub">Pressemitteilungen</a>
<span class="text-ink-4">/</span>
<span class="text-hub font-semibold">Neue Mitteilung</span>
</div>
<span class="flex-1"></span>
<span class="bridge-row">
<span class="dot-pe"></span> presseecho
<span class="text-ink-4 mx-1">·</span>
<span class="dot-bp"></span> businessportal24
</span>
<span class="w-px h-5 bg-bg-rule"></span>
<span class="autosave">
<span class="dot"></span>
Zuletzt gespeichert vor <strong class="text-ink-2 font-semibold mx-1">8 Sek.</strong>
<span class="text-ink-4">·</span>
<span class="font-mono text-[10.5px] text-ink-4">PM-DRAFT-2026-0026</span>
</span>
<span class="w-px h-5 bg-bg-rule"></span>
<button class="btn-ghost">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M2 8l4 4 8-8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Speichern
</button>
<button class="btn-secondary">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.3"/><path d="M5 8.5L7 10.5 11 6.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Vorschau
</button>
</div>
</div>
<!-- Inhalt -->
<div class="px-10 py-8">
<!-- ============== PAGE HEADER ============== -->
<header class="grid items-end gap-8 mb-7" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">User Backend</span>
<span class="eyebrow muted">Mein Bereich · A · 02 · Neu</span>
<span class="badge muted dot">Entwurf</span>
</div>
<h1 class="text-[32px] font-bold tracking-[-0.7px] text-ink leading-[1.1] m-0">Neue Pressemitteilung</h1>
<p class="mt-2 text-[13px] text-ink-3 leading-[1.55] max-w-[640px] m-0">
Schreibfläche links, Steuerung rechts. Speichert automatisch alle paar Sekunden. Beim Klick auf
<span class="font-semibold text-hub">„Zur Prüfung senden"</span> läuft eine kurze Qualitäts-Checkliste.
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<a href="User Pressemitteilungen presseportale.html" class="btn-secondary whitespace-nowrap">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M7.5 3L4.5 6l3 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Zur Liste
</a>
<button class="btn-secondary whitespace-nowrap text-err" style="color:#8E2A19;border-color:#E0B0A5;">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M3 4h10M5.5 4V2.5h5V4M5 4l.5 9.5h5L11 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Entwurf verwerfen
</button>
</div>
</header>
<!-- ============== 2-COLUMN GRID ============== -->
<div class="grid gap-7" style="grid-template-columns:minmax(0,1fr) 360px;">
<!-- ===================================================== -->
<!-- LINKS: SCHREIBFLÄCHE -->
<!-- ===================================================== -->
<div class="space-y-6 min-w-0">
<!-- ─── 1) FIRMA-SELEKTOR ─── -->
<section class="panel" style="padding:14px 18px;">
<div class="flex items-center gap-5">
<div class="field-label" style="margin-bottom:0;">Für Firma</div>
<button class="flex items-center gap-2.5 px-3 py-1.5 bg-white border border-hub rounded-[4px] hover:bg-hub-soft transition-colors" style="box-shadow:inset 0 0 0 1px #1A2540;">
<span class="w-5 h-5 rounded-[3px] bg-hub-soft border border-hub-soft-2 flex items-center justify-center text-hub text-[9.5px] font-bold">TB</span>
<span class="text-[13px] font-semibold text-hub">Tegernseer Brauerei AG</span>
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" class="text-hub-3"><path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="text-[11.5px] text-ink-3">
Boilerplate und Pressekontakt werden vorbefüllt.
</span>
<span class="flex-1"></span>
<a href="#" class="btn-ghost text-[11.5px]">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
Neue Firma anlegen
</a>
</div>
</section>
<!-- ─── 2) TITEL ─── -->
<section class="panel" style="padding:20px 22px;">
<div class="flex items-center justify-between mb-2">
<div class="field-label" style="margin-bottom:0;">
Titel / Headline <span class="req">*</span>
</div>
<div class="flex items-center gap-3">
<span class="meter good">
<span class="bar"><i style="width:62%;"></i></span>
62 / 100
</span>
<span class="badge-bald">KI-Titel · bald</span>
</div>
</div>
<input class="inp title" value="Tegernseer Brauerei eröffnet Craft-Beer-Manufaktur am Tegernseer Hafen" />
<div class="field-help">
4090 Zeichen empfohlen. Konkret, ohne Marketing-Floskeln. Wer? Was? Wo?
</div>
</section>
<!-- ─── 3) SUBLINE ─── -->
<section class="panel" style="padding:20px 22px;">
<div class="flex items-center justify-between mb-2">
<div class="field-label" style="margin-bottom:0;">
Untertitel <span class="text-ink-4 font-normal" style="letter-spacing:0;text-transform:none;">— optional</span>
</div>
<span class="meter">
<span class="bar"><i style="width:48%;"></i></span>
118 / 200
</span>
</div>
<input class="inp subtitle" value="Neue Schau-Manufaktur mit Verkostungsraum und Besucherführungen — Eröffnung 12. Juni 2026." />
</section>
<!-- ─── 4) FLIESSTEXT ─── -->
<section class="panel">
<div class="flex items-center justify-between" style="padding:13px 18px 0 18px;">
<div class="field-label" style="margin-bottom:0;">
Fließtext <span class="req">*</span>
</div>
<div class="flex items-center gap-3">
<span class="meter good">
<span class="bar"><i style="width:38%;"></i></span>
1.420 / 3.500 Z.
</span>
<span class="badge-bald">KI-Lektorat · bald</span>
</div>
</div>
<!-- Toolbar -->
<div class="toolbar mt-3 mx-3" style="border-radius:4px;border:1px solid #E2DDD0;">
<button class="tb-btn" title="Absatz / Stilebene">
<span class="lbl">Absatz</span>
<svg width="9" height="9" viewBox="0 0 12 12" fill="none" class="ml-1 opacity-70"><path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="tb-sep"></span>
<button class="tb-btn" title="Fett" style="font-weight:700;">B</button>
<button class="tb-btn" title="Kursiv" style="font-style:italic;">I</button>
<span class="tb-sep"></span>
<button class="tb-btn" title="Zwischenüberschrift">H₂</button>
<button class="tb-btn" title="Aufzählung">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><circle cx="3" cy="4" r="1" fill="currentColor"/><circle cx="3" cy="8" r="1" fill="currentColor"/><circle cx="3" cy="12" r="1" fill="currentColor"/><path d="M6 4h8M6 8h8M6 12h6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
</button>
<button class="tb-btn" title="Nummerierte Liste">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M6 4h8M6 8h8M6 12h8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><text x="1.5" y="6" font-size="4.5" font-family="JetBrains Mono" fill="currentColor">1</text><text x="1.5" y="10" font-size="4.5" font-family="JetBrains Mono" fill="currentColor">2</text><text x="1.5" y="14" font-size="4.5" font-family="JetBrains Mono" fill="currentColor">3</text></svg>
</button>
<button class="tb-btn" title="Zitat">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M3 5.5C3 4.7 3.7 4 4.5 4H6v3.5H4.5C3.7 7.5 3 8.2 3 9V11" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M9 5.5C9 4.7 9.7 4 10.5 4H12v3.5h-1.5C9.7 7.5 9 8.2 9 9V11" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>
</button>
<span class="tb-sep"></span>
<button class="tb-btn" title="Link">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M6 10l4-4M5 6.5L4 7.5a2.5 2.5 0 003.5 3.5l1-1M11 9.5l1-1a2.5 2.5 0 00-3.5-3.5l-1 1" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="tb-sep"></span>
<button class="tb-btn" title="Rückgängig">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M5 5L2.5 7.5 5 10M2.5 7.5h7.5a3 3 0 010 6H8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button class="tb-btn" title="Wiederholen">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M11 5l2.5 2.5L11 10M13.5 7.5H6a3 3 0 000 6h2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="flex-1"></span>
<span class="text-[10.5px] text-ink-4 mr-2">Reduzierter Editor — bewusst eingeschränkt für konsistentes Format.</span>
</div>
<!-- Editor body -->
<div class="editor-body mx-3 mb-3" style="border:1px solid #E2DDD0;border-top:0;border-radius:0 0 4px 4px;background:#fff;">
<p class="lede">
<strong>Tegernsee, 11.05.2026.</strong> Die Tegernseer Brauerei AG eröffnet am 12. Juni 2026 ihre neue
Craft-Beer-Manufaktur direkt am Tegernseer Hafen. Auf rund 1.200&nbsp;m² entsteht eine
Schau-Brauerei mit angeschlossenem Verkostungsraum und ganzjährigen Besucherführungen.
</p>
<p>
Der Neubau ergänzt den 1675 gegründeten Stammsitz und setzt einen Fokus auf
experimentelle Sude in kleinen Chargen. „Wir wollen Bier-Handwerk wieder
<mark class="suggest">erlebbar machen</mark> — direkt am Wasser, mit offenen Kesseln und
unseren Brauern als Gastgebern", erklärt Vorstandsvorsitzender Dr. Markus Weishaupt.
</p>
<h2>Verkostung, Manufaktur-Touren, regionale Wertschöpfung</h2>
<p>
Die Manufaktur kombiniert drei Erlebnisformate unter einem Dach:
</p>
<ul>
<li>Schau-Brauerei mit gläsernem Sudhaus und 30-minütigen Kurzführungen.</li>
<li>Verkostungsraum für bis zu 60 Personen, mit wechselnden Sorten aus der Pilot-Anlage.</li>
<li>Direktverkauf ab Werk — ausschließlich aus Tegernseer Roh<editor-cursor></editor-cursor></li>
</ul>
<p style="color:#8A918D;font-style:italic;">
Hier weiterschreiben …
</p>
</div>
<!-- AI Hint -->
<div class="mx-3 mb-4">
<div class="ai-hint">
<span class="ai-ico">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M8 2l1.5 4 4 1.5L9.5 9 8 13l-1.5-4L2.5 7.5 6.5 6z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>
</span>
<span><strong class="font-semibold">KI-Lektorat</strong> liest Korrektur, schlägt Kürzungen vor und prüft auf werbliche Sprache. Erscheint hier inline — bald verfügbar.</span>
<span class="ai-cta">bald</span>
</div>
</div>
</section>
<!-- ─── 5) MEDIEN ─── -->
<section class="panel" style="padding:18px 20px 20px;">
<div class="flex items-center justify-between mb-3">
<div class="field-label" style="margin-bottom:0;">
Medien / Bilder
<span class="text-ink-4 font-normal" style="letter-spacing:0;text-transform:none;">— mindestens 1 Bild empfohlen</span>
</div>
<div class="flex items-center gap-3">
<span class="text-[11.5px] text-ink-3"><strong class="text-ink-2 font-semibold">2</strong> Bilder hochgeladen · 1 Vorschau</span>
<span class="badge-bald">KI-Bildgenerierung · bald</span>
</div>
</div>
<div class="grid grid-cols-3 gap-3 mb-3">
<!-- Bild 1 -->
<div class="media-tile">
<div class="media-thumb" style="background:linear-gradient(135deg,#8A6A3A 0%,#3A4D2F 50%,#1A2540 100%);">
<span class="media-cover-flag badge ok dot" style="font-size:9px;padding:2px 7px;letter-spacing:.10em;">Titelbild</span>
<span class="media-actions">
<button class="media-act" title="Bearbeiten"><svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M11 3l2 2-8 8H3v-2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg></button>
<button class="media-act" title="Entfernen"><svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
</span>
<!-- Visual mock -->
<svg viewBox="0 0 200 125" class="absolute inset-0 w-full h-full" preserveAspectRatio="xMidYMid slice">
<defs>
<linearGradient id="bsky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#3E5B7A"/><stop offset="1" stop-color="#8A9DAE"/></linearGradient>
</defs>
<rect width="200" height="80" fill="url(#bsky)"/>
<rect y="80" width="200" height="45" fill="#2E3B25"/>
<polygon points="0,80 40,60 70,72 110,55 150,68 200,50 200,80" fill="#3F2D1E" opacity="0.85"/>
<rect x="60" y="70" width="80" height="22" fill="#C8A86E" opacity="0.95"/>
<rect x="62" y="72" width="76" height="3" fill="#8A5E27"/>
<rect x="70" y="76" width="6" height="12" fill="#1A1F1C" opacity="0.7"/>
<rect x="80" y="76" width="6" height="12" fill="#1A1F1C" opacity="0.7"/>
<rect x="90" y="76" width="6" height="12" fill="#1A1F1C" opacity="0.7"/>
<rect x="100" y="76" width="6" height="12" fill="#1A1F1C" opacity="0.7"/>
<rect x="110" y="76" width="6" height="12" fill="#1A1F1C" opacity="0.7"/>
<rect x="120" y="76" width="6" height="12" fill="#1A1F1C" opacity="0.7"/>
<rect x="130" y="76" width="6" height="12" fill="#1A1F1C" opacity="0.7"/>
</svg>
</div>
<div class="media-meta">
<div class="media-cap">Außenansicht der neuen Manufaktur am Tegernseer Hafen.</div>
<div class="media-alt">Alt: Holzgebäude mit Glasfront direkt am See, Abendlicht.</div>
</div>
</div>
<!-- Bild 2 -->
<div class="media-tile">
<div class="media-thumb" style="background:#4A2F1E;">
<span class="media-actions">
<button class="media-act" title="Als Titelbild"><svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M8 2l1.5 3.5L13 6l-2.5 2.5L11 12l-3-1.7L5 12l.5-3.5L3 6l3.5-.5z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg></button>
<button class="media-act" title="Bearbeiten"><svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M11 3l2 2-8 8H3v-2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg></button>
<button class="media-act" title="Entfernen"><svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
</span>
<svg viewBox="0 0 200 125" class="absolute inset-0 w-full h-full" preserveAspectRatio="xMidYMid slice">
<rect width="200" height="125" fill="#3A2516"/>
<ellipse cx="100" cy="50" rx="80" ry="30" fill="#C8A86E" opacity="0.18"/>
<rect x="40" y="30" width="34" height="80" rx="3" fill="#B8843A"/>
<rect x="42" y="32" width="30" height="6" fill="#8A5E27"/>
<rect x="42" y="92" width="30" height="14" fill="#8A5E27"/>
<rect x="48" y="46" width="18" height="32" fill="#EFEADC"/>
<rect x="83" y="20" width="34" height="90" rx="3" fill="#D4A04A"/>
<rect x="85" y="22" width="30" height="6" fill="#A87A1F"/>
<rect x="85" y="92" width="30" height="14" fill="#A87A1F"/>
<rect x="91" y="42" width="18" height="32" fill="#FBFAF6"/>
<rect x="126" y="38" width="34" height="72" rx="3" fill="#A87A4A"/>
<rect x="128" y="40" width="30" height="6" fill="#5A3D1E"/>
<rect x="128" y="92" width="30" height="14" fill="#5A3D1E"/>
<rect x="134" y="54" width="18" height="32" fill="#F1E6D3"/>
</svg>
</div>
<div class="media-meta">
<div class="media-cap">Drei neue Sorten aus der Pilot-Anlage — Spezialeditionen.</div>
<div class="media-alt">Alt: Drei Bierflaschen mit unterschiedlichen Etiketten auf Holztisch.</div>
</div>
</div>
<!-- Dropzone -->
<button class="dropzone" style="flex-direction:column;justify-content:center;text-align:center;min-height:100%;">
<span class="dz-ico" style="width:38px;height:38px;">
<svg width="18" height="18" viewBox="0 0 16 16" fill="none"><path d="M8 11V4M5 7L8 4l3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><rect x="2.5" y="11.5" width="11" height="2" rx="1" stroke="currentColor" stroke-width="1.4"/></svg>
</span>
<div>
<div class="text-[12.5px] font-semibold text-hub leading-tight">Bilder hinzufügen</div>
<div class="text-[10.5px] text-ink-3 mt-1 leading-tight">JPG, PNG · max. 8 MB · min. 1.200 × 800 px</div>
</div>
</button>
</div>
<div class="field-help">
Bildunterschrift und <strong class="text-ink-2 font-semibold">Alt-Text</strong> sind pro Bild Pflicht für die Veröffentlichung. Das Titelbild erscheint in der Liste auf presseecho &amp; businessportal24.
</div>
</section>
<!-- ─── 6) ANHÄNGE ─── -->
<section class="panel" style="padding:18px 20px 18px;">
<div class="flex items-center justify-between mb-3">
<div class="field-label" style="margin-bottom:0;">
Anhänge / Downloads
<span class="text-ink-4 font-normal" style="letter-spacing:0;text-transform:none;">— optional</span>
</div>
<button class="btn-ghost text-[11.5px]">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
Datei hinzufügen
</button>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="flex items-center gap-3 p-3 border border-bg-rule rounded-[4px] bg-bg-elev">
<span class="w-9 h-11 rounded-[3px] bg-white border border-bg-rule flex items-center justify-center text-[9px] font-bold text-err font-mono">PDF</span>
<div class="min-w-0 flex-1">
<div class="text-[12.5px] font-semibold text-ink truncate">Pressemappe_Manufaktur.pdf</div>
<div class="text-[10.5px] text-ink-3 font-mono mt-0.5">2,4 MB · 12 Seiten</div>
</div>
<button class="menu-trigger w-7 h-7 rounded-[3px] hover:bg-bg-rule-2"><svg width="13" height="13" viewBox="0 0 12 12" fill="none"><path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg></button>
</div>
<button class="dropzone" style="padding:14px;">
<span class="dz-ico" style="width:32px;height:32px;">
<svg width="15" height="15" viewBox="0 0 16 16" fill="none"><path d="M6 2.5h4l3 3v8H3v-8z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M10 2.5V5.5h3M8 8v4M6 10l2 2 2-2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<div>
<div class="text-[12.5px] font-semibold text-hub leading-tight">PDF oder Dokument ziehen</div>
<div class="text-[10.5px] text-ink-3 mt-1">max. 25 MB pro Datei</div>
</div>
</button>
</div>
</section>
<!-- ─── 7) BOILERPLATE ─── -->
<section class="panel" style="padding:18px 20px 18px;">
<div class="flex items-center justify-between mb-3">
<div class="field-label" style="margin-bottom:0;">
Über das Unternehmen
<span class="text-ink-4 font-normal" style="letter-spacing:0;text-transform:none;">— Boilerplate aus Firma</span>
</div>
<div class="flex items-center gap-2">
<span class="badge muted dot" style="font-size:9.5px;">aus Firmenprofil</span>
<button class="btn-ghost text-[11.5px]">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M11 3l2 2-8 8H3v-2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>
Für diese PM überschreiben
</button>
</div>
</div>
<div class="boiler font-serif text-[13.5px] text-ink-2 leading-[1.6]">
<p class="m-0 mb-2">
<strong class="text-ink font-semibold">Über die Tegernseer Brauerei AG.</strong>
Die Tegernseer Brauerei wurde 1675 als Klosterbrauerei der Benediktinerabtei Tegernsee gegründet und
ist heute eine der ältesten kontinuierlich brauenden Brauereien Bayerns. Im Familienbesitz der Herzöge
von Bayern werden jährlich rund 280.000 Hektoliter gebraut. Schwerpunkt: helle Lagerbiere und
Spezialitäten nach dem bayerischen Reinheitsgebot.
</p>
<p class="m-0 text-[12px] text-ink-3 mt-3">
<span class="font-semibold text-ink-2">Sitz:</span> Tegernsee · <span class="font-semibold text-ink-2">Mitarbeiter:</span> 142 · <span class="font-semibold text-ink-2">Web:</span> tegernseer-brauerei.de
</p>
</div>
<div class="field-help">
Wird automatisch unter jeder Pressemitteilung dieser Firma angefügt. Pro PM editierbar — Änderungen wirken sich nicht auf andere Mitteilungen aus.
</div>
</section>
</div>
<!-- /Schreibfläche -->
<!-- ===================================================== -->
<!-- RECHTS: SETTINGS-SIDEBAR -->
<!-- ===================================================== -->
<aside class="space-y-4 sticky-aside" style="align-self:start;">
<!-- ─── Status & Aktion (PRIMÄR) ─── -->
<div class="meta-card" style="padding:16px 18px;border-color:#1A2540;background:linear-gradient(180deg,#FBFAF6 0%,#fff 60%);">
<div class="flex items-center justify-between mb-3">
<span class="meta-title"><span class="num">01</span>Status &amp; Absenden</span>
<span class="badge muted dot">Entwurf</span>
</div>
<!-- Checkliste -->
<div class="bg-bg-elev border border-bg-rule rounded-[4px] p-3 mb-3">
<div class="flex items-center justify-between mb-1">
<span class="eyebrow muted" style="font-size:9.5px;letter-spacing:.16em;">Pre-Submit-Check</span>
<span class="text-[10.5px] font-mono text-ok font-semibold">4 / 6 ok</span>
</div>
<div class="check-row ok">
<span class="ic"><svg width="9" height="9" viewBox="0 0 12 12" fill="none"><path d="M2.5 6l2.5 2.5L9.5 3.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="lbl">Titel vorhanden <span class="sub">62 Zeichen — gute Länge</span></span>
</div>
<div class="check-row ok">
<span class="ic"><svg width="9" height="9" viewBox="0 0 12 12" fill="none"><path d="M2.5 6l2.5 2.5L9.5 3.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="lbl">Mindestlänge Fließtext erreicht <span class="sub">1.420 / min. 600 Zeichen</span></span>
</div>
<div class="check-row ok">
<span class="ic"><svg width="9" height="9" viewBox="0 0 12 12" fill="none"><path d="M2.5 6l2.5 2.5L9.5 3.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="lbl">Firma zugeordnet <span class="sub">Tegernseer Brauerei AG</span></span>
</div>
<div class="check-row ok">
<span class="ic"><svg width="9" height="9" viewBox="0 0 12 12" fill="none"><path d="M2.5 6l2.5 2.5L9.5 3.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="lbl">Mindestens 1 Bild <span class="sub">2 Bilder · Titelbild gesetzt</span></span>
</div>
<div class="check-row warn">
<span class="ic"><svg width="9" height="9" viewBox="0 0 12 12" fill="none"><path d="M6 3.5v3M6 8.5h0" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></span>
<span class="lbl">Themen-Tags fehlen <span class="sub">empfohlen für SEO &amp; Auffindbarkeit</span></span>
</div>
<div class="check-row warn">
<span class="ic"><svg width="9" height="9" viewBox="0 0 12 12" fill="none"><path d="M6 3.5v3M6 8.5h0" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></span>
<span class="lbl">Untertitel optional gesetzt — kein Pressekontakt-Telefon <span class="sub">Journalisten können dich nicht erreichen</span></span>
</div>
</div>
<!-- Primärer Submit -->
<button class="btn-primary full">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M2 8l5 5 7-10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
Zur Prüfung senden
</button>
<p class="text-[11px] text-ink-3 mt-2 leading-[1.45] m-0">
Warnungen (<span class="text-warn font-semibold"></span>) blockieren nicht. Pflichtfelder (<span class="text-err font-semibold"></span>) blockieren. Die Redaktion prüft typ. innerhalb von 24 h.
</p>
<hr class="rule my-3" />
<div class="flex items-center gap-2">
<button class="btn-secondary" style="flex:1;">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M2 8l4 4 8-8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Speichern
</button>
<button class="btn-secondary" style="flex:1;">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.3"/><path d="M5 8.5L7 10.5 11 6.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Vorschau
</button>
</div>
</div>
<!-- ─── Portal-Auswahl ─── -->
<div class="meta-card">
<div class="meta-head">
<span class="meta-title"><span class="num">02</span>Portal</span>
<span class="text-[10.5px] text-ink-3"><strong class="font-mono text-ink-2">1</strong> ausgewählt</span>
</div>
<div class="space-y-2">
<label class="portal-opt is-checked">
<span class="pcheck"></span>
<span>
<span class="ptitle"><span class="dot-pe"></span>presseecho</span>
<span class="psub">Allgemeine Pressewelt · breite Reichweite, redaktionelle Prüfung 24 h</span>
</span>
</label>
<label class="portal-opt">
<span class="pcheck"></span>
<span>
<span class="ptitle"><span class="dot-bp"></span>businessportal24</span>
<span class="psub">B2B &amp; Mittelstand · höhere Sichtbarkeit auf Business-Themen</span>
</span>
</label>
</div>
<p class="text-[11px] text-ink-4 mt-3 mb-0 leading-[1.45] flex items-start gap-1.5">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" class="mt-0.5 flex-shrink-0 text-ink-3"><circle cx="6" cy="6" r="4.5" stroke="currentColor" stroke-width="1.2"/><path d="M6 5v3M6 4v.4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
<span>Im MVP ein Portal pro PM. Cross-Publishing auf beide Portale folgt in Phase 2.</span>
</p>
</div>
<!-- ─── Pressekontakt ─── -->
<div class="meta-card">
<div class="meta-head">
<span class="meta-title"><span class="num">03</span>Pressekontakt</span>
<span class="flex items-center gap-0.5 bg-bg-elev border border-bg-rule rounded-[3px] p-0.5">
<span class="tab-mini is-active">Aus Firma</span>
<span class="tab-mini">Eigener</span>
</span>
</div>
<div class="space-y-2">
<div>
<label class="block text-[10.5px] font-semibold text-ink-3 uppercase tracking-[.10em] mb-1">Name</label>
<input class="inp small" value="Maria Schwarz" />
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="block text-[10.5px] font-semibold text-ink-3 uppercase tracking-[.10em] mb-1">Funktion</label>
<input class="inp small" value="Leitung Kommunikation" />
</div>
<div>
<label class="block text-[10.5px] font-semibold text-ink-3 uppercase tracking-[.10em] mb-1">Telefon</label>
<input class="inp small" placeholder="+49 …" style="border-color:#E1C883;background:#FBF6EB;" />
</div>
</div>
<div>
<label class="block text-[10.5px] font-semibold text-ink-3 uppercase tracking-[.10em] mb-1">E-Mail</label>
<input class="inp small" value="presse@tegernseer-brauerei.de" />
</div>
</div>
<div class="flex items-center gap-2 mt-3 text-[11px] text-warn">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M6 1L11 11H1z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M6 5v2.5M6 9v.2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Telefon empfohlen — Journalisten greifen oft direkt zum Hörer.
</div>
</div>
<!-- ─── Themen-Tags ─── -->
<div class="meta-card">
<div class="meta-head">
<span class="meta-title"><span class="num">04</span>Themen-Tags</span>
<span class="text-[10.5px] text-ink-4"><strong class="font-mono text-ink-2">0</strong> / 5</span>
</div>
<div class="border border-bg-rule rounded-[4px] bg-white px-2 py-2 min-h-[58px] flex flex-wrap items-center gap-1.5">
<span class="text-[11.5px] text-ink-4 italic px-1.5">Tippen, um Tag hinzuzufügen …</span>
</div>
<div class="mt-2.5">
<div class="eyebrow muted mb-1.5" style="font-size:9.5px;">Vorschläge aus Firmenprofil</div>
<div class="flex flex-wrap gap-1.5">
<button class="filter-tag">+ Mittelstand</button>
<button class="filter-tag">+ Brauerei</button>
<button class="filter-tag">+ Tegernsee</button>
<button class="filter-tag">+ Tourismus</button>
<button class="filter-tag">+ Eröffnung</button>
</div>
</div>
<style>
.filter-tag{padding:3px 9px;background:#FBFAF6;border:1px dashed #CFD6E4;border-radius:3px;font-size:11.5px;color:#3A413D;font-weight:500;transition:border-color .12s,background .12s,color .12s;}
.filter-tag:hover{border-style:solid;border-color:#1A2540;background:#E5E9F1;color:#1A2540;}
</style>
<p class="text-[10.5px] text-ink-4 mt-2.5 mb-0 leading-[1.45]">
Kategorie auf presseecho wird automatisch aus dem ersten Tag abgeleitet.
</p>
</div>
<!-- ─── Veröffentlichung ─── -->
<div class="meta-card">
<div class="meta-head">
<span class="meta-title"><span class="num">05</span>Veröffentlichung</span>
</div>
<div class="space-y-2">
<label class="flex items-start gap-2.5 p-2 border border-hub rounded-[4px] bg-hub-soft cursor-pointer" style="box-shadow:inset 0 0 0 1px #1A2540;">
<span class="w-3 h-3 rounded-full bg-hub flex-shrink-0 mt-0.5 flex items-center justify-center"><span class="w-1.5 h-1.5 rounded-full bg-white"></span></span>
<span>
<span class="text-[12.5px] font-semibold text-hub block leading-tight">Sofort nach Freigabe</span>
<span class="text-[11px] text-ink-3 block mt-0.5 leading-tight">geht live, sobald die Redaktion grünes Licht gibt</span>
</span>
</label>
<label class="flex items-start gap-2.5 p-2 border border-bg-rule rounded-[4px] bg-bg-elev cursor-not-allowed" style="opacity:.75;">
<span class="w-3 h-3 rounded-full border-2 border-ink-4 flex-shrink-0 mt-0.5"></span>
<span class="flex-1">
<span class="text-[12.5px] font-semibold text-ink-2 block leading-tight flex items-center gap-2">Geplanter Termin <span class="badge-bald">bald</span></span>
<span class="text-[11px] text-ink-3 block mt-0.5 leading-tight">Datum + Uhrzeit, automatische Veröffentlichung</span>
</span>
</label>
</div>
<hr class="rule my-3" />
<label class="flex items-center gap-2 text-[12px] text-ink-2 cursor-pointer">
<input type="checkbox" class="rounded border-bg-rule" style="accent-color:#1A2540;" />
<span>Sperrfrist setzen (Embargo) — frühestens 12. Juni 2026, 06:00 Uhr</span>
</label>
</div>
<!-- ─── SEO (collapsed) ─── -->
<div class="meta-card">
<div class="collapse-head">
<span class="meta-title"><span class="num">06</span>SEO</span>
<span class="flex items-center gap-2">
<span class="text-[10.5px] text-ink-4">automatisch aus Titel</span>
<svg class="chev" viewBox="0 0 16 16" fill="none"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</div>
</div>
<!-- ─── Phase 2 Footer ─── -->
<div class="bg-accent-soft border border-[#E1C883] rounded-[5px] p-3.5">
<div class="flex items-center gap-2 mb-2">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" class="text-accent-deep"><path d="M8 2l1.5 4 4 1.5L9.5 9 8 13l-1.5-4L2.5 7.5 6.5 6z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>
<span class="eyebrow accent">Phase 2 — bald</span>
</div>
<ul class="text-[11.5px] text-accent-deep leading-[1.55] list-none p-0 m-0 space-y-1">
<li>· KI-Titel-Optimierung &amp; -Lektorat live</li>
<li>· Geplante Veröffentlichung / Scheduling</li>
<li>· Versionshistorie &amp; Kommentare</li>
<li>· Portal-Vorschau (presseecho vs. BP24)</li>
</ul>
</div>
</aside>
</div>
<!-- ============== FUSSZEILE ============== -->
<footer class="flex items-center justify-between pt-6 pb-2 mt-8 text-[11px] text-ink-3 border-t border-bg-rule">
<span>© 2026 presseportale.com · Publisher-Hub</span>
<span class="flex items-center gap-5">
<a href="#" class="hover:text-hub">Tastenkürzel</a>
<a href="#" class="hover:text-hub">Hilfe zum Editor</a>
<a href="/cdn-cgi/l/email-protection#43303633332c313703333126303026332c3137222f266d202c2e" class="hover:text-hub">Support</a>
</span>
</footer>
</div>
</main>
</div>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script></body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -165,7 +165,7 @@
<span class="absolute left-0 top-0 bottom-0 w-[3px] bg-brand"></span>
<div>
<div class="eyebrow mb-2.5 text-[10.5px]" style="color:#FF8B6F;">Einreichen im Publisher-Bereich</div>
<h3 class="font-serif m-0 text-[23px] font-semibold leading-[1.25] tracking-[-0.3px] text-ink-on-dark">Die Einreichung läuft über presseportale.com.</h3>
<h3 class="font-serif m-0 text-[23px] font-semibold leading-[1.25] tracking-[-0.3px] text-ink-on-dark">Die Einreichung läuft über pressekonto.de.</h3>
<p class="mt-2.5 mb-0 text-[13.5px] leading-[1.55] text-ink-on-dark-2 max-w-[540px]">Dort verwalten Sie Mitteilungen, Credits und Newsroom — einmaliges Konto, beide Portale nutzbar (businessportal24 &amp; presseecho.de).</p>
</div>
<div class="flex flex-col items-end gap-2.5">
@ -342,7 +342,7 @@
<p class="mt-4.5 mb-0 text-[12px] text-ink-3" style="margin-top:18px;">
Die Veröffentlichung erfolgt über den zentralen Publisher-Bereich auf
<a href="#" class="text-brand font-medium border-b border-brand">presseportale.com</a>.
<a href="#" class="text-brand font-medium border-b border-brand">pressekonto.de</a>.
Cross-Publishing nach presseecho.de ist optional verfügbar.
</p>
</div>
@ -583,7 +583,7 @@
Zum Publisher-Bereich
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" class="ml-0.5"><path d="M4 8L8.5 3.5M8.5 3.5H5M8.5 3.5V7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="text-[12px] text-ink-on-dark-2">Einreichung läuft über presseportale.com · Login per Magic-Link</span>
<span class="text-[12px] text-ink-on-dark-2">Einreichung läuft über pressekonto.de · Login per Magic-Link</span>
<a href="#" class="mt-1.5 text-[13px] text-ink-on-dark border-b pb-0.5" style="border-color:rgba(255,255,255,0.35);">Oder zuerst Beispiele ansehen →</a>
</div>
</div>
@ -597,7 +597,7 @@
<div class="grid pb-7" style="grid-template-columns:1.5fr 1fr 1fr 1fr;gap:56px;border-bottom:1px solid rgba(255,255,255,0.08);">
<div>
<div class="font-serif text-[22px] font-semibold" style="letter-spacing:-0.4px;">businessportal<span class="text-brand">24</span></div>
<p class="mt-3 text-[12.5px] text-ink-on-dark-2 leading-[1.6] max-w-[380px]">businessportal24 ist ein Service der Presseportale-Gruppe. Plattform für Pressemitteilungen mittelständischer Unternehmen, Selbstständiger und PR-Agenturen im deutschsprachigen Raum.</p>
<p class="mt-3 text-[12.5px] text-ink-on-dark-2 leading-[1.6] max-w-[380px]">businessportal24 ist ein Service der Pressekonto-Gruppe. Plattform für Pressemitteilungen mittelständischer Unternehmen, Selbstständiger und PR-Agenturen im deutschsprachigen Raum.</p>
</div>
<div>
<div class="eyebrow mb-3.5 text-[10px]" style="color:rgba(255,255,255,0.5);">Einreichen</div>
@ -628,7 +628,7 @@
</div>
</div>
<div class="mt-5.5 flex justify-between items-center flex-wrap gap-4 text-[12px] text-ink-on-dark-2" style="margin-top:22px;">
<div>© 2026 Presseportale-Gruppe · Alle Rechte vorbehalten</div>
<div>© 2026 Pressekonto-Gruppe · Alle Rechte vorbehalten</div>
<div class="flex items-center gap-4">
<span class="text-ink-on-dark-2">Für fachlich-spezifische Themen:</span>
<a href="#" class="text-ink-on-dark border-b pb-px" style="border-color:rgba(255,255,255,0.3);">presseecho.de →</a>

View file

@ -24,7 +24,7 @@ Diese Frontends sollen künftig **gegen das neue Backend arbeiten** via dire
### 1.3 Neues Backend (Aufgabe dieses Projekts)
- Läuft unter `presseportale.test` / `presseportale.com`
- Läuft unter `pressekonto.test` / `pressekonto.de`
- Stack: **Laravel 12, PHP 8.4, Livewire 4, Volt, Flux UI 2, MySQL 8, Tailwind 4**
- **Vorarbeiten bereits vorhanden**: Admin-UI-Gerüst, Routes, Auth-Stack
- **Noch zu tun**: Eloquent-Models, Migrations, Services, Daten-Migration, API, Payment (Stripe), Cron-Jobs
@ -58,7 +58,7 @@ Diese Frontends sollen künftig **gegen das neue Backend arbeiten** via dire
| Kriterium | Messbar an |
|---|---|
| Backend unter `presseportale.test` erreichbar | HTTP 200 auf Login-Seite |
| Backend unter `pressekonto.test` erreichbar | HTTP 200 auf Login-Seite |
| Admin-Login (Fortify + 2FA) funktioniert | Feature-Test `AuthTest` |
| Customer-Portal via Magic-Link erreichbar | Feature-Test `MagicLinkLoginTest` |
| Pressemitteilungen CRUD (Admin + Customer) | Livewire-Tests `PressReleaseIndex/Create/Edit/Show` |

View file

@ -4,7 +4,7 @@
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Presseportale Backend (presseportale.test) │
│ Pressekonto Backend (pressekonto.test) │
│ Laravel 12 · PHP 8.4 · Livewire 4 · Volt · Flux UI 2 │
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌─────────┐ │
@ -268,7 +268,7 @@ app/
## 6. Multi-Domain & Portal-Scope
- Pro Domain ein Frontend, aber alle Admin-Screens auf `presseportale.test`.
- Pro Domain ein Frontend, aber alle Admin-Screens auf `pressekonto.test`.
- Middleware `SetCurrentPortal`:
- Web (presseecho.test / businessportal24.test) → `app()->instance('current_portal', Portal::Presseecho|Businessportal24)`
- Admin → Portal-Auswahl via Dropdown, gespeichert in Session (`current_portal`)

View file

@ -224,7 +224,7 @@ Die Views existieren bereits als Blade/Livewire-Stubs (`resources/views/admin/*`
| 11.3 | Produktiv-Import-**Rehearsal** gegen aktuellen Legacy-Snapshot | ⬜ |
| 11.4 | **Go-Live-Mailing**: Passwort-Reset + Sicherheitshinweis an alle User | 🔄 Command/Mailable fertig; Versand und finale Texte offen |
| 11.5 | **API-Kunden-Kommunikation** (Token-Migration) | 🔄 Kundenreport fertig; Freigabeliste und operative Mailtexte offen |
| 11.6 | DNS-Cutover-Plan: `presseportale.com` auf neuen Server | ⬜ |
| 11.6 | DNS-Cutover-Plan: `pressekonto.de` auf neuen Server | ⬜ |
| 11.7 | Read-Only-Modus auf Legacy-Systemen während Final-Import | ⬜ |
| 11.8 | Abschalten der alten Symfony-Server (nach Review-Periode) | ⬜ |

View file

@ -175,8 +175,8 @@ Content-Type: application/json
{
"message": "Legacy API keys are no longer supported.",
"migration_url": "https://presseportale.com/account/tokens",
"docs_url": "https://presseportale.com/docs/api/v1"
"migration_url": "https://pressekonto.de/account/tokens",
"docs_url": "https://pressekonto.de/docs/api/v1"
}
```

View file

@ -154,7 +154,7 @@ php artisan test --compact tests/Feature/LegacyInvoiceArchiveCommandTest.php tes
## 2026-05-04 Architektur-Entscheidung: ein gemeinsames Admin-Panel mit rollenbasierter Sichtbarkeit
Der Auftraggeber hat festgelegt: **es gibt nur ein gemeinsames Admin-Panel** unter dem Presseportale-Backend. Admins, Editoren und Customer arbeiten im selben UI Sichtbarkeit von Menüpunkten und Aktionen entscheidet die Rolle/Permission.
Der Auftraggeber hat festgelegt: **es gibt nur ein gemeinsames Admin-Panel** unter dem Pressekonto-Backend. Admins, Editoren und Customer arbeiten im selben UI Sichtbarkeit von Menüpunkten und Aktionen entscheidet die Rolle/Permission.
- Aktuell laufen Admin-/Editor-Funktionen unter `/admin/*` und Customer-Funktionen unter `/customer/*`. Diese Trennung wird **vor dem Pressemitteilungs-Veröffentlichungs-Block** in eine **gemeinsame Panel-Architektur** überführt.
- Sidebar wird rollenbasiert gefiltert (Admin/Editor sehen alle PMs, Customer nur seine etc.). Routen-Konsolidierung Customer → Admin-Panel.
@ -318,7 +318,7 @@ Status in `03-MIGRATION-PLAN.md` ist auf `⏸️ Vertagt 2026-05-04` gesetzt.
### Was wurde gemacht
#### Paket A Test-Suite stabilisiert
- `phpunit.xml`: `APP_URL=https://presseportale.test` ergänzt, damit `route('login')` etc. nicht mehr auf eine fremde Domain ohne Auth-Routen zeigen.
- `phpunit.xml`: `APP_URL=https://pressekonto.test` ergänzt, damit `route('login')` etc. nicht mehr auf eine fremde Domain ohne Auth-Routen zeigen.
- `tests/Feature/Auth/EmailVerificationTest.php`: Tests werden jetzt sauber per `markTestSkipped()` übersprungen, solange `Features::emailVerification()` in `config/fortify.php` deaktiviert ist. Sobald das Feature aktiviert wird, laufen die Tests automatisch wieder.
- `tests/Feature/ExampleTest.php`: testet jetzt den Health-Endpoint `/up` statt der von Vite/Theme-Layout abhängigen Startseite.
- Leerer `tests/Feature/Feature/` / `tests/Feature/Feature/Billing/`-Doppelpfad entfernt.

View file

@ -166,7 +166,7 @@ Hängt komplett an §2 (Stripe-Account + Produktliste).
| 11.3 | Produktiv-Import-**Rehearsal** gegen aktuellen Legacy-Snapshot | 🔴 | ⬜ |
| 11.4 | Go-Live-Mailing (Passwort-Reset + Sicherheitshinweis) versenden | 🔴 | 🔄 (Code fertig, Texte/Versand offen) |
| 11.5 | API-Kunden-Kommunikation (Token-Migration) versenden | 🔴 | 🔄 (Report fertig, Texte/Versand offen) |
| 11.6 | DNS-Cutover-Plan: `presseportale.com` auf neuen Server | 🔴 | ⬜ |
| 11.6 | DNS-Cutover-Plan: `pressekonto.de` auf neuen Server | 🔴 | ⬜ |
| 11.7 | Read-Only-Modus auf Legacy-Systemen während Final-Import | 🔴 | ⬜ |
| 11.8 | Abschalten der alten Symfony-Server (nach Review-Periode) | 🟡 | ⬜ |

View file

@ -8,6 +8,8 @@ Stand: 2026-05-04. Dieses Kurz-Runbook spiegelt den aktuell implementierten Comm
php artisan legacy:import --source=all --dry-run
php artisan legacy:archive-invoices --dry-run
php artisan legacy:verify --no-report
php artisan legacy:migrate-media --portal=all --type=all --base-path=dev/migration --dry-run
```
Hinweis: `legacy:archive-invoices` importiert die Legacy-Rechnungen vollständig in `legacy_invoices`, inkl. Status/User-Zuordnung, `raw_snapshot`, `pdf_payload` und Report. Die PDF-Erzeugung erfolgt im Customer-Bereich bei Abruf aus diesen Archivdaten.
@ -38,6 +40,7 @@ php artisan legacy:import --source=all --force
php artisan legacy:archive-invoices
php artisan legacy:fix-timestamps
php artisan legacy:verify
php artisan legacy:migrate-media --portal=all --type=all --base-path=dev/migration
```
## Noch nicht im Runbook finalisiert

View file

@ -1,6 +1,6 @@
# Migration 2026 Presseportale Backend
# Migration 2026 Pressekonto Backend
> **Migration des Backends** der beiden Legacy-Portale `presseecho` und `businessportal24` (Symfony 1.4, PHP 5.6) in ein **gemeinsames, modernes Laravel 12 Backend** unter der Domain `presseportale.test` / `presseportale.com`.
> **Migration des Backends** der beiden Legacy-Portale `presseecho` und `businessportal24` (Symfony 1.4, PHP 5.6) in ein **gemeinsames, modernes Laravel 12 Backend** unter der Domain `pressekonto.test` / `pressekonto.de`.
**Start:** 23.04.2026
**Lead-Technologie:** Laravel 12 · PHP 8.4 · Livewire 4 · Volt · Flux UI 2 · Tailwind CSS 4 · MySQL 8
@ -43,7 +43,7 @@ Historisch existieren zwei technisch identische Symfony-1.4-Installationen (`pre
- **DB-Dumps** (2026-04-23): `businessportal24` 42 Tabellen / 578 MB, `presseecho` 43 Tabellen / 369 MB 41 Tabellen sind strukturell identisch, 3 abweichend (`press_release_image_old` nur BP24, `category_pe_data` + `press_release_pe_data` nur PE)
Aktueller Implementierungsstand (Code-Abgleich 2026-04-29):
- ✅ **Domain-basiertes Theme-System** (`presseportale` / `presseecho` / `businessportal24`)
- ✅ **Domain-basiertes Theme-System** (`pressekonto` / `presseecho` / `businessportal24`)
- ✅ **Admin-UI** mit echten Daten für Dashboard, Users/Roles, Companies, Contacts, Categories-Index und PressRelease-CRUD/-Workflow
- ✅ **Auth-Stack**: Fortify, Sanctum, Spatie/Permission
- ✅ **Admin-Schutz und Portal-Scoping**: `EnsureUserIsAdmin`, `SetCurrentPortal`, `PortalScope`, Sidebar-Portal-Switcher

View file

@ -26,7 +26,7 @@ services:
DB_PORT: 3306
# Hier definieren wir nur die Haupt-Datenbank für .env
# Die anderen beiden richtest du in Laravel ein
DB_DATABASE: presseportale
DB_DATABASE: pressekonto
DB_USERNAME: root
DB_PASSWORD: password
MAIL_HOST: global-mailpit
@ -40,23 +40,23 @@ services:
labels:
- "traefik.enable=true"
# Portal Domain
- "traefik.http.routers.presseportale.rule=Host(`presseportale.test`)"
- "traefik.http.routers.presseportale.entrypoints=websecure"
- "traefik.http.routers.presseportale.tls=true"
- "traefik.http.routers.presseportale.service=presseportale-service-prc"
- "traefik.http.routers.pressekonto.rule=Host(`pressekonto.test`)"
- "traefik.http.routers.pressekonto.entrypoints=websecure"
- "traefik.http.routers.pressekonto.tls=true"
- "traefik.http.routers.pressekonto.service=pressekonto-service-prc"
# Presseecho Domain
- "traefik.http.routers.presseecho.rule=Host(`presseecho.test`)"
- "traefik.http.routers.presseecho.entrypoints=websecure"
- "traefik.http.routers.presseecho.tls=true"
- "traefik.http.routers.presseecho.service=presseportale-service-prc"
- "traefik.http.routers.presseecho.service=pressekonto-service-prc"
# Business Portal Domain
- "traefik.http.routers.businessportal.rule=Host(`businessportal24.test`)"
- "traefik.http.routers.businessportal.entrypoints=websecure"
- "traefik.http.routers.businessportal.tls=true"
- "traefik.http.routers.businessportal.service=presseportale-service-prc"
- "traefik.http.routers.businessportal.service=pressekonto-service-prc"
# Asset Domain für Vite-Server Portal (Port 5177)
- "traefik.http.routers.assets-portal.rule=Host(`assets.presseportale.test`)"
- "traefik.http.routers.assets-portal.rule=Host(`assets.pressekonto.test`)"
- "traefik.http.routers.assets-portal.entrypoints=websecure"
- "traefik.http.routers.assets-portal.tls=true"
- "traefik.http.routers.assets-portal.service=assets-portal-service-prc"
@ -74,7 +74,7 @@ services:
- "traefik.http.routers.assets-businessportal.service=assets-web-service-prc"
# Service Definition - NUR EINMAL!
- "traefik.http.services.presseportale-service-prc.loadbalancer.server.port=80"
- "traefik.http.services.pressekonto-service-prc.loadbalancer.server.port=80"
- "traefik.http.services.assets-portal-service-prc.loadbalancer.server.port=5177"
- "traefik.http.services.assets-portal-service-prc.loadbalancer.server.scheme=http"
- "traefik.http.services.assets-web-service-prc.loadbalancer.server.port=5178"

View file

@ -19,7 +19,7 @@
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_URL" value="https://presseportale.test"/>
<env name="APP_URL" value="https://pressekonto.test"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>

View file

@ -0,0 +1,144 @@
/* inter-tight-100 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 100;
src: url('../fonts/inter-tight-v9-latin-100.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-100italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 100;
src: url('../fonts/inter-tight-v9-latin-100italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-200 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 200;
src: url('../fonts/inter-tight-v9-latin-200.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-200italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 200;
src: url('../fonts/inter-tight-v9-latin-200italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-300 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 300;
src: url('../fonts/inter-tight-v9-latin-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-300italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 300;
src: url('../fonts/inter-tight-v9-latin-300italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-regular - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 400;
src: url('../fonts/inter-tight-v9-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 400;
src: url('../fonts/inter-tight-v9-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-500 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 500;
src: url('../fonts/inter-tight-v9-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-500italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 500;
src: url('../fonts/inter-tight-v9-latin-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-600 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 600;
src: url('../fonts/inter-tight-v9-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-600italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 600;
src: url('../fonts/inter-tight-v9-latin-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-700 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 700;
src: url('../fonts/inter-tight-v9-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-700italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 700;
src: url('../fonts/inter-tight-v9-latin-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-800 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 800;
src: url('../fonts/inter-tight-v9-latin-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-800italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 800;
src: url('../fonts/inter-tight-v9-latin-800italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-900 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 900;
src: url('../fonts/inter-tight-v9-latin-900.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-900italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 900;
src: url('../fonts/inter-tight-v9-latin-900italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}

View file

@ -0,0 +1,128 @@
/* jetbrains-mono-100 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 100;
src: url('../fonts/jetbrains-mono-v24-latin-100.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-100italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 100;
src: url('../fonts/jetbrains-mono-v24-latin-100italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-200 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 200;
src: url('../fonts/jetbrains-mono-v24-latin-200.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-200italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 200;
src: url('../fonts/jetbrains-mono-v24-latin-200italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-300 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 300;
src: url('../fonts/jetbrains-mono-v24-latin-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-300italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 300;
src: url('../fonts/jetbrains-mono-v24-latin-300italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-regular - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
src: url('../fonts/jetbrains-mono-v24-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 400;
src: url('../fonts/jetbrains-mono-v24-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-500 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 500;
src: url('../fonts/jetbrains-mono-v24-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-500italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 500;
src: url('../fonts/jetbrains-mono-v24-latin-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-600 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 600;
src: url('../fonts/jetbrains-mono-v24-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-600italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 600;
src: url('../fonts/jetbrains-mono-v24-latin-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-700 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 700;
src: url('../fonts/jetbrains-mono-v24-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-700italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 700;
src: url('../fonts/jetbrains-mono-v24-latin-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-800 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 800;
src: url('../fonts/jetbrains-mono-v24-latin-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-800italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 800;
src: url('../fonts/jetbrains-mono-v24-latin-800italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}

Some files were not shown because too many files have changed in this diff Show more