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}
+