feat: drawings in circuit style, visible hero digits, hourly booking only on the freelance page

Drawings: "How I work" and the step drawings under "How it runs" now use
the same language as the story thread. Systems and people are chips with
pins, connections are tracks with 45 degree bends and solder points, and
what travels is a bit instead of a round dot (circuit.tsx).

Hero: the digits in the background were close to invisible on white. They
are about twice as strong now, and the ones near the pointer light up indigo.

Homepage vs. freelance page: the homepage is for clients who buy a result,
so it shows three cases. The developer case (100 EUR per hour, booked by the
month) and its steps live on the freelance page only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 11:37:26 +02:00
co-authored by Claude Opus 5.5
parent affd4b7ee7
commit f0c25b802b
11 changed files with 430 additions and 294 deletions
+15 -1
View File
@@ -2,6 +2,7 @@ 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 { TopicButton } from "@/src/components/home/TopicButton";
import { PipelineRun } from "@/src/components/illustrations/PipelineRun";
import { Reveal } from "@/src/components/Reveal";
@@ -38,6 +39,8 @@ export default async function FreelancePage({ params }: Props) {
const content = contentFor(locale);
const { freelance, technology } = content;
const { booking } = freelance;
const { services } = content;
const teamPath = services.paths.find((path) => path.id === "developer");
return (
<div className="relative flex flex-col bg-white">
@@ -137,8 +140,19 @@ export default async function FreelancePage({ params }: Props) {
</Reveal>
</Block>
{teamPath && (
<Block index="03" label={services.howItRuns} title={teamPath.headline}>
<Reveal>
<p className={`${BODY} mb-12 max-w-3xl`}>{teamPath.intro}</p>
</Reveal>
<Reveal>
<PathStory path={teamPath} active />
</Reveal>
</Block>
)}
<Block
index="03"
index="04"
label={booking.label}
title={
<>
@@ -58,6 +58,11 @@ export const AbstractCircuit: React.FC<{
const GAP = 20;
const PHONE_WIDTH = 640;
const PHONE_INK = 2.4;
// On white the digits were close to invisible (owner feedback).
const DESKTOP_INK = 2.1;
/** Digits near the pointer light up in the indigo of the site. */
const POINTER_RADIUS = 190;
let pointer: { x: number; y: number } | null = null;
const PHONE_GROW = 2;
const seed = () => {
@@ -66,7 +71,7 @@ export const AbstractCircuit: React.FC<{
// On a phone the field is small and held close: the digits need more
// size and ink there, or they are not seen at all.
const small = w < PHONE_WIDTH;
const ink = small ? PHONE_INK : 1;
const ink = small ? PHONE_INK : DESKTOP_INK;
const grow = small ? PHONE_GROW : 0;
// Horizontal lanes
@@ -148,14 +153,24 @@ export const AbstractCircuit: React.FC<{
const breath = Math.sin(t + c.phase) * 0.015;
let alpha = c.baseAlpha + breath;
const near = pointer
? Math.max(
0,
1 - Math.hypot(c.x - pointer.x, c.y - pointer.y) / POINTER_RADIUS,
)
: 0;
alpha += near * 0.75;
// ── Draw ──
const sz = c.size;
ctx.font = `bold ${sz}px "SF Mono", "Fira Code", "Cascadia Code", "Menlo", monospace`;
const shade = Math.min(1, Math.max(alpha, 0));
ctx.fillStyle = invert
? `rgba(255,255,255,${Math.max(alpha, 0)})`
: `rgba(100,116,139,${Math.max(alpha, 0)})`;
? `rgba(255,255,255,${shade})`
: near > 0.05
? `rgba(79,70,229,${shade})`
: `rgba(100,116,139,${shade})`;
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
@@ -167,7 +182,15 @@ export const AbstractCircuit: React.FC<{
// ── Event listeners ──
// Listen on the PARENT element (the section) to capture all mouse movement
const onPointerMove = (event: PointerEvent) => {
const box = canvas.getBoundingClientRect();
pointer =
event.pointerType === "mouse"
? { x: event.clientX - box.left, y: event.clientY - box.top }
: null;
};
window.addEventListener("resize", resize);
window.addEventListener("pointermove", onPointerMove, { passive: true });
resize();
raf = requestAnimationFrame(render);
@@ -175,6 +198,7 @@ export const AbstractCircuit: React.FC<{
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("resize", resize);
window.removeEventListener("pointermove", onPointerMove);
};
}, [invert]);
@@ -8,7 +8,7 @@ export const HeroStatement: React.FC<{ hero: Content["hero"] }> = ({
hero,
}) => (
<section className="relative overflow-hidden bg-white">
<div className="absolute inset-0 opacity-70">
<div className="absolute inset-0">
<AbstractCircuit />
</div>
+9 -15
View File
@@ -2,23 +2,26 @@
import * as React from "react";
import { motion } from "framer-motion";
import Link from "next/link";
import { BitAura } from "@/src/components/bits/BitAura";
import { DecodeText } from "@/src/components/bits/DecodeText";
import { PathScene } from "@/src/components/illustrations/PathScenes";
import { useContent, useLocale } from "@/src/components/ContentProvider";
import { pathFor } from "@/src/domain/routes";
import { useContent } from "@/src/components/ContentProvider";
import { TopicButton } from "./TopicButton";
import type { ServicePath } 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.
*/
const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({
export const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({
path,
active,
}) => {
@@ -151,8 +154,7 @@ const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({
export const PathPicker: React.FC = () => {
const { services } = useContent();
const { estimate } = services;
const locale = useLocale();
const paths = services.paths;
const paths = services.paths.filter((path) => path.id !== TEAM_PATH);
const [activeId, setActiveId] = React.useState(paths[0]?.id ?? "");
return (
@@ -160,7 +162,7 @@ export const PathPicker: React.FC = () => {
<div
role="tablist"
aria-label={services.tablistLabel}
className="grid grid-cols-2 gap-2 md:gap-3 lg:grid-cols-4 lg:gap-4"
className="grid grid-cols-1 gap-2 md:grid-cols-3 md:gap-4"
>
{paths.map((path, index) => {
const selected = path.id === activeId;
@@ -285,14 +287,6 @@ export const PathPicker: React.FC = () => {
</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>
{path.id === "developer" && (
<Link
href={pathFor("freelance", locale)}
className="border-b border-slate-900 pb-0.5 font-sans text-base font-bold text-slate-900 no-underline transition-opacity hover:opacity-60"
>
{estimate.freelance} →
</Link>
)}
</div>
</div>
</div>
@@ -1,52 +1,11 @@
"use client";
import * as React from "react";
import { motion } from "framer-motion";
import { useContent } from "@/src/components/ContentProvider";
import { Bit, Chip, CIRCUIT, Pad, Track } from "./circuit";
import { useNarrow } from "./useNarrow";
const INK = "#f8fafc";
const LINE = "#475569";
const MARK = "#fde68a";
const NODE_W = 112;
const NODE_H = 38;
const Node: React.FC<{
x: number;
y: number;
label: string;
size: number;
solid?: boolean;
muted?: boolean;
}> = ({ x, y, label, size, solid = false, muted = false }) => (
<g>
<rect
x={x - NODE_W / 2}
y={y - NODE_H / 2}
width={NODE_W}
height={NODE_H}
rx={NODE_H / 2}
fill={solid ? INK : "#0f172a"}
stroke={muted ? LINE : INK}
strokeWidth={1.5}
strokeDasharray={muted ? "4 4" : undefined}
/>
<text
x={x}
y={y + size * 0.36}
textAnchor="middle"
style={{
fontFamily: "var(--font-jetbrains-mono), ui-monospace, monospace",
fontSize: size,
letterSpacing: "0.12em",
textTransform: "uppercase",
fill: solid ? "#0f172a" : muted ? "#94a3b8" : INK,
}}
>
{label}
</text>
</g>
);
const CHIP = { width: 104, height: 36 } as const;
const Caption: React.FC<{
x: number;
@@ -63,35 +22,40 @@ const Caption: React.FC<{
fontSize: size,
letterSpacing: "0.12em",
textTransform: "uppercase",
fill: strong ? INK : "#64748b",
fill: strong ? CIRCUIT.ink : "#64748b",
}}
>
{children}
</text>
);
/** The signal gets weaker with every hand it passes through. */
const fading = (stops: readonly number[]): number[] =>
stops.map((_, i) => 0.95 - (i / stops.length) * 0.75);
/**
* ContactDiagram: a request passing slowly from hand to hand, next to the direct
* line you get when you talk to the person who does the work. Wide screens show
* the two lanes as rows, phones as two columns.
* ContactDiagram: a request passing slowly from chip to chip along a broken
* track and losing strength, next to the direct track you get when you talk to
* the person who does the work. Wide screens show the two lanes as rows, phones
* as two columns.
*/
export const ContactDiagram: React.FC = () => {
const tall = useNarrow();
const copy = useContent().working.diagram;
const chain = copy.chain;
const label = copy.description;
if (tall) {
// Two columns: the chain on the left, the direct line on the right.
// Two columns: the chain on the left, the direct track on the right.
const ys = chain.map((_, i) => 96 + i * 84);
const first = ys[0] ?? 96;
const last = ys[ys.length - 1] ?? 432;
const stops = ys.flatMap((y) => [y, y]);
const edge = CHIP.height / 2;
return (
<svg
viewBox="0 0 340 500"
role="img"
aria-label={label}
aria-label={copy.description}
className="block h-auto w-full"
>
<Caption x={24} y={38} size={12}>
@@ -100,60 +64,47 @@ export const ContactDiagram: React.FC = () => {
<Caption x={196} y={38} size={12} strong>
{copy.withMe}
</Caption>
<line
x1={90}
x2={90}
y1={first}
y2={last}
stroke={LINE}
strokeWidth={1.5}
strokeDasharray="4 4"
/>
<Track d={`M88 ${first}V${last}`} muted />
{chain.map((name, i) => (
<Node
<Chip
key={name}
x={90}
x={88}
y={ys[i] ?? 0}
{...CHIP}
label={name}
size={12}
muted={i > 0}
size={11}
tone={i > 0 ? "muted" : "line"}
pins={3}
/>
))}
<motion.circle
r={5}
cx={160}
fill={MARK}
initial={{ cy: first }}
animate={{
cy: stops,
opacity: stops.map((_, i) => 0.95 - (i / stops.length) * 0.6),
}}
transition={{
duration: 9,
repeat: Infinity,
repeatDelay: 0.8,
ease: "easeInOut",
}}
<Bit
xs={stops.map(() => 156)}
ys={stops}
opacity={fading(stops)}
duration={9}
pause={0.8}
/>
<line
x1={250}
x2={250}
y1={first}
y2={last}
stroke={INK}
strokeWidth={2}
<Track d={`M252 ${first + edge}V${last - edge}`} bus />
<Chip x={252} y={first} {...CHIP} label={copy.you} size={11} pins={3} />
<Chip
x={252}
y={last}
{...CHIP}
label={copy.me}
size={11}
tone="lit"
pins={3}
/>
<Node x={250} y={first} label={copy.you} size={12} />
<Node x={250} y={last} label={copy.me} size={12} solid />
<motion.circle
r={5}
cx={250}
fill={MARK}
initial={{ cy: first + NODE_H / 2 }}
animate={{
cy: [first + NODE_H / 2, last - NODE_H / 2, first + NODE_H / 2],
}}
transition={{ duration: 2.4, repeat: Infinity, ease: "easeInOut" }}
<Pad x={252} y={first + edge} />
<Pad x={252} y={last - edge} lit />
<Bit
xs={[252, 252, 252]}
ys={[first + edge + 10, last - edge - 10, first + edge + 10]}
opacity={[1, 1, 1]}
duration={2.4}
pause={0}
/>
</svg>
);
@@ -163,11 +114,12 @@ export const ContactDiagram: React.FC = () => {
const first = xs[0] ?? 76;
const last = xs[xs.length - 1] ?? 604;
const stops = xs.flatMap((x) => [x, x]);
const edge = CHIP.width / 2;
return (
<svg
viewBox="0 0 680 250"
role="img"
aria-label={label}
aria-label={copy.description}
className="block h-auto w-full"
>
<defs>
@@ -182,67 +134,43 @@ export const ContactDiagram: React.FC = () => {
</defs>
<rect width="680" height="250" fill="url(#contact-grid)" />
<Caption x={20} y={30} size={10}>
<Caption x={20} y={26} size={10}>
{copy.oftenLong}
</Caption>
<line
x1={first}
x2={last}
y1={74}
y2={74}
stroke={LINE}
strokeWidth={1.5}
strokeDasharray="4 4"
/>
<Track d={`M${first} 82H${last}`} muted />
{chain.map((name, i) => (
<Node
<Chip
key={name}
x={xs[i] ?? 0}
y={74}
y={82}
{...CHIP}
label={name}
size={10}
muted={i > 0}
tone={i > 0 ? "muted" : "line"}
/>
))}
<motion.circle
r={5}
cy={46}
fill={MARK}
initial={{ cx: first }}
animate={{
cx: stops,
opacity: stops.map((_, i) => 0.95 - (i / stops.length) * 0.6),
}}
transition={{
duration: 9,
repeat: Infinity,
repeatDelay: 0.8,
ease: "easeInOut",
}}
<Bit
xs={stops}
ys={stops.map(() => 46)}
opacity={fading(stops)}
duration={9}
pause={0.8}
/>
<Caption x={20} y={160} size={10} strong>
{copy.withMeLong}
</Caption>
<line
x1={first}
x2={last}
y1={204}
y2={204}
stroke={INK}
strokeWidth={2}
/>
<Node x={first} y={204} label={copy.you} size={10} />
<Node x={last} y={204} label={copy.me} size={10} solid />
<motion.circle
r={5}
cy={204}
fill={MARK}
initial={{ cx: first + NODE_W / 2 }}
animate={{
cx: [first + NODE_W / 2, last - NODE_W / 2, first + NODE_W / 2],
}}
transition={{ duration: 2.4, repeat: Infinity, ease: "easeInOut" }}
<Track d={`M${first + edge} 204H${last - edge}`} bus />
<Chip x={first} y={204} {...CHIP} label={copy.you} size={10} />
<Chip x={last} y={204} {...CHIP} label={copy.me} size={10} tone="lit" />
<Pad x={first + edge} y={204} />
<Pad x={last - edge} y={204} lit />
<Bit
xs={[first + edge + 14, last - edge - 14, first + edge + 14]}
ys={[204, 204, 204]}
opacity={[1, 1, 1]}
duration={2.4}
pause={0}
/>
</svg>
);
@@ -2,6 +2,7 @@
import * as React from "react";
import { motion } from "framer-motion";
import { Bit, Chip, Pad, Track } from "./circuit";
import { useContent } from "@/src/components/ContentProvider";
import type { SceneLabels } from "@/src/content/types";
import { fill } from "@/src/domain/template";
@@ -48,29 +49,6 @@ type Scene = React.FC<{ l: SceneLabels }>;
type Pt = readonly [number, number];
/** Points along a cubic curve, for packets that travel on it. */
const along = (p0: Pt, p1: Pt, p2: Pt, p3: Pt, samples = 12) => {
const xs: number[] = [];
const ys: number[] = [];
for (let i = 0; i <= samples; i++) {
const t = i / samples;
const u = 1 - t;
xs.push(
u * u * u * p0[0] +
3 * u * u * t * p1[0] +
3 * u * t * t * p2[0] +
t * t * t * p3[0],
);
ys.push(
u * u * u * p0[1] +
3 * u * u * t * p1[1] +
3 * u * t * t * p2[1] +
t * t * t * p3[1],
);
}
return { xs, ys };
};
/** A small packet that keeps travelling a route. */
const Packet: React.FC<{
xs: readonly number[];
@@ -78,28 +56,7 @@ const Packet: React.FC<{
delay?: number;
duration?: number;
pause?: number;
}> = ({ xs, ys, delay = 0, duration = 1.2, pause = 1.4 }) => (
<motion.circle
r={3.5}
fill={MARK}
stroke={MARK}
strokeOpacity={0.28}
strokeWidth={6}
initial={{ cx: xs[0], cy: ys[0], opacity: 0 }}
animate={{
cx: [...xs],
cy: [...ys],
opacity: xs.map((_, i) => (i === 0 || i === xs.length - 1 ? 0 : 1)),
}}
transition={{
duration,
delay,
repeat: Infinity,
repeatDelay: pause,
ease: "easeInOut",
}}
/>
);
}> = (props) => <Bit {...props} />;
const Person: React.FC<{ x: number; label: string; filled?: boolean }> = ({
x,
@@ -544,44 +501,45 @@ const Decide: Scene = ({ l }) => (
>
{l.you}
</text>
<Pad x={116} y={150} />
{[70, 150, 230]
.map((y, i) => ({ y, label: l.options[i] ?? "" }))
.map((option, i) => (
<g key={option.label}>
<motion.path
d={`M 116 150 C 190 150 180 ${option.y} 250 ${option.y}`}
fill="none"
stroke={INK}
strokeWidth={FINE}
{...draw(0.3 + i * 0.4)}
/>
<Packet
{...along([116, 150], [190, 150], [180, option.y], [250, option.y])}
delay={1.6 + i * 1.3}
duration={1.1}
pause={2.8}
/>
<motion.g {...appear(0.7 + i * 0.4, 0)}>
<rect
x={250}
y={option.y - 16}
width={180}
height={32}
rx={16}
fill={PAPER}
stroke={LINE}
.map((option, i) => {
// Straight out, one 45 degree bend, straight in.
const bend = Math.abs(option.y - 150);
const turn = 150 + bend;
return (
<g key={option.label}>
<motion.path
d={`M116 150H150L${turn} ${option.y}H250`}
fill="none"
stroke={INK}
strokeWidth={1.4}
strokeLinecap="square"
{...draw(0.3 + i * 0.4)}
/>
<text
x={340}
y={option.y + 4}
textAnchor="middle"
style={{ ...MONO, fill: INK }}
>
{option.label}
</text>
</motion.g>
</g>
))}
<Packet
xs={[116, 150, turn, 250]}
ys={[150, 150, option.y, option.y]}
delay={1.6 + i * 1.3}
duration={1.1}
pause={2.8}
/>
<motion.g {...appear(0.7 + i * 0.4, 0)}>
<Chip
x={340}
y={option.y}
width={180}
height={32}
label={option.label}
size="var(--scene-label, 10px)"
pins={6}
/>
<Pad x={250} y={option.y} />
</motion.g>
</g>
);
})}
</g>
);
@@ -852,21 +810,34 @@ const Tool: React.FC<{ x: number; y: number; label: string }> = ({
y,
label,
}) => (
<g>
<rect
x={x - 56}
y={y - 22}
width={112}
height={44}
rx={10}
fill={SOFT}
stroke={RIM}
strokeWidth={FINE}
/>
<text x={x} y={y + 4} textAnchor="middle" style={{ ...MONO, fill: INK }}>
{label}
</text>
</g>
<Chip
x={x}
y={y}
width={112}
height={44}
label={label}
size="var(--scene-label, 10px)"
/>
);
/** Where the tracks between the tools end. */
const TOOL_PADS: readonly Pt[] = [
[146, 80],
[334, 80],
[90, 102],
[90, 178],
[390, 102],
[390, 178],
[146, 200],
[334, 200],
];
const ToolPads: React.FC<{ delay: number }> = ({ delay }) => (
<motion.g {...appear(delay, 0)}>
{TOOL_PADS.map(([x, y]) => (
<Pad key={`${x}-${y}`} x={x} y={y} />
))}
</motion.g>
);
/** Automation, step 2: today a person carries the data from tool to tool. */
@@ -937,6 +908,7 @@ const Wiring: Scene = ({ l }) => (
{TOOLS.map((tool, i) => (
<Tool key={i} {...tool} label={l.tools[i] ?? ""} />
))}
<ToolPads delay={1.9} />
<motion.text
x={240}
y={268}
@@ -950,42 +922,35 @@ const Wiring: Scene = ({ l }) => (
);
/** Automation, step 4: the work travels between the systems on its own. */
/** The four runs between the tools, each as from x, from y, to x, to y. */
const RUNS: readonly (readonly [number, number, number, number])[] = [
[146, 80, 334, 80],
[390, 102, 390, 178],
[334, 200, 146, 200],
[90, 178, 90, 102],
];
const Running: Scene = ({ l }) => (
<g>
{WIRES.map((d) => (
<path key={d} d={d} fill="none" stroke={INK} strokeWidth={1.5} />
))}
{[
{ from: [146, 80], to: [334, 80], delay: 0 },
{ from: [390, 102], to: [390, 178], delay: 0.7 },
{ from: [334, 200], to: [146, 200], delay: 1.4 },
{ from: [90, 178], to: [90, 102], delay: 2.1 },
].map((dot) => (
<motion.circle
key={dot.delay}
r={3.5}
fill={MARK}
stroke={MARK}
strokeOpacity={0.28}
strokeWidth={6}
initial={{ cx: dot.from[0], cy: dot.from[1], opacity: 0 }}
animate={{
cx: [dot.from[0]!, dot.to[0]!],
cy: [dot.from[1]!, dot.to[1]!],
opacity: [0, 1, 1, 0],
}}
transition={{
duration: 1.4,
delay: dot.delay,
repeat: Infinity,
repeatDelay: 1.4,
ease: "easeInOut",
}}
/>
<Track key={d} d={d} bus />
))}
{TOOLS.map((tool, i) => (
<Tool key={i} {...tool} label={l.tools[i] ?? ""} />
))}
<ToolPads delay={0} />
{RUNS.map(([fx, fy, tx, ty], i) => (
<Bit
key={i}
xs={[fx, fx, tx, tx]}
ys={[fy, fy, ty, ty]}
opacity={[0, 1, 1, 0]}
delay={i * 0.7}
duration={1.4}
pause={1.4}
value={i % 2 === 0 ? 1 : 0}
/>
))}
<motion.text
x={240}
y={268}
@@ -0,0 +1,210 @@
"use client";
import * as React from "react";
import { motion } from "framer-motion";
/**
* The parts the drawings share with the story thread: everything is laid out
* like a circuit board. Systems and people are chips with pins, connections are
* tracks with solder points, and what travels along them is a bit.
*/
export const CIRCUIT = {
ink: "#f8fafc",
ground: "#0f172a",
line: "#475569",
rim: "#64748b",
soft: "#1e293b",
mark: "#fde68a",
live: "#10b981",
light: "#a5b4fc",
tint: "rgba(248, 250, 252, 0.14)",
} as const;
const MONO = "var(--font-jetbrains-mono), ui-monospace, monospace";
const PIN = 5;
type ChipTone = "line" | "lit" | "muted";
/** A system or a person: a chip with pins on its long sides and a label. */
export const Chip: React.FC<{
x: number;
y: number;
width: number;
height: number;
label: string;
/** Font size as a number or a CSS value. */
size: number | string;
tone?: ChipTone;
/** Pins per long side. */
pins?: number;
}> = ({ x, y, width, height, label, size, tone = "line", pins = 4 }) => {
const muted = tone === "muted";
const stroke = muted ? CIRCUIT.line : CIRCUIT.ink;
const left = x - width / 2;
const top = y - height / 2;
return (
<g>
{Array.from({ length: pins }, (_, i) => {
const px = left + ((i + 1) * width) / (pins + 1);
return (
<path
key={i}
d={`M${px} ${top}v${-PIN}M${px} ${top + height}v${PIN}`}
stroke={muted ? CIRCUIT.line : CIRCUIT.rim}
strokeWidth={1.25}
/>
);
})}
<rect
x={left}
y={top}
width={width}
height={height}
rx={4}
fill={tone === "lit" ? CIRCUIT.tint : CIRCUIT.ground}
stroke={stroke}
strokeWidth={tone === "lit" ? 1.5 : 1.1}
strokeDasharray={muted ? "4 4" : undefined}
style={
tone === "lit"
? { filter: "drop-shadow(0 0 6px rgba(165, 180, 252, 0.7))" }
: undefined
}
/>
{/* The notch that marks pin one. */}
<circle
cx={left + 7}
cy={top + 7}
r={1.6}
fill={muted ? CIRCUIT.line : CIRCUIT.rim}
/>
<text
x={x}
y={y}
textAnchor="middle"
dominantBaseline="central"
style={{
fontFamily: MONO,
fontSize: size,
letterSpacing: "0.12em",
textTransform: "uppercase",
fill: muted ? "#94a3b8" : CIRCUIT.ink,
}}
>
{label}
</text>
</g>
);
};
/** A solder point: where a track ends, bends or meets a chip. */
export const Pad: React.FC<{
x: number;
y: number;
muted?: boolean;
lit?: boolean;
}> = ({ x, y, muted = false, lit = false }) => (
<circle
cx={x}
cy={y}
r={3.2}
fill={lit ? CIRCUIT.light : CIRCUIT.ground}
stroke={muted ? CIRCUIT.line : CIRCUIT.ink}
strokeWidth={1.25}
/>
);
/**
* A bit travelling a route, again and again. `xs` and `ys` are the stops; it
* fades in after the first and out before the last unless `opacity` says otherwise.
*/
export const Bit: React.FC<{
xs: readonly number[];
ys: readonly number[];
opacity?: readonly number[];
delay?: number;
duration?: number;
pause?: number;
/** The digit it shows. */
value?: 0 | 1;
}> = ({
xs,
ys,
opacity,
delay = 0,
duration = 1.2,
pause = 1.4,
value = 1,
}) => (
<motion.g
initial={{ x: xs[0], y: ys[0], opacity: 0 }}
animate={{
x: [...xs],
y: [...ys],
opacity: opacity
? [...opacity]
: xs.map((_, i) => (i === 0 || i === xs.length - 1 ? 0 : 1)),
}}
transition={{
duration,
delay,
repeat: Infinity,
repeatDelay: pause,
ease: "easeInOut",
}}
>
<rect
x={-6}
y={-6}
width={12}
height={12}
rx={2}
fill={CIRCUIT.mark}
style={{ filter: "drop-shadow(0 0 6px rgba(251, 191, 36, 0.9))" }}
/>
<text
textAnchor="middle"
dominantBaseline="central"
style={{
fontFamily: MONO,
fontSize: 9,
fontWeight: 700,
fill: CIRCUIT.ground,
}}
>
{value}
</text>
</motion.g>
);
/** A track: straight runs and 45 degree bends, with a second fainter track beside it. */
export const Track: React.FC<{
d: string;
muted?: boolean;
/** Draw the parallel second track. */
bus?: boolean;
}> = ({ d, muted = false, bus = false }) => (
<g fill="none" strokeLinecap="square" strokeLinejoin="miter">
{bus && (
<path
d={d}
stroke={CIRCUIT.light}
strokeWidth={1}
opacity={0.35}
transform="translate(4 4)"
/>
)}
<path
d={d}
stroke={muted ? CIRCUIT.line : CIRCUIT.ink}
strokeWidth={muted ? 1.25 : 1.6}
strokeDasharray={muted ? "4 4" : undefined}
style={
muted
? undefined
: { filter: "drop-shadow(0 0 4px rgba(165, 180, 252, 0.6))" }
}
/>
</g>
);
+1 -2
View File
@@ -74,7 +74,7 @@ export const de: Content = {
},
services: {
label: "Leistungen",
title: { lead: "Was brauchen Sie?", rest: "Vier typische Fälle." },
title: { lead: "Was brauchen Sie?", rest: "Drei typische Fälle." },
tablistLabel: "Was brauchen Sie?",
youGet: "Das bekommen Sie",
howItRuns: "So läuft es ab",
@@ -98,7 +98,6 @@ export const de: Content = {
orientationLabel: "Zur Orientierung",
net: "Alle Preise netto.",
cta: "Schätzung anfragen",
freelance: "Buchung und Preise für Teams",
},
paths: [
{
+1 -2
View File
@@ -73,7 +73,7 @@ export const en: Content = {
},
services: {
label: "Services",
title: { lead: "What do you need?", rest: "Four typical cases." },
title: { lead: "What do you need?", rest: "Three typical cases." },
tablistLabel: "What do you need?",
youGet: "What you get",
howItRuns: "How it runs",
@@ -97,7 +97,6 @@ export const en: Content = {
orientationLabel: "For orientation",
net: "All prices net.",
cta: "Ask for an estimate",
freelance: "Booking and rates for teams",
},
paths: [
{
-2
View File
@@ -197,8 +197,6 @@ export type Content = {
readonly orientationLabel: string;
readonly net: string;
readonly cta: string;
/** The pointer to the booking terms on the freelance page. */
readonly freelance: string;
};
readonly paths: readonly ServicePath[];
readonly scenes: SceneLabels;