diff --git a/frontend/apps/artcraft-website-next/DESIGN.md b/frontend/apps/artcraft-website-next/DESIGN.md new file mode 100644 index 0000000000..3aa9cf454d --- /dev/null +++ b/frontend/apps/artcraft-website-next/DESIGN.md @@ -0,0 +1,59 @@ +# Design Ideology + +The working philosophy behind the ArtCraft marketing site +(`artcraft-website-next`). This captures direction and standards, not a +finished visual spec — the aesthetic itself is still under experiment. + +## What we are building + +A product website with a sales/marketing angle. The bar: it should read as +a professionally developed, premium site with a design language that +belongs to ArtCraft — not a template, not AI-slop defaults, and not a +studio/agency showpiece. Creative technology (Three.js, GSAP, smooth +scroll) is welcome and encouraged, but subordinated to selling the +product: used where it deepens the product story, never as decoration for +its own sake, and never overdone. + +## The premium principle + +Premium is felt immediately, and it is built from accumulated small +details — not from one big idea. Small details make a big difference. + +- **One or two elements are never enough.** A single hero gimmick floating + in unoccupied space reads amateur and unfinished — the "blank/empty + negative space" failure. Compositions need layered density: many quiet, + coordinated details rather than one loud one. +- **Negative space must be structured, not merely empty.** Rails, rules, + ticks, annotations, and measurements make emptiness intentional. +- **Everything is choreographed.** Nothing just appears; elements arrive + with easing and stagger. Motion that maps to scroll is scrub-linked and + reversible. +- **Finish every surface.** Raw flat color, unlit 3D, and default + materials read as demos. Grain, lighting, shadow, vignette, and + micro-contrast are the difference between blocked-out and finished. +- **One voice for micro-detail.** Annotations, counters, tick marks, and + labels share a single vocabulary (mono uppercase HUD labels, hairline + rules, crosshair ticks) so density never becomes noise. + +## Current experimental language (subject to change) + +Brutalist/instrument direction. Nothing here is final; treat every item as +a hypothesis under test, replaceable by a better experiment: + +- Two themes, one structure: dark "viewport" (near-black terminal), light + "blueprint" (warm paper). Light-dominant positioning, deliberately apart + from dark competitors. +- Hard edges, zero border radius, hairline rules, ticked 1280px rails. +- Type: Archivo display (stretched), Instrument Serif italic for one + contrast word per headline, Inter body, Geist Mono for HUD micro-labels. +- One blue accent, used sparingly. Color discipline over rainbow. + +## Working practices + +- **Experiments over debates.** Contested aesthetics get built behind the + dev tuner and judged on the page, not argued in the abstract. +- **Every tweakable constant is registered with the tuner** (see + TUNER.md). A missing slider costs a whole build round-trip. +- **Progressive enhancement is non-negotiable.** Reduced motion, coarse + pointers, no-JS crawlers, and WebGL failure all receive a complete, + legible page. diff --git a/frontend/apps/artcraft-website-next/TUNER.md b/frontend/apps/artcraft-website-next/TUNER.md index 4086adbea9..52bed09056 100644 --- a/frontend/apps/artcraft-website-next/TUNER.md +++ b/frontend/apps/artcraft-website-next/TUNER.md @@ -37,11 +37,22 @@ iteration/build round-trip. import { defineTunables } from "@/lib/tuner"; export const glowTuner = defineTunables("glow", "Glow", { - radius: { label: "Radius px", min: 0, max: 400, step: 5, default: 120 }, - strength: { label: "Strength", min: 0, max: 2, step: 0.05, default: 0.8 }, + radius: { + label: "Radius px", min: 0, max: 400, step: 5, default: 120, + info: "Blur radius of the glow behind the hero card.", + }, + strength: { + label: "Strength", min: 0, max: 2, step: 0.05, default: 0.8, + info: "Peak intensity of the glow (0 = off).", + }, }); ``` +**Every tunable must carry an `info` one-liner.** It renders as the row's +`i`-mark hover in the panel — a slider nobody can decipher ("Ghost alpha"?) +costs more than the sentence explaining it. Write what the knob visibly +does, not what the variable is named. + Then read values where they're used: - **Per-frame consumers** (`useFrame`, rAF loops): call `glowTuner.read()` @@ -52,8 +63,9 @@ Then read values where they're used: `hero-wall.tsx`). The panel picks up new groups automatically — no panel changes needed. -Registered groups: Wall layout, Wall motion, Wall look (all for the hero -render wall; see `src/components/landing/hero-tunables.ts`). +Registered groups: Wall layout, Wall motion, Wall look (hero render wall; +`src/components/landing/hero-tunables.ts`) and Ruler layout, Ruler motion, +Ruler look (scroll ruler; `src/components/ruler/ruler-tunables.ts`). ## Shipping tuned values diff --git a/frontend/apps/artcraft-website-next/next.config.ts b/frontend/apps/artcraft-website-next/next.config.ts index f1b0100002..54e7d572cf 100644 --- a/frontend/apps/artcraft-website-next/next.config.ts +++ b/frontend/apps/artcraft-website-next/next.config.ts @@ -2,12 +2,18 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { reactStrictMode: true, - // Same-origin proxy for large media served from the live site. The hero - // rail draws these as WebGL video textures, which need CORS-clean - // sources — getartcraft.com serves its videos without - // Access-Control-Allow-Origin, so we route them through our own origin. + // Same-origin proxies for large media served from other origins. The hero + // wall draws its clips as WebGL video textures, which need CORS-clean + // sources, and neither CDN sends Access-Control-Allow-Origin, so those + // requests route through our own origin. async rewrites() { return [ + // Showcase clips for the hero wall (see cdnMediaUrl in lib/links.ts). + { + source: "/cdn-media/:path*", + destination: "https://frontend-cdn.fakeyou.com/:path*", + }, + // Feature footage and other assets still hosted on the live site. { source: "/ext-media/:path*", destination: "https://getartcraft.com/:path*", diff --git a/frontend/apps/artcraft-website-next/src/app/globals.css b/frontend/apps/artcraft-website-next/src/app/globals.css index 5d2db3e7e5..4b922a0efb 100644 --- a/frontend/apps/artcraft-website-next/src/app/globals.css +++ b/frontend/apps/artcraft-website-next/src/app/globals.css @@ -195,6 +195,47 @@ body { height: 1px; } +/* Scroll ruler. JS stamps data-ruler on when the instrument mounts + (capable device only), so no-JS visitors and small/coarse-pointer devices + keep their native scrollbar untouched. "on" = full instrument (native + scrollbar hidden); "static" = reduced-motion rail (scrollbar stays). + Either way the rail's gutter is reserved on main/footer — the nav stays + full-bleed and simply overlaps the rail's top. */ +html[data-ruler="on"] { + scrollbar-width: none; +} +html[data-ruler="on"]::-webkit-scrollbar { + display: none; + width: 0; + height: 0; +} +html[data-ruler][data-ruler-side="right"] main, +html[data-ruler][data-ruler-side="right"] footer { + padding-right: var(--ruler-rail, 48px); +} +html[data-ruler][data-ruler-side="left"] main, +html[data-ruler][data-ruler-side="left"] footer { + padding-left: var(--ruler-rail, 48px); +} + +/* Rolling odometer for the ruler's percent readout. Digit columns translate + by whole digits; the reduced-motion blanket below disables the roll. */ +.ruler-digits { + display: inline-flex; + height: 1em; + overflow: hidden; + line-height: 1; +} +.ruler-digit-col { + display: flex; + flex-direction: column; + transition: transform 280ms cubic-bezier(0.2, 0.9, 0.2, 1); +} +.ruler-digit-col > span { + display: block; + height: 1em; +} + /* Mono capability marquee. Duplicated track scrolls -50%; the reduced-motion * blanket below freezes it in place. */ .marquee { diff --git a/frontend/apps/artcraft-website-next/src/app/layout.tsx b/frontend/apps/artcraft-website-next/src/app/layout.tsx index a877bc451c..fecfc1e5b0 100644 --- a/frontend/apps/artcraft-website-next/src/app/layout.tsx +++ b/frontend/apps/artcraft-website-next/src/app/layout.tsx @@ -9,6 +9,7 @@ import { import SiteNav from "@/components/site-nav"; import SiteFooter from "@/components/site-footer"; import MotionProvider from "@/components/motion-provider"; +import ScrollRuler from "@/components/ruler/scroll-ruler"; import TunerPanel from "@/components/dev/tuner-panel"; import "./globals.css"; @@ -121,8 +122,9 @@ export default function RootLayout({ /> -
{children}
+
{children}
+
diff --git a/frontend/apps/artcraft-website-next/src/components/dev/tuner-panel.tsx b/frontend/apps/artcraft-website-next/src/components/dev/tuner-panel.tsx index b998d8a6e6..7b4a868a1a 100644 --- a/frontend/apps/artcraft-website-next/src/components/dev/tuner-panel.tsx +++ b/frontend/apps/artcraft-website-next/src/components/dev/tuner-panel.tsx @@ -197,17 +197,37 @@ function Row({ const value = useTunerStore((s) => s.values[storeKey] ?? def.default); const setValue = useTunerStore((s) => s.setValue); const modified = value !== def.default; + const [showInfo, setShowInfo] = useState(false); return (