From dda30c1831a54d87ccb9ac7d59bede6155bdb07c Mon Sep 17 00:00:00 2001 From: Kody Sale Date: Wed, 10 Jun 2026 10:40:29 -0700 Subject: [PATCH 1/3] Recolor the light theme to a cream palette with muted pizza colors. --- lib/theme/tokens.ts | 22 ++++++------- styles/theme-tokens.css | 69 +++++++++++++++++++++-------------------- 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/lib/theme/tokens.ts b/lib/theme/tokens.ts index 964c88b..cef6247 100644 --- a/lib/theme/tokens.ts +++ b/lib/theme/tokens.ts @@ -4,24 +4,24 @@ */ import type { SliceColor } from "@lib/rubric/types"; -export const THEME_BRAND = "#16968e"; +export const THEME_BRAND = "#2c7a64"; export const PIZZA_FILL: Record = { - green: "#1f9b6b", - yellow: "#e7c300", - red: "#bb1122", + green: "#3a9e80", + yellow: "#cf9a3a", + red: "#c46044", }; export const PIZZA_RING: Record = { - green: "#82edcc", - yellow: "#ffe566", - red: "#e85c6f", + green: "#1f7a5e", + yellow: "#a8761f", + red: "#a64428", }; -export const PIZZA_PLATE = "#ffffff"; -export const PIZZA_EMPTY = "#f0f0f0"; -export const PIZZA_EMPTY_STROKE = "#cccccc"; -export const PIZZA_INK = "#000000"; +export const PIZZA_PLATE = "#fffdf7"; +export const PIZZA_EMPTY = "#e9e5d8"; +export const PIZZA_EMPTY_STROKE = "#d2cbb8"; +export const PIZZA_INK = "#0e1b1d"; export const CONFETTI_BRAND = [ THEME_BRAND, diff --git a/styles/theme-tokens.css b/styles/theme-tokens.css index 8ed606e..1411e9d 100644 --- a/styles/theme-tokens.css +++ b/styles/theme-tokens.css @@ -1,9 +1,11 @@ /* ============================================================================= VALIDATOR BEAT — THEME (edit this file to change colors) - Light surfaces with semantic success / warning / danger. Brand accents draw - from the Obol palette (obol-ui DESIGN.md: obolDarkDark / obolGreen / obolGreenHover) - while staying readable on white backgrounds. + Light surface is a warm, ecosystem-neutral cream paper with muted sage / earth + signal colors (green = good, yellow = partial, red = SPOF). The cream palette + reads as a coalition surface rather than an Obol-branded one, and applies across + the whole stack — landing, assessment, methodology — via the alias layer below + and the obol-ui bridge (styles/obol-bridge.css). Only change the hex values in the block below. The rest of the app maps these automatically (CSS variables + pizza SVG colors in lib/theme/tokens.ts). @@ -11,43 +13,42 @@ :root { /* ---- Brand accent (links, primary buttons, wordmark highlight) ---- - Obol-palette greens, picked for contrast on white surfaces. */ - --theme-brand: #16968e; /* obolDarkDark — readable on white */ - --theme-brand-hover: #0f7c76; /* darker neighbour in Obol palette */ - --theme-brand-highlight: #2fe4ab; /* obolGreen — decorative tints / confetti */ - - /* ---- Page surfaces ---- */ - --theme-surface-page: #f7f7f7; - --theme-surface-card: #ffffff; - --theme-surface-panel: #f0f0f0; - --theme-surface-hover: #ebebeb; - --theme-border: #e6e6e6; - --theme-border-strong: #cccccc; - - /* ---- Text ---- */ - --theme-text-primary: #000000; - --theme-text-secondary: #696969; - --theme-text-muted: #999999; - --theme-text-faint: #cccccc; + Muted deep sage-green, readable on the cream paper. */ + --theme-brand: #2c7a64; /* deep sage — primary CTA / links */ + --theme-brand-hover: #235f4e; /* pressed / hover sage */ + --theme-brand-highlight: #2fe4ab; /* mint — decorative tints / confetti */ + + /* ---- Page surfaces — warm cream ---- */ + --theme-surface-page: #f6f4ec; /* paper */ + --theme-surface-card: #fffefa; /* raised paper */ + --theme-surface-panel: #edf1e7; /* alternating section band */ + --theme-surface-hover: #e7e2d3; + --theme-border: #e6e1d1; /* hairline */ + --theme-border-strong: #d8d2bf; + + /* ---- Text — near-black teal ink ramp ---- */ + --theme-text-primary: #0e1b1d; + --theme-text-secondary: #45565a; + --theme-text-muted: #7d8a86; + --theme-text-faint: #aab3ad; --theme-text-on-brand: #ffffff; /* ---- Pizza / risk slices (green = good, yellow = partial, red = SPOF) ---- - Stage-2 green nudged toward the Obol mint family while staying legible as - "green" alongside the yellow and red in the traffic-light triad. */ - --theme-risk-green: #1f9b6b; - --theme-risk-yellow: #e7c300; - --theme-risk-red: #bb1122; + Muted earthy variants tuned to sit calmly on cream. */ + --theme-risk-green: #3a9e80; + --theme-risk-yellow: #cf9a3a; + --theme-risk-red: #c46044; - /* ---- Pizza ring tints (lighter than slice fill, for center progress ring) ---- */ - --theme-risk-green-ring: #82edcc; /* obolGreenHover */ - --theme-risk-yellow-ring: #ffe566; - --theme-risk-red-ring: #e85c6f; + /* ---- Pizza ring tints (center progress ring; darker than fill on cream) ---- */ + --theme-risk-green-ring: #1f7a5e; + --theme-risk-yellow-ring: #a8761f; + --theme-risk-red-ring: #a64428; /* ---- Pizza plate / empty wedge ---- */ - --theme-pizza-plate: #ffffff; - --theme-pizza-empty: #f0f0f0; - --theme-pizza-empty-stroke: #cccccc; - --theme-pizza-ink: #000000; + --theme-pizza-plate: #fffdf7; + --theme-pizza-empty: #e9e5d8; + --theme-pizza-empty-stroke: #d2cbb8; + --theme-pizza-ink: #0e1b1d; } /* ============================================================================= From 3d065759d231c4543d8d6186aeb5a3e03cb2dbcd Mon Sep 17 00:00:00 2001 From: Kody Sale Date: Wed, 10 Jun 2026 10:44:36 -0700 Subject: [PATCH 2/3] Restore the heartbeat animation and fix landing layout regressions. --- components/assessment/stitches.ts | 57 ++++++++++++++++++++++++++++++- components/landing/Landing.tsx | 25 +++++++++----- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/components/assessment/stitches.ts b/components/assessment/stitches.ts index 79c55e4..49e4c58 100644 --- a/components/assessment/stitches.ts +++ b/components/assessment/stitches.ts @@ -1,4 +1,4 @@ -import { Box, Text, styled } from "@obolnetwork/obol-ui"; +import { Box, Text, keyframes, styled } from "@obolnetwork/obol-ui"; import type { SliceColor } from "@lib/rubric/types"; import NextLink from "next/link"; @@ -59,6 +59,61 @@ export const BrandAccent = styled("span", { color: "var(--theme-brand)", }); +/* "beat" — a heartbeat that cycles the three signal colors (green → yellow → red), + with a pulsing halo behind it. Reads from the --vb-* tokens, so it follows the + light/dark theme automatically. */ +const beatColor = keyframes({ + "0%": { color: "var(--vb-green)" }, + "10%": { color: "var(--vb-green)" }, + "33%": { color: "var(--vb-yellow)" }, + "43%": { color: "var(--vb-yellow)" }, + "66%": { color: "var(--vb-red)" }, + "76%": { color: "var(--vb-red)" }, + "100%": { color: "var(--vb-green)" }, +}); +const beatRing = keyframes({ + "0%": { opacity: 0, transform: "translate(-50%,-50%) scale(0.6)", boxShadow: "0 0 0 0 var(--vb-green)" }, + "6%": { opacity: 0.5, transform: "translate(-50%,-50%) scale(0.92)", boxShadow: "0 0 22px 5px var(--vb-green)" }, + "14%": { opacity: 0.18, transform: "translate(-50%,-50%) scale(1.04)" }, + "20%": { opacity: 0.55, transform: "translate(-50%,-50%) scale(1.0)", boxShadow: "0 0 26px 7px var(--vb-green)" }, + "30%": { opacity: 0, transform: "translate(-50%,-50%) scale(1.12)", boxShadow: "0 0 0 0 var(--vb-yellow)" }, + "39%": { opacity: 0.5, transform: "translate(-50%,-50%) scale(0.92)", boxShadow: "0 0 22px 5px var(--vb-yellow)" }, + "47%": { opacity: 0.18, transform: "translate(-50%,-50%) scale(1.04)" }, + "53%": { opacity: 0.55, transform: "translate(-50%,-50%) scale(1.0)", boxShadow: "0 0 26px 7px var(--vb-yellow)" }, + "63%": { opacity: 0, transform: "translate(-50%,-50%) scale(1.12)", boxShadow: "0 0 0 0 var(--vb-red)" }, + "72%": { opacity: 0.5, transform: "translate(-50%,-50%) scale(0.92)", boxShadow: "0 0 22px 5px var(--vb-red)" }, + "80%": { opacity: 0.18, transform: "translate(-50%,-50%) scale(1.04)" }, + "86%": { opacity: 0.55, transform: "translate(-50%,-50%) scale(1.0)", boxShadow: "0 0 26px 7px var(--vb-red)" }, + "100%": { opacity: 0, transform: "translate(-50%,-50%) scale(1.12)", boxShadow: "0 0 0 0 var(--vb-green)" }, +}); + +export const Beat = styled("span", { + position: "relative", + display: "inline-block", + fontWeight: 800, + isolation: "isolate", // contain the negative-z pulse above the section background + animation: `${beatColor} 3s cubic-bezier(.16,1,.3,1) infinite`, + "@media (prefers-reduced-motion: reduce)": { + animation: "none", + color: "var(--theme-brand)", + }, +}); + +export const BeatPulse = styled("span", { + position: "absolute", + left: "50%", + top: "54%", + width: "1.55em", + height: "1.55em", + transform: "translate(-50%, -50%) scale(0.6)", + borderRadius: "50%", + zIndex: -1, + opacity: 0, + pointerEvents: "none", + animation: `${beatRing} 3s cubic-bezier(.16,1,.3,1) infinite`, + "@media (prefers-reduced-motion: reduce)": { animation: "none", display: "none" }, +}); + export const TagPill = styled(Box, { fontSize: "$1", fontWeight: "$semibold", diff --git a/components/landing/Landing.tsx b/components/landing/Landing.tsx index 0d20854..22b8d3e 100644 --- a/components/landing/Landing.tsx +++ b/components/landing/Landing.tsx @@ -2,6 +2,8 @@ import { Box, Text } from "@obolnetwork/obol-ui"; import NextLink from "next/link"; import { Pizza } from "@components/pizza/Pizza"; import { + Beat, + BeatPulse, BrandAccent, BrandLink, Card, @@ -438,7 +440,12 @@ function LHero() { A simple view into validator operations - The validator ecosystem is missing its beat. + The validator ecosystem is missing its{" "} + + beat + + . When you stake ETH, you always (sometimes unknowingly) pick an operator. You{" "} @@ -458,7 +465,7 @@ function LHero() { - + An example operator profile @@ -507,7 +514,7 @@ function LGap() { the rest of the network. - That invisible risk is unpriced. + That invisible risk is unpriced. {" "}Validator Beat is the public entry point that compares these risks and gives operators a reason to compete on it. @@ -597,9 +604,9 @@ function LRead() { Each slice scored{" "} - green,{" "} - yellow, or{" "} - red + green,{" "} + yellow, or{" "} + red @@ -700,7 +707,7 @@ function LValos() { Validator Beat - The who — the public dashboard + The who — the public dashboard @@ -709,7 +716,7 @@ function LValos() { valOS - The how — the operating standard + The how — the operating standard @@ -781,7 +788,7 @@ function LFooter() { - Validator Beat + Validator Beat A simple view into validator operations From ad83f5fd1e76c3b3da3efb22b92ce86f3aebe2c5 Mon Sep 17 00:00:00 2001 From: Kody Sale Date: Wed, 10 Jun 2026 10:49:05 -0700 Subject: [PATCH 3/3] Share one header and footer across the landing and assessment. --- CLAUDE.md | 4 +- README.md | 18 ++-- components/assessment/AssessmentApp.tsx | 26 +---- components/assessment/stitches.ts | 2 +- components/landing/Landing.tsx | 112 ++------------------ components/layout/Footer.tsx | 27 ----- components/layout/Navbar.tsx | 58 ----------- components/layout/SiteFooter.tsx | 61 +++++++++++ components/layout/SiteHeader.tsx | 133 ++++++++++++++++++++++++ components/layout/SiteLayout.tsx | 37 ------- constants/index.ts | 5 + pages/methodology.tsx | 21 +--- 12 files changed, 227 insertions(+), 277 deletions(-) delete mode 100644 components/layout/Footer.tsx delete mode 100644 components/layout/Navbar.tsx create mode 100644 components/layout/SiteFooter.tsx create mode 100644 components/layout/SiteHeader.tsx delete mode 100644 components/layout/SiteLayout.tsx diff --git a/CLAUDE.md b/CLAUDE.md index dd69717..9610f2c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -86,7 +86,7 @@ Pre-commit (husky) runs `yarn test` + `yarn lint`. Lint-staged additionally runs **Default:** build UI with `@obolnetwork/obol-ui` primitives — `Box`, `Text`, `Button`, `Link` — and the project’s Stitches tokens (`$bg01`, `$body`, `$textMiddle`, `var(--theme-brand)`, etc.). Bridge tokens live in `styles/theme-tokens.css` and `styles/obol-bridge.css`. -- Prefer the **`css` prop** on `Box` / `Text` (see `components/layout/Navbar.tsx`, `components/landing/Landing.tsx`) or **reuse styled exports** from `components/assessment/stitches.ts` (`Card`, `Eyebrow`, `BrandLink`, `TopNavLink`, `risk`, …) instead of new page-specific CSS files or BEM class strings. +- Prefer the **`css` prop** on `Box` / `Text` (see `components/layout/SiteHeader.tsx`, `components/landing/Landing.tsx`) or **reuse styled exports** from `components/assessment/stitches.ts` (`Card`, `Eyebrow`, `BrandLink`, `TopNavLink`, `risk`, …) instead of new page-specific CSS files or BEM class strings. - Use **`VbButton`** (`components/ui/VbButton.tsx`) for primary actions in the assessment flow. - **Do not** add separate per-page CSS files (e.g. a dedicated `landing.css`) unless there is a strong reason; the landing page was migrated off that pattern. @@ -96,7 +96,7 @@ Pre-commit (husky) runs `yarn test` + `yarn lint`. Lint-staged additionally runs - **SVG visuals** tied to the product (`Pizza`, brand mark) — not generic UI icons; use `@radix-ui/react-icons` (already in the tree via obol-ui) for arrows, external-link, etc. (`components/landing/icons.tsx`). - **Methodology** — still uses `styles/methodology.css` for now; new work should still prefer obol-ui where practical. -**Site chrome (future):** landing and assessment currently use different headers/footers. Planned UX is a shared slim header + compact assess footer — see **Future UX** in [`README.md`](./README.md). Do not implement unless asked. +**Site chrome:** the landing, assessment, and methodology share one header and footer — `components/layout/SiteHeader.tsx` and `SiteFooter.tsx` (the landing nav, generalized). `SiteHeader` reads the route to adapt: it hides the "Assess your validator" CTA on `/assess` and points "How it works" at `/#how` from inner pages. Both take a `contentWidth` prop to align the inner content with the page's main column (landing/methodology `1140`, assessment `1440`). The assessment renders them as the top/bottom rows of the 100vh `Shell` (`MainGrid` is `flex:1`). ## Deferred work (don't build yet unless asked) diff --git a/README.md b/README.md index 5300a1f..21bbcf9 100644 --- a/README.md +++ b/README.md @@ -34,21 +34,15 @@ Every push to `main` runs CI and, on success, deploys the static `out/` folder t - `NEXT_PUBLIC_SITE_URL=https://obolnetwork.github.io/validator-beat` — share links and OG URLs 3. **Custom domain** (`validatorbeat.com`): point DNS at GitHub Pages, then rebuild with `NEXT_PUBLIC_BASE_PATH=` (empty) and `NEXT_PUBLIC_SITE_URL=https://validatorbeat.com`. -**Theming:** edit [`styles/theme-tokens.css`](styles/theme-tokens.css) to change colors (Lido forum–aligned by default). If you change pizza slice colors, also update [`lib/theme/tokens.ts`](lib/theme/tokens.ts). +**Theming:** edit [`styles/theme-tokens.css`](styles/theme-tokens.css) to change colors (warm cream, ecosystem-neutral by default; an Obol-branded dark palette under `[data-theme="dark"]`). If you change pizza slice colors, also update [`lib/theme/tokens.ts`](lib/theme/tokens.ts). -## Future UX: shared chrome across landing and assessment +## Site chrome: shared header and footer -Today the **landing** (`/`) and **assessment** (`/assess/`) use different shells: the marketing page has a full nav + footer; the assessment uses a slim `TopBar` and a one-line `Footnote` inside a `100vh` focus layout (`AssessmentApp`). That is intentional for the six-question wizard (less distraction, pizza always visible) but can feel like leaving the site when users click “Assess your validator.” +The **landing** (`/`), **assessment** (`/assess/`), and **methodology** (`/methodology/`) share one header and footer — [`components/layout/SiteHeader.tsx`](components/layout/SiteHeader.tsx) and [`SiteFooter.tsx`](components/layout/SiteFooter.tsx) (the landing nav, generalized). -**Recommended direction (not implemented yet):** - -1. **One shared slim header** on `/`, `/assess/`, and `/methodology/` — logo (home), Methodology, valOS (external), theme toggle; primary CTA on marketing pages only. -2. **Compact footer on assess** — keep the trust line (“nothing submitted or stored”) plus links (Methodology · valOS · Home), not the full landing footer. -3. **Keep focus mode during Q1–Q6** — do not wrap the wizard in the full marketing layout; optional richer links on intro/results only if needed later. - -**Avoid:** pasting the full landing nav + footer around the assessment grid — it costs too much vertical space on mobile and fights the locked viewport. - -See `components/assessment/AssessmentApp.tsx` (`TopBar`, `Footnote`, `Shell`) and `components/landing/Landing.tsx` (`LNav`, `LFooter`) for the current split. +- **`SiteHeader`** adapts to the route: it hides the "Assess your validator" CTA on `/assess` (you're already there) and points "How it works" at `/#how` from inner pages. The logo mark reads the `--vb-*` tokens, so it follows the theme. +- Both take a **`contentWidth`** prop so the inner content lines up with the page's main column (landing/methodology `1140`, assessment `1440`). +- The assessment renders them as the top/bottom rows of the `100vh` `Shell`; `MainGrid` is `flex:1` and absorbs the remaining height, so the focus layout still fits one screen. ## v0.1 routes diff --git a/components/assessment/AssessmentApp.tsx b/components/assessment/AssessmentApp.tsx index 4a46633..830b724 100644 --- a/components/assessment/AssessmentApp.tsx +++ b/components/assessment/AssessmentApp.tsx @@ -17,12 +17,10 @@ import { } from "./Blockers"; import { Intro } from "./Intro"; import { Question } from "./Question"; -import { ThemeToggle } from "./ThemeToggle"; import { LevelUp, ResultHero, ShareModal } from "./Results"; +import { SiteHeader } from "@components/layout/SiteHeader"; +import { SiteFooter } from "@components/layout/SiteFooter"; import { - BrandAccent, - BrandLink, - Footnote, LeftCard, MainGrid, PizzaWrap, @@ -31,10 +29,6 @@ import { RightPanel, SectionLabel, Shell, - TagPill, - TopBar, - TopNavLink, - TopSpacer, } from "./stitches"; type AssessmentAppProps = { @@ -111,15 +105,7 @@ export function AssessmentApp({ initialShareCode }: AssessmentAppProps) { return ( - - - Validator Beat - - v0.1 · self-assessment - - Methodology - - + @@ -175,10 +161,8 @@ export function AssessmentApp({ initialShareCode }: AssessmentAppProps) { - - A neutral, public-good resource · nothing is submitted or stored · runs entirely in your - browser - + + {share && a.stage != null && ( - ); -} - const HERO_SAMPLE: Answers = { keyCustody: "green", clientDiversity: "green", @@ -404,32 +357,6 @@ const STAGES = [ }, ]; -function LNav() { - return ( - - - - Validator Beat - - - - - How it works - - Methodology - - valOS - - - - - Assess your validator - - - - ); -} - function LHero() { return ( @@ -783,31 +710,10 @@ function LClosing() { ); } -function LFooter() { - return ( - - - - Validator Beat - - - A simple view into validator operations - - - Methodology - - valOS - - Assess - - - ); -} - export function Landing() { return ( - + @@ -816,7 +722,7 @@ export function Landing() { - + ); } diff --git a/components/layout/Footer.tsx b/components/layout/Footer.tsx deleted file mode 100644 index a56c408..0000000 --- a/components/layout/Footer.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Box, Link, Text } from "@obolnetwork/obol-ui"; - -export function Footer() { - return ( - - - Validator Beat — transparency for Ethereum node operators.{" "} - - Contribute - - - - ); -} diff --git a/components/layout/Navbar.tsx b/components/layout/Navbar.tsx deleted file mode 100644 index a8b52f7..0000000 --- a/components/layout/Navbar.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Box, Text } from "@obolnetwork/obol-ui"; -import NextLink from "next/link"; -import { useRouter } from "next/router"; -import { SITE_NAME } from "@constants/index"; - -const NAV_ITEMS = [ - { href: "/", label: "Manifesto" }, - { href: "/operators/", label: "Operators" }, - { href: "/methodology/", label: "Methodology" }, -] as const; - -export function Navbar() { - const router = useRouter(); - - return ( - - - - {SITE_NAME} - - - - {NAV_ITEMS.map(({ href, label }) => { - const active = - href === "/" - ? router.pathname === "/" - : router.pathname.startsWith(href.replace(/\/$/, "")); - return ( - - {label} - - ); - })} - - - ); -} diff --git a/components/layout/SiteFooter.tsx b/components/layout/SiteFooter.tsx new file mode 100644 index 0000000..eb5d312 --- /dev/null +++ b/components/layout/SiteFooter.tsx @@ -0,0 +1,61 @@ +import { Box, Text } from "@obolnetwork/obol-ui"; +import type { CSS } from "@stitches/react"; +import { TopNavLink, TopSpacer } from "@components/assessment/stitches"; +import { ASSESS_PATH, METHODOLOGY_PATH, VALOS_URL } from "@constants/index"; + +const navLink: CSS = { + fontSize: "$2", + fontWeight: "$medium", + color: "$textMiddle", + textDecoration: "none", + "&:hover": { color: "$body" }, +}; + +type SiteFooterProps = { + /** Max width of the inner content — match the page's main column. */ + contentWidth?: number; +}; + +/** Shared site footer — reused across the landing, assessment, and methodology. */ +export function SiteFooter({ contentWidth = 1140 }: SiteFooterProps) { + return ( + + + + Validator{" "} + + Beat + + + + A simple view into validator operations + + + Methodology + + valOS + + Assess + + + ); +} diff --git a/components/layout/SiteHeader.tsx b/components/layout/SiteHeader.tsx new file mode 100644 index 0000000..11556d5 --- /dev/null +++ b/components/layout/SiteHeader.tsx @@ -0,0 +1,133 @@ +import { Box } from "@obolnetwork/obol-ui"; +import NextLink from "next/link"; +import { useRouter } from "next/router"; +import type { CSS } from "@stitches/react"; +import { + BrandAccent, + BrandLink, + TopNavLink, + TopSpacer, +} from "@components/assessment/stitches"; +import { ThemeToggle } from "@components/assessment/ThemeToggle"; +import { IconArrowRight } from "@components/landing/icons"; +import { ASSESS_PATH, METHODOLOGY_PATH, VALOS_URL } from "@constants/index"; + +const bar: CSS = { + position: "sticky", + top: 0, + zIndex: 50, + borderBottom: "1px solid $bg05", + backgroundColor: "$bg01", + backdropFilter: "blur(10px)", +}; +const navLink: CSS = { + fontSize: "$2", + fontWeight: "$medium", + color: "$textMiddle", + textDecoration: "none", + "&:hover": { color: "$body" }, +}; +const links: CSS = { + display: "flex", + alignItems: "center", + gap: 22, + "@media (max-width: 720px)": { display: "none" }, +}; +const cta: CSS = { + display: "inline-flex", + alignItems: "center", + gap: 8, + fontSize: "$3", + fontWeight: "$semibold", + textDecoration: "none", + whiteSpace: "nowrap", + borderRadius: "$3", + padding: "12px 22px", + backgroundColor: "var(--theme-brand)", + color: "var(--theme-text-on-brand)", + "&:hover": { backgroundColor: "var(--theme-brand-hover)" }, +}; + +/** Validator Beat pizza mark. Reads the --vb-* tokens so it follows the theme + (muted on cream, bright on dark) like the in-app pizza. */ +function Mark() { + return ( + + ); +} + +type SiteHeaderProps = { + /** Max width of the inner content — match the page's main column (landing 1140, assessment 1440). */ + contentWidth?: number; +}; + +/** Shared site header — the landing nav, reused across the landing, assessment, + and methodology. The "Assess your validator" CTA hides on /assess (you're + already there); "How it works" jumps to the landing's section from inner pages. */ +export function SiteHeader({ contentWidth = 1140 }: SiteHeaderProps) { + const router = useRouter(); + const onLanding = router.pathname === "/"; + const onAssess = router.pathname === "/assess"; + const howHref = onLanding ? "#how" : "/#how"; + + return ( + + + + Validator Beat + + + + + How it works + + Methodology + + valOS + + + + {!onAssess && ( + + Assess your validator + + )} + + + ); +} diff --git a/components/layout/SiteLayout.tsx b/components/layout/SiteLayout.tsx deleted file mode 100644 index c20096a..0000000 --- a/components/layout/SiteLayout.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Box } from "@obolnetwork/obol-ui"; -import type { ReactNode } from "react"; -import { Footer } from "./Footer"; -import { Navbar } from "./Navbar"; - -type SiteLayoutProps = { - children: ReactNode; -}; - -export function SiteLayout({ children }: SiteLayoutProps) { - return ( - - - - {children} - -