Fix homepage Tablet layout: Hero grid, Tools icon, About columns, Newsletter/Footer stacking

- Hero.tsx: structural breakpoint reverted from lg: back to md: — the
  smaller CTA/subtitle/icon sizes added during the Mobile pass fit
  comfortably in the ~283px Tablet column, so the original 3-line-wrap
  problem that justified lg: doesn't recur. Tablet gets the real 5/7
  grid (image beside text) again instead of a stacked mobile layout.
- Tools.tsx: full-size (56px) card icon pushed from md: to lg: — at
  Tablet it dwarfed the still-close-to-floor title/description text.
- About.tsx: text/photo flex ratio swapped at Tablet (text gets the
  bigger share, no overlap) vs. the original ratio + overlap trick from
  lg: up, where it was designed for — Tablet's text column was too
  narrow for its fixed-width statement + quote/bio row otherwise.
- Newsletter.tsx/Footer.tsx: the input+button row and the logo/handle/
  legal-links row both went side-by-side at md:, but their surrounding
  columns didn't leave enough width at 768px — pushed to lg:flex-row.
This commit is contained in:
Marco
2026-07-24 21:20:47 +00:00
parent 797d9d42fe
commit 7b4b54a9ac
6 changed files with 115 additions and 59 deletions
+34
View File
@@ -1479,6 +1479,40 @@ while auditing every page's metadata were fixed in the same pass: 3
route (now uses the real order number), and `/shop`/`/blog` were missing an
Open Graph image.
## Tablet responsive fixes (2026-07-24)
A follow-up to the Mobile pass above — testing at real Tablet widths
(768-1023px) surfaced a few spots where the Mobile-focused fixes had left
Tablet worse off than before, or where a component's own structural
breakpoint no longer needed to be as conservative as originally set:
- **`Hero.tsx` reverted from `lg:` back to `md:` as its structural
breakpoint.** It was moved to `lg:` earlier (see the figma-to-nextjs
skill's Gotcha #5) because at `md:col-span-5` the text column was only
~283px at 768px and the then-full-size CTA/subtitle text made the whole
row wrap to 3 cramped lines. The smaller fixed CTA/subtitle/icon sizes
added during the Mobile pass (for true phone widths) are well under half
that column's width, so the original wrapping problem doesn't recur —
moving back to `md:` means Tablet gets the real 5/7 grid (image beside
text) again instead of a phone-style stacked layout. Every `lg:`-gated
size override in that file moved to `md:` to match.
- **`Tools.tsx`'s card icon** — full 56px size pushed from `md:` to `lg:`;
at Tablet the title/description text is still close to its own fluid
floor, and the full-size icon read as too big next to it.
- **`About.tsx`'s text/photo columns** — the Desktop overlap layout
(`flex-[1.4_0_0]` on the photo, `-ml-48`, gradient) kicked in from `md:`
already, giving the text column (fixed against the smaller 1:1.4 share)
too little room for its fixed-width statement + quote/bio row at Tablet.
The ratio is swapped at Tablet (text gets the bigger 1.4 share, photo the
smaller one, no overlap) and reverts to the original ratio + overlap only
from `lg:` up, where it was designed for.
- **`Newsletter.tsx`'s input+submit row** and **`Footer.tsx`'s logo/handle/
legal-links row** — both went side-by-side at `md:`, but the columns
around them (the newsletter card's fixed-width copy column; the footer's
3 groups sharing one `justify-between` row) didn't leave enough width at
768px. Both pushed from `md:flex-row` to `lg:flex-row` — stacked through
the whole Tablet range, side by side again once there's real room.
## Tests
`npm run test:unit` (Vitest, `node` environment, no jsdom/Next.js runtime
+19 -10
View File
@@ -6,9 +6,16 @@ export function About() {
<section id="ueber-bjoern" className="bg-bg-dark flex flex-col md:flex-row md:items-stretch w-full">
{/* Text content relative + z-10 so it renders above the overlapping
photo at md+. Comes first in DOM at every breakpoint (no reorder
here unlike Hero, there's no conversion CTA at stake). */}
<Reveal className="flex flex-col gap-4 justify-center px-[var(--layout-padding-x)] py-8 md:flex-[1_0_0] min-w-0 relative z-10">
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">
{/* Large serif statement — width-constrained as per design */}
<p
@@ -68,11 +75,13 @@ export function About() {
</div>
</Reveal>
{/* Author photo overlaps the text column via -ml-48 from md+ only
(that overlap trick has nothing to blend into once stacked);
plain full-width photo below the text on Mobile. */}
{/* 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. */}
<Reveal
className="relative overflow-hidden w-full md:flex-[1.4_0_0] md:-ml-48"
className="relative overflow-hidden w-full md:flex-[1_0_0] lg:flex-[1.4_0_0] lg:-ml-48"
style={{ minHeight: "14rem" }}
delay={0.15}
>
@@ -80,11 +89,11 @@ export function About() {
alt="Björn"
src="/about-author.jpg"
fill
sizes="(min-width: 768px) 58vw, 100vw"
sizes="(min-width: 1024px) 58vw, (min-width: 768px) 42vw, 100vw"
className="object-cover object-center pointer-events-none"
/>
{/* Left gradient: wide enough to cover the text-column overlap — md+ only */}
<div className="hidden md: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 — 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" />
</Reveal>
</section>
+6 -2
View File
@@ -18,8 +18,12 @@ export function Footer() {
{/* Footer inner — max-width 1280px, centered */}
<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 md */}
<div className="flex flex-col md:flex-row items-center md:justify-between gap-6 md:gap-0 px-8 md:px-16 w-full">
{/* 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">
{/* Logo: "einfach produktiv" white + "." gold */}
<div className="flex items-center p-2 shrink-0">
+41 -42
View File
@@ -11,7 +11,7 @@ function HeroImage() {
alt=""
fill
priority
sizes="(min-width: 1024px) 58vw, 100vw"
sizes="(min-width: 768px) 58vw, 100vw"
className="object-cover"
style={{
WebkitMaskImage:
@@ -28,36 +28,39 @@ function HeroImage() {
export function Hero() {
return (
<section className="bg-bg-base w-full overflow-hidden">
{/* Structural breakpoint is lg: (1024px) here, not the site-wide md:
(768px) a documented exception (see Gotcha in the figma-to-nextjs
skill). At md:col-span-5 the text column was only ~320px at
768-1023px viewports, too narrow for the heading/CTA/social-proof
row (which wrapped to 3 cramped lines). Staying stacked full-width
through the whole Tablet range and only splitting into the 5/7
grid once there's real room (1024px) fixes that without touching
the 5/7 ratio itself, which is fine once it has space. */}
<div className="flex flex-col lg:grid lg:grid-cols-12 lg:items-center gap-8 lg:gap-[var(--layout-grid-gap)] pt-10 md:pt-12 lg:pt-0">
{/* Structural breakpoint is md: (768px), same as the site-wide
convention briefly moved to lg: (see the figma-to-nextjs
skill's Gotcha #5) because at md:col-span-5 the text column was
only ~283px at 768px and the full-size CTA/subtitle text made the
heading/CTA/social-proof row wrap to 3 cramped lines. Reverted
2026-07-24: the CTA/subtitle/icon sizing added since then for
true mobile widths (see each one's own comment below) uses fixed
sizes well under half that column width, so the original problem
doesn't recur moving back to md: means Tablet gets the real
5/7 grid (image beside text) instead of stacking full-width like
a phone. */}
<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">
{/* 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 lg:order-none lg:col-span-5 flex flex-col gap-7 items-start pl-[var(--layout-padding-x)] pr-10 lg:pr-0">
<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">
{/* Heading forced break after "darf" only in the sm-lg tablet
range (Hero stacked full-width, natural wrap there landed
{/* Heading forced break after "darf" only in the sm-md tablet
range (Hero stacked full-width there, natural wrap landed
awkwardly). Removed at true mobile widths (below sm:) 2026-07-24
narrower still, natural wrap there reads fine, and the
forced break made "darf" the whole first line, leaving very
little text above the CTA. Natural wrap from lg: up too, once
little text above the CTA. Natural wrap from md: up too, once
the 5/12 grid gives the column real width again. */}
<p
className="font-semibold leading-[0] shrink-0 text-[0px] text-text-primary"
style={{ fontFamily: "var(--font-playfair)" }}
>
<span className="text-display">
Produktivität darf<br className="hidden sm:inline lg:hidden" /> sich leicht anfühlen
Produktivität darf<br className="hidden sm:inline md:hidden" /> sich leicht anfühlen
</span>
{/* Brand's signature orange dot (also in the logo/footer)
bouncy pop-in once the heading scrolls into view, timed to
@@ -70,10 +73,10 @@ export function Hero() {
</PopIn>
</p>
{/* Subheading smaller fixed size below lg:, text-h-emphasis's
{/* Subheading smaller fixed size below md:, text-h-emphasis's
own 20px floor read too large next to the now-smaller mobile
CTA text. leading shrinks to match, not just font-size. */}
<p className="font-semibold leading-[1.75rem] lg:leading-[2.375rem] min-w-full shrink-0 text-text-primary text-[1rem] lg:text-h-emphasis w-[min-content] [word-break:break-word] not-italic">
<p className="font-semibold leading-[1.75rem] md:leading-[2.375rem] min-w-full shrink-0 text-text-primary text-[1rem] md:text-h-emphasis w-[min-content] [word-break:break-word] not-italic">
Für Menschen mit Familie, Verantwortung und zu wenig Zeit
</p>
@@ -82,28 +85,28 @@ export function Hero() {
href="/challenge"
className="flex gap-4 items-center justify-center overflow-clip px-6 py-3 rounded-sm shrink-0 max-w-full bg-brand hover:brightness-95 active:scale-[0.97] transition-all"
>
{/* Letting this wrap to two lines below lg: (tried 2026-07-24)
{/* Letting this wrap to two lines below md: (tried 2026-07-24)
put the icon beside a two-line text block, which read as
broken rather than intentional. Smaller fixed size below
lg: instead, so the full phrase fits on one line within
md: instead, so the full phrase fits on one line within
the stacked mobile column's width — text-h3's own 19px
floor was still too wide for that at ~236px of available
text budget on a 375px phone. */}
<span className="font-semibold leading-[2.375rem] text-text-primary text-[0.8125rem] lg:text-h3 whitespace-nowrap not-italic">
<span className="font-semibold leading-[2.375rem] text-text-primary text-[0.8125rem] md:text-h3 whitespace-nowrap not-italic">
Starte mit der 7-Tage-Challenge
</span>
{/* Scaled down to match the smaller mobile CTA text (same
~0.76 aspect ratio as the lg: size), full size again from
lg: up alongside text-h3. */}
<div className="relative h-[0.8125rem] w-[1.0625rem] lg:h-[1.1875rem] lg:w-[1.5625rem] shrink-0">
<Image alt="" src="/icon-check.svg" fill sizes="(min-width: 1024px) 26px, 17px" />
~0.76 aspect ratio as the md: size), full size again from
md: up alongside text-h3. */}
<div className="relative h-[0.8125rem] w-[1.0625rem] md:h-[1.1875rem] md:w-[1.5625rem] shrink-0">
<Image alt="" src="/icon-check.svg" fill sizes="(min-width: 768px) 26px, 17px" />
</div>
</Link>
{/* Social proof always avatars-then-text on two lines below
lg: (not just when it happens to overflow), single row again
from lg: up where the real column width fits it fine. */}
<div className="flex flex-col lg:flex-row gap-3 items-center justify-center overflow-clip shrink-0 w-full">
md: (not just when it happens to overflow), single row again
from md: up where the real column width fits it fine. */}
<div className="flex flex-col md:flex-row gap-3 items-center justify-center overflow-clip shrink-0 w-full">
{/* Avatars — gap 2px, not overlapping */}
<div className="flex gap-[0.125rem] items-center shrink-0">
{["/avatar-1.jpg", "/avatar-2.jpg", "/avatar-3.jpg"].map((src, i) => (
@@ -123,33 +126,29 @@ export function Hero() {
</Reveal>
{/* Image bleeds to the true edge at every breakpoint (never
padded). Below lg: (stacked layout) the full 887:583 aspect
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 lg:+ (grid, image only
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. A small negative top margin below lg: pulls it up to
slightly tuck under the text block (deliberately less than the
social-proof row's height, so it never covers the avatars/text).
Scoped to md:-only (mt-0 at base and again at lg:) it's a
Tablet-specific touch, not a permanent effect. 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 lg: whileInView's -80px
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
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 lg: instead; Reveal's fade
kept from lg: up, where the image is beside the text with
Plain, always-visible image below md: instead; Reveal's fade
kept from md: up, where the image is beside the text with
plenty of room and this was never an issue. */}
<div className="order-2 lg:hidden relative w-full aspect-[887/583] max-h-[16rem] md:max-h-[22rem] mt-0 md:-mt-6">
<div className="order-2 md:hidden relative w-full aspect-[887/583] max-h-[16rem]">
<HeroImage />
</div>
<Reveal
className="hidden lg:block lg:col-span-7 relative w-full aspect-[887/583]"
className="hidden md:block md:col-span-7 relative w-full aspect-[887/583]"
delay={0.15}
>
<HeroImage />
+8 -2
View File
@@ -98,8 +98,14 @@ export function Newsletter({
) : (
<form onSubmit={handleSubmit} className="flex flex-1 flex-col gap-4 min-w-0 w-full">
{/* Input + submit button — stacked below md, side by side from md+ */}
<div className="flex flex-col md:flex-row gap-4 items-stretch 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:. */}
<div className="flex flex-col lg:flex-row gap-4 items-stretch w-full">
<input
ref={emailRef}
type="email"
+7 -3
View File
@@ -42,9 +42,13 @@ export async function Tools() {
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,
which does shrink toward its own fluid floor there. */}
<div className="relative flex items-center justify-center shrink-0 size-11 md:size-14">
<Image alt="" src={tool.icon} fill sizes="(min-width: 768px) 56px, 44px" className="object-contain" />
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. */}
<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>
{/* Card content self-stretch + h-full + justify-between so