* docs: full accuracy overhaul, grounded claim-by-claim in real code Every doc in this repo (CLAUDE.md, README.md, SECURITY.md, docs/*.md) was audited and rewritten by a multi-pass process: independent rewrite per cluster, adversarial re-verification of that rewrite against the actual code, then a final cross-doc consistency pass — each stage re-checking claims against the repo directly rather than trusting the previous stage. Two corrections matter most: 1. docs/ICEBERG_WRITE_BOUNDARY.md, docs/11-lakehouse-write-plan.md, and docs/P0_QUERY_SOURCE_FLOW.md asserted src/services/quality-check-service.ts, quality-report-service.ts, src/data-platform/services/catalog-service.ts, and sql/002_p0_dataset_quality.sql/sql/003_*.sql as real, tested code. git log --all across every branch shows zero commits for any of these paths, ever. 0pt's data-quality/QA enforcement does not exist in code. Corrected everywhere, with a new docs/P0_DATASET_QUALITY_FLOW.md added as an explicit target-spec (not-yet-built) replacement. 2. github.com/sax3l/0pt (this repo) and git.siax.io/sax3l/0pt (Gitea) are two diverged remotes sharing part of the same history (services/api/ workers/adaptive_router.py is byte-identical between them). Gitea's main independently accumulated a WAF-bypass/proxy-rotation scraper stack in three 2026-07-13 commits, since removed via a Gitea PR; GitHub's main never had it. Which remote is canonical/deployed is unresolved — flagged consistently across the corpus, not decided here. Also newly and consistently documented: the tenant_id/project_id-read-from- client-body gap on the P0 lakehouse/rate-policy/source-query routes (no auth/session system exists anywhere in this repo yet); that every CL0UD/ N0D/AUD0/ST0RE platform client is real but mock-only (zero process.env reads for any of their *_URL vars); that two P0 mutation routes (lakehouse/mappings, sources/rate-policies) always fail over HTTP today because their prerequisite entity can never be created via any route; a schema bug in sql/004 (FK references to opt_datasets/opt_sources, which don't exist); and that 0pt.app's live deployment was independently confirmed (curled this pass) to be running this exact codebase with DATABASE_URL/REDIS_URL unset in production. New file: docs/12-edge-capability-api.md — the edge-native P0 TypeScript capability contract (app/api/0pt/*, /api/screen, /api/ai/parse), previously undocumented and distinct from docs/04-api.md's FastAPI-engine-only scope. Verified before commit: tsc --noEmit clean; vitest run 88 passed, 1 todo, 0 failures (13 files) — docs-only change, no source touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs: reflect PR #5's lakehouse-mapping-registry + status-code fixes Follow-up to keep docs/12-edge-capability-api.md accurate once both PRs land: fix/0pt-full-bug-hunt (#5) fixed the shared-instance bug in the lakehouse mapping routes and added proper status-code mapping to the document extraction route. This doc was written against the pre-fix state in the same pass that added it, so it needs the same correction. Does not claim the lakehouse write path fully works now -- the deeper dataset-registration gap (no route ever calls DatasetService.registerDataset) is unchanged and still blocks it end-to-end; only the shared-instance half of the bug is fixed. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
132 lines
17 KiB
Markdown
132 lines
17 KiB
Markdown
# 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 `<img>` 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<T extends HTMLElement = HTMLElement>() {
|
||
const ref = useRef<T>(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. **`<RouteReveal />`** — `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 `<Link>`/`<button>` elements (see `components/header.tsx`, `components/sections/hero-section.tsx`). It's applied consistently in practice, but it's a repeated pattern, not a reusable component — worth extracting if more CTAs are added.
|
||
|
||
## Visual system — no photos (`DataArtifact`)
|
||
|
||
`components/data-artifact.tsx` is the one generative primitive; every "image" surface uses it, animating with the effect it replaces. Confirmed implemented variants (`variant` prop): `"table" | "map" | "schema" | "chart" | "grid"`.
|
||
- `table` (rows cascade in via `.reveal-left`, provenance square per fact cell) — used in `components/sections/gallery-section.tsx` (showcase tile) and `app/katalog/[id]/page.tsx` (`SampleTab`, plus `OverviewTab` for datasets whose `artifact` field is `"table"`). Real fields used: `dim_company`-style rows with company name, source label, provenance level. **Correction:** the previous version of this doc said this variant is used on "hero/cards" — verified false. `components/sections/hero-section.tsx` does not import or render `DataArtifact` at all; it has its own hand-rolled `ROWS`/`PROV` mini-table that visually mimics the same pattern independently (same provenance-square styling, separately maintained). `components/sections/featured-products-section.tsx` (the dataset "cards" grid) doesn't use `DataArtifact` either — it renders `ProvenanceBadge` per list row instead. Flagging the hero duplication as the same kind of repeated-pattern-not-a-shared-component issue already noted below for buttons.
|
||
- `map` (country-code cells in a grid, coverage-% fill bar animates width on view) — coverage. Sample cells: SE/NO/DK/FI (populated) and IS/NL/DE/FR (0%, i.e. not yet sourced).
|
||
- `schema` (mini ER diagram over a graph-paper background; key entities get a red top border) — schema previews. Sample entities: `dim_company`, `fact_financials`, `bridge_provenance`.
|
||
- `chart` (black bars, exactly ONE red bar — the last one — grow on view) — dashboards, labeled "coverage" with a large faint "06" numeral watermark.
|
||
- `grid` (hairline graph-paper + faint oversized "0pt" wordmark + one floating red square using `.animate-float`) — pure texture.
|
||
|
||
Rules, verified in the component: code-only (Tailwind + inline styles + CSS background-image, no images/canvas/SVG assets), color discipline inside visuals (mono + provenance CSS vars `--prov-0..3` + one red `--accent-red`), `grain-overlay` class applied to the outer wrapper, `prefers-reduced-motion` settles to static (handled globally in `app/globals.css`, not per-component).
|
||
|
||
**Gap:** the previous version of this doc pointed to "the coverage doc" for the full mapping of template image elements → replacement + effect kept. That doc (`docs/design/0pt-design-coverage-and-visuals.md`) does not exist (see callout at top of this file), so that mapping is not written down anywhere beyond the five variants and their usage sites listed above.
|
||
|
||
**Optional, not implemented:** Lenis smooth scroll is mentioned as an optional layer on top of this motion system. Verified: `lenis` is not a dependency in `package.json` and is not referenced anywhere in the codebase. If added later, it should not require changing any of the reveal/grain classes above.
|
||
|
||
**Vestigial component, not on the "no photography" path:** `components/fade-image.tsx` (`FadeImage`, wrapping `next/image` with an intersection-triggered fade/scale-in) exists in the repo but is not imported or used anywhere (verified via repo-wide search). It predates the "no photography" rule or was left over from the source template. Harmless dead code; flagging in case someone reaches for it assuming it's wired in.
|
||
|
||
## The signature (carry everywhere)
|
||
|
||
Provenance + confidence is the identity: the provenance square on every fact, one red, mono numerals as texture, section spine, identical motion. A screen that lacks these is off-brand.
|
||
|
||
Confirmed carried through beyond the marketing pages: `components/provenance-badge.tsx` (`ProvenanceBadge`, `CellProvenance`, `PROVENANCE_LEGEND`) implements the provenance square + label pattern for reuse in data-bearing UI (portal, admin), and `components/order-stepper.tsx` / `components/portal-shell.tsx` / `components/states.tsx` (loading/empty/error states) all follow the same mono-hairline-square-radius-0 chrome — the design system is not marketing-page-only, it extends into `/portal` and `/admin`.
|