diff --git a/app/components/Navbar.tsx b/app/components/Navbar.tsx
index ae12921..b65d208 100644
--- a/app/components/Navbar.tsx
+++ b/app/components/Navbar.tsx
@@ -9,7 +9,7 @@ import { NewsletterModal } from "./NewsletterModal";
import { useCartFly } from "./CartFly";
const NAVBAR_HEIGHT = 100; // px — matches h-[6.25rem]
-const SCROLL_DURATION = 800; // ms
+const SCROLL_DURATION = 200; // ms
function smoothScrollTo(targetY: number) {
const startY = window.scrollY;
@@ -224,13 +224,13 @@ export function Navbar() {
const focusables = panel?.querySelectorAll
(
'a[href], button:not([disabled])'
);
- focusables?.[0]?.focus();
+ focusables?.[0]?.focus({ preventScroll: true });
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
setMobileOpen(false);
- hamburgerRef.current?.focus();
+ hamburgerRef.current?.focus({ preventScroll: true });
return;
}
if (e.key !== "Tab" || !focusables || focusables.length === 0) return;
@@ -238,10 +238,10 @@ export function Navbar() {
const last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
- last.focus();
+ last.focus({ preventScroll: true });
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
- first.focus();
+ first.focus({ preventScroll: true });
}
};
diff --git a/app/components/NewsletterModal.tsx b/app/components/NewsletterModal.tsx
index 4b54fd1..e9de47f 100644
--- a/app/components/NewsletterModal.tsx
+++ b/app/components/NewsletterModal.tsx
@@ -34,13 +34,40 @@ export function NewsletterModal({ open, onClose }: { open: boolean; onClose: ()
const dialogRef = useRef(null);
const closeButtonRef = useRef(null);
- // Body scroll lock while open.
+ // Background scroll lock while open — intercepts and cancels the wheel/
+ // touch input that would cause scrolling, instead of toggling
+ // overflow/position on body or html. Two earlier approaches (plain
+ // overflow:hidden on body; position:fixed with a negative top offset to
+ // compensate) each fixed one symptom while causing another — overflow
+ // alone reset scrollY to 0 for a frame when opened away from the top of
+ // the page (e.g. parked at #ueber-bjoern), and position:fixed took body
+ // out of normal flow, which detached the Navbar's `position: sticky`
+ // from its scrolling container and made it visibly snap. Neither is a
+ // risk here: scrollY, overflow and layout are never touched at all, so
+ // there's nothing that can jump, reflow, or need restoring on close —
+ // the events that would move the page just never get to.
+ // `{ passive: false }` is required for preventDefault() to have any
+ // effect on wheel/touchmove. Events that originate inside the dialog
+ // (which has its own overflow-y-auto) are let through untouched, so the
+ // modal's own content still scrolls normally.
useEffect(() => {
if (!open) return;
- const prevOverflow = document.body.style.overflow;
- document.body.style.overflow = "hidden";
+
+ const isInsideDialog = (target: EventTarget | null) =>
+ target instanceof Node && !!dialogRef.current?.contains(target);
+
+ const onWheel = (e: WheelEvent) => {
+ if (!isInsideDialog(e.target)) e.preventDefault();
+ };
+ const onTouchMove = (e: TouchEvent) => {
+ if (!isInsideDialog(e.target)) e.preventDefault();
+ };
+
+ document.addEventListener("wheel", onWheel, { passive: false });
+ document.addEventListener("touchmove", onTouchMove, { passive: false });
return () => {
- document.body.style.overflow = prevOverflow;
+ document.removeEventListener("wheel", onWheel);
+ document.removeEventListener("touchmove", onTouchMove);
};
}, [open]);
@@ -50,7 +77,16 @@ export function NewsletterModal({ open, onClose }: { open: boolean; onClose: ()
useEffect(() => {
if (!open) return;
- closeButtonRef.current?.focus();
+ // preventScroll: true — otherwise the browser's implicit
+ // scrollIntoView on focus() sees this button sitting close to the
+ // viewport's top edge and "corrects" for the global
+ // scroll-padding-top (reserved for the sticky Navbar, see
+ // globals.css) by nudging the whole page upward — pointless here
+ // since the modal is position:fixed and always fully in view
+ // regardless of document scroll, but that nudge is exactly the
+ // "page scrolls up a bit and the modal lands in a weird position"
+ // jank on open.
+ closeButtonRef.current?.focus({ preventScroll: true });
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
@@ -67,10 +103,10 @@ export function NewsletterModal({ open, onClose }: { open: boolean; onClose: ()
const last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
- last.focus();
+ last.focus({ preventScroll: true });
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
- first.focus();
+ first.focus({ preventScroll: true });
}
};
diff --git a/app/components/VersandModal.tsx b/app/components/VersandModal.tsx
index d5bbeae..7ec5d27 100644
--- a/app/components/VersandModal.tsx
+++ b/app/components/VersandModal.tsx
@@ -26,7 +26,7 @@ export function VersandModal({ open, onClose }: { open: boolean; onClose: () =>
useEffect(() => {
if (!open) return;
- closeButtonRef.current?.focus();
+ closeButtonRef.current?.focus({ preventScroll: true });
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
@@ -43,10 +43,10 @@ export function VersandModal({ open, onClose }: { open: boolean; onClose: () =>
const last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
- last.focus();
+ last.focus({ preventScroll: true });
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
- first.focus();
+ first.focus({ preventScroll: true });
}
};
diff --git a/app/globals.css b/app/globals.css
index fac1c0f..49611e5 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -141,6 +141,12 @@
html {
scroll-padding-top: 6.25rem; /* navbar height */
+ /* Reserves the scrollbar's width permanently, so any modal's
+ overflow:hidden scroll lock (NewsletterModal, VersandModal) never
+ removes/re-adds the scrollbar itself — without this, losing the
+ scrollbar on open widens the viewport by its width and every fixed/sticky
+ element (Navbar included) visibly snaps sideways for one frame. */
+ scrollbar-gutter: stable;
}
body {
diff --git a/app/lib/payload.ts b/app/lib/payload.ts
index 8d88c74..0b21412 100644
--- a/app/lib/payload.ts
+++ b/app/lib/payload.ts
@@ -119,18 +119,21 @@ export type LegalPage = {
type: LegalPageType;
title: string;
content: unknown;
+ attachment: { url: string; title: string } | null;
};
type PayloadLegalPage = {
type: LegalPageType;
title: string;
content: unknown;
+ attachment: { url: string; title: string } | number | null;
};
export async function getLegalPage(type: LegalPageType): Promise {
const params = new URLSearchParams({
"where[tenant.slug][equals]": TENANT_SLUG,
"where[type][equals]": type,
+ depth: "1",
limit: "1",
});
@@ -145,5 +148,13 @@ export async function getLegalPage(type: LegalPageType): Promise
+
+
+
+ Startseite
+ ›
+ Widerruf
+
+
+ Widerrufsrecht
+
+ Stand: Juli 2026
+
+
+
+
+
+
+ {/* Static, not part of the CMS content — same reasoning as
+ the Impressum/Datenschutz/AGB pages' own callout cards. */}
+
+

+
+ Nachhaltig handeln
+
+
+ Wir setzen auf digitale Produkte und vermeiden unnötige Ressourcenverschwendung.
+
+
Für eine bessere Zukunft – für uns alle.
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}