diff --git a/AGENTS.md b/AGENTS.md index b34a1b6..6f0ff42 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,6 +40,11 @@ Der Entwicklungsplan steht in ENTWICKLUNGSPLAN.md. Produktname und Stack sind no - Nur tatsächlich ausgeführte Prüfungen als bestanden melden. Fehlende Zugriffe oder nicht ausführbare Prüfungen klar festhalten. - Keine garantierten SEO-Rankings oder KI-Zitierungen versprechen. +## Arbeitsweise mit Codex + +- Claude baut, Codex prüft nur lesend: `.\scripts\codex-review.ps1` (seit dem letzten Release-Tag), mit `-Seit `, `-Commit `, `-Offen` oder `-Alles`. Sinnvoll vor einem Release und nach Änderungen an Rechten, Speichern/Publizieren, Migrationen, Updater oder Formularversand. +- Befunde von Codex sind Hinweise, keine Wahrheit: jeden am Code nachvollziehen, echte Fehler mit Regressionstest beheben, verworfene kurz begründen. + ## Zusammenarbeit und Übergabe - Mit Umut auf Deutsch, klar und direkt kommunizieren. diff --git a/README.md b/README.md index 3f0e4e5..f3e17e9 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,16 @@ Die öffentliche Adresse der Website steht als `SITE_ORIGIN` in `.env` – mit o `test:links` folgt der Sitemap, sammelt jeden internen Verweis samt Bildern, Stylesheets und Hintergrundbildern und meldet jede Antwort ab 400 – gebaut, nachdem im Nachbau von kant-it.de vier Verweise ins Leere zeigten, die kein Test finden konnte. +**Zweitprüfung mit Codex:** Claude baut, Codex prüft ab und zu nur lesend – in einer sauberen Git-Kopie ohne `.env`, `var/` und Kundendaten. Die Prüfregeln stehen in `scripts/codex-review-prompt.md`; vorausgesetzt ist die angemeldete Codex-CLI (`codex`). Die Schlussantwort steht am Ende der Ausgabe und zusätzlich unter `reports/codex-review-.md`. + +```powershell +.\scripts\codex-review.ps1 # alles seit dem letzten Release-Tag +.\scripts\codex-review.ps1 -Seit v1.42.0 # seit einem Tag oder Commit +.\scripts\codex-review.ps1 -Commit # ein einzelner Commit +.\scripts\codex-review.ps1 -Offen # noch nicht committete Änderungen (ohne neue, unversionierte Dateien) +.\scripts\codex-review.ps1 -Alles # gesamter Code +``` + Das Lighthouse-Skript misst dreimal je Profil und prüft die Mediane gegen die Projektziele. Welche Adressen geprüft werden, sagt `CMS_PATHS` (mit Komma getrennt, Standard `/`); in der Git-Bash davor `MSYS_NO_PATHCONV=1` setzen, sonst wird aus `/` ein Windows-Pfad. `test:editor -- --audit` prüft die im isolierten Editortest gestaltete v3-Seite mit Abschnittsvorlage, Galerie, tatsächlichen Bildvarianten und Effekten; Berichte stehen unter `reports/lighthouse-editor`. Uploads und Datenbank des Tests liegen getrennt unter `var/browser-tests`. Diese kleinen Prototypseiten ersetzen nicht die spätere Referenzsuite mit Kundeninhalten, Fotos, Beitrag und Formular. Messgrenzen und Ergebnisse: `docs/QUALITY.md`. ## Inhalte und Entwicklung diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a1dbd24..d5ca753 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -56,7 +56,7 @@ Stand: 10.11.2026. Arbeitstitel KantSites; Produktname offen. **Entscheidung:** Eigene Tabelle `video` und `VideoLibrary` nach dem Vorbild der Dokumente, Dateien als `.mp4`/`.webm` im Medienordner, bis 64 MB, Typ nach den ersten Bytes (MOV abgewiesen). Ausgeliefert unter `/media/.` mit Range-Anfragen und `default-src 'none'`. Im Dokument stehen Adressen, nicht Kennungen: Baustein `video` (`src`, `srcWebm`, `poster`, `title`, `mode`: `player`/`loop`) und an der Sektion `props.backgroundVideo`/`backgroundVideoWebm`. Der Validator lässt nur Adressen der eigenen Mediathek zu; fremde Videos bleiben Sache von „Externer Inhalt“ mit Einwilligung. -Schleifen stehen ohne `autoplay` im HTML, mit `preload="none"`. `video.js` startet sie, solange sie zu sehen sind, und zeigt den Knopf zum Anhalten (WCAG 2.2.2). Bei „Bewegung reduzieren“ oder Datensparen bleibt das Standbild, starten lässt es sich von Hand. Das Skript kommt nur auf Seiten mit Schleife; ein Spieler lässt `script-src 'none'` stehen. +Schleifen stehen ohne `autoplay` im HTML, mit `preload="none"`. `video.js` startet sie, solange sie zu sehen sind, und zeigt den Knopf zum Anhalten (WCAG 2.2.2). Bei „Bewegung reduzieren“ oder Datensparen bleibt das Standbild, starten lässt es sich von Hand. Verweigert der Browser den Start (`NotAllowedError`), holt die erste Berührung oder Taste ihn nach; ein Klick auf den Knopf zählt dabei nur als dessen eigene Entscheidung. Das Skript kommt nur auf Seiten mit Schleife; ein Spieler lässt `script-src 'none'` stehen. **Folgen:** Kein Umrechnen auf dem Server: Wer zu große Dateien hochlädt, bekommt zu große Dateien, deshalb der Hinweis im Dialog. Die PHP-Grenzen `upload_max_filesize` und `post_max_size` des Hostings gelten zusätzlich. Die Überlagerung einer Sektion ist Teil ihres CSS-Hintergrunds und liegt damit unter dem Video. Website-Pakete von vor v28 bleiben importierbar, die Tabelle gilt dort als leer. diff --git a/docs/STATUS.md b/docs/STATUS.md index e53e86b..9d7cfd4 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,5 +1,33 @@ # Projektstatus +## Codex als Zweitprüfer (11.11.2026, noch nicht committet) + +**Anlass:** Umut: Codex-CLI wie bei Domira als Bugprüfer einrichten, ab und zu mitlaufen lassen. + +**Erledigt:** `scripts/codex-review.ps1` (nach Domira-Vorbild: nur lesend, `--ephemeral`, saubere Git-Kopie ohne `.env`/`var/`) mit den Umfängen seit letztem Release-Tag (Standard), `-Seit`, `-Commit`, `-Offen`, `-Alles`; Schlussantwort zusätzlich unter `reports/codex-review-.md`. Prüfregeln für KantSites in `scripts/codex-review-prompt.md`. README (Prüfen) und AGENTS.md (Arbeitsweise mit Codex) ergänzt. + +**Geprüft:** Probelauf `-Commit 84cdf6a` mit codex-cli 0.167.1: läuft durch (Exit 0, rund 47 000 Tokens), Antwort auf Deutsch („keine konkret belegbaren Fehler“), Kopie danach entfernt. Skript fehlerfrei geparst, reines ASCII. **Nicht geprüft:** `-Offen`, `-Alles` und die Ablage unter `reports/` (nach dem Probelauf ergänzt) in einem echten Lauf. + +**Nächster Schritt:** vor dem nächsten Release `.\scripts\codex-review.ps1` laufen lassen und die Befunde nachvollziehen. + +## Luximmo: Instagram und Facebook im Fuß (11.11.2026, noch nicht committet, nicht live) + +**Erledigt:** `seed-luximmo.php`: Baustein `fuss-profile` (runde Symbole, Instagram und Facebook) unter dem Spruch im Fuß; Adressen aus `LUXIMMO_INSTAGRAM`/`LUXIMMO_FACEBOOK`, sonst leer und unsichtbar. Datenbank und `var/luximmo-paket.zip` neu gebaut (ohne Adressen). + +**Geprüft:** In einer Kopie der Datenbank mit Beispieladressen: beide Links im Fuß, neuer Tab, Namen für Screenreader, 39×39 px. **Offen:** echte Profiladressen (nicht auf lux-immo.net und nicht im Netz gefunden) – live unter Einstellungen → Unternehmensdaten → Soziale Profile eintragen. + +## Luximmo: Autostart, Symbole im Ablauf, Linien der Leistungen (11.11.2026, noch nicht committet, nicht live) + +**Anlass:** Umut: Video startet nicht von selbst, Ziffern im Ablauf ersetzen, Aufklapp-Linien bei den Leistungen auf eine Höhe. + +**Erledigt:** `video.js` (Kern): Verweigert der Browser den Start, startet das Video bei der ersten Berührung oder Taste. `seed-luximmo.php`: Symbole statt Ziffern im Ablauf (Brief, Personen, Dokument, Schlüssel), Leistungen als Subgrid. Lokale Datenbank und `var/luximmo-paket.zip` neu gebaut. + +**Geprüft (Playwright, 8108):** Linien bei 1440 und 2000 px auf einer Höhe, bei 900 px je Reihe; ein geöffneter Eintrag verschiebt die Nachbarn nicht; kein Querscrollen bis 390 px. Start ohne Eingabe läuft; simulierte Verweigerung startet nach dem ersten Klick, der Knopf hält danach an. Nicht geprüft: `test:browser`, Lighthouse, PHPUnit, Firefox, Safari. + +**Bekannte Fehler / offen:** Umut muss auf seinem Gerät Play drücken. Nicht nachgestellt: lux-immo.net startet von selbst in Chrome (mit Profil) und frisch in Chrome, Edge und Chromium unter Windows. Bei „Bewegung reduzieren“, Datensparen oder einer Browser-Sperre bleibt der Autostart aus; Video nur als WebM. + +**Nächster Schritt:** Browser und Gerät von Umut klären, danach Release und Paket-Import für die Live-Seite. + ## Fassung 1.43.1: Einwilligung – beide Antworten gleich gestaltet (11.11.2026) **Anlass:** SUASIO-Scan von huefner-gartenbau.de: Einwilligung „nicht rechtskonform“ wegen Nudging (grün gefülltes „Alle akzeptieren“ neben Rahmenknopf „Nur notwendige“). ADR 131. diff --git a/public/assets/video.js b/public/assets/video.js index 3a9d9fc..ff60e48 100644 --- a/public/assets/video.js +++ b/public/assets/video.js @@ -16,6 +16,23 @@ for (const video of document.querySelectorAll('[data-video-loop]')) { if (!toggle) continue; let wanted = !reduced.matches && !saving; let visible = false; + // Verweigert der Browser den Start, holt die erste Berührung oder Taste ihn nach. Der Knopf + // entscheidet selbst: Sein Klick würde das nachgeholte Abspielen sonst gleich wieder anhalten. + const retry = event => { + if (toggle.contains(event.target)) return; + stopWaiting(); + wanted = true; + paint(); + sync(); + }; + const waitForGesture = () => { + addEventListener('pointerup', retry); + addEventListener('keydown', retry); + }; + const stopWaiting = () => { + removeEventListener('pointerup', retry); + removeEventListener('keydown', retry); + }; const paint = () => { toggle.classList.toggle('is-paused', !wanted); toggle.setAttribute('aria-label', wanted ? toggle.dataset.pause : toggle.dataset.play); @@ -24,13 +41,13 @@ for (const video of document.querySelectorAll('[data-video-loop]')) { if (!wanted || !visible) { video.pause(); return; } video.play().catch(error => { // Ein Anhalten mitten im Starten ist kein Fehler. Verweigert der Browser das Abspielen, - // etwa im Stromsparmodus, zeigt der Knopf die Wahrheit. - if (error.name === 'NotAllowedError') { wanted = false; paint(); } + // etwa im Stromsparmodus, zeigt der Knopf die Wahrheit, bis eine Eingabe den Start erlaubt. + if (error.name === 'NotAllowedError') { wanted = false; paint(); waitForGesture(); } }); }; toggle.hidden = false; - toggle.addEventListener('click', () => { wanted = !wanted; paint(); sync(); }); - reduced.addEventListener('change', () => { if (reduced.matches) { wanted = false; paint(); sync(); } }); + toggle.addEventListener('click', () => { stopWaiting(); wanted = !wanted; paint(); sync(); }); + reduced.addEventListener('change', () => { if (reduced.matches) { stopWaiting(); wanted = false; paint(); sync(); } }); new IntersectionObserver(([entry]) => { visible = entry.isIntersecting; sync(); }).observe(video); paint(); } diff --git a/scripts/codex-review-prompt.md b/scripts/codex-review-prompt.md new file mode 100644 index 0000000..81b1750 --- /dev/null +++ b/scripts/codex-review-prompt.md @@ -0,0 +1,13 @@ +Du prüfst Code von KantSites, einem selbst betriebenen CMS für Kundenwebsites (PHP 8.4, Symfony, Doctrine, Twig, SQLite oder MySQL/MariaDB, Shared Hosting ohne Node). Die Regeln stehen in AGENTS.md, Entscheidungen in docs/ARCHITECTURE.md. Zusätzlich gilt: keine nativen Browser-Dialoge (`confirm()`, `alert()`, `prompt()`); Rückfragen laufen über einen Dialog der Anwendung. + +Ändere nichts. Melde nur echte Fehler, bei denen du ein konkretes Fehlerszenario benennen kannst: + +1. Sicherheit: Umgehung von Anmeldung oder Rollen in der Verwaltung, fehlende Rechteprüfung, CSRF, XSS (zum Beispiel `|raw` in Twig, Inhalte aus Blöcken, Formularen oder Plugins), offene Redirects, Pfadmanipulation bei Uploads, Medien, Inhaltspaketen und Updater, Verstöße gegen die Schutzkopfzeilen. +2. Korrektheit: Logikfehler beim Speichern, Publizieren und Laden von Seiten, Verlust von Entwürfen, nicht erkannte veraltete Schreibzugriffe (`StaleWrite`), fehlerhafte Migrationen von Dokumentschema oder Datenbank, Unterschiede zwischen SQLite und MySQL, veralteter Cache nach dem Publizieren, Fehler in der Übersetzungsebene, im Formularversand und im Updater. +3. Öffentliche Ausgabe: Editorcode im öffentlichen Paket, fehlender Hauptinhalt im HTML, Rückschritte bei Barrierefreiheit (fehlende Beschriftungen, Tastaturbedienung, Kontrast), Leistung (LCP, CLS, render-blockierende Ressourcen) und SEO (Canonical, Sitemap, strukturierte Daten). +4. Fehlende Tests für riskante Pfade. +5. Verstöße gegen die Regeln in AGENTS.md, zum Beispiel native Browser-Dialoge. + +Keine Stilfragen, keine Vorschläge für Refactorings. Die Skripte `scripts/seed-*.php` und `scripts/reset-*.sh` bauen Referenzwebsites mit absichtlich festen Inhalten auf. Ignoriere `vendor/`, `node_modules/`, `public/assets/build/`, `public/assets/fonts/`, `composer.lock` und `package-lock.json`. + +Antworte auf Deutsch. Für jeden Befund: Datei und Zeile, Schwere (hoch, mittel, niedrig), konkretes Fehlerszenario, Vorschlag zur Behebung. Wenn nichts zu melden ist, sag das in einem Satz. diff --git a/scripts/codex-review.ps1 b/scripts/codex-review.ps1 new file mode 100644 index 0000000..df0dbff --- /dev/null +++ b/scripts/codex-review.ps1 @@ -0,0 +1,63 @@ +# Laesst Codex einen Stand pruefen, nur lesend und in einer sauberen Kopie ohne .env. +# Die Datei bleibt reines ASCII, weil Windows PowerShell 5.1 sonst Umlaute falsch liest. +# .\scripts\codex-review.ps1 alles seit dem letzten Release-Tag +# .\scripts\codex-review.ps1 -Seit v1.42.0 alles seit einem Tag oder Commit +# .\scripts\codex-review.ps1 -Commit einen einzelnen Commit +# .\scripts\codex-review.ps1 -Offen die noch nicht committeten Aenderungen +# .\scripts\codex-review.ps1 -Alles den gesamten Code +param( + [string]$Seit = "", + [string]$Commit = "", + [switch]$Offen, + [switch]$Alles +) + +$utf8 = New-Object System.Text.UTF8Encoding $false +$OutputEncoding = $utf8 +[Console]::OutputEncoding = $utf8 + +$repo = (git rev-parse --show-toplevel).Trim() +$regeln = Get-Content -Raw -Encoding UTF8 (Join-Path $repo "scripts\codex-review-prompt.md") +$ziel = if ($Commit) { $Commit } else { "HEAD" } + +if ($Alles) { + $umfang = "Scope: the whole code under src/, templates/, public/assets/ (without build/ and fonts/), plugins/, migrations/, config/ and zentrale/." +} elseif ($Commit) { + $umfang = "Scope: the changes from: git show $Commit" +} elseif ($Offen) { + $umfang = "Scope: the uncommitted changes, visible with: git diff HEAD (new untracked files are not included)." +} else { + if (-not $Seit) { $Seit = (git -C $repo describe --tags --abbrev=0 HEAD).Trim() } + if ($LASTEXITCODE -ne 0 -or -not $Seit) { Write-Error "Kein Release-Tag gefunden, bitte -Seit angeben."; exit 1 } + $umfang = "Scope: the changes from: git diff $Seit..HEAD" +} +Write-Host $umfang + +$kopie = Join-Path $env:TEMP ("kantsites-review-" + [guid]::NewGuid().ToString("N").Substring(0, 8)) + +git -C $repo worktree add --quiet --detach $kopie $ziel +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +try { + if ($Offen) { + $patch = Join-Path $env:TEMP ("kantsites-review-" + [guid]::NewGuid().ToString("N").Substring(0, 8) + ".patch") + git -C $repo diff HEAD --binary --output="$patch" + if ((Get-Item $patch).Length -eq 0) { Write-Host "Keine offenen Aenderungen."; Remove-Item $patch; exit 0 } + git -C $kopie apply --whitespace=nowarn "$patch" + $angewendet = $LASTEXITCODE + Remove-Item $patch + if ($angewendet -ne 0) { Write-Error "Offene Aenderungen liessen sich nicht in die Kopie uebernehmen."; exit $angewendet } + } + $berichte = Join-Path $repo "reports" + if (-not (Test-Path $berichte)) { New-Item -ItemType Directory $berichte | Out-Null } + $bericht = Join-Path $berichte ("codex-review-" + (Get-Date -Format "yyyy-MM-dd-HHmm") + ".md") + "$umfang`n`n$regeln" | codex exec --sandbox read-only --ephemeral --cd $kopie --output-last-message $bericht - + $status = $LASTEXITCODE + if (Test-Path $bericht) { + Write-Host "`n===== Ergebnis (auch in $bericht) =====" + Get-Content -Raw -Encoding UTF8 $bericht | Write-Host + } +} finally { + git -C $repo worktree remove --force $kopie +} +exit $status diff --git a/scripts/seed-luximmo.php b/scripts/seed-luximmo.php index fb6c112..10db172 100644 --- a/scripts/seed-luximmo.php +++ b/scripts/seed-luximmo.php @@ -494,6 +494,11 @@ $fuss = [ $column('fuss-marke', [ ['id' => 'fuss-logo', 'type' => 'html', 'props' => ['html' => $logoSvg('role="img" aria-label="'.$marke.'"')], 'style' => ['desktop' => ['width' => '96px', 'maxWidth' => '96px', 'margin' => $seiten('0px')]], 'css' => $logoCss], $text('fuss-spruch', 'Ihr neuer Immobilienpartner vor Ort. Ein Lichtblick für Ihre Immobilie.', ['desktop' => ['fontSize' => '15px', 'lineHeight' => 1.6, 'color' => '@text', 'maxWidth' => '280px', 'margin' => ['top' => '20px', 'bottom' => '0px']], 'mobile' => ['maxWidth' => 'none']]), + // Adressen aus Einstellungen → Unternehmensdaten → Soziale Profile; ohne Eintrag + // bleibt der Baustein unsichtbar. + ['id' => 'fuss-profile', 'type' => 'social', 'props' => ['size' => 20, 'shape' => 'circle', 'layout' => 'horizontal', 'labels' => false, 'networks' => ['instagram', 'facebook']], + 'style' => ['desktop' => ['margin' => ['top' => '22px', 'bottom' => '0px']]], + 'css' => "& a{color:#141414;border:1px solid #E6DED2;transition:background-color .2s,color .2s,border-color .2s}\n& a:hover{background:#141414;color:#fff;border-color:#141414}"], ]), $column('fuss-anschrift', [ $kleinzeile('fuss-anschrift-titel', 'Adresse', '@gold-dunkel', '8px'), @@ -543,6 +548,8 @@ $vorteil = static fn (int $n, string $icon, string $titel, string $satz) => $col "transition:transform .3s ease,background-color .3s ease\n&:hover{transform:translateY(-4px);background-color:#F2ECE2}"); /** Eine Leistung: Foto, Titel, Satz, aufklappbar „Was dazugehört“. */ +// Die vier Teile liegen als Subgrid auf gemeinsamen Zeilen des Rasters: Die Aufklapp-Linien stehen so +// auf einer Höhe, auch bei ungleich langen Sätzen, und ein geöffneter Eintrag verschiebt die anderen nicht. $leistung = static function (string $id, array $foto, string $alt, string $titel, string $satz, array $punkte) use ($column, $bild, $heading, $text, $nurOben): array { return $column($id, [ $bild($id.'-bild', $foto, $alt, [ @@ -551,7 +558,7 @@ $leistung = static function (string $id, array $foto, string $alt, string $titel 'mobile' => ['imageHeight' => '260px'], ], null, "overflow:hidden;border-radius:24px\n& img{display:block;width:100%;transition:transform .6s ease}\n&:hover img{transform:scale(1.04)}"), $heading($id.'-titel', $titel, 3, ['desktop' => ['fontSize' => '24px', 'fontWeight' => '500', 'letterSpacing' => '-0.02em', 'color' => '@tinte', 'margin' => ['top' => '0px', 'bottom' => '6px']]]), - $text($id.'-satz', $satz, ['desktop' => ['fontSize' => '17px', 'lineHeight' => 1.55, 'color' => '@text', 'margin' => ['bottom' => '0px']]]), + $text($id.'-satz', $satz, ['desktop' => ['fontSize' => '17px', 'lineHeight' => 1.55, 'color' => '@text', 'margin' => ['bottom' => '0px']]], 'overflow-wrap:break-word'), ['id' => $id.'-mehr', 'type' => 'accordion', 'props' => ['faq' => false], 'style' => ['desktop' => ['borderWidth' => $nurOben('1px'), 'borderStyle' => 'solid', 'borderColor' => '@linie', 'margin' => ['top' => '20px']]], 'children' => [ @@ -564,13 +571,17 @@ $leistung = static function (string $id, array $foto, string $alt, string $titel ], ], ], - ]); + ], ['tablet' => ['padding' => ['bottom' => '56px']]], 'display:grid;grid-row:span 4;grid-template-rows:subgrid;grid-template-columns:minmax(0,1fr);row-gap:0'); }; -/** Ein Schritt des Wechsels: große Ziffer in Gold, Titel, Satz. */ -$schritt = static fn (int $n, string $titel, string $satz) => $column('schritt-'.$n, [ - $text('schritt-'.$n.'-nummer', '0'.$n, ['desktop' => ['fontSize' => '15px', 'fontWeight' => '500', 'letterSpacing' => '0.08em', 'color' => '@gold-dunkel', 'margin' => ['bottom' => '28px']]], - "display:flex;align-items:center;gap:12px\n&::after{content:'';flex:1;height:1px;background:#E6DED2}"), +/** Ein Schritt des Wechsels: Symbol im weißen Kreis mit Linie zum nächsten Schritt, Titel, Satz. */ +$schritt = static fn (int $n, string $icon, string $titel, string $satz) => $column('schritt-'.$n, [ + $column('schritt-'.$n.'-kopf', [ + $column('schritt-'.$n.'-kreis', [$symbol('schritt-'.$n.'-icon', $icon, 24, ['desktop' => ['color' => '@gold-dunkel']])], ['desktop' => [ + 'display' => 'flex', 'alignItems' => 'center', 'justifyContent' => 'center', 'flexShrink' => 0, 'width' => '52px', 'height' => '52px', 'backgroundColor' => '@papier', 'borderRadius' => $rund('50%'), + ]]), + ], ['desktop' => ['display' => 'flex', 'alignItems' => 'center', 'gap' => '12px', 'margin' => ['bottom' => '24px']]], + "&::after{content:'';flex:1;height:1px;background:#E6DED2}"), $heading('schritt-'.$n.'-titel', $titel, 3, ['desktop' => ['fontSize' => '22px', 'fontWeight' => '500', 'letterSpacing' => '-0.015em', 'color' => '@tinte', 'margin' => ['top' => '0px', 'bottom' => '10px']]]), $text('schritt-'.$n.'-satz', $satz, ['desktop' => ['fontSize' => '16px', 'lineHeight' => 1.6, 'color' => '@text', 'margin' => ['bottom' => '0px']]]), ]); @@ -660,7 +671,7 @@ $startseite = [ 'Marktgerechte Bewertung', 'Persönliche Begleitung bis zum erfolgreichen Verkauf', ]), - $leistung('leistung-weg', $medien['weg'], 'Wohnanlage mit mehreren Gebäuden und grünem Innenhof', 'WEG-Verwaltung', 'Strukturierte, transparente und zuverlässige Verwaltung von Wohnungseigentümergemeinschaften.', [ + $leistung('leistung-weg', $medien['weg'], 'Wohnanlage mit mehreren Gebäuden und grünem Innenhof', 'WEG-Verwaltung', "Strukturierte, transparente und zuverlässige Verwaltung von Wohnungseigentümer\u{AD}gemeinschaften.", [ 'Eigentümerversammlung vorbereiten, leiten, protokollieren', 'Beschlüsse umsetzen und Beschlusssammlung führen', 'Wirtschaftsplan und Jahresabrechnung', @@ -682,8 +693,9 @@ $startseite = [ 'Übergaben und Wohnungsabnahmen', ]), ], [ - 'desktop' => ['display' => 'grid', 'gridColumns' => 4, 'columnGap' => '28px', 'rowGap' => '56px', 'alignItems' => 'flex-start'], - 'tablet' => ['gridColumns' => 2], + 'desktop' => ['display' => 'grid', 'gridColumns' => 4, 'columnGap' => '28px', 'rowGap' => '0px'], + // Der Abstand zwischen den Reihen sitzt in den Karten, ein Zeilenabstand läge zwischen ihren Teilen. + 'tablet' => ['gridColumns' => 2, 'margin' => ['bottom' => '-56px']], 'mobile' => ['gridColumns' => 1], ]), ], $abschnitt('0px', '0px'), 'leistungen', '', 'wide'), @@ -699,10 +711,10 @@ $startseite = [ 'mobile' => ['margin' => ['bottom' => '40px']], ]), $column('wechsel-raster', [ - $schritt(1, 'Anfrage', 'Sie schildern uns kurz Ihr Objekt – online in drei Schritten oder im Gespräch.'), - $schritt(2, 'Kennenlernen', 'Wir sehen uns das Objekt vor Ort an und klären, was Ihnen wichtig ist.'), - $schritt(3, 'Angebot', 'Sie erhalten ein klares Angebot mit allen Leistungen – ohne Kleingedrucktes.'), - $schritt(4, 'Übergabe', 'Wir übernehmen Unterlagen, Konten und Verträge und informieren Mieter und Eigentümer.'), + $schritt(1, 'mail', 'Anfrage','Sie schildern uns kurz Ihr Objekt – online in drei Schritten oder im Gespräch.'), + $schritt(2, 'people', 'Kennenlernen','Wir sehen uns das Objekt vor Ort an und klären, was Ihnen wichtig ist.'), + $schritt(3, 'document', 'Angebot','Sie erhalten ein klares Angebot mit allen Leistungen – ohne Kleingedrucktes.'), + $schritt(4, 'key', 'Übergabe','Wir übernehmen Unterlagen, Konten und Verträge und informieren Mieter und Eigentümer.'), ], [ 'desktop' => ['display' => 'grid', 'gridColumns' => 4, 'gap' => '40px'], 'tablet' => ['gridColumns' => 2, 'rowGap' => '48px'], @@ -1134,6 +1146,8 @@ $settings->save([ 'organisationLogoMediaId' => $medien['logo']['id'], 'faviconMediaId' => $medien['favicon']['id'], 'stickyHeader' => false, 'headerOverlay' => false, + // Instagram und Facebook stehen im Fuß; leer bleiben sie unsichtbar. + 'social' => ['instagram' => getenv('LUXIMMO_INSTAGRAM') ?: '', 'facebook' => getenv('LUXIMMO_FACEBOOK') ?: ''], 'motion' => 'always', 'homePath' => 'home', 'imprintPath' => 'impressum', 'privacyPath' => 'datenschutz', // Die Seite lädt nichts von Dritten; deshalb keine Einwilligungsleiste.