Startseite ruhiger, Otterpohl mit Montage, Marken-Bilder und Farbwelt-Entwurf

- Hinweisleiste per B2IN_ANNOUNCEMENT_BAR ausgeblendet (Standard aus)
- Hero: „Räume aus einer Hand.“ als neue Überschrift, zweiter Button entfernt
- Projekttext mit zentralem deutschen Vertragspartner (Punkt statt Gedankenstrich)
- Otterpohl als vierte Kernmarke: Text v2, Abschnitt „Montage“, Titelbild getauscht
- Markenabschnitte: Absätze mit „- “ als Liste mit fetter Einleitung
- Titelbilder und Galerien für CABINET, Rotpunkt, Febrü und Otterpohl (WebP)
- Feedback 17.09. in den Markentexten, Demobilder entfernt, WhatsApp-Nummer
- Palette „Modern“ fest ohne Schalter, Entwurf /entwurf mit Hero-Slideshow
- Seeder-Liste fürs Deployment im Entwicklungsplan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-10-01 10:34:21 +00:00
parent 3211569fee
commit 4b07bbdcae
124 changed files with 3618 additions and 496 deletions

View file

@ -151,6 +151,102 @@
--font-secondary: 'IBM Plex Sans', 'Inter', system-ui, -apple-system, sans-serif;
}
/* ── Interior, Variante „Modern“ (Nachtrag 18) ──────────────────────
Marcels Rückmeldung vom 23.09.: die Creme-Flächen wirken altbacken. Diese
Variante lässt Tanne und Ocker aus dem Logo unangetastet und hellt nur die
Flächen auf: Grund fast weiß, Karten weiß, Sektionswechsel in hellem Grau-
Beige, Footer in Tanne. Aktiv, sobald der Farb-Schalter (palette-switch)
`palette-modern` an <html> setzt; die Wahl liegt im localStorage. */
.palette-modern .world-interior {
--background: 40 25% 98%; /* #fbfaf8 fast weiß, leicht warm */
--card: 0 0% 100%; /* Karten reinweiß */
--popover: 0 0% 100%;
--muted: 40 12% 94%; /* #f2f0ed */
--muted-foreground: 24 8% 38%;
--accent: 36 14% 93%; /* #efece7 Sektionswechsel, klar statt Sand */
--accent-dark: 36 14% 88%;
--border: 36 12% 88%; /* #e6e2dc */
--input: 0 0% 100%;
--hero-container: 40 14% 95%;
--hero-container-dark: 40 12% 90%;
--hero-container-light: 40 25% 98%;
/* Footer dunkel in Tanne – gibt der hellen Seite unten einen klaren Abschluss */
--footer-bg: 175 16% 15%;
--footer-fg: 38 33% 95%;
--footer-muted: 38 20% 76%;
--footer-line: 38 20% 76%;
/* Karten sind jetzt weiß auf fast weiß: etwas mehr Schatten, damit sie sich lösen */
--shadow-card: 0 6px 24px -10px hsl(var(--foreground) / 0.14);
--shadow-elevated: 0 24px 48px -20px hsl(var(--foreground) / 0.2);
}
/* ── Interior, Entwurf „Greige“ (Nachtrag 19, Briefing Marcel 24.09.) ─────
Heller, leicht kühler Grundton (Greige statt Creme), deutlich präsenteres
Tannengrün, Gold nur als feine Linie. Liegt als Klasse am <body> der
Entwurfsseite /entwurf – nicht am <html> wie der Browser-Schalter, damit
sich beide nicht überlagern. */
.world-interior.palette-greige {
--background: 45 14% 93%; /* #f0eeeb Greige, Off-White */
--foreground: 166 25% 13%; /* Tannengrau für Text */
--card: 40 20% 97%; /* #f8f7f4 – hell, aber kein steriles Weiß */
--card-foreground: 166 25% 13%;
--popover: 40 20% 97%;
--popover-foreground: 166 25% 13%;
--primary: 165 44% 14%; /* #14342c Tannengrün, klar und dunkel */
--primary-light: 165 32% 22%;
--primary-lighter: 165 12% 82%;
--primary-dark: 165 50% 10%;
--primary-foreground: 45 20% 95%;
--secondary: 38 45% 46%; /* #aa8341 gedecktes Gold */
--secondary-light: 38 45% 56%;
--secondary-lighter: 40 40% 80%;
--secondary-dark: 38 48% 38%;
--secondary-deep: 38 55% 34%; /* Gold als Textfarbe auf hellem Grund */
--cta-band-line: 38 45% 46%;
--secondary-foreground: 45 20% 95%;
--clay: 30 8% 50%;
--muted: 45 12% 90%;
--muted-foreground: 160 8% 36%;
--accent: 45 12% 89%; /* #e8e6e0 Sektionswechsel */
--accent-dark: 45 12% 84%;
--accent-foreground: 166 25% 13%;
--dark-bg: 165 44% 14%;
--dark-text: 45 20% 95%;
--dark-muted: 45 15% 78%;
/* Footer in Tannengrün, Haarlinie in Gold */
--footer-bg: 165 44% 14%;
--footer-fg: 45 20% 95%;
--footer-muted: 45 15% 78%;
--footer-line: 38 45% 46%;
--cta-band-end: 165 30% 22%;
--border: 40 10% 84%;
--input: 40 20% 98%;
--ring: 165 44% 14%;
--hero-container: 40 20% 97%;
--hero-container-dark: 45 12% 90%;
--hero-container-light: 40 25% 98%;
--shadow-card: 0 6px 24px -10px hsl(var(--foreground) / 0.14);
--shadow-elevated: 0 30px 60px -28px hsl(var(--primary) / 0.35);
}
}
/* Interior: Ocker ist Akzent, kein Flächenton unter Text – Creme auf Ocker erreicht
@ -173,4 +269,202 @@
color: hsl(var(--secondary-deep));
}
/* Variante „Modern“: auf dem dunklen Footer trägt Ocker in voller Stärke. */
.palette-modern .world-interior .section-footer .text-secondary {
color: hsl(var(--secondary));
}
/* Variante „Modern“: Abschlussband und Footer sind beide dunkel – eine helle Haarlinie trennt sie. */
.palette-modern .world-interior .section-footer {
border-top: 1px solid hsl(var(--footer-muted) / 0.35);
}
/* Der schwebende Termin-Button liegt über Bildern und weißen Karten: er bekommt eine
Fläche in Kartenfarbe, damit die Kontur nicht im Hintergrund verschwindet. */
.world-interior .appointment-toggle.btn-secondary-accent {
background: hsl(var(--card)) !important;
}
.world-interior .appointment-toggle.btn-secondary-accent:hover {
background: hsl(var(--primary)) !important;
}
/* Cookie-Manager (acme/cookie-consent) in der Interior-Welt: die Paketfarben aus
config/cookie-consent.php sind das Blau der Immobilien-Welt. Hier folgt der schwebende
Button dem Akzent-Button der Seite (Tanne, Hover heller), Icon, Schalter und Link-Hover
tragen Ocker, „Speichern“ liegt in Tanne. Die Immobilien-Welt behält ihr Blau. */
.world-interior .cookie-consent-wrapper {
--cc-primary: hsl(var(--secondary));
--cc-primary-hover: hsl(var(--secondary-dark));
--cc-save: hsl(var(--primary));
--cc-save-hover: hsl(var(--primary-light));
--cc-button-bg: hsl(var(--primary));
--cc-button-hover: hsl(var(--primary-light));
}
.world-interior .cookie-consent-wrapper .cc-floating-btn {
--tw-ring-color: hsl(var(--secondary)) !important;
color: hsl(var(--primary-foreground));
background: linear-gradient(145deg, hsl(var(--primary)) 0%, hsl(var(--primary-light)) 100%);
}
.world-interior .cookie-consent-wrapper .cc-floating-btn:hover {
background: linear-gradient(145deg, hsl(var(--primary-light)) 0%, hsl(var(--primary)) 100%);
}
/* Variante „Modern“: der Footer ist dunkel, also das negative Logo statt des positiven. */
.footer-logo-dark {
display: none;
}
.palette-modern .world-interior .footer-logo-light {
display: none;
}
.palette-modern .world-interior .footer-logo-dark {
display: block;
}
@import "./shared-styles.css";
/* ── Entwurf „Greige“: Footer dunkel mit Goldlinie und negativem Logo ─────── */
.world-interior.palette-greige .section-footer {
border-top: 1px solid hsl(var(--secondary) / 0.6);
}
.world-interior.palette-greige .section-footer .text-secondary {
color: hsl(var(--secondary));
}
.world-interior.palette-greige .footer-logo-light {
display: none;
}
.world-interior.palette-greige .footer-logo-dark {
display: block;
}
/* ── Hero des Entwurfs: Rahmen mit Goldkante, grüne Flanken, Slider ────────
Goldkanten als Verlaufs-Border: transparenter Rand, Fläche auf padding-box,
Goldverlauf auf border-box. Kein Pseudo-Rand mit Maske mehr – der ließ an
der gerundeten Kante eine helle Linie durchscheinen. Die grünen Flanken sind
Inline-SVGs (components/web/hero-flank-*), skaliert über die Rahmenhöhe. */
.hero-draft .hero-frame {
border: 1.5px solid transparent;
background:
linear-gradient(hsl(var(--hero-container)), hsl(var(--hero-container))) padding-box,
linear-gradient(
125deg,
hsl(var(--secondary-light)) 0%,
hsl(var(--secondary)) 28%,
hsl(var(--secondary) / 0.35) 62%,
hsl(var(--secondary-light)) 100%
) border-box;
box-shadow: var(--shadow-elevated);
isolation: isolate;
}
.hero-draft .hero-flank {
position: absolute;
z-index: 0;
width: auto;
}
.hero-draft .hero-flank-right {
top: 0;
right: 0;
height: 100%;
}
.hero-draft .hero-flank-left {
left: 0;
bottom: 0;
height: 46%;
}
.hero-draft .hero-image {
position: relative;
z-index: 1;
}
/* Goldkante um das Bild, gleiche Technik wie am Rahmen */
.hero-draft .hero-image-frame {
border: 1.5px solid transparent;
border-radius: 1.25rem;
background:
linear-gradient(hsl(var(--card)), hsl(var(--card))) padding-box,
linear-gradient(
135deg,
hsl(var(--secondary-light)) 0%,
hsl(var(--secondary)) 40%,
hsl(var(--secondary) / 0.4) 100%
) border-box;
box-shadow: var(--shadow-elevated);
}
.hero-draft .hero-eyebrow {
display: inline-flex;
align-items: center;
gap: 1rem;
font-size: 0.75rem;
letter-spacing: 0.28em;
text-transform: uppercase;
color: hsl(var(--muted-foreground));
}
.hero-draft .hero-eyebrow::after {
content: "";
width: 3.5rem;
height: 1px;
background: hsl(var(--secondary));
}
.hero-draft .hero-title {
font-family: var(--font-secondary);
font-size: clamp(2rem, 3.1vw, 3.3rem);
line-height: 1.12;
font-weight: 400;
letter-spacing: -0.01em;
color: hsl(var(--primary));
}
.hero-draft .hero-title em {
font-style: normal;
color: hsl(var(--secondary-deep));
}
/* Button: Goldrand rundum, unten kräftiger, Pfeil in Gold */
.hero-draft .btn-hero {
display: inline-flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.75rem;
border-radius: 0.75rem;
font-weight: 500;
color: hsl(var(--primary-foreground));
background: linear-gradient(145deg, hsl(var(--primary)) 0%, hsl(var(--primary-light)) 100%);
border: 1px solid hsl(var(--secondary));
border-bottom-width: 3px;
box-shadow: 0 14px 30px -14px hsl(var(--primary) / 0.6);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.hero-draft .btn-hero svg {
color: hsl(var(--secondary-light));
}
.hero-draft .btn-hero:hover {
transform: translateY(-1px);
box-shadow: 0 18px 34px -14px hsl(var(--primary) / 0.7);
}
.hero-draft .hero-slide {
transition: opacity 1.4s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.hero-draft .hero-slide {
transition: none;
}
}

View file

@ -80,14 +80,12 @@ return [
],
],
'hero' => [
'title' => '<span class="text-secondary">B2in Interior</span> – Premium-Einrichtung von starken Marken. Für Ihr Zuhause. Für Ihr Projekt.',
'title' => '<span class="text-secondary">Räume aus einer Hand.</span><br>Mit starken Marken und persönlichen Lösungen.',
'subtitle' => 'Von der einzelnen Maßanfertigung bis zur vollständigen Ausstattung – mit CABINET, Rotpunkt Küchen, Febrü und einem Netzwerk regionaler Fachexperten.',
'image' => 'b2in/hero-room.jpg',
'image_alt' => 'B2in Interior – Premium-Einrichtung von starken Marken',
'cta1_text' => 'Unsere Marken entdecken',
'cta1_link' => '/marken',
'cta2_text' => 'Termin anfragen',
'cta2_link' => '/contact',
'stats' => [
'Premium-Marken',
'Maßgefertigte Lösungen',
@ -96,6 +94,15 @@ return [
'card_title' => 'B2in Interior',
'card_text' => 'Einrichtung, <span class="text-secondary">die bleibt.</span>',
],
'hero_slider' => [
'eyebrow' => 'B2in Interior',
'title' => '<em>Räume aus einer Hand.</em> Mit starken Marken und persönlichen Lösungen.',
'subtitle' => 'Wir vereinen CABINET, Rotpunkt Küchen, Febrü und ein Netzwerk regionaler Experten, um individuelle Interior-Lösungen für Ihr Zuhause oder Ihr Projekt zu realisieren.',
'cta_text' => 'Unsere Marken entdecken',
'cta_link' => '/marken',
'interval' => 7,
'slides' => [],
],
'founder_bar' => [
'image' => 'b2in/marcel-scheibe.jpg',
'name' => 'Marcel Scheibe',
@ -203,7 +210,7 @@ return [
[
'icon' => 'building-office-2',
'title' => 'Ausstattung für Ihr Projekt',
'description' => 'Vollständige Einrichtung für Objekte, Büros und Projektentwicklungen – koordiniert aus einer Hand, von der Küche bis zur letzten Blume. Mit deutscher Vertragssicherheit und einem eingespielten Herstellernetzwerk.',
'description' => 'Vollständige Einrichtung für Objekte, Büros und Projektentwicklungen. Koordiniert aus einer Hand, von der Küche bis zur letzten Blume. Mit einem zentralen deutschen Vertragspartner und einem eingespielten Herstellernetzwerk.',
'button_text' => 'Projekt besprechen',
'button_link' => '/contact',
],
@ -251,15 +258,15 @@ return [
],
[
'slug' => 'tischlerei',
'name' => 'Tischlerei',
'tagline' => 'Maßarbeit, die den Raum vollendet.',
'teaser' => 'Aufbau, Sonderlösungen und Veredelung aus einer Tischlerei in Ostwestfalen – dort, wo Serienmöbel aufhören und Ihr Raum anfängt.',
'name' => 'Otterpohl Möbelservice',
'tagline' => 'Nicht easy click & buy. Aber dennoch easy to get.',
'teaser' => 'Individuelle Möbel, Türen, Treppen, Küchen, Wandverkleidungen und Objektbau – bis hin zum kompletten Innenausbau. Tischlerhandwerk aus Rheda-Wiedenbrück.',
'image' => '',
'logo' => '',
'link_text' => 'Mehr zur Tischlerei',
'logo' => 'marken/otterpohl.png',
'link_text' => 'Mehr zu Otterpohl',
'origin_city' => 'Rheda-Wiedenbrück',
'origin_distance' => 'rund 35 km',
'origin_role' => 'Unsere Werkstatt für Aufbau, Sonderlösungen und Veredelung.',
'origin_role' => 'Tischlerei und Innenausbau aus Rheda-Wiedenbrück – vom einzelnen Möbel bis zum ganzen Raum, inklusive Montage.',
],
],
],
@ -292,10 +299,30 @@ return [
],
[
'title' => 'Aufbau vor Ort, Sonderwünsche inklusive',
'description' => 'Unsere Tischlerei baut auf und ergänzt, was der Raum zusätzlich braucht: Blenden, Anschlüsse, Sonderlösungen.',
'description' => 'Otterpohl baut auf und ergänzt, was der Raum zusätzlich braucht: Blenden, Anschlüsse, Sonderlösungen.',
],
],
],
// Gemeinsamer Handwerk-Abschluss jeder Markenseite (Feedback 17.09., Nachtrag 15). Erscheint auf
// allen Markenseiten außer bei Otterpohl selbst; der Link führt zur Otterpohl-Seite.
'brand_craft' => [
'title' => 'Planung plus Handwerk',
'paragraphs' => [
'CABINET, Rotpunkt und Febrü decken bereits eine enorme planerische und gestalterische Bandbreite ab. Es geht deshalb ausdrücklich nicht um Sonderanfertigungen um jeden Preis.',
'Natürlich gibt es trotzdem diesen einen Fall, bei dem selbst ein hervorragendes System an seine Grenze kommt: der Übergang zur Architektur, die besondere Verkleidung, die Wand, die noch fehlt, oder der letzte Millimeter. Dann greift unsere Zusammenarbeit mit Otterpohl aus Rheda-Wiedenbrück – Tischlerhandwerk, Innenausbau und Montage aus einer Hand.',
'Wir wägen gestalterischen Mehrwert, technische Notwendigkeit und wirtschaftliche Sinnhaftigkeit ehrlich miteinander ab. Am Ende entscheiden aber Ihre Wünsche – nicht unser Drang, Ihnen noch eine Sonderlösung zu verkaufen.',
],
'detail' => [
'title' => 'Wenn das Handwerk den letzten Millimeter übernimmt',
'paragraphs' => [
'Möglich sind beispielsweise passgenaue Nischenlösungen, besondere Verkleidungen, raumhohe Einbauten, Übergänge zu angrenzenden Möbeln, individuelle Lackierungen und Echtholzfurniere oder Anpassungen an die vorhandene Architektur.',
'Bei Bedarf endet das Konzept nicht am Möbel. Auch Trockenbau, Tapeten, Fußböden, Wandgestaltung, Beleuchtung und weitere Details können einbezogen werden.',
],
],
'claim' => 'Die Möglichkeiten eines Maßsystems. Die Flexibilität des Handwerks.',
'link_text' => 'Mehr zu Otterpohl',
'brand_slug' => 'tischlerei',
],
'sponsoring' => [
'title' => 'Regional <span class="text-secondary">verbunden</span>',
'subtitle' => 'Wir unterstützen Vereine und Organisationen aus der Region – weil Verwurzelung mehr ist als eine Adresse.',

View file

@ -17,6 +17,20 @@ return [
'menu_open' => 'Menü öffnen',
// Welten-Wechsler (Interior / Immobilien)
// Farb-Schalter für die Abstimmung der Palette (Nachtrag 18)
'palette' => [
'label' => 'Farbwelt',
'classic' => 'Bisher',
'modern' => 'Neu',
'draft' => 'Entwurf',
],
// Slideshow im Hero des Entwurfs (Nachtrag 19)
'hero_slider' => [
'label' => 'Einblicke in unsere Marken',
'dots' => 'Bild wählen',
'dot' => 'Bild :n von :total: :label',
'empty' => 'Noch keine Bilder hinterlegt.',
],
'worlds_label' => 'Bereich wechseln',
'world_interior' => 'Interior',
'world_immobilien' => 'Immobilien',
@ -39,7 +53,8 @@ return [
'success' => 'Vielen Dank! Wir melden uns zeitnah bei Ihnen und stimmen den Termin ab.',
'new_request' => 'Weitere Anfrage',
'date_in_past' => 'Bitte einen Termin ab heute wählen.',
'whatsapp_intro' => 'Lieber direkt schreiben? Wir bereiten die Nachricht mit Ihren Angaben vor.',
'whatsapp_intro' => 'Am schnellsten per WhatsApp – wir bereiten die Nachricht mit Ihren Angaben vor.',
'or_form' => 'oder per Formular',
'whatsapp_text' => 'Hallo B2in Interior, ich hätte gern einen Beratungstermin.',
'whatsapp_note' => 'Öffnet WhatsApp mit vorbereitetem Text. Abgeschickt wird die Nachricht erst von Ihnen.',
],
@ -58,12 +73,22 @@ return [
'more_about' => 'Mehr zu :name',
// Markengalerie – realisierte Projekte vs. Entwuerfe
'brand_gallery_title' => 'Referenzen und Inspirationen',
'brand_gallery_intro' => 'Ausgeführte Arbeiten aus unseren Aufträgen – und Visualisierungen, die zeigen, was in einem Raum möglich ist.',
'brand_gallery_title' => 'Inspirationen und Entwürfe',
'brand_gallery_intro' => 'Visualisierungen, die zeigen, was in einem Raum möglich ist – noch bevor das erste Möbel steht.',
// Varianten je Galerie-Inhalt (Nachtrag 19): nur Entwürfe / nur Projekte und Werkstatt / gemischt
'brand_gallery_title_inspirations' => 'Inspirationen und Entwürfe',
'brand_gallery_intro_inspirations' => 'Visualisierungen, die zeigen, was in einem Raum möglich ist – noch bevor das erste Möbel steht.',
'brand_gallery_title_projects' => 'Referenzen',
'brand_gallery_intro_projects' => 'Realisierte Projekte – so sieht es aus, wenn Planung und Handwerk fertig sind.',
'brand_gallery_title_projects_company' => 'Referenzen und Einblicke',
'brand_gallery_intro_projects_company' => 'Realisierte Projekte und ein Blick in die Werkstatt, in der sie entstehen.',
'brand_gallery_title_mixed' => 'Projekte und Entwürfe',
'brand_gallery_intro_mixed' => 'Umgesetzte Projekte und Visualisierungen, die zeigen, was in einem Raum möglich ist.',
'brand_gallery_tab_projects' => 'Realisierte Projekte',
'brand_gallery_tab_inspirations' => 'Inspirationen und Entwürfe',
'brand_gallery_badge_project' => 'Realisiertes Projekt',
'brand_gallery_badge_inspiration' => 'Visualisierung',
'brand_gallery_badge_company' => 'Aus der Werkstatt',
'brand_gallery_inspiration_note' => 'Entwürfe und Visualisierungen. Sie zeigen gestalterische Möglichkeiten, noch bevor das erste Möbel steht.',
'previous' => 'Vorheriges Bild',
'next' => 'Nächstes Bild',

View file

@ -77,14 +77,12 @@ return [
],
],
'hero' => [
'title' => '<span class="text-secondary">B2in Interior</span> – Premium furnishing from strong brands. For your home. For your project.',
'title' => '<span class="text-secondary">Spaces from a single source.</span><br>With strong brands and personal solutions.',
'subtitle' => 'From a single bespoke piece to a complete fit-out – with CABINET, Rotpunkt Küchen, Febrü and a network of regional specialists.',
'image' => 'b2in/hero-room.jpg',
'image_alt' => 'B2in Interior – Premium furnishing from strong brands',
'cta1_text' => 'Discover our brands',
'cta1_link' => '/marken',
'cta2_text' => 'Request an appointment',
'cta2_link' => '/contact',
'stats' => [
'Premium brands',
'Bespoke solutions',
@ -93,6 +91,15 @@ return [
'card_title' => 'B2in Interior',
'card_text' => 'Interiors <span class="text-secondary">that last.</span>',
],
'hero_slider' => [
'eyebrow' => 'B2in Interior',
'title' => '<em>Rooms from a single source.</em> With strong brands and personal solutions.',
'subtitle' => 'We bring together CABINET, Rotpunkt Küchen, Febrü and a network of regional experts to create individual interior solutions for your home or your project.',
'cta_text' => 'Discover our brands',
'cta_link' => '/marken',
'interval' => 7,
'slides' => [],
],
'founder_bar' => [
'image' => 'b2in/marcel-scheibe.jpg',
'name' => 'Marcel Scheibe',
@ -200,7 +207,7 @@ return [
[
'icon' => 'building-office-2',
'title' => 'Fit-out for your project',
'description' => 'Complete furnishing for properties, offices and developments – coordinated from a single source, from the kitchen to the last flower. With German contractual reliability and an established manufacturer network.',
'description' => 'Complete furnishing for properties, offices and developments. Coordinated from a single source, from the kitchen to the last flower. With a single German contract partner and an established manufacturer network.',
'button_text' => 'Discuss your project',
'button_link' => '/contact',
],
@ -248,15 +255,15 @@ return [
],
[
'slug' => 'tischlerei',
'name' => 'Joinery',
'tagline' => 'Craftsmanship that completes the room.',
'teaser' => 'Assembly, bespoke solutions and finishing from a joinery in East Westphalia – where series furniture ends and your room begins.',
'name' => 'Otterpohl Möbelservice',
'tagline' => 'Not easy click & buy. But still easy to get.',
'teaser' => 'Bespoke furniture, doors, stairs, kitchens, wall cladding and contract work – up to complete interior fit-outs. Joinery from Rheda-Wiedenbrück.',
'image' => '',
'logo' => '',
'link_text' => 'More about the joinery',
'logo' => 'marken/otterpohl.png',
'link_text' => 'More about Otterpohl',
'origin_city' => 'Rheda-Wiedenbrück',
'origin_distance' => 'about 35 km',
'origin_role' => 'Our workshop for assembly, custom solutions and finishing.',
'origin_role' => 'Joinery and interior fit-out from Rheda-Wiedenbrück – from a single piece to the whole room, installation included.',
],
],
],
@ -286,10 +293,28 @@ return [
],
[
'title' => 'Assembly on site, special requests included',
'description' => 'Our joinery assembles and adds what the room needs beyond that: panels, connections, custom solutions.',
'description' => 'Otterpohl assembles and adds what the room needs beyond that: panels, connections, custom solutions.',
],
],
],
'brand_craft' => [
'title' => 'Planning plus craftsmanship',
'paragraphs' => [
'CABINET, Rotpunkt and Febrü already cover an enormous range of planning and design options. So this is explicitly not about custom-made pieces at any price.',
'Still, there is always that one case where even an excellent system reaches its limit: the transition to the architecture, a special cladding, the wall that is still missing, or the last millimetre. That is where our collaboration with Otterpohl from Rheda-Wiedenbrück comes in – joinery, interior fit-out and installation from a single source.',
'We weigh design value, technical necessity and economic sense honestly. In the end, your wishes decide – not our urge to sell you yet another special solution.',
],
'detail' => [
'title' => 'When craftsmanship takes over the last millimetre',
'paragraphs' => [
'Possible are, for example, precisely fitted alcove solutions, special claddings, full-height built-ins, transitions to adjoining furniture, individual lacquer finishes and real-wood veneers, or adaptations to the existing architecture.',
'If needed, the concept does not end at the furniture. Drywall, wallpaper, flooring, wall design, lighting and further details can be included.',
],
],
'claim' => 'The possibilities of a made-to-measure system. The flexibility of craftsmanship.',
'link_text' => 'More about Otterpohl',
'brand_slug' => 'tischlerei',
],
'sponsoring' => [
'title' => 'Rooted <span class="text-secondary">in the region</span>',
'subtitle' => 'We support clubs and organisations from our region – because being rooted is more than an address.',

View file

@ -17,6 +17,20 @@ return [
'menu_open' => 'Open menu',
// World switcher (Interior / Real estate)
// Farb-Schalter für die Abstimmung der Palette (Nachtrag 18)
'palette' => [
'label' => 'Colour scheme',
'classic' => 'Current',
'modern' => 'New',
'draft' => 'Draft',
],
// Slideshow in the draft hero (Nachtrag 19)
'hero_slider' => [
'label' => 'Glimpses of our brands',
'dots' => 'Choose image',
'dot' => 'Image :n of :total: :label',
'empty' => 'No images yet.',
],
'worlds_label' => 'Switch area',
'world_interior' => 'Interior',
'world_immobilien' => 'Real estate',
@ -39,7 +53,8 @@ return [
'success' => 'Thank you! We will get back to you shortly to arrange the appointment.',
'new_request' => 'Another request',
'date_in_past' => 'Please choose a date from today onwards.',
'whatsapp_intro' => 'Prefer to write directly? We prepare the message with your details.',
'whatsapp_intro' => 'Fastest via WhatsApp – we prepare the message with your details.',
'or_form' => 'or use the form',
'whatsapp_text' => 'Hello B2in Interior, I would like to arrange an appointment.',
'whatsapp_note' => 'Opens WhatsApp with a prepared text. You send the message yourself.',
],
@ -58,12 +73,21 @@ return [
'more_about' => 'More about :name',
// Brand gallery – realised projects vs. drafts
'brand_gallery_title' => 'References and inspiration',
'brand_gallery_intro' => 'Completed work from our projects – and visualisations showing what is possible in a room.',
'brand_gallery_title' => 'Inspiration and drafts',
'brand_gallery_intro' => 'Visualisations that show what is possible in a room – before the first piece of furniture is in place.',
'brand_gallery_title_inspirations' => 'Inspiration and drafts',
'brand_gallery_intro_inspirations' => 'Visualisations that show what is possible in a room – before the first piece of furniture is in place.',
'brand_gallery_title_projects' => 'References',
'brand_gallery_intro_projects' => 'Completed projects – this is what it looks like once planning and craft are done.',
'brand_gallery_title_projects_company' => 'References and insights',
'brand_gallery_intro_projects_company' => 'Completed projects and a look into the workshop where they are made.',
'brand_gallery_title_mixed' => 'Projects and drafts',
'brand_gallery_intro_mixed' => 'Completed projects and visualisations that show what is possible in a room.',
'brand_gallery_tab_projects' => 'Completed projects',
'brand_gallery_tab_inspirations' => 'Inspiration and drafts',
'brand_gallery_badge_project' => 'Completed project',
'brand_gallery_badge_inspiration' => 'Visualisation',
'brand_gallery_badge_company' => 'From the workshop',
'brand_gallery_inspiration_note' => 'Drafts and visualisations. They show design possibilities before the first piece of furniture is in place.',
'previous' => 'Previous image',
'next' => 'Next image',

View file

@ -0,0 +1,55 @@
{{-- Farb-Schalter für die Abstimmung (Nachtrag 18 + 19): bisherige Creme-Palette, die hellere
Variante „Neu“ und der Entwurf vom 24.09. Schwimmt am linken Rand in der Mitte – unten links
sitzt der Cookie-Button, unten rechts die Terminanfrage.
„Bisher“ und „Neu“ schalten im Browser um (localStorage b2in-palette, im <head> gesetzt,
?palette=modern|classic per Link). „Entwurf“ ist eine eigene Seite (/entwurf) mit fester
Palette; von dort führen die beiden anderen Einträge zurück auf die Startseite. --}}
@props(['draft' => false])
<div class="fixed left-3 top-1/2 z-40 -translate-y-1/2 print:hidden" data-palette-switch
@unless ($draft)
x-data="{
palette: document.documentElement.classList.contains('palette-modern') ? 'modern' : 'classic',
choose(value) {
this.palette = value;
document.documentElement.classList.toggle('palette-modern', value === 'modern');
try { localStorage.setItem('b2in-palette', value); } catch (e) {}
},
}"
@endunless>
<div class="flex flex-col items-stretch gap-1 rounded-full border border-border bg-card/95 p-1 text-[11px] font-medium shadow-lg backdrop-blur-sm"
role="group" aria-label="{{ __('ui.palette.label') }}">
<span class="px-2 pt-1 text-center text-[10px] uppercase tracking-wide text-muted-foreground" aria-hidden="true">
{{ __('ui.palette.label') }}
</span>
@if ($draft)
<a href="{{ url('/?palette=classic') }}"
class="rounded-full px-3 py-1.5 text-center text-muted-foreground transition-colors duration-200 hover:text-foreground">
{{ __('ui.palette.classic') }}
</a>
<a href="{{ url('/?palette=modern') }}"
class="rounded-full px-3 py-1.5 text-center text-muted-foreground transition-colors duration-200 hover:text-foreground">
{{ __('ui.palette.modern') }}
</a>
<span aria-current="page" class="rounded-full bg-primary px-3 py-1.5 text-center text-primary-foreground">
{{ __('ui.palette.draft') }}
</span>
@else
<button type="button" x-on:click="choose('classic')" x-bind:aria-pressed="(palette === 'classic').toString()"
x-bind:class="palette === 'classic' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground'"
class="cursor-pointer rounded-full px-3 py-1.5 transition-colors duration-200">
{{ __('ui.palette.classic') }}
</button>
<button type="button" x-on:click="choose('modern')" x-bind:aria-pressed="(palette === 'modern').toString()"
x-bind:class="palette === 'modern' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground'"
class="cursor-pointer rounded-full px-3 py-1.5 transition-colors duration-200">
{{ __('ui.palette.modern') }}
</button>
<a href="{{ route('home.draft') }}"
class="rounded-full px-3 py-1.5 text-center text-muted-foreground transition-colors duration-200 hover:text-foreground">
{{ __('ui.palette.draft') }}
</a>
@endif
</div>
</div>

View file

@ -0,0 +1,18 @@
{{-- Linke Flanke des Entwurfs-Heros (Nachtrag 19): schmaler Haken unten links, oben gerundet,
mit Goldlinie. Höhe rund 46 % des Rahmens, Breite aus dem viewBox-Verhältnis. --}}
<svg class="hero-flank hero-flank-left hidden lg:block" viewBox="0 0 80 480" preserveAspectRatio="xMinYMax meet"
aria-hidden="true" focusable="false" data-hero-flank="left">
<defs>
<linearGradient id="hero-flank-left-green" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" style="stop-color: hsl(var(--primary-light))" />
<stop offset="1" style="stop-color: hsl(var(--primary-dark))" />
</linearGradient>
<linearGradient id="hero-flank-left-gold" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" style="stop-color: hsl(var(--secondary-light))" />
<stop offset="1" style="stop-color: hsl(var(--secondary)); stop-opacity: 0.5" />
</linearGradient>
</defs>
<path d="M0 480V56C0 25 25 0 56 0H80V480Z" fill="url(#hero-flank-left-green)" />
<path d="M14 480V60C14 35 35 14 60 14H80" fill="none" stroke="url(#hero-flank-left-gold)" stroke-width="1.5"
vector-effect="non-scaling-stroke" />
</svg>

View file

@ -0,0 +1,24 @@
{{-- Rechte Flanke des Entwurfs-Heros (Nachtrag 19): tannengrüner Block mit gerundeter Innenkante,
Goldlinie mit Verlauf entlang der Rundung und einer kurzen Zierlinie wie in der Visualisierung.
Skaliert über die Rahmenhöhe (height: 100 %, Breite aus dem viewBox-Verhältnis). Farben
kommen aus den Tokens, damit die Flanke jede Palette mitmacht. Kann später 1:1 gegen eine
gelieferte SVG-Datei getauscht werden. --}}
<svg class="hero-flank hero-flank-right hidden lg:block" viewBox="0 0 140 1000" preserveAspectRatio="xMaxYMid meet"
aria-hidden="true" focusable="false" data-hero-flank="right">
<defs>
<linearGradient id="hero-flank-right-green" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" style="stop-color: hsl(var(--primary-light))" />
<stop offset="1" style="stop-color: hsl(var(--primary-dark))" />
</linearGradient>
<linearGradient id="hero-flank-right-gold" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" style="stop-color: hsl(var(--secondary-light))" />
<stop offset="0.5" style="stop-color: hsl(var(--secondary)); stop-opacity: 0.45" />
<stop offset="1" style="stop-color: hsl(var(--secondary-light))" />
</linearGradient>
</defs>
<path d="M140 0H72C32 0 0 32 0 72V928C0 968 32 1000 72 1000H140Z" fill="url(#hero-flank-right-green)" />
<path d="M140 14H74C41 14 14 41 14 74V926C14 959 41 986 74 986H140" fill="none"
stroke="url(#hero-flank-right-gold)" stroke-width="1.5" vector-effect="non-scaling-stroke" />
<path d="M124 330V600" fill="none" stroke="url(#hero-flank-right-gold)" stroke-width="1.5"
vector-effect="non-scaling-stroke" />
</svg>

View file

@ -565,13 +565,16 @@ $cancelForm = function () {
wire:click="addGalleryImage('{{ CmsBrand::GALLERY_PROJECT }}')">Projekt</flux:button>
<flux:button size="xs" variant="ghost" icon="plus"
wire:click="addGalleryImage('{{ CmsBrand::GALLERY_INSPIRATION }}')">Inspiration</flux:button>
<flux:button size="xs" variant="ghost" icon="plus"
wire:click="addGalleryImage('{{ CmsBrand::GALLERY_COMPANY }}')">Werkstatt</flux:button>
</div>
</div>
<div class="mb-3 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-200">
<strong>Projekt</strong> = tatsächlich ausgeführter Auftrag.
<strong>Projekt</strong> = tatsächlich ausgeführter Auftrag (erscheint auch unter „Referenzen“, solange dort nichts gepflegt ist).
<strong>Werkstatt</strong> = Einblick in Fertigung oder Montage des Herstellers.
<strong>Inspiration</strong> = Entwurf, Rendering oder KI-Darstellung.
Auf der Seite werden beide getrennt gezeigt und beschriftet – bitte nichts vermischen.
Auf der Seite trägt jedes Bild seine Beschriftung – bitte nichts vermischen.
</div>
@if (empty($gallery))
@ -589,6 +592,7 @@ $cancelForm = function () {
<flux:select wire:model="gallery.{{ $i }}.category" size="sm" class="mb-2">
<flux:select.option value="{{ CmsBrand::GALLERY_PROJECT }}">Realisiertes Projekt</flux:select.option>
<flux:select.option value="{{ CmsBrand::GALLERY_INSPIRATION }}">Inspiration / Entwurf</flux:select.option>
<flux:select.option value="{{ CmsBrand::GALLERY_COMPANY }}">Aus der Werkstatt</flux:select.option>
</flux:select>
<flux:input wire:model="gallery.{{ $i }}.caption" size="sm" class="mb-2"

View file

@ -71,13 +71,29 @@
<section class="pb-16">
<div class="container-narrow space-y-14">
@foreach ($brand['sections'] as $index => $section)
<div wire:key="section-{{ $index }}">
<div wire:key="section-{{ $index }}" @if (!empty($section['anchor'])) id="{{ $section['anchor'] }}" @endif>
@if (!empty($section['title']))
<h2 class="text-2xl lg:text-3xl font-medium">{{ $section['title'] }}</h2>
@endif
@foreach ($section['paragraphs'] as $i => $paragraph)
<p class="text-muted-foreground leading-relaxed mt-4">{{ $paragraph }}</p>
@foreach (\App\Models\CmsBrand::paragraphBlocks($section['paragraphs']) as $block)
@if ($block['type'] === 'list')
<ul class="mt-6 space-y-4">
@foreach ($block['items'] as $item)
<li class="flex gap-3 text-muted-foreground leading-relaxed">
<span class="mt-2.5 h-1.5 w-1.5 shrink-0 rounded-full bg-secondary" aria-hidden="true"></span>
<span>
@if ($item['lead'] !== '')
<strong class="font-medium text-foreground">{{ $item['lead'] }}:</strong>
@endif
{{ $item['text'] }}
</span>
</li>
@endforeach
</ul>
@else
<p class="text-muted-foreground leading-relaxed mt-4">{{ $block['text'] }}</p>
@endif
@endforeach
@if (!empty($section['quote']))
@ -129,6 +145,14 @@
@if (!empty($section['claim']))
<p class="text-large text-secondary font-medium mt-6">{{ $section['claim'] }}</p>
@endif
@if (!empty($section['link']) && !empty($section['link_text']))
<a href="{{ $section['link'] }}"
class="text-secondary font-medium mt-4 inline-flex items-center gap-1 hover:underline">
{{ $section['link_text'] }}
<span aria-hidden="true">&rarr;</span>
</a>
@endif
</div>
@endforeach
</div>
@ -153,98 +177,65 @@
</section>
@endif
{{-- 4. Galerie: realisierte Projekte und Inspirationen strikt getrennt --}}
{{-- 4. Galerie. Realisierte Projekte, Einblicke in die Werkstatt und Visualisierungen stehen in
einem Raster, unterschieden nur durch das Badge – der Rubrikwechsel ist ausgesetzt
(Abstimmung 22.09., Nachtrag 17). Reihenfolge: Projekte, Werkstatt, Visualisierungen.
Überschrift und Zeile darunter richten sich nach dem, was die Galerie enthält (Nachtrag 19):
nur Visualisierungen → „Inspirationen und Entwürfe“, nur Projekte → „Referenzen“, Projekte
und Werkstatt → „Referenzen und Einblicke“, mit Visualisierungen → „Projekte und Entwürfe“. --}}
@php
$galleryProjects = $gallery['projects'] ?? [];
$galleryInspirations = $gallery['inspirations'] ?? [];
$hasBothGalleryGroups = $galleryProjects !== [] && $galleryInspirations !== [];
$initialGalleryTab = $galleryProjects !== [] ? 'projects' : 'inspirations';
$galleryItems = array_merge($gallery['projects'] ?? [], $gallery['company'] ?? [], $gallery['inspirations'] ?? []);
$galleryLightbox = collect(['projects' => $galleryProjects, 'inspirations' => $galleryInspirations])
->map(fn (array $items) => collect($items)->map(fn (array $item) => [
'src' => theme_image_url($item['image']),
'caption' => $item['caption'] ?? '',
'badge' => $item['category'] === \App\Models\CmsBrand::GALLERY_PROJECT
? __('ui.brand_gallery_badge_project')
: __('ui.brand_gallery_badge_inspiration'),
])->values()->all())
->all();
$galleryHasReal = ! empty($gallery['projects']) || ! empty($gallery['company']);
$galleryHasDrafts = ! empty($gallery['inspirations']);
$galleryVariant = match (true) {
! $galleryHasReal => 'inspirations',
$galleryHasDrafts => 'mixed',
! empty($gallery['company']) => 'projects_company',
default => 'projects',
};
$galleryLightbox = collect($galleryItems)->map(fn (array $item) => [
'src' => theme_image_url($item['image']),
'caption' => $item['caption'] ?? '',
'badge' => \App\Models\CmsBrand::galleryBadge($item['category']),
])->values()->all();
@endphp
@if ($galleryProjects || $galleryInspirations)
@if ($galleryItems)
<section id="galerie" class="section-padding"
x-data="{
tab: '{{ $initialGalleryTab }}',
groups: @js($galleryLightbox),
items: @js($galleryLightbox),
lightbox: false,
current: 0,
get items() { return this.groups[this.tab] ?? [] },
open(index) { this.current = index; this.lightbox = true },
next() { this.current = (this.current + 1) % this.items.length },
prev() { this.current = (this.current - 1 + this.items.length) % this.items.length },
}">
<div class="container-padding">
<div class="text-center max-w-2xl mx-auto">
<h2 class="text-section-title">{{ __('ui.brand_gallery_title') }}</h2>
<p class="text-muted-foreground leading-relaxed mt-4">{{ __('ui.brand_gallery_intro') }}</p>
<h2 class="text-section-title">{{ __('ui.brand_gallery_title_'.$galleryVariant) }}</h2>
<p class="text-muted-foreground leading-relaxed mt-4">{{ __('ui.brand_gallery_intro_'.$galleryVariant) }}</p>
</div>
@if ($hasBothGalleryGroups)
<div class="flex justify-center mt-10" role="tablist" aria-label="{{ __('ui.brand_gallery_title') }}">
<div class="inline-flex rounded-full bg-accent p-1 gap-1">
@foreach (['projects' => __('ui.brand_gallery_tab_projects'), 'inspirations' => __('ui.brand_gallery_tab_inspirations')] as $tabKey => $tabLabel)
<button type="button" role="tab" wire:key="gallery-tab-{{ $tabKey }}"
x-on:click="tab = '{{ $tabKey }}'"
x-bind:aria-selected="tab === '{{ $tabKey }}' ? 'true' : 'false'"
x-bind:class="tab === '{{ $tabKey }}'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'"
class="px-6 py-2.5 rounded-full text-sm font-medium transition-colors cursor-pointer">
{{ $tabLabel }}
</button>
@endforeach
</div>
</div>
@endif
@foreach (['projects' => $galleryProjects, 'inspirations' => $galleryInspirations] as $groupKey => $groupItems)
@if ($groupItems)
<div wire:key="gallery-group-{{ $groupKey }}"
@if ($hasBothGalleryGroups) x-show="tab === '{{ $groupKey }}'" x-cloak @endif
class="mt-10">
@unless ($hasBothGalleryGroups)
<h3 class="text-xl font-medium text-center mb-8">
{{ $groupKey === 'projects' ? __('ui.brand_gallery_tab_projects') : __('ui.brand_gallery_tab_inspirations') }}
</h3>
@endunless
@if ($groupKey === 'inspirations')
<p class="text-sm text-muted-foreground text-center max-w-2xl mx-auto mb-8">
{{ __('ui.brand_gallery_inspiration_note') }}
</p>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-12">
@foreach ($galleryItems as $index => $item)
<figure wire:key="gallery-{{ $index }}" class="group">
<button type="button" x-on:click="open({{ $index }})"
class="relative block w-full overflow-hidden rounded-2xl cursor-pointer">
<img src="{{ theme_image_url($item['image']) }}"
alt="{{ $item['caption'] ?: $brand['name'] }}" loading="lazy"
class="w-full aspect-[4/3] object-cover transition-transform duration-300 group-hover:scale-105" />
<span class="absolute top-3 left-3 rounded-full bg-black/60 px-3 py-1 text-xs font-medium text-white backdrop-blur-sm">
{{ \App\Models\CmsBrand::galleryBadge($item['category']) }}
</span>
</button>
@if (!empty($item['caption']))
<figcaption class="text-sm text-muted-foreground mt-3">{{ $item['caption'] }}</figcaption>
@endif
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach ($groupItems as $index => $item)
<figure wire:key="gallery-{{ $groupKey }}-{{ $index }}" class="group">
<button type="button" x-on:click="open({{ $index }})"
class="relative block w-full overflow-hidden rounded-2xl cursor-pointer">
<img src="{{ theme_image_url($item['image']) }}"
alt="{{ $item['caption'] ?: $brand['name'] }}" loading="lazy"
class="w-full aspect-[4/3] object-cover transition-transform duration-300 group-hover:scale-105" />
<span class="absolute top-3 left-3 rounded-full bg-black/60 px-3 py-1 text-xs font-medium text-white backdrop-blur-sm">
{{ $groupKey === 'projects' ? __('ui.brand_gallery_badge_project') : __('ui.brand_gallery_badge_inspiration') }}
</span>
</button>
@if (!empty($item['caption']))
<figcaption class="text-sm text-muted-foreground mt-3">{{ $item['caption'] }}</figcaption>
@endif
</figure>
@endforeach
</div>
</div>
@endif
@endforeach
</figure>
@endforeach
</div>
</div>
{{-- Lightbox: ins body geteleportet, damit keine Eltern-Constraints stören --}}

View file

@ -1,139 +1,102 @@
{{-- Hero des Entwurfs (Nachtrag 19): Rahmen mit feiner Goldkante und grünen Flanken, links
Text mit einem Button, rechts die Slideshow. Das Bild hat ein festes Seitenverhältnis –
das Motiv wird auf großen Bildschirmen nicht mehr unten beschnitten (Briefing Punkt 2).
Der Wechsel läuft langsam (Überblendung 1,4 s, Standard alle 7 s), pausiert bei Hover und
bleibt bei „reduzierter Bewegung“ stehen. --}}
<section id="hero" class="hero-draft section-padding relative">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
x-data="{
active: 0,
count: {{ count($slides) }},
interval: {{ $this->intervalMs }},
timer: null,
paused: false,
reduced: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
start() {
if (this.reduced || this.count < 2) { return; }
this.stop();
this.timer = setInterval(() => { if (!this.paused && !document.hidden) { this.next(); } }, this.interval);
},
stop() { if (this.timer) { clearInterval(this.timer); this.timer = null; } },
go(i) { this.active = (i + this.count) % this.count; this.start(); },
next() { this.go(this.active + 1); },
prev() { this.go(this.active - 1); },
}"
x-init="start()" x-on:keydown.arrow-right.prevent="next()" x-on:keydown.arrow-left.prevent="prev()">
<div x-data="heroSlider()" x-init="init()">
<section class="relative mb-10">
<!-- Slider Container -->
<div class="absolute inset-0 overflow-hidden">
@if(!empty($content['slides']))
@foreach($content['slides'] as $index => $slide)
<div
class="absolute inset-0 transition-opacity duration-1000 ease-in-out"
:class="currentSlide === {{ $index }} ? 'opacity-100' : 'opacity-0'"
>
<img
src="{{ theme_image_url($slide['image']) }}"
alt="{{ $slide['image_alt'] }}"
class="size-full object-cover"
/>
<div class="absolute inset-0 bg-gray-900 opacity-20"></div>
</div>
@endforeach
@endif
</div>
<div class="hero-frame relative overflow-hidden rounded-[28px]">
<x-web.hero-flank-right />
<x-web.hero-flank-left />
<!-- Content -->
<div class="relative mx-auto flex max-w-3xl flex-col items-center px-6 py-32 text-center sm:py-64 lg:px-0">
<h1 class="text-hero text-white">
{!! $content['title'] !!}
</h1>
<p class="mt-4 text-xl text-white">{!! $content['subtitle'] !!}</p>
<a href="{{ $content['cta1_link'] }}" class="mt-8 inline-block btn-secondary-accent">{{ $content['cta1_text'] }}</a>
</div>
{{-- Rechts wenig Innenabstand: das Bild liegt über der grünen Flanke, wie in der Visualisierung. --}}
<div class="relative grid items-center gap-10 p-6 sm:p-10 lg:grid-cols-12 lg:gap-10 lg:py-5 lg:pl-16 lg:pr-12">
{{-- Text --}}
<div class="space-y-6 lg:col-span-5 lg:py-10 lg:pr-4">
@if (!empty($content['eyebrow']))
<p class="hero-eyebrow">{{ $content['eyebrow'] }}</p>
@endif
<!-- Navigation Dots -->
@if(!empty($content['slides']) && count($content['slides']) > 1)
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2">
<div class="flex space-x-3">
@foreach($content['slides'] as $index => $slide)
<button
@click="setSlide({{ $index }})"
class="w-3 h-3 rounded-full border-2 border-white/50 transition-all duration-300"
:class="currentSlide === {{ $index }} ? 'bg-white border-white' : 'bg-transparent hover:border-white/80'"
aria-label="{{ __('ui.show_slide', ['number' => $index + 1]) }}"
></button>
@endforeach
<h1 class="hero-title">{!! $content['title'] ?? '' !!}</h1>
@if (!empty($content['subtitle']))
<p class="max-w-md text-base leading-relaxed text-muted-foreground">{!! $content['subtitle'] !!}</p>
@endif
<div>
<a href="{{ $content['cta_link'] ?? '/marken' }}" class="btn-hero">
{{ $content['cta_text'] ?? __('ui.all_brands_cta') }}
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"
aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
</svg>
</a>
</div>
</div>
{{-- Slideshow --}}
<div class="hero-image lg:col-span-7">
<div class="hero-image-frame">
<div class="relative aspect-[4/3] overflow-hidden rounded-[calc(1.25rem-1.5px)] bg-muted lg:aspect-[5/4]"
role="group" aria-roledescription="carousel" aria-label="{{ __('ui.hero_slider.label') }}"
x-on:mouseenter="paused = true" x-on:mouseleave="paused = false"
x-on:focusin="paused = true" x-on:focusout="paused = false" tabindex="0">
@forelse ($slides as $index => $slide)
<figure wire:key="hero-slide-{{ $index }}" data-hero-slide="{{ $index }}"
class="hero-slide absolute inset-0 m-0 {{ $index === 0 ? 'opacity-100' : 'opacity-0' }}"
x-bind:class="active === {{ $index }} ? 'opacity-100' : 'opacity-0 pointer-events-none'"
x-bind:aria-hidden="(active !== {{ $index }}).toString()">
<img src="{{ theme_image_url($slide['image']) }}" alt="{{ $slide['alt'] }}"
class="h-full w-full object-cover" @if ($index > 0) loading="lazy" @endif />
@if ($slide['label'] !== '')
<figcaption
class="absolute bottom-5 left-5 rounded-full bg-card/85 px-3 py-1 text-[10px] font-medium uppercase tracking-[0.18em] text-foreground/80 backdrop-blur-sm">
{{ $slide['label'] }}
</figcaption>
@endif
</figure>
@empty
<div class="flex h-full items-center justify-center text-sm text-muted-foreground">
{{ __('ui.hero_slider.empty') }}
</div>
@endforelse
</div>
</div>
</div>
</div>
</div>
</div>
@endif
<!-- Arrow Navigation (Optional) -->
@if(!empty($content['slides']) && count($content['slides']) > 1)
<button
@click="previousSlide()"
class="absolute left-6 top-1/2 transform -translate-y-1/2 text-white/70 hover:text-white transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-white/50 rounded-full p-2"
aria-label="{{ __('ui.previous_image') }}"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
<button
@click="nextSlide()"
class="absolute right-6 top-1/2 transform -translate-y-1/2 text-white/70 hover:text-white transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-white/50 rounded-full p-2"
aria-label="{{ __('ui.next_image') }}"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
@endif
</section>
</div>
<script>
function heroSlider() {
return {
currentSlide: 0,
totalSlides: {{ !empty($content['slides']) ? count($content['slides']) : 0 }},
autoplayInterval: null,
autoplayTimeout: null,
init() {
if (this.totalSlides > 1) {
this.startAutoplay();
}
},
// Automatischer Wechsel (nur für Intervall)
autoNextSlide() {
this.currentSlide = (this.currentSlide + 1) % this.totalSlides;
},
// Manuelle Navigation
nextSlide() {
this.currentSlide = (this.currentSlide + 1) % this.totalSlides;
this.resetAutoplay();
},
previousSlide() {
this.currentSlide = (this.currentSlide - 1 + this.totalSlides) % this.totalSlides;
this.resetAutoplay();
},
setSlide(index) {
this.currentSlide = index;
this.resetAutoplay();
},
startAutoplay() {
// Zuerst alle bestehenden Timer löschen
this.stopAutoplay();
this.autoplayInterval = setInterval(() => {
this.autoNextSlide();
}, 5000); // Wechsel alle 5 Sekunden
},
stopAutoplay() {
if (this.autoplayInterval) {
clearInterval(this.autoplayInterval);
this.autoplayInterval = null;
}
if (this.autoplayTimeout) {
clearTimeout(this.autoplayTimeout);
this.autoplayTimeout = null;
}
},
resetAutoplay() {
// Alle Timer stoppen
this.stopAutoplay();
// Nach 5 Sekunden Pause wieder starten
this.autoplayTimeout = setTimeout(() => {
this.startAutoplay();
}, 1000);
}
}
}
</script>
{{-- Punkte unter dem Rahmen --}}
@if (count($slides) > 1)
<div class="mt-6 flex items-center justify-center gap-3" role="tablist" aria-label="{{ __('ui.hero_slider.dots') }}">
@foreach ($slides as $index => $slide)
<button type="button" role="tab" wire:key="hero-dot-{{ $index }}" x-on:click="go({{ $index }})"
x-bind:aria-selected="(active === {{ $index }}).toString()"
x-bind:class="active === {{ $index }} ? 'bg-primary w-14' : 'bg-border w-10 hover:bg-muted-foreground/40'"
class="h-1 cursor-pointer rounded-full transition-all duration-500 {{ $index === 0 ? 'bg-primary w-14' : 'bg-border w-10' }}"
aria-label="{{ __('ui.hero_slider.dot', ['n' => $index + 1, 'total' => count($slides), 'label' => $slide['label']]) }}">
</button>
@endforeach
</div>
@endif
</div>
</section>

View file

@ -16,9 +16,12 @@
<a href="{{ $content['cta1_link'] }}" class="btn-primary-accent">
{{ $content['cta1_text'] }}
</a>
<x-appointment-link :href="$content['cta2_link']" class="btn-secondary-accent">
{{ $content['cta2_text'] }}
</x-appointment-link>
{{-- Zweiter Button nur, wenn gepflegt – seit 01.10.2026 leer, „Termin anfragen“ bleibt im Widget. --}}
@if (filled($content['cta2_text'] ?? null))
<x-appointment-link :href="$content['cta2_link'] ?? '/contact'" class="btn-secondary-accent">
{{ $content['cta2_text'] }}
</x-appointment-link>
@endif
</div>
@if(isset($content['stats']))

View file

@ -63,7 +63,32 @@
<p class="text-sm text-muted-foreground leading-relaxed mt-2">{{ $content['intro'] }}</p>
@endif
<form wire:submit="submit" class="space-y-4 mt-6">
{{-- WhatsApp zuerst (Abstimmung 23.09.): der schnellste Weg steht oben, das Formular
darunter. Nur sichtbar, wenn im CMS eine Nummer hinterlegt ist. Der Link ist ein
normaler Klick-zum-Chat-Link – auf dem Handy öffnet die installierte App, sonst
WhatsApp Web. Der Text ist aus dem Formularstand vorbereitet, abgeschickt wird er
vom Besucher selbst. --}}
@if ($this->whatsappUrl !== '')
<div class="mt-5" data-appointment-whatsapp>
<p class="text-sm text-muted-foreground">{{ __('ui.appointment.whatsapp_intro') }}</p>
<a x-bind:href="whatsappHref" target="_blank" rel="noopener"
class="mt-3 flex w-full items-center justify-center gap-2 rounded-lg bg-[#25D366] px-6 py-3 font-medium text-white transition-transform hover:scale-[1.02]">
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.372-.025-.521-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.872.118.571-.085 1.758-.719 2.006-1.413.247-.694.247-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884a9.82 9.82 0 016.988 2.896 9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.885-9.885 9.885M20.52 3.449C18.24 1.245 15.24 0 12.045 0 5.463 0 .104 5.359.101 11.945c0 2.096.549 4.142 1.595 5.945L0 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.582 0 11.941-5.359 11.944-11.945a11.87 11.87 0 00-3.417-8.4" />
</svg>
{{ __('ui.whatsapp') }}
</a>
<p class="mt-2 text-xs text-muted-foreground">{{ __('ui.appointment.whatsapp_note') }}</p>
<div class="mt-6 flex items-center gap-3 text-xs uppercase tracking-wide text-muted-foreground" aria-hidden="true">
<span class="h-px flex-1 bg-border"></span>
<span>{{ __('ui.appointment.or_form') }}</span>
<span class="h-px flex-1 bg-border"></span>
</div>
</div>
@endif
<form wire:submit="submit" class="space-y-4 mt-5">
<div>
<label for="appointment-name" class="text-foreground mb-2 block text-sm font-medium">
{{ __('ui.appointment.name') }} <span aria-hidden="true">*</span>
@ -150,23 +175,6 @@
</button>
</form>
{{-- Alternative: WhatsApp. Nur sichtbar, wenn im CMS eine Nummer hinterlegt ist.
Der Link ist ein normaler Klick-zum-Chat-Link – auf dem Handy öffnet die
installierte App, sonst WhatsApp Web. Der Text ist vorbereitet, abgeschickt
wird er vom Besucher selbst. --}}
@if ($this->whatsappUrl !== '')
<div class="mt-6 pt-6 border-t border-border/60">
<p class="text-sm text-muted-foreground">{{ __('ui.appointment.whatsapp_intro') }}</p>
<a x-bind:href="whatsappHref" target="_blank" rel="noopener"
class="mt-3 flex w-full items-center justify-center gap-2 rounded-lg bg-[#25D366] px-6 py-3 font-medium text-white transition-transform hover:scale-[1.02]">
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.372-.025-.521-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.872.118.571-.085 1.758-.719 2.006-1.413.247-.694.247-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884a9.82 9.82 0 016.988 2.896 9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.885-9.885 9.885M20.52 3.449C18.24 1.245 15.24 0 12.045 0 5.463 0 .104 5.359.101 11.945c0 2.096.549 4.142 1.595 5.945L0 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.582 0 11.941-5.359 11.944-11.945a11.87 11.87 0 00-3.417-8.4" />
</svg>
{{ __('ui.whatsapp') }}
</a>
<p class="mt-2 text-xs text-muted-foreground">{{ __('ui.appointment.whatsapp_note') }}</p>
</div>
@endif
@endif
</div>
@ -174,7 +182,7 @@
<button type="button" x-on:click="open = !open" x-bind:aria-expanded="open.toString()"
aria-controls="appointment-widget-title"
class="btn-secondary-accent flex items-center gap-2 rounded-full px-5 py-3 shadow-lg cursor-pointer
max-w-[calc(100vw-2rem)] text-left leading-tight">
max-w-[calc(100vw-2rem)] text-left leading-tight appointment-toggle">
<svg class="h-5 w-5 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"
aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round"

View file

@ -1,5 +1,6 @@
{{-- Der Footer folgt der Farbwelt der Seite: Interior auf Sand mit Tanne und positivem Logo,
Immobilien dunkel mit negativem Logo (Tokens `--footer-*` in theme-b2in.css, Logo in mount()).
In der Farbvariante „Modern“ liegt auch der Interior-Footer dunkel und zeigt das negative Logo.
pb-28 auf kleinen Schirmen: darunter schwebt das Terminanfrage-Widget, das sonst die
letzten Zeilen des Footers verdecken wuerde. --}}
<footer id="footer" class="section-footer section-padding pb-28 sm:pb-16">
@ -9,9 +10,16 @@
<div class="spacing-section">
<div class="flex items-center justify-center">
<img src="{{ asset($logoPath) }}"
alt="{{ $domainName ?? 'B2in Interior' }} Logo" class="h-16 w-auto"
alt="{{ $domainName ?? 'B2in Interior' }} Logo" class="footer-logo-light h-16 w-auto"
width="81" height="64"
loading="lazy" />
@if ($logoPathDark !== '')
{{-- Farbvariante „Modern“: dunkler Footer, negatives Logo (Umschaltung per CSS). --}}
<img src="{{ asset($logoPathDark) }}"
alt="{{ $domainName ?? 'B2in Interior' }} Logo" class="footer-logo-dark h-16 w-auto"
width="81" height="64"
loading="lazy" />
@endif
</div>
<div class="container-narrow spacing-content">

View file

@ -0,0 +1,33 @@
{{-- Entwurf der Startseite in der Farbwelt vom 24.09.2026 (Nachtrag 19). Nur zur Abstimmung:
Palette „greige“ am Body, Gold-Logo, neuer Hero mit Slideshow, keine Ankündigungsleiste.
Der Rest der Seite ist die reguläre Startseite – die Sektionen färben sich über die Tokens. --}}
@extends('web.layouts.web-master')
@section('title', 'Entwurf – B2in Interior')
@section('meta_description', 'Entwurf der Startseite in der neuen Farbwelt – nur zur Abstimmung.')
@section('announcement_bar', 'off')
@push('meta')
<meta name="robots" content="noindex, nofollow">
@endpush
@section('content')
<div class="bg-background">
<livewire:web.components.ui.header />
<main class="variante-glass-flow">
<livewire:web.components.sections.hero-slider />
<livewire:web.components.sections.founder-bar />
<livewire:web.components.sections.audience-switch />
<livewire:web.components.sections.core-brands bg="bg-accent" />
<livewire:web.components.sections.origin-section />
<livewire:web.components.sections.vision-section />
<livewire:web.components.sections.sponsoring bg="bg-accent" />
<livewire:web.components.sections.display-feed />
<livewire:web.components.sections.immobilien-teaser />
<livewire:web.components.sections.c-t-a-section />
</main>
<livewire:web.components.ui.footer />
</div>
@endsection

View file

@ -1,5 +1,14 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
@php
// Farbpalette der Interior-Welt (Nachtrag 18): ohne Schalter setzt der Server die Klasse aus
// config('app.palette'); mit Schalter entscheidet das Skript im <head> anhand des localStorage.
$paletteFixed = config('app.theme', 'b2in') === 'b2in'
&& b2in_world() === 'interior'
&& ! config('app.palette_switch')
&& b2in_palette() === ''
&& config('app.palette') === 'modern';
@endphp
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"{!! $paletteFixed ? ' class="palette-modern"' : '' !!}>
<head>
<meta charset="utf-8">
@ -51,8 +60,40 @@
@php
$theme = config('app.theme', 'b2in');
// Farb-Schalter nur auf den Interior-Seiten von b2in, solange die Palette abgestimmt wird.
$showPaletteSwitch = $theme === 'b2in' && b2in_world() === 'interior' && config('app.palette_switch');
// Entwurf /entwurf (Nachtrag 19): feste Palette „greige“ am Body, kein Browser-Schalter.
$paletteDraft = b2in_palette() === 'greige';
// Global per B2IN_ANNOUNCEMENT_BAR, einzeln per @section('announcement_bar', 'off').
$hideAnnouncementBar = ! config('app.announcement_bar') || trim($__env->yieldContent('announcement_bar', 'on')) === 'off';
@endphp
@if ($showPaletteSwitch && ! $paletteDraft)
{{-- Läuft vor dem Stylesheet: setzt die gemerkte Palette, bevor die Seite gezeichnet wird.
?palette=modern|classic in der URL überschreibt und merkt sich die Wahl; ohne
gemerkte Wahl gilt config('app.palette'). --}}
<script>
(function () {
var fromUrl = new URLSearchParams(window.location.search).get('palette');
var palette = null;
try {
if (fromUrl === 'modern' || fromUrl === 'classic') {
localStorage.setItem('b2in-palette', fromUrl);
}
palette = localStorage.getItem('b2in-palette');
} catch (e) {
palette = fromUrl;
}
if (palette === null) {
palette = @js(config('app.palette'));
}
if (palette === 'modern') {
document.documentElement.classList.add('palette-modern');
}
})();
</script>
@endif
@vite([
\App\Helpers\ThemeHelper::getThemeCssPath(),
'resources/js/app.js'
@ -113,13 +154,15 @@
@stack('styles')
</head>
<body class="antialiased bg-background text-foreground world-{{ b2in_world() }}">
<body class="antialiased bg-background text-foreground world-{{ b2in_world() }}{{ $paletteDraft ? ' palette-greige' : '' }}">
{{-- GTM noscript (nur bei GTM-Nutzung) --}}
<x-cookie-consent::gtm-noscript />
<!-- Announcement Bar -->
<livewire:web.components.ui.announcement-bar />
{{-- Ankündigungsleiste – global per B2IN_ANNOUNCEMENT_BAR, je Seite mit @section('announcement_bar', 'off') abschaltbar --}}
@unless ($hideAnnouncementBar)
<livewire:web.components.ui.announcement-bar />
@endunless
{{-- TopBar (Backup: Sprachwechsel & Social Icons – folgt später) --}}
{{-- <livewire:web.components.ui.top-bar /> --}}
@ -129,6 +172,10 @@
{{-- Terminanfrage: schwebend unten rechts, auf allen Seiten --}}
<livewire:web.components.ui.appointment-widget />
@if ($showPaletteSwitch)
<x-palette-switch :draft="$paletteDraft" />
@endif
{{-- Cookie Consent Manager (vor Livewire-Scripts) --}}
<x-cookie-consent::manager />

View file

@ -1,6 +1,6 @@
@extends('web.layouts.web-master')
@section('title', 'Unsere Marken – CABINET, Rotpunkt Küchen, Febrü & Tischlerei | B2in Interior')
@section('title', 'Unsere Marken – CABINET, Rotpunkt Küchen, Febrü & Otterpohl | B2in Interior')
@section('meta_description', 'Ausgewählte Hersteller statt beliebiger Kataloge: maßgefertigte Einbauschränke, Premium-Küchen und Möbel fürs Arbeiten – mit persönlicher Beratung in der Region.')
@section('content')