# 05 · Design system (mono) Self-contained spec. This file is the source of truth for tokens + motion; the tokens and CSS below are **already implemented** in `app/globals.css` and consumed across `components/` and `app/` — this is not aspirational, it's a description of what ships today, kept here so new screens stay on-brand. Derived from the **mono / EVASION** template (itself v0-generated). Light, editorial, monochrome, sharp, with a single red accent and animated grain + scroll-reveal motion. **No photography** — imagery is rendered data. (Verified: no `` or `next/image` usage anywhere in `app/` or `components/` except a single unused leftover component, see "Vestigial component, not on the 'no photography' path" further down.) > **Doc-index gap:** `CLAUDE.md`'s documentation index lists two files this doc used to point to — `docs/design/0pt-v0-build-brief.md` (per-component v0 prompts) and `docs/design/0pt-design-coverage-and-visuals.md` (full page/component inventory + visual rules). **Neither file exists anywhere in this repo** (confirmed via full-repo search). They are referenced but were never written. This doc no longer treats them as available references — anything they were supposed to cover (per-component prompts, the full visual-mapping table) is not documented anywhere right now. Whoever owns design docs next should either write them or remove the rows from `CLAUDE.md`'s index; not resolved in this pass. ## Tokens Verified against `app/globals.css` (the file actually imported by `app/layout.tsx`): ``` THEME (light, default): --background #ffffff --foreground #0a0a0a --muted-foreground #737373 --border #e5e5e5 --card #ffffff --input #f5f5f5 THEME (dark sections — footer, editorial/manifesto block, technology/engine block): --background #0a0a0a --foreground #fafafa --muted-foreground #a3a3a3 --border #262626 ACCENT (single, sparing): red --accent-red #e40014 (one CTA / one number / one marker per screen) PROVENANCE squares (ONLY non-mono color besides the red CTA): level 0 #737373 (unavailable/licensed) · 1 #143aff (single source) · 2 #fcbb00 (partial) · 3 #00d294 (corroborated) RADIUS: 0 everywhere (`--radius: 0rem` in app/globals.css; shadcn/ui components inherit --radius-sm/md/lg/xl derived from it, so they render square too). TYPE: Inter (everything; headlines weight 500–600, letter-spacing -0.02em, large) · JetBrains Mono (eyebrows, labels, numbers, codes, table headers — UPPERCASE, letter-spacing mostly 0.08–0.22em but ranging down to 0.02em on some labels/table headers (e.g. `provenance-badge.tsx`, the source table headers in `app/kallor/page.tsx`), ~10–12px, #737373). Loaded via `next/font/google` in `app/layout.tsx` as CSS vars `--font-inter` / `--font-jetbrains-mono`. GRID: 8px base; graph-paper hairlines; generous whitespace; left-aligned headlines. SECTION SPINE: real ordered indices 00–06 as mono eyebrows (hero=00, philosophy=01, catalog=02, [03 unused/reserved], collection/coverage=04, testimonials=05, pricing=06). The two dark sections (editorial "Manifesto", technology "The engine") intentionally break the numbered spine — they carry a text eyebrow instead of a number. ``` **Correction — a third section also breaks the spine, and it isn't dark:** verified render order in `app/page.tsx` is Hero(00) → Philosophy(01) → FeaturedProducts/Catalog(02) → **Technology (dark, no number)** → **GallerySection (no number)** → Collection(04) → **Editorial (dark, no number)** → Testimonials(05) → Pricing(06) → Footer(dark). `components/sections/gallery-section.tsx` sits exactly where a "03" would fall but carries a text eyebrow (`"— Visualizations"`) like the dark sections, plus its own unrelated local `01`–`05` indices on its five tiles (a per-tile counter, not a continuation of the page's section spine — don't confuse the two). So "03 unused/reserved" is accurate in that no section ever shows the numeral 03, but three sections break the numbered spine, not two — only two of those three happen to be dark-themed. **Correction vs. previous version of this doc:** the dark theme was previously described as appearing on "footer, one editorial block" (2 places). Verified in `components/sections/`: **three** sections actually use the dark theme — `footer-section.tsx`, `editorial-section.tsx` (the "Manifesto" block), and `technology-section.tsx` (the "#motorn" / "The engine" block, `id="motorn"`). All three set `className="dark ..." ` and a `bg-[#0a0a0a] text-[#fafafa]` pairing. **Implementation note (not yet reconciled in code):** `editorial-section.tsx` and `technology-section.tsx` use the documented dark border `#262626` consistently. `footer-section.tsx` instead hardcodes `#1f1f1f` for its borders — a different, undocumented shade. All three sections hardcode hex literals (`#0a0a0a`, `#fafafa`, `#737373`, `#a3a3a3`, plus the two different border shades) rather than referencing the `.dark` CSS-variable overrides that already exist in `app/globals.css`'s `--border`/`--foreground`/`--muted-foreground` tokens. This works today because the values happen to match (except the footer border), but it means the dark sections don't actually pick up `.dark`-scoped custom-property overrides — they're hand-pinned. Flagging as a small cleanup opportunity, not fixing here (out of scope for a docs pass). ## globals.css (already live in `app/globals.css` — do not re-add elsewhere) The block below is the actual motion + grain CSS, condensed for reference. It ships today in `app/globals.css` (with the keyframes/classes spelled out at full length rather than this compact form — functionally identical): ```css @keyframes reveal-up{0%{opacity:0;transform:translateY(60px)}to{opacity:1;transform:translateY(0)}} @keyframes reveal-left{0%{opacity:0;transform:translateX(-60px)}to{opacity:1;transform:translateX(0)}} @keyframes reveal-right{0%{opacity:0;transform:translateX(60px)}to{opacity:1;transform:translateX(0)}} @keyframes scale-in{0%{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}} @keyframes fadeIn{0%{opacity:0;transform:scale(1.02)}to{opacity:1;transform:scale(1)}} @keyframes slideUp{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}} @keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-20px)}} @keyframes grain{0%,100%{transform:translate(0,0)}10%{transform:translate(-5%,-10%)}20%{transform:translate(-15%,5%)}30%{transform:translate(7%,-25%)}40%{transform:translate(-5%,25%)}50%{transform:translate(-15%,10%)}60%{transform:translate(15%,0)}70%{transform:translateY(15%)}80%{transform:translate(3%,35%)}90%{transform:translate(-10%,10%)}} .reveal{opacity:0} .reveal.in-view{animation:.8s ease-out forwards reveal-up} .reveal-left.in-view{animation:.8s ease-out forwards reveal-left} .reveal-right.in-view{animation:.8s ease-out forwards reveal-right} .reveal-scale.in-view{animation:.6s ease-out forwards scale-in} .reveal-fade.in-view{animation:.7s ease-out forwards fadeIn} .delay-1{animation-delay:.1s}.delay-2{animation-delay:.2s}.delay-3{animation-delay:.3s}.delay-4{animation-delay:.4s}.delay-5{animation-delay:.5s}.delay-6{animation-delay:.6s} .animate-float{animation:6s ease-in-out infinite float} .grain-overlay::before{content:"";position:absolute;inset:-100%;opacity:.03;pointer-events:none;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");animation:8s steps(10,end) infinite grain} @media(prefers-reduced-motion:reduce){.reveal,.reveal-left,.reveal-right,.reveal-scale,.reveal-fade{opacity:1;animation:none}.animate-float,.grain-overlay::before{animation:none}} ``` The provenance colors and `--accent-red` also live in `app/globals.css` as CSS custom properties (`--accent-red`, `--prov-0..3`), exposed to Tailwind via `@theme inline` as `--color-accent-red`, `--color-prov-0..3`. **Known leftover, unused file:** `styles/globals.css` also exists in this repo and is **not** imported anywhere (`app/layout.tsx` imports `./globals.css`, i.e. `app/globals.css`, only). `styles/globals.css` is a stray copy of the original shadcn/v0 scaffold — it uses `oklch()` colors, Geist/Geist Mono fonts, and `--radius: 0.625rem`, none of which match this design system. It is dead code. Don't edit it expecting it to affect the app, and consider deleting it in a future cleanup (not done in this pass — out of scope). ## Reveal / motion system Three pieces work together; the doc previously only described one of them: 1. **`useReveal()` hook** — `hooks/use-reveal.ts`. Wire a section's ref with it; descendants carrying `.reveal` (+ optional `.reveal-left/-right/-scale/-fade` and `.delay-N`) animate to `.in-view` via `IntersectionObserver` (`threshold: 0.15`, `rootMargin: "0px 0px -10% 0px"`) when the section scrolls into view. **Difference from a naive IntersectionObserver:** if the section's top edge is already inside the viewport on mount (e.g. direct navigation to a page whose hero is already visible), the hook applies `.in-view` immediately via `requestAnimationFrame` instead of waiting for a scroll event — this avoids a flash of invisible content on first paint. ```tsx "use client"; import { useEffect, useRef } from "react"; export function useReveal() { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const applyReveal = () => { el.querySelectorAll(".reveal, .reveal-left, .reveal-right, .reveal-scale, .reveal-fade") .forEach((n) => n.classList.add("in-view")); }; const rect = el.getBoundingClientRect(); if (rect.top < window.innerHeight * 0.9) { requestAnimationFrame(applyReveal); return; } const io = new IntersectionObserver((entries) => entries.forEach((e) => { if (e.isIntersecting) { applyReveal(); io.unobserve(el); } }), { threshold: 0.15, rootMargin: "0px 0px -10% 0px" }); io.observe(el); return () => io.disconnect(); }, []); return ref; } ``` 2. **``** — `components/route-reveal.tsx`, mounted once, globally, in `app/layout.tsx` inside the providers. On every client-side route change (via `usePathname()`), it force-applies `.in-view` to any `.reveal*` element whose top edge is already within 95% of the viewport height — covering standalone pages (e.g. `/bestall`, `/katalog/[id]`) that render fully visible on load, where per-section `IntersectionObserver`s might not fire before paint. Runs on an immediate pass, a `requestAnimationFrame` pass, and a 600ms-delayed pass (for async-loaded content). **This component was not documented in the previous version of this file** — it's load-bearing for anything not covered by `useReveal()`'s own "already visible" check (e.g. content that becomes visible slightly after mount). 3. **CSS keyframes/classes** in `app/globals.css` (above) define the actual animation. Every section wraps in a tag using `useReveal()`; children get `reveal[/-left/-right/-scale/-fade]` + `delay-N`. This pattern is used consistently across `components/sections/*.tsx`. ## Buttons & labels - Primary: solid `#0a0a0a` (`--foreground`) bg, `--background` text, square, mono uppercase ~12px, padding varies by placement (header CTA and hero CTAs use `px-5..7 py-2.5..4`, i.e. roughly 10–16px vertical / 20–28px horizontal — treat the doc's original "14px 26px" as an approximate baseline, not an exact contract); hover → bg `var(--accent-red)`. - Secondary: transparent, 1px `border-foreground`; hover → bg `--foreground`, `--background` text. - Eyebrow: mono uppercase 11px, spacing ~0.14–0.18em, `--muted-foreground`, with a 2-digit index where the section spine applies (e.g. `"01 — Philosophy"`). - Headlines: sentence case, never uppercase. - **Implementation note:** there is no shared `Button`/`MonoButton` component implementing this pattern — `components/ui/button.tsx` is the generic, unmodified shadcn/ui button (rounded via `--radius-*` tokens, which happen to resolve to 0 here, but otherwise a stock `cva` variant set: default/destructive/outline/secondary/ghost/link). The mono square CTA look described above is hand-rolled per call site as inline Tailwind classes on raw ``/`