diff --git a/CLAUDE.md b/CLAUDE.md index 66c1c4a..ec92da4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,8 +75,11 @@ App lives in `apps/web`. Social media videos live in `apps/video` (Remotion, sta - Drawings use the circuit parts in `apps/web/src/components/illustrations/circuit.tsx`: `Chip` (a system or person, with pins), `Track` (straight runs, 45 degree bends), `Pad` (solder point), `Bit` (what travels). New drawings are built from these, not from pills, curves and round dots. -- The homepage is for clients who buy a result (website, automation, a reliable app). Booking by the hour - for teams (the `developer` path, 100 EUR per hour, by the month) is shown only on the freelance page. +- Two audiences, two pages (owner, 2026-10-05). The homepage is for people who commission a result and have + no software of their own: a website or process automation (plus web presentations). It shows no prices + and no hourly terms, only "ask for an offer". Everything for companies that book by the hour is on the + freelance page only: the developer case, making an existing app reliable, how working together goes, + project fit, tools, safeguards, rates and booking. Do not put any of that back on the homepage. - `DecodeText` is for monospaced text only; in proportional type the changing glyph widths make text jump. - Never put an in-view trigger on an element that clips itself away (clip-path): a fully clipped element never counts as visible. Observe an unclipped wrapper (see `BitWipe`). diff --git a/apps/web/app/[locale]/freelance/page.tsx b/apps/web/app/[locale]/freelance/page.tsx index 991cadb..56a3f07 100644 --- a/apps/web/app/[locale]/freelance/page.tsx +++ b/apps/web/app/[locale]/freelance/page.tsx @@ -2,8 +2,10 @@ import type { Metadata } from "next"; import { DecodeText } from "@/src/components/bits/DecodeText"; import { Block } from "@/src/components/home/Block"; import { ClosingCta } from "@/src/components/home/ClosingCta"; -import { PathStory } from "@/src/components/home/PathPicker"; +import { BitAura } from "@/src/components/bits/BitAura"; +import { PathPicker } from "@/src/components/home/PathPicker"; import { TopicButton } from "@/src/components/home/TopicButton"; +import { ContactDiagram } from "@/src/components/illustrations/ContactDiagram"; import { PipelineRun } from "@/src/components/illustrations/PipelineRun"; import { Reveal } from "@/src/components/Reveal"; import { SwipeRow } from "@/src/components/SwipeRow"; @@ -40,7 +42,7 @@ export default async function FreelancePage({ params }: Props) { const { freelance, technology } = content; const { booking } = freelance; const { services } = content; - const teamPath = services.paths.find((path) => path.id === "developer"); + const { working } = content; return (
@@ -127,6 +129,21 @@ export default async function FreelancePage({ params }: Props) { + {freelance.cases.lead}{" "} + {freelance.cases.rest} + + } + > + + + + + + @@ -140,19 +157,39 @@ export default async function FreelancePage({ params }: Props) { - {teamPath && ( - - -

{teamPath.intro}

-
- - - -
- )} - + {working.title.lead}{" "} + {working.title.rest} + + } + > + + +
+ +
+
+
+ + {working.principles.map((item, i) => ( + +
+

+ {item.title} +

+

{item.text}

+
+
+ ))} +
+
+ + diff --git a/apps/web/app/[locale]/page.tsx b/apps/web/app/[locale]/page.tsx index 2774bb7..2fb7eac 100644 --- a/apps/web/app/[locale]/page.tsx +++ b/apps/web/app/[locale]/page.tsx @@ -11,7 +11,6 @@ import { PortraitSection } from "@/src/components/home/PortraitSection"; import { StickyBook } from "@/src/components/home/StickyBook"; import { AutomationFlow } from "@/src/components/illustrations/AutomationFlow"; import { CareerLine } from "@/src/components/illustrations/CareerLine"; -import { ContactDiagram } from "@/src/components/illustrations/ContactDiagram"; import { PresentationCompare, ReasonDrawing, @@ -64,7 +63,6 @@ export default async function LandingPage({ params }: Props) { presentations, story, freelance, - working, work, faq, } = content; @@ -112,7 +110,31 @@ export default async function LandingPage({ params }: Props) { title={} > <Reveal> - <PathPicker /> + <PathPicker ids={["automation", "website"]} pricing="offer" /> + </Reveal> + <Reveal> + <Link + href={pathFor("freelance", locale)} + className="group mt-14 flex flex-col gap-5 rounded-3xl bg-slate-50 p-6 no-underline transition-colors duration-300 hover:bg-slate-100 md:mt-24 md:flex-row md:items-center md:justify-between md:p-10" + > + <span className="block max-w-2xl"> + <span className="block font-sans text-2xl font-bold tracking-tight text-slate-900 md:text-3xl"> + {freelance.teaser.title} + </span> + <span className="mt-3 block font-sans text-base leading-relaxed text-slate-500 md:text-lg"> + {freelance.teaser.text} + </span> + </span> + <span className="inline-flex shrink-0 items-center gap-3 self-start rounded-full bg-slate-900 px-6 py-3 font-sans text-[11px] font-bold uppercase tracking-[0.15em] text-white md:self-auto"> + {freelance.teaser.cta} + <span + aria-hidden="true" + className="transition-transform duration-300 group-hover:translate-x-1" + > + → + </span> + </span> + </Link> </Reveal> </Block> @@ -224,59 +246,8 @@ export default async function LandingPage({ params }: Props) { <Block index="05" - label={working.label} - chapter={{ line: story.working, figure: "circle", flip: true }} - title={<Title title={working.title} />} - > - <Reveal> - <BitAura source="bottom" tone="light" className="mb-16 md:mb-24"> - <div className="overflow-hidden rounded-3xl bg-slate-900 p-4 md:p-10"> - <ContactDiagram /> - </div> - </BitAura> - </Reveal> - <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"> - <h3 className="m-0 font-sans text-3xl font-bold tracking-tight text-slate-900 md:text-4xl"> - {item.title} - </h3> - <p className={`${BODY} mt-4`}>{item.text}</p> - </div> - </Reveal> - ))} - </SwipeRow> - <Reveal> - <Link - href={pathFor("freelance", locale)} - className="group mt-14 flex flex-col gap-5 rounded-3xl bg-slate-50 p-6 no-underline transition-colors duration-300 hover:bg-slate-100 md:mt-24 md:flex-row md:items-center md:justify-between md:p-10" - > - <span className="block max-w-2xl"> - <span className="block font-sans text-2xl font-bold tracking-tight text-slate-900 md:text-3xl"> - {freelance.teaser.title} - </span> - <span className="mt-3 block font-sans text-base leading-relaxed text-slate-500 md:text-lg"> - {freelance.teaser.text} - </span> - </span> - <span className="inline-flex shrink-0 items-center gap-3 self-start rounded-full bg-slate-900 px-6 py-3 font-sans text-[11px] font-bold uppercase tracking-[0.15em] text-white md:self-auto"> - {freelance.teaser.cta} - <span - aria-hidden="true" - className="transition-transform duration-300 group-hover:translate-x-1" - > - → - </span> - </span> - </Link> - </Reveal> - </Block> - - <Block - index="06" label={work.label} - chapter={{ line: story.work, figure: "square", flip: false }} + chapter={{ line: story.work, figure: "square", flip: true }} tone="dark" title={<Title title={work.title} muted="text-slate-500" />} > @@ -289,9 +260,9 @@ export default async function LandingPage({ params }: Props) { </Block> <Block - index="07" + index="06" label={faq.label} - chapter={{ line: story.faq, figure: "triangle", flip: true }} + chapter={{ line: story.faq, figure: "triangle", flip: false }} title={<Title title={faq.title} />} > <Reveal> diff --git a/apps/web/src/components/home/PathPicker.tsx b/apps/web/src/components/home/PathPicker.tsx index 015b15f..82ac89c 100644 --- a/apps/web/src/components/home/PathPicker.tsx +++ b/apps/web/src/components/home/PathPicker.tsx @@ -7,21 +7,16 @@ import { DecodeText } from "@/src/components/bits/DecodeText"; import { PathScene } from "@/src/components/illustrations/PathScenes"; import { useContent } from "@/src/components/ContentProvider"; import { TopicButton } from "./TopicButton"; -import type { ServicePath } from "@/src/content/types"; +import type { ServicePath, ServicePathId } from "@/src/content/types"; const MONO = "font-mono text-[11px] uppercase tracking-[0.2em] text-slate-500"; const STEP_MS = 4800; -/** - * Booked by the hour by a team, not bought as a result: this case is not on the - * homepage. It lives on the freelance page. - */ -const TEAM_PATH = "developer"; /** * PathStory: the four steps of one path, each with a drawing that explains it. * The steps advance on their own until the visitor picks one. */ -export const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({ +const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({ path, active, }) => { @@ -151,10 +146,17 @@ export const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({ * what they get, how it runs, what it costs, what to do next. * All three panels stay in the DOM so the content is readable without JavaScript. */ -export const PathPicker: React.FC = () => { +export const PathPicker: React.FC<{ + /** The cases to show, in this order. */ + ids: readonly ServicePathId[]; + /** `offer`: ask for an offer, no figures. `estimate`: the way to a price, with figures. */ + pricing: "offer" | "estimate"; +}> = ({ ids, pricing }) => { const { services } = useContent(); const { estimate } = services; - const paths = services.paths.filter((path) => path.id !== TEAM_PATH); + const paths = ids.flatMap((id) => + services.paths.filter((path) => path.id === id), + ); const [activeId, setActiveId] = React.useState(paths[0]?.id ?? ""); return ( @@ -162,7 +164,7 @@ export const PathPicker: React.FC = () => { <div role="tablist" aria-label={services.tablistLabel} - className="grid grid-cols-1 gap-2 md:grid-cols-3 md:gap-4" + className="grid grid-cols-1 gap-2 md:grid-cols-2 md:gap-4" > {paths.map((path, index) => { const selected = path.id === activeId; @@ -249,48 +251,70 @@ export const PathPicker: React.FC = () => { <PathStory path={path} active={selected} /> </div> - <div className="mt-12 rounded-3xl bg-slate-50 p-6 md:mt-16 md:p-10"> - <p className={`${MONO} m-0`}>{estimate.label}</p> - <div className="mt-6 grid grid-cols-1 gap-10 md:grid-cols-12 md:gap-12"> - <div className="md:col-span-7"> + {pricing === "offer" ? ( + <div className="mt-12 flex flex-col gap-8 rounded-3xl bg-slate-50 p-6 md:mt-16 md:flex-row md:items-center md:justify-between md:gap-12 md:p-10"> + <div className="max-w-2xl"> <p className="m-0 font-sans text-2xl font-bold leading-tight tracking-tight text-slate-900 md:text-4xl"> - {estimate.title} + {services.offer.title} + </p> + <p className="m-0 mt-4 font-sans text-base leading-relaxed text-slate-500 md:text-lg"> + {services.offer.text} </p> - <ol className="m-0 mt-6 list-none p-0"> - {estimate.steps.map((item, index) => ( - <li - key={item.title} - className="flex gap-5 border-t border-slate-200 py-4" - > - <span className="w-6 shrink-0 font-sans text-2xl font-black leading-none tracking-tighter text-slate-300"> - {index + 1} - </span> - <span> - <span className="block font-sans text-lg font-bold tracking-tight text-slate-900"> - {item.title} - </span> - <span className="mt-1 block font-sans text-base leading-relaxed text-slate-500"> - {item.text} - </span> - </span> - </li> - ))} - </ol> </div> - <div className="flex flex-col md:col-span-5"> - <p className={`${MONO} m-0`}>{estimate.orientationLabel}</p> - <p className="m-0 mt-3 font-sans text-2xl font-black leading-tight tracking-tighter text-slate-900 md:text-4xl"> - {path.orientation} - </p> - <p className="m-0 mt-4 font-sans text-base leading-relaxed text-slate-500"> - {path.orientationNote} {estimate.net} - </p> - <div className="mt-8 flex flex-col items-start gap-5 md:mt-auto md:pt-8"> - <TopicButton topic={path.id}>{estimate.cta}</TopicButton> + <div className="shrink-0"> + <TopicButton topic={path.id}> + {services.offer.cta} + </TopicButton> + </div> + </div> + ) : ( + <div className="mt-12 rounded-3xl bg-slate-50 p-6 md:mt-16 md:p-10"> + <p className={`${MONO} m-0`}>{estimate.label}</p> + <div className="mt-6 grid grid-cols-1 gap-10 md:grid-cols-12 md:gap-12"> + <div className="md:col-span-7"> + <p className="m-0 font-sans text-2xl font-bold leading-tight tracking-tight text-slate-900 md:text-4xl"> + {estimate.title} + </p> + <ol className="m-0 mt-6 list-none p-0"> + {estimate.steps.map((item, index) => ( + <li + key={item.title} + className="flex gap-5 border-t border-slate-200 py-4" + > + <span className="w-6 shrink-0 font-sans text-2xl font-black leading-none tracking-tighter text-slate-300"> + {index + 1} + </span> + <span> + <span className="block font-sans text-lg font-bold tracking-tight text-slate-900"> + {item.title} + </span> + <span className="mt-1 block font-sans text-base leading-relaxed text-slate-500"> + {item.text} + </span> + </span> + </li> + ))} + </ol> + </div> + <div className="flex flex-col md:col-span-5"> + <p className={`${MONO} m-0`}> + {estimate.orientationLabel} + </p> + <p className="m-0 mt-3 font-sans text-2xl font-black leading-tight tracking-tighter text-slate-900 md:text-4xl"> + {path.orientation} + </p> + <p className="m-0 mt-4 font-sans text-base leading-relaxed text-slate-500"> + {path.orientationNote} {estimate.net} + </p> + <div className="mt-8 flex flex-col items-start gap-5 md:mt-auto md:pt-8"> + <TopicButton topic={path.id}> + {estimate.cta} + </TopicButton> + </div> </div> </div> </div> - </div> + )} </motion.div> </div> ); diff --git a/apps/web/src/content/de.ts b/apps/web/src/content/de.ts index bcfa27a..e89c946 100644 --- a/apps/web/src/content/de.ts +++ b/apps/web/src/content/de.ts @@ -74,10 +74,15 @@ export const de: Content = { }, services: { label: "Leistungen", - title: { lead: "Was brauchen Sie?", rest: "Drei typische Fälle." }, + title: { lead: "Was brauchen Sie?", rest: "Meist eines von beiden." }, tablistLabel: "Was brauchen Sie?", youGet: "Das bekommen Sie", howItRuns: "So läuft es ab", + offer: { + title: "Sie bekommen ein Angebot, bevor etwas kostet.", + text: "Schreiben Sie mir kurz, was Sie brauchen. Wir sprechen etwa 20 Minuten, danach bekommen Sie ein schriftliches Angebot. Ich fange erst an, wenn Sie zugestimmt haben.", + cta: "Angebot anfragen", + }, estimate: { label: "Was es kostet", title: "So kommen Sie zu einem Preis", @@ -277,7 +282,10 @@ export const de: Content = { }, automation: { label: "Prozessautomatisierung", - title: { lead: "Lassen Sie die Systeme", rest: "das Kopieren übernehmen." }, + title: { + lead: "Ihre Programme erledigen die Routine.", + rest: "Niemand tippt mehr ab.", + }, intro: "In den meisten Unternehmen tragen Menschen Informationen von einem Programm ins andere: hier lesen, dort eintippen, eine E-Mail dazu schreiben. Automatisierung verbindet diese Programme, sodass die Arbeit von selbst vom einen zum nächsten wandert. Drei typische Beispiele:", examplesLabel: "Beispielabläufe", @@ -489,7 +497,6 @@ export const de: Content = { "Manchmal ist es gar keine neue Software, sondern weniger Handarbeit.", presentations: "Und manchmal ist es ein Termin, bei dem Sie überzeugen müssen.", - working: "Wenn klar ist, was entsteht, läuft es immer gleich ab.", work: "Am Ende steht etwas, das läuft.", faq: "Vorher bleiben meist ein paar Fragen.", contact: "Und dann beginnt es mit einer E-Mail.", @@ -506,6 +513,7 @@ export const de: Content = { intro: "Diese Seite ist für alle, die selbst entwickeln oder ein Team leiten: für welche Projekte ich passe, womit ich arbeite und wie ich Änderungen absichere, bevor sie live gehen.", pipelineLabel: "Absicherung", + cases: { lead: "Wofür Teams mich buchen.", rest: "Zwei typische Fälle." }, booking: { label: "Buchung", title: { lead: "Gebucht wird monatsweise.", rest: "Nicht tageweise." }, @@ -646,7 +654,10 @@ export const de: Content = { }, working: { label: "Zusammenarbeit", - title: { lead: "So arbeite ich.", rest: "Nichts Ungewöhnliches." }, + title: { + lead: "So läuft die Zusammenarbeit.", + rest: "Direkt und nachvollziehbar.", + }, principles: [ { title: "Ein Ansprechpartner", diff --git a/apps/web/src/content/en.ts b/apps/web/src/content/en.ts index 2bb1e30..5c85665 100644 --- a/apps/web/src/content/en.ts +++ b/apps/web/src/content/en.ts @@ -73,10 +73,15 @@ export const en: Content = { }, services: { label: "Services", - title: { lead: "What do you need?", rest: "Three typical cases." }, + title: { lead: "What do you need?", rest: "Usually one of the two." }, tablistLabel: "What do you need?", youGet: "What you get", howItRuns: "How it runs", + offer: { + title: "You get an offer before anything costs money.", + text: "Write me briefly what you need. We talk for about 20 minutes, then you get a written offer. I only start once you have agreed.", + cta: "Ask for an offer", + }, estimate: { label: "What it costs", title: "How you get to a price", @@ -273,7 +278,10 @@ export const en: Content = { }, automation: { label: "Process automation", - title: { lead: "Let the systems", rest: "do the copying." }, + title: { + lead: "Your tools do the routine work.", + rest: "Nobody retypes anything.", + }, intro: "In most companies, people move information from one tool to another: read it here, type it in there, send an email about it. Automation connects those tools so that the work passes from one to the next on its own. Three typical examples:", examplesLabel: "Example workflows", @@ -477,8 +485,6 @@ export const en: Content = { "Sometimes it is not new software at all, but less work by hand.", presentations: "And sometimes it is a meeting in which you have to convince.", - working: - "Once it is clear what will be built, it always runs the same way.", work: "In the end there is something that runs.", faq: "Before that, a few questions usually remain.", contact: "And then it starts with an email.", @@ -495,6 +501,7 @@ export const en: Content = { intro: "This page is for people who develop themselves or lead a team: which projects I am right for, what I work with, and how I secure changes before they go live.", pipelineLabel: "Safeguards", + cases: { lead: "What teams book me for.", rest: "Two typical cases." }, booking: { label: "Booking", title: { lead: "Booked by the month.", rest: "Not by the day." }, @@ -631,7 +638,10 @@ export const en: Content = { }, working: { label: "Working with me", - title: { lead: "How I work.", rest: "Nothing unusual." }, + title: { + lead: "How working together goes.", + rest: "Direct and easy to follow.", + }, principles: [ { title: "One contact", diff --git a/apps/web/src/content/types.ts b/apps/web/src/content/types.ts index 5b2f83b..e4721eb 100644 --- a/apps/web/src/content/types.ts +++ b/apps/web/src/content/types.ts @@ -125,7 +125,6 @@ export type ChapterId = | "services" | "automation" | "presentations" - | "working" | "work" | "faq" | "contact"; @@ -189,6 +188,8 @@ export type Content = { readonly tablistLabel: string; readonly youGet: string; readonly howItRuns: string; + /** The homepage's way to a price: ask for an offer. No figures there. */ + readonly offer: TitledText & { readonly cta: string }; /** How a visitor gets to a price: the homepage shows this instead of a price list. */ readonly estimate: { readonly label: string; @@ -251,6 +252,8 @@ export type Content = { readonly title: SplitTitle; readonly intro: string; readonly pipelineLabel: string; + /** Headline over the cases teams book the owner for. */ + readonly cases: SplitTitle; /** How the owner is booked as a developer: by the month, not by the day. */ readonly booking: { readonly label: string;