diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md new file mode 100644 index 0000000..f2bf683 --- /dev/null +++ b/DESIGN_SYSTEM.md @@ -0,0 +1,88 @@ +# EarthPrints Design System + +Single source of truth for visual consistency across marketing pages, the map, and canvas/WebGL code. + +## File map + +| File | Purpose | +|---|---| +| `src/styles/tokens.css` | CSS custom properties (colors, spacing, type, motion, map chrome) | +| `src/styles/primitives.css` | Reusable UI patterns (`.island`, `.ds-kicker`, …) | +| `src/lib/design-system/tokens.ts` | Same values for TypeScript / canvas / deck.gl | +| `src/app/globals.css` | Page-specific layout; imports tokens + primitives | + +## Colors + +Black / white surfaces with a teal accent: + +- **Light mode accent:** `#006C66` (`--accent-solid`, `--accent`) +- **Dark mode accent:** `#52D4C8` (`--accent` on dark backgrounds) + +Use semantic tokens, not raw hex, in new CSS: + +| Token | Use for | +|---|---| +| `--text` | Primary copy | +| `--text-secondary` | Body subtitles, hints | +| `--text-muted` | Large labels only (18px+) | +| `--text-dim` | Decorative only — never body text | +| `--accent` | Links, active states, data highlights | +| `--surface` / `--surface-2` | Nested backgrounds, hovers | +| `--elevated-bg` + `--elevated-border` | Floating panels and chips | + +## Spacing & radius + +```css +--space-1 … --space-6 /* 4px → 24px */ +--space-page-x /* horizontal inset for map chrome */ +--radius-sm | md | lg | pill +``` + +## Typography primitives + +| Class | Use for | +|---|---| +| `.ds-brand-word` | “EarthPrints” wordmark (nav, map chip, footer) | +| `.ds-title` | Panel headings (“Pixel location”) | +| `.ds-kicker` | Uppercase dataset / status pills (`FLUXCOM-X NEE`) | +| `.ds-label` | Form section labels (`History window`) | +| `.ds-hint` | Secondary explanatory copy | + +Pair layout-specific classes when needed: `className="ds-title map-readout-title"`. + +## Surfaces + +| Class | Use for | +|---|---| +| `.island` / `.map-island` | Glass panels on the map (readout, menu) | +| `.elevated-chip` / `.map-nav-chip` | Top chrome pills (brand, menu trigger) | +| `.ds-nav-link` | Vertical nav items in map menu | + +## Map layout tokens + +```css +--map-chrome-top / --map-chrome-height /* top row reserved for brand + menu */ +--map-panel-top /* panels start below chrome */ +``` + +Do not place content over the top-left brand chip. + +## TypeScript usage + +```ts +import { accentRgb, primitives } from "@/lib/design-system/tokens"; + +// deck.gl layer color +getLineColor: [...accentRgb.onDark, 255] + +// React class names + +``` + +Legacy imports from `@/lib/constants/theme` still work — they re-export from the design system. + +## Adding new UI + +1. Check `primitives.css` for an existing pattern before writing one-off styles. +2. Use tokens for spacing, radius, and color — avoid magic numbers in `globals.css`. +3. If a pattern appears twice, add a `.ds-*` primitive and document it in this file. diff --git a/package-lock.json b/package-lock.json index 6adff83..8e33d14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "react": "19.2.7", "react-dom": "19.2.7", "react-map-gl": "^8.1.1", + "recharts": "^3.9.0", "zarrita": "^0.7.3" }, "devDependencies": { @@ -2405,6 +2406,42 @@ "integrity": "sha512-4VpAyMHOqydSvPlEyHwXaE+AkIdR03nX+Qhlxsk2D/IW4OVmDZgIsvJB1cDzyEEtcfKcnaEbfXeiPgejBceT6g==", "license": "MIT" }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", @@ -2717,7 +2754,12 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, "node_modules/@swc/helpers": { @@ -3296,8 +3338,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/d3-fetch": { "version": "3.0.7", @@ -3345,7 +3386,6 @@ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "license": "MIT", - "peer": true, "dependencies": { "@types/d3-color": "*" } @@ -3354,8 +3394,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/d3-polygon": { "version": "3.0.2", @@ -3439,7 +3478,6 @@ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "license": "MIT", - "peer": true, "dependencies": { "@types/d3-path": "*" } @@ -3448,8 +3486,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/d3-time-format": { "version": "4.0.3", @@ -3462,8 +3499,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/d3-transition": { "version": "3.0.9", @@ -3559,7 +3595,7 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -3596,6 +3632,12 @@ "license": "MIT", "peer": true }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", @@ -5220,6 +5262,15 @@ "node": ">=0.8" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", @@ -5628,7 +5679,6 @@ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=12" } @@ -5716,7 +5766,6 @@ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -5841,7 +5890,6 @@ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "license": "ISC", - "peer": true, "dependencies": { "d3-path": "^3.1.0" }, @@ -5878,7 +5926,6 @@ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -6078,6 +6125,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deck.gl": { "version": "9.3.3", "resolved": "https://registry.npmjs.org/deck.gl/-/deck.gl-9.3.3.tgz", @@ -6472,7 +6525,6 @@ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz", "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==", "license": "MIT", - "peer": true, "workspaces": [ "docs", "benchmarks" @@ -6924,6 +6976,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -7568,6 +7626,16 @@ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -9624,7 +9692,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/react-map-gl": { @@ -9651,6 +9718,29 @@ } } }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -9672,6 +9762,51 @@ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, + "node_modules/recharts": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.9.0.tgz", + "integrity": "sha512-dCEcE9y20c8H2tkVeByrAXhhnBJk6/QLbxKmn+dJUptOfc5NMjwRh1jo0vZPRLD+5dMrHrP+hPEsfbGBMfnf5Q==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reference-spec-reader": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/reference-spec-reader/-/reference-spec-reader-0.2.0.tgz", @@ -9721,6 +9856,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, "node_modules/resolve": { "version": "2.0.0-next.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", @@ -10633,6 +10774,12 @@ "license": "MIT", "peer": true }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -11093,12 +11240,52 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/victory-vendor/node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, "node_modules/vite": { "version": "8.0.16", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", diff --git a/package.json b/package.json index ec79f0b..31adf17 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "react": "19.2.7", "react-dom": "19.2.7", "react-map-gl": "^8.1.1", + "recharts": "^3.9.0", "zarrita": "^0.7.3" }, "devDependencies": { diff --git a/src/app/globals.css b/src/app/globals.css index f0a0725..ec4c858 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,59 +1,9 @@ +@import "../styles/tokens.css"; +@import "../styles/primitives.css"; + /* ============================================================ - EarthPrints design system - black / white / teal accent (#006C66 light, #52D4C8 dark) + Global base ============================================================ */ -:root { - --bg: #0a0a0a; - --bg-deep: #000000; - --surface: #161616; - --surface-2: #1f1f1f; - --nav-bg: #121212; - --text: #ffffff; - --text-secondary: rgba(255, 255, 255, 0.80); /* 5.7:1 — AA body text, subtitles */ - --text-muted: rgba(255, 255, 255, 0.56); /* 3.8:1 — AA large text only (18px+) */ - --text-dim: rgba(255, 255, 255, 0.34); /* 2.3:1 — decorative only, never body */ - --border: rgba(255, 255, 255, 0.1); - --border-strong: rgba(255, 255, 255, 0.18); - /** Brand dark teal — icons on white chips, light-mode accent. */ - --accent-solid: #006c66; - --accent: #52d4c8; - --accent-bright: #7df5e8;; - --accent-on-dark: #52d4c8; - --scrim: rgba(10, 10, 10, 0.92); - --scrim-soft: rgba(10, 10, 10, 0.55); - --grid-line: rgba(255, 255, 255, 0.05); - --shadow: 0 18px 50px -24px rgba(0, 0, 0, 0.7); - --elevated-bg: rgba(36, 36, 36, 0.94); - --elevated-border: rgba(255, 255, 255, 0.24); - --elevated-shadow: - 0 10px 28px -14px rgba(0, 0, 0, 0.5), - 0 0 0 1px rgba(255, 255, 255, 0.08) inset; -} -:root.light { - --bg: #ffffff; - --bg-deep: #f4f4f2; - --surface: #ffffff; - --surface-2: #f6f6f4; - --nav-bg: #ffffff; - --text: #0a0a0a; - --text-secondary: rgba(10, 10, 10, 0.72); /* 5.0:1 — AA body text, subtitles */ - --text-muted: rgba(10, 10, 10, 0.56); /* 3.8:1 — AA large text only (18px+) */ - --text-dim: rgba(10, 10, 10, 0.36); /* 2.4:1 — decorative only, never body */ - --border: rgba(10, 10, 10, 0.1); - --border-strong: rgba(10, 10, 10, 0.16); - --accent-solid: #006c66; - --accent: #006c66; - --accent-bright: #00837b; - --accent-on-dark: #006c66; - --scrim: rgba(255, 255, 255, 0.92); - --scrim-soft: rgba(255, 255, 255, 0.55); - --grid-line: rgba(10, 10, 10, 0.05); - --shadow: 0 18px 50px -28px rgba(0, 0, 0, 0.22); - --elevated-bg: color-mix(in srgb, var(--surface) 92%, var(--bg-deep)); - --elevated-border: var(--border-strong); - --elevated-shadow: 0 8px 20px -16px rgba(0, 0, 0, 0.1); -} - * { box-sizing: border-box; margin: 0; @@ -68,7 +18,7 @@ body { body { font-family: var(--font-geist-sans), "Geist", "Geist Fallback", -apple-system, system-ui, sans-serif; - background: var(--bg); + background: var(--page-bg); color: var(--text); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; @@ -155,16 +105,6 @@ button { -webkit-mask-position: center; } -.brand .word { - font-weight: 600; - font-size: 17px; - letter-spacing: -0.02em; -} - -.brand .word b { - font-weight: 600; -} - .nav-links { position: absolute; left: 50%; @@ -422,7 +362,7 @@ button { height: 100svh; width: 100%; overflow: hidden; - background: var(--bg-deep); + background: var(--page-bg); } #heroCanvas { @@ -439,7 +379,7 @@ button { pointer-events: none; background: radial-gradient( - 70% 55% at 22% 78%, + 70% 55% at 40% 52%, color-mix(in srgb, var(--accent) 26%, transparent), transparent 60% ), @@ -457,15 +397,6 @@ button { opacity: 0.5; } -.hero-scrim { - position: absolute; - inset: 0; - pointer-events: none; - background: - linear-gradient(to top, var(--scrim) 0%, var(--scrim-soft) 24%, transparent 52%), - linear-gradient(to bottom, var(--scrim-soft) 0%, transparent 16%); -} - .hero-grain { position: absolute; inset: 0; @@ -486,6 +417,22 @@ button { max-width: 1200px; } +.hero-content::before { + content: ""; + position: absolute; + left: 34%; + top: 50%; + transform: translate(-50%, -50%); + width: min(960px, calc(100% - 24px)); + height: calc(100% + 160px); + z-index: -1; + pointer-events: none; + border-radius: 50%; + background: color-mix(in srgb, var(--page-bg) 76%, transparent); + filter: blur(68px); + opacity: 0.97; +} + .meta { display: flex; align-items: center; @@ -530,6 +477,19 @@ button { text-transform: uppercase; } +.hero-content .meta .sep { + color: var(--text-muted); +} + +.hero-content .meta .mono { + color: var(--text-secondary); +} + +.hero-content .meta .chip { + background: color-mix(in srgb, var(--page-bg) 70%, transparent); + border-color: color-mix(in srgb, var(--text) 22%, transparent); +} + .hero h1 { font-size: clamp(42px, 7.6vw, 96px); line-height: 0.98; @@ -545,23 +505,22 @@ button { color: var(--bg-deep) !important; } -:root:not(.light) body:has(.map-shell) .nav-inner { - box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08) inset; +:root:not(.light) body:has(.map-shell) .map-nav-chip { + box-shadow: + var(--elevated-shadow), + 0 0 0 1px rgba(255, 255, 255, 0.08) inset; } .hero h1 .accent { color: var(--accent-bright); text-shadow: - 0 0 40px color-mix(in srgb, var(--bg-deep) 80%, transparent), - 0 0 80px color-mix(in srgb, var(--bg-deep) 80%, transparent); + 0 0 40px color-mix(in srgb, var(--page-bg) 80%, transparent), + 0 0 80px color-mix(in srgb, var(--page-bg) 80%, transparent); } .hero p.sub { - background: color-mix(in srgb, var(--bg-deep) 28%, transparent); - backdrop-filter: blur(2px); - -webkit-backdrop-filter: blur(2px); border-radius: 24px; - padding: 12px 16px; + padding: 12px 0; font-size: clamp(16px, 1.5vw, 19px); line-height: 1.55; color: var(--text-secondary); @@ -602,6 +561,17 @@ button { background: var(--surface-2); } +:root:not(.light) .hero-cta .btn-outline { + background: #ffffff; + color: #0a0a0a; + border-color: #ffffff; +} + +:root:not(.light) .hero-cta .btn-outline:hover { + background: rgba(255, 255, 255, 0.92); + border-color: rgba(255, 255, 255, 0.92); +} + .btn-primary.lg { height: 48px; padding: 0 7px 0 24px; @@ -616,21 +586,36 @@ button { .readout { position: absolute; right: clamp(20px, 5vw, 56px); - bottom: clamp(34px, 7vh, 64px); + bottom: clamp(72px, 11vh, 108px); z-index: 10; text-align: right; font-family: var(--font-geist-mono), "Geist Mono", monospace; font-size: 11.5px; - line-height: 1.9; - color: var(--text-dim); + line-height: 1.55; + color: var(--text-secondary); letter-spacing: 0.03em; opacity: 0; animation: rise 1s ease 1s forwards; } +.readout::before { + content: ""; + position: absolute; + right: 0; + bottom: -6%; + width: min(240px, 42vw); + height: min(140px, 18vh); + z-index: -1; + pointer-events: none; + border-radius: 50%; + background: color-mix(in srgb, var(--page-bg) 58%, transparent); + filter: blur(40px); + opacity: 0.8; +} + .readout b { - color: var(--text-muted); - font-weight: 400; + color: var(--text); + font-weight: 500; } .readout .pip { @@ -668,10 +653,81 @@ button { /* ============================================================ MAP ============================================================ */ + +.map-nav { + position: fixed; + top: var(--map-chrome-top); + left: var(--space-page-x); + right: var(--space-page-x); + z-index: 80; + display: flex; + align-items: center; + justify-content: space-between; + pointer-events: none; +} + +.map-nav-chip { + pointer-events: auto; + display: grid; + place-items: center; + width: var(--map-chrome-height); + height: var(--map-chrome-height); +} + +.map-nav-brand { + display: inline-flex; + align-items: center; + gap: 10px; + width: auto; + padding: 0 14px 0 8px; + border-radius: var(--radius-pill); +} + +.map-nav-actions { + pointer-events: auto; + display: flex; + align-items: center; + gap: var(--space-2); +} + +.map-nav-chip.icon-btn, +.map-nav-theme.icon-btn { + width: var(--map-chrome-height); + height: var(--map-chrome-height); + border-radius: 50%; + color: var(--text-secondary); +} + +.map-nav-menu { + width: max-content; + min-width: 9.5rem; + max-width: min(200px, calc(100vw - 32px)); + padding-left: var(--space-3); + padding-right: var(--space-3); +} + +.map-nav-menu-links { + display: flex; + flex-direction: column; + gap: 2px; +} + .map-shell { position: relative; height: 100svh; - background: var(--bg-deep); + max-height: 100svh; + overflow: hidden; + background: var(--map-bg); +} + +html:has(.map-shell), +body:has(.map-shell) { + overflow: hidden; + height: 100%; +} + +body:has(.map-shell) .footer { + display: none; } .map-stage { @@ -689,63 +745,20 @@ button { display: grid; place-items: center; color: var(--text-muted); - background: var(--bg-deep); + background: var(--map-bg); } .map-readout { - position: absolute; - top: 92px; - left: clamp(16px, 3vw, 28px); - z-index: 2; - width: min(360px, calc(100vw - 32px)); - padding: 18px 18px 16px; - --map-readout-border: var(--elevated-border); - border: 1px solid var(--map-readout-border); - border-radius: 18px; - color: var(--text); - background: var(--elevated-bg); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - box-shadow: var(--elevated-shadow); + width: min(420px, calc(100vw - 32px)); + --map-readout-border: var(--map-island-border, var(--elevated-border)); } .map-readout-heading { display: flex; align-items: center; justify-content: space-between; - gap: 12px; - margin-bottom: 8px; -} - -.map-readout-kicker { - display: inline-flex; - align-items: center; - width: fit-content; - flex-shrink: 0; - font-size: 11px; - font-weight: 600; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--accent); - padding: 5px 10px; - border-radius: 100px; - border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent); - background: color-mix(in srgb, var(--accent) 14%, transparent); -} - -.map-readout-title { - font-size: 20px; - font-weight: 600; - letter-spacing: -0.03em; - margin-bottom: 0; - color: var(--text); -} - -.map-readout-hint, -.map-readout-empty { - color: var(--text-secondary); - font-size: 13px; - line-height: 1.55; + gap: var(--space-3); + margin-bottom: var(--space-2); } .map-readout-control { @@ -760,15 +773,7 @@ button { display: flex; align-items: baseline; justify-content: space-between; - gap: 12px; -} - -.map-readout-control-label { - font-size: 12px; - letter-spacing: 0.06em; - text-transform: uppercase; - color: var(--text-secondary); - font-weight: 500; + gap: var(--space-3); } .map-readout-control-value { @@ -822,13 +827,62 @@ button { border-top: 1px solid var(--map-readout-border); } +.map-readout-series-section { + margin-top: var(--space-4); + padding-top: var(--space-4); + border-top: 1px solid var(--map-readout-border); +} + +.map-readout-series-label { + margin-bottom: var(--space-3); +} + +.map-readout-series { + display: grid; + gap: var(--space-3); + min-width: 0; +} + +.map-readout-series-status { + margin: 0; +} + +.map-time-series-plot { + width: 100%; + min-width: 0; + margin-inline: calc(var(--space-1) * -1); + padding-inline: var(--space-1); +} + +.map-time-series-caption { + margin-top: var(--space-2); + font-size: var(--font-size-xs); + color: var(--text-secondary); + line-height: 1.5; +} + +.map-time-series-loading { + position: relative; +} + +.map-time-series-loading-label { + position: absolute; + inset: 12px 8px 28px 48px; + display: grid; + place-items: center; + margin: 0; + pointer-events: none; + font-size: var(--font-size-meta); + color: var(--text-secondary); +} + /* ============================================================ FOOTER ============================================================ */ .footer { border-top: 1px solid var(--border); padding: 28px clamp(22px, 6vw, 84px) 34px; - background: color-mix(in srgb, var(--surface) 35%, transparent); + background: var(--page-bg); } .footer-inner { @@ -911,7 +965,7 @@ button { min-height: 100svh; padding: clamp(110px, 14vh, 140px) clamp(22px, 6vw, 84px) clamp(48px, 8vh, 80px); - background: var(--bg-deep); + background: var(--page-bg); } .about-inner { @@ -1025,7 +1079,7 @@ button { line-height: 1.65; letter-spacing: 0.03em; text-transform: uppercase; - color: var(--text-dim); + color: var(--text); margin-bottom: 14px; } @@ -1073,6 +1127,11 @@ button { padding: 0 clamp(16px, 5vw, 32px) clamp(32px, 6vh, 56px); } + .hero-content::before { + left: 50%; + width: min(960px, calc(100% + 48px)); + } + .hero p.sub { margin-left: 0; max-width: 100%; diff --git a/src/components/hero/HeroCanvas.tsx b/src/components/hero/HeroCanvas.tsx index 019c6c2..02d6e5a 100644 --- a/src/components/hero/HeroCanvas.tsx +++ b/src/components/hero/HeroCanvas.tsx @@ -12,18 +12,36 @@ export function HeroCanvas() { const canvas = canvasRef.current; if (!canvas) return; + const reduceMotion = window.matchMedia( + "(prefers-reduced-motion: reduce)", + ).matches; let raf = 0; - - const draw = () => { - drawHeroField(canvas, isLight); + let start = performance.now(); + + const drawFrame = (now: number) => { + const time = reduceMotion ? 0 : (now - start) / 1000; + drawHeroField(canvas, isLight, time); + if (!reduceMotion) { + raf = requestAnimationFrame(drawFrame); + } }; const onResize = () => { cancelAnimationFrame(raf); - raf = requestAnimationFrame(draw); + start = performance.now(); + if (reduceMotion) { + drawHeroField(canvas, isLight, 0); + } else { + raf = requestAnimationFrame(drawFrame); + } }; - draw(); + if (reduceMotion) { + drawHeroField(canvas, isLight, 0); + } else { + raf = requestAnimationFrame(drawFrame); + } + window.addEventListener("resize", onResize); return () => { diff --git a/src/components/hero/HeroOverlays.tsx b/src/components/hero/HeroOverlays.tsx index e041114..00c02a5 100644 --- a/src/components/hero/HeroOverlays.tsx +++ b/src/components/hero/HeroOverlays.tsx @@ -3,7 +3,6 @@ export function HeroOverlays() { <>
- > ); } diff --git a/src/components/layout/Nav.tsx b/src/components/layout/Nav.tsx index e2e665e..4dd814f 100644 --- a/src/components/layout/Nav.tsx +++ b/src/components/layout/Nav.tsx @@ -1,13 +1,14 @@ "use client"; +import { usePathname } from "next/navigation"; import { useId, useState } from "react"; import { Brand } from "@/components/nav/Brand"; -import { MobileMenuButton } from "@/components/nav/MobileMenuButton"; +import { MapNav } from "@/components/nav/MapNav"; import { MobileMenuDropdown } from "@/components/nav/MobileMenuDropdown"; import { NavActions } from "@/components/nav/NavActions"; import { NavLinks } from "@/components/nav/NavLinks"; -export function Nav() { +function MarketingNav() { const [menuOpen, setMenuOpen] = useState(false); const menuId = useId(); @@ -30,3 +31,14 @@ export function Nav() { ); } + +export function Nav() { + const pathname = usePathname(); + const onMapPage = pathname === "/map" || pathname.startsWith("/map/"); + + if (onMapPage) { + return