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}