diff --git a/apps/web/app/about/page.tsx b/apps/web/app/about/page.tsx index 9d6d189..b483f76 100644 --- a/apps/web/app/about/page.tsx +++ b/apps/web/app/about/page.tsx @@ -12,6 +12,7 @@ import { ParticleNetwork, GridLines, } from "../../src/components/Landing"; +import { Signature } from "../../src/components/Signature"; import { Check } from "lucide-react"; import { H1, @@ -115,10 +116,8 @@ export default function AboutPage() { Agenturen, Konzerne, Startups – ich habe die Branche von allen Seiten kennengelernt. Was hängen geblieben ist:{" "} - - Ergebnisse zählen. - {" "} - Nicht der Weg dorthin. + Ergebnisse zählen. Nicht der + Weg dorthin. @@ -228,53 +227,68 @@ export default function AboutPage() { - {/* Section 03: Philosophie – what drives me */} -
-
+ {/* Section 03: Garantie – The Pledge */} +
+
-

- Ich stehe für
- meine Arbeit gerade. -

-
+
+

+ Ich stehe für
+ meine Arbeit gerade. +

-
-
- - - Keine Hierarchien, keine Ausreden. Wenn etwas nicht passt, - liegt die Verantwortung bei mir – und ich{" "} - - löse es. +
+

+ Keine Hierarchien. Keine Ausreden. Wenn etwas nicht passt, + liegt die Verantwortung bei mir. +

+

+ Ich liefere nicht nur Code, sondern{" "} + + Ergebnisse + + + - - -

- {[ - "Vollständige Transparenz", - "Ein Ansprechpartner", - "Messbare Qualität", - "Langfristige Partnerschaft", - ].map((item, i) => ( - -
-
- -
- -
-
- ))} + , auf die Sie bauen können. +

+
+ +
+
+

+ Fixpreis-Garantie +

+

+ Keine versteckten Kosten. Der vereinbarte Preis ist final. +

+
+
+

+ Satisfaction Guarantee +

+

+ Wir gehen erst live, wenn Sie zu 100% zufrieden sind. +

+
+
+ +
+
+ +
- - {/* Decorative terminal */} - - - -
+
@@ -306,9 +320,7 @@ export default function AboutPage() { Lassen Sie uns gemeinsam etwas bauen, das{" "} - - wirklich funktioniert. - + wirklich funktioniert. diff --git a/apps/web/app/case-studies/klz-cables/page.tsx b/apps/web/app/case-studies/klz-cables/page.tsx index 7f9584b..e1af421 100644 --- a/apps/web/app/case-studies/klz-cables/page.tsx +++ b/apps/web/app/case-studies/klz-cables/page.tsx @@ -98,7 +98,7 @@ export default function KLZCablesCaseStudy() {
Engineering eines
- B2B Commerce Systems. + Systems.
Vom statischen Altsystem zum industriellen Standard. Ich @@ -172,8 +172,8 @@ export default function KLZCablesCaseStudy() { Attribute in einer zentralen relationalen Instanz. Durch die Implementierung nativer PHP-Services und den Verzicht auf volatile Drittanbieter-Plugins wurde ein System geschaffen, - das keine technologischen Überraschungen zulässt.{" "} - Stability by Design. + das keine technologischen Überraschungen zulässt. Stability by{" "} + Design.
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 1f178d7..8f5977f 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -25,6 +25,7 @@ import { IconList, IconListItem } from "../src/components/IconList"; import { HeroSection } from "../src/components/HeroSection"; import { GlitchText } from "../src/components/GlitchText"; import { Marker } from "../src/components/Marker"; +import { PenCircle } from "../src/components/PenCircle"; export default function LandingPage() { return ( @@ -45,12 +46,7 @@ export default function LandingPage() {

Kein Agentur-Zirkus.
- - Nur{" "} - - Ergebnisse. - - + Ergebnisse.

@@ -104,12 +100,7 @@ export default function LandingPage() {

Ich arbeite für das Ergebnis,
- - nicht gegen die{" "} - - Uhr. - - + nicht gegen die Uhr.

@@ -118,14 +109,24 @@ export default function LandingPage() { negativeLabel="Klassisch" negativeText="Wochen in Planung, bevor eine einzige Zeile Code geschrieben wird." positiveLabel="Mein Weg" - positiveText="Schnelle Prototypen. Ergebnisse in Tagen, nicht Monaten." + positiveText={ + <> + Schnelle Prototypen. Ergebnisse in{" "} + Tagen, nicht Monaten. + + } delay={0.1} /> + Fixpreise. Sie wissen von + Anfang an, was es kostet. + + } reverse delay={0.2} /> @@ -290,7 +291,7 @@ export default function LandingPage() { Beschreiben Sie kurz Ihr Vorhaben. Ich melde mich{" "} - zeitnah + zeitnah {" "} bei Ihnen. diff --git a/apps/web/app/websites/page.tsx b/apps/web/app/websites/page.tsx index 29d2fc0..2d9cbe0 100644 --- a/apps/web/app/websites/page.tsx +++ b/apps/web/app/websites/page.tsx @@ -42,11 +42,9 @@ export default function WebsitesPage() { SYSTEM ENGINEERING

- Websites, die
+ Websites, die einfach
- - einfach funktionieren. - + funktionieren.

@@ -121,9 +119,7 @@ export default function WebsitesPage() {

Geschwindigkeit ist
- - kein Extra. Sie ist Standard. - + kein Extra. Sie ist Standard.

diff --git a/apps/web/src/components/Effects/ArchitectureVisualizer.tsx b/apps/web/src/components/Effects/ArchitectureVisualizer.tsx index 306c4eb..741960d 100644 --- a/apps/web/src/components/Effects/ArchitectureVisualizer.tsx +++ b/apps/web/src/components/Effects/ArchitectureVisualizer.tsx @@ -59,19 +59,24 @@ const Node: React.FC<{ ); const Connector: React.FC<{ active?: boolean }> = ({ active }) => ( -
+
{active && ( )}
diff --git a/apps/web/src/components/Landing/ComparisonRow.tsx b/apps/web/src/components/Landing/ComparisonRow.tsx index 131bbfc..80dcfed 100644 --- a/apps/web/src/components/Landing/ComparisonRow.tsx +++ b/apps/web/src/components/Landing/ComparisonRow.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { ArrowRight } from "lucide-react"; import { Reveal } from "../Reveal"; import { Label, H3, LeadText } from "../Typography"; +import { Strikethrough } from "../Strikethrough"; import { cn } from "../../utils/cn"; interface ComparisonRowProps { @@ -38,11 +39,11 @@ export const ComparisonRow: React.FC = ({ )} >
-
diff --git a/apps/web/src/components/Marker.tsx b/apps/web/src/components/Marker.tsx index e2742d3..e9270db 100644 --- a/apps/web/src/components/Marker.tsx +++ b/apps/web/src/components/Marker.tsx @@ -1,14 +1,9 @@ "use client"; -import React from "react"; +import React, { useId, useMemo } from "react"; import { motion } from "framer-motion"; import { cn } from "../utils/cn"; -/** - * TECHNICAL MARKER COMPONENT - * Implements the "hand-drawn marker" effect. - * Animates in when entering the viewport. - */ interface MarkerProps { children: React.ReactNode; delay?: number; @@ -16,74 +11,171 @@ interface MarkerProps { color?: string; } +/** + * Copic Marker Component + * + * Uses hand-drawn filled polygon paths (NOT rects or strokes) for an + * organic, human feel. Each highlight band is a wobbly shape with + * bezier curves, not a perfect rectangle. Second strokes are offset + * to simulate imperfect re-highlighting. + */ + +// Seeded PRNG for SSR-safe randomness +function createRng(seed: string) { + let s = 0; + for (let i = 0; i < seed.length; i++) { + s = ((s << 5) - s + seed.charCodeAt(i)) | 0; + } + return () => { + s = (s * 16807 + 0) % 2147483647; + return (s & 0x7fffffff) / 0x7fffffff; + }; +} + +/** + * Generate a hand-drawn highlight band path. + * Instead of a rectangle, we trace an organic polygon with wobbly edges. + * + * The shape goes: left edge → top edge (left to right) → right edge → bottom edge (right to left) + * Each edge has 1-2 control points for subtle curves. + * + * ViewBox: 0 0 100 100 with preserveAspectRatio="none" + * So x=0..100 maps to text width, y=0..100 maps to text height. + */ +function generateBandPath(rng: () => number, bandIndex: number) { + // Vertical position: center band in the text height + // First band centered around y=50, second shifted up or down + const yOffset = bandIndex === 0 ? 0 : (rng() - 0.5) * 20; // -10 to +10 shift + const yCenter = 50 + yOffset; + + // Band thickness: covers ~40-50% of text height + const halfHeight = 20 + rng() * 8; // 20-28 units (40-56% of viewBox) + const yTop = yCenter - halfHeight; + const yBottom = yCenter + halfHeight; + + // Horizontal extent: slight random overshoot/undershoot + const xStart = -2 + rng() * 3; // -2 to 1 + const xEnd = 99 + rng() * 3; // 99 to 102 + + // Generate wobbly top edge points (left to right) + const topWobble1 = yTop + (rng() - 0.5) * 6; + const topWobble2 = yTop + (rng() - 0.5) * 6; + const topWobble3 = yTop + (rng() - 0.5) * 6; + + // Generate wobbly bottom edge points (right to left) + const botWobble1 = yBottom + (rng() - 0.5) * 6; + const botWobble2 = yBottom + (rng() - 0.5) * 6; + const botWobble3 = yBottom + (rng() - 0.5) * 6; + + // Slight angle on left/right edges + const leftTopY = yTop + (rng() - 0.5) * 4; + const leftBotY = yBottom + (rng() - 0.5) * 4; + const rightTopY = yTop + (rng() - 0.5) * 4; + const rightBotY = yBottom + (rng() - 0.5) * 4; + + // Build the path: + // Start at top-left, trace top edge with curves, down right edge, + // trace bottom edge with curves back left, close. + const path = [ + // Start top-left + `M ${xStart},${leftTopY}`, + // Top edge: 3 curve segments left→right + `C ${xStart + 15},${topWobble1} ${xStart + 30},${topWobble1} ${33},${topWobble2}`, + `C ${45},${topWobble2} ${55},${topWobble3} ${67},${topWobble3}`, + `C ${78},${topWobble3} ${xEnd - 10},${rightTopY} ${xEnd},${rightTopY}`, + // Right edge down + `L ${xEnd + (rng() - 0.5) * 2},${rightBotY}`, + // Bottom edge: 3 curve segments right→left + `C ${xEnd - 10},${botWobble1} ${78},${botWobble1} ${67},${botWobble2}`, + `C ${55},${botWobble2} ${45},${botWobble3} ${33},${botWobble3}`, + `C ${xStart + 30},${botWobble3} ${xStart + 15},${leftBotY} ${xStart},${leftBotY}`, + // Close + `Z`, + ].join(" "); + + return path; +} + export const Marker: React.FC = ({ children, delay = 0, className = "", - color = "rgba(255,235,59,0.7)", + color = "rgba(250, 204, 21, 0.5)", }) => { + const id = useId(); + const filterId = `marker-rough-${id.replace(/:/g, "")}`; + + const bands = useMemo(() => { + const rng = createRng(id); + const numBands = rng() > 0.5 ? 2 : 1; + const result = []; + + for (let i = 0; i < numBands; i++) { + result.push({ + d: generateBandPath(rng, i), + delay: delay + i * 0.15, + duration: 0.4 + rng() * 0.15, + }); + } + return result; + }, [id, delay]); + return ( - {children} + + {children} + ); }; diff --git a/apps/web/src/components/PenCircle.tsx b/apps/web/src/components/PenCircle.tsx new file mode 100644 index 0000000..226dd3e --- /dev/null +++ b/apps/web/src/components/PenCircle.tsx @@ -0,0 +1,124 @@ +"use client"; + +import React, { useId, useMemo } from "react"; +import { motion } from "framer-motion"; +import { cn } from "../utils/cn"; + +interface PenCircleProps { + children: React.ReactNode; + delay?: number; + className?: string; + color?: string; +} + +/** + * Ballpoint Pen Circle Component + * + * Draws a hand-drawn ellipse around children using SVG. + * Key: uses preserveAspectRatio="none" so the ellipse stretches + * to match the element dimensions regardless of text length. + */ + +function createRng(seed: string) { + let s = 0; + for (let i = 0; i < seed.length; i++) { + s = ((s << 5) - s + seed.charCodeAt(i)) | 0; + } + return () => { + s = (s * 16807 + 0) % 2147483647; + return (s & 0x7fffffff) / 0x7fffffff; + }; +} + +function generateCirclePath(rng: () => number) { + // We draw an ellipse in a 0-100 x 0-100 viewBox. + // preserveAspectRatio="none" will stretch it to fit the element. + const cx = 50; + const cy = 50; + + // Radii in viewBox units — will be stretched by the element + const rx = 50; + const ry = 50; + + // Wobble for organic feel + const w = () => (rng() - 0.5) * 6; + + const k = 0.5522847; + const kx = rx * k; + const ky = ry * k; + + // 4 cardinal points with wobble + const top = { x: cx + w(), y: cy - ry + w() * 0.5 }; + const right = { x: cx + rx + w() * 0.3, y: cy + w() }; + const bottom = { x: cx + w(), y: cy + ry + w() * 0.5 }; + const left = { x: cx - rx + w() * 0.3, y: cy + w() }; + + // End slightly offset from start for imperfect closure + const endOffX = (rng() - 0.5) * 8; + const endOffY = (rng() - 0.5) * 4; + + return [ + `M ${top.x},${top.y}`, + `C ${top.x + kx + w()},${top.y + w()} ${right.x + w()},${right.y - ky + w()} ${right.x},${right.y}`, + `C ${right.x + w()},${right.y + ky + w()} ${bottom.x + kx + w()},${bottom.y + w()} ${bottom.x},${bottom.y}`, + `C ${bottom.x - kx + w()},${bottom.y + w()} ${left.x + w()},${left.y + ky + w()} ${left.x},${left.y}`, + `C ${left.x + w()},${left.y - ky + w()} ${top.x - kx + w()},${top.y + w()} ${top.x + endOffX},${top.y + endOffY}`, + ].join(" "); +} + +export const PenCircle: React.FC = ({ + children, + delay = 0, + className = "", + color = "rgba(37, 99, 235, 0.65)", // Blue ballpoint pen +}) => { + const id = useId(); + + const path = useMemo(() => { + const rng = createRng(id); + return generateCirclePath(rng); + }, [id]); + + return ( + + + {children} + + ); +}; diff --git a/apps/web/src/components/Signature.tsx b/apps/web/src/components/Signature.tsx new file mode 100644 index 0000000..240f176 --- /dev/null +++ b/apps/web/src/components/Signature.tsx @@ -0,0 +1,65 @@ +"use client"; + +import React from "react"; +import { motion } from "framer-motion"; +import { cn } from "../utils/cn"; + +interface SignatureProps { + className?: string; + delay?: number; +} + +export const Signature: React.FC = ({ + className, + delay = 0, +}) => { + return ( +
+ + {/* M */} + + {/* a r c (scribble) */} + + {/* Mintel (scribble + underline) */} + + +
+ Marc Mintel +
+
+ ); +}; diff --git a/apps/web/src/components/Strikethrough.tsx b/apps/web/src/components/Strikethrough.tsx new file mode 100644 index 0000000..56e9c82 --- /dev/null +++ b/apps/web/src/components/Strikethrough.tsx @@ -0,0 +1,140 @@ +"use client"; + +import React, { useId, useMemo } from "react"; +import { motion } from "framer-motion"; +import { cn } from "../utils/cn"; + +interface StrikethroughProps { + children: string; // Enforce string children for splitting + delay?: number; + className?: string; + color?: string; +} + +/** + * Hand-drawn Strikethrough Component + * + * NOW SUPPORTS MULTI-LINE TEXT! + * + * Strategy: Splits the text into individual words. Each word is wrapped + * in a span with its own SVG strikethrough. This allows the text to + * wrap naturally across lines, and each word carries its strikethrough + * with it. + */ + +function createRng(seed: string) { + let s = 0; + for (let i = 0; i < seed.length; i++) { + s = ((s << 5) - s + seed.charCodeAt(i)) | 0; + } + return () => { + s = (s * 16807 + 0) % 2147483647; + return (s & 0x7fffffff) / 0x7fffffff; + }; +} + +function generateStrikePath(rng: () => number) { + // Simple approach: just a slightly wobbly line from left to right + // through vertical center (y=50). + const points: { x: number; y: number }[] = []; + const numPoints = 4; // Fewer points for shorter word segments + + for (let i = 0; i <= numPoints; i++) { + const x = (i / numPoints) * 100; + const y = 55 + (rng() - 0.5) * 15; // ±7.5 units wobble + points.push({ x, y }); + } + + // Build a smooth path through all points + let d = `M ${points[0].x},${points[0].y}`; + for (let i = 1; i < points.length - 1; i++) { + const midX = (points[i].x + points[i + 1].x) / 2; + const midY = (points[i].y + points[i + 1].y) / 2; + d += ` Q ${points[i].x},${points[i].y} ${midX},${midY}`; + } + const last = points[points.length - 1]; + d += ` L ${last.x},${last.y}`; + + return d; +} + +const WordStrike: React.FC<{ + word: string; + index: number; + baseDelay: number; + color: string; + seed: string; +}> = ({ word, index, baseDelay, color, seed }) => { + // Unique seed per word so they look different + const uniqueSeed = `${seed}-${index}-${word}`; + + const path = useMemo(() => { + const rng = createRng(uniqueSeed); + return generateStrikePath(rng); + }, [uniqueSeed]); + + return ( + + + {word} + + ); +}; + +export const Strikethrough: React.FC = ({ + children, + delay = 0, + className = "", + color = "rgba(220, 50, 50, 0.8)", +}) => { + const id = useId(); + + // Split by spaces but preserve them as separate elements for spacing + // Actually, we can just split by space and rejoin with non-struck spaces + const words = children.split(" "); + + return ( + + {words.map((word, i) => ( + + + {i < words.length - 1 && " "} + + ))} + + ); +};