feat: binary glow effects, refined drawings and calls to action

Dark surfaces now carry light: frames glow along their edge and shed binary
digits (BitAura), dark sections open with a lit seam (BitSeam), the career
path is a line of light and "My work" became a dark stage. Buttons carry a
travelling light and a spot that follows the pointer (BitGlow).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 13:07:09 +02:00
co-authored by Claude Opus 5.5
parent e9f05b02fc
commit e89a248ca5
21 changed files with 1532 additions and 606 deletions
+10 -1
View File
@@ -91,6 +91,8 @@ export const BitHeadline: React.FC<{
const reducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
// Touch screens have no pointer to light the digits up, so a spot wanders on its own.
const noHover = window.matchMedia("(hover: none)").matches;
let measured: Measured[] = [];
let width = 0;
@@ -306,6 +308,13 @@ export const BitHeadline: React.FC<{
if (disposed) return;
if (now - lastFrame >= FRAME_MS) {
lastFrame = now;
if (noHover) {
const seconds = now / 1000;
pointer = {
x: width * (0.5 + 0.46 * Math.sin(seconds * 0.55)),
y: height * (0.5 + 0.4 * Math.sin(seconds * 0.9 + 1)),
};
}
paint(now);
}
raf = requestAnimationFrame(loop);
@@ -339,7 +348,7 @@ export const BitHeadline: React.FC<{
restart();
setPainted(true);
resizeObserver.observe(wrap);
if (!reducedMotion) {
if (!reducedMotion && !noHover) {
window.addEventListener("pointermove", onPointerMove);
document.addEventListener("pointerleave", onPointerLeave);
}
+60 -32
View File
@@ -1,5 +1,6 @@
import * as React from "react";
import { BinaryTicker } from "@/src/components/bits/BinaryTicker";
import { BitSeam } from "@/src/components/bits/BitSeam";
import { BitWipe } from "@/src/components/bits/BitWipe";
import { DecodeText } from "@/src/components/bits/DecodeText";
import { Reveal } from "@/src/components/Reveal";
@@ -10,6 +11,8 @@ interface BlockProps {
label: string;
title: React.ReactNode;
id?: string;
/** `dark` turns the section into a dark stage. */
tone?: "light" | "dark";
children: React.ReactNode;
}
@@ -22,36 +25,61 @@ export const Block: React.FC<BlockProps> = ({
label,
title,
id,
tone = "light",
children,
}) => (
<section
id={id}
className="relative scroll-mt-24 border-t border-slate-200 bg-white"
>
<div className="mx-auto w-full max-w-[1400px] px-5 py-20 md:px-12 md:py-36">
<Reveal>
<div className="mb-8 flex items-center gap-4 md:mb-12">
<span className="font-mono text-[11px] font-bold tracking-[0.2em] text-slate-900">
{index}
</span>
<span className="h-px w-10 bg-slate-300" />
<DecodeText
text={label}
className="font-mono text-[11px] uppercase tracking-[0.25em] text-slate-500"
/>
<BinaryTicker
value={parseInt(index, 10)}
alive
className="ml-auto hidden font-mono text-[10px] tracking-[0.3em] text-slate-300 md:block"
/>
</div>
</Reveal>
<BitWipe delay={0.1}>
<h2 className="m-0 max-w-6xl text-4xl font-bold leading-[0.95] tracking-tighter text-slate-900 md:text-7xl lg:text-[6.5rem]">
{title}
</h2>
</BitWipe>
<div className="mt-14 md:mt-24">{children}</div>
</div>
</section>
);
}) => {
const dark = tone === "dark";
return (
<section
id={id}
className={`relative scroll-mt-24 border-t ${
dark
? "overflow-clip border-slate-800 bg-slate-900"
: "overflow-x-clip border-slate-200 bg-white"
}`}
>
{dark && (
<div
className="pointer-events-none absolute inset-0 opacity-70"
style={{
backgroundImage: "radial-gradient(#1e293b 1px, transparent 1px)",
backgroundSize: "20px 20px",
}}
aria-hidden="true"
/>
)}
{dark && <BitSeam />}
<div className="relative mx-auto w-full max-w-[1400px] px-5 py-20 md:px-12 md:py-36">
<Reveal>
<div className="mb-8 flex items-center gap-4 md:mb-12">
<span
className={`font-mono text-[11px] font-bold tracking-[0.2em] ${dark ? "text-white" : "text-slate-900"}`}
>
{index}
</span>
<span
className={`h-px w-10 ${dark ? "bg-slate-700" : "bg-slate-300"}`}
/>
<DecodeText
text={label}
className={`font-mono text-[11px] uppercase tracking-[0.25em] ${dark ? "text-slate-400" : "text-slate-500"}`}
/>
<BinaryTicker
value={parseInt(index, 10)}
alive
className={`ml-auto hidden font-mono text-[10px] tracking-[0.3em] md:block ${dark ? "text-slate-600" : "text-slate-300"}`}
/>
</div>
</Reveal>
<BitWipe delay={0.1} tone={dark ? "light" : "dark"}>
<h2
className={`m-0 max-w-6xl text-4xl font-bold leading-[0.95] tracking-tighter md:text-7xl lg:text-[6.5rem] ${dark ? "text-white" : "text-slate-900"}`}
>
{title}
</h2>
</BitWipe>
<div className="mt-14 md:mt-24">{children}</div>
</div>
</section>
);
};
+9 -2
View File
@@ -1,6 +1,8 @@
import * as React from "react";
import { Reveal } from "@/src/components/Reveal";
import { EmailLink } from "@/src/components/EmailLink";
import { BitAura } from "@/src/components/bits/BitAura";
import { BitSeam } from "@/src/components/bits/BitSeam";
import { BitHeadline, type BitLine } from "./BitHeadline";
import { ContactComposer } from "./ContactComposer";
@@ -13,7 +15,8 @@ const CLOSING: readonly BitLine[] = [
export const ClosingCta: React.FC = () => (
<section
id="contact"
className="relative border-t border-slate-200 bg-slate-900 text-white"
data-glow
className="relative overflow-clip border-t border-slate-200 bg-slate-900 text-white"
>
<div
className="pointer-events-none absolute inset-0 opacity-70"
@@ -23,6 +26,8 @@ export const ClosingCta: React.FC = () => (
}}
aria-hidden="true"
/>
<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">
<Reveal className="lg:col-span-6">
<BitHeadline
@@ -40,7 +45,9 @@ export const ClosingCta: React.FC = () => (
</div>
</Reveal>
<Reveal delay={0.15} className="lg:col-span-6">
<ContactComposer />
<BitAura source="around" tone="dark">
<ContactComposer />
</BitAura>
</Reveal>
</div>
</section>
@@ -122,7 +122,7 @@ export const ContactComposer: React.FC = () => {
<dl className="m-0 border-b border-slate-800 px-5 py-4 font-mono text-[12px] leading-relaxed md:px-7 md:text-[13px]">
<div className="flex gap-4">
<dt className="w-16 shrink-0 text-slate-500">to</dt>
<dd className="m-0 text-slate-200">
<dd className="m-0 select-all text-slate-200">
{address ?? spellOutEmail(CONTACT_EMAIL_PARTS)}
</dd>
</div>
@@ -148,7 +148,7 @@ export const ContactComposer: React.FC = () => {
href={
address ? mailtoHref(CONTACT_EMAIL_PARTS, topic.draft) : "#contact"
}
className="group inline-flex flex-1 items-center justify-between gap-4 rounded-full bg-white px-7 py-4 font-sans text-[12px] font-bold uppercase tracking-[0.18em] text-slate-900 no-underline transition-transform duration-300 hover:-translate-y-0.5"
className="bit-cta bit-cta-light group inline-flex flex-1 items-center justify-between gap-4 rounded-full bg-white px-7 py-4 font-sans text-[12px] font-bold uppercase tracking-[0.18em] text-slate-900 no-underline transition-transform duration-300 hover:-translate-y-0.5"
>
Open in my mail app
<span
+29 -18
View File
@@ -3,6 +3,7 @@
import * as React from "react";
import { motion } from "framer-motion";
import { Button } from "@/src/components/Button";
import { BitAura } from "@/src/components/bits/BitAura";
import { DecodeText } from "@/src/components/bits/DecodeText";
import { PathScene } from "@/src/components/illustrations/PathScenes";
import { SERVICE_PATHS, type ServicePath } from "@/src/content/paths";
@@ -49,25 +50,33 @@ const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({
return (
<div ref={ref} className="grid grid-cols-1 gap-8 md:grid-cols-12 md:gap-12">
<div className="md:order-2 md:col-span-7">
<div className="relative overflow-hidden rounded-3xl bg-slate-900 md:sticky md:top-28">
<div className="flex items-center justify-between border-b border-slate-800 px-5 py-4 md:px-7">
<DecodeText
key={current?.title}
text={current?.title ?? ""}
className="font-mono text-[11px] uppercase tracking-[0.2em] text-slate-300"
/>
<span className="font-mono text-[11px] tracking-[0.2em] text-slate-500">
{step + 1} / {path.steps.length}
</span>
<BitAura source="top" tone="light" className="md:sticky md:top-28">
<div
data-glow
className="relative overflow-hidden rounded-3xl bg-slate-900"
>
<span className="bit-glow bit-glow-dots" aria-hidden="true" />
<div className="relative flex items-center justify-between border-b border-slate-800 px-5 py-4 md:px-7">
<DecodeText
key={current?.title}
text={current?.title ?? ""}
className="font-mono text-[11px] uppercase tracking-[0.2em] text-slate-300"
/>
<span className="font-mono text-[11px] tracking-[0.2em] text-slate-500">
{step + 1} / {path.steps.length}
</span>
</div>
<div className="relative px-2 py-4 md:px-10 md:py-8">
<PathScene
pathId={path.id}
step={step}
title={
current ? `${current.title}. ${current.text}` : path.need
}
/>
</div>
</div>
<div className="px-2 py-4 md:px-10 md:py-8">
<PathScene
pathId={path.id}
step={step}
title={current ? `${current.title}. ${current.text}` : path.need}
/>
</div>
</div>
</BitAura>
</div>
<ol className="m-0 list-none p-0 md:order-1 md:col-span-5">
@@ -154,12 +163,14 @@ export const PathPicker: React.FC = () => {
aria-selected={selected}
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 ${
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"
}`}
>
<span className="bit-glow bit-glow-edge" aria-hidden="true" />
<span className="font-mono text-[11px] tracking-[0.2em] text-slate-400">
0{index + 1}
</span>
@@ -1,5 +1,6 @@
"use client";
import { BitSeam } from "@/src/components/bits/BitSeam";
import * as React from "react";
import Image from "next/image";
import { motion } from "framer-motion";
@@ -32,12 +33,14 @@ export const PortraitSection: React.FC<{
>
<Image
src="/marc-mintel.png"
alt="Portrait of Marc Mintel"
alt="Marc Mintel, freelance web developer"
fill
sizes="(min-width: 768px) 62vw, 100vw"
className="object-cover object-[50%_18%] grayscale"
priority={false}
/>
{/* Slow light behind the fades, so the dark surface is not flat. */}
<div className="bit-aurora" aria-hidden="true" />
{/* Fades: into the surface on the left, and at top and bottom. */}
<div className="absolute inset-0 hidden bg-gradient-to-r from-slate-900 via-slate-900/75 to-slate-900/10 md:block" />
<div className="absolute inset-x-0 bottom-0 h-2/5 bg-gradient-to-t from-slate-900 to-transparent" />
@@ -72,5 +75,6 @@ export const PortraitSection: React.FC<{
<div className="mt-10 md:mt-14">{children}</div>
</motion.div>
</div>
<BitSeam density={1} />
</section>
);
+1 -1
View File
@@ -37,7 +37,7 @@ export const StickyBook: React.FC = () => {
<a
href="#contact"
tabIndex={visible ? 0 : -1}
className="flex items-center gap-3 rounded-full bg-slate-900 px-7 py-4 font-sans text-[11px] font-bold uppercase tracking-[0.2em] text-white no-underline shadow-2xl shadow-slate-900/20 transition-transform duration-300 hover:-translate-y-0.5"
className="bit-cta flex items-center gap-3 rounded-full bg-slate-900 px-7 py-4 font-sans text-[11px] font-bold uppercase tracking-[0.2em] text-white no-underline shadow-2xl shadow-slate-900/20 transition-transform duration-300 hover:-translate-y-0.5"
>
Get in touch <span aria-hidden="true">→</span>
</a>