From dc4a01cbe4efece03a187b331645a6389363f7db Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 4 Oct 2026 00:14:22 +0200 Subject: [PATCH] feat: binary micro-interactions across the page Section headlines are written out behind a column of digits, mono labels resolve from binary, the portrait assembles itself from bits, section indexes rattle and keep one bit alive, cards get corner marks, the header shows the scroll position as an eight-bit number, pressed links and buttons shed a few digits, and the work stage leans towards the pointer. All effects respect reduced motion and leave the content readable without JavaScript. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 10 +++ apps/web/app/(site)/globals.css | 84 +++++++++++++++++ apps/web/app/(site)/layout.tsx | 2 + apps/web/app/(site)/page.tsx | 3 +- apps/web/src/components/Footer.tsx | 10 +-- apps/web/src/components/Header.tsx | 6 +- apps/web/src/components/bits/BinaryTicker.tsx | 63 +++++++++++++ apps/web/src/components/bits/BitBurst.tsx | 44 +++++++++ apps/web/src/components/bits/BitReveal.tsx | 89 +++++++++++++++++++ apps/web/src/components/bits/BitWipe.tsx | 66 ++++++++++++++ apps/web/src/components/bits/DecodeText.tsx | 63 +++++++++++++ apps/web/src/components/bits/ScrollBits.tsx | 29 ++++++ apps/web/src/components/bits/useInViewOnce.ts | 31 +++++++ apps/web/src/components/home/Block.tsx | 23 ++--- apps/web/src/components/home/PathPicker.tsx | 11 ++- .../src/components/home/PortraitSection.tsx | 24 +++-- .../components/illustrations/WorkShowcase.tsx | 31 ++++++- 17 files changed, 556 insertions(+), 33 deletions(-) create mode 100644 apps/web/src/components/bits/BinaryTicker.tsx create mode 100644 apps/web/src/components/bits/BitBurst.tsx create mode 100644 apps/web/src/components/bits/BitReveal.tsx create mode 100644 apps/web/src/components/bits/BitWipe.tsx create mode 100644 apps/web/src/components/bits/DecodeText.tsx create mode 100644 apps/web/src/components/bits/ScrollBits.tsx create mode 100644 apps/web/src/components/bits/useInViewOnce.ts diff --git a/CLAUDE.md b/CLAUDE.md index a531183..3852442 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,6 +36,16 @@ App lives in `apps/web`. Concept and scope: `apps/web/plans/freelance-relaunch.m - The hero design (binary-digit headline) is approved: do not change its design. - Look at the result before reporting: take screenshots (desktop and mobile), do not ship blind. +## Motion (binary micro-interactions) + +- Building blocks live in `apps/web/src/components/bits/`: `DecodeText` (mono labels resolving from digits), + `BitWipe` (headline revealed behind a column of digits), `BitReveal` (picture assembling from bits), + `BinaryTicker`, `ScrollBits`, `BitBurst`. Reuse them instead of inventing new effects. +- `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`). +- Every effect must respect `prefers-reduced-motion` and leave content readable without JavaScript. + ## Architecture rules - Functional core, imperative shell. Pure logic goes in `apps/web/src/domain/` (no I/O, no clock, no diff --git a/apps/web/app/(site)/globals.css b/apps/web/app/(site)/globals.css index 670000e..7bb419f 100644 --- a/apps/web/app/(site)/globals.css +++ b/apps/web/app/(site)/globals.css @@ -1004,3 +1004,87 @@ pre:has(code[class*="language-"]) { opacity: 0.4; } } + +/* ── Binary micro-interactions ─────────────────────────────────────────────── */ + +/* Digits that fly out from a pressed link or button (see BitBurst). */ +.bit-burst { + position: fixed; + z-index: 200; + pointer-events: none; + /* On the layer, not the digits: a fixed layer blends as a whole with the page. */ + mix-blend-mode: difference; +} +.bit-burst span { + position: absolute; + left: 0; + top: 0; + font-family: var(--font-jetbrains-mono), ui-monospace, monospace; + font-size: 13px; + font-weight: 700; + color: #ffffff; + opacity: 0; + animation: bit-burst-fly 620ms cubic-bezier(0.16, 1, 0.3, 1) forwards; +} +@keyframes bit-burst-fly { + 0% { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + 100% { + opacity: 0; + transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) + scale(0.6); + } +} + +/* Corner marks that snap onto a card on hover or focus. */ +.bit-card { + position: relative; +} +.bit-card::before, +.bit-card::after { + content: ""; + position: absolute; + width: 14px; + height: 14px; + border: 1.5px solid currentColor; + opacity: 0; + transition: + opacity 250ms ease, + transform 350ms cubic-bezier(0.16, 1, 0.3, 1); + pointer-events: none; +} +.bit-card::before { + top: 12px; + right: 12px; + border-left: 0; + border-bottom: 0; + transform: translate(-6px, 6px); +} +.bit-card::after { + bottom: 12px; + left: 12px; + border-right: 0; + border-top: 0; + transform: translate(6px, -6px); +} +.bit-card:hover::before, +.bit-card:hover::after, +.bit-card:focus-visible::before, +.bit-card:focus-visible::after, +.bit-card[aria-selected="true"]::before, +.bit-card[aria-selected="true"]::after { + opacity: 0.55; + transform: translate(0, 0); +} + +@media (prefers-reduced-motion: reduce) { + .bit-burst { + display: none; + } + .bit-card::before, + .bit-card::after { + transition: none; + } +} diff --git a/apps/web/app/(site)/layout.tsx b/apps/web/app/(site)/layout.tsx index 8a71f3c..3ff5e85 100644 --- a/apps/web/app/(site)/layout.tsx +++ b/apps/web/app/(site)/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Inter, JetBrains_Mono, Newsreader } from "next/font/google"; import { Analytics } from "@/src/components/Analytics"; +import { BitBurst } from "@/src/components/bits/BitBurst"; import { Footer } from "@/src/components/Footer"; import { Header } from "@/src/components/Header"; import { InteractiveElements } from "@/src/components/InteractiveElements"; @@ -70,6 +71,7 @@ export default function RootLayout({
{children}