From ca0eddfde818e7af91f07c411f3f29492cfe0b08 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 4 Oct 2026 15:50:26 +0200 Subject: [PATCH] feat: a shorter page on phones On a phone the homepage was about 29 screens long; it is now about 22. The desktop layout is unchanged to the pixel. Lists of equal items (automation notes, project fit, principles, reasons for a web presentation, by hand vs. automated) are swiped through side by side (SwipeRow) instead of stacked. The four service cards form a compact 2x2 grid, step lists spell out only the current step, the workflow legend and the per-project task lists are left out, and sections use less padding. Co-Authored-By: Claude Opus 5.5 --- apps/web/app/[locale]/page.tsx | 23 ++++++------- apps/web/app/globals.css | 32 +++++++++++++++++++ apps/web/src/components/SwipeRow.tsx | 18 +++++++++++ apps/web/src/components/home/Block.tsx | 4 +-- apps/web/src/components/home/ClosingCta.tsx | 2 +- apps/web/src/components/home/PathPicker.tsx | 21 ++++++------ .../illustrations/AutomationFlow.tsx | 9 +++--- .../components/illustrations/PipelineRun.tsx | 5 ++- .../components/illustrations/WorkShowcase.tsx | 2 +- 9 files changed, 87 insertions(+), 29 deletions(-) create mode 100644 apps/web/src/components/SwipeRow.tsx diff --git a/apps/web/app/[locale]/page.tsx b/apps/web/app/[locale]/page.tsx index 61ea35d..c52aafe 100644 --- a/apps/web/app/[locale]/page.tsx +++ b/apps/web/app/[locale]/page.tsx @@ -19,6 +19,7 @@ import { } from "@/src/components/illustrations/WebPresentation"; import { WorkShowcase } from "@/src/components/illustrations/WorkShowcase"; import { Reveal } from "@/src/components/Reveal"; +import { SwipeRow } from "@/src/components/SwipeRow"; import { contentFor } from "@/src/content"; import type { SplitTitle } from "@/src/content/types"; import { DEFAULT_LOCALE, parseLocale } from "@/src/domain/locale"; @@ -122,7 +123,7 @@ export default async function LandingPage({ params }: Props) { -
+ {automation.notes.map((group, i) => (
@@ -140,7 +141,7 @@ export default async function LandingPage({ params }: Props) {
))} -
+ -
+ {presentations.reasons.map((reason, i) => (
@@ -176,7 +177,7 @@ export default async function LandingPage({ params }: Props) {

))} -
+
@@ -220,7 +221,7 @@ export default async function LandingPage({ params }: Props) { label={technology.label} title={} > - <div className="grid grid-cols-1 gap-12 md:grid-cols-3 md:gap-10"> + <SwipeRow className="grid grid-cols-1 gap-12 md:grid-cols-3 md:gap-10"> {technology.fit.map((group, i) => ( <Reveal key={group.title} delay={0.1 * i}> <div @@ -242,12 +243,12 @@ export default async function LandingPage({ params }: Props) { </div> </Reveal> ))} - </div> + </SwipeRow> <Reveal> - <div className="mt-16 rounded-3xl bg-slate-50 p-6 md:mt-24 md:p-10"> + <div className="mt-12 rounded-3xl bg-slate-50 p-6 md:mt-24 md:p-10"> <p className={`${MONO} m-0`}>{technology.techLabel}</p> - <div className="mt-8 grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-4"> + <div className="mt-6 grid grid-cols-1 gap-7 sm:grid-cols-2 md:mt-8 md:gap-10 lg:grid-cols-4"> {technology.tech.map((group) => ( <div key={group.area}> <p className="m-0 font-sans text-xl font-bold tracking-tight text-slate-900 md:text-2xl"> @@ -269,7 +270,7 @@ export default async function LandingPage({ params }: Props) { </div> </Reveal> - <div className="mt-16 md:mt-24"> + <div className="mt-12 md:mt-24"> <Reveal> <h3 className="m-0 font-sans text-3xl font-bold tracking-tighter text-slate-900 md:text-5xl"> {technology.pipelineTitle} @@ -296,7 +297,7 @@ export default async function LandingPage({ params }: Props) { </div> </BitAura> </Reveal> - <div className="grid grid-cols-1 gap-x-12 gap-y-12 md:grid-cols-2 md:gap-y-16"> + <SwipeRow className="grid grid-cols-1 gap-x-12 gap-y-12 md:grid-cols-2 md:gap-y-16"> {working.principles.map((item, i) => ( <Reveal key={item.title} delay={0.08 * i}> <div className="border-t border-slate-900 pt-6"> @@ -307,7 +308,7 @@ export default async function LandingPage({ params }: Props) { </div> </Reveal> ))} - </div> + </SwipeRow> </Block> <Block diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index e574f24..7caba2b 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -1352,6 +1352,38 @@ pre:has(code[class*="language-"]) { } } +/* + * SwipeRow: on phones equal items stand side by side and are swiped through, + * with the next one peeking in. From tablet width on the slide wrappers + * dissolve and the items sit in the grid the row was given. + */ +@media (max-width: 767px) { + .swipe-row { + display: flex; + gap: 1rem; + overflow-x: auto; + overscroll-behavior-x: contain; + scroll-snap-type: x mandatory; + /* Bleed to the screen edges, but start in line with the text. */ + margin-inline: -1.25rem; + padding-inline: 1.25rem; + scroll-padding-inline: 1.25rem; + scrollbar-width: none; + } + .swipe-row::-webkit-scrollbar { + display: none; + } + .swipe-slide { + flex: 0 0 82%; + scroll-snap-align: start; + } +} +@media (min-width: 768px) { + .swipe-slide { + display: contents; + } +} + /* * Native feel: the page should behave like a well-made app on every input type. */ diff --git a/apps/web/src/components/SwipeRow.tsx b/apps/web/src/components/SwipeRow.tsx new file mode 100644 index 0000000..7ea85c5 --- /dev/null +++ b/apps/web/src/components/SwipeRow.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; + +/** + * SwipeRow: on phones the items stand side by side and are swiped through, + * with the next one peeking in; from tablet width on they fall back into the + * grid given by `className`. It keeps long lists of equal items from + * stretching the page on a small screen. Styles: `swipe-row` in globals.css. + */ +export const SwipeRow: React.FC<{ + className?: string; + children: React.ReactNode; +}> = ({ className = "", children }) => ( + <div className={`swipe-row ${className}`}> + {React.Children.map(children, (child) => ( + <div className="swipe-slide">{child}</div> + ))} + </div> +); diff --git a/apps/web/src/components/home/Block.tsx b/apps/web/src/components/home/Block.tsx index 697f452..066c547 100644 --- a/apps/web/src/components/home/Block.tsx +++ b/apps/web/src/components/home/Block.tsx @@ -49,7 +49,7 @@ export const Block: React.FC<BlockProps> = ({ /> )} {dark && <BitSeam />} - <div className="relative mx-auto w-full max-w-[1400px] px-5 py-20 md:px-12 md:py-36"> + <div className="relative mx-auto w-full max-w-[1400px] px-5 py-14 md:px-12 md:py-36"> <Reveal> <div className="mb-8 flex items-center gap-4 md:mb-12"> <span @@ -78,7 +78,7 @@ export const Block: React.FC<BlockProps> = ({ {title} </h2> </BitWipe> - <div className="mt-14 md:mt-24">{children}</div> + <div className="mt-10 md:mt-24">{children}</div> </div> </section> ); diff --git a/apps/web/src/components/home/ClosingCta.tsx b/apps/web/src/components/home/ClosingCta.tsx index 5e7b792..ae68ed4 100644 --- a/apps/web/src/components/home/ClosingCta.tsx +++ b/apps/web/src/components/home/ClosingCta.tsx @@ -26,7 +26,7 @@ export const ClosingCta: React.FC<{ contact: Content["contact"] }> = ({ /> <div className="bit-glow bit-glow-dots" aria-hidden="true" /> <BitSeam /> - <div className="relative mx-auto grid w-full max-w-[1400px] grid-cols-1 gap-14 px-5 py-24 md:px-12 md:py-40 lg:grid-cols-12 lg:items-center lg:gap-12"> + <div className="relative mx-auto grid w-full max-w-[1400px] grid-cols-1 gap-12 px-5 py-16 md:px-12 md:py-40 lg:grid-cols-12 lg:items-center lg:gap-12"> <Reveal className="lg:col-span-6"> <BitHeadline as="h2" diff --git a/apps/web/src/components/home/PathPicker.tsx b/apps/web/src/components/home/PathPicker.tsx index 6325c3c..341f784 100644 --- a/apps/web/src/components/home/PathPicker.tsx +++ b/apps/web/src/components/home/PathPicker.tsx @@ -92,7 +92,7 @@ const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({ setStep(index); setAuto(false); }} - className={`relative flex w-full gap-5 border-t py-6 text-left transition-colors duration-300 md:gap-6 ${ + className={`relative flex w-full gap-5 border-t py-4 text-left transition-colors duration-300 md:gap-6 md:py-6 ${ selected ? "border-slate-900" : "border-slate-200" }`} > @@ -122,8 +122,11 @@ const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({ {item.title} </span> <span - className={`mt-1.5 block font-sans text-base leading-relaxed transition-colors duration-300 ${ - selected ? "text-slate-600" : "text-slate-400" + className={`mt-1.5 font-sans text-base leading-relaxed transition-colors duration-300 ${ + // On phones only the current step is spelled out. + selected + ? "block text-slate-600" + : "hidden text-slate-400 md:block" }`} > {item.text} @@ -153,7 +156,7 @@ export const PathPicker: React.FC = () => { <div role="tablist" aria-label={services.tablistLabel} - className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4 lg:gap-4" + className="grid grid-cols-2 gap-2 md:gap-3 lg:grid-cols-4 lg:gap-4" > {paths.map((path, index) => { const selected = path.id === activeId; @@ -167,7 +170,7 @@ export const PathPicker: React.FC = () => { aria-controls={`path-panel-${path.id}`} onClick={() => setActiveId(path.id)} data-glow - className={`bit-card group flex h-full flex-col justify-between rounded-3xl border p-5 text-left transition-all duration-300 md:p-7 ${ + className={`bit-card group flex h-full flex-col justify-between rounded-3xl border p-4 text-left transition-all duration-300 md:p-7 ${ selected ? "border-slate-900 bg-slate-900 text-white" : "border-slate-200 bg-white text-slate-900 hover:-translate-y-1 hover:border-slate-400" @@ -177,11 +180,11 @@ export const PathPicker: React.FC = () => { <span className="font-mono text-[11px] tracking-[0.2em] text-slate-400"> 0{index + 1} </span> - <span className="mt-3 block font-sans text-xl font-bold leading-tight tracking-tight md:mt-10 md:text-2xl"> + <span className="mt-3 block font-sans text-base font-bold leading-tight tracking-tight md:mt-10 md:text-2xl"> {path.need} </span> <span - className={`mt-2 block font-sans text-sm leading-relaxed md:mt-4 md:text-base ${selected ? "text-slate-300" : "text-slate-500"}`} + className={`mt-2 hidden font-sans text-sm leading-relaxed md:mt-4 md:block md:text-base ${selected ? "text-slate-300" : "text-slate-500"}`} > {path.forWhom} </span> @@ -199,7 +202,7 @@ export const PathPicker: React.FC = () => { id={`path-panel-${path.id}`} aria-labelledby={`path-tab-${path.id}`} hidden={!selected} - className="mt-12 md:mt-20" + className="mt-8 md:mt-20" > <motion.div key={selected ? "on" : "off"} @@ -251,7 +254,7 @@ export const PathPicker: React.FC = () => { {path.pricingText} </p> </div> - <div className="grid grid-cols-1 gap-8 sm:grid-cols-2 md:col-span-6"> + <div className="grid grid-cols-2 gap-6 md:col-span-6 md:gap-8"> {path.prices.map((price) => ( <div key={price.label}> <p className="m-0 font-sans text-base text-slate-500 md:text-lg"> diff --git a/apps/web/src/components/illustrations/AutomationFlow.tsx b/apps/web/src/components/illustrations/AutomationFlow.tsx index b834f14..5080919 100644 --- a/apps/web/src/components/illustrations/AutomationFlow.tsx +++ b/apps/web/src/components/illustrations/AutomationFlow.tsx @@ -1,6 +1,7 @@ "use client"; import { BitAura } from "@/src/components/bits/BitAura"; +import { SwipeRow } from "@/src/components/SwipeRow"; import * as React from "react"; import { useNarrow } from "./useNarrow"; import { useContent } from "@/src/components/ContentProvider"; @@ -391,7 +392,7 @@ export const AutomationFlow: React.FC = () => { </div> <div className="grid grid-cols-1 border-t border-slate-800 md:grid-cols-12"> - <div className="border-b border-slate-800 px-5 py-5 md:col-span-7 md:border-b-0 md:border-r md:px-7"> + <div className="px-5 py-5 md:col-span-7 md:border-r md:border-slate-800 md:px-7"> <p className={`${MONO} m-0 text-slate-500`}>{automation.log}</p> <ol className="m-0 mt-3 min-h-[7.5rem] list-none p-0 font-mono text-[12px] leading-relaxed text-slate-300 md:text-[13px]" @@ -413,7 +414,7 @@ export const AutomationFlow: React.FC = () => { ))} </ol> </div> - <ul className="m-0 flex list-none flex-col justify-center gap-2 px-5 py-5 md:col-span-5 md:px-7"> + <ul className="m-0 hidden list-none flex-col justify-center gap-2 px-5 py-5 md:col-span-5 md:flex md:px-7"> {KINDS.filter((kind) => flow.nodes.some((node) => node.kind === kind), ).map((kind) => ( @@ -442,7 +443,7 @@ export const AutomationFlow: React.FC = () => { </div> </BitAura> - <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-2"> + <SwipeRow className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="rounded-3xl border border-slate-200 p-6 md:p-8"> <p className={`${MONO} m-0 text-slate-400`}>{automation.byHand}</p> <p className="m-0 mt-3 font-sans text-lg leading-relaxed text-slate-500 md:text-xl"> @@ -455,7 +456,7 @@ export const AutomationFlow: React.FC = () => { {flow.automated} </p> </div> - </div> + </SwipeRow> </div> ); }; diff --git a/apps/web/src/components/illustrations/PipelineRun.tsx b/apps/web/src/components/illustrations/PipelineRun.tsx index 2e480c6..286eb98 100644 --- a/apps/web/src/components/illustrations/PipelineRun.tsx +++ b/apps/web/src/components/illustrations/PipelineRun.tsx @@ -241,7 +241,10 @@ export const PipelineRun: React.FC = () => { </p> <p className={`m-0 mt-2 font-sans text-base leading-relaxed transition-colors duration-300 ${ - active ? "text-slate-600" : "text-slate-400" + // On phones only the current step is spelled out. + active + ? "block text-slate-600" + : "hidden text-slate-400 md:block" }`} > {item.text} diff --git a/apps/web/src/components/illustrations/WorkShowcase.tsx b/apps/web/src/components/illustrations/WorkShowcase.tsx index a84e624..ab0896a 100644 --- a/apps/web/src/components/illustrations/WorkShowcase.tsx +++ b/apps/web/src/components/illustrations/WorkShowcase.tsx @@ -203,7 +203,7 @@ export const WorkShowcase: React.FC = () => { {copy.text} </p> - <ul className="m-0 mt-8 list-none p-0"> + <ul className="m-0 mt-8 hidden list-none p-0 md:block"> {copy.did.map((line) => ( <li key={line}