No description
Find a file
2026-08-24 16:21:20 +02:00
.claude Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
assets Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
docs Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
scripts Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
404.html Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
bewerbung.html Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
datenschutz.html Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
DEV-NOTES.md Translate README and DEV-NOTES to German 2026-08-24 16:21:20 +02:00
impressum.html Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
index.html Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
karriere-ausbildung.html Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
karriere-kuechenmonteur.html Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
karriere-moebelmonteur.html Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
karriere-subunternehmer.html Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
karriere.html Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
kontakt.html Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
nachrichten.html Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
README.md Translate README and DEV-NOTES to German 2026-08-24 16:21:20 +02:00
standort-hamburg.html Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
ueber-uns.html Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00
unsere-mission.html Initial commit: Translogistik website 2026-08-24 16:12:27 +02:00

Translogistik — Website-Build aus zwei Konzepten

Eine ganz normale statische Website: kein Build-Prozess, kein npm, kein Framework. Wird per Doppelklick auf index.html oder über einen beliebigen Static-Server geöffnet.


1. Struktur

translogistik/
├── index.html                  ← Startseite, Markup ist per Kommentar in Blöcke gegliedert
├── ueber-uns.html              ← Seite «Über uns» (siehe Punkt 7)
├── assets/
│   ├── css/
│   │   ├── tokens.css          ← EINZIGE Quelle für Farbe/Theme/Geometrie
│   │   ├── base.css            ← Reset, Typografie, Sektionsrhythmus, Utilities
│   │   ├── components.css      ← .btn / .card / accordion / combobox / pill
│   │   ├── layout.css          ← Header, Mobilmenü, Footer
│   │   ├── sections.css        ← hero, prinzip, standorte, wrench, leistungen,
│   │   │                         gruppe, team, faq, cta
│   │   └── about.css           ← NUR «Über uns»: geschichte/timeline + werte
│   ├── img/
│   │   ├── wrench.png          ← Schraubenschlüssel für die Scroll-Szene (war inline base64)
│   │   ├── *.jpg               ← ARCHIV der Fotoshootings: 11 Bilder, 69 MB. Ist an
│   │   │                         die Website nicht angebunden, liegt nur als Quelle
│   │   ├── photos/             ← Web-Set: 11 × 4 Breiten, 4 MB (siehe Punkt 8)
│   │   └── svg/
│   │       ├── Hamburg.svg     ← Skyline-Masken für die Standort-Buttons (8 Stück,
│   │       ├── …                 Langenselbold fehlt noch)
│   │       └── _source/        ← Originale vor der Optimierung (18 MB)
│   └── js/
│       ├── theme.js            ← Theme: system / light / dark
│       ├── accordion.js        ← universelles Akkordeon auf data-Attributen
│       │                         (damit läuft auch das FAQ auf der Startseite)
│       ├── form-modal.js       ← «Gesendet»-Modal für kontakt/bewerbung
│       ├── locations.js        ← Karte + Liste + Suche + Ansicht zurücksetzen
│       ├── wrench.js           ← Scroll-Kinematik mit dem Schraubenschlüssel
│       ├── timeline.js         ← «Unsere Geschichte»: Sticky-Jahre + Meilensteine
│       ├── gruppe.js           ← «Krieger Gruppe»: Filter + Marken-Karussell
│       └── main.js             ← Bootstrap, Mobilmenü, Scroll-Spy, Reveal
├── scripts/
│   ├── optimize-skyline.ps1    ← Vektorisierter SVG-Export → leichte Maske
│   ├── optimize-photos.ps1     ← Kamerafotos → Web-Set (siehe Punkt 8)
│   └── serve.js                ← statischer Server ohne Abhängigkeiten
├── import/                     ← ursprüngliche Konzepte (nicht an die Website angebunden)
└── README.md

Reihenfolge der Sektionen in index.html:

hero → logo-bar → prinzip → standorte → wrench-scene → leistungen
     → gruppe → team → faq → cta → footer

Reihenfolge der Sektionen in ueber-uns.html:

hero (Über uns) → logo-bar → geschichte → werte → team → footer

Jede Seite bindet nur ein, was sie wirklich braucht. «Über uns» lädt kein Leaflet, kein locations.js, wrench.js, faq.js oder accordion.js — dort gibt es weder eine Karte noch die Szene mit dem Schraubenschlüssel noch ein Akkordeon. Das Bootstrap in main.js ruft ein Modul nur auf, wenn es definiert ist (if (TL.timeline) …), deshalb ist die Liste der <script>-Tags auf einer Seite zugleich ihre Konfiguration.

Externe Abhängigkeiten gibt es nur über CDN: Google Fonts (Inter + Instrument Serif) und Leaflet 1.9.4 (Karte).


2. Was woher stammt

Block auf der Website Quelle Was geändert wurde
Header (sticky, TL-Logo, Nav, Theme-Umschalter, CTA) Translogistik-Codex-V2-Mit.html Tailwind-Klassen → normales CSS; funktionierendes Mobilmenü und Scroll-Spy ergänzt
Footer (3 Spalten + untere Zeile) Translogistik-Codex-V2-Mit.html Jahr wird per Skript eingesetzt; Links sind mit den Sektionen verknüpft
Hero (Titel Inter + Instrument Serif italic, Lead, 2 Buttons, 3 Zahlen, roter Gradient, Badge «Trusted by Küchenhäuser») Translogistik-Codex-V2-Mit.html 3 Fotos rechts durch EIN einziges (.hero__photo) ersetzt; auf der Startseite zusätzlich .hero--bg — siehe unten
Partnerleiste (Nobilia, Häcker, Nolte …) Translogistik-Codex-V2-Mit.html Tippfehler Balller zu Ballerina korrigiert
«Worauf du dich bei uns verlassen kannst.» — 7 Prinzipien Translogistik-Codex-V2-Mit.html Akkordeon auf semantisches HTML + ARIA umgebaut (aria-expanded, Pfeile, Home/End)
«Wir übernehmen den Rest damit du dich aufs Verkaufen konzentrierst.» — 3 Karten + 2 Plaketten Translogistik-Codex-V2-Mit.html Inhaltlich unverändert
Standorte: Filialkarte + Liste translogistik-template.html (Leaflet, echte Koordinaten) Komplett neue Mechanik — siehe Punkt 3
«Kurze Wege / 150 km Radius» Translogistik-Codex-V2-Mit.html Unter das Kartenraster verschoben, damit die gleiche Spaltenhöhe nicht bricht
Team & Vertrauen: Fotocollage-«Bogen» aus 9 Bildern + 3 Bewertungen translogistik-template.html Überschrift ersetzt durch «Bereit, mit uns zu wachsen? / Hier siehst du unser Team…» (wie gewünscht). Buttons aus dem alten Block grow-cta sind unter die Bewertungen gewandert
Scroll-Szene «VON DER LIEFERUNG BIS ZUR LETZTEN SCHRAUBE» + Raster 0104 translogistik-wrench-scroll.html PNG des Schlüssels aus Inline-base64 nach assets/img/wrench.png ausgelagert; das rote #c43c2e durch das Marken---accent ersetzt; Demo-Platzhalter «Scroll» / «Zurück» entfernt; Pre-Roll ergänzt (siehe Punkt 4)
FAQ: rote Karte, 4 Kacheln tauschen den Text rechts aus Translogistik-Faq-Brighter-Style.html Umschalt-Kacheln durch ein normales Akkordeon ersetzt — siehe Punkt 3c. Antworten stehen im Markup (SEO + Funktion ohne JS); die internen Labels «BLOCK» und «FAQ Modul • Brighter Style» entfernt; «über 20 Jahren Erfahrung» → «seit 2014» (siehe unten)
Abschließendes CTA (Karte «Karriere» mit Foto eines Lkw) Translogistik-Codex-V2-Mit.html Überschrift geändert zu «Wir suchen Monteure, Fahrer und Dispo-Talente.», weil «Bereit, mit uns zu wachsen?» jetzt im Team-Block steht
Mini-Block «Krieger Gruppe»: Filter + Marken-Karussell krieger-gruppe-filter-karussell.html Auf Design-Tokens umgestellt, Marken ins Markup verlagert, Pillen wurden zu einem echten Filter nach Bereichen — siehe Punkt 7

Texte, Zahlen (98,2 %, 450+, 1200+, 9 Standorte, 150 km) und Standortdaten wurden unverändert aus den Konzepten übernommen — nichts wurde dazuerfunden.

Ein Widerspruch in den Quellen: Das FAQ-Konzept schrieb «mit über 20 Jahren Erfahrung», während der Rest der Website «seit 2014» / «Gründung 2014» sagt. Beibehalten wurde «seit 2014» — sonst stünden auf einer Seite zwei sich widersprechende Aussagen. Falls 20 Jahre stimmen, müssen auch die anderen Blöcke angepasst werden.

Entfernte «Hinweistexte»

Auf Wunsch wurden Mikro-Labels entfernt, die vom Inhalt ablenkten:

  • Kicker-Labels über Überschriften: Unsere Leistungen, Unsere Standorte, Das Translogistik-Prinzip, Karriere;
  • Scroll-Hinweise: Scroll to explore / Deutschland Fokus, Scroll für Deutschland (Plakette auf der Karte), Öffne die Prinzipien →;
  • interne Labels des FAQ-Konzepts: BLOCK, FAQ Modul • Brighter Style;
  • Demo-Platzhalter der Schlüssel-Szene: Scroll, Zurück.

Sinnvolle Labels blieben erhalten, das waren keine reinen Hinweistexte: Team & Vertrauen, Aktuell, Kurze Wege, Alle Hubs, die Statuszeile der Karte und die Tags auf den Leistungskarten.

In einem zweiten Durchgang auf der Startseite entfernt:

  • FAQ / Häufige Fragen über der FAQ-Überschrift sowie die Fußzeile der Sektion Translogistik • 9 Standorte • 450+ Monteure • Bundesweit im Einsatz;
  • «Mehr Details →» in den drei Leistungskarten (.service__foot) — dahinter stand kein Link. Das CSS .service__foot* blieb erhalten: es wird von unsere-mission.html genutzt, wo diese Karten tatsächlich zu Links wurden (siehe Punkt 6b).

In einem dritten Durchgang — die Reste der FAQ-Kachelmechanik: der Hinweis Wähle 01 / 02 / 03 / 04, die Untertexte in den Kacheln und die Buttons Ansehen. Fragen klappen jetzt per Akkordeon auf, ein «was ist klickbar» muss nicht mehr erklärt werden.

Die Klasse .eyebrow in components.css blieb — für künftige Blöcke.


2b. Hero und Partnerleiste auf Mobilgeräten

.hero--bg (nur index.html, unter 1024px):

  • .hero__visual wird zu position: absolute; inset: 0 — das Foto legt sich also über die gesamte Fläche der Sektion und fungiert als deren Hintergrund; .hero__copy liegt darüber (z-index: 1);
  • .hero__frame::before (rote Unterlegung) ist deaktiviert, ::after erhält einen kräftigen Scrim — Titel und Lead liegen direkt auf dem Foto;
  • .hero__badge (Bewertung «Trusted by Küchenhäuser • 4.97/5») ist ausgeblendet: sie doppelte die Zahlen aus .hero__stats und verlängerte den ersten Bildschirm.

Warum der Hintergrund über ein <img> läuft und nicht über CSS background-image: Beim Bild bleiben srcset/sizes erhalten (Smartphone lädt 800w statt 1200w), und das ist genau eine Anfrage. Mit separatem background-image würde das Bild doppelt geladen — ein per display: none verstecktes <img> lädt der Browser trotzdem.

Am Desktop wurde nichts verändert: dort gibt es weiterhin zwei Spalten mit gerahmtem Foto und Bewertung. ueber-uns.html nutzt dasselbe Hero-Markup ohne .hero--bg, deshalb bleiben die gemeinsamen .hero__*-Regeln erhalten.

.logo-bar: height: 72px + overflow: hidden wurden durch min-height + padding-block ersetzt, beide Zeilen brechen um (flex-wrap) und werden zentriert, die Abstände sind adaptiv (clamp). Die Regeln span:nth-child(n + 4/5) { display: none } wurden entfernt — auf schmalen Bildschirmen müssen Marken nicht mehr versteckt werden, sie rutschen einfach in die zweite Zeile.


3. Sektion Standorte — Funktionsweise

Gleiche Höhe von Karte und Liste

Das Raster .locations besteht aus zwei Spalten und zwei Zeilen:

"search   listhead"   ← Zeile auto: Suche | «Alle Hubs» + Zähler
"map      list"       ← Zeile 1fr: Karte | scrollende Liste

Am Desktop bekommt das gesamte Raster eine feste Höhe clamp(600px, 78vh, 800px) — das ist der einzige Weg, «Karte genauso hoch wie die Liste» zu garantieren. Die Liste scrollt in sich selbst, die Karte streckt sich über die ganze Zeile. Auf Mobilgeräten zerfällt das Raster in eine Spalte, die Karte bekommt feste 460px, und der interne Scroll der Liste wird deaktiviert (overflow: visible + touch-action: pan-y auf .location): der Finger scrollt die Seite statt nach der schmalen Scroll-Spur neben den Plaketten zu suchen.

Kartenstart

assets/js/locations.js initialisiert die Karte sofort auf Deutschland und sofort interaktiv:

  1. germanyView() ist keine Konstante, sondern ein fitBounds über alle 9 Filialen (map.getBoundsZoom), sodass sich der Kartenausschnitt an den Container anpasst. Die Berechnung läuft erst nach L.map(): davor hat der Container keine Maße.
  2. Die Statuszeile zeigt sofort «Deutschland 9 Hubs», 9 Marker und 9 Listenzeilen erscheinen nacheinander (55 ms Abstand).
  3. resize (auch bei Drehung des Smartphones) berechnet den Ausschnitt neu — aber nur, wenn die Kamera nicht auf einer ausgewählten Filiale steht (viewLocked).

Früher gab es hier ein Scroll-Intro «Welt → Deutschland»: die Karte startete bei center [36, 2], zoom 2.4, näherte sich mit dem Scroll-Fortschritt und aktivierte Drag/Zoom erst bei settle(). Das wurde entfernt — man musste bis ans Ende der Sektion scrollen, damit die Karte überhaupt auf Eingaben reagierte. Zusammen damit fielen progress/renderProgress/onScroll/rewind/settle und der scroll-Listener weg.

zoomSnap: 0 bleibt erhalten, für gebrochenen Zoom aus getBoundsZoom. Das Mausrad wird bewusst nicht abgefangen (scrollWheelZoom: false), damit das Scrollen der Seite nicht bricht; gezoomt wird über die Buttons unten rechts oder per Doppelklick.

Die CARTO-Kacheln werden bei devicePixelRatio > 1.3 als @2x angefragt, detectRetina ist deaktiviert: es halbiert tileSize und erhöht zoomOffset, lädt also die vierfache Menge an Kacheln — der teuerste Anfrageblock der Sektion auf dem Smartphone.

Aktive Filiale

Die rote Plakette markiert die ausgewählte Filiale, immer genau eine. Standardmäßig aktiv ist die Filiale mit highlight in DATA (Bremen) — setActive() schaltet nur Klassen um, die Kamera bewegt sich nicht. Wird eine andere Stadt gewählt, kehrt die vorherige Plakette zu Weiß zurück.

Früher war Rot fest an .location--new gebunden, deshalb leuchtete Bremen immer, unabhängig von der Auswahl; die Klasse wurde entfernt, «Neuheit» trägt jetzt nur noch die Pille NEU (.location__flag) und der weiße Pin map-pin--new auf der Karte.

Das Leeren der Suche (Kreuz oder Backspace bis zum leeren Eingabefeld) löst resetView() aus: die Kamera fliegt zurück zur Gesamtansicht Deutschlands mit allen 9 Standorten, das Popup schließt sich, die Hervorhebung springt zurück zur Standard-Filiale.

Bei prefers-reduced-motion: reduce sind alle Kameraflüge sofort, ohne Animation.

Suche mit Dropdown-Liste

combobox über der Karte (Rolle combobox + listbox, aria-activedescendant):

  • filtert nach Name, Region, Notiz und Aliasen (keywords: koeln, cologne, nuernberg, leipzig, frankfurt …);
  • Umlaute werden normalisiert (ä→a, ö→o, ü→u, ß→ss) — funktioniert sowohl mit Köln als auch mit koeln;
  • Treffer werden mit <mark> hervorgehoben;
  • Tastatur: / — Navigation, Enter — auswählen, Esc — schließen;
  • Auswahl einer Filiale: flyTo bis Zoom 9.5, Popup öffnet sich, die Zeile in der Liste wird hervorgehoben und ins Bild gescrollt.

Die Standortdaten liegen an einer einzigen Stelle — im Array DATA am Anfang von locations.js. Eine Filiale hinzufügen = ein Objekt ergänzen (name, lat, lng, region, note, keywords, highlight). Liste, Marker, Suche und Zähler bauen sich von selbst zusammen.

Skyline-Illustrationen in den Buttons

Jeder Standort-Button kann eine Line-Art-Silhouette der Stadt zeigen. Angebunden sind 8 von 9: Hamburg, Bremen, Berlin, Paderborn, Günthersdorf, Köln, Fürth, München. Für Langenselbold gibt es noch keine Zeichnung, dieser Standort hat kein skyline-Feld, der Button sieht aus wie zuvor.

So funktioniert es:

  • die Datei liegt unter assets/img/svg/<Name>.svg;
  • in DATA bekommt die Filiale das Feld skyline: '<Name>' — fehlt es, bleibt der Button unverändert, es gibt also keinen 404;
  • locations.js legt die fertige URL in --skyline ab (encodeURIComponent wegen der Umlaute in Köln.svg / München.svg);
  • CSS nutzt das SVG als Maske, nicht als Bild: die Farbe kommt aus currentColor, die Grafik passt sich also automatisch dem Theme an (schwarz im Light-, weiß im Dark-Mode) und wird auf der roten Plakette der aktiven Filiale weiß.

Zwei verschachtelte Elemente — .location__skyline (Auflösung nach links, damit der Hintergrund unter Nummer und Namen sauber bleibt) und .location__skyline-art (die Stadt selbst). Getrennt, weil jedes genau eine Maske braucht: mask-composite verhält sich browserabhängig unterschiedlich.

Die Transparenz setzt sich aus drei unabhängigen Faktoren zusammen — alle liegen auf .location, damit sie nicht um Spezifität konkurrieren:

Variable Wer setzt sie Wofür
--skyline-op / --skyline-op-hover Dark-Theme ein weißer Strich auf Schwarz «leuchtet» stärker
--skyline-boost skylineBoost in DATA eine bestimmte Zeichnung abdämpfen (derzeit ungenutzt)
--skyline-mob @media (max-width: 560px) in der schmalen Spalte braucht der Text die ganze Breite

Ergebnis: opacity: calc(<Basiswert des Zustands> * var(--skyline-mul)). Würden stattdessen Selektoren ergänzt, würde das Dark-Theme .is-active mit seiner roten Plakette überschreiben.

Auch die Geometrie sind Variablen, beide werden aus DATA gespeist:

Variable Feld in DATA Standardwert
--skyline-h skylineHeight 82% der Button-Höhe
--skyline-x skylineX calc(100% + 24px) — Verschiebung nach rechts, weg vom Text

Die Verschiebung nach rechts schneidet rechts einen schmalen Streifen ab: bei allen Panoramen steht dort gewöhnliche Bebauung, die markanten Bauwerke stehen in der Mitte. Auflösung nach links — transparent 22% → #000 70%: die detaillierte Architektur ist dichter als die konturierte Silhouette von Hamburg, deshalb ist der Freiraum unter Name und Notiz größer, als für Hamburg allein nötig wäre.

Der Kreis hat eine deckende Füllung var(--bg) bekommen — darunter verläuft die Skyline, ohne Hintergrund ging der Pfeil in den Linien unter. Bei Hover bekommt der Kreis eine rote Umrandung. var(--surface) passt nicht: im Dark-Theme ist es halbtransparent.

⚠️ Die URL in --skyline muss unbedingt absolut sein (new URL(..., document.baseURI)). Ein relativer Pfad innerhalb einer Custom Property wird nicht relativ zum Dokument aufgelöst, sondern relativ zum Stylesheet, das den var() einsetzt — also relativ zu assets/css/. Aus assets/img/... würde assets/css/assets/img/... und ein 404, ohne eine einzige Fehlermeldung in der Konsole.

Eine neue Stadt hinzufügen:

  1. Die Quelldatei nach assets/img/svg/_source/<Name>.svg legen.
  2. Durch den Optimierer laufen lassen (siehe unten) nach assets/img/svg/<Name>.svg.
  3. skyline: '<Name>' im passenden DATA-Objekt ergänzen.

Der Dateiname entspricht dem Filialnamen in DATA. Umlaute sind kein Problem (Köln.svg, München.svg, Fürth.svg) — die URL wird in JS kodiert.

Das Seitenverhältnis liegt bei etwa 6.6 : 1 (Panorama): darauf sind mask-size: auto 82% und die Auflösung nach links abgestimmt. Ein echter Vektor wird unverändert eingebunden, der Optimierer wird dafür nicht gebraucht.

Der Optimierer

Die gelieferten SVGs sind Exporte eines Vektorisierers: dasselbe Raster-PNG ist doppelt eingebettet (base64), einmal als Luminanz-Maske, einmal als schwarze Fläche. Daher 0,74,8 MB pro Datei. Das Skript holt die Masken-Ebene heraus, wandelt Helligkeit in Alpha um, schneidet auf den Inhalt zu und baut daraus ein einziges leichtes SVG:

scripts/optimize-skyline.ps1 `
  -Source assets/img/svg/_source/Berlin.svg `
  -Target assets/img/svg/Berlin.svg `
  -MaxWidth 900 -Dilate 0

Ergebnis: 18 MB → 455 KB für neun Dateien.

Die Maske wird über das <mask>-Element ermittelt, nicht über die Größe des base64-Blocks: bei Köln, München und Fürth ist die schwarze Fläche größer als die Maske (3,03,9 MB gegenüber 0,81,0 MB), und die Heuristik «größter Block» hätte die falsche Ebene gewählt — das Ergebnis wäre ein leeres Bild gewesen.

-Dilate <r> verdickt den Strich per Maximum-Filter. Der Bedarf ist unterschiedlich — -MaxWidth wird auf die doppelte Anzeigegröße abgestimmt, dann ergibt -Dilate 1 etwa +1px fertigen Strich:

Zeichnung MaxWidth Dilate Warum
Hamburg (einkonturige Silhouette) 900 1 Linie mit 0,15 % der Breite, ohne Verdickung zerfällt sie auf Button-Größe zu Rauschen
Bremen 900 1 entstand, als Bremens Plakette dauerhaft rot war: Weiß auf Rot lässt sich nicht kontrastreicher machen, der dünne Strich war kaum lesbar. Jetzt ist Rot = aktive Filiale, aber die Verdickung stört nicht
Berlin, Köln, München, Paderborn, Fürth 900 0 Fenster und Ornamente verschmelzen bei Verdickung zu einem schwarzen Klumpen
Günthersdorf 440 1 wird doppelt so schmal wie die Panoramen gezeichnet, daher auch die kleinere Quelle

Bei Bremen füllt -Dilate 2 bereits die Kuppel des Universums zu einem Fleck — mit dem Auge in echter Größe prüfen, nicht in der Vollbreiten-Vorschau.

Günthersdorf — ein Sonderfall

Das ist keine Skyline, sondern ein nachgezeichnetes Bodenfoto der Fläche: Seitenverhältnis 3.5 : 1 statt 6.5. Bei gleicher Höhe wird es halb so schmal wie die Panoramen, deshalb:

  • skylineHeight: '78%' — größer als bei den anderen, sonst geht sie unter;
  • skylineX: '100%'ohne Verschiebung nach rechts: die schmale Zeichnung würde sonst unter den Kreis geraten;
  • gebaut mit -Dilate 1 — der Strich passt zu den übrigen im Ton.

Sollte ein echtes Panorama entstehen, können beide Felder entfernt werden.

Stilunterschied

Hamburg ist mit einer Kontur ohne Details gezeichnet, die übrigen 7 sind detaillierte Architektur mit Fenstern und Ornamenten. In der Liste fällt das auf: Hamburg wirkt leichter und leerer. Damit das Set einheitlich wird, bräuchte es entweder Hamburg im Detailstil oder die anderen im Konturstil — das ist eine Frage der Quelldateien, per Code lässt sich das nicht lösen.


3b. Scroll-Szene mit dem Schraubenschlüssel

assets/js/wrench.js + .wrench-* in sections.css.

Die Szene ist 300vh hoch (240vh auf Mobilgeräten), darin steckt ein Sticky-Frame über 100vh. p = 0…1 wird aus dem Fortschritt durch die Szene berechnet und auf drei Phasen aufgeteilt:

p Was passiert
0 → 0,08 der Schlüssel fliegt von unten ein: translateY 75% → 30%, scale .35 → .9
0,08 → 0,55 er dreht sich im Bild: rotate -28° → -2°, scale mit Sinuskurve
0,55 → 1 er zieht nach oben ab und löst sich auf

Überschrift und Raster 0104 laufen über eigene Rampen.

Pre-Roll. Während die Sektion noch von unten hereinfährt, ist p noch 0 — und das erste Bild der Szene war ein leerer schwarzer Bildschirm. Deshalb läuft die Überschrift über einen eigenen Einfahr-Fortschritt (pre), und sobald das Bild einrastet, steht der Schriftzug bereits an seinem Platz. Schlüssel und Raster bleiben am Sticky-Fortschritt.

Bei prefers-reduced-motion: reduce wird das Skript gar nicht erst geladen: CSS staucht die Szene zu einer normalen Sektion und zeigt ein statisches, lesbares Bild.

3c. FAQ — ein gewöhnliches Akkordeon in roter Karte

Ein eigenes Skript hat das FAQ nicht mehr: es läuft über das allgemeine assets/js/accordion.js, die Stile liegen in .faq-head / .faq-accordion in sections.css (nur die Palette «Weiß auf Rot» ist speziell, Mechanik und Markup sind dieselben .accordion__*).

Was es vorher gab und warum es ersetzt wurde. Das ursprüngliche Konzept (Translogistik-Faq-Brighter-Style.html) bot 4 Umschalt-Kacheln 0104 und ein großes «Display» für die Antwort darüber: ein Klick auf die Kachel tauschte Titel und Text im Display aus. Zusammen mit den Untertexten in den Kacheln, den Buttons «Ansehen», dem Hinweis «Wähle 01 / 02 / 03 / 04» und min-height: 180px bei den Kacheln nahm der Block auf dem Smartphone fast drei Bildschirme ein — bei vier Fragen. All das ist entfernt; geblieben sind die Fragestellung, das «Plus» und die aufklappende Antwort.

Die Gruppe trägt data-accordion-scroll: nach dem Aufklappen zieht accordion.js den Punkt sanft ins Bild (scrollIntoView, block: 'start', Reserve für den fixierten Header über scroll-margin-top an .accordion__item). 'start' statt 'nearest': eine lange Antwort ist höher als der Bildschirm, und 'nearest' würde ihr unteres Ende heranziehen — der Leser sähe das Ende des Texts statt des Anfangs. Ist der Punkt ohnehin komplett sichtbar (Desktop), gibt es keinen Scroll.

Wie bisher liegen die Antworttexte im Markup, nicht in einem JS-Array: Fragen und Antworten werden von Suchmaschinen indexiert, und ohne JS klappt <noscript> alle Panels sofort auf und entfernt die «Plus»-Zeichen.

Das «Plus»-Symbol in allen Akkordeons

.accordion__icon zeichnet das «Plus» mit zwei Balken (::before / ::after), nicht als Textzeichen +. Dafür gibt es zwei Gründe:

  1. Der optische Mittelpunkt des Glyphen liegt auf der mathematischen Achse der Schrift, nicht in der Mitte seiner Zeilenbox. Bei jeder flex-/grid-Zentrierung saß das + etwa 1,5px unterhalb der Kreismitte.
  2. Früher tauschte accordion.js das Zeichen gegen ×, und CSS drehte den Kreis zusätzlich um 45°. Das gedrehte × sah wieder wie ein + aus — der geöffnete Zustand war optisch nicht vom geschlossenen zu unterscheiden. Jetzt macht die Drehung ehrlich aus dem + ein ×, und das Skript rührt den Icon-Text gar nicht mehr an.

Mit demselben Kniff ist auch das Schließen-Kreuz des Modals gezeichnet (.modal__x).

4. Theme (light / dark / system)

Von Anfang an eingeplant, wie gewünscht:

  • <html data-theme="light|dark"> — wird per Inline-Skript im <head> vor der ersten Darstellung aufgelöst, deshalb gibt es kein Aufblitzen von Weiß.
  • Drei Modi: system (Standard) → lightdark, der Button im Header schaltet durch; Icon ◐ / ☀ / ☾.
  • Im Modus system folgt das Theme dem Betriebssystem live — über den Listener matchMedia('(prefers-color-scheme: dark)'). Schaltet der Nutzer das Theme in Windows/macOS um, färbt sich die Website ohne Neuladen um.
  • Die Wahl wird in localStorage gespeichert (tl-theme); system = Key entfernt.
  • Das Event tl:themechange ({ mode, resolved }) ist ein Erweiterungspunkt. Wird bereits genutzt: die Karte schaltet die CARTO-Kacheln zwischen light_all und dark_all um.

Alle Farben laufen ausschließlich über Tokens in tokens.css. Im CSS braucht es genau einen Dark-Block :root[data-theme='dark'], weil das Auflösen JS übernimmt.

Separat gibt es die Klasse .on-dark — für Sektionen, die immer dunkel sind (Hero, Prinzip, Footer). Sie überschreibt lokal dieselben Tokens, sodass die Komponenten darin (.btn, .card, .eyebrow) nichts vom Kontext wissen müssen und überall gleich funktionieren.


5. Was es sonst noch gibt

  • Mobilmenü (Burger → Drawer), Esc schließt es.
  • Scroll-Spy: der aktive Navigationspunkt wird rot unterstrichen.
  • [data-reveal] + data-reveal-delay — Einblenden von Blöcken beim Scrollen über IntersectionObserver.
  • prefers-reduced-motion wird respektiert: Animationen und Kameraflüge der Karte werden deaktiviert.
  • Degradation ohne JS: <noscript> macht versteckte Blöcke wieder sichtbar; lädt Leaflet nicht, wird die Karte durch Text ersetzt, während die Standortliste voll funktionsfähig bleibt.
  • Die Plakette «Aktuell: Bremen Neueröffnung» (.extra-flash) ist komplett ein <a>-Link zu nachrichten.html, nicht nur ein klickbarer Pfeil in einem <div>.

Laden von Schriften und Karte

Eine Anfrage statt sechs: Inter:wght@400..900 — variabler Schriftschnitt, Instrument+Serif:ital@1 — nur kursiv (.serif und .marquee-symbol verwenden keinen geraden Schnitt). Die URL ist auf allen Seiten identisch — sonst würde der Wechsel index → karriere einen zweiten Dateisatz am Cache vorbei laden.

index.html öffnet vorab Verbindungen zu den Hosts, die nur die Karte braucht: preconnect zu unpkg.com (CSS + JS von Leaflet) und zu a.basemaps.cartocdn.com, dns-prefetch zu b./c. — ohne crossorigin, alle drei Anfragearten laufen ohne CORS.


6. Die Seite «Über uns» — ueber-uns.html

Die zweite Seite der Website. Das Grundgerüst (Header, Hero, Logo-Leiste, Footer, Block «Bereit, mit uns zu wachsen?») wurde ohne CSS-Änderung von der Startseite übernommen — neue Stile brauchten nur zwei Sektionen, sie liegen in einem eigenen about.css.

Block Quelle Was geändert wurde
Header / Footer / Logo-Leiste index.html Links zu Sektionen der Startseite führen über index.html#…; Punkt «Über uns» mit aria-current="page" ergänzt
Hero index.html Gleiches Markup, Text passend zu «Über uns»: «Wir sind Translogistik. Und wir liefern mehr als nur Küchen.», Zahlen → 2014 / 450+ / 9, Buttons → Werte / Geschichte
«Von der Idee zum deutschlandweiten Netzwerk.» — Timeline 2014 → 2026 Gestaltung: Translogistik-Codex-V2-Mit.html, Überschrift: translogistik-template.html Tailwind + React → semantisches HTML + timeline.js. Mini-Kicker «Unsere Geschichte» und Hinweis «Scroll • Timeline» entfernt
«Werte, die uns stark machen.» — 7 Werte Gestaltung: translogistik-template.html, Block «Warum unsere Partner mit uns arbeiten» Dunkle Sektion und Kachel «Icon + Titel + Text» beibehalten, Inhalt durch 7 Werte ersetzt; Glyphen ◆ ◇ ▣ ◈ durch Stroke-SVGs ersetzt (24×24, currentColor)
«Bereit, mit uns zu wachsen?» (Collage + 3 Bewertungen + Buttons) index.html Eins zu eins übernommen, Buttons führen zu index.html#karriere / #standorte

Timeline — timeline.js

<div data-timeline>
  <nav><button data-timeline-year aria-controls="ID">2014</button></nav>
  <div><article data-timeline-item id="ID"></article></div>
</div>
  • Jahre und Meilensteine sind über ihre Reihenfolge im DOM verknüpft: der n-te Button ↔ der n-te Meilenstein. Ein Meilenstein hinzufügen = einen Button und ein <article> ergänzen, das JS weiß nichts von Jahreszahlen.
  • Der aktive Meilenstein wird über die Nähe der Blockmitte zur Bildschirmmitte bestimmt (scroll + requestAnimationFrame), nicht über intersectionRatio: auf dem Smartphone sind Meilensteine kürzer als der Beobachtungsbereich, sonst wären mehrere gleichzeitig im Bild.
  • Der Scroll wird nur beobachtet, solange die Sektion im Bild ist — Gate über IntersectionObserver mit rootMargin: 20%.
  • Ein Klick auf ein Jahr zieht den Meilenstein in die Mitte (scrollIntoView), Pfeile / Home / End schalten die Jahre per Tastatur um.
  • Die Rollen tablist / tab / tabpanel werden nicht verwendet: die Meilensteine sind alle gleichzeitig sichtbar, das ist eine Sprungmarken-Navigation, keine Tabs. Das aktive Jahr trägt aria-current, alle Buttons bleiben in der Tab-Reihenfolge.
  • Bei prefers-reduced-motion: reduce und ohne IntersectionObserver zeigt das Skript einfach alle Meilensteine mit voller Deckkraft. Ohne JS macht <noscript> dasselbe.

Sticky-Jahre und Grid

Die Jahresspalte funktioniert mit position: sticky, weil bei einem Grid-Element der Container für das Anheften seine Grid-Area ist, und die Zeilenhöhe von der Nachbarspalte mit den Meilensteinen bestimmt wird. Auf Mobilgeräten ist das Raster einspaltig, das Jahresband hat seine eigene Zeile in Eigenhöhe — nichts, woran es haften könnte, deshalb wird daraus ein horizontales Band über den Meilensteinen. Kein Bug, sondern Folge des Modells: eine Reparatur müsste die Jahre aus dem Raster herauslösen.

Zwei Korrekturen an diesem Band:

  • Abschnitt links. Beim aktiven Jahr skaliert der Punkt auf scale(1.8), und bei dem ersten (2014) steht er ganz am Anfang des Scroll-Containers — die linke Hälfte wurde vom Rand des overflow-x abgeschnitten. Das Band bekam padding-inline: 8px 4px und scroll-padding-inline: 8px, der gesamte Block ist also etwas nach rechts verschoben.

  • Scrollbalken. timeline.js baut unter dem Band .timeline__rail-bar mit einem Schieber: die Breite entspricht dem sichtbaren Anteil, die Position scrollLeft. Ohne ihn ist im statischen Bild nicht erkennbar, dass rechts noch weitere Jahre folgen. Die native Scrollbar passte nicht: auf Touch-Geräten liegt sie als Overlay darüber und erscheint nur während der Geste, deshalb wird sie ausgeblendet (scrollbar-width: none). Der Schieber lässt sich ziehen (Pointer Events + setPointerCapture), passt das Band komplett hinein, versteckt sich das Element von selbst.

    Der Schieber wird als Geschwister-Element des Bandes eingefügt, nicht als Wrapper: ein Wrapper würde position: sticky am Desktop brechen (es gäbe innerhalb eines Blocks in Eigenhöhe des Bandes nichts, woran man haften könnte). Deshalb wird er eine eigene Rasterzelle, und ein negativer margin-top in Höhe von --geschichte-gap zieht ihn direkt unter die Jahre. Am Desktop ist er display: none.

Das aktive Jahr wird zusätzlich im Band zentriert (revealYear) — dabei wird rail.scrollLeft bewegt, nicht scrollIntoView: das würde den vertikalen Seitenscroll gleichzeitig mit dem Scroll des Nutzers mitziehen.

Werte-Raster ohne «Waise»

7 Kacheln würden bei 2 und bei 3 Spalten eine unvollständige letzte Zeile hinterlassen. Die siebte (Ehrlichkeit) trägt .wert--wide: grid-column: 1 / -1 und horizontales Layout — die Zeile schließt bei jedem Breakpoint.

Navigation zwischen den Seiten

  • Header und Mobilmenü sind auf beiden Seiten identisch (7 Punkte): Home / Über uns / Prinzipien / Standorte / Leistungen / Team / FAQ.
  • .is-active steuert das Scroll-Spy nach Sektionen der aktuellen Seite, [aria-current="page"] markiert dauerhaft eine einzelne Seite. Das Scroll-Spy stört das nicht: es fasst nur .is-active an.
  • 7 Punkte + Logo + CTA passten nicht in 1024px, deshalb ist bei 10241199px der Navigationsabstand auf 20px gestaucht und der Zusatz «Service GmbH» ausgeblendet.
  • Die Sektionen von «Über uns» sind über den Footer der Startseite erreichbar (ueber-uns.html#geschichte, #werte), deshalb mussten sie nicht in den Header und dessen Einheitlichkeit zwischen den Seiten brechen.

6b. Die Seite «Unsere Mission» — unsere-mission.html

Stile — pages.css (plus die allgemeinen sections.css / karriere.css).

Benefits — «Eine neue Küche ist eine Investition ins Leben.»

Das Layout wurde vom «Zickzack» auf linksbündige Ausrichtung umgestellt. Vorher: eine mittige Linie (left: 50% bei ≥768px), der Text abwechselnd links und rechts davon. Bei langen Überschriften überlappten sich die Spalten — die Linie schnitt Text und Icons.

Jetzt ein einziges Layout für alle Größen:

[Icon 72px] [Text mit padding-left]
      ↑
   Linie left: 36px — genau mittig in der Icon-Spalte

grid-column / grid-row sind explizit gesetzt, die Reihenfolge der Elemente im Markup hat also keinen Einfluss auf die Anordnung; .benefits-timeline__spacer (leere Zellen des Zickzacks) wurden aus dem Markup entfernt. margin-inline: 0 beim Viewport statt auto: das Band beginnt an derselben Führungslinie wie die Sektionsüberschrift. Unter 768px werden Icon-Spalte und Abstand gestaucht (56px / 20px), sonst blieben der Überschrift weniger als 250px. Das Kicker-Label «Benefits für Kunden, Partner & Team» über der Überschrift wurde entfernt — es trug nichts bei, was Überschrift und Lead nicht ohnehin sagten.

Nebeneffekt der Linksausrichtung: die Icons rückten dicht an den linken Rand der Container, und der Box-Shadow des aktiven Kreises (is-active/ is-passed, rotes Leuchten) wurde links abgeschnitten — die «Plakette» sah aus, als fehle ihr eine Seite. Der Beschnitt geschah gleich auf drei Ebenen:

  • .benefits-timeline__viewportoverflow: hidden wird für die vertikale Sticky-Maske gebraucht (Fade oben/unten beim Scrollen);
  • .benefits-timeline__sticky — der Wrapper der GESAMTEN Sektion (Überschriftentext + Viewport), stand ebenfalls auf overflow: hidden, obwohl der vertikale Beschnitt nur ihn selbst betrifft — den horizontalen schnitt er als Nebeneffekt mit ab;
  • body (base.css) — globales overflow-x: hidden, Schutz der gesamten Website vor versehentlichem horizontalem Scroll. Diese Kante schneidet JEDEN Box-Shadow ab, der darüber hinausragt, unabhängig vom Overflow der lokalen Sektions-Wrapper — sie ließ sich nicht einfach «öffnen», ohne den Schutz der ganzen Website aufzuheben.

Die ersten beiden Ebenen wurden über eine unabhängige Achse statt einem pauschalen overflow geöffnet (overflow-x: visible; overflow-y: hidden; — ein Box-Shadow erzeugt kein scrollbares Overflow, aktiviert also keinen horizontalen Scroll). Das reichte aber nicht aus: auf schmalen Bildschirmen bleiben zwischen Icon und echtem Seitenrand nur 2436px (Container-Padding), und der ursprüngliche Glow-Blur betrug 24px — er stieß direkt an die Schutzkante von body.

Da die äußere Schutzkante nicht angetastet werden durfte und der Blur selbst nicht künstlich verkleinert werden sollte (das Leuchten würde kleiner wirken), wurde stattdessen der ganze Block nach rechts verschoben und bekam so eigenen Abstand zum Rand:

.benefits-timeline__track { --benefits-inset: 16px; }
.benefits-timeline__step  { padding-left: var(--benefits-inset); }
.benefits-timeline__line  { left: calc(var(--benefits-inset) + 36px); } /* war 36px */

--benefits-inset liegt auf .benefits-timeline__track, nicht auf __step: .benefits-timeline__line ist ein Geschwisterelement der Steps (beide liegen direkt im Track), nicht deren Nachfahre, und könnte die Variable nicht vom Step übernehmen — es braucht einen gemeinsamen Elternteil, von dem der Wert nach unten an beide vererbt wird. Linie und Icon-Spalte sind um denselben Betrag verschoben, die Zentrierung (Linie genau mittig im Kreis) bricht daher bei keinem Breakpoint (auf Mobilgeräten 28px statt 36px, aber --benefits-inset bleibt gleich).

Mit diesem Abstand wird selbst beim engsten Breakpoint (7681023px, 44px bis zur body-Kante) der ursprüngliche box-shadow: 0 8px 24px var(--accent-glow) nicht mehr beschnitten — der Blur wurde in Originalgröße zurückgesetzt, verkleinert werden musste nichts.

Challenges — «Herausforderungen wechseln.»

  • Der Autowechsel läuft strikt der Reihe nach 1 → 2 → 3 → 4 → 1. In der Quelle (import/Herausanforgerungen.html) wurde der nächste Punkt zufällig gewählt: Nutzer konnten nicht erkennen, wie viele Punkte es insgesamt gibt und ob sie alle gesehen hatten.
  • Auf dem Smartphone ist das kapselförmige Umschalt-Band komplett ausgeblendet (.challenges__track-wrap { display: none }): vertikal nahm es einen halben Bildschirm ein und doppelte die Überschriften der Karten selbst. Die Punkte blättert die Autoplay-Funktion durch. Das Markup bleibt bestehen — daraus werden Indizes und Prozentwerte gelesen.
  • Ist Bewegung deaktiviert (prefers-reduced-motion), gibt es keine Autoplay-Funktion, deshalb werden auf dem Smartphone alle vier Punkte als Liste mit Trennlinien gezeigt — sonst wäre nur der erste sichtbar.

Die drei Karten «Was wir uns selbst abverlangen» sind jetzt <a class="service">, die gesamte Fläche ist klickbar: «Mehr Details →» versprach schon vorher einen Link, klickbar war aber nur das Wort selbst. Sie führen zu «Was du bekommst» auf karriere.html#benefits. Die Plakette «Offene Stellen für Küchenmonteure» ist komplett ein Link zu karriere.html#stellen (vorher war nur der Pfeil klickbar). Der Hover-Zustand für a.service liegt in sections.css.


6c. Video-Sektion «Ein Tag auf Montage» — karriere.html

.montage__grid wurde von zentriertem Flex auf Grid umgestellt:

grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 440px));
justify-content: start;
gap: 56px;   /* 40px ≤900px, 24px ≤640px */

Das bringt: Das erste Video liegt auf derselben linken Führungslinie wie der Text von .section-head darüber (vorher war es zentriert, und das Raster «hing» relativ zur Überschrift); ein dritter Clip erfordert keine CSS-Anpassung — die Spalte entsteht von selbst, und auf schmalen Bildschirmen rutschen die Clips in eine Spalte. max-width bei .montage__card-wrap wurde entfernt — die Breite bestimmt die Rasterspur. Der Gap ist bewusst groß (56px) — neben Videos bis 780px Höhe wirkten 32px wie «verklebte» Karten statt wie ein Abstand.

Beim Play-Icon wurde margin-left: 2px entfernt — der «optische Mittelpunkt» des Dreiecks wirkte wie ein Verlaufsfehler, der Container zentriert ohnehin über Flex.


6d. Formulare: Sende-Bestätigung

assets/js/form-modal.js + .modal in components.css. Genutzt von kontakt.html und bewerbung.html.

Vorher: ein Inline-Block .form__done / .wizard__done unter dem Button — bei einem langen Formular musste man ihn erst herunterscrollen, um ihn zu finden. Jetzt: ein modales Fenster über der Seite (role="dialog", aria-modal, Scroll-Sperre über body.modal-open).

  • Schließen: Button Schließen, Kreuz, Klick auf die Unterlage, Esc.
  • Nach dem Schließen wird das Formular geleert (resetForm: Werte + Klassen .is-invalid + aria-invalid), damit es erneut abgeschickt werden kann; der Wizard springt zusätzlich zurück auf den ersten Schritt.
  • Der Fokus geht beim Öffnen auf den Schließen-Button, beim Schließen kehrt er dorthin zurück, wo er herkam.

Nebenbei wurde die Validierung der Einwilligung repariert. Die Checkbox «Ich bin damit einverstanden…» trägt nicht die Klasse .field__control (sie hat ihr eigenes Markup .field--check), und die Skripte sammelten Steuerelemente nur über diese Klasse. Das Formular steht mit novalidate, der Browser prüfte also ebenfalls nicht — die Checkbox war nur auf dem Papier verpflichtend, und die Bewerbung ließ sich ohne Einwilligung zur Datenverarbeitung absenden. Jetzt greift der Selektor auf [data-field] input[type="checkbox"] zu, und .field--check.is-invalid input bekommt eine rote Umrandung.


7. Mini-Block «Krieger Gruppe» — #gruppe auf der Startseite

Steht zwischen leistungen und team: zuerst was wir tun, dann wozu wir gehören, dann das Team. Ein Band auf --bg-alt mit Linien oben und unten — so liest sich der Block als eigener Mini-Block, nicht als weitere große Sektion.

Quelle der Gestaltung ist import/krieger-gruppe-filter-karussell.html. Was bei der Anpassung an die Website geändert wurde:

Im Konzept Auf der Website Warum
--panel:#f2f1ee, --highlight:#eaeefb (blau) var(--surface), Hover → var(--accent) Blau gehört nicht zur Palette; der Block funktioniert jetzt im Light- und Dark-Theme ohne zweiten Farbsatz
h1 Georgia 48px .display Inter 800 + .serif italic Typografie der Website: Inter + Instrument Serif
Marken im JS-Array brands[] Karten im Markup, data-cat an jeder Karte Wie die FAQ-Antworten: indexierbar und funktionsfähig ohne JS
Icons als Strings in JS <symbol>-Sprite im Markup + <use> Fünf Formen für 9 Karten ohne Wiederholung
Pille pro Marke, Klick hob eine Karte hervor Pillen als Kategorien (Alle + Bereiche), Karussell wird gefiltert, Zähler zeigt das Ergebnis Der Block heißt «filter-karussell», einen Filter gab es aber nicht
.faded (transparent, aber weiter im Fluss) hidden (verlassen den Fluss) Sonst müsste man durch den Filter hindurch Geister-Karten wegscrollen, und der Zähler würde falsch angezeigt
Auflösung an den Rändern #fff linear-gradient(90deg, var(--bg-alt), transparent) Im Dark-Theme wäre der weiße Schleier sichtbar gewesen
mousedown/mousemove pointerdown/pointermove, nur bei pointerType === 'mouse' Touch-Scroll funktioniert ohnehin über overflow-x
Pfeilschritt als Konstante 280px card.getBoundingClientRect().width + columnGap verrutscht nicht bei geänderter Kartenbreite

Die erste Karte — rot, «Das sind wir» — ist kein Link, sondern ein div, um zu zeigen, wo Translogistik innerhalb der Gruppe steht. Sollte Translogistik laut Firmenstruktur nicht als Marke der Gruppe geführt werden, den Block .gruppe__card--self entfernen, die übrigen 8 Marken und der Zähler passen sich von selbst an.

Die Marken-Links führen zu externen Websites (target="_blank" rel="noopener noreferrer"). Ohne JS gibt es keine Pillen, aber das Karussell lässt sich mit Finger und Mausrad durchblättern; <noscript> entfernt Pfeile und Randauflösung, weil sie ohne Skript nicht aktualisiert werden.


8. Fotografien

assets/img/*.jpg — Archiv des Fotoshootings (11 Bilder, 69 MB, ~6000 px). An die Website ist es nicht angebunden: im Markup steht das Web-Set aus assets/img/photos/.

scripts/optimize-photos.ps1              # 400 / 800 / 1200 / 1600 px, q82
scripts/optimize-photos.ps1 -Force       # fertige Dateien überschreiben

69,5 MB → 4,0 MB. Der Kameradateiname wird auf einen Slug «Datum-Bild» verkürzt: 2025-08-21 - Translogistik Günthersdorf … -49.jpg2025-08-21-49-800.jpg. Keine Abhängigkeiten — System.Drawing aus dem .NET Framework.

Bildausschnitt — --pos

Die Aufnahmen sind durchgehend im Format 3:2 (einzige Ausnahme 2025-09-02-2, Hochformat 2:3), die Slots sind teils vertikal: Hero-Rahmen, Collage-Kacheln 100×140…136×196, runde Avatare 28 px. Ein zentraler object-fit: cover schneidet Personen am Gesicht ab.

Deshalb wird bei allen Foto-Slots object-position aus einer Custom Property gelesen:

<img style="--pos: 62% 26%" >
.collage img { object-position: var(--pos, 50% 30%); }

Der Wert liegt direkt beim Bild, weil er eine Eigenschaft des Ausschnitts ist, nicht des Blocks: dasselbe Foto braucht im Hero und im Avatar einen unterschiedlichen Fokus. Die Hooks stehen auf .hero__photo, .collage img, .service__media img, .cta__media img, .avatar-row img, .gruppe__photo img; ohne --pos greift ein sinnvoller Standardwert.

Was wohin gewandert ist

Slot Bild Warum
Hero der Startseite 2025-09-02-2 Das einzige Hochformat 2:3 — nur es wird im vertikalen Rahmen nicht beschnitten; im Hintergrund ist der Fuhrpark zu sehen
Hero «Über uns» 2025-08-21-49 Gruppenaufnahme im Lager — «Wir sind Translogistik»
Fachgerechter Transport 2025-09-02-18 Umwickeln mit Stretchfolie im Lkw = wortwörtlich Ladungssicherung
Präzise Montage 2025-09-02-37 Ein verpacktes Element wird beim Kunden hereingetragen
Komplexe Logistikprozesse 2025-08-21-49 Lager, Rollcontainer, Team
Krieger Gruppe 2025-09-02-15 Translogistik-Jacke + Lkw «Montageteam Höffner» — bildlicher Beleg der Verbindung zur Gruppe
Abschließendes CTA 2025-08-21-56 Zwei lachende Kollegen — stärker für die Anwerbung als ein Lkw
Collage Team (9 Kacheln) alle 11 außer dem Porträt Menschen, Lager, Montage, Monteurcasino
Hero-Avatare -57, 09-02-15, -52 Gesichter näher an der Bildmitte, weniger betroffen vom runden Zuschnitt

Was bewusst von Unsplash geblieben ist: die drei Avatare bei den Bewertungen (Markus R., Sandra L., Thomas K.). Das sind Kunden, keine Mitarbeiter — dort Gesichter von Monteuren einzusetzen, hieße eigene Leute als fremde Auftraggeber auszugeben. Es braucht echte Fotos — nur zu echten Bewertungen.

Zwei Bildunterschriften wurden dem neuen Foto angepasst, um nicht falsch zu sein:

  • CTA: Fuhrpark • 120+ FahrzeugeTeamgeist • Standort Günthersdorf; die Angabe «120+ Fahrzeuge» ist auf den Tag der Karte «Fachgerechter Transport» gewandert.

9. Weiteres

Aus import/Translogistik-Codex-V2-Mit.html nicht verwendet:

  • «Wir sind Translogistik» (Über uns, 2 Spalten mit Foto — Block «Wer wir sind / Wie wir arbeiten / Was uns antreibt»);
  • Block mit 4 Zahlen (98,2 % / 9 / 450+ / 24h).

Aus translogistik-template.html blieben übrig: «Unsere Gruppe» (Firmenumschalter) und der vierstufige Prozess.