- Empties Klaro's own "Mit diesem Schalter..." boilerplate under the
modal's per-service toggle-all switch — redundant next to a plainly
labeled switch.
- .cn-ok/.cn-buttons/.cm-buttons now explicit flex-wrap rows (side by
side until they genuinely don't fit, then wrap) instead of relying
on Klaro's own fragile inline-block flow.
- Every tracking-codes provider gets a real description in the modal's
service list (was title-only) — what it does and, where publicly
documented and stable, the actual cookie names/lifetimes (_ga/
_ga_<container-id> for GA4, _fbp/fr for Facebook Pixel). GTM/Maps
descriptions are deliberately non-specific about exact cookies —
neither has a fixed, publicly enumerable list.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Uses Klaro's own built-in contextual-consent mechanism (not
loadTrackingCode.ts's script-injection path) — the iframe renders
with data-name="google-maps" and its real src; Klaro's DOM scan finds
it after mount and blanks/restores src based on that service's
consent state, showing its own placeholder (styled via
KlaroConsentManager.tsx's KlaroTheme) in the meantime.
No map embedded anywhere in the app yet — this is prep work only, per
explicit request ahead of an actual page needing one.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ProductGallery.tsx (main image + thumbnail strip) wired into
TodoKartenHero.tsx, only replacing the static hero photo once a
product actually has extra gallery photos — no visual change
otherwise. Backend field: Products.gallery (see payload repo).
Klaro's hard black border was still showing after the previous
color-only pass — the targeted border:none rule wasn't enough, so
this blanket-resets border/outline on every .klaro descendant and
re-adds only the shadow this theme actually wants. Also disables the
"Realisiert mit Klaro!" attribution link (BSD-3-Clause has no
on-page-attribution requirement, Klaro's own disablePoweredBy flag
covers this cleanly).
Updates the frontend README with the gallery + Klaro fixes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CSS-var overrides alone (colors, corner position) still left Klaro's
own border/shadow/spacing/typography, which read as an obviously
bolted-on library widget next to this site's hand-designed
components. New KlaroTheme component overrides Klaro's real DOM
classnames directly (confirmed against kiprotect/klaro's own scss
source) — kills the default border, adds this site's own soft-shadow
card look, restyles every button, uses the site's actual serif/sans
font pairing. Also warms up the notice/modal copy.
Updates both READMEs with the tracking-codes/Klaro/back-in-stock/
settings-search/DHL-shipment-label work from this session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Was full-width ('wide' theme) with Klaro's default green/dark
palette, neither matching the brand. Now: compact bottom-left corner
notice (~380px), brand colors via Klaro's CSS-var overrides
(dark1/light1 etc. — confusingly named, they're background/text, not
a dark-mode switch), neutral gray decline button instead of red.
Also adds a persistent bottom-left cookie icon (KlaroConsentManager.tsx)
so a visitor can reopen the consent manager any time via Klaro.show(),
not just on first visit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Swaps the hand-rolled CookieBanner.tsx/useConsent.ts/TrackingScripts.tsx
for kiprotect/klaro — brings a real per-service consent list and
bundled German UI translations that a custom implementation would
have had to build from scratch (per user feedback that a proper CMP
is worth it over a purely custom binary accept/reject banner).
klaroConfig.ts builds Klaro's config dynamically from the existing
tracking-codes backend collection (one Klaro "service" per row,
grouped by consentCategory as its purpose). loadTrackingCode.ts is
the actual script-injection side effect, wired in via each service's
`callback(consent)` — same GA4/Facebook-Pixel/GTM/custom loader logic
TrackingScripts.tsx had, just triggered imperatively instead of
declaratively rendered. Brand color applied via Klaro's CSS custom
property overrides (styling.green1 etc.), not custom SCSS.
No @types/klaro package exists — types/klaro.d.ts declares only the
small surface actually used.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
renderBackInStockHtml() (and sendBackInStockEmails.ts on the backend)
now show a small product image above the product name, when the
product has one. Also left-aligns NotifyMeForm's button text to match
the email input's own left-aligned placeholder — was centered, read
as inconsistent next to it.
Also adds the klaro dependency, ahead of swapping the custom cookie
banner for it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Replace the invisible reserved-space trick with items-start on every
product grid (ProductGrid/MerklisteGrid/RelatedProducts) — the
reservation looked worse in practice (visible dead space under
in-stock cards' buttons) than letting an out-of-stock card simply be
taller than its siblings.
- New renderBackInStockHtml() in lib/emailTemplates.ts, used by the
Live Preview instead of the generic order-status renderer — that one
showed a fake order number and "Bestellung ansehen", neither of
which apply to a back-in-stock mail (no order exists). CTA is now
"Zum Produkt", matching the real backend send.
- Shortened NotifyMeForm's button label ("Benachrichtigen") — the
longer version wrapped to two lines on narrow single-column cards.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CookieBanner.tsx (equally-weighted Akzeptieren/Ablehnen, TTDSG) +
useConsent() cookie hook + TrackingScripts.tsx render active
tracking-codes rows only once their consentCategory is actually
accepted ('necessary' always renders). Wired into layout.tsx via the
new getTrackingCodes() fetcher. This is what makes the Phase 1
backend collection (tracking-codes) actually usable end to end.
Also reworks NotifyMeForm back to always-visible input+button (better
UX than a collapse-to-reveal step) — the resulting taller CTA is now
reserved on every card via NotifyMeFormReservedSpace, an invisible
twin rendered behind the real button, so an in-stock card's row
height matches an out-of-stock sibling's without the grid's
row-stretch pushing buttons out of alignment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two problems in the previous back-in-stock commit: the disabled
"Ausverkauft" button and NotifyMeForm stacked, making out-of-stock
cards visibly taller than in-stock siblings — and since ProductGrid's
cards rely on plain CSS Grid row-stretch for equal card height, that
extra height stretched sibling cards and pushed their own buttons
down (screenshot: "In den Warenkorb" CTAs misaligned across a row).
NotifyMeForm now replaces the button slot entirely when out of stock
(matching the height of a normal button when collapsed), and only
expands to the email input after a click.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Narrow product cards (shop grid, related products) truncated the
email placeholder when input and button shared a row.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
NotifyMeForm.tsx replaces the disabled Add-to-cart button's spot once
a product/variant is out of stock — POSTs to the new
/api/stock-notifications route, which forwards to the backend's new
stock-notifications collection. Threaded product.numericId (not the
commerce slug id) into AddToCartButton/AddToCartInlineButton for this,
same split WishlistButton already uses.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cross-device sync only ever worked one way (local→server on every
change via CartSync) plus a server→local merge at the exact moment of
login/register — a customer already signed in on a second device (no
fresh login action happening) never picked up changes made elsewhere.
Adds pullServerCart(): idempotent (server qty overwrites a matching
local line rather than adding to it, safe to call repeatedly, unlike
mergeServerCartIntoLocal()'s additive merge which only stays correct
right after a login clears the local cart's ambiguity). Triggered on
CartSync's mount and on window focus — covers "open the site while
already logged in" and "switch back to this tab after changing the
cart on another device," without a polling interval.
Was always visible in ProductSpotlight.tsx, unlike ProductGrid.tsx's
cards — inconsistent for what's still a single card in a hoverable
context. The `group` ancestor needed for revealOnHover already exists
there (used for the image's hover-zoom effect), so this is just the
prop.
Same font-fallback bug ArrowRightIcon.tsx already documents and fixed
for "→" elsewhere (Tools.tsx, Blog.tsx, ProductGrid.tsx) — the site's
custom web fonts don't cover U+2190, so it fell back to a system font
whose vertical metrics sit visibly low on mobile. Missed in that
earlier pass since CartContent.tsx uses the left-pointing twin.
SearchOverlay was rendered as a <header> descendant, so the header's
conditional backdrop-blur-md (once scrolled) made it the containing
block for the overlay's fixed positioning, clipping the opaque
background to the header's height and letting page content show
through underneath. Now rendered as a header sibling, same pattern
already used for NewsletterModal.
Blog category chips are now a client component gating navigation
behind useTransition, disabling the chips while a navigation is
pending so rapid clicks can't fire overlapping RSC navigations that
commit out of order and briefly show an empty result.
Same mount-guard pattern as CartLink's own pulse fix — useWishlist()'s
count only fills in after its client-side fetch resolves, so without the
guard this would have pulsed on every page load too, not just a real
add/remove during the session. Fires on either direction (add or
remove) since the wishlist has no separate "flying" animation like the
cart's ball-to-icon effect to lean on instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018unaXmuzVA8ct1b6WoyP1U
- RichText.tsx: Lexical's auto-detected "autolink" nodes (a typed-out
email/URL, as opposed to an editor-inserted link) fell through to
Payload's unstyled default converter — only "link" was overridden.
AGB's Vertragspartner email rendered as plain black text because of
this. Both node types now get the same text-brand/hover:underline
treatment.
- Breadcrumb color revert: the breadcrumb Startseite links across the
4 legal pages/shop should stay their original hover:text-brand
treatment — only the actual content/contact links needed the
always-brand-colored style, not page-level breadcrumbs.
- /blog was the one page missing both a breadcrumb and the
text-h-feature heading size every other section page (/shop, legal
pages) already uses — added to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018unaXmuzVA8ct1b6WoyP1U
- Cart badge pulsed on every page load/reload, not just real cart
changes — useCartCount's SSR snapshot is always 0, so hydration alone
satisfied the "count increased" check. Skip the very first effect run.
- WishlistButton: touch-device smaller/subtler sizing now applies
regardless of revealOnHover (e.g. the homepage spotlight heart), not
just the shop grid's hover-reveal mode.
- /blog: the big featured-style hero card was applied to whatever post
sorted first post-filter, even when that post isn't actually the
`featured` one (a category filter can exclude the real featured post
entirely). Now gated on the post's own `featured` flag.
- Legal pages' "Startseite" breadcrumb link now matches the same
text-brand/hover:underline treatment already used elsewhere on these
pages (e.g. Impressum's Anbieter email link), instead of staying muted
until hover.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018unaXmuzVA8ct1b6WoyP1U
- CustomSelect.tsx: mousedown on an option blurred the trigger button
before the click landed, unmounting the list before anything could be
selected — add preventDefault on the list's mousedown to stop that.
- KontoShell no longer renders "Eingeloggt als ..." above each page's
content; each page renders its own title then AccountIdentity right
below it, and ProfileForm's now-duplicate email/Kundennummer line is
removed.
- Blog category filter: drop the separate "Zurücksetzen" link — clicking
an active category badge again already deactivates it.
- Werkzeuge cards: more vertical gap between stacked cards below sm:.
- Legal pages' "Stand: ..." line is now derived from the LegalPages doc's
own updatedAt instead of a hand-typed string.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018unaXmuzVA8ct1b6WoyP1U
- Remove Merkliste from AccountNav — the Navbar's own wishlist icon already covers it, a second nav entry was redundant.
- Fix duplicate "Mein Profil" heading (ProfileForm already renders its own; KontoShell no longer adds a second one).
- "Eingeloggt als ..." now renders once in KontoShell, consistently on every /konto/* page, instead of only on the orders page.
- Blog category filter chips moved into the hero's own text column (was a separate bar below the entire hero including the photo) — visible immediately on every width instead of requiring a scroll past the hero image first.
- ArrowRightIcon's arrowhead wings shortened (looked like a generic oversized chevron at full length).
- Search overlay's "Esc" text button replaced with an X icon; the actual Escape keyboard shortcut is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Konto-Aktionen links (profile, wishlist, logout) used to live at the bottom of the orders page's own content — unreachable once the order list got long enough. New KontoShell + AccountNav give every /konto/* page a persistent sidebar (sm+) / tab bar (mobile) instead, always reachable regardless of list length. The order filters also collapse behind a "Filter" toggle on mobile now, since the tab bar above them left little room for 3 full-width dropdowns.
Also: replaced the Unicode "→" arrow in CTA links (Tools.tsx, Blog.tsx, ProductGrid.tsx) with an SVG icon — the glyph isn't covered by the site's custom fonts, so browsers fall back to a system font per-platform; confirmed sitting visibly low relative to the label text on Android/Chrome (Galaxy S22), not reproducible in desktop Chromium. An SVG has no font-fallback path, so it renders identically everywhere.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Revert the 40px icon-shrink below 375px (hard to tap accurately) and reclaim that space from the logo instead (76-130px depending on width, vs the always-181px controls). Re-verified overflow-free at 320px via Playwright with all 4 icons at full 44px.
Also: wishlist heart's touch-friendly smaller/subtler size now applies to already-wishlisted (filled) hearts too, not just not-yet-wishlisted ones. The wishlist page's login redirect now returns to the wishlist instead of the generic account/orders overview. Desktop nav-link gap now scales fluidly between 1024-1300px instead of a fixed 48px that felt too wide right where nav labels have the least room.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both were deliberately hidden below 640px to avoid icon-row overflow. Verified via an actual Playwright viewport sweep (320-640px) that simply un-hiding them did overflow — the header's fixed 181px logo + 32px padding left no room. Fixed properly: logo shrinks to 130px and padding drops to px-4 below sm, plus a 40px icon-size step below 375px. Re-verified overflow-free down to 320px.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fully transparent made the heart invisible against some product photos — keeps it smaller than the default but with a semi-transparent (60%) background circle instead of none, and stops dimming the icon itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Full-size, always-visible hearts on touch devices recreated the exact "a heart on every card" visual noise revealOnHover exists to avoid. Touch devices now get a smaller, backgroundless, slightly transparent heart instead — present and tappable everywhere, but visually quiet. Desktop hover-reveal behavior is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
revealOnHover relied purely on group-hover/group-focus-within, both mouse/keyboard-only pseudo-classes with no reliable touch equivalent — a tap on a card never reliably triggers group-hover the way a mouse hover does, leaving the heart permanently hidden on touch devices. Adds pointer-coarse:opacity-100 so it's always visible there; desktop hover-reveal behavior is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per-product spotlightShowWishlist opt-in (independent of the global
wishlistEnabled toggle), a reveal-on-hover WishlistButton variant for
multi-card grids (avoids a heart on every card reading as visual noise),
and related PriceRangeFilter/ProductGrid/CustomSelect adjustments.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Promoted CustomSelect from konto/bestellungen/components/ to a shared
app/components/ location. New includeAllOption (checkout doesn't want
a "clear" pseudo-option — a country is always genuinely selected) and
fullWidth (matches the other w-full form fields, no sm: shrink) props.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Search icon now gated behind the new backend toggle, same pattern as
wishlistEnabled — off by default.
Fixed: blog category filter bar was invisible — it was wrapped in
<Reveal>, which sits right at/just past the hero's bottom edge, the
exact "already near the initial viewport" position Reveal.tsx's own
comment documents as a whileInView(margin:"-80px") trap (an element
already visible without scrolling can permanently never register as
"entered view", since `once: true` never gets a second chance). Now a
plain div, no scroll-reveal animation needed for a filter bar anyway.
Fixed: wishlist count showing one behind on the Navbar badge —
useWishlist.ts's toggle() broadcast the "refetch me" event immediately
after the optimistic local update, before the POST request was even
sent. Another instance's resulting refetch could race the actual
server-side write, get the pre-toggle list, and then never get told to
refetch again. Broadcast now only fires after the request settles.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Separate commit on purpose so this can be reverted independently if
needed. Search icon in the Navbar (hidden below sm:, same reasoning
as WishlistLink — Account+Cart are the only always-visible icons on
true mobile) opens a debounced (250ms) overlay searching both
collections at once via a new /api/search route.
Plain Payload `where[...][contains]` queries (Postgres ILIKE), not a
real search index (Meilisearch/Algolia) — matches the catalog's
current small size, see [[project-ecommerce-sota-gaps]]'s own "search
becomes necessary past ~20 products" note. Worth upgrading later
without touching the overlay component itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New: useWishlist.ts (fetch+optimistic-toggle hook, server-backed since
a wishlist needs a logged-in customer, unlike the guest-friendly cart),
WishlistButton.tsx (heart toggle, login-redirects on 401), Navbar's
wishlist icon+badge (hidden below sm: — Account+Cart are the only
always-visible icons on true mobile, a 3rd icon there risks the same
computed nav-overflow class of bug documented in the figma-to-nextjs
skill), and /konto/merkliste (list page, 404s if the feature gets
disabled after a customer already has rows).
Product gained numericId (the raw Payload id) alongside its existing
slug id — WishlistItems.product is a real numeric relationship field,
unlike cart/checkout's slug-keyed "commerce id".
Backend counterpart (WishlistItems collection, CompanySettings toggle,
migration) already deployed separately.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
OrderFilters.tsx: filter chips (7 status + 5 payment-status + N year
options as pills) read as cluttered and ate a lot of vertical space.
Replaced with 3 native <select>s in a row (a small Client Component
just for the onChange→router.push navigation) — same URL-search-param
filtering underneath, just a much more compact control.
TrustRow.tsx: replaced the hardcoded min-h-[3.2rem] title reservation
(a guess, and it visibly over-reserved space for single-line titles)
with a real CSS Grid + `grid-template-rows: subgrid` — every badge's
icon+title box shares the same row-track height (auto-sized to
whichever title actually needs 2 lines), so descriptions align
without any hardcoded value. The two structurally different layouts
(icon-above-title below lg vs. icon-beside-a-title/description-stack
at lg) now render as two separate blocks instead of fighting one
shared shape.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TrustRow.tsx: reserve 2 lines of height for the title below lg: so the
description starts at the same y-position regardless of whether that
badge's own title happened to wrap to 1 or 2 lines in its narrow
stacked column.
BestellbestaetigungContent.tsx: same fix category as NewsletterModal's
dialog width cap — max-w-[56rem] doesn't constrain anything below
896px, so the order-details card stretched full width through the
640-1023px tablet range. Added max-w-[36rem] there, widening to the
real cap only once lg:'s sidebar split needs the room.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cart summary trust badges: side by side between sm and lg
TrustRow.tsx: rebuilt as a single continuous below-lg tier instead of
mobile-nebeneinander/tablet-stacked — icon above text (shrinking each
badge to its text column's width) plus dropping whitespace-nowrap on
title/description below lg (wrapping within their own narrow column
instead of forcing overflow) means a full row of 3 badges now fits
without needing to wrap or stack, avoiding the previously-reverted
"odd item alone on its own line" issue a flex-wrap attempt hit.
CartContent.tsx: the sidebar's trust-badge list (title only) now goes
side by side between sm (640px) and lg (1024px) instead of always
stacked — that sidebar is full page width through that whole range,
plenty of room for 3 short titles in a row.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Was sm:grid-cols-5 — with the 2-up sm:grid-cols-2 outer grid, each
card's own width through 640-1023px was too narrow for image+text
side by side. Now stacks through lg (1024px), row layout only once
each card has real width. Featured post untouched (already fine).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Newsletter: left-align copy above mobile, fix missing vertical padding
Tools.tsx: icon+text now stacks below lg (1024px) instead of always
being a row — a 3-up grid from sm (640px) left each card too narrow
for side-by-side icon+text through 640-1023px. Centered on true
mobile, left-aligned from sm up.
Newsletter.tsx: same left-alignment treatment (centered only on true
mobile, left-aligned from sm up). Also removed the outer card's
sm:py-0 — it relied entirely on items-center centering against the
copy column's height for top/bottom breathing room, which broke down
whenever the form column (input/checkbox/privacy note) was as tall or
taller, leaving the input and "Keine Werbung" note flush against the
card edges. py-8 now applies at every breakpoint.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Icon+text switched to row layout at sm (640px), but the outer card
already puts this copy column next to the form from that same
breakpoint — squeezed the text through 640-1023px. Row layout for
icon+text now only kicks in at lg, where there's actually room.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sm:flex-row (640px) put the 380px image side-by-side with text well
before there was comfortable room for it — tablet widths (768-1023px)
read as cramped. Now stays stacked like mobile through lg (1024px),
with a max-w-[32rem] cap between sm and lg so the stacked card reads
as centered/narrower instead of stretching to fill the tablet
viewport (true mobile stays unconstrained, the viewport is already
narrow there).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The shared ease-out curve (fast start, slow finish) worked well for
the open reveal but made the close shrink almost instantly then
linger on a barely-visible sliver — read as abrupt. Gave exit its own
longer, easeInOut transition instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Added an onClick on <header> that closes the drawer whenever it's
open — the panel is a sibling, not a descendant, so this never fires
for clicks inside the open drawer, only the bar itself. The
hamburger's own click now stops propagation so toggling the drawer
open doesn't get immediately undone by this same handler.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Posts.categories is now hasMany — blog list/detail/live-preview render
a comma-joined list instead of a single category.
- Checkout's "Abweichende Lieferadresse" gains optional Firma + Kontakt-
E-Mail/Telefon fields (handed to the shipping carrier, not used for
customer communication).
- Customer profile can now store its own shipping address (mirroring
Customers.ts's new "Lieferadresse" tab), prefilling the checkout
override instead of always starting blank.
- Product-level optional SKU (previously only on variants) snapshots onto
each order item and shows up on invoices (visual + EN16931 XML),
the confirmation email, and the account order detail page.
- Newsletter.tsx: max-w-[1280px] mx-auto, matching /lebensuhr's own
newsletter-style section and Footer.tsx's existing 1280px convention —
previously unbounded and stretched edge-to-edge on wide viewports.
- Product pages: noShippingCost now shows "Keine Versandkosten" instead
of just omitting "zzgl. Versand" silently.
- /versand's exemption note no longer implies only digital products can
be exempt — noShippingCost is a general per-product flag.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- api/checkout/route.ts: authoritative shipping charge is 0 whenever
every cart line opts out via noShippingCost, regardless of the
free-shipping threshold.
- Cart/checkout order summaries: the whole "Versand" line (cost, free-
shipping note, delivery time) is hidden entirely rather than showing
"Kostenlos" — that's a different state from hitting the threshold.
- ProductSpotlight/Pricing/TodoKartenHero: "zzgl. Versand" and delivery-
time hints drop for an exempted product's own page.
- /versand + its shared modal: one clarifying sentence that digital
products are exempt.
- Widerrufsformular link: opens inline in a new tab (no forced download),
arrow icon changed from a download glyph to a plain right arrow to
match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- PaymentStep: bg-brand-primary was never a real Tailwind class, leaving
the "Jetzt bezahlen" button unstyled/invisible; switched to bg-brand
with dark text, matching every other CTA on the site.
- New app/icon.svg + app/apple-icon.png + app/favicon.ico: brand-yellow
rounded square with a bold italic "e", replacing the Next.js default.
- New public/og-image.png (logo on the site's cream background) wired in
as the SEO fallback default OG image/description/title in payload.ts.
- Navbar CTA: "Klarheitskompass" -> "Mein 3x3-System", now links to
/3x3-system (page not built yet).
- Blog overview hero image anchored to the top instead of center-cropped.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The flex-wrap experiment (2 badges per row + 1 wrapped centered below)
looked disorganized with exactly 3 badges — the wrapped single item
doesn't align under either item in the row above it, reported as
"durcheinander". Reverts to strict single-column stacking below lg:
(the inner-shrink-to-fit-group + outer center/left-align pattern),
which stays visually clean regardless of how many badges exist.
TrustRow now uses flex-wrap + justify-center so badges flow as many
per row as actually fit at the current width, wrapping (and
auto-centering, a native flexbox behavior for the last wrapped line)
instead of needing a breakpoint or risking overflow. Replaces the
earlier lg:-only structural exception entirely.
NewsletterModal's close button was absolutely positioned inside the
dialog's own scrolling container, so it scrolled away with the
content. Switched to sticky (top-6, ml-auto for horizzontal position,
-mb-6 to cancel its own height so it doesn't push content down) so it
stays pinned to the top-right corner while scrolling.