@@ -305,26 +370,43 @@ export default function StepFileTo2dDrawing() {
Frequently asked
- {FAQS.map((faq) => (
-
-
- {faq.question}
-
- {faq.answer}
-
+ {FAQS.map((faq, index) => (
+
+
+
+
+
+
))}
-
-
+
+
+
+
+ Get a real drawing out of your model
+
+
+
+
>
);
}
diff --git a/src/pages/illustrations.tsx b/src/pages/illustrations.tsx
new file mode 100644
index 0000000..a969afb
--- /dev/null
+++ b/src/pages/illustrations.tsx
@@ -0,0 +1,108 @@
+// Simple thin-line mechanical illustrations for the About page — bolt,
+// calipers, bracket. Deliberately schematic rather than photorealistic;
+// stroke-only in the brand blue to match TriangleMark's visual language.
+
+type IllustrationProps = {
+ size?: number;
+ className?: string;
+};
+
+const SHARED_PROPS = {
+ fill: "none",
+ stroke: "var(--color-blue-500)",
+ strokeWidth: 2,
+ strokeLinecap: "round" as const,
+ strokeLinejoin: "round" as const,
+ "aria-hidden": true,
+ focusable: false,
+};
+
+export function BoltIllustration({ size = 96, className }: IllustrationProps) {
+ return (
+
+ {/* Hex head */}
+
+
+ {/* Shaft */}
+
+
+
+
+ );
+}
+
+export function CalipersIllustration({
+ size = 96,
+ className,
+}: IllustrationProps) {
+ return (
+
+ {/* Beam (scale bar) */}
+
+ {/* Scale ticks */}
+
+ {/* Fixed jaw, foot pointing right */}
+
+ {/* Sliding jaw, foot pointing left to face the fixed jaw */}
+
+ {/* Thumbwheel on the sliding jaw */}
+
+
+
+ );
+}
+
+// Wireframe box — the viewer's true-empty-state placeholder (no file
+// loaded yet). Removed the instant a file loads; not used anywhere else.
+export function EmptyViewportIllustration({
+ size = 96,
+ className,
+}: IllustrationProps) {
+ return (
+
+
+
+
+ );
+}
+
+export function BracketIllustration({
+ size = 96,
+ className,
+}: IllustrationProps) {
+ return (
+
+ {/* Top (L-shaped) face */}
+
+ {/* Thickness / side faces at the outer corner */}
+
+ {/* Mounting holes */}
+
+
+
+ );
+}
diff --git a/src/pages/landingIcons.tsx b/src/pages/landingIcons.tsx
new file mode 100644
index 0000000..8ece18e
--- /dev/null
+++ b/src/pages/landingIcons.tsx
@@ -0,0 +1,98 @@
+// Icon tiles for the Landing page hero/capability section — a rounded
+// swatch with four corner-bracket accents (a static echo of CornerMark's
+// registration-mark motif, used here since these tiles animate in as a
+// unit via Reveal rather than drawing in on their own).
+import type { ReactNode } from "react";
+import styles from "./Landing.module.css";
+
+export function IconTile({
+ size = "sm",
+ children,
+}: {
+ size?: "sm" | "lg";
+ children: ReactNode;
+}) {
+ return (
+
+
+
+
+
+ {children}
+
+ );
+}
+
+const STROKE = {
+ fill: "none",
+ stroke: "var(--color-blue-500)",
+ "aria-hidden": true,
+ focusable: false,
+} as const;
+
+export function ViewFormatsIcon({ size = 34 }: { size?: number }) {
+ return (
+
+
+
+
+ );
+}
+
+export function PdfExportIcon({ size = 28 }: { size?: number }) {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+export function AssemblyIcon({ size = 28 }: { size?: number }) {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export function MeasureIcon({ size = 28 }: { size?: number }) {
+ return (
+
+
+
+
+
+
+ );
+}
+
+export function UploadIcon({ size = 26 }: { size?: number }) {
+ return (
+
+
+
+
+
+ );
+}
+
+export function ConnectorArrowIcon({ size = 18 }: { size?: number }) {
+ return (
+
+
+
+ );
+}
diff --git a/src/styles/design-tokens.css b/src/styles/design-tokens.css
new file mode 100644
index 0000000..26a0b72
--- /dev/null
+++ b/src/styles/design-tokens.css
@@ -0,0 +1,78 @@
+/* Design tokens — single source of truth for color and type across the
+ * site. Every color/font reference sitewide should resolve through these
+ * custom properties rather than a hardcoded literal. Mirrors the
+ * "one canonical file" pattern used by drafting-rules.ts for the drawing
+ * sheet — this is the equivalent file for the brand/UI layer, and is a
+ * completely separate system from that one (see drafting-rules.ts).
+ */
+
+@font-face {
+ font-family: "IBM Plex Sans";
+ src: url("/fonts/ibm-plex-sans-400.woff2") format("woff2");
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "IBM Plex Sans";
+ src: url("/fonts/ibm-plex-sans-500.woff2") format("woff2");
+ font-weight: 500;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "IBM Plex Mono";
+ src: url("/fonts/ibm-plex-mono-400.woff2") format("woff2");
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "IBM Plex Mono";
+ src: url("/fonts/ibm-plex-mono-500.woff2") format("woff2");
+ font-weight: 500;
+ font-style: normal;
+ font-display: swap;
+}
+
+:root {
+ /* Brand blue ramp */
+ --color-blue-50: #eff6ff;
+ --color-blue-100: #dbeafe;
+ --color-blue-500: #3b82f6;
+ --color-blue-600: #2563eb;
+ --color-blue-700: #1d4ed8;
+
+ /* The one "blueprint" accent surface — used sparingly, one per page */
+ --color-navy-900: #0f2557;
+
+ /* Secondary accent — export/download actions only, at most one per page */
+ --color-amber-50: #fef3c7;
+ --color-amber-500: #f59e0b;
+ --color-amber-700: #b45309;
+
+ /* Ink / neutral ramp — consolidates the three near-duplicate grayscales
+ * that previously existed independently in Landing.module.css, the
+ * guide/About pages, and src/ui/App.css. */
+ --color-ink: #0f172a;
+ --color-ink-muted: #475569;
+ --color-ink-faint: #64748b;
+ --color-border: #d9dee5;
+ --color-bg: #f7f8fa;
+ --color-bg-alt: #eef1f5;
+ --color-white: #ffffff;
+
+ /* Destructive/error — not part of the brand ramp, kept separate on
+ * purpose so error states never get confused with brand blue/navy/amber. */
+ --color-error-500: #dc2626;
+ --color-error-50: #fef2f2;
+
+ /* Type */
+ --font-display: "IBM Plex Sans", "Segoe UI", -apple-system, BlinkMacSystemFont,
+ "Helvetica Neue", Arial, sans-serif;
+ --font-mono: "IBM Plex Mono", "SFMono-Regular", Menlo, Monaco, Consolas,
+ "Liberation Mono", "Courier New", monospace;
+}
diff --git a/src/styles/tokenValues.ts b/src/styles/tokenValues.ts
new file mode 100644
index 0000000..4f23259
--- /dev/null
+++ b/src/styles/tokenValues.ts
@@ -0,0 +1,8 @@
+// Plain JS values mirroring design-tokens.css, for the handful of
+// consumers (Three.js renderer clear color, canvas fillStyle, etc.) that
+// can't resolve a CSS custom property. Keep in sync with design-tokens.css.
+
+// Matches --color-bg. Used as the 3D viewport clear color in both the
+// SSG-only ViewerPage and the lazy-loaded App shell, which previously
+// carried two slightly different near-white literals for the same job.
+export const VIEWER_BG_COLOR = "#f7f8fa";
diff --git a/src/ui/App.css b/src/ui/App.css
index 5e7da46..ea4600b 100644
--- a/src/ui/App.css
+++ b/src/ui/App.css
@@ -1,21 +1,14 @@
:root {
- font-family:
- "Inter",
- "Segoe UI",
- -apple-system,
- BlinkMacSystemFont,
- "Helvetica Neue",
- Arial,
- sans-serif;
- --bg-base: #e2e8f0;
- --bg-soft: #f8fafc;
+ font-family: var(--font-display);
+ --bg-base: var(--color-bg-alt);
+ --bg-soft: var(--color-bg);
--bg-panel: rgba(255, 255, 255, 0.92);
--surface-border: rgba(148, 163, 184, 0.35);
- --text-primary: #0f172a;
- --text-secondary: #475569;
- --text-muted: #64748b;
- --brand: #1d4ed8;
- --brand-strong: #1e40af;
+ --text-primary: var(--color-ink);
+ --text-secondary: var(--color-ink-muted);
+ --text-muted: var(--color-ink-faint);
+ --brand: var(--color-blue-700);
+ --brand-strong: var(--color-blue-700);
--brand-soft: rgba(59, 130, 246, 0.14);
--shadow-soft: 0 18px 40px rgba(15, 23, 42, 0.08);
}
@@ -403,8 +396,8 @@ a {
height: 100%;
display: grid;
grid-template-rows: auto auto 1fr;
- background: #f7f8fa;
- color: #0f172a;
+ background: var(--color-bg);
+ color: var(--color-ink);
}
.cad-app__header {
@@ -428,7 +421,7 @@ a {
.cad-app__title-wrap h1 {
margin: 0;
font-size: 20px;
- font-weight: 700;
+ font-weight: 500;
}
.cad-app__actions {
@@ -445,15 +438,15 @@ a {
border-radius: 8px;
padding: 9px 14px;
font-size: 13px;
- font-weight: 600;
- color: #ffffff;
+ font-weight: 500;
+ color: var(--color-white);
border: none;
- background: #2563eb;
+ background: var(--color-blue-600);
cursor: pointer;
}
.cad-app__upload-btn:hover {
- background: #1d4ed8;
+ background: var(--color-blue-700);
}
.cad-app__upload-btn input {
@@ -467,9 +460,9 @@ a {
border-radius: 8px;
border: none;
background: transparent;
- color: #64748b;
+ color: var(--color-ink-faint);
font-size: 13px;
- font-weight: 600;
+ font-weight: 500;
padding: 9px 14px;
cursor: pointer;
}
@@ -477,7 +470,7 @@ a {
.cad-app__actions button:hover:not(:disabled) {
background: rgba(100, 116, 139, 0.08);
border-radius: 8px;
- color: #334155;
+ color: var(--color-ink-muted);
}
.cad-app__actions button:disabled {
@@ -493,12 +486,12 @@ a {
padding: 10px 16px;
border-bottom: 1px solid rgba(148, 163, 184, 0.25);
font-size: 13px;
- color: #334155;
+ color: var(--color-ink-muted);
}
.cad-app__error {
- color: #b91c1c;
- font-weight: 600;
+ color: var(--color-error-500);
+ font-weight: 500;
}
.cad-app__viewer {
@@ -511,6 +504,25 @@ a {
height: 100%;
}
+.cad-app__viewer-empty {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-end;
+ padding-bottom: 12%;
+ gap: 0.75rem;
+ pointer-events: none;
+}
+
+.cad-app__viewer-empty p {
+ margin: 0;
+ color: var(--color-ink-faint);
+ font-family: var(--font-display);
+ font-size: 0.9rem;
+}
+
@media (max-width: 1080px) {
.feature-grid,
.audience-grid {
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index 588961a..62efc26 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -2,7 +2,9 @@ import { useCallback, useEffect, useMemo, useState, startTransition } from "reac
import { Link } from "react-router-dom";
import { consumePendingFile } from "../fileStore";
import { CAD_EXTS, CadViewer, MESH_ASSEMBLY_EXTS } from "../components/cad/cad-viewer";
+import { EmptyViewportIllustration } from "../pages/illustrations";
import TriangleMark from "../pages/TriangleMark";
+import { VIEWER_BG_COLOR } from "../styles/tokenValues";
import "./App.css";
const ACCEPTED_FORMATS = [
@@ -102,7 +104,7 @@ export default function App() {
onDrop={handleViewerDrop}
style={{
colorScheme: "light",
- outline: viewerDragging ? "2px solid #3b82f6" : "none",
+ outline: viewerDragging ? "2px solid var(--color-blue-500)" : "none",
outlineOffset: "-2px",
transition: "outline 0.12s ease",
}}
@@ -141,9 +143,15 @@ export default function App() {
showControls
showViewCube
showHomeButton
- backgroundColor="#f0f2f5"
+ backgroundColor={VIEWER_BG_COLOR}
className="cad-viewer-host"
/>
+ {!file && (
+
+
+
Drop a CAD file to get started
+
+ )}
{viewerDragging && (
@@ -167,10 +175,10 @@ export default function App() {
border: "1px solid rgba(59,130,246,0.4)",
borderRadius: "10px",
padding: "0.7rem 1.6rem",
- color: "#3b82f6",
+ color: "var(--color-blue-500)",
fontSize: "1rem",
- fontWeight: 600,
- fontFamily: "monospace",
+ fontWeight: 500,
+ fontFamily: "var(--font-mono)",
letterSpacing: "0.04em",
}}
>
diff --git a/src/ui/LoadingOverlay.tsx b/src/ui/LoadingOverlay.tsx
index 0519b57..ec59372 100644
--- a/src/ui/LoadingOverlay.tsx
+++ b/src/ui/LoadingOverlay.tsx
@@ -51,7 +51,7 @@ export default function LoadingOverlay({
>
@@ -82,8 +83,8 @@ export default function LoadingOverlay({
-
+
{stage}
{dots}
@@ -112,13 +123,15 @@ export default function LoadingOverlay({
marginBottom: "0.4rem",
}}
>
-
Loading
+
+ Loading
+
{Math.round(progress)}%
@@ -128,7 +141,7 @@ export default function LoadingOverlay({
style={{
width: "100%",
height: "6px",
- background: "#e5e7eb",
+ background: "var(--color-border)",
borderRadius: "999px",
overflow: "hidden",
}}
@@ -137,7 +150,8 @@ export default function LoadingOverlay({
style={{
height: "100%",
width: `${progress}%`,
- background: "linear-gradient(90deg, #3b82f6, #60a5fa)",
+ background:
+ "linear-gradient(90deg, var(--color-blue-500), #60a5fa)",
borderRadius: "999px",
transition: "width 0.35s ease",
}}
@@ -148,7 +162,7 @@ export default function LoadingOverlay({
diff --git a/src/ui/MarketingLayout.tsx b/src/ui/MarketingLayout.tsx
new file mode 100644
index 0000000..ae516e0
--- /dev/null
+++ b/src/ui/MarketingLayout.tsx
@@ -0,0 +1,25 @@
+// Shared layout for the marketing/content routes (Home, About, Guides) —
+// a subtle fade between them on navigation. Deliberately not used for the
+// /viewer route, which is a sibling top-level route outside this layout
+// and stays instant.
+import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
+import { Outlet, useLocation } from "react-router-dom";
+
+export default function MarketingLayout() {
+ const location = useLocation();
+ const prefersReducedMotion = useReducedMotion();
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/ui/ViewerPage.tsx b/src/ui/ViewerPage.tsx
index 943d00b..0d90b25 100644
--- a/src/ui/ViewerPage.tsx
+++ b/src/ui/ViewerPage.tsx
@@ -1,7 +1,9 @@
import React, { useMemo, useState } from "react";
import { Link } from "react-router-dom";
import { CAD_EXTS, CadViewer, MESH_ASSEMBLY_EXTS } from "../components/cad/cad-viewer";
+import { EmptyViewportIllustration } from "../pages/illustrations";
import TriangleMark from "../pages/TriangleMark";
+import { VIEWER_BG_COLOR } from "../styles/tokenValues";
const ACCEPTED_FORMATS = [
".step",
@@ -94,9 +96,15 @@ export default function ViewerPage() {
showControls
showViewCube
showHomeButton
- backgroundColor="#f1f5f9"
+ backgroundColor={VIEWER_BG_COLOR}
className="cad-viewer-host"
/>
+ {!file && (
+
+
+
Drop a CAD file to get started
+
+ )}
);