Files
KantSites/scripts/seed-huefner.php
T
kantarciandClaude Opus 5.5 5bfe1f3c6e Build Hüfner Gartenbau as a ninth installation, let buttons call and write, colour the button arrow like its text
- Website of Garten- und Landschaftsbau Hüfner GmbH rebuilt from the delivered design with
  imprint, privacy, 404 and the enquiry form (scripts/reset-huefner.sh, ports 8102/8103),
  Barlow and Barlow Condensed served locally with their licence.
- Buttons, icon cards, calls to action, icons and linked images accept tel: and mailto:
  links, the same targets menus and rich text already allowed (ADR 107).
- The arrow inside a button takes the button's text colour instead of the accent colour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:47:06 +02:00

1252 lines
96 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Baut den Entwurf „Garten- und Landschaftsbau Hüfner GmbH“ als Inhalt dieses CMS auf.
*
* Vorlage ist Umuts Ordner „huefner-website“ (06.10.2026): eine statische Seite (index.html,
* 16 Fotos und das Logo) mit Leiste, klebendem Kopf, Hero „Gärten auf Premium-Niveau.“,
* Einleitung mit Bildcollage, Leistungen als Kachelraster, Referenzen auf Dunkelgrün, Ablauf in
* vier Schritten, Fuhrpark, Über uns, Karriere, Instagram-Raster, Abschlussband, Kontakt mit
* Formular und Fuß. Wortlaut, Farben (Grün #2F6B22, Rot #B71C22, Nacht #14201A, Hellgrün
* #F3F8EF) und Schriften (Barlow, Barlow Condensed) sind daraus übernommen. Gebaut ist alles aus
* gewöhnlichen Bausteinen, Gestaltungswerten und eigenem CSS je Baustein – kein Kerncode, kein
* Plugin.
*
* Bewusste Abweichungen:
* - Die Platzhalter der Vorlage („[Öffnungszeiten]“, „[Ort] · [Jahr]“, „[XX] Jahre“,
* „[Kurzer Absatz zur Firmengeschichte]“) erscheinen nicht auf der Seite. An ihrer Stelle stehen
* Aussagen, die die Vorlage selbst schon macht (Einsatzgebiet, 15-t-Bagger, ein
* Team). Impressumsangaben aus dem Handelsregister (HRB 742159, Amtsgericht Mannheim,
* Geschäftsführer Florian Hüfner), Öffnungszeiten und Hoster (IONOS) von Umut.
* - Das Formular ist ein echtes CMS-Formular mit Versand an info@huefner-gartenbau.de, nicht
* der Knopf ohne Funktion der Vorlage. „Leistung“ ist eine Auswahl wie dort.
* - Die Stellen unter „Karriere“ sind Links (E-Mail mit Betreff), weil sie einen Pfeil tragen.
* - Anrufknöpfe sind echte Knöpfe mit tel: (Kern seit 06.10.2026), der Pfeil im Knopf hat die
* Schriftfarbe.
* - Das LKW-Symbol der Leiste gibt es im Symbolsatz des Kerns nicht; es steht als „Eigenes
* HTML“ mit dem Pfad der Vorlage da.
* - Leiste und Kopf kleben gemeinsam oben (die Einstellung gilt für den ganzen Kopfbereich); in
* der Vorlage klebt nur der Kopf. Auf dem Telefon ist die Leiste deshalb einzeilig.
* - Impressum, Datenschutz und 404 sind neu. Die Vorlage verweist nur auf #impressum.
*
* Das Skript schreibt ausschließlich in die Datenbank aus DATABASE_URL und in MEDIA_DIRECTORY,
* beide müssen auf eine frische, migrierte Installation zeigen (siehe reset-huefner.sh).
* Erstes Argument: der Ordner aus build-huefner-assets.php.
*/
require dirname(__DIR__).'/config/bootstrap.php';
use App\Content\DocumentValidator;
use App\Content\FormRepository;
use App\Content\MediaLibrary;
use App\Content\MenuRepository;
use App\Content\PageRepository;
use App\Content\PartialRepository;
use App\Content\SiteSettings;
use App\Content\Theme;
use App\Infrastructure\ConnectionFactory;
use Symfony\Component\HttpFoundation\File\UploadedFile;
$root = dirname(__DIR__);
$assets = $argv[1] ?? null;
if (null === $assets || !is_dir($assets)) {
fwrite(STDERR, "Bitte den Ordner mit den Bildern als Argument angeben.\n");
exit(1);
}
$url = str_replace('%kernel.project_dir%', str_replace('\\', '/', $root), (string) $_SERVER['DATABASE_URL']);
$mediaDirectory = str_replace('%kernel.project_dir%', $root, (string) $_SERVER['MEDIA_DIRECTORY']);
if (!str_contains($url, 'huefner')) {
fwrite(STDERR, "Sicherung: DATABASE_URL muss auf die Hüfner-Datenbank zeigen.\n");
exit(1);
}
$db = ConnectionFactory::create($url);
if (false !== $db->fetchOne('SELECT id FROM page LIMIT 1')) {
fwrite(STDERR, "Diese Datenbank enthält bereits Seiten. Abbruch, damit nichts überschrieben wird.\n");
exit(1);
}
$media = new MediaLibrary($db, $mediaDirectory);
$settings = new SiteSettings($db, $media);
$pages = new PageRepository($db, new DocumentValidator(), $settings);
$partials = new PartialRepository($db, new DocumentValidator());
$menus = new MenuRepository($db, $settings);
$forms = new FormRepository($db);
$theme = new Theme($db);
$firma = 'Garten- und Landschaftsbau Hüfner GmbH';
$kurz = 'Hüfner Garten- und Landschaftsbau';
$mail = 'info@huefner-gartenbau.de';
$telefon = '07229 6992826';
$tel = 'tel:+4972296992826';
$strasse = 'Am Forlenspitzen 6';
$plz = '76473';
$ort = 'Iffezheim';
$instagram = 'https://www.instagram.com/galabau_florianhuefner/';
$facebook = 'https://www.facebook.com/huefnergalabau/';
$SCHEMA = 24;
// Öffnungszeiten von Umut (06.10.2026, aus dem Google-Eintrag): Mo–Fr 8–17 Uhr, Sa 8–12 Uhr.
$zeiten = 'Mo–Fr 8–17 Uhr · Sa 8–12 Uhr';
$zeitenDaten = [
...array_map(static fn (string $tag): array => ['day' => $tag, 'opens' => '08:00', 'closes' => '17:00'], ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']),
['day' => 'Saturday', 'opens' => '08:00', 'closes' => '12:00'],
];
// ---------------------------------------------------------------------------------------------
// Bausteine
// ---------------------------------------------------------------------------------------------
$seiten = static fn (string $w): array => ['top' => $w, 'right' => $w, 'bottom' => $w, 'left' => $w];
$nurOben = static fn (string $w): array => ['top' => $w, 'right' => '0px', 'bottom' => '0px', 'left' => '0px'];
$nurLinks = static fn (string $w): array => ['top' => '0px', 'right' => '0px', 'bottom' => '0px', 'left' => $w];
$nurUnten = static fn (string $w): array => ['top' => '0px', 'right' => '0px', 'bottom' => $w, 'left' => '0px'];
$rund = static fn (string $w): array => ['topLeft' => $w, 'topRight' => $w, 'bottomRight' => $w, 'bottomLeft' => $w];
$section = static function (string $id, array $children, array $style = [], ?string $anchor = null, string $css = ''): array {
$block = ['id' => $id, 'type' => 'section', 'children' => $children, 'style' => $style];
if (null !== $anchor) {
$block['anchor'] = $anchor;
}
if ('' !== $css) {
$block['css'] = $css;
}
return $block;
};
$row = static function (string $id, array $children, array $design = [], array $style = [], string $css = ''): array {
$block = ['id' => $id, 'type' => 'row', 'children' => $children];
if ([] !== $design) {
$block['layout'] = ['design' => $design];
}
if ([] !== $style) {
$block['style'] = $style;
}
if ('' !== $css) {
$block['css'] = $css;
}
return $block;
};
/** Zwei Spalten auf dem Desktop, eine darunter. */
$zweiSpalten = ['desktop' => ['columns' => 'two'], 'tablet' => ['columns' => 'one'], 'mobile' => ['columns' => 'one']];
/** Eine Zeile, die nebeneinander stellt statt zu rastern. */
$zeile = static function (string $id, array $children, string $gap, array $extra = [], array $mobil = [], array $tablet = [], string $css = '') use ($row): array {
$style = ['desktop' => ['display' => 'flex', 'alignItems' => 'center', 'gap' => $gap, 'margin' => ['top' => '0px'], ...$extra]];
if ([] !== $tablet) {
$style['tablet'] = $tablet;
}
if ([] !== $mobil) {
$style['mobile'] = $mobil;
}
return $row($id, $children, [], $style, $css);
};
/** Wie $zeile, aber als Spalte: nur eine Spalte trägt eine Fläche (Zeilen sind im Kern durchsichtig). */
$karte = static function (string $id, array $children, string $gap, array $extra = [], array $mobil = [], array $tablet = [], string $css = '') use (&$zeile): array {
$block = $zeile($id, $children, $gap, $extra, $mobil, $tablet, $css);
$block['type'] = 'column';
return $block;
};
$column = static function (string $id, array $children, array $style = [], string $css = '', string $sichtbar = 'all'): array {
$block = ['id' => $id, 'type' => 'column', 'children' => $children];
if ([] !== $style) {
$block['style'] = $style;
}
if ('' !== $css) {
$block['css'] = $css;
}
if ('all' !== $sichtbar) {
$block['layout'] = ['visibility' => $sichtbar];
}
return $block;
};
$heading = static function (string $id, string $text, int $level, array $style = [], string $css = ''): array {
$block = ['id' => $id, 'type' => 'heading', 'props' => ['text' => $text, 'level' => $level]];
if ([] !== $style) {
$block['style'] = $style;
}
if ('' !== $css) {
$block['css'] = $css;
}
return $block;
};
$text = static function (string $id, string $inhalt, array $style = [], string $css = '', string $sichtbar = 'all'): array {
$block = ['id' => $id, 'type' => 'text', 'props' => ['text' => $inhalt]];
if ([] !== $style) {
$block['style'] = $style;
}
if ('' !== $css) {
$block['css'] = $css;
}
if ('all' !== $sichtbar) {
$block['layout'] = ['visibility' => $sichtbar];
}
return $block;
};
/** Eine reine Fläche: Striche, Linien. */
$flaeche = static function (string $id, array $style, string $css = '', string $sichtbar = 'all'): array {
$block = ['id' => $id, 'type' => 'spacer', 'props' => ['size' => 'small'], 'style' => $style];
if ('' !== $css) {
$block['css'] = $css;
}
if ('all' !== $sichtbar) {
$block['layout'] = ['visibility' => $sichtbar];
}
return $block;
};
$bild = static function (string $id, array $eintrag, string $alt, array $style = [], ?string $href = null, string $css = ''): array {
$gross = end($eintrag['variants']);
$props = [
'src' => $gross['src'], 'alt' => $alt, 'decorative' => '' === $alt, 'width' => $gross['width'], 'height' => $gross['height'],
'mediaId' => $eintrag['id'],
'sources' => array_map(static fn (array $v): array => ['src' => $v['src'], 'width' => $v['width']], $eintrag['variants']),
];
if (null !== $href) {
$props['href'] = $href;
$props['click'] = 'link';
}
$block = ['id' => $id, 'type' => 'image', 'props' => $props, 'style' => $style];
if ('' !== $css) {
$block['css'] = $css;
}
return $block;
};
$symbol = static function (string $id, string $name, int $groesse, array $style = []): array {
return ['id' => $id, 'type' => 'icon', 'props' => ['icon' => $name, 'size' => $groesse, 'decorative' => true, 'label' => ''], 'style' => $style];
};
/**
* Ein Button. Der Pfeil des Kerns zeigt schräg nach oben; um 45 Grad gedreht zeigt er nach
* rechts wie in der Vorlage. Alle heben sich beim Überfahren leicht an.
*
* @param string $art rot (Theme), glas (durchsichtig auf Bild), weiss, dunkel
*/
$knopf = static function (string $id, string $inhalt, string $href, string $art = 'rot', bool $pfeil = true, array $style = [], string $sichtbar = 'all'): array {
$farbe = [
'rot' => '',
'glas' => "background:rgba(255,255,255,.12);color:#fff;box-shadow:inset 0 0 0 1px rgba(255,255,255,.5);\n&:hover{background:rgba(255,255,255,.22);color:#fff}\n",
'weiss' => "background:#fff;color:#17221a;\n&:hover{background:#eef5e9;color:#17221a}\n",
'dunkel' => "background:#17221a;color:#fff;\n&:hover{background:#2f6b22;color:#fff}\n",
'linie' => "background:transparent;color:#fff;box-shadow:inset 0 0 0 1px rgba(255,255,255,.35);\n&:hover{background:rgba(255,255,255,.1);color:#fff}\n",
][$art];
$css = "gap:12px;min-height:58px;padding:0 30px;margin-top:0;transition:transform .2s ease,box-shadow .2s ease,background-color .2s ease;\n"
.$farbe
."&:hover{transform:translateY(-2px);box-shadow:0 10px 24px rgba(0,0,0,.18)}\n"
.($pfeil ? "& .button-arrow svg{transform:rotate(45deg)}\n& .button-arrow{display:inline-flex}" : '& .button-arrow{display:none}');
$block = ['id' => $id, 'type' => 'button', 'props' => ['text' => $inhalt, 'href' => $href], 'css' => $css];
if ([] !== $style) {
$block['style'] = $style;
}
if ('all' !== $sichtbar) {
$block['layout'] = ['visibility' => $sichtbar];
}
return $block;
};
/**
* Setzt die Einblendungen nach einer Liste.
*
* @param array<string, array{0: string, 1?: string}> $plan Block-ID => [Art, Verzögerung]
*/
$einblenden = static function (array $dokument, array $plan): array {
$gehen = function (array $bloecke) use (&$gehen, $plan): array {
foreach ($bloecke as $i => $block) {
if (isset($plan[$block['id'] ?? ''])) {
[$art, $verzug] = [...$plan[$block['id']], 'none'];
$bloecke[$i]['layout'] = [...($block['layout'] ?? []), 'effects' => ['entrance' => $art, 'delay' => $verzug]];
}
if (isset($block['children'])) {
$bloecke[$i]['children'] = $gehen($block['children']);
}
}
return $bloecke;
};
$dokument['blocks'] = $gehen($dokument['blocks']);
return $dokument;
};
// Wiederkehrende Gestaltungswerte der Vorlage.
$h2Stil = [
'desktop' => ['fontSize' => '64px', 'fontWeight' => '800', 'lineHeight' => 0.98, 'letterSpacing' => '0px', 'textTransform' => 'uppercase', 'color' => '@tinte', 'margin' => ['top' => '18px', 'bottom' => '28px']],
'tablet' => ['fontSize' => '46px'],
'mobile' => ['fontSize' => '38px', 'margin' => ['top' => '14px', 'bottom' => '22px']],
];
$h3Stil = static fn (string $groesse, string $farbe = '@tinte', string $unten = '10px'): array => [
'desktop' => ['fontSize' => $groesse, 'fontWeight' => '800', 'lineHeight' => 1.0, 'letterSpacing' => '0px', 'textTransform' => 'uppercase', 'color' => $farbe, 'margin' => ['top' => '0px', 'bottom' => $unten]],
];
$fliess = static fn (string $groesse = '17px', string $farbe = '@text', string $unten = '18px', float $zeile = 1.7): array => [
'desktop' => ['fontSize' => $groesse, 'lineHeight' => $zeile, 'color' => $farbe, 'margin' => ['top' => '0px', 'bottom' => $unten]],
];
$em = '& em{font-style:normal;color:#8fd16f}';
$linkCss = "& a{color:#2f6b22;text-decoration:underline;text-underline-offset:3px}\n& a:hover{color:#1f4a16}";
$emGruen = '& em{font-style:normal;color:#2f6b22}';
/** Die Kleinzeile in Versalien mit dem kurzen Strich davor („— LEISTUNGEN“), auf Wunsch auch dahinter. */
$kleinzeile = static function (string $id, string $inhalt, string $farbe = '@rot', bool $beidseitig = false, string $unten = '0px') use ($text, $zeile, $flaeche): array {
$strich = static fn (string $sid): array => $flaeche($sid, ['desktop' => ['width' => '32px', 'height' => '2px', 'backgroundColor' => $farbe, 'flexShrink' => 0, 'margin' => ['top' => '0px', 'right' => '0px', 'bottom' => '0px', 'left' => '0px']]], 'min-height:0');
$kinder = [$strich($id.'-strich'), $text($id.'-text', $inhalt, ['desktop' => [
'fontFamily' => 'heading', 'fontSize' => '17px', 'fontWeight' => '700', 'letterSpacing' => '0.2em', 'lineHeight' => 1.2,
'textTransform' => 'uppercase', 'color' => $farbe, 'margin' => ['top' => '0px', 'bottom' => '0px'],
]])];
if ($beidseitig) {
$kinder[] = $strich($id.'-strich-2');
}
return $zeile($id, $kinder, '12px', ['justifyContent' => $beidseitig ? 'center' : 'flex-start', 'margin' => ['top' => '0px', 'bottom' => $unten]]);
};
/** Ein Symbol im grünen Kreis (Kontaktkarten, Direkter Draht). */
$kreis = static fn (string $id, string $icon, string $groesse = '48px') => ['id' => $id, 'type' => 'column', 'children' => [
['id' => $id.'-icon', 'type' => 'icon', 'props' => ['icon' => $icon, 'size' => 20, 'decorative' => true, 'label' => ''], 'style' => ['desktop' => ['color' => '@papier']]],
], 'style' => ['desktop' => ['display' => 'flex', 'alignItems' => 'center', 'justifyContent' => 'center', 'width' => $groesse, 'height' => $groesse, 'flexShrink' => 0, 'backgroundColor' => '@gruen', 'borderRadius' => ['topLeft' => '50%', 'topRight' => '50%', 'bottomRight' => '50%', 'bottomLeft' => '50%']]]];
// Ein Link über die ganze Karte: Die Karte ist positioniert, der Text darin nicht, also spannt
// das ::after des Links die ganze Karte auf. Ein Link, ein Name, ganze Fläche klickbar.
$ganzeKarte = "position:relative;\n& a{color:inherit;text-decoration:none}\n& a::after{content:'';position:absolute;top:0;right:0;bottom:0;left:0;border-radius:inherit}\n& a:focus-visible{outline:none}\n&:focus-within{outline:3px solid #2f6b22;outline-offset:3px}";
// ---------------------------------------------------------------------------------------------
// Website, Gestaltung, Medien
// ---------------------------------------------------------------------------------------------
$db->insert('website', ['id' => 'default', 'name' => $kurz, 'default_locale' => 'de', 'theme' => '{}']);
$theme->save(
[
['key' => 'gruen', 'name' => 'Grün (Akzent)', 'value' => '#2f6b22'],
['key' => 'gruen-dunkel', 'name' => 'Grün dunkel (Links beim Überfahren)', 'value' => '#1f4a16'],
['key' => 'hellgruen', 'name' => 'Hellgrün (auf Dunkel)', 'value' => '#8fd16f'],
['key' => 'rot', 'name' => 'Rot (Knöpfe, Kleinzeilen)', 'value' => '#b71c22'],
['key' => 'rot-dunkel', 'name' => 'Rot dunkel (Knöpfe beim Überfahren)', 'value' => '#931419'],
['key' => 'tinte', 'name' => 'Schrift', 'value' => '#17221a'],
['key' => 'nacht', 'name' => 'Nachtgrün (Leiste, Referenzen)', 'value' => '#14201a'],
['key' => 'fuss', 'name' => 'Fuß', 'value' => '#0f1813'],
['key' => 'mint', 'name' => 'Hellgrün (Seitengrund)', 'value' => '#f3f8ef'],
['key' => 'chip', 'name' => 'Hellgrün (Flächen)', 'value' => '#eef5e9'],
['key' => 'linie', 'name' => 'Linien', 'value' => '#dce8d4'],
['key' => 'text', 'name' => 'Fließtext', 'value' => '#55635a'],
['key' => 'text-stark', 'name' => 'Fließtext kräftig', 'value' => '#3a483d'],
['key' => 'auf-dunkel', 'name' => 'Schrift auf Dunkel', 'value' => '#b9c7b5'],
['key' => 'papier', 'name' => 'Weiß', 'value' => '#ffffff'],
],
[
['key' => 'body', 'name' => 'Fließtext', 'family' => 'Barlow', 'source' => 'google', 'weights' => [400, 600, 700], 'fallback' => 'sans-serif'],
['key' => 'heading', 'name' => 'Überschriften', 'family' => 'Barlow Condensed', 'source' => 'google', 'weights' => [700, 800], 'fallback' => 'sans-serif'],
],
['fontFamily' => 'body', 'headingFontFamily' => 'heading', 'fontSize' => '16px', 'lineHeight' => 1.6, 'color' => '@tinte', 'backgroundColor' => '@mint', 'linkColor' => '@gruen', 'containerWidth' => '1280px', 'headingScale' => 1.25, 'linkUnderline' => false, 'imageRadius' => '0px'],
['shape' => 'pill', 'appearance' => 'solid', 'size' => 'normal', 'fontFamily' => 'heading', 'fontSize' => 19, 'fontWeight' => 700, 'letterSpacing' => 1.5, 'textTransform' => 'uppercase', 'backgroundColor' => '@rot', 'color' => '@papier', 'hoverBackgroundColor' => '@rot-dunkel', 'hoverColor' => '@papier'],
);
echo "Farben, Schriften und Buttons gesetzt\n";
$medien = [];
foreach ([
'logo' => ['logo.png', 'Garten- und Landschaftsbau Hüfner GmbH'],
'favicon' => ['favicon.png', 'Baum aus dem Hüfner-Logo'],
'hero' => ['hero-pool-stufen.jpg', 'Pool mit Sitzstufen in Anthrazit und Keramikumrandung'],
'pool-stufen' => ['pool-stufen.jpg', 'Pool mit Sitzstufen und Keramikumrandung'],
'plattenweg' => ['plattenweg.jpg', 'Plattenweg im Rasen entlang eines Neubaus'],
'pool-fachwerk' => ['pool-fachwerk.jpg', 'Pool mit Natursteinterrasse am Fachwerkhaus'],
'terrasse' => ['terrasse.jpg', 'Keramikterrasse am Pool'],
'pflaster-verlegt' => ['pflaster-verlegt.jpg', 'Neu verlegtes Pflaster in Grau- und Brauntönen'],
'naturstein' => ['naturstein.jpg', 'Natursteinmauer mit Trittplatten im Rasen'],
'rasen' => ['rasen.jpg', 'Frisch verlegter Rollrasen'],
'pool-garten' => ['pool-garten.jpg', 'Pool mit Hortensien und Gartenanlage'],
'pool-terrasse' => ['pool-terrasse.jpg', 'Pool mit Natursteinterrasse und Gabionen'],
'sichtschutz' => ['sichtschutz.jpg', 'Moderner Sichtschutz aus Aluminium'],
'pflaster' => ['pflaster.jpg', 'Hanggarten mit Natursteinmauer und Treppe'],
'baustelle' => ['poolbau-baustelle.jpg', 'Poolbau im Rohbau mit Minibagger und Radlader im Hintergrund'],
'team' => ['team.jpg', 'Das Hüfner-Team in Arbeitsjacken mit Firmenlogo'],
] as $schluessel => [$datei, $alt]) {
$pfad = rtrim($assets, '/\\').'/'.$datei;
if (!is_file($pfad)) {
fwrite(STDERR, "Bild fehlt: $pfad\n");
exit(1);
}
$eintrag = $media->upload(new UploadedFile($pfad, basename($datei), null, null, true));
$media->update($eintrag['id'], ['title' => pathinfo($datei, PATHINFO_FILENAME), 'alt' => $alt, 'archived' => false, 'version' => $eintrag['version']]);
$medien[$schluessel] = $media->find($eintrag['id']);
echo "Bild übernommen: $datei\n";
}
// ---------------------------------------------------------------------------------------------
// Formular
// ---------------------------------------------------------------------------------------------
$forms->save('anfrage', 'Projektanfrage', [
['name' => 'name', 'type' => 'text', 'label' => 'Name', 'required' => true, 'help' => 'Vor- und Nachname'],
['name' => 'telefon', 'type' => 'phone', 'label' => 'Telefon', 'required' => false, 'help' => 'Für Rückfragen'],
['name' => 'email', 'type' => 'email', 'label' => 'E-Mail', 'required' => true],
['name' => 'leistung', 'type' => 'select', 'label' => 'Leistung', 'required' => true, 'options' => ['Pool', 'Terrasse', 'Pflaster', 'Natursteine', 'Komplette Außenanlage', 'Sonstiges']],
['name' => 'vorhaben', 'type' => 'textarea', 'label' => 'Ihr Vorhaben', 'required' => true, 'help' => 'Kurze Beschreibung, Größe der Fläche, Wunschtermin …'],
['name' => 'datenschutz', 'type' => 'consent', 'label' => 'Ich stimme der Verarbeitung meiner Angaben gemäß Datenschutzhinweisen zu.'],
], ['recipient' => $mail, 'subject' => 'Neue Projektanfrage über die Website', 'success' => 'Vielen Dank! Ihre Anfrage ist bei uns angekommen. Wir melden uns zeitnah und vereinbaren einen Termin bei Ihnen vor Ort.', 'replyToField' => 'email', 'buttonLabel' => 'Anfrage absenden'], 0);
echo "Formular angelegt: anfrage\n";
// ---------------------------------------------------------------------------------------------
// Kopfbereich: dunkle Leiste, darunter der weiße Kopf mit Menü, Profilen und Knopf
// ---------------------------------------------------------------------------------------------
$leistenLink = "& a{color:#fff;text-decoration:none;transition:color .2s}\n& a:hover{color:#8fd16f}";
$leistenPunkt = static fn (string $id, string $icon, string $inhalt, string $sichtbar = 'all') => $zeile($id, [
$symbol($id.'-icon', $icon, 16, ['desktop' => ['color' => '#8fcb7c']]),
$text($id.'-text', $inhalt, ['desktop' => ['fontSize' => '14px', 'lineHeight' => 1.4, 'color' => '#c9d6c5', 'margin' => ['bottom' => '0px']]], $leistenLink),
], '8px') + ('all' === $sichtbar ? [] : ['layout' => ['visibility' => $sichtbar]]);
$kopf = [
'schemaVersion' => $SCHEMA, 'title' => 'Kopfbereich', 'description' => 'Erscheint auf jeder Seite über dem Inhalt.',
'blocks' => [
$section('kopf-leiste', [
$zeile('kopf-leiste-zeile', [
$zeile('kopf-leiste-kontakt', [
$leistenPunkt('kopf-telefon', 'phone', '<a href="'.$tel.'">'.$telefon.'</a>'),
$leistenPunkt('kopf-mail', 'mail', '<a href="mailto:'.$mail.'">'.$mail.'</a>', 'hide-mobile'),
$leistenPunkt('kopf-adresse', 'pin', $strasse.', '.$plz.' '.$ort, 'desktop'),
], '28px', ['flexWrap' => 'wrap', 'rowGap' => '8px']),
['id' => 'kopf-leiste-profile', 'type' => 'social', 'props' => ['size' => 16, 'shape' => 'plain', 'layout' => 'horizontal', 'labels' => true, 'networks' => ['instagram', 'facebook']],
'style' => ['desktop' => ['fontSize' => '15px', 'color' => '#c9d6c5', 'gap' => '24px']],
'css' => "& a{font-weight:400}\n& a:hover{color:#fff;transform:none}",
'layout' => ['visibility' => 'hide-mobile']],
], '28px', ['justifyContent' => 'space-between']),
], [
'desktop' => ['backgroundColor' => '@nacht', 'color' => '#c9d6c5', 'padding' => ['top' => '9px', 'bottom' => '9px']],
'mobile' => ['padding' => ['top' => '7px', 'bottom' => '7px']],
]),
$section('kopf-haupt', [
$zeile('kopf-haupt-zeile', [
$bild('kopf-logo', $medien['logo'], $kurz.' – zur Startseite', [
// Feste Breite: sonst bekommt das Logo seine Breite erst nach dem Laden (CLS).
'desktop' => ['width' => '153px', 'maxWidth' => '153px', 'flexShrink' => 0, 'margin' => $seiten('0px')],
'mobile' => ['width' => '121px', 'maxWidth' => '121px'],
], '/', '& img{display:block;width:100%;height:auto;border-radius:0}'),
$column('kopf-navigation', [
['id' => 'kopf-menue', 'type' => 'menu', 'props' => ['slot' => 'header', 'layout' => 'horizontal', 'collapse' => 'tablet', 'burgerLabel' => 'icon'],
'style' => ['desktop' => [
'fontFamily' => 'heading', 'fontSize' => '18px', 'fontWeight' => '700', 'letterSpacing' => '0.06em', 'textTransform' => 'uppercase', 'color' => '@tinte',
'burgerColor' => '@papier', 'burgerBackgroundColor' => '@tinte', 'burgerBorderColor' => '@tinte', 'burgerBorderRadius' => '999px',
]],
// Unterstrich, der beim Überfahren von links einläuft. Das Klappmenü auf
// Tablet und Telefon liegt als Fläche unter dem ganzen Kopf.
'css' => "& .menu-list{gap:4px 34px;justify-content:center;align-items:center}\n"
."& .menu-list a{position:relative;padding:6px 0;text-decoration:none;color:#17221a}\n"
."& .menu-list a::after{content:'';position:absolute;left:0;right:100%;bottom:0;height:2px;background:#2f6b22;transition:right .25s ease}\n"
."& .menu-list a:hover::after{right:0}\n"
."& .menu-burger summary{width:46px;height:46px;display:flex;align-items:center;justify-content:center;padding:0}\n"
."& .menu-burger-body{position:absolute;left:0;right:0;top:100%;background:#fff;padding:4px 20px 20px;border-top:1px solid #dce8d4;box-shadow:0 18px 30px rgba(20,32,26,.12);font-size:22px}\n"
."& .menu-burger-body .menu-list{flex-direction:column;align-items:stretch;gap:0}\n"
."& .menu-burger-body .menu-list a{display:block;padding:14px 0;border-bottom:1px solid #e6efe0}\n"
.'& .menu-burger-body .menu-list a::after{display:none}'],
], ['desktop' => ['flexGrow' => 1, 'display' => 'flex', 'justifyContent' => 'center'], 'tablet' => ['justifyContent' => 'flex-end', 'order' => 3, 'flexGrow' => 0], 'mobile' => ['justifyContent' => 'flex-end', 'order' => 3, 'flexGrow' => 0]]),
$zeile('kopf-aktionen', [
['id' => 'kopf-profile', 'type' => 'social', 'props' => ['size' => 20, 'shape' => 'plain', 'layout' => 'horizontal', 'labels' => false, 'networks' => ['instagram', 'facebook']],
'style' => ['desktop' => ['color' => '@tinte', 'gap' => '10px']],
'css' => "& a{width:46px;height:46px;justify-content:center;border-radius:50%;box-shadow:inset 0 0 0 1px #d3e3ca}\n& a:hover{background:#eef5e9;color:#2f6b22}",
'layout' => ['visibility' => 'hide-mobile']],
$knopf('kopf-knopf', 'Angebot anfragen', '/#kontakt', 'rot', true, [], 'desktop'),
], '10px', ['flexShrink' => 0], ['margin' => ['left' => 'auto']], ['margin' => ['left' => 'auto']]),
], '24px', ['justifyContent' => 'space-between', 'minHeight' => '82px'], ['minHeight' => '66px', 'gap' => '12px']),
], [
'desktop' => ['backgroundColor' => '#fffffff5', 'padding' => ['top' => '0px', 'bottom' => '0px'], 'borderWidth' => $nurUnten('1px'), 'borderStyle' => 'solid', 'borderColor' => '@linie'],
], null, 'box-shadow:0 8px 24px rgba(20,32,26,.06)'),
],
];
// Der rote Knopf im Kopf ist kleiner als die im Inhalt.
$kleinerKnopf = static fn (array $b): array => [...$b, 'css' => str_replace('min-height:58px;padding:0 30px', 'min-height:50px;padding:0 22px;font-size:16px;white-space:nowrap', $b['css'])];
$kopf['blocks'][1]['children'][0]['children'][2]['children'][1]['css'] = str_replace('min-height:58px;padding:0 30px', 'min-height:46px;padding:0 24px;font-size:17px', $kopf['blocks'][1]['children'][0]['children'][2]['children'][1]['css']);
// ---------------------------------------------------------------------------------------------
// Fußbereich
// ---------------------------------------------------------------------------------------------
$fussLink = "& a{color:#b9c7b5;text-decoration:none;transition:color .2s}\n& a:hover{color:#fff}";
$fussTitel = static fn (string $id, string $inhalt) => $text($id, $inhalt, ['desktop' => [
'fontFamily' => 'heading', 'fontSize' => '17px', 'fontWeight' => '700', 'letterSpacing' => '0.14em', 'textTransform' => 'uppercase', 'color' => '@papier', 'margin' => ['bottom' => '16px'],
]]);
$fussListe = static fn (string $id, array $links) => $text($id, implode('<br>', array_map(static fn (array $l): string => '<a href="'.$l[1].'">'.$l[0].'</a>', $links)), [
'desktop' => ['fontSize' => '15px', 'lineHeight' => 2.1, 'color' => '@auf-dunkel', 'margin' => ['bottom' => '0px']],
], $fussLink);
$fuss = [
'schemaVersion' => $SCHEMA, 'title' => 'Fußbereich', 'description' => 'Erscheint auf jeder Seite unter dem Inhalt.',
'blocks' => [
$section('fuss', [
$column('fuss-raster', [
$column('fuss-marke', [
$column('fuss-logo-flaeche', [
$bild('fuss-logo', $medien['logo'], $firma, ['desktop' => ['width' => '137px', 'maxWidth' => '137px', 'margin' => $seiten('0px')]], null, '& img{display:block;width:100%;height:auto;border-radius:0}'),
], ['desktop' => ['display' => 'inline-block', 'backgroundColor' => '@papier', 'borderRadius' => $rund('12px'), 'padding' => ['top' => '12px', 'right' => '16px', 'bottom' => '12px', 'left' => '16px']]]),
$text('fuss-spruch', 'Garten- und Landschaftsbau auf Premium-Niveau – aus Iffezheim für die Region.', ['desktop' => ['maxWidth' => '300px', 'fontSize' => '15px', 'lineHeight' => 1.65, 'color' => '@auf-dunkel', 'margin' => ['top' => '20px', 'bottom' => '20px']]]),
['id' => 'fuss-profile', 'type' => 'social', 'props' => ['size' => 20, 'shape' => 'plain', 'layout' => 'horizontal', 'labels' => false, 'networks' => ['instagram', 'facebook']],
'style' => ['desktop' => ['color' => '@auf-dunkel', 'gap' => '10px']],
'css' => "& a{width:44px;height:44px;justify-content:center;border-radius:50%;box-shadow:inset 0 0 0 1px #2c3a2f}\n& a:hover{color:#fff;box-shadow:inset 0 0 0 1px #8fd16f}"],
], ['desktop' => ['display' => 'flex', 'flexDirection' => 'column', 'alignItems' => 'flex-start']]),
$column('fuss-leistungen', [
$fussTitel('fuss-leistungen-titel', 'Leistungen'),
$fussListe('fuss-leistungen-liste', [['Pools', '/#leistungen'], ['Terrassen', '/#leistungen'], ['Pflaster', '/#leistungen'], ['Natursteine', '/#leistungen'], ['Außenanlagen', '/#leistungen']]),
]),
$column('fuss-unternehmen', [
$fussTitel('fuss-unternehmen-titel', 'Unternehmen'),
$fussListe('fuss-unternehmen-liste', [['Über uns', '/#ueber-uns'], ['Referenzen', '/#projekte'], ['Ablauf', '/#ablauf'], ['Karriere', '/#karriere'], ['Kontakt', '/#kontakt']]),
]),
$column('fuss-kontakt', [
$fussTitel('fuss-kontakt-titel', 'Kontakt'),
$text('fuss-kontakt-text', 'Garten- und Landschaftsbau<br>Hüfner GmbH<br>'.$strasse.'<br>'.$plz.' '.$ort.'<br><br><a href="'.$tel.'">'.$telefon.'</a><br><a href="mailto:'.$mail.'">'.$mail.'</a>', [
'desktop' => ['fontSize' => '15px', 'lineHeight' => 1.7, 'color' => '@auf-dunkel', 'margin' => ['bottom' => '0px']],
], $fussLink."\n& a{overflow-wrap:anywhere}"),
]),
], [
'desktop' => ['display' => 'grid', 'gridColumns' => 4, 'gap' => '48px', 'padding' => ['top' => '80px', 'bottom' => '48px']],
'tablet' => ['gridColumns' => 2],
'mobile' => ['gridColumns' => 1, 'gap' => '36px', 'padding' => ['top' => '56px', 'bottom' => '36px']],
]),
$zeile('fuss-unten', [
$text('fuss-copyright', '© 2026 '.$firma, ['desktop' => ['fontSize' => '14px', 'color' => '@auf-dunkel', 'margin' => ['bottom' => '0px']]]),
$zeile('fuss-links', [
['id' => 'fuss-rechtliches', 'type' => 'legal', 'props' => ['label' => 'Rechtliches'],
'style' => ['desktop' => ['fontSize' => '14px', 'gap' => '24px', 'color' => '@auf-dunkel']],
'css' => $fussLink],
$text('fuss-agentur', '<a href="https://kant-it.de">Website by Kant-IT Solutions</a>', ['desktop' => ['fontSize' => '14px', 'margin' => ['bottom' => '0px']]], $fussLink),
], '24px', ['flexWrap' => 'wrap'], ['rowGap' => '10px', 'columnGap' => '18px']),
], '24px', ['justifyContent' => 'space-between', 'flexWrap' => 'wrap', 'rowGap' => '12px', 'borderWidth' => $nurOben('1px'), 'borderStyle' => 'solid', 'borderColor' => '#22302a', 'padding' => ['top' => '24px', 'bottom' => '36px']],
['flexDirection' => 'column', 'alignItems' => 'flex-start']),
], [
'desktop' => ['backgroundColor' => '@fuss', 'color' => '@auf-dunkel', 'padding' => ['top' => '0px', 'bottom' => '0px']],
]),
],
];
$partials->create('header', 'default', 'Kopfbereich', $kopf);
$partials->create('footer', 'default', 'Fußbereich', $fuss);
echo "Kopf- und Fußbereich angelegt\n";
// ---------------------------------------------------------------------------------------------
// Startseite
// ---------------------------------------------------------------------------------------------
/**
* Eine Bildkachel: Foto über die ganze Fläche, dunkler Verlauf von unten, Text darauf. Beim
* Überfahren zoomt nur das Foto, die Kachel bekommt Schatten. Mit $href ist das Foto der Link
* (sein Alternativtext ist dann der Name des Links), der Text darüber lässt Klicks durch.
*/
$kachel = static function (string $id, array $foto, string $alt, array $inhalt, string $innen, ?string $href = null, array $hoehe = [], string $verlauf = '', bool $mehr = false) use ($column, $bild, $seiten, $rund): array {
return $column($id, [
// Foto, Verlauf und Fokusrahmen stehen einmal im CSS des Abschnitts ($kachelFlaeche),
// nicht in jeder Kachel: sonst wird die Seite über das HTML-Budget von 150 KB schwer.
$bild($id.'-foto', $foto, $alt, ['desktop' => ['position' => 'absolute', 'top' => '0px', 'right' => '0px', 'bottom' => '0px', 'left' => '0px', 'margin' => $seiten('0px'), 'zIndex' => 0]], $href),
...$inhalt,
], [
'desktop' => ['position' => 'relative', 'overflow' => 'hidden', 'display' => 'flex', 'flexDirection' => 'column', 'justifyContent' => 'flex-end', 'backgroundColor' => '#2a372c', 'color' => '@papier',
'borderRadius' => $rund('18px'), 'padding' => $seiten($innen), ...($hoehe['desktop'] ?? [])],
...array_diff_key($hoehe, ['desktop' => true]),
], "transition:box-shadow .3s ease;\n&:hover{box-shadow:0 18px 40px rgba(20,30,22,.22)}\n&:hover img{transform:scale(1.06)}\n&:focus-within img{transform:scale(1.06)}".($mehr ? "\n& .block-row{position:relative;pointer-events:none;transition:gap .25s ease}\n&:hover .block-row{gap:14px}" : ''));
};
/** Was alle Kacheln eines Abschnitts teilen; die Kacheln sind dort die einzigen Bilder. */
$kachelFlaeche = "& .block-image img{width:100%;height:100%;object-fit:cover;border-radius:0;transition:transform .7s cubic-bezier(.2,.7,.2,1)}\n"
."& .image-link{height:100%}\n"
."& .image-link:focus-visible{outline:3px solid #8fd16f;outline-offset:-8px}\n"
."& .block-image::after{content:'';position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(0deg,rgba(10,18,13,.88) 0%,rgba(10,18,13,0) 58%);pointer-events:none}\n"
."& .block-image+h3{position:relative;pointer-events:none}\n"
."& .block-image~.block-text{position:relative;pointer-events:none}\n"
.'& .block-image~h3{position:relative;pointer-events:none}';
$kachelTitel = static fn (string $id, string $titel, string $groesse = '30px') => $heading($id, $titel, 3, [
'desktop' => ['fontSize' => $groesse, 'fontWeight' => '800', 'lineHeight' => 1.0, 'letterSpacing' => '0px', 'textTransform' => 'uppercase', 'color' => '@papier', 'margin' => ['top' => '4px', 'bottom' => '6px']],
'mobile' => ['fontSize' => '30px'],
]);
$kachelSatz = static fn (string $id, string $satz, string $groesse = '15px', string $unten = '0px', string $breite = 'none') => $text($id, $satz, ['desktop' => ['fontSize' => $groesse, 'lineHeight' => 1.5, 'color' => '#dce6d8', 'maxWidth' => $breite, 'margin' => ['bottom' => $unten]]]);
$kachelKlein = static fn (string $id, string $klein, string $groesse = '13px') => $text($id, $klein, ['desktop' => ['fontSize' => $groesse, 'fontWeight' => '600', 'letterSpacing' => '0.06em', 'textTransform' => 'uppercase', 'color' => '@hellgruen', 'margin' => ['bottom' => '2px']]]);
/** Ein Vorteil im Band unter dem Hero; die Trennlinien hängen am Platz im Raster. */
$vorteil = static function (int $n, array $icon, string $titel, string $satz) use ($zeile, $text): array {
$rahmen = static fn (bool $oben, bool $links): array => ['borderWidth' => ['top' => $oben ? '1px' : '0px', 'right' => '0px', 'bottom' => '0px', 'left' => $links ? '1px' : '0px'], 'borderStyle' => 'solid', 'borderColor' => '#ffffff24'];
// Desktop: vier nebeneinander, Linie links ab dem zweiten. Darunter zwei mal zwei.
$zwei = $rahmen($n > 2, 0 === $n % 2);
$mobil = ['flexDirection' => 'column', 'alignItems' => 'flex-start', 'gap' => '8px', 'padding' => ['top' => '16px', 'right' => '16px', 'bottom' => '16px', 'left' => '16px'], ...$zwei];
return $zeile('vorteil-'.$n, [
$icon,
$text('vorteil-'.$n.'-text', '<strong>'.$titel.'</strong>'.$satz, [
'desktop' => ['fontSize' => '14px', 'lineHeight' => 1.45, 'color' => '@auf-dunkel', 'margin' => ['bottom' => '0px']],
'mobile' => ['fontSize' => '12.5px'],
], "& strong{display:block;margin-bottom:2px;font-size:16px;color:#fff}"),
], '14px', ['padding' => ['top' => '24px', 'right' => '32px', 'bottom' => '24px', 'left' => '32px'], ...$rahmen(false, $n > 1)], $mobil, $zwei);
};
$vorteilIcon = static fn (int $n, string $name) => $symbol('vorteil-'.$n.'-icon', $name, 32, ['desktop' => ['color' => '@hellgruen', 'flexShrink' => 0]]);
// Der LKW fehlt im Symbolsatz des Kerns; der Pfad ist der aus der Vorlage.
$lkw = ['id' => 'vorteil-3-icon', 'type' => 'html', 'props' => ['html' => '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#8fd16f" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="1" y="7" width="13" height="10" rx="1"></rect><path d="M14 10h4l3 3v4h-7z"></path><circle cx="6" cy="18.5" r="2"></circle><circle cx="17" cy="18.5" r="2"></circle></svg>'],
'style' => ['desktop' => ['flexShrink' => 0]], 'css' => '& svg{display:block}'];
$haken = static fn (string $id, string $inhalt) => $zeile($id, [
$symbol($id.'-icon', 'check', 24, ['desktop' => ['color' => '@gruen', 'flexShrink' => 0]]),
$text($id.'-text', $inhalt, ['desktop' => ['fontSize' => '16px', 'fontWeight' => '600', 'lineHeight' => 1.4, 'color' => '@tinte', 'margin' => ['bottom' => '0px']]]),
], '10px');
$chip = static fn (string $id, string $inhalt) => $text($id, $inhalt, [
'desktop' => ['fontSize' => '15px', 'fontWeight' => '600', 'lineHeight' => 1.3, 'color' => '@tinte', 'backgroundColor' => '@chip', 'borderRadius' => $rund('999px'), 'padding' => ['top' => '10px', 'right' => '18px', 'bottom' => '10px', 'left' => '18px'], 'margin' => ['bottom' => '0px']],
], "transition:background-color .2s ease,color .2s ease;\n&:hover{background-color:#2f6b22;color:#fff}");
$schritt = static fn (int $n, string $titel, string $satz, string $farbe = '@gruen') => $column('schritt-'.$n, [
$text('schritt-'.$n.'-nummer', sprintf('%02d', $n), [
'desktop' => ['display' => 'flex', 'alignItems' => 'center', 'justifyContent' => 'center', 'width' => '72px', 'height' => '72px', 'margin' => ['top' => '0px', 'right' => 'auto', 'bottom' => '24px', 'left' => 'auto'],
'borderRadius' => $rund('50%'), 'backgroundColor' => $farbe, 'color' => '@papier', 'fontFamily' => 'heading', 'fontSize' => '28px', 'fontWeight' => '800', 'lineHeight' => 1.0],
], 'position:relative;z-index:1;box-shadow:0 0 0 10px #f3f8ef;transition:transform .3s cubic-bezier(.2,.7,.2,1)'),
$heading('schritt-'.$n.'-titel', $titel, 3, $h3Stil('26px')),
$text('schritt-'.$n.'-satz', $satz, ['desktop' => ['maxWidth' => '260px', 'fontSize' => '16px', 'lineHeight' => 1.6, 'color' => '@text', 'margin' => ['top' => '0px', 'right' => 'auto', 'bottom' => '0px', 'left' => 'auto']]]),
], ['desktop' => ['textAlign' => 'center']], '&:hover [id="schritt-'.$n.'-nummer"]{transform:scale(1.08)}');
$geraet = static fn (string $id, string $titel, string $satz) => $text($id, '<strong>'.$titel.'</strong>'.$satz, [
'desktop' => ['fontSize' => '14px', 'lineHeight' => 1.5, 'color' => '@text', 'backgroundColor' => '@mint', 'borderRadius' => $rund('12px'), 'padding' => $seiten('18px'), 'margin' => ['bottom' => '0px']],
], "& strong{display:block;font-size:16px;color:#17221a;hyphens:auto;overflow-wrap:break-word}");
$kennzahl = static fn (string $id, string $zahl, string $satz, bool $linie) => $column($id, [
$text($id.'-zahl', $zahl, ['desktop' => ['fontFamily' => 'heading', 'fontSize' => '52px', 'fontWeight' => '800', 'lineHeight' => 1.0, 'color' => '@gruen', 'margin' => ['bottom' => '6px']], 'mobile' => ['fontSize' => '40px']]),
$text($id.'-satz', $satz, ['desktop' => ['fontSize' => '15px', 'lineHeight' => 1.4, 'color' => '@text', 'margin' => ['bottom' => '0px']]]),
], $linie ? ['desktop' => ['borderWidth' => $nurLinks('1px'), 'borderStyle' => 'solid', 'borderColor' => '@linie', 'padding' => ['left' => '24px']], 'mobile' => ['padding' => ['left' => '14px']]] : []);
$stelle = static fn (string $id, string $titel, string $satz, string $betreff) => $karte($id, [
$text($id.'-text', '<a href="mailto:'.$mail.'?subject='.rawurlencode('Bewerbung: '.$betreff).'"><strong>'.$titel.'</strong></a><br>'.$satz, [
'desktop' => ['fontSize' => '14px', 'lineHeight' => 1.5, 'color' => '#cdebbe', 'margin' => ['bottom' => '0px']],
], '& strong{font-size:18px;color:#fff}'),
$symbol($id.'-pfeil', 'arrow-right', 20, ['desktop' => ['color' => '@papier', 'flexShrink' => 0]]),
], '16px', [
'justifyContent' => 'space-between', 'backgroundColor' => '#ffffff1f', 'borderWidth' => $seiten('1px'), 'borderStyle' => 'solid', 'borderColor' => '#ffffff33', 'borderRadius' => $rund('14px'),
'padding' => ['top' => '18px', 'right' => '22px', 'bottom' => '18px', 'left' => '22px'],
], [], [], $ganzeKarte."\ntransition:background-color .2s ease;\n&:hover{background-color:rgba(255,255,255,.2)}\n& .block-icon{transition:transform .25s ease}\n&:hover .block-icon{transform:translateX(4px)}");
$kontaktKarte = static fn (string $id, string $icon, string $klein, string $wert, ?string $href) => $karte($id, [
$kreis($id.'-kreis', $icon),
$text($id.'-text', $klein.'<br><strong>'.(null === $href ? $wert : '<a href="'.$href.'">'.$wert.'</a>').'</strong>', [
'desktop' => ['fontSize' => '13px', 'lineHeight' => 1.5, 'color' => '#6a766c', 'minWidth' => '0', 'margin' => ['bottom' => '0px']],
], '& strong{display:block;font-size:18px;color:#17221a;overflow-wrap:anywhere}'),
], '16px', [
'backgroundColor' => '@papier', 'borderRadius' => $rund('14px'), 'padding' => ['top' => '16px', 'right' => '18px', 'bottom' => '16px', 'left' => '18px'],
'shadowY' => '6px', 'shadowBlur' => '18px', 'shadowColor' => '#14201a0d',
], [], [], null === $href ? '' : $ganzeKarte."\ntransition:transform .2s ease,box-shadow .2s ease;\n&:hover{transform:translateY(-2px);box-shadow:0 14px 30px rgba(20,32,26,.1)}");
$instaBild = static fn (string $id, array $foto, string $was) => $bild($id, $foto, $was.' – Beiträge auf Instagram ansehen', ['desktop' => ['margin' => $seiten('0px'), 'borderRadius' => $rund('12px'), 'overflow' => 'hidden']], $instagram,
"& img{width:100%;aspect-ratio:1;object-fit:cover;border-radius:0;transition:transform .5s cubic-bezier(.2,.7,.2,1)}\n&:hover img{transform:scale(1.05)}\n& .image-link:focus-visible{outline:3px solid #2f6b22;outline-offset:-6px}");
// Formular im Zuschnitt der Vorlage: volle Breite statt der 640 px des Kerns, Name und Telefon
// nebeneinander, Felder hellgrün auf weiß.
$formularCss = '& .block-form{max-width:none;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:20px;row-gap:20px}
& .block-form>.form-field{grid-column:1/-1}
& .block-form>.cms-button{grid-column:1/-1}
& .block-form>.form-error-summary{grid-column:1/-1}
& .block-form>.form-success{grid-column:1/-1}
& .block-form>.form-missing{grid-column:1/-1}
& .block-form>[data-label="Name"]{grid-column:span 1}
& .block-form>[data-label="Telefon"]{grid-column:span 1}
& .form-field{align-content:start}
& .form-field label{font-size:14px;font-weight:700;letter-spacing:0.02em;color:#17221a}
& .form-field input{background:#f8fbf6;border:1px solid #cfe0c5;border-radius:10px;padding:14px 16px;font-size:16px;color:#17221a}
& .form-field select{background:#f8fbf6;border:1px solid #cfe0c5;border-radius:10px;padding:14px 16px;font-size:16px;color:#17221a}
& .form-field textarea{background:#f8fbf6;border:1px solid #cfe0c5;border-radius:10px;padding:14px 16px;font-size:16px;color:#17221a;min-height:140px}
& .form-field input:focus{border-color:#2f6b22}
& .form-field textarea:focus{border-color:#2f6b22}
& .field-note{font-size:13px;color:#55635a;opacity:1}
& .check-row label{font-weight:400;font-size:14px;line-height:1.6;color:#55635a}
& .check-row input{accent-color:#2f6b22}
& .cms-button{width:100%;justify-content:center;min-height:58px;transition:transform .2s ease,box-shadow .2s ease,background-color .2s ease}
& .cms-button:hover{transform:translateY(-2px);box-shadow:0 10px 24px rgba(0,0,0,.18)}
& .cms-button .button-arrow{display:none}';
$abschnitt = static fn (string $farbe = '', array $extra = []) => [
'desktop' => [...('' === $farbe ? [] : ['backgroundColor' => $farbe]), 'padding' => ['top' => '128px', 'bottom' => '128px'], ...($extra['desktop'] ?? [])],
'tablet' => ['padding' => ['top' => '80px', 'bottom' => '80px'], ...($extra['tablet'] ?? [])],
'mobile' => ['padding' => ['top' => '64px', 'bottom' => '64px'], ...($extra['mobile'] ?? [])],
];
$startseite = [
'schemaVersion' => $SCHEMA,
'title' => 'Gärten auf Premium-Niveau',
'description' => 'Garten- und Landschaftsbau auf Premium-Niveau aus Iffezheim: Terrassen, Pools, Pflaster, Natursteine und komplette Außenanlagen – geplant, gebaut und übergeben aus einer Hand.',
'blocks' => [
// 1 – Hero: Foto über die ganze Fläche, Verlauf von links und von unten, Text unten links,
// rechts die Karte „Direkter Draht“, darunter das Band mit vier Vorteilen.
$section('hero', [
$zeile('hero-zeile', [
$column('hero-text', [
$heading('hero-titel', 'Gärten auf<br><em>Premium-</em>Niveau.', 1, [
'desktop' => ['fontSize' => '104px', 'fontWeight' => '800', 'lineHeight' => 0.92, 'letterSpacing' => '-0.01em', 'textTransform' => 'uppercase', 'color' => '@papier', 'margin' => ['bottom' => '0px']],
'tablet' => ['fontSize' => '72px'],
'mobile' => ['fontSize' => '50px'],
], $em),
$text('hero-intro', 'Terrassen, Pools, Pflaster, Natursteine und komplette Außenanlagen – geplant, gebaut und übergeben aus einer Hand. Mit 100 % Detailarbeit.', [
'desktop' => ['maxWidth' => '580px', 'fontSize' => '21px', 'lineHeight' => 1.55, 'color' => '#e3ebe0', 'margin' => ['top' => '28px', 'bottom' => '40px']],
'mobile' => ['fontSize' => '18px', 'margin' => ['top' => '22px', 'bottom' => '30px']],
]),
$zeile('hero-knoepfe', [
$knopf('hero-beratung', 'Kostenlose Beratung', '#kontakt', 'rot', true, ['mobile' => ['width' => '100%', 'justifyContent' => 'center']]),
$knopf('hero-referenzen', 'Referenzen ansehen', '#projekte', 'glas', false, ['mobile' => ['width' => '100%', 'justifyContent' => 'center']]),
], '14px', ['flexWrap' => 'wrap']),
], ['desktop' => ['flexGrow' => 1, 'flexBasis' => '560px', 'maxWidth' => '760px'], 'mobile' => ['flexBasis' => 'auto']]),
$column('hero-karte', [
$text('hero-karte-klein', 'Direkter Draht', ['desktop' => ['fontSize' => '13px', 'fontWeight' => '700', 'letterSpacing' => '0.12em', 'textTransform' => 'uppercase', 'color' => '@gruen', 'margin' => ['bottom' => '8px']]]),
$text('hero-karte-titel', 'Vor-Ort-Termin vereinbaren', ['desktop' => ['fontFamily' => 'heading', 'fontSize' => '30px', 'fontWeight' => '800', 'lineHeight' => 1.05, 'textTransform' => 'uppercase', 'color' => '@tinte', 'margin' => ['bottom' => '18px']]]),
$zeile('hero-karte-telefon', [
$kreis('hero-karte-kreis', 'phone', '44px'),
$text('hero-karte-nummer', 'Rufen Sie uns an<br><a href="'.$tel.'"><strong>'.$telefon.'</strong></a>', ['desktop' => ['fontSize' => '13px', 'lineHeight' => 1.45, 'color' => '@text', 'margin' => ['bottom' => '0px']]], '& strong{display:block;font-size:20px;color:#17221a}'),
], '12px', [], [], [], $ganzeKarte),
$text('hero-karte-hinweis', '<strong>Mo–Fr</strong> 8–17 Uhr · <strong>Sa</strong> 8–12 Uhr', ['desktop' => ['fontSize' => '14px', 'lineHeight' => 1.5, 'color' => '@text', 'borderWidth' => $nurOben('1px'), 'borderStyle' => 'solid', 'borderColor' => '#e3ecdd', 'padding' => ['top' => '14px'], 'margin' => ['top' => '14px', 'bottom' => '0px']]], '& strong{color:#17221a}'),
], ['desktop' => ['flexBasis' => '300px', 'flexShrink' => 0, 'backgroundColor' => '#fffffff2', 'borderRadius' => $rund('16px'), 'padding' => $seiten('24px'), 'shadowY' => '24px', 'shadowBlur' => '60px', 'shadowColor' => '#0000004d']], '', 'desktop'),
], '40px', ['justifyContent' => 'space-between', 'alignItems' => 'flex-end', 'flexWrap' => 'wrap', 'padding' => ['bottom' => '72px']], ['padding' => ['bottom' => '36px']]),
// Das Band reicht über die ganze Breite; der Abschnitt schneidet ab, was übersteht.
$column('hero-vorteile', [
$column('hero-vorteile-raster', [
$vorteil(1, $vorteilIcon(1, 'home'), 'Alles aus einer Hand', 'Planung bis Übergabe'),
$vorteil(2, $vorteilIcon(2, 'star'), '100 % Detailarbeit', 'Saubere Ausführung'),
$vorteil(3, $lkw, 'Eigener Fuhrpark', 'LKW, Bagger & Maschinen'),
$vorteil(4, $vorteilIcon(4, 'pin'), 'Regional vor Ort', 'Iffezheim, Rastatt, Baden-Baden'),
], [
'desktop' => ['display' => 'grid', 'gridColumns' => 4, 'gap' => '0px', 'maxWidth' => '1280px', 'margin' => ['left' => 'auto', 'right' => 'auto']],
'tablet' => ['gridColumns' => 2],
'mobile' => ['gridColumns' => 2],
]),
], ['desktop' => ['backgroundColor' => '#0c140f8c', 'borderWidth' => $nurOben('1px'), 'borderStyle' => 'solid', 'borderColor' => '#ffffff24']],
'margin-left:calc(50% - 50vw);margin-right:calc(50% - 50vw);backdrop-filter:blur(6px)'),
], [
'desktop' => ['position' => 'relative', 'overflow' => 'hidden', 'display' => 'flex', 'flexDirection' => 'column', 'justifyContent' => 'flex-end', 'minHeight' => '800px', 'backgroundColor' => '#2a372c',
'backgroundImage' => $medien['hero']['id'], 'backgroundSize' => 'cover', 'backgroundPosition' => 'center',
'gradientFrom' => '#0c140fdb', 'gradientTo' => '#0c140f1a', 'gradientAngle' => 90,
'padding' => ['top' => '160px', 'bottom' => '0px']],
'tablet' => ['minHeight' => '680px', 'padding' => ['top' => '120px', 'bottom' => '0px']],
// Auf dem Telefon steht der Text über der ganzen Breite: dort deckt der Verlauf überall.
'mobile' => ['minHeight' => '0px', 'gradientFrom' => '#0c140fd9', 'gradientTo' => '#0c140f8c', 'padding' => ['top' => '64px', 'bottom' => '0px']],
], null, "&::before{content:'';position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(0deg,rgba(12,20,15,.9) 0%,rgba(12,20,15,0) 40%);pointer-events:none}\n& .section-inner{position:relative}"),
// 2 – Einleitung: Text links, Collage rechts.
$section('willkommen', [
$row('willkommen-zeile', [
$column('willkommen-text', [
$kleinzeile('willkommen-label', 'Willkommen bei Hüfner'),
$heading('willkommen-titel', 'Wir bauen Außenanlagen, die bleiben.', 2, $h2Stil),
$text('willkommen-lead', 'Ob neue Terrasse, Pool oder die komplette Außenanlage am Neubau: Wir begleiten Ihr Projekt vom ersten Gespräch bis zur Übergabe – mit eigenem Team, eigenem Fuhrpark und hochwertigen Materialien.', [
...$fliess('20px', '@text-stark', '18px', 1.65), 'mobile' => ['fontSize' => '18px'],
]),
$text('willkommen-satz', 'Klare Linien, saubere Fugen, stimmige Übergänge. Darauf legen wir Wert – bei jedem Projekt, ob privat oder öffentlich.', $fliess('17px', '@text', '36px')),
$column('willkommen-haken', [
$haken('haken-1', 'Persönliche Beratung'),
$haken('haken-2', 'Transparentes Angebot'),
$haken('haken-3', 'Geschultes Fachpersonal'),
$haken('haken-4', 'Saubere Baustelle'),
], ['desktop' => ['display' => 'grid', 'gridColumns' => 2, 'rowGap' => '14px', 'columnGap' => '24px'], 'mobile' => ['gridColumns' => 1]]),
]),
$column('willkommen-collage', [
$bild('collage-pool', $medien['pool-stufen'], 'Pool mit Sitzstufen und Keramikumrandung', [
'desktop' => ['position' => 'absolute', 'top' => '0px', 'right' => '0px', 'width' => '78%', 'height' => '78%', 'margin' => $seiten('0px'), 'overflow' => 'hidden', 'borderRadius' => $rund('18px'), 'backgroundColor' => '#2a372c',
'shadowY' => '30px', 'shadowBlur' => '60px', 'shadowColor' => '#14201a2e'],
], null, "& img{width:100%;height:100%;object-fit:cover;border-radius:0;transition:transform .7s cubic-bezier(.2,.7,.2,1)}\n&:hover img{transform:scale(1.04)}"),
$bild('collage-weg', $medien['plattenweg'], 'Plattenweg im Rasen entlang eines Neubaus', [
'desktop' => ['position' => 'absolute', 'left' => '0px', 'bottom' => '0px', 'width' => '52%', 'height' => '56%', 'margin' => $seiten('0px'), 'overflow' => 'hidden', 'borderRadius' => $rund('18px'), 'zIndex' => 1,
'borderWidth' => $seiten('8px'), 'borderStyle' => 'solid', 'borderColor' => '@mint', 'backgroundColor' => '#2a372c', 'shadowY' => '30px', 'shadowBlur' => '60px', 'shadowColor' => '#14201a33'],
'mobile' => ['borderWidth' => $seiten('6px')],
], null, "& img{width:100%;height:100%;object-fit:cover;border-radius:0;transition:transform .7s cubic-bezier(.2,.7,.2,1)}\n&:hover img{transform:scale(1.04)}"),
$column('collage-abzeichen', [
$text('collage-abzeichen-zahl', '100 %', ['desktop' => ['fontFamily' => 'heading', 'fontSize' => '48px', 'fontWeight' => '800', 'lineHeight' => 1.0, 'color' => '@papier', 'margin' => ['bottom' => '2px']], 'mobile' => ['fontSize' => '38px']]),
$text('collage-abzeichen-text', 'Detailarbeit', ['desktop' => ['fontSize' => '15px', 'fontWeight' => '600', 'color' => '#dcefd3', 'margin' => ['bottom' => '0px']]]),
], [
'desktop' => ['position' => 'absolute', 'right' => '24px', 'bottom' => '36px', 'zIndex' => 2, 'backgroundColor' => '@gruen', 'borderRadius' => $rund('16px'), 'padding' => ['top' => '20px', 'right' => '24px', 'bottom' => '20px', 'left' => '24px'],
'shadowY' => '20px', 'shadowBlur' => '40px', 'shadowColor' => '#14201a40'],
'mobile' => ['right' => '0px', 'bottom' => '20px', 'padding' => ['top' => '14px', 'right' => '18px', 'bottom' => '14px', 'left' => '18px']],
]),
], ['desktop' => ['position' => 'relative', 'height' => '600px'], 'tablet' => ['height' => '520px'], 'mobile' => ['height' => '420px']]),
], $zweiSpalten, [
'desktop' => ['gap' => '96px', 'alignItems' => 'center', 'margin' => ['top' => '0px']],
'tablet' => ['gap' => '48px'],
]),
], $abschnitt()),
// 3 – Leistungen: Kopf mit Satz rechts, links die große Pool-Kachel, rechts vier kleine,
// darunter weitere Leistungen als Chips.
$section('leistungen-bereich', [
$row('leistungen-kopf', [
$column('leistungen-kopf-titel', [
$kleinzeile('leistungen-label', 'Leistungen'),
$heading('leistungen-titel', 'Alles für Ihren Außenbereich', 2, ['desktop' => [...$h2Stil['desktop'], 'margin' => ['top' => '18px', 'bottom' => '0px']], 'tablet' => $h2Stil['tablet'], 'mobile' => $h2Stil['mobile']]),
], ['desktop' => ['maxWidth' => '680px']]),
$text('leistungen-kopf-text', 'Vom Erdbau über Pflaster- und Natursteinarbeiten bis zur fertigen Gartenanlage – alles aus einer Hand.', [
'desktop' => ['maxWidth' => '420px', 'fontSize' => '18px', 'lineHeight' => 1.65, 'color' => '@text', 'margin' => ['bottom' => '0px']],
]),
], [], [
'desktop' => ['display' => 'flex', 'flexWrap' => 'wrap', 'justifyContent' => 'space-between', 'alignItems' => 'flex-end', 'rowGap' => '24px', 'columnGap' => '64px', 'margin' => ['top' => '0px', 'bottom' => '56px']],
'mobile' => ['margin' => ['bottom' => '36px']],
]),
$row('leistungen-raster', [
$kachel('leistung-pool', $medien['pool-fachwerk'], 'Pools & Schwimmbecken – Beratung anfragen', [
$kachelTitel('leistung-pool-titel', 'Pools &amp; Schwimmbecken', '44px'),
$kachelSatz('leistung-pool-satz', 'Poolanlagen inklusive Umrandung, Technik und stimmiger Gestaltung rundherum – vom Aushub bis zur fertigen Terrasse.', '17px', '18px', '460px'),
$zeile('leistung-pool-mehr', [
$text('leistung-pool-mehr-text', 'Beratung anfragen', ['desktop' => ['fontSize' => '15px', 'fontWeight' => '700', 'letterSpacing' => '0.06em', 'textTransform' => 'uppercase', 'color' => '@papier', 'margin' => ['bottom' => '0px']]]),
$symbol('leistung-pool-mehr-pfeil', 'arrow-right', 16, ['desktop' => ['color' => '@papier']]),
], '8px'),
], '36px', '#kontakt', ['desktop' => ['minHeight' => '620px'], 'tablet' => ['minHeight' => '380px'], 'mobile' => ['minHeight' => '440px', 'padding' => $seiten('24px')]], '', true),
$column('leistungen-klein', [
$kachel('leistung-terrasse', $medien['terrasse'], 'Terrassen – Beratung anfragen', [
$kachelTitel('leistung-terrasse-titel', 'Terrassen'),
$kachelSatz('leistung-terrasse-satz', 'Keramik, Naturstein, Holz oder WPC.'),
], '24px', '#kontakt'),
$kachel('leistung-pflaster', $medien['pflaster-verlegt'], 'Pflaster – Beratung anfragen', [
$kachelTitel('leistung-pflaster-titel', 'Pflaster'),
$kachelSatz('leistung-pflaster-satz', 'Einfahrten, Wege und Plätze.'),
], '24px', '#kontakt'),
$kachel('leistung-naturstein', $medien['naturstein'], 'Natursteine – Beratung anfragen', [
$kachelTitel('leistung-naturstein-titel', 'Natursteine'),
$kachelSatz('leistung-naturstein-satz', 'Mauern, Stufen, Einfassungen.'),
], '24px', '#kontakt'),
$kachel('leistung-aussen', $medien['rasen'], 'Außenanlagen – Beratung anfragen', [
$kachelTitel('leistung-aussen-titel', 'Außenanlagen'),
$kachelSatz('leistung-aussen-satz', 'Rasen, Bepflanzung, Gesamtanlage.'),
], '24px', '#kontakt'),
], ['desktop' => ['display' => 'grid', 'gridColumns' => 2, 'gap' => '20px'], 'mobile' => ['gridColumns' => 1]], 'grid-auto-rows:300px'),
], $zweiSpalten, ['desktop' => ['gap' => '20px', 'margin' => ['top' => '0px']]]),
$zeile('leistungen-chips', [
$text('leistungen-chips-label', 'Außerdem', ['desktop' => ['fontFamily' => 'heading', 'fontSize' => '16px', 'fontWeight' => '700', 'letterSpacing' => '0.14em', 'textTransform' => 'uppercase', 'color' => '@text', 'margin' => ['right' => '8px', 'bottom' => '0px']]]),
$chip('chip-erdbau', 'Erdbau'),
$chip('chip-rollrasen', 'Rollrasen'),
$chip('chip-sichtschutz', 'Sichtschutz &amp; Zäune'),
$chip('chip-gabionen', 'Gabionen'),
$chip('chip-bewaesserung', 'Bewässerung'),
$chip('chip-entwaesserung', 'Entwässerung &amp; Rinnen'),
$chip('chip-spielplaetze', 'Spielplätze'),
], '12px', ['flexWrap' => 'wrap', 'margin' => ['top' => '40px']]),
], $abschnitt('@papier'), 'leistungen', $kachelFlaeche),
// 4 – Referenzen auf Nachtgrün.
$section('referenzen-bereich', [
$row('referenzen-kopf', [
$column('referenzen-kopf-titel', [
$kleinzeile('referenzen-label', 'Referenzen', '@hellgruen'),
$heading('referenzen-titel', 'Der Hüfner-Look', 2, ['desktop' => [...$h2Stil['desktop'], 'color' => '@papier', 'margin' => ['top' => '18px', 'bottom' => '0px']], 'tablet' => $h2Stil['tablet'], 'mobile' => $h2Stil['mobile']]),
]),
$kleinerKnopf($knopf('referenzen-instagram', 'Alle Projekte auf Instagram', $instagram, 'linie', true, [])),
], [], [
'desktop' => ['display' => 'flex', 'flexWrap' => 'wrap', 'justifyContent' => 'space-between', 'alignItems' => 'flex-end', 'gap' => '24px', 'margin' => ['top' => '0px', 'bottom' => '56px']],
'mobile' => ['margin' => ['bottom' => '36px']],
]),
$row('referenzen-raster', [
$kachel('referenz-garten', $medien['pool-garten'], 'Pool mit Hortensien und Gartenanlage', [
$kachelKlein('referenz-garten-klein', 'Pool · Garten · Terrasse', '14px'),
$kachelTitel('referenz-garten-titel', 'Poolanlage mit Gartengestaltung', '36px'),
], '32px', null, ['desktop' => ['minHeight' => '620px'], 'tablet' => ['minHeight' => '380px'], 'mobile' => ['minHeight' => '360px', 'padding' => $seiten('24px')]], '45%'),
$column('referenzen-klein', [
$kachel('referenz-terrasse', $medien['pool-terrasse'], 'Pool mit Natursteinterrasse und Gabionen', [
$kachelKlein('referenz-terrasse-klein', 'Pool · Naturstein'),
$kachelTitel('referenz-terrasse-titel', 'Pool &amp; Terrasse', '26px'),
], '24px', null, [], '55%'),
$kachel('referenz-zaun', $medien['sichtschutz'], 'Moderner Sichtschutz aus Aluminium', [
$kachelKlein('referenz-zaun-klein', 'Zaun'),
$kachelTitel('referenz-zaun-titel', 'Sichtschutz', '26px'),
], '24px', null, [], '55%'),
$kachel('referenz-hang', $medien['pflaster'], 'Hanggarten mit Natursteinmauer und Treppe', [
$kachelKlein('referenz-hang-klein', 'Hanggarten'),
$kachelTitel('referenz-hang-titel', 'Mauer &amp; Treppe', '26px'),
], '24px', null, [], '55%'),
], ['desktop' => ['display' => 'grid', 'gridColumns' => 2, 'gap' => '20px'], 'mobile' => ['gridColumns' => 1]], "grid-auto-rows:300px\n& [id=\"referenz-terrasse\"]{grid-column:1/-1}"),
], $zweiSpalten, ['desktop' => ['gap' => '20px', 'margin' => ['top' => '0px']]]),
$karte('referenzen-kommunen', [
$text('referenzen-kommunen-titel', 'Auch für Kommunen', ['desktop' => ['fontFamily' => 'heading', 'fontSize' => '22px', 'fontWeight' => '800', 'letterSpacing' => '0.04em', 'textTransform' => 'uppercase', 'color' => '@hellgruen', 'margin' => ['bottom' => '0px']]]),
$text('referenzen-kommunen-text', 'Zum Beispiel der Spielplatz an der Pagodenburg in Rastatt oder frischer Spielsand für die Spielplätze der Stadt Karlsruhe.', ['desktop' => ['flexGrow' => 1, 'flexBasis' => '300px', 'fontSize' => '16px', 'lineHeight' => 1.6, 'color' => '#c9d6c5', 'margin' => ['bottom' => '0px']], 'mobile' => ['flexBasis' => 'auto']]),
], '40px', ['flexWrap' => 'wrap', 'rowGap' => '16px', 'margin' => ['top' => '32px'], 'padding' => ['top' => '28px', 'right' => '32px', 'bottom' => '28px', 'left' => '32px']],
['padding' => $seiten('24px')], [], 'background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);border-radius:18px'),
], $abschnitt('@nacht', ['desktop' => ['color' => '@papier']]), 'projekte', $kachelFlaeche),
// 5 – Ablauf in vier Schritten mit gestrichelter Linie hinter den Nummern.
$section('ablauf-bereich', [
$column('ablauf-kopf', [
$kleinzeile('ablauf-label', 'So arbeiten wir', '@rot', true),
$heading('ablauf-titel', 'In vier Schritten zum Gartentraum', 2, ['desktop' => [...$h2Stil['desktop'], 'textAlign' => 'center', 'margin' => ['top' => '18px', 'bottom' => '0px']], 'tablet' => $h2Stil['tablet'], 'mobile' => $h2Stil['mobile']]),
], ['desktop' => ['maxWidth' => '760px', 'margin' => ['left' => 'auto', 'right' => 'auto', 'bottom' => '72px']], 'mobile' => ['margin' => ['bottom' => '48px']]]),
$column('ablauf-schritte', [
$flaeche('ablauf-linie', ['desktop' => ['position' => 'absolute', 'left' => '12.5%', 'right' => '12.5%', 'top' => '36px', 'height' => '2px', 'margin' => $seiten('0px')]],
"min-height:0;background:repeating-linear-gradient(90deg,#9dc38c 0 10px,transparent 10px 18px)", 'desktop'),
$column('ablauf-raster', [
$schritt(1, 'Beratung vor Ort', 'Wir schauen uns Ihr Grundstück an und hören zu, was Sie sich wünschen.'),
$schritt(2, 'Planung &amp; Angebot', 'Sie erhalten ein klares Konzept und ein transparentes Angebot.'),
$schritt(3, 'Ausführung', 'Unser Team baut termintreu, sauber und mit Liebe zum Detail.'),
$schritt(4, 'Übergabe', 'Gemeinsame Abnahme Ihres fertigen Außenbereichs.', '@rot'),
], [
'desktop' => ['display' => 'grid', 'gridColumns' => 4, 'gap' => '32px'],
'tablet' => ['gridColumns' => 2, 'rowGap' => '48px'],
'mobile' => ['gridColumns' => 1, 'rowGap' => '44px'],
]),
], ['desktop' => ['position' => 'relative']]),
], $abschnitt(), 'ablauf'),
// 6 – Fuhrpark: weiße Karte mit Baustellenfoto und vier Geräten.
$section('fuhrpark-bereich', [
$column('fuhrpark-karte', [
$row('fuhrpark-zeile', [
$column('fuhrpark-foto-spalte', [
$bild('fuhrpark-foto', $medien['baustelle'], 'Poolbau im Rohbau mit Minibagger und Radlader im Hintergrund', ['desktop' => ['height' => '100%', 'margin' => $seiten('0px')]], null,
'& img{width:100%;height:100%;min-height:360px;object-fit:cover;border-radius:0}'),
], ['desktop' => ['backgroundColor' => '#2a372c']]),
$column('fuhrpark-text', [
$kleinzeile('fuhrpark-label', 'Fuhrpark &amp; Maschinen'),
$heading('fuhrpark-titel', 'Eigenes Gerät. Kurze Wege.', 2, ['desktop' => [...$h2Stil['desktop'], 'fontSize' => '52px', 'margin' => ['top' => '18px', 'bottom' => '20px']], 'tablet' => ['fontSize' => '44px'], 'mobile' => $h2Stil['mobile']]),
$text('fuhrpark-satz', 'Weil wir mit eigenem Fuhrpark arbeiten, sind wir unabhängig, flexibel und schnell auf der Baustelle – auch bei großen Flächen.', $fliess('17px', '@text', '28px', 1.65)),
$column('fuhrpark-geraete', [
$geraet('geraet-lkw', 'LKW &amp; Kipper', 'für Material &amp; Aushub'),
$geraet('geraet-bagger', 'Bagger bis 15 t', 'inkl. Tieflader'),
$geraet('geraet-pflaster', 'Pflasterverlegemaschine', 'für große Flächen'),
$geraet('geraet-container', 'Abrollcontainer', 'für Entsorgung'),
], ['desktop' => ['display' => 'grid', 'gridColumns' => 2, 'gap' => '12px']]),
], ['desktop' => ['padding' => $seiten('56px')], 'mobile' => ['padding' => ['top' => '32px', 'right' => '24px', 'bottom' => '32px', 'left' => '24px']]]),
], $zweiSpalten, ['desktop' => ['gap' => '0px', 'margin' => ['top' => '0px'], 'columnWidths' => '11 10', 'alignItems' => 'stretch']]),
], ['desktop' => ['backgroundColor' => '@papier', 'borderRadius' => $rund('24px'), 'overflow' => 'hidden', 'shadowY' => '30px', 'shadowBlur' => '60px', 'shadowColor' => '#14201a14']]),
], ['desktop' => ['padding' => ['top' => '0px', 'bottom' => '128px']], 'tablet' => ['padding' => ['top' => '0px', 'bottom' => '80px']], 'mobile' => ['padding' => ['top' => '0px', 'bottom' => '64px']]]),
// 7 – Über uns.
$section('ueber-bereich', [
$row('ueber-zeile', [
$column('ueber-foto-spalte', [
$bild('ueber-foto', $medien['team'], 'Das Hüfner-Team in Arbeitsjacken mit Firmenlogo', [
'desktop' => ['margin' => $seiten('0px'), 'overflow' => 'hidden', 'borderRadius' => $rund('18px'), 'backgroundColor' => '#2a372c', 'imageHeight' => '560px'],
'mobile' => ['imageHeight' => '320px'],
], null, '& img{width:100%;object-fit:cover;border-radius:0}'),
$text('ueber-abzeichen', 'Miteinander. 100 %.', [
'desktop' => ['position' => 'absolute', 'left' => '-1px', 'bottom' => '32px', 'backgroundColor' => '@rot', 'color' => '@papier', 'fontFamily' => 'heading', 'fontSize' => '22px', 'fontWeight' => '800', 'letterSpacing' => '0.06em', 'textTransform' => 'uppercase', 'lineHeight' => 1.2,
'padding' => ['top' => '16px', 'right' => '24px', 'bottom' => '16px', 'left' => '24px'], 'borderRadius' => ['topLeft' => '0px', 'topRight' => '12px', 'bottomRight' => '12px', 'bottomLeft' => '0px'], 'margin' => ['bottom' => '0px']],
'mobile' => ['fontSize' => '18px', 'bottom' => '20px'],
]),
], ['desktop' => ['position' => 'relative']], '', 'all'),
$column('ueber-text', [
$kleinzeile('ueber-label', 'Über uns'),
$heading('ueber-titel', 'Handwerk mit Anspruch.', 2, $h2Stil),
$text('ueber-lead', 'Hinter Garten- und Landschaftsbau Hüfner steht Florian Hüfner mit seinem Team. Wir bauen Außenanlagen, die lange Freude machen – sauber geplant, präzise ausgeführt und bis ins Detail durchdacht.', [
...$fliess('20px', '@text-stark', '18px', 1.65), 'mobile' => ['fontSize' => '18px'],
]),
$text('ueber-satz', 'Unsere Mitarbeiter bilden sich regelmäßig weiter – etwa in moderner Bewässerungstechnik oder im Einbau und der Verdichtung von Asphalt.', $fliess('17px', '@text', '18px')),
// Laut Handelsregister (North Data, 06.10.2026): bis April 2022 „Garten- und
// Landschaftsbau Florian Hüfner e. K.“, seither GmbH mit Sitz in Iffezheim.
$text('ueber-geschichte', 'Angefangen hat alles als Einzelunternehmen von Florian Hüfner. Seit 2022 sind wir die Garten- und Landschaftsbau Hüfner GmbH – mit Sitz in Iffezheim und Baustellen in der ganzen Region.', $fliess('17px', '@text', '36px')),
$column('ueber-zahlen', [
$kennzahl('zahl-detail', '100 %', 'Detailarbeit', false),
$kennzahl('zahl-bagger', '15 t', 'Bagger im eigenen Fuhrpark', true),
$kennzahl('zahl-team', '1', 'Team von Planung bis Übergabe', true),
], ['desktop' => ['display' => 'grid', 'gridColumns' => 3, 'gap' => '0px', 'borderWidth' => $nurOben('1px'), 'borderStyle' => 'solid', 'borderColor' => '@linie', 'padding' => ['top' => '28px']], 'mobile' => ['gridColumns' => 3]], 'column-gap:24px'),
]),
], $zweiSpalten, [
'desktop' => ['gap' => '96px', 'alignItems' => 'center', 'margin' => ['top' => '0px']],
'tablet' => ['gap' => '48px'],
]),
], $abschnitt('@papier'), 'ueber-uns'),
// 8 – Karriere: grüne Karte.
$section('karriere-bereich', [
$karte('karriere-karte', [
$column('karriere-text', [
$text('karriere-klein', 'Karriere', ['desktop' => ['fontFamily' => 'heading', 'fontSize' => '17px', 'fontWeight' => '700', 'letterSpacing' => '0.2em', 'textTransform' => 'uppercase', 'color' => '#cdebbe', 'margin' => ['bottom' => '0px']]]),
$heading('karriere-titel', 'Werde Teil unseres Teams!', 2, ['desktop' => [...$h2Stil['desktop'], 'fontSize' => '52px', 'color' => '@papier', 'margin' => ['top' => '12px', 'bottom' => '14px']], 'tablet' => ['fontSize' => '44px'], 'mobile' => $h2Stil['mobile']]),
$text('karriere-satz', 'Du arbeitest gerne draußen, packst mit an und bist zuverlässig im Team? Dann melde dich bei uns – ein Anruf genügt.', $fliess('17px', '#e1f0da', '0px', 1.6)),
], ['desktop' => ['flexGrow' => 1, 'flexBasis' => '340px'], 'mobile' => ['flexBasis' => 'auto']]),
$column('karriere-stellen', [
$stelle('stelle-helfer', 'Mitarbeiter &amp; Helfer (m/w/d)', 'Vollzeit · Garten- und Landschaftsbau', 'Mitarbeiter und Helfer (m/w/d)'),
$stelle('stelle-lkw', 'LKW-Fahrer C/CE (m/w/d)', 'Vollzeit · Fuhrpark', 'LKW-Fahrer C/CE (m/w/d)'),
$knopf('karriere-anrufen', 'Jetzt anrufen: '.$telefon, $tel, 'weiss', false, ['desktop' => ['alignSelf' => 'flex-start', 'margin' => ['top' => '6px']], 'mobile' => ['fontSize' => '16px', 'width' => '100%', 'justifyContent' => 'center']]),
], ['desktop' => ['flexGrow' => 1, 'flexBasis' => '300px', 'display' => 'flex', 'flexDirection' => 'column', 'gap' => '12px'], 'mobile' => ['flexBasis' => 'auto']]),
], '64px', [
'flexWrap' => 'wrap', 'rowGap' => '32px', 'justifyContent' => 'space-between', 'backgroundColor' => '@gruen', 'color' => '@papier', 'borderRadius' => $rund('24px'),
'padding' => ['top' => '56px', 'right' => '64px', 'bottom' => '56px', 'left' => '64px'],
], ['padding' => ['top' => '32px', 'right' => '24px', 'bottom' => '32px', 'left' => '24px']]),
], ['desktop' => ['padding' => ['top' => '96px', 'bottom' => '96px']], 'mobile' => ['padding' => ['top' => '64px', 'bottom' => '64px']]], 'karriere'),
// 9 – Instagram.
$section('social-bereich', [
$row('social-kopf', [
$column('social-kopf-titel', [
$kleinzeile('social-label', 'Social Media'),
$heading('social-titel', 'Live von der Baustelle', 2, ['desktop' => [...$h2Stil['desktop'], 'fontSize' => '48px', 'lineHeight' => 1.0, 'margin' => ['top' => '12px', 'bottom' => '0px']], 'mobile' => ['fontSize' => '36px']]),
]),
$zeile('social-knoepfe', [
['id' => 'social-instagram', 'type' => 'social', 'props' => ['size' => 20, 'shape' => 'plain', 'layout' => 'horizontal', 'labels' => true, 'networks' => ['instagram']],
'css' => "& a{min-height:50px;padding:0 24px;border-radius:999px;background:#17221a;color:#fff;font-family:var(--kf-heading);font-size:17px;font-weight:700;letter-spacing:0.06em}\n& a:hover{background:#2f6b22;box-shadow:0 10px 24px rgba(0,0,0,.18)}"],
['id' => 'social-facebook', 'type' => 'social', 'props' => ['size' => 20, 'shape' => 'plain', 'layout' => 'horizontal', 'labels' => true, 'networks' => ['facebook']],
'css' => "& a{min-height:50px;padding:0 24px;border-radius:999px;background:#1f4fa0;color:#fff;font-family:var(--kf-heading);font-size:17px;font-weight:700;letter-spacing:0.06em}\n& a:hover{background:#173d7d;box-shadow:0 10px 24px rgba(0,0,0,.18)}"],
], '10px', ['flexWrap' => 'wrap']),
], [], [
'desktop' => ['display' => 'flex', 'flexWrap' => 'wrap', 'justifyContent' => 'space-between', 'alignItems' => 'flex-end', 'gap' => '20px', 'margin' => ['top' => '0px', 'bottom' => '28px']],
]),
$column('social-raster', [
$instaBild('insta-1', $medien['pool-stufen'], 'Pool mit Sitzstufen'),
$instaBild('insta-2', $medien['naturstein'], 'Natursteinmauer mit Trittplatten'),
$instaBild('insta-3', $medien['pool-garten'], 'Pool mit Gartenanlage'),
$instaBild('insta-4', $medien['plattenweg'], 'Plattenweg im Rasen'),
$instaBild('insta-5', $medien['terrasse'], 'Keramikterrasse am Pool'),
$instaBild('insta-6', $medien['team'], 'Das Hüfner-Team'),
], ['desktop' => ['display' => 'grid', 'gridColumns' => 6, 'gap' => '12px'], 'tablet' => ['gridColumns' => 3], 'mobile' => ['gridColumns' => 2]]),
], ['desktop' => ['padding' => ['top' => '0px', 'bottom' => '128px']], 'tablet' => ['padding' => ['top' => '0px', 'bottom' => '80px']], 'mobile' => ['padding' => ['top' => '0px', 'bottom' => '64px']]]),
// 10 – Abschlussband auf Foto.
$section('band', [
$column('band-inhalt', [
$heading('band-titel', 'Bereit für Ihren <em>Gartentraum?</em>', 2, [
'desktop' => ['fontSize' => '72px', 'fontWeight' => '800', 'lineHeight' => 0.95, 'letterSpacing' => '0px', 'textTransform' => 'uppercase', 'color' => '@papier', 'textAlign' => 'center', 'margin' => ['bottom' => '22px']],
'tablet' => ['fontSize' => '56px'],
'mobile' => ['fontSize' => '40px'],
], $em),
$text('band-satz', 'Erzählen Sie uns von Ihrem Vorhaben. Wir melden uns und vereinbaren einen Termin bei Ihnen vor Ort.', [
'desktop' => ['maxWidth' => '560px', 'fontSize' => '19px', 'lineHeight' => 1.6, 'color' => '#dce6d8', 'textAlign' => 'center', 'margin' => ['left' => 'auto', 'right' => 'auto', 'bottom' => '36px']],
]),
$zeile('band-knoepfe', [
$knopf('band-anfragen', 'Projekt anfragen', '#kontakt', 'rot', false),
$knopf('band-anrufen', $telefon, $tel, 'weiss', false),
], '14px', ['flexWrap' => 'wrap', 'justifyContent' => 'center']),
], ['desktop' => ['maxWidth' => '820px', 'margin' => ['left' => 'auto', 'right' => 'auto']]]),
], [
'desktop' => ['backgroundColor' => '#2a372c', 'backgroundImage' => $medien['pool-fachwerk']['id'], 'backgroundSize' => 'cover', 'backgroundPosition' => 'center', 'overlayColor' => '#0c140f', 'overlayOpacity' => 0.72,
'color' => '@papier', 'padding' => ['top' => '120px', 'bottom' => '120px']],
'mobile' => ['padding' => ['top' => '72px', 'bottom' => '72px']],
]),
// 11 – Kontakt mit Formular.
$section('kontakt-bereich', [
$row('kontakt-zeile', [
$column('kontakt-text', [
$kleinzeile('kontakt-label', 'Kontakt'),
$heading('kontakt-titel', 'Ihr Projekt. Unser Handwerk.', 2, ['desktop' => [...$h2Stil['desktop'], 'margin' => ['top' => '18px', 'bottom' => '24px']], 'tablet' => $h2Stil['tablet'], 'mobile' => $h2Stil['mobile']]),
$text('kontakt-satz', 'Schreiben Sie uns oder rufen Sie direkt an – wir melden uns zeitnah und vereinbaren einen Termin bei Ihnen vor Ort.', $fliess('18px', '@text', '40px', 1.65)),
$column('kontakt-karten', [
$kontaktKarte('kontakt-telefon', 'phone', 'Telefon', $telefon, $tel),
$kontaktKarte('kontakt-mail', 'mail', 'E-Mail', $mail, 'mailto:'.$mail),
$kontaktKarte('kontakt-adresse', 'pin', 'Adresse', $strasse.', '.$plz.' '.$ort, null),
$kontaktKarte('kontakt-zeiten', 'clock', 'Öffnungszeiten', $zeiten, null),
], ['desktop' => ['display' => 'flex', 'flexDirection' => 'column', 'gap' => '14px']]),
]),
$column('kontakt-formular', [
$heading('kontakt-formular-titel', 'Anfrage senden', 3, $h3Stil('30px', '@tinte', '20px')),
['id' => 'kontakt-formular-block', 'type' => 'form', 'props' => ['form' => 'anfrage'], 'css' => $formularCss],
$text('kontakt-formular-hinweis', 'Ihre Angaben verwenden wir nur für Ihre Anfrage. Mehr dazu in unseren <a href="/datenschutz">Datenschutzhinweisen</a>.', [
'desktop' => ['fontSize' => '13px', 'lineHeight' => 1.6, 'color' => '@text', 'margin' => ['top' => '16px', 'bottom' => '0px']],
], $linkCss),
], [
'desktop' => ['backgroundColor' => '@papier', 'borderRadius' => $rund('24px'), 'padding' => $seiten('48px'), 'shadowY' => '30px', 'shadowBlur' => '60px', 'shadowColor' => '#14201a14'],
'mobile' => ['padding' => $seiten('24px')],
]),
], $zweiSpalten, [
'desktop' => ['gap' => '80px', 'alignItems' => 'flex-start', 'margin' => ['top' => '0px'], 'columnWidths' => '9 11'],
'tablet' => ['gap' => '48px'],
]),
], $abschnitt(), 'kontakt'),
],
];
// ---------------------------------------------------------------------------------------------
// Seiten anlegen und veröffentlichen
// ---------------------------------------------------------------------------------------------
$anlegen = static function (string $slug, array $dokument, string $seoTitel = '', string $teilbild = '') use ($pages, $db): string {
$id = $pages->createPage($slug, null, 'page', $dokument);
if ('' !== $seoTitel) {
$db->executeStatement('UPDATE page SET seo_title = ? WHERE id = ?', [$seoTitel, $id]);
}
if ('' !== $teilbild) {
$db->executeStatement('UPDATE page SET og_media_id = ? WHERE id = ?', [$teilbild, $id]);
}
$seite = $db->fetchAssociative('SELECT draft_revision_id, version FROM page WHERE id = ?', [$id]);
$pages->publish($id, $seite['draft_revision_id'], (int) $seite['version']);
echo 'Seite angelegt: /'.$db->fetchOne('SELECT path FROM page WHERE id = ?', [$id])."\n";
return $id;
};
// Wie die Vorlage: der Hero-Text steigt beim Laden auf, das Foto nicht (LCP). Weiter unten
// erscheinen Köpfe und Raster beim Scrollen.
$startseite = $einblenden($startseite, [
'hero-titel' => ['rise', 'short'],
'hero-intro' => ['rise', 'medium'],
'hero-knoepfe' => ['rise', 'medium'],
'hero-karte' => ['rise', 'medium'],
'willkommen-text' => ['rise'],
'willkommen-collage' => ['rise', 'short'],
'leistungen-kopf' => ['rise'],
'leistungen-raster' => ['rise', 'short'],
'leistungen-chips' => ['fade', 'short'],
'referenzen-kopf' => ['rise'],
'referenzen-raster' => ['rise', 'short'],
'ablauf-kopf' => ['rise'],
'schritt-1' => ['rise'],
'schritt-2' => ['rise', 'short'],
'schritt-3' => ['rise', 'medium'],
'schritt-4' => ['rise', 'long'],
'fuhrpark-karte' => ['rise'],
'ueber-foto-spalte' => ['rise'],
'ueber-text' => ['rise', 'short'],
'karriere-karte' => ['rise'],
'social-kopf' => ['rise'],
'social-raster' => ['fade', 'short'],
'band-inhalt' => ['rise'],
'kontakt-text' => ['rise'],
'kontakt-formular' => ['rise', 'short'],
]);
$homeId = $anlegen('home', $startseite, 'Garten- und Landschaftsbau Hüfner GmbH – Terrassen, Pools, Pflaster & Natursteine', $medien['pool-fachwerk']['id']);
/** Eine Rechtsseite: Kleinzeile, Titel und Vorspann auf Nachtgrün, dann Absätze mit Zwischenüberschriften. */
$rechtsseite = static function (string $praefix, string $label, string $titel, string $vorspann, array $absaetze) use ($section, $row, $column, $kleinzeile, $heading, $text, $nurOben, $linkCss): array {
$kopf = $section($praefix.'-kopf', [
$kleinzeile($praefix.'-label', $label, '@hellgruen'),
$heading($praefix.'-h1', $titel, 1, [
'desktop' => ['fontSize' => '72px', 'fontWeight' => '800', 'lineHeight' => 0.95, 'letterSpacing' => '0px', 'textTransform' => 'uppercase', 'color' => '@papier', 'margin' => ['top' => '18px', 'bottom' => '20px']],
'mobile' => ['fontSize' => '46px'],
]),
$text($praefix.'-vorspann', $vorspann, [
'desktop' => ['maxWidth' => '640px', 'fontSize' => '18px', 'lineHeight' => 1.7, 'color' => '@auf-dunkel', 'margin' => ['bottom' => '0px']],
'mobile' => ['fontSize' => '16px'],
]),
], [
'desktop' => ['backgroundColor' => '@nacht', 'color' => '@papier', 'padding' => ['top' => '88px', 'bottom' => '72px']],
'mobile' => ['padding' => ['top' => '56px', 'bottom' => '48px']],
]);
$punkte = [];
foreach ($absaetze as $i => [$zwischentitel, $inhalt]) {
$punkte[] = $row($praefix.'-punkt-'.$i, [
$column($praefix.'-punkt-'.$i.'-titel', '' === $zwischentitel ? [] : [
$heading($praefix.'-h2-'.$i, $zwischentitel, 2, [
'desktop' => ['fontSize' => '22px', 'fontWeight' => '800', 'lineHeight' => 1.2, 'letterSpacing' => '0.02em', 'textTransform' => 'uppercase', 'color' => '@tinte', 'margin' => ['bottom' => '0px']],
]),
], [
'desktop' => ['flexBasis' => '260px', 'flexShrink' => 0],
'tablet' => ['flexBasis' => '200px'],
'mobile' => ['flexBasis' => 'auto'],
]),
$column($praefix.'-punkt-'.$i.'-text', [
$text($praefix.'-p-'.$i, $inhalt, ['desktop' => ['fontSize' => '16px', 'lineHeight' => 1.75, 'color' => '@text', 'margin' => ['bottom' => '0px']]], $linkCss),
], ['desktop' => ['flexGrow' => 1, 'flexBasis' => '0', 'minWidth' => '0'], 'mobile' => ['flexBasis' => 'auto']]),
], [], [
'desktop' => ['display' => 'flex', 'alignItems' => 'flex-start', 'gap' => '44px', 'margin' => ['top' => '0px'],
'borderWidth' => $nurOben('1px'), 'borderStyle' => 'solid', 'borderColor' => '@linie', 'padding' => ['top' => '26px', 'bottom' => '26px']],
'tablet' => ['gap' => '28px'],
'mobile' => ['flexDirection' => 'column', 'gap' => '8px', 'padding' => ['top' => '20px', 'bottom' => '20px']],
]);
}
$inhalt = $section($praefix, [
$column($praefix.'-inhalt', $punkte, ['desktop' => ['maxWidth' => '920px']]),
], ['desktop' => ['padding' => ['top' => '48px', 'bottom' => '96px']], 'mobile' => ['padding' => ['top' => '24px', 'bottom' => '64px']]]);
return [$kopf, $inhalt];
};
$anlegen('impressum', [
'schemaVersion' => $SCHEMA, 'title' => 'Impressum',
'description' => 'Anbieterangaben der Garten- und Landschaftsbau Hüfner GmbH, Iffezheim.',
'blocks' => [
...$rechtsseite('impressum', 'Rechtliche Angaben', 'Impressum', 'Angaben gemäß § 5 DDG und § 18 Abs. 2 MStV.', [
['Anbieter', $firma.'<br>'.$strasse.'<br>'.$plz.' '.$ort],
// Von Umut am 06.10.2026: HRB 742159, Mannheim. Laut Netzwerkauszug ist heute Florian
// Hüfner mit der GmbH verbunden, Simone Hüfner nur früher.
['Vertreten durch', 'Geschäftsführer: Florian Hüfner'],
['Registereintrag', 'Eintragung im Handelsregister<br>Registergericht: Amtsgericht Mannheim<br>Registernummer: HRB 742159'],
['Kontakt', 'Telefon: <a href="'.$tel.'">'.$telefon.'</a><br>E-Mail: <a href="mailto:'.$mail.'">'.$mail.'</a>'],
// Eine USt-IdNr. gehört nur hinein, wenn es eine gibt; weder Register noch North Data
// nennen eine (06.10.2026). Falls doch: hier als ['Umsatzsteuer', '… nach § 27a UStG: DE…'].
['Verantwortlich für den Inhalt', 'Florian Hüfner, Anschrift wie oben.'],
['Verbraucherstreitbeilegung', 'Wir sind nicht bereit und nicht verpflichtet, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.'],
['Haftung für Inhalte', 'Als Diensteanbieter sind wir für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Wir sind jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen. Bei Bekanntwerden von Rechtsverletzungen entfernen wir diese Inhalte umgehend.'],
['Haftung für Links', 'Diese Website verweist auf unsere Profile bei Instagram und Facebook. Für deren Inhalte ist der jeweilige Anbieter verantwortlich.'],
['Urheberrecht', 'Die von uns erstellten Inhalte, Fotos und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechts bedürfen der schriftlichen Zustimmung.'],
['Bildnachweis', 'Alle Fotos sind eigene Aufnahmen der '.$firma.'. Logo: '.$firma.'.'],
]),
],
], 'Impressum | '.$kurz);
$anlegen('datenschutz', [
'schemaVersion' => $SCHEMA, 'title' => 'Datenschutz',
'description' => 'Welche Daten beim Besuch der Website der Garten- und Landschaftsbau Hüfner GmbH und bei einer Anfrage verarbeitet werden.',
'blocks' => [
...$rechtsseite('datenschutz', 'Ihre Daten', 'Datenschutz', 'Hier erfahren Sie, welche Daten beim Besuch dieser Website und bei einer Anfrage verarbeitet werden – und welche Rechte Sie haben.', [
['1. Verantwortlicher', $firma.', '.$strasse.', '.$plz.' '.$ort.'. Telefon <a href="'.$tel.'">'.$telefon.'</a>, E-Mail <a href="mailto:'.$mail.'">'.$mail.'</a>.'],
['2. Besuch dieser Website', 'Beim Aufruf verarbeitet der Hostingdienst technisch erforderliche Verbindungsdaten: IP-Adresse, Zeitpunkt, angeforderte Adresse, Browserangaben und HTTP-Status. Das dient der sicheren Bereitstellung und der Fehlersuche. Rechtsgrundlage ist unser berechtigtes Interesse an einem störungsfreien Betrieb, Art. 6 Abs. 1 lit. f DSGVO.<br><br>Unser Hostinganbieter ist die IONOS SE, Elgendorfer Straße 57, 56410 Montabaur. Mit IONOS besteht ein Vertrag über die Auftragsverarbeitung nach Art. 28 DSGVO. Die Serverprotokolle werden nach spätestens sieben Tagen gelöscht oder anonymisiert, sofern sie nicht zur Aufklärung eines Sicherheitsvorfalls länger gebraucht werden.'],
['3. Cookies und externe Dienste', 'Diese Website setzt keine Analyse- oder Werbecookies. Bilder, Schriften und Gestaltung kommen vom eigenen Server; es werden keine Karten, Videos oder Schriften von Dritten nachgeladen. Deshalb gibt es auch keinen Einwilligungsdialog.'],
['4. Anfrageformular', 'Wenn Sie uns über das Formular schreiben, verarbeiten wir Ihren Namen, Ihre E-Mail-Adresse, auf Wunsch Ihre Telefonnummer, die gewählte Leistung und Ihre Beschreibung, um Ihre Anfrage zu beantworten und ein Angebot vorzubereiten (Art. 6 Abs. 1 lit. b DSGVO). Die Anfrage wird in unserem System gespeichert und per E-Mail an uns weitergeleitet. Wir löschen sie, wenn sie erledigt ist und keine gesetzliche Aufbewahrungspflicht besteht.'],
['5. Kontakt per E-Mail oder Telefon', 'Wenn Sie uns anrufen oder schreiben, verarbeiten wir Ihre Angaben, um Ihr Anliegen zu bearbeiten (Art. 6 Abs. 1 lit. b oder f DSGVO).'],
['6. Instagram und Facebook', 'Auf dieser Seite stehen nur Links zu unseren Profilen. Erst wenn Sie einem Link folgen, gelangen Sie zum jeweiligen Anbieter (Meta Platforms Ireland Ltd.); dort gelten dessen Datenschutzhinweise.'],
['7. Ihre Rechte', 'Sie haben unter den gesetzlichen Voraussetzungen das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit. Einer Verarbeitung auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO können Sie widersprechen.'],
['8. Beschwerderecht', 'Sie können sich bei einer Datenschutz-Aufsichtsbehörde beschweren, für uns beim Landesbeauftragten für den Datenschutz und die Informationsfreiheit Baden-Württemberg.'],
['', 'Fragen zum Datenschutz beantworten wir gern unter <a href="mailto:'.$mail.'">'.$mail.'</a>.'],
]),
],
], 'Datenschutz | '.$kurz);
$fehlerId = $anlegen('404', [
'schemaVersion' => $SCHEMA, 'title' => 'Seite nicht gefunden',
'description' => 'Die gesuchte Seite gibt es nicht oder sie wurde verschoben.',
'blocks' => [
$section('fehler', [
$kleinzeile('fehler-label', 'Fehler 404'),
$heading('fehler-titel', 'Hier wächst <em>noch nichts.</em>', 1, [
'desktop' => ['fontSize' => '80px', 'fontWeight' => '800', 'lineHeight' => 0.95, 'letterSpacing' => '0px', 'textTransform' => 'uppercase', 'color' => '@tinte', 'margin' => ['top' => '18px', 'bottom' => '22px']],
'mobile' => ['fontSize' => '48px'],
], $emGruen),
$text('fehler-text', 'Diese Seite gibt es nicht oder sie wurde verschoben. Über die Startseite finden Sie zurück.', [
'desktop' => ['maxWidth' => '480px', 'fontSize' => '18px', 'lineHeight' => 1.7, 'color' => '@text', 'margin' => ['bottom' => '32px']],
]),
$knopf('fehler-knopf', 'Zur Startseite', '/', 'rot', true),
], [
'desktop' => ['padding' => ['top' => '120px', 'bottom' => '140px']],
'mobile' => ['padding' => ['top' => '64px', 'bottom' => '80px']],
]),
],
]);
$db->executeStatement('UPDATE page SET robots = ? WHERE id = ?', ['noindex', $fehlerId]);
// ---------------------------------------------------------------------------------------------
// Menü, Einstellungen, Konto
// ---------------------------------------------------------------------------------------------
$sprung = static fn (string $label, string $anchor): array => ['label' => $label, 'type' => 'page', 'pageId' => $homeId, 'anchor' => $anchor, 'children' => []];
$menus->save('header', 'Hauptnavigation', [
$sprung('Leistungen', 'leistungen'),
$sprung('Referenzen', 'projekte'),
$sprung('Ablauf', 'ablauf'),
$sprung('Über uns', 'ueber-uns'),
$sprung('Karriere', 'karriere'),
$sprung('Kontakt', 'kontakt'),
], 0);
echo "Menü angelegt\n";
$settings->assignNotFoundPage($fehlerId, '');
$settings->save([
'legalName' => $firma,
'description' => 'Garten- und Landschaftsbau auf Premium-Niveau aus Iffezheim: Terrassen, Pools, Pflaster, Natursteine und komplette Außenanlagen – geplant, gebaut und übergeben aus einer Hand.',
'phone' => '+49 7229 6992826', 'email' => $mail,
'street' => $strasse, 'postalCode' => $plz, 'city' => $ort, 'country' => 'DE',
'social' => ['instagram' => $instagram, 'facebook' => $facebook],
'areaServed' => implode("\n", ['Iffezheim', 'Rastatt', 'Baden-Baden', 'Karlsruhe', 'Mittelbaden']),
'openingHours' => $zeitenDaten,
'localBusiness' => true, 'llmsTxt' => true,
'faviconMediaId' => $medien['favicon']['id'],
'stickyHeader' => true, 'headerOverlay' => false,
'motion' => 'always',
'homePath' => 'home', 'imprintPath' => 'impressum', 'privacyPath' => 'datenschutz',
// Die Seite lädt nichts von Dritten; deshalb keine Einwilligungsleiste.
'consentBanner' => false, 'scrollTop' => true, 'postMetaHeader' => false,
]);
echo "Einstellungen gespeichert\n";
$passwort = bin2hex(random_bytes(12));
$db->insert('cms_user', ['id' => bin2hex(random_bytes(16)), 'username' => 'umut', 'password_hash' => password_hash($passwort, PASSWORD_BCRYPT), 'roles' => '["ROLE_ADMIN"]']);
echo "\nFertig.\nVerwaltung: Benutzer „umut“, Passwort: $passwort\n";