= {
+ green: "var(--theme-risk-green-ring)",
+ yellow: "var(--theme-risk-yellow-ring)",
+ red: "var(--theme-risk-red-ring)",
+};
+const PIZZA_PLATE = "var(--vb-plate)";
+const PIZZA_EMPTY = "var(--vb-empty)";
+const PIZZA_EMPTY_STROKE = "var(--vb-empty-stroke)";
+const PIZZA_INK = "var(--vb-ink)";
const RAD = Math.PI / 180;
@@ -185,8 +197,8 @@ export function Pizza({
)}
{showLabels && (
= 1 ? "butt" : "round",
strokeDasharray:
@@ -257,7 +269,7 @@ export function Pizza({
y={cy - size * 0.028}
className="vbpizza__stagek"
textAnchor="middle"
- style={{ fontSize: size * 0.033 }}
+ style={{ fill: PIZZA_INK, fontSize: size * 0.033 }}
>
STAGE
diff --git a/hooks/useAssessment.ts b/hooks/useAssessment.ts
index 460e9fa..98bc04f 100644
--- a/hooks/useAssessment.ts
+++ b/hooks/useAssessment.ts
@@ -44,6 +44,8 @@ export function useAssessment() {
const start = useCallback(() => setStarted(true), []);
const back = useCallback(() => setStep((s) => Math.max(s - 1, 0)), []);
+ /** Return to the intro screen, keeping answers and step so progress resumes. */
+ const toIntro = useCallback(() => setStarted(false), []);
const goto = useCallback((i: number) => {
setStarted(true);
setStep(Math.max(0, Math.min(i, total)));
@@ -87,6 +89,7 @@ export function useAssessment() {
total,
choose,
back,
+ toIntro,
goto,
showResults,
takeResultsConfetti,
diff --git a/package.json b/package.json
index 696331e..88bb8d3 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"@stitches/react": "1.2.8",
"html-to-image": "1.11.13",
"next": "15.5.15",
+ "next-themes": "^0.4.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-is": "19.2.3"
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 6f6d6e8..7dc44e8 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -1,3 +1,4 @@
+import { AppThemeProvider } from "@components/AppThemeProvider";
import { globalCss, Provider as TooltipProvider } from "@obolnetwork/obol-ui";
import "@styles/colors_and_type.css";
import "@styles/obol-bridge.css";
@@ -48,8 +49,14 @@ export default function App({ Component, pageProps }: AppProps) {
: undefined;
return (
-
-
+
+
+
{title}
@@ -65,8 +72,9 @@ export default function App({ Component, pageProps }: AppProps) {
>
)}
-
-
-
+
+
+
+
);
}
diff --git a/pages/_document.tsx b/pages/_document.tsx
index 8fd08be..69c98f5 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -13,7 +13,7 @@ const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
export default function Document() {
return (
-
+
diff --git a/pages/methodology.tsx b/pages/methodology.tsx
index bf90ce4..39de130 100644
--- a/pages/methodology.tsx
+++ b/pages/methodology.tsx
@@ -4,7 +4,9 @@ import {
BrandLink,
TagPill,
TopBar,
+ TopSpacer,
} from "@components/assessment/stitches";
+import { ThemeToggle } from "@components/assessment/ThemeToggle";
import { SLICES } from "@lib/rubric";
import type { GetStaticProps } from "next";
@@ -12,12 +14,12 @@ export default function MethodologyPage() {
return (
-
-
- Validator Beat
-
-
+
+ Validator Beat
+
v0.1 · methodology
+
+
diff --git a/styles/theme-tokens.css b/styles/theme-tokens.css
index ae96599..8ed606e 100644
--- a/styles/theme-tokens.css
+++ b/styles/theme-tokens.css
@@ -50,6 +50,58 @@
--theme-pizza-ink: #000000;
}
+/* =============================================================================
+ DARK THEME — Obol-branded dark palette, mirrors the standalone prototype.
+ Activated by data-theme="dark" on (see next-themes ThemeProvider in _app).
+ Overrides the --theme-* source values; the alias layer below recomputes
+ automatically. --bg-01..05 are re-pointed past obol-ui's --colors-* tokens.
+ ============================================================================= */
+:root[data-theme="dark"] {
+ /* ---- Brand accent (bright mint reads on dark surfaces) ---- */
+ --theme-brand: #2fe4ab; /* obolGreen */
+ --theme-brand-hover: #0f7c76;
+ --theme-brand-highlight: #b6ea5c; /* lime */
+
+ /* ---- Page surfaces ---- */
+ --theme-surface-page: #091011;
+ --theme-surface-card: #111f22;
+ --theme-surface-panel: #182d32;
+ --theme-surface-hover: #243d42;
+ --theme-border: #2d4d53;
+ --theme-border-strong: #3a5f66;
+
+ /* ---- Text ---- */
+ --theme-text-primary: #e1e9eb;
+ --theme-text-secondary: #97b2b8;
+ --theme-text-muted: #667a80;
+ --theme-text-faint: #475e64;
+ --theme-text-on-brand: #091011;
+
+ /* ---- Pizza / risk slices ---- */
+ --theme-risk-green: #2fe4ab;
+ --theme-risk-yellow: #e8b339;
+ --theme-risk-red: #dd603c;
+ --theme-risk-green-ring: #6ff0c8;
+ --theme-risk-yellow-ring: #f0c95f;
+ --theme-risk-red-ring: #e8856a;
+
+ /* ---- Pizza plate / empty wedge ---- */
+ --theme-pizza-plate: #0a1214;
+ --theme-pizza-empty: #16282d;
+ --theme-pizza-empty-stroke: #2d4d53;
+ --theme-pizza-ink: #e1e9eb;
+
+ /* Re-point surface aliases past obol-ui's scoped --colors-* tokens. */
+ --bg-01: var(--theme-surface-page);
+ --bg-02: var(--theme-surface-card);
+ --bg-03: var(--theme-surface-panel);
+ --bg-04: var(--theme-surface-hover);
+ --bg-05: var(--theme-border);
+
+ /* --ice diverges from brand in the dark palette (muted ice-blue). */
+ --ice: #9cc2c9;
+}
+
/* ---- App aliases (handoff CSS; mirror obol-ui $bg01 / $obolGreen tokens) ---- */
:root {
--bg-01: var(--colors-bg01, var(--theme-surface-page));
diff --git a/yarn.lock b/yarn.lock
index b1dfd90..cbe8fd0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4669,6 +4669,11 @@ neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+next-themes@^0.4.6:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6"
+ integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==
+
next@14.2.35:
version "14.2.35"
resolved "https://registry.yarnpkg.com/next/-/next-14.2.35.tgz#7c68873a15fe5a19401f2f993fea535be3366ee9"