Move structural breakpoint to 640px, shorten Hero heading

Tablet layout (768-1023px) hit the md: grid switch exactly where the
fluid clamp() tokens were already at their floor, leaving no room to
shrink. Moves the fluid floor and structural breakpoint down together
to sm: (640px) so real tablets always get the fluid, desktop-like
structure; consolidates the ad-hoc md:+lg: patchwork in Hero/About/
Newsletter/Footer/Tools back onto one line, leaving documented lg:
exceptions where content genuinely doesn't fit yet. Also shortens the
Hero heading to a single sentence with no trailing period (the brand's
orange dot already renders one, animated).
This commit is contained in:
Marco
2026-07-29 10:49:56 +00:00
parent 94cba756c8
commit cc935420ec
25 changed files with 327 additions and 232 deletions
@@ -183,6 +183,12 @@ export function BestellbestaetigungContent({ defaultTaxRate }: { defaultTaxRate:
</Reveal>
{!productsLoading && (
// Outer card+sidebar split stays lg: (fixed 18rem delivery-status
// sidebar, L297 below — same fixed-width-block category as Tier C
// exceptions elsewhere), while the inner order-meta/items row
// already switches at sm: — intentional, not an inconsistent
// leftover: the inner row has no fixed-width sidebar to fight for
// room against, just two flexible columns.
<Reveal
delay={0.05}
className="w-full max-w-[56rem] mx-auto bg-bg-base border border-border rounded-md overflow-hidden flex flex-col lg:flex-row mb-16"
@@ -328,8 +334,8 @@ export function BestellbestaetigungContent({ defaultTaxRate }: { defaultTaxRate:
identical height regardless of how many lines the quote wraps
to (a min-height alone lets whichever column has more content
stretch the row past what the other side visually fills). */}
<Reveal className="relative w-full flex items-stretch h-[14rem] md:h-[16rem] bg-bg-muted overflow-hidden">
<div className="relative w-full md:w-[45%] shrink-0">
<Reveal className="relative w-full flex items-stretch h-[14rem] sm:h-[16rem] bg-bg-muted overflow-hidden">
<div className="relative w-full sm:w-[45%] shrink-0">
{/* -inset-1, not inset-0 — this section fades in via Reveal's
y:28→0 transform; a plain inset-0 image can leave a
hairline gap at the top edge while that's still settling
@@ -343,10 +349,10 @@ export function BestellbestaetigungContent({ defaultTaxRate }: { defaultTaxRate:
src="/bestellbestaetigung-testimonial-photo.jpg"
width={366}
height={126}
sizes="(min-width: 768px) 45vw, 100vw"
sizes="(min-width: 640px) 45vw, 100vw"
className="absolute -inset-1 w-[calc(100%+0.5rem)] h-[calc(100%+0.5rem)] object-cover"
/>
<div className="hidden md:block absolute inset-y-0 right-0 w-40 bg-gradient-to-l from-bg-muted to-transparent" />
<div className="hidden sm:block absolute inset-y-0 right-0 w-40 bg-gradient-to-l from-bg-muted to-transparent" />
</div>
<div className="flex-1 flex flex-col justify-center gap-3 px-8 md:px-16">
<p
+9 -9
View File
@@ -30,8 +30,8 @@ export default async function BlogOverviewPage() {
{/* Header — copy left, photo bleeds to the viewport edge on the
right with a left-edge fade into bg-base, matching Figma's
hero-fade-left/hero-fade-bottom overlays (node 4577:330). */}
<Reveal className="relative flex flex-col md:flex-row items-center w-full min-h-[20rem] md:min-h-[27.5rem] border-b border-border overflow-hidden">
<div className="relative z-10 flex flex-col gap-4 items-start px-[var(--layout-padding-x)] py-10 md:py-0 w-full md:w-auto md:max-w-[26rem]">
<Reveal className="relative flex flex-col sm:flex-row items-center w-full min-h-[20rem] sm:min-h-[27.5rem] border-b border-border overflow-hidden">
<div className="relative z-10 flex flex-col gap-4 items-start px-[var(--layout-padding-x)] py-10 sm:py-0 w-full sm:w-auto sm:max-w-[26rem]">
<p
className="font-semibold text-display text-text-primary"
style={{ fontFamily: "var(--font-lora)" }}
@@ -42,9 +42,9 @@ export default async function BlogOverviewPage() {
Gedanken, Methoden und Impulse für einen leichteren und klareren Alltag.
</p>
</div>
<div className="relative w-full md:absolute md:inset-y-0 md:right-0 md:w-[68%] h-56 md:h-full">
<Image alt="" src="/hero.jpg" fill sizes="(min-width: 768px) 68vw, 100vw" className="object-cover" />
<div className="hidden md:block absolute inset-y-0 left-0 w-72 bg-gradient-to-r from-bg-base to-transparent" />
<div className="relative w-full sm:absolute sm:inset-y-0 sm:right-0 sm:w-[68%] h-56 sm:h-full">
<Image alt="" src="/hero.jpg" fill sizes="(min-width: 640px) 68vw, 100vw" className="object-cover" />
<div className="hidden sm:block absolute inset-y-0 left-0 w-72 bg-gradient-to-r from-bg-base to-transparent" />
<div className="absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-bg-base to-transparent" />
</div>
</Reveal>
@@ -56,14 +56,14 @@ export default async function BlogOverviewPage() {
<Reveal delay={0.1} className="relative z-10 w-full px-[var(--layout-padding-x)] -mt-8 pb-4">
<Link
href={`/blog/${featured.slug}`}
className="group grid grid-cols-1 md:grid-cols-2 w-full max-w-[80rem] mx-auto rounded-md overflow-hidden bg-bg-muted transition-transform duration-300 hover:-translate-y-1"
className="group grid grid-cols-1 sm:grid-cols-2 w-full max-w-[80rem] mx-auto rounded-md overflow-hidden bg-bg-muted transition-transform duration-300 hover:-translate-y-1"
>
<div className="relative w-full aspect-video md:aspect-auto md:h-full bg-bg-muted">
<div className="relative w-full aspect-video sm:aspect-auto sm:h-full bg-bg-muted">
{featured.thumbnail && (
<Image alt="" src={featured.thumbnail} fill sizes="(min-width: 768px) 40rem, 100vw" className="object-cover" />
<Image alt="" src={featured.thumbnail} fill sizes="(min-width: 640px) 40rem, 100vw" className="object-cover" />
)}
</div>
<div className="flex flex-col justify-center gap-3 p-8 md:p-12 min-w-0">
<div className="flex flex-col justify-center gap-3 p-8 sm:p-12 min-w-0">
<div className="flex items-center gap-2 font-semibold text-text-muted text-body-sm uppercase tracking-wide">
<span>{featured.category}</span>
<span></span>
+4 -3
View File
@@ -176,9 +176,10 @@ export function CartContent({
{/* Cart card — lg:-only split from the sidebar (same "wide content
next to sidebar" shape as the Hero's image/text split, see
figma-to-nextjs skill Gotcha #5: Figma's 830px card alone
already exceeds the 768px Tablet floor, so md: would never
have had room for a real 2-column layout at Tablet widths
anyway). */}
already exceeds even the site's 640px structural floor, so
sm: would never have had room for a real 2-column layout at
Tablet widths anyway — a deliberate exception to the site-wide
sm: consolidation, not a leftover of it). */}
<Reveal className="w-full lg:flex-1 flex flex-col gap-6 items-start bg-bg-base border border-border rounded-md p-6 md:p-8">
{items.map(({ entry, product }, i) => {
const discount = discountPercent(product.price, product.compareAtPrice);
+5 -5
View File
@@ -123,7 +123,7 @@ export function RelatedProducts({ defaultTaxRate, kleinunternehmer }: { defaultT
(opacity: 0) — invisible. Not worth chasing a fix for a
scroll-reveal nicety on a list that mutates; a static grid
renders correctly with no animation risk. */}
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 md:gap-[var(--layout-grid-gap)] w-full max-w-[75rem]">
<div className="grid grid-cols-1 sm:grid-cols-12 gap-6 sm:gap-[var(--layout-grid-gap)] w-full max-w-[75rem]">
{displayProducts.map((product, i) => {
const discount = discountPercent(product.price, product.compareAtPrice);
const taxRate = effectiveTaxRate(product, defaultTaxRate);
@@ -134,7 +134,7 @@ export function RelatedProducts({ defaultTaxRate, kleinunternehmer }: { defaultT
<div
key={product.id}
className={
"group md:col-span-4 bg-bg-base border border-border rounded-md overflow-hidden flex flex-col gap-4 transition-transform duration-300 hover:-translate-y-1 " +
"group sm:col-span-4 bg-bg-base border border-border rounded-md overflow-hidden flex flex-col gap-4 transition-transform duration-300 hover:-translate-y-1 " +
// Center the row when there are fewer than 3 cards to show
// (e.g. only 1 active product left once the others are
// already in the cart) — only the first card needs an
@@ -142,9 +142,9 @@ export function RelatedProducts({ defaultTaxRate, kleinunternehmer }: { defaultT
// it. 3-card case keeps the default left-to-right flow.
(i === 0
? displayProducts.length === 1
? "md:col-start-5"
? "sm:col-start-5"
: displayProducts.length === 2
? "md:col-start-3"
? "sm:col-start-3"
: ""
: "")
}
@@ -154,7 +154,7 @@ export function RelatedProducts({ defaultTaxRate, kleinunternehmer }: { defaultT
src={product.image}
alt={product.name}
fill
sizes="(min-width: 768px) 320px, 100vw"
sizes="(min-width: 640px) 320px, 100vw"
className="object-cover transition-transform duration-500 group-hover:scale-105"
/>
{/* Same top-left pill pattern as ProductGrid.tsx/
+29 -25
View File
@@ -3,19 +3,20 @@ import { Reveal } from "./Reveal";
export function About() {
return (
<section id="ueber-bjoern" className="bg-bg-dark flex flex-col md:flex-row md:items-stretch w-full">
<section id="ueber-bjoern" className="bg-bg-dark flex flex-col sm:flex-row sm:items-stretch w-full">
{/* Text content — relative + z-10 so it renders above the overlapping
photo at lg+. Comes first in DOM at every breakpoint (no reorder
here — unlike Hero, there's no conversion CTA at stake).
md:flex-[1.4_0_0] lg:flex-[1_0_0] — at Tablet the text column got
the narrower 1:1.4 share meant for Desktop's overlap layout,
leaving it too cramped for the fixed-width statement + quote/bio
row. Widened at Tablet (text gets the bigger share, image the
smaller one, no overlap yet) and reverted to the original ratio
from lg: up, where the overlap trick actually needs the image to
have more room. */}
<Reveal className="flex flex-col gap-4 justify-center px-[var(--layout-padding-x)] py-8 md:flex-[1.4_0_0] lg:flex-[1_0_0] min-w-0 relative z-10">
photo from sm+. Comes first in DOM at every breakpoint (no
reorder here — unlike Hero, there's no conversion CTA at stake).
Used to be md:flex-[1.4_0_0] lg:flex-[1_0_0]: a two-step ratio
where Tablet got a wider, non-overlap-ready share and only
reverted to the real 1:1.4 Desktop ratio at lg: — that in-between
step existed purely to compensate for the grid arriving (at the
old md: 768px) before the fluid tokens had room to shrink.
Now the grid itself starts at sm: (640px), aligned with the
fluid floor, so the single real ratio applies from sm: up
directly — no intermediate step needed. */}
<Reveal className="flex flex-col gap-4 justify-center px-[var(--layout-padding-x)] py-8 sm:flex-[1_0_0] min-w-0 relative z-10">
{/* Large serif statement — width-constrained as per design */}
<p
@@ -26,11 +27,13 @@ export function About() {
</p>
{/* Quote row: script quote / divider / author bio — side-by-side
from lg: (was md:) — even with the text column's wider Tablet
share above, quote + divider + the whitespace-nowrap bio ("Gründer
von einfach-produktiv.") together still needed more room than
Tablet's ~384px column has. Stacked with a horizontal divider
through the whole Tablet range instead, side-by-side (vertical
only from lg: (1024px), a deliberate exception to the site's
sm: (640px) structural consolidation: quote + divider + the
whitespace-nowrap bio ("Gründer von einfach-produktiv.")
together need more room than the sm:-anchored text column has
through the whole 640-1023px range, independent of the outer
text/image ratio above. Stacked with a horizontal divider
through that whole range instead, side-by-side (vertical
divider) only once there's real room at lg:. */}
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-6 lg:gap-0 w-full">
@@ -80,13 +83,14 @@ export function About() {
</div>
</Reveal>
{/* Author photo — overlaps the text column via -ml-48 from lg+ only
(that overlap trick has nothing to blend into once stacked, and
at Tablet it would eat back into the extra width the text column
above just gained); plain full-width photo below the text on
Mobile, plain side-by-side (no overlap) at Tablet. */}
{/* Author photo — overlaps the text column via -ml-48 from sm+ now
(moved down together with the ratio collapse above — the overlap
trick and the 1:1.4 share are the same Desktop-style treatment,
previously gated behind the extra lg: step for the same reason
the ratio was, so both move together); plain full-width photo
below the text on true Mobile only. */}
<Reveal
className="relative overflow-hidden w-full md:flex-[1_0_0] lg:flex-[1.4_0_0] lg:-ml-48"
className="relative overflow-hidden w-full sm:flex-[1.4_0_0] sm:-ml-48"
style={{ minHeight: "14rem" }}
delay={0.15}
>
@@ -94,11 +98,11 @@ export function About() {
alt="Björn"
src="/about-author.jpg"
fill
sizes="(min-width: 1024px) 58vw, (min-width: 768px) 42vw, 100vw"
sizes="(min-width: 640px) 58vw, 100vw"
className="object-cover object-center pointer-events-none"
/>
{/* Left gradient: wide enough to cover the text-column overlap — lg+ only */}
<div className="hidden lg:block absolute inset-y-0 left-0 w-72 bg-gradient-to-r from-bg-dark to-transparent pointer-events-none" />
{/* Left gradient: wide enough to cover the text-column overlap — moved to sm+ with the overlap itself */}
<div className="hidden sm:block absolute inset-y-0 left-0 w-72 bg-gradient-to-r from-bg-dark to-transparent pointer-events-none" />
</Reveal>
</section>
+13 -13
View File
@@ -31,20 +31,20 @@ export async function Blog() {
{/* Posts grid */}
<RevealGroup className="flex flex-col gap-10 items-start px-[var(--layout-padding-x)] w-full">
{/* Featured post — image on top on Mobile, side-by-side from md+ */}
<RevealItem className="w-full border border-border rounded-xl overflow-hidden grid grid-cols-1 md:grid-cols-12 transition-transform duration-300 hover:-translate-y-1">
<div className="relative w-full aspect-video md:aspect-auto md:col-span-7 md:h-[220px] bg-bg-muted">
{/* Featured post — image on top on Mobile, side-by-side from sm+ */}
<RevealItem className="w-full border border-border rounded-xl overflow-hidden grid grid-cols-1 sm:grid-cols-12 transition-transform duration-300 hover:-translate-y-1">
<div className="relative w-full aspect-video sm:aspect-auto sm:col-span-7 sm:h-[220px] bg-bg-muted">
{featured.thumbnail && (
<Image
alt=""
src={featured.thumbnail}
fill
sizes="(min-width: 768px) 58vw, 100vw"
sizes="(min-width: 640px) 58vw, 100vw"
className="object-cover pointer-events-none"
/>
)}
</div>
<div className="flex flex-col justify-between gap-4 py-4 px-4 md:pr-4 md:pl-4 md:col-span-5 min-w-0">
<div className="flex flex-col justify-between gap-4 py-4 px-4 sm:pr-4 sm:pl-4 sm:col-span-5 min-w-0">
<div className="flex flex-col gap-3">
<div className="flex gap-2 items-center font-semibold text-text-muted text-body uppercase whitespace-nowrap">
<span>{featured.category}</span>
@@ -58,7 +58,7 @@ export async function Blog() {
>
{featured.title}
</p>
<p className="font-normal text-body leading-6 line-clamp-2 md:line-clamp-none">
<p className="font-normal text-body leading-6 line-clamp-2 sm:line-clamp-none">
{featured.excerpt}
</p>
</div>
@@ -75,25 +75,25 @@ export async function Blog() {
</div>
</RevealItem>
{/* Secondary posts — image on top on Mobile, side-by-side from md+ */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full">
{/* Secondary posts — image on top on Mobile, side-by-side from sm+ */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 w-full">
{secondary.map((post) => (
<RevealItem
key={post.id}
className="border border-border rounded-xl overflow-hidden grid grid-cols-1 md:grid-cols-5 transition-transform duration-300 hover:-translate-y-1"
className="border border-border rounded-xl overflow-hidden grid grid-cols-1 sm:grid-cols-5 transition-transform duration-300 hover:-translate-y-1"
>
<div className="relative w-full aspect-video md:aspect-auto md:col-span-2 md:h-[230px] bg-bg-muted">
<div className="relative w-full aspect-video sm:aspect-auto sm:col-span-2 sm:h-[230px] bg-bg-muted">
{post.thumbnail && (
<Image
alt=""
src={post.thumbnail}
fill
sizes="(min-width: 768px) 29vw, 100vw"
sizes="(min-width: 640px) 29vw, 100vw"
className="object-cover pointer-events-none"
/>
)}
</div>
<div className="flex flex-col justify-between gap-4 py-3 px-4 md:col-span-3 min-w-0 text-text-primary">
<div className="flex flex-col justify-between gap-4 py-3 px-4 sm:col-span-3 min-w-0 text-text-primary">
<div className="flex flex-col gap-3">
<div className="flex gap-2 items-center font-semibold text-text-muted text-body uppercase whitespace-nowrap">
<span>{post.category}</span>
@@ -107,7 +107,7 @@ export async function Blog() {
>
{post.title}
</p>
<p className="font-normal text-body leading-6 line-clamp-1 md:line-clamp-none">
<p className="font-normal text-body leading-6 line-clamp-1 sm:line-clamp-none">
{post.excerpt}
</p>
</div>
+4 -5
View File
@@ -19,11 +19,10 @@ export function Footer() {
<div className="flex flex-col items-center w-full max-w-[1280px] py-8 md:py-4">
{/* Three groups: logo | @handle | links — stacked + centered below
lg: (was md:). Logo + handle + 5 legal links all side by side
with justify-between read too cramped on Tablet — stacked
through that range instead, side by side again once there's
real room at lg:. */}
<div className="flex flex-col lg:flex-row items-center lg:justify-between gap-6 lg:gap-0 px-8 md:px-16 w-full">
sm: (640px, moved down from the old md:/lg: skip-pattern now
that grid/flex structure across the site aligns with the fluid
floor). Side by side again once there's real room at sm:. */}
<div className="flex flex-col sm:flex-row items-center sm:justify-between gap-6 sm:gap-0 px-8 md:px-16 w-full">
{/* Logo: "einfach produktiv" white + "." gold */}
<div className="flex items-center p-2 shrink-0">
+44 -36
View File
@@ -11,7 +11,7 @@ function HeroImage() {
alt=""
fill
priority
sizes="(min-width: 768px) 58vw, 100vw"
sizes="(min-width: 640px) 58vw, 100vw"
className="object-cover"
style={{
WebkitMaskImage:
@@ -28,44 +28,52 @@ function HeroImage() {
export function Hero() {
return (
<section className="bg-bg-base w-full overflow-hidden">
{/* Structural breakpoint is md: (768px) for the GRID only the text
column stays ~283-320px wide through the whole 768-1023px Tablet
{/* Structural breakpoint is sm: (640px, moved down from the old md:
768px so the grid arrives exactly where the fluid token floor
also sits see fluid.ts/globals.css) for the GRID only the
text column stays narrow through the whole 640-1023px Tablet
range regardless. Below, every piece of *content* inside the text
column (heading/subtitle/CTA/social-proof) keeps its smaller,
fixed-below-lg: sizing all the way through Tablet too, not just
true Mobile reusing the full fluid-token sizes at md: (as a
first pass 2026-07-24 briefly did) put the original ~19-44px
fluid floors right back in that narrow column, recreating the
exact 3-line-wrap problem the old `lg:` structural exception
existed to avoid. Splitting "grid at md:" from "full-size content
at lg:" gets both: Tablet shows the real 5/7 grid, but with
content sized for its column's actual width, not the column
width `lg:` was designed for. */}
<div className="flex flex-col md:grid md:grid-cols-12 md:items-center gap-8 md:gap-[var(--layout-grid-gap)] pt-10 md:pt-0">
column (heading/subtitle/CTA/social-proof) deliberately keeps its
smaller, fixed-below-lg: sizing all the way through Tablet too,
not just true Mobile reusing the full fluid-token sizes that
early put the original ~19-44px fluid floors right back in that
narrow column, recreating the exact 3-line-wrap problem the old
`lg:` structural exception existed to avoid. This is a narrower,
intentional exception to the site-wide sm: consolidation the
column's real width at sm: hasn't been visually verified yet
(no browser in this environment), so full-size content stays
gated behind lg: as a fast-follow rather than a guess. Splitting
"grid at sm:" from "full-size content at lg:" gets both: Tablet
shows the real 5/7 grid, but with content sized for its column's
actual width, not the column width `lg:` was designed for. */}
<div className="flex flex-col sm:grid sm:grid-cols-12 sm:items-center gap-8 sm:gap-[var(--layout-grid-gap)] pt-10 sm:pt-0">
{/* Text content first in DOM/visual order at every breakpoint so
the CTA stays above the fold on Mobile (deliberate exception to
the "keep DOM order" default, see Hero decision in the plan).
Reveal fires ~immediately since Hero is already in the initial
viewport this doubles as the page's entrance animation. */}
<Reveal className="order-1 md:order-none md:col-span-5 flex flex-col gap-7 items-start pl-[var(--layout-padding-x)] pr-10 md:pr-0">
<Reveal className="order-1 sm:order-none sm:col-span-5 flex flex-col gap-7 items-start pl-[var(--layout-padding-x)] pr-10 sm:pr-0">
{/* Heading smaller fixed-ish size below lg: (text-h1, still a
real paired font-size+line-height token, not an arbitrary
value) text-display's own 44px floor wraps very heavily in
a ~283-320px Tablet column (even a single word can approach
that width). Forced break after "darf" only in the sm-md
tablet range (natural wrap there landed awkwardly); removed
at true mobile widths (below sm:) 2026-07-24 narrower
still, natural wrap reads fine there, and the forced break
made "darf" the whole first line. Full text-display only
from lg: up, where the column has real room again. */}
that width). The old forced break after "darf" only existed
for a "wide single-column, not yet grid" band (640-767px
under the old md: 768px grid switch) that no longer exists
now the grid itself starts at sm: (640px) below sm: the
stack is narrower than that old band ever was, where natural
wrap already reads fine (confirmed 2026-07-24), so the break
is removed rather than re-anchored to a new range. Full
text-display only from lg: up, where the column has real
room again. */}
<p
className="font-semibold leading-[0] shrink-0 text-[0px] text-text-primary"
style={{ fontFamily: "var(--font-playfair)" }}
>
<span className="text-h1 lg:text-display">
Verliere dich nicht<br className="hidden sm:inline md:hidden" /> im mehr. Finde heraus, was wichtig ist.
Finde heraus, was wichtig ist
</span>
{/* Brand's signature orange dot (also in the logo/footer)
bouncy pop-in once the heading scrolls into view, timed to
@@ -133,29 +141,29 @@ export function Hero() {
</Reveal>
{/* Image bleeds to the true edge at every breakpoint (never
padded). Below md: (stacked layout) the full 887:583 aspect
ratio at 100vw would make the image ~600-900px tall and
dominate the page, so height is capped and object-cover crops
it into a supporting banner instead; at md:+ (grid, image only
58% width) the full aspect ratio looks right again, so the cap
is lifted. No shadow: a plain box-shadow reads as a hard
rectangular edge against the existing corner/right/bottom
mask-gradient fade below, which looked worse than no shadow at
all tried and reverted. */}
{/* No Reveal (fade-in-on-scroll) below md: whileInView's -80px
padded). Below sm: (stacked layout, moved down from the old
md: 768px) the full 887:583 aspect ratio at 100vw would make
the image ~600-900px tall and dominate the page, so height is
capped and object-cover crops it into a supporting banner
instead; at sm:+ (grid, image only 58% width) the full aspect
ratio looks right again, so the cap is lifted. No shadow: a
plain box-shadow reads as a hard rectangular edge against the
existing corner/right/bottom mask-gradient fade below, which
looked worse than no shadow at all tried and reverted. */}
{/* No Reveal (fade-in-on-scroll) below sm: whileInView's -80px
viewport margin means the image doesn't fade in until scrolled
that much further into view; on a short mobile viewport this
image sits right at the initial fold, so it stayed at
opacity:0 (a white gap, matching the section's own bg-bg-base)
above the fold until the user scrolled (reported 2026-07-24).
Plain, always-visible image below md: instead; Reveal's fade
kept from md: up, where the image is beside the text with
Plain, always-visible image below sm: instead; Reveal's fade
kept from sm: up, where the image is beside the text with
plenty of room and this was never an issue. */}
<div className="order-2 md:hidden relative w-full aspect-[887/583] max-h-[16rem]">
<div className="order-2 sm:hidden relative w-full aspect-[887/583] max-h-[16rem]">
<HeroImage />
</div>
<Reveal
className="hidden md:block md:col-span-7 relative w-full aspect-[887/583]"
className="hidden sm:block sm:col-span-7 relative w-full aspect-[887/583]"
delay={0.15}
>
<HeroImage />
+11 -1
View File
@@ -428,7 +428,17 @@ export function Navbar({ singleActiveProduct }: { singleActiveProduct: boolean }
Figma's Navbar-Mobile component set exactly, see the plan.
Matches the Figma NavLink component's hover: text never
changes color, an underline (brand, 2px x 40px) fades in on
hover and stays on for the active section. */}
hover and stays on for the active section.
Deliberate exception to the site-wide sm: (640px) structural
consolidation (see the 640px-breakpoint plan): this md:/lg:
3-tier scheme (hamburger-only <768, hamburger+inline-CTAs
768-1023, full-inline 1024) is untouched by that migration.
Horizontal nav-link overflow is a different failure mode than
the vertical grid/flex reflows the rest of the site has
there's no fluid token that shrinks link text to make a full
inline nav fit at 640px, and nothing here was ever tied to
the fluid token scale the way Hero/About/etc. were. */}
<nav className="hidden lg:flex items-center gap-12">
{navLinks.map((link) => {
const isActive = isNavLinkActive(link.href, pathname, activeSection);
+23 -19
View File
@@ -43,17 +43,17 @@ export function Newsletter({
{/* Outer section padding — same fluid horizontal padding as all other sections */}
<div className="px-[var(--layout-padding-x)] w-full">
{/* Rounded card: cream bg, stacks below md */}
<Reveal className="bg-bg-muted flex flex-col md:flex-row gap-8 md:gap-12 items-center px-8 py-8 md:py-0 rounded-md w-full">
{/* Rounded card: cream bg, stacks below sm (moved down from the old md:) */}
<Reveal className="bg-bg-muted flex flex-col sm:flex-row gap-8 sm:gap-12 items-center px-8 py-8 sm:py-0 rounded-md w-full">
{/* Left: copy fixed width from md+ so the form always gets the remaining space.
Icon+text stacked (icon on top, centered) below md: side by
side they squeezed the text into a ~164px column on a 375px
phone (icon width + gap eating most of the card's inner
width), wrapping awkwardly. Row layout with the icon beside
the text is fine again from md+, where the fixed copy-column
width leaves real room. */}
<div className="flex flex-col items-center gap-4 text-center w-full md:flex-row md:items-start md:gap-8 md:text-left md:w-[var(--newsletter-copy-width)] md:py-4 md:shrink-0">
{/* Left: copy fixed width from sm+ so the form always gets the remaining space.
Icon+text stacked (icon on top, centered) below sm: side by
side they squeezed the text into a narrow column on a phone
(icon width + gap eating most of the card's inner width),
wrapping awkwardly. Row layout with the icon beside the text
is fine again from sm+, where the fixed copy-column width
leaves real room. */}
<div className="flex flex-col items-center gap-4 text-center w-full sm:flex-row sm:items-start sm:gap-8 sm:text-left sm:w-[var(--newsletter-copy-width)] sm:py-4 sm:shrink-0">
{/* Decorative envelope icon, tilted -4° as per design.
w-[4rem], not w-16 this project's --spacing-16 is a
@@ -89,20 +89,24 @@ export function Newsletter({
</div>
</div>
{/* Right: form — takes remaining space, centered vertically from md+ */}
<div className="flex w-full md:flex-1 items-center md:self-stretch min-w-0">
{/* Right: form — takes remaining space, centered vertically from sm+ */}
<div className="flex w-full sm:flex-1 items-center sm:self-stretch min-w-0">
{status === "success" ? (
<p className="text-body text-text-primary font-medium">{successMessage}</p>
) : (
<form onSubmit={handleSubmit} className="flex flex-1 flex-col gap-4 min-w-0 w-full">
{/* Input + submit button stacked below lg: (was md:).
The card above already goes side-by-side at md: with a
fixed-width copy column (--newsletter-copy-width), which
only leaves ~200px for this form column at 768px not
enough room for input+button side by side. Stacked
through the whole Tablet range instead, side by side
again once the form column has real room at lg:. */}
{/* Input + submit button stacked below lg:, a deliberate
exception to the site's sm: (640px) structural
consolidation, not a leftover of it. The card above
already goes side-by-side at sm: with a fixed-width
copy column (--newsletter-copy-width), which only
leaves ~128px for this form column at 640px and
~197px at 768px not enough room for input+button
side by side even at the new, lower floor. Stacked
through the whole 640-1023px range instead, side by
side again once the form column has real room (333px)
at lg:. */}
<div className="flex flex-col lg:flex-row gap-4 items-stretch w-full">
<input
ref={emailRef}
+7 -7
View File
@@ -156,14 +156,14 @@ export function NewsletterModal({ open, onClose }: { open: boolean; onClose: ()
<Image alt="" src="/icon-close.png" width={24} height={24} className="size-full object-contain" />
</button>
{/* modal-top: photo + copy/form, stacked below md */}
<div className="flex flex-col md:flex-row items-stretch border-b border-border">
<div className="relative w-full md:flex-1 aspect-[4/3] md:aspect-auto">
{/* modal-top: photo + copy/form, stacked below sm (moved down from the old md:) */}
<div className="flex flex-col sm:flex-row items-stretch border-b border-border">
<div className="relative w-full sm:flex-1 aspect-[4/3] sm:aspect-auto">
<Image
src="/newsletter-modal-photo.jpg"
alt="Notizbuch mit Kaffee und Stift"
fill
sizes="(min-width: 768px) 50vw, 100vw"
sizes="(min-width: 640px) 50vw, 100vw"
className="object-cover"
/>
</div>
@@ -172,8 +172,8 @@ export function NewsletterModal({ open, onClose }: { open: boolean; onClose: ()
{/* -scale-y-100 is required, not just -rotate-4 the SVG
itself is authored upside-down (matches how Newsletter.tsx
uses this exact same asset); without it the icon renders
flipped. Hidden below md: removed on mobile 2026-07-24. */}
<div className="hidden md:block w-16 h-14 -rotate-4 -scale-y-100">
flipped. Hidden below sm: removed on mobile 2026-07-24. */}
<div className="hidden sm:block w-16 h-14 -rotate-4 -scale-y-100">
<Image alt="" src="/newsletter-icon.svg" width={64} height={56} className="w-full h-full" />
</div>
@@ -263,7 +263,7 @@ export function NewsletterModal({ open, onClose }: { open: boolean; onClose: ()
items-start + a fixed icon bounding box (icons have different
native proportions, e.g. the sparkle glyph isn't square) fixes
it without needing the flip trick. */}
<div className="flex flex-col md:flex-row items-start px-8 md:px-20 py-6 md:py-9 gap-8 md:gap-6">
<div className="flex flex-col sm:flex-row items-start px-8 md:px-20 py-6 md:py-9 gap-8 sm:gap-6">
{features.map((f) => (
<div key={f.title} className="flex-1 flex gap-6 items-start w-full">
<div className="relative h-10 w-10 shrink-0 flex items-center justify-center">
+4 -4
View File
@@ -42,14 +42,14 @@ export async function ProductSpotlight() {
// id="spotlight" — the Navbar's "Shop" link becomes an anchor to this
// section instead of navigating to /shop whenever exactly 1 product is
// active (see Navbar.tsx/layout.tsx).
<section id="spotlight" className="w-full bg-bg-base py-12 md:py-16 px-[var(--layout-padding-x)]">
<Reveal className="max-w-[75rem] mx-auto rounded-md flex flex-col md:flex-row gap-8 md:gap-12 items-center p-6 md:p-10">
<div className="group relative w-full md:w-[23.75rem] md:shrink-0 aspect-[410/227] rounded-sm overflow-hidden">
<section id="spotlight" className="w-full bg-bg-base py-12 sm:py-16 px-[var(--layout-padding-x)]">
<Reveal className="max-w-[75rem] mx-auto rounded-md flex flex-col sm:flex-row gap-8 sm:gap-12 items-center p-6 sm:p-10">
<div className="group relative w-full sm:w-[23.75rem] sm:shrink-0 aspect-[410/227] rounded-sm overflow-hidden">
<Image
src={image}
alt={product.name}
fill
sizes="(min-width: 768px) 380px, 100vw"
sizes="(min-width: 640px) 380px, 100vw"
className="object-cover transition-transform duration-500 group-hover:scale-105"
/>
{fullyOutOfStock ? (
+3 -2
View File
@@ -61,8 +61,9 @@ function useActiveSection(sections: TOCSection[]) {
// lg:-only sidebar — same "wide fixed-width block next to content" shape
// as the cart's order-summary sidebar (see figma-to-nextjs skill Gotcha
// #5): a 360px TOC card plus a readable content column already exceeds
// the 768px Tablet floor, so md: wouldn't leave room for a real 2-column
// split at Tablet widths.
// even the site's 640px structural floor, so sm: wouldn't leave room for
// a real 2-column split at Tablet widths — a deliberate exception to the
// site-wide sm: consolidation, not a leftover of it.
//
// Generic over `sections` — originally written just for /versand
// (VersandTOC), generalized once /datenschutz needed the identical
+3 -3
View File
@@ -15,7 +15,7 @@ export function TestimonialsGrid({ testimonials }: { testimonials: Testimonial[]
if (testimonials.length === 0) return null;
return (
<section className="w-full bg-bg-base flex flex-col gap-8 items-center py-12 md:py-16 px-[var(--layout-padding-x)]">
<section className="w-full bg-bg-base flex flex-col gap-8 items-center py-12 sm:py-16 px-[var(--layout-padding-x)]">
<div className="max-w-[1600px] mx-auto w-full flex flex-col gap-8 items-center">
<Reveal
className="font-semibold text-h-emphasis text-text-primary text-center"
@@ -24,11 +24,11 @@ export function TestimonialsGrid({ testimonials }: { testimonials: Testimonial[]
Was andere sagen
</Reveal>
<RevealGroup className="grid grid-cols-1 md:grid-cols-12 gap-6 md:gap-[var(--layout-grid-gap)] w-full">
<RevealGroup className="grid grid-cols-1 sm:grid-cols-12 gap-6 sm:gap-[var(--layout-grid-gap)] w-full">
{testimonials.map((t) => (
<RevealItem
key={t.id}
className="group relative md:col-span-4 bg-bg-muted rounded-xl p-6 flex flex-col gap-4 transition-transform duration-300 hover:-translate-y-1"
className="group relative sm:col-span-4 bg-bg-muted rounded-xl p-6 flex flex-col gap-4 transition-transform duration-300 hover:-translate-y-1"
>
<span
aria-hidden
+15 -9
View File
@@ -30,23 +30,29 @@ export async function Tools() {
</p>
</Reveal>
{/* Tools grid 3 cols md+, stacked below md; internal icon+text layout
stays horizontal at every size, only the outer span changes */}
<RevealGroup className="grid grid-cols-1 md:grid-cols-12 gap-10 md:gap-[var(--layout-grid-gap)] px-[var(--layout-padding-x)] w-full">
{/* Tools grid 3 cols sm+ (moved down from the old md: so the grid
arrives where the fluid floor also sits), stacked below sm;
internal icon+text layout stays horizontal at every size, only
the outer span changes */}
<RevealGroup className="grid grid-cols-1 sm:grid-cols-12 gap-10 sm:gap-[var(--layout-grid-gap)] px-[var(--layout-padding-x)] w-full">
{tools.map((tool) => (
<RevealItem
key={tool.id}
className="md:col-span-4 flex gap-8 items-start rounded-md transition-transform duration-300 hover:-translate-y-1"
className="sm:col-span-4 flex gap-8 items-start rounded-md transition-transform duration-300 hover:-translate-y-1"
>
{/* Icon uniform box, pre-flipped/rotated source asset.
size-14 (56px) is a fixed value at every width (14 isn't
one of this project's fluid spacing-scale steps) smaller
below md: so it doesn't dwarf the title/description text,
below lg: so it doesn't dwarf the title/description text,
which does shrink toward its own fluid floor there. Full
56px only from lg: up at md: (Tablet, where this grid
already switches to 3-up) the title/description are still
fairly close to their own fluid floor, so the full-size
icon read as too big next to them too. */}
56px only from lg: up a deliberate exception to the
site's sm: (640px) structural consolidation: the grid now
switches to 3-up at sm:, but title/description are still
fairly close to their own fluid floor through the whole
640-1023px range, so the full-size icon still reads too big
next to them there. Not re-verified visually narrower
exception kept as-is, same reasoning as Hero's content
sizing. */}
<div className="relative flex items-center justify-center shrink-0 size-11 lg:size-14">
<Image alt="" src={tool.icon} fill sizes="(min-width: 1024px) 56px, 44px" className="object-contain" />
</div>
+3 -3
View File
@@ -11,10 +11,10 @@ export async function TrustRow() {
if (items.length === 0) return null;
return (
<div className="w-full bg-bg-base flex flex-col md:flex-row gap-6 md:gap-12 items-start md:items-center justify-center py-8 px-[var(--layout-padding-x)]">
<div className="w-full bg-bg-base flex flex-col sm:flex-row gap-6 sm:gap-12 items-start sm:items-center justify-center py-8 px-[var(--layout-padding-x)]">
{items.map((item, i) => (
<div key={item.id} className="flex items-center gap-6 md:gap-12">
{i > 0 && <div className="hidden md:block h-10 w-px bg-border" />}
<div key={item.id} className="flex items-center gap-6 sm:gap-12">
{i > 0 && <div className="hidden sm:block h-10 w-px bg-border" />}
<div className="flex gap-4 items-center">
<div className="relative size-8 shrink-0">
<Image alt="" src={item.icon} fill sizes="32px" className="object-contain" />
+57 -53
View File
@@ -2,10 +2,10 @@
/*
* Fluid design tokens every value below scales continuously via clamp()
* between the Tablet breakpoint (768px) and Desktop (1440px). Below 768px
* the clamp() floor freezes the value automatically (no separate mobile
* media query needed for sizing only for structural layout changes,
* see the `md:` breakpoint usage in components).
* between the structural breakpoint (640px) and Desktop (1440px). Below
* 640px the clamp() floor freezes the value automatically (no separate
* mobile media query needed for sizing only for structural layout
* changes, see the `sm:` breakpoint usage in components).
*
* Spacing overrides Tailwind's own numeric scale (--spacing-4 etc.) at the
* exact numbers the styleguide already used (n times 0.25rem equals the
@@ -53,10 +53,10 @@
--shadow-modal: 0 8px 40px rgba(26, 26, 24, 0.16);
--shadow-sidebar: 0 2px 8px rgba(26, 26, 24, 0.08);
/* Type scale (fluid 768->1440) */
--text-display: clamp(2.75rem, 1.3214rem + 2.9762vw, 4rem);
/* Type scale (fluid 640->1440) */
--text-display: clamp(2.75rem, 1.75rem + 2.5vw, 4rem);
--text-display--line-height: 1.1;
--text-h-feature: clamp(1.875rem, 1.1607rem + 1.4881vw, 2.5rem);
--text-h-feature: clamp(1.875rem, 1.375rem + 1.25vw, 2.5rem);
--text-h-feature--line-height: 1.2;
/* New role, added for page-todo-karten's hero heading (48px Desktop)
doesn't match any existing scale step (h-feature=40, h1=36), Figma's
@@ -67,21 +67,21 @@
etc.)" per styleguide.md §2.2 a different, smaller role). Also used
by page-weekly-impulses's hero heading same 48px role, reused
rather than minting a near-duplicate token. */
--text-h-page: clamp(2.5rem, 1.9286rem + 1.1905vw, 3rem);
--text-h-page: clamp(2.5rem, 2.1rem + 1vw, 3rem);
--text-h-page--line-height: 1.15;
--text-h1: clamp(1.75rem, 1.1786rem + 1.1905vw, 2.25rem);
--text-h1: clamp(1.75rem, 1.35rem + 1vw, 2.25rem);
--text-h1--line-height: 1.15;
--text-h2: clamp(1.625rem, 1.1964rem + 0.8929vw, 2rem);
--text-h2: clamp(1.625rem, 1.325rem + 0.75vw, 2rem);
--text-h2--line-height: 1.25;
--text-h-section: clamp(1.4375rem, 1.0804rem + 0.744vw, 1.75rem);
--text-h-section: clamp(1.4375rem, 1.1875rem + 0.625vw, 1.75rem);
--text-h-section--line-height: 1.3;
--text-h-emphasis: clamp(1.25rem, 0.9643rem + 0.5952vw, 1.5rem);
--text-h-emphasis: clamp(1.25rem, 1.05rem + 0.5vw, 1.5rem);
--text-h-emphasis--line-height: 1.4;
--text-h3: clamp(1.1875rem, 0.9732rem + 0.4464vw, 1.375rem);
--text-h3: clamp(1.1875rem, 1.0375rem + 0.375vw, 1.375rem);
--text-h3--line-height: 1.35;
--text-h-small: clamp(1.125rem, 0.9821rem + 0.2976vw, 1.25rem);
--text-h-small: clamp(1.125rem, 1.025rem + 0.25vw, 1.25rem);
--text-h-small--line-height: 1.4;
--text-h4: clamp(1rem, 0.8571rem + 0.2976vw, 1.125rem);
--text-h4: clamp(1rem, 0.9rem + 0.25vw, 1.125rem);
--text-h4--line-height: 1.4;
--text-body: 1rem;
--text-body--line-height: 1.6;
@@ -90,21 +90,21 @@
--text-label: 0.75rem;
--text-label--line-height: 1.4;
/* Spacing scale (fluid 768->1440) overrides Tailwind's default n*0.25rem
/* Spacing scale (fluid 640->1440) overrides Tailwind's default n*0.25rem
at these exact steps, so p-4/gap-6/px-20/etc. become fluid automatically */
--spacing-1: clamp(0.25rem, 0.25rem + 0vw, 0.25rem);
--spacing-2: clamp(0.5rem, 0.5rem + 0vw, 0.5rem);
--spacing-3: clamp(0.625rem, 0.4821rem + 0.2976vw, 0.75rem);
--spacing-4: clamp(0.875rem, 0.7321rem + 0.2976vw, 1rem);
--spacing-5: clamp(1rem, 0.7143rem + 0.5952vw, 1.25rem);
--spacing-6: clamp(1.25rem, 0.9643rem + 0.5952vw, 1.5rem);
--spacing-8: clamp(1.5rem, 0.9286rem + 1.1905vw, 2rem);
--spacing-10: clamp(1.75rem, 0.8929rem + 1.7857vw, 2.5rem);
--spacing-12: clamp(2rem, 0.8571rem + 2.381vw, 3rem);
--spacing-16: clamp(2.5rem, 0.7857rem + 3.5714vw, 4rem);
--spacing-20: clamp(3rem, 0.7143rem + 4.7619vw, 5rem);
--spacing-24: clamp(3.5rem, 0.6429rem + 5.9524vw, 6rem);
--spacing-30: clamp(4rem, 0rem + 8.3333vw, 7.5rem);
--spacing-3: clamp(0.625rem, 0.525rem + 0.25vw, 0.75rem);
--spacing-4: clamp(0.875rem, 0.775rem + 0.25vw, 1rem);
--spacing-5: clamp(1rem, 0.8rem + 0.5vw, 1.25rem);
--spacing-6: clamp(1.25rem, 1.05rem + 0.5vw, 1.5rem);
--spacing-8: clamp(1.5rem, 1.1rem + 1vw, 2rem);
--spacing-10: clamp(1.75rem, 1.15rem + 1.5vw, 2.5rem);
--spacing-12: clamp(2rem, 1.2rem + 2vw, 3rem);
--spacing-16: clamp(2.5rem, 1.3rem + 3vw, 4rem);
--spacing-20: clamp(3rem, 1.4rem + 4vw, 5rem);
--spacing-24: clamp(3.5rem, 1.5rem + 5vw, 6rem);
--spacing-30: clamp(4rem, 1.2rem + 7vw, 7.5rem);
/* Transitions */
--transition-fast: 150ms ease;
@@ -116,18 +116,22 @@
/* Layout not part of Tailwind's numeric spacing scale, referenced via
arbitrary-value syntax (e.g. px-[var(--layout-padding-x)]) */
--layout-max-width: 75rem; /* 1200px */
--layout-padding-x: clamp(2rem, -1.4286rem + 7.1429vw, 5rem);
--layout-grid-gap: clamp(1rem, 0.4286rem + 1.1905vw, 1.5rem);
--layout-grid-gap-lg: clamp(1.25rem, 0.3929rem + 1.7857vw, 2rem);
--layout-padding-x: clamp(2rem, -0.4rem + 6vw, 5rem);
--layout-grid-gap: clamp(1rem, 0.6rem + 1vw, 1.5rem);
--layout-grid-gap-lg: clamp(1.25rem, 0.65rem + 1.5vw, 2rem);
/* One-off fluid values that aren't part of a broader scale */
--hero-avatar-size: clamp(2.25rem, 1.6071rem + 1.3393vw, 2.8125rem);
--hero-avatar-size: clamp(2.25rem, 1.8rem + 1.125vw, 2.8125rem);
/* Newsletter copy column was a hard 38.25rem, which didn't leave enough
room for the form column at the 768px tablet floor (only 656px of
content width available for 612px column plus 56px gap), crushing the
input/button/privacy text to near-zero width and overflowing the page.
Scales down to 23rem at 768px instead. */
--newsletter-copy-width: clamp(23rem, 5.5714rem + 36.3095vw, 38.25rem);
room for the form column at the 640px structural floor (only ~576px of
content width available for a 612px column plus gap at that width),
crushing the input/button/privacy text to near-zero width and
overflowing the page. Scales down to 23rem at 640px instead. Note: the
inner input+button row still switches to a row layout at `lg:` (1024px),
not at the site's `sm:` (640px) structural line even at this reduced
floor there isn't enough room for input+button side by side below
~900px, see Newsletter.tsx's own comment. */
--newsletter-copy-width: clamp(23rem, 10.8rem + 30.5vw, 38.25rem);
/* Divider section icons (arrow separators, sparkle) previously hard
rem values while the "Klarheit/Fokus/Entlastung" text next to them
@@ -135,30 +139,30 @@
while the words scaled. Same fluid(minPx, maxPx) formula as the rest
of this file, floor ~82.5% of the Desktop value per the styleguide's
80-85% Mobile-ratio guidance. */
--divider-arrow-w: clamp(2.125rem, 1.5536rem + 1.1905vw, 2.625rem);
--divider-arrow-h: clamp(0.625rem, 0.4821rem + 0.2976vw, 0.75rem);
--divider-sparkle-w: clamp(1.5625rem, 1.1554rem + 0.8482vw, 1.91875rem);
--divider-sparkle-h: clamp(2.0625rem, 1.5554rem + 1.0565vw, 2.50625rem);
--divider-sparkle-inner-w: clamp(1.4375rem, 1.09875rem + 0.706vw, 1.734rem);
--divider-sparkle-inner-h: clamp(1.9375rem, 1.4375rem + 1.0417vw, 2.375rem);
--divider-arrow-w: clamp(2.125rem, 1.725rem + 1vw, 2.625rem);
--divider-arrow-h: clamp(0.625rem, 0.525rem + 0.25vw, 0.75rem);
--divider-sparkle-w: clamp(1.5625rem, 1.2775rem + 0.7125vw, 1.9188rem);
--divider-sparkle-h: clamp(2.0625rem, 1.7075rem + 0.8875vw, 2.5063rem);
--divider-sparkle-inner-w: clamp(1.4375rem, 1.2003rem + 0.593vw, 1.734rem);
--divider-sparkle-inner-h: clamp(1.9375rem, 1.5875rem + 0.875vw, 2.375rem);
/* Own token, mirrors --text-h2's clamp() exactly rather than the Word
component reading var(--text-h2) directly that's what lets the
mobile override below shrink just this component's words without
touching every other text-h2 heading site-wide. */
--divider-word-size: clamp(1.625rem, 1.1964rem + 0.8929vw, 2rem);
--divider-word-size: clamp(1.625rem, 1.325rem + 0.75vw, 2rem);
}
/* This project's fluid() scale (see fluid.ts) is calibrated for the
768-1440px Tablet-Desktop range and floors out at the 768px value for
any narrower viewport (clamp()'s MIN bound) by design, see the other
fluid tokens above. Divider is the one spot that floor doesn't work:
the "Klarheit → Fokus → Entlastung" phrase plus its connector icons
needs ~550px of width to lay out on one row even at the 768px floor
size, far more than a phone's ~310px content width. Below Tailwind's
sm: breakpoint, shrink these tokens further so the phrase gets much
closer to fitting on one row instead of stacking into three separate
centered lines (see Divider.tsx's gap-x-3/gap-3 mobile overrides,
same breakpoint). Scoped to these component-only tokens, not
640-1440px structural-to-Desktop range and floors out at the 640px value
for any narrower viewport (clamp()'s MIN bound) by design, see the
other fluid tokens above. Divider is the one spot that floor doesn't
work: the "Klarheit → Fokus → Entlastung" phrase plus its connector
icons needs ~550px of width to lay out on one row even at the 640px
floor size, far more than a phone's ~310px content width. Below
Tailwind's sm: breakpoint, shrink these tokens further so the phrase
gets much closer to fitting on one row instead of stacking into three
separate centered lines (see Divider.tsx's gap-x-3/gap-3 mobile
overrides, same breakpoint). Scoped to these component-only tokens, not
--text-h2 itself. */
@media (max-width: 639px) {
:root {
+1 -1
View File
@@ -1,4 +1,4 @@
const MIN_VW = 768;
const MIN_VW = 640;
const MAX_VW = 1440;
/**
+9 -9
View File
@@ -39,7 +39,7 @@ const steps = [
export function HowItWorks() {
return (
<section className="w-full bg-bg-base flex flex-col gap-12 items-center py-12 md:py-16 px-[var(--layout-padding-x)]">
<section className="w-full bg-bg-base flex flex-col gap-12 items-center py-12 sm:py-16 px-[var(--layout-padding-x)]">
<Reveal className="flex flex-col gap-2 items-center text-center">
<p
className="font-semibold text-h-emphasis text-text-primary"
@@ -53,14 +53,14 @@ export function HowItWorks() {
</Reveal>
{/* Same pattern as /todo-cards's HowItWorks: arrows always visible
(rotated to point down while stacked below md:), RevealGroup/
(rotated to point down while stacked below sm:), RevealGroup/
RevealItem stagger the steps in. lg:px-[10rem] mirrors Figma's
Desktop-only px-[160px] inset (no fluid token pure aesthetic
narrowing, fine to just drop below lg:). */}
<RevealGroup className="flex flex-col md:flex-row gap-8 items-center md:items-start w-full lg:px-[10rem]">
<RevealGroup className="flex flex-col sm:flex-row gap-8 items-center sm:items-start w-full lg:px-[10rem]">
{steps.map((step, i) => (
<Fragment key={step.title}>
<RevealItem className="group flex flex-col gap-4 items-center text-center flex-1 max-w-xs md:max-w-none">
<RevealItem className="group flex flex-col gap-4 items-center text-center flex-1 max-w-xs sm:max-w-none">
<Image
src={step.icon}
alt=""
@@ -72,15 +72,15 @@ export function HowItWorks() {
<p className="text-body-sm text-text-primary text-center">{step.desc}</p>
</RevealItem>
{i < steps.length - 1 && (
// md:mt-[1.5rem] centers the arrow on the h-16 icon above it,
// sm:mt-[1.5rem] centers the arrow on the h-16 icon above it,
// same technique as todo-cards'/Challenge's own step
// connector. Below md: pulled up with a negative margin so it
// connector. Below sm: pulled up with a negative margin so it
// sits nearer the icon row above it instead of dead-center in
// the whole gap between steps (fixed 2026-07-24, consistency
// with Challenge's icon-at-top layout). Bigger below md:
// with Challenge's icon-at-top layout). Bigger below sm:
// (w-8 h-8, was w-6 h-6) per explicit feedback.
<div className="flex items-center justify-center shrink-0 -mt-2 md:mt-[1.5rem]">
<StepArrow className="w-8 h-8 rotate-90 md:w-10 md:h-4 md:rotate-0" />
<div className="flex items-center justify-center shrink-0 -mt-2 sm:mt-[1.5rem]">
<StepArrow className="w-8 h-8 rotate-90 sm:w-10 sm:h-4 sm:rotate-0" />
</div>
)}
</Fragment>
@@ -42,9 +42,10 @@ export function WeeklyImpulsesHero() {
<section className="bg-bg-base w-full overflow-hidden">
{/* Same lg:-only structural exception as Home/todo-cards Hero (see
figma-to-nextjs skill Gotcha #5) a wide fixed-ratio photo next
to a text column gets too cramped at Tablet widths under the
site-wide md: convention. lg:items-center removed see the
breadcrumb/centering comment below, same fix as /todo-cards. */}
to a text column gets too cramped at Tablet widths even under
the site-wide sm: (640px) structural consolidation. lg:items-
center removed see the breadcrumb/centering comment below,
same fix as /todo-cards. */}
{/* pt-10 md:pt-12, no lg override same fix and same reasoning as
/todo-cards's Hero: this is now the only thing positioning the
breadcrumb, and it must match /todo-cards's and /challenge's
+6 -6
View File
@@ -140,11 +140,11 @@ export default function NotFound() {
<div className="h-[0.125rem] w-8 bg-brand" />
</Reveal>
<RevealGroup className="grid grid-cols-1 md:grid-cols-3 gap-10 md:gap-8 w-full max-w-[64rem] divide-y md:divide-y-0 md:divide-x divide-border">
<RevealGroup className="grid grid-cols-1 sm:grid-cols-3 gap-10 sm:gap-8 w-full max-w-[64rem] divide-y sm:divide-y-0 sm:divide-x divide-border">
{helpLinks.map((link) => (
<RevealItem
key={link.href}
className="group flex flex-col items-center text-center gap-3 pt-10 md:pt-0 first:pt-0 px-4 transition-transform duration-200 hover:-translate-y-1"
className="group flex flex-col items-center text-center gap-3 pt-10 sm:pt-0 first:pt-0 px-4 transition-transform duration-200 hover:-translate-y-1"
>
<div className="flex size-14 items-center justify-center rounded-full bg-bg-muted text-brand transition-transform duration-300 group-hover:scale-110">
{link.icon}
@@ -180,17 +180,17 @@ export default function NotFound() {
reusing an unrelated existing asset same real-photo
extraction convention the Figma rebuild used, just done on
the flat mockup image instead of in Figma. */}
<Reveal className="relative w-full flex items-stretch h-[14rem] md:h-[16rem] bg-bg-muted overflow-hidden">
<div className="relative w-full md:w-[45%] shrink-0">
<Reveal className="relative w-full flex items-stretch h-[14rem] sm:h-[16rem] bg-bg-muted overflow-hidden">
<div className="relative w-full sm:w-[45%] shrink-0">
<Image
alt=""
src="/404-testimonial-photo.jpg"
width={810}
height={291}
sizes="(min-width: 768px) 45vw, 100vw"
sizes="(min-width: 640px) 45vw, 100vw"
className="absolute -inset-1 w-[calc(100%+0.5rem)] h-[calc(100%+0.5rem)] object-cover"
/>
<div className="hidden md:block absolute inset-y-0 right-0 w-40 bg-gradient-to-l from-bg-muted to-transparent" />
<div className="hidden sm:block absolute inset-y-0 right-0 w-40 bg-gradient-to-l from-bg-muted to-transparent" />
</div>
<div className="flex-1 flex flex-col justify-center gap-3 px-8 md:px-16 py-6">
<p
+3 -3
View File
@@ -31,7 +31,7 @@ export async function ProductGrid() {
return (
<section className="w-full bg-bg-base flex flex-col pb-16 md:pb-20 px-[var(--layout-padding-x)]">
<RevealGroup className="grid grid-cols-1 md:grid-cols-12 gap-6 md:gap-[var(--layout-grid-gap)] w-full">
<RevealGroup className="grid grid-cols-1 sm:grid-cols-12 gap-6 sm:gap-[var(--layout-grid-gap)] w-full">
{products.map((product) => {
const discount = discountPercent(product.price, product.compareAtPrice);
const taxRate = effectiveTaxRate(product, defaultTaxRate);
@@ -48,14 +48,14 @@ export async function ProductGrid() {
return (
<RevealItem
key={product.id}
className="group md:col-span-3 bg-bg-base border border-border rounded-md overflow-hidden flex flex-col h-full transition-transform duration-300 hover:-translate-y-1"
className="group sm:col-span-3 bg-bg-base border border-border rounded-md overflow-hidden flex flex-col h-full transition-transform duration-300 hover:-translate-y-1"
>
<div className="relative w-full aspect-[276/210] overflow-hidden">
<Image
src={product.image}
alt={product.name}
fill
sizes="(min-width: 768px) 25vw, 100vw"
sizes="(min-width: 640px) 25vw, 100vw"
className={`object-cover transition-transform duration-500 group-hover:scale-105 ${fullyOutOfStock ? "opacity-60" : ""}`}
/>
{fullyOutOfStock ? (
+8 -8
View File
@@ -32,7 +32,7 @@ const steps = [
export function HowItWorks() {
return (
<section className="w-full bg-bg-base flex flex-col gap-12 items-center py-12 md:py-16 px-[var(--layout-padding-x)]">
<section className="w-full bg-bg-base flex flex-col gap-12 items-center py-12 sm:py-16 px-[var(--layout-padding-x)]">
<Reveal className="flex flex-col gap-2 items-center text-center">
<p
className="font-semibold text-h-emphasis text-text-primary"
@@ -47,17 +47,17 @@ export function HowItWorks() {
{/* Arrows always visible (see Divider lesson hiding them below a
breakpoint isn't the fix), just rotated 90° to point down while
stacked below md:. lg:px-[16.25rem] mirrors Figma's Desktop-only
stacked below sm:. lg:px-[16.25rem] mirrors Figma's Desktop-only
px-[260px] inset that value doesn't get its own fluid token
since it's a pure aesthetic narrowing, not something that breaks
if it's just absent below lg:. RevealGroup/RevealItem stagger the
3 steps in on scroll, same pattern as Tools.tsx's card grid
arrows aren't part of the stagger (purely decorative connectors,
not content), they just sit in the DOM between items. */}
<RevealGroup className="flex flex-col md:flex-row gap-8 items-center md:items-start w-full lg:px-[16.25rem]">
<RevealGroup className="flex flex-col sm:flex-row gap-8 items-center sm:items-start w-full lg:px-[16.25rem]">
{steps.map((step, i) => (
<Fragment key={step.title}>
<RevealItem className="group flex flex-col gap-4 items-center text-center flex-1 max-w-xs md:max-w-none">
<RevealItem className="group flex flex-col gap-4 items-center text-center flex-1 max-w-xs sm:max-w-none">
{/* Explicit per-icon width/height (real pixel dimensions, not
a guessed/uniform size) matters since one of the three
source PNGs isn't square (icon-step-2 is 180x168); passing
@@ -79,13 +79,13 @@ export function HowItWorks() {
<p className="text-body-sm text-text-primary text-center">{step.desc}</p>
</RevealItem>
{i < steps.length - 1 && (
// md:mt-[1.625rem] (26px) centers the arrow on the h-16
// sm:mt-[1.625rem] (26px) centers the arrow on the h-16
// (64px) icon above it — (64 - arrow's own 16px height) / 2
// — same margin-based centering technique as Challenge's
// step connector, not just the same icon asset. Bigger below
// md: (w-8 h-8, was w-6 h-6) per explicit feedback.
<div className="flex items-center justify-center shrink-0 md:mt-[1.5rem]">
<StepArrow className="w-8 h-8 rotate-90 md:w-10 md:h-4 md:rotate-0" />
// sm: (w-8 h-8, was w-6 h-6) per explicit feedback.
<div className="flex items-center justify-center shrink-0 sm:mt-[1.5rem]">
<StepArrow className="w-8 h-8 rotate-90 sm:w-10 sm:h-4 sm:rotate-0" />
</div>
)}
</Fragment>
+2 -1
View File
@@ -50,7 +50,8 @@ export async function TodoKartenHero() {
{/* Same lg:-only structural exception as the Home Hero (see
figma-to-nextjs skill Gotcha #5) a wide fixed-ratio photo next
to a text column is exactly the shape that gets too cramped at
Tablet widths under the site-wide md: convention. lg:items-center
Tablet widths even under the site-wide sm: (640px) structural
consolidation. lg:items-center
removed (default grid align-items is stretch) that's what lets
the breadcrumb below sit at a fixed Y position across every hero
section on the site instead of shifting per page depending on how