Files
0pt/docs/05-design-system.md
adminandClaude Sonnet 5 2902a26bac docs: full accuracy overhaul, grounded claim-by-claim in real code (#4)
* 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>
2026-07-22 18:49:01 +02:00

17 KiB
Raw Permalink Blame History

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):

@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.
    "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 IntersectionObservers 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.