From 9073757db88ff9fd7f6ed7bff8d2bc02002733fe Mon Sep 17 00:00:00 2001 From: Kody Sale Date: Tue, 9 Jun 2026 17:50:28 -0700 Subject: [PATCH 1/3] Add a landing page in front of the assessment app. --- components/landing/Landing.tsx | 553 +++++++++++++++++++++++++++++++++ components/pizza/Pizza.tsx | 5 +- pages/_app.tsx | 1 + pages/assess.tsx | 14 + pages/index.tsx | 8 +- pages/methodology.tsx | 2 +- styles/landing.css | 340 ++++++++++++++++++++ 7 files changed, 917 insertions(+), 6 deletions(-) create mode 100644 components/landing/Landing.tsx create mode 100644 pages/assess.tsx create mode 100644 styles/landing.css diff --git a/components/landing/Landing.tsx b/components/landing/Landing.tsx new file mode 100644 index 0000000..433f0ce --- /dev/null +++ b/components/landing/Landing.tsx @@ -0,0 +1,553 @@ +import Link from "next/link"; +import { Pizza } from "@components/pizza/Pizza"; +import { ThemeToggle } from "@components/assessment/ThemeToggle"; +import type { Answers, SliceId } from "@lib/rubric/types"; + +/** + * Marketing landing page — the public entry point in front of the assessment. + * Ported from the approved design handoff (light/cream, ecosystem-neutral). + * Scoped under .vb-landing; reuses the shared Pizza component, which reads the + * cream-tuned CSS tokens defined in styles/landing.css. + */ + +const ASSESS = "/assess/"; +const METHODOLOGY = "/methodology/"; +const VALOS = "https://lidofinance.github.io/valos/valos-spec.html"; + +/* muted signal hexes for the static slice dots (match the cream tokens) */ +const FUNC = { green: "#3a9e80", yellow: "#cf9a3a", red: "#c46044" } as const; + +/* ---- tiny line-icon set (single-color, currentColor) --------------------- */ +type IconProps = { size?: number; sw?: number; children?: React.ReactNode }; +const Icon = ({ size = 22, sw = 1.8, children }: IconProps) => ( + +); +const IArrowR = (p: IconProps) => ( + + + + +); +const IArrowD = (p: IconProps) => ( + + + + +); +const IExt = (p: IconProps) => ( + + + + + +); +const ISlash = (p: IconProps) => ( + + + + + +); +const IPulse = (p: IconProps) => ( + + + +); + +const Mark = () => ( + +); + +/* ---- §0 Nav --------------------------------------------------------------- */ +const LNav = () => ( + +); + +/* ---- §1 Hero -------------------------------------------------------------- */ +const HERO_SAMPLE: Answers = { + keyCustody: "green", + clientDiversity: "green", + infraDiversity: "yellow", + osDiversity: "green", + cpuDiversity: "red", + geoDiversity: "yellow", +}; +const LHero = () => ( +
+
+
+
+

A simple view into validator operations

+

+ The validator ecosystem is missing its{" "} + + beat + . +

+

+ When you stake ETH, you always (sometimes unknowingly) pick an operator. You{" "} + cannot see how they hold their keys, which clients they run, or how + correlated their setup is with everyone else securing the network. Validator Beat makes + that visible. +

+
+ + Assess your validator + + + See how it works + +
+
+
+
+
+ +
+ + An example operator profile + +
+
+
+
+); + +/* ---- §2 The gap ----------------------------------------------------------- */ +const LGap = () => ( +
+
+
+

The gap

+

+ Transparency has been built into every layer, except the validators. +

+
+
+ + L2Beat + + + + + + Walletbeat + + + + + + Validator Beat + +
+

+ Rollups have L2Beat. Wallets have Walletbeat. The validator operators securing tens of + millions of staked ETH have had no way to differentiate themselves outside of performance + and reputation. Stakers, from ETH holders to institutional allocators, choose an operator or + protocol and forget about it. Compliance attestations like SOC 2 cover process and custody, + but they say nothing about the choices that actually determine whether a validator stays safe + and online: how signing keys are split, whether the operator runs a single client + implementation, how concentrated their infrastructure is, and how much of that overlaps with + the rest of the network. +

+

+ 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. +

+
+
+); + +/* ---- §3 How validators actually fail -------------------------------------- */ +const LFail = () => ( +
+
+
+

Why it matters

+

How validators actually fail

+

+ There are two ways a validator fails, and decentralization is the insurance against both. +

+
+
+
+
+ +
+

Safety failure · slashing

+

It signs something it never should have.

+

+ A double attestation, or a supermajority vote on the wrong chain. The protocol slashes + the stake. This is the expensive failure. +

+
+
+
+ +
+

Liveness failure · downtime

+

It goes offline and misses its duties.

+

+ No slashing, but no rewards either, and a weaker network. The validator simply isn't + there when it's needed. +

+
+
+

+ Most safety and liveness issues trace back to a single point of failure: + one machine, one team member, one client, one provider, one region. Validator Beat measures + how many of those single points of failure an operator has removed. +

+
+
+); + +/* ---- §4 How VB reads an operator ------------------------------------------ */ +const SLICE_DESC: Record = { + keyCustody: + "How signing keys are held, and how many independent parties must cooperate to sign.", + clientDiversity: "Exposure to a bug in any single consensus or execution client.", + infraDiversity: "Concentration on a single cloud or hosting provider.", + osDiversity: "Correlated risk from running a single operating system across the fleet.", + cpuDiversity: "Correlated risk from running a single chipset across the fleet.", + geoDiversity: + "Concentration in one region and exposure to power failure or natural disaster.", +}; +const READ_LABEL: Record = { + keyCustody: "Key Custody", + clientDiversity: "Client Diversity", + infraDiversity: "Provider Diversity", + osDiversity: "OS Diversity", + cpuDiversity: "CPU Architecture", + geoDiversity: "Geographic Diversity", +}; +const READ_SAMPLE: Answers = { + keyCustody: "green", + clientDiversity: "green", + infraDiversity: "yellow", + osDiversity: "green", + cpuDiversity: "green", + geoDiversity: "yellow", +}; +const ORDER: SliceId[] = [ + "keyCustody", + "clientDiversity", + "infraDiversity", + "geoDiversity", + "osDiversity", + "cpuDiversity", +]; +const LRead = () => ( +
+
+
+

The assessment

+

How Validator Beat assesses an operator

+

+ Most operators start exposed with two stages to climb and a six-slice risk profile. +

+
+
+
+
+ 0 +
+
Stage 0 — Exposed
+
Starting point
+
+
+

+ At least one single point of failure remains. Most operators start here, it's the + baseline. +

+
+
+
+ 1 +
+
Stage 1 — Slashing-averse
+
Safety
+
+
+

+ No single compromise of one machine, one team member, or one signer can produce a + slashable message. +

+
+
+
+ 2 +
+
Stage 2 — Downtime-averse
+
Liveness
+
+
+

+ No single point of failure in the operator's infrastructure can take the validator + offline. This is the end game. +

+
+
+
+
+ + + Each slice scored green,{" "} + yellow, or red + +
+
+ {ORDER.map((id) => ( +
+
+ {" "} + {READ_LABEL[id]} +
+

{SLICE_DESC[id]}

+
+ ))} +
+
+

+ This can be read at a glance. For the exact thresholds and rubric, see the{" "} + + methodology + + . To see where your own validator lands,{" "} + + take the assessment + + . +

+

Validator Beat is self-reported and consent-based.

+
+
+); + +/* ---- §5 Both sides -------------------------------------------------------- */ +const LSides = () => ( +
+
+
+

Who it's for

+

Built for both sides of the stake

+
+
+
+

If you delegate your ETH

+

Read any operator's profile in five seconds.

+

+ Before you stake, not after an incident. Two stages and six colors give you insight into + validator operations that the marketing page never will. +

+
+
+

If you're an operatooor

+

Proof of work.

+

+ Run the assessment, earn your stages, and show off your pizza wherever you list your + product. Operators who have removed their single points of failure now have a way to show + it. +

+
+ + Assess your validator + +
+
+
+
+
+); + +/* ---- §6 valOS ------------------------------------------------------------- */ +const LValos = () => ( +
+
+
+
+

The standard behind the score

+

Validator Beat and valOS

+

+ Validator Beat is the public-facing who is running validators. valOS, + the Validator Operating Standard, is the technical how: a deep catalog + of the controls and mitigations behind professional validator operations. +

+

+ A staker can quickly read an operator's stages and profile here. An operator doing + the hard work and implementing the mitigations should dig into valOS. Follow valOS, and + you'll end up at stage 2. +

+ +
+
+
+
Validator Beat
+
+ The who — the public dashboard +
+
+
+ +
+
+
valOS
+
+ The how — the operating standard +
+
+
+
+
+
+); + +/* ---- §7 Neutral standard -------------------------------------------------- */ +const LNeutral = () => ( +
+
+
+

Credibility

+

A neutral dashboard, built in the open

+

+ Validator Beat is co-authored by Obol and Lido. It is deliberately neutral and no single + team owns the rubric. The methodology is public and is meant to be adopted, challenged, + and improved by the whole ecosystem. +

+

+ The goal is a race to the top, where transparency about validator risk + becomes the default expectation. +

+
+ + Obol + + + Lido + +
+
+
+
+); + +/* ---- §8 Closing CTA + footer ---------------------------------------------- */ +const LClosing = () => ( +
+
+

Find out how secure your validator really is.

+
+ + Assess your validator + +
+
+ + Read the methodology + + · + + Explore valOS + +
+
+
+); +const LFooter = () => ( +
+
+ + Validator Beat + + + A simple view into validator operations + + + + Methodology + + + valOS + + + Assess + +
+
+); + +export function Landing() { + return ( +
+ + + + + + + + + + +
+ ); +} diff --git a/components/pizza/Pizza.tsx b/components/pizza/Pizza.tsx index 14c963e..8ef712c 100644 --- a/components/pizza/Pizza.tsx +++ b/components/pizza/Pizza.tsx @@ -46,6 +46,8 @@ export type PizzaProps = { labelScale?: number; showCenter?: boolean; stage?: Stage | null; + /** Wedge glow strength. Softer (e.g. 0.28) reads better on light surfaces. */ + glowOpacity?: number; }; export function Pizza({ @@ -58,6 +60,7 @@ export function Pizza({ labelScale = 1, showCenter = true, stage = null, + glowOpacity = 0.5, }: PizzaProps) { const big = size >= 120; const pop = big ? size * 0.022 : 0; @@ -115,7 +118,7 @@ export function Pizza({ dy="0" stdDeviation={size * 0.013} floodColor={PIZZA_FILL[c]} - floodOpacity="0.5" + floodOpacity={glowOpacity} /> ))} diff --git a/pages/_app.tsx b/pages/_app.tsx index 7dc44e8..02a0f31 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -4,6 +4,7 @@ import "@styles/colors_and_type.css"; import "@styles/obol-bridge.css"; import "@styles/pizza.css"; import "@styles/methodology.css"; +import "@styles/landing.css"; import { SITE_DESCRIPTION, SITE_NAME } from "@constants/index"; import type { AppProps } from "next/app"; import Head from "next/head"; diff --git a/pages/assess.tsx b/pages/assess.tsx new file mode 100644 index 0000000..7315452 --- /dev/null +++ b/pages/assess.tsx @@ -0,0 +1,14 @@ +import { AssessmentApp } from "@components/assessment/AssessmentApp"; +import type { GetStaticProps } from "next"; + +export default function AssessPage() { + return ; +} + +export const getStaticProps: GetStaticProps = () => ({ + props: { + title: "Self-assessment", + description: + "Six questions about your Ethereum validator setup. Score your resilience in about a minute — nothing is submitted or stored.", + }, +}); diff --git a/pages/index.tsx b/pages/index.tsx index a151268..a6a4860 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,14 +1,14 @@ -import { AssessmentApp } from "@components/assessment/AssessmentApp"; +import { Landing } from "@components/landing/Landing"; import type { GetStaticProps } from "next"; export default function HomePage() { - return ; + return ; } export const getStaticProps: GetStaticProps = () => ({ props: { - title: "Self-assessment", + title: "The standard for validator resilience", description: - "Six questions about your Ethereum validator setup. Score your resilience in about a minute — nothing is submitted or stored.", + "When you stake, you pick an operator — but you can't see how they run. Validator Beat makes validator resilience visible: two stages, six slices, one pizza.", }, }); diff --git a/pages/methodology.tsx b/pages/methodology.tsx index 39de130..34950ec 100644 --- a/pages/methodology.tsx +++ b/pages/methodology.tsx @@ -170,7 +170,7 @@ export default function MethodologyPage() {

- + Take the assessment →

diff --git a/styles/landing.css b/styles/landing.css new file mode 100644 index 0000000..c255467 --- /dev/null +++ b/styles/landing.css @@ -0,0 +1,340 @@ +/* ============================================================================ + Validator Beat — LANDING PAGE theme (light / cream, ecosystem-neutral). + Deliberately non-Obol coalition surface: warm cream paper, sage accents, + the shared pizza/stage visual language. Ported from the approved design + handoff. EVERYTHING is scoped under .vb-landing so the token overrides + (--vb-*, --fg-*, etc.) never leak into the dark-teal assessment app. + Dark mode (the app's existing palette) is mapped under [data-theme="dark"]. + ========================================================================== */ +.vb-landing { + /* fonts */ + --font-sans: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + + /* surfaces — warm cream */ + --paper: #f6f4ec; + --paper-2: #fffefa; + --band: #edf1e7; + /* ink ramp — near-black teal → muted */ + --ink-1: #0e1b1d; + --ink-2: #45565a; + --ink-3: #7d8a86; + /* hairlines */ + --line: #e6e1d1; + --line-2: #d8d2bf; + /* primary button — muted deep sage-green */ + --cta: #2c7a64; + --cta-press: #235f4e; + --cta-ink: #fff; + --green-ink: #20705c; + --mint: #2fe4ab; + --lime: #b6ea5c; + --shadow-card: 0 1px 2px rgba(20, 35, 28, 0.05), 0 10px 30px rgba(20, 45, 33, 0.07); + --shadow-cta: 0 2px 8px rgba(44, 122, 100, 0.24); + /* functional signal colors — muted earthy variants tuned for cream */ + --vb-green: #3a9e80; + --vb-yellow: #cf9a3a; + --vb-red: #c46044; + --vb-yellow-ink: #9a7012; + --vb-red-ink: #b0492f; + + /* pizza internals consumed by the reused Pizza component (cream-tuned) */ + --vb-plate: #fffdf7; + --vb-empty: #e9e5d8; + --vb-empty-stroke: #d2cbb8; + --vb-ink: #0e1b1d; + --theme-risk-green-ring: #1f7a5e; + --theme-risk-yellow-ring: #a8761f; + --theme-risk-red-ring: #a64428; + /* Pizza reads --fg-1 / --fg-3 for its wedge labels */ + --fg-1: #0e1b1d; + --fg-3: #5e6e69; + + min-height: 100vh; + background: var(--paper); + color: var(--ink-2); + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + line-height: 1.55; + scroll-behavior: smooth; +} +@media (prefers-reduced-motion: reduce) { + .vb-landing { scroll-behavior: auto; } +} + +.vb-landing .l-wrap { max-width: 1140px; margin: 0 auto; padding: 0 28px; } +.vb-landing .l-section { padding: 88px 0; } +.vb-landing .l-band { background: var(--band); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } + +/* ---- Type ----------------------------------------------------------------- */ +.vb-landing .l-eyebrow { font-size: 12px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--green-ink); margin: 0 0 16px; } +.vb-landing .l-h1 { font-size: 56px; line-height: 1.04; letter-spacing: -0.03em; font-weight: 700; color: var(--ink-1); margin: 0; text-wrap: balance; } + +/* "beat" — a heartbeat pulse cycling the three signal colors (green → yellow → red). */ +.vb-landing .l-beat { position: relative; display: inline-block; font-weight: 800; + isolation: isolate; /* contain the negative-z pulse above the section background */ + animation: l-beat-color 3s cubic-bezier(.16,1,.3,1) infinite; } +.vb-landing .l-beat__pulse { position: absolute; left: 50%; top: 54%; width: 1.55em; height: 1.55em; + transform: translate(-50%, -50%) scale(0.6); border-radius: 50%; + z-index: -1; opacity: 0; pointer-events: none; + animation: l-beat-ring 3s cubic-bezier(.16,1,.3,1) infinite; } +@keyframes l-beat-color { + 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); } +} +@keyframes l-beat-ring { + 0% { opacity: 0; transform: translate(-50%,-50%) scale(0.6); box-shadow: 0 0 0 0 var(--vb-green); } + 6% { opacity: .5; transform: translate(-50%,-50%) scale(0.92); box-shadow: 0 0 22px 5px var(--vb-green); } + 14% { opacity: .18; transform: translate(-50%,-50%) scale(1.04); } + 20% { opacity: .55; transform: translate(-50%,-50%) scale(1.0); box-shadow: 0 0 26px 7px var(--vb-green); } + 30% { opacity: 0; transform: translate(-50%,-50%) scale(1.12); box-shadow: 0 0 0 0 var(--vb-yellow); } + 39% { opacity: .5; transform: translate(-50%,-50%) scale(0.92); box-shadow: 0 0 22px 5px var(--vb-yellow); } + 47% { opacity: .18; transform: translate(-50%,-50%) scale(1.04); } + 53% { opacity: .55; transform: translate(-50%,-50%) scale(1.0); box-shadow: 0 0 26px 7px var(--vb-yellow); } + 63% { opacity: 0; transform: translate(-50%,-50%) scale(1.12); box-shadow: 0 0 0 0 var(--vb-red); } + 72% { opacity: .5; transform: translate(-50%,-50%) scale(0.92); box-shadow: 0 0 22px 5px var(--vb-red); } + 80% { opacity: .18; transform: translate(-50%,-50%) scale(1.04); } + 86% { opacity: .55; transform: translate(-50%,-50%) scale(1.0); box-shadow: 0 0 26px 7px var(--vb-red); } + 100% { opacity: 0; transform: translate(-50%,-50%) scale(1.12); box-shadow: 0 0 0 0 var(--vb-green); } +} +@media (prefers-reduced-motion: reduce) { + .vb-landing .l-beat { animation: none; color: var(--green-ink); } + .vb-landing .l-beat__pulse { animation: none; display: none; } +} +.vb-landing .l-h2 { font-size: 38px; line-height: 1.1; letter-spacing: -0.025em; font-weight: 700; color: var(--ink-1); margin: 0; text-wrap: balance; } +.vb-landing .l-h3 { font-size: 20px; line-height: 1.25; letter-spacing: -0.01em; font-weight: 700; color: var(--ink-1); margin: 0; } +.vb-landing .l-lede { font-size: 18px; line-height: 1.6; color: var(--ink-2); margin: 20px 0 0; text-wrap: pretty; } +.vb-landing .l-prose { font-size: 16.5px; line-height: 1.65; color: var(--ink-2); margin: 18px 0 0; text-wrap: pretty; } +.vb-landing .l-prose strong, .vb-landing .l-lede strong { color: var(--ink-1); font-weight: 600; } +.vb-landing .l-section__head { max-width: 760px; } + +/* ---- Buttons / links ------------------------------------------------------ */ +.vb-landing .lbtn { display: inline-flex; align-items: center; gap: 9px; font-family: var(--font-sans); font-size: 15px; font-weight: 600; border: 1px solid transparent; border-radius: 11px; padding: 13px 22px; cursor: pointer; text-decoration: none; white-space: nowrap; transition: background .15s, transform .15s, box-shadow .15s, border-color .15s; } +.vb-landing .lbtn--primary { background: var(--cta); color: var(--cta-ink); box-shadow: var(--shadow-cta); } +.vb-landing .lbtn--primary:hover { background: var(--cta-press); transform: translateY(-1px); } +.vb-landing .lbtn--lg { font-size: 16px; padding: 16px 28px; border-radius: 12px; } +.vb-landing .lbtn--ghost { background: transparent; color: var(--ink-1); border-color: var(--line-2); } +.vb-landing .lbtn--ghost:hover { background: var(--paper-2); border-color: var(--ink-3); } +.vb-landing .lbtn--onDark { background: #5cc6a6; color: #08110f; box-shadow: none; } +.vb-landing .lbtn--onDark:hover { background: #6fd4b6; transform: translateY(-1px); } +.vb-landing .llink { display: inline-flex; align-items: center; gap: 7px; font-size: 15px; font-weight: 600; color: var(--green-ink); text-decoration: none; cursor: pointer; } +.vb-landing .llink:hover { color: var(--cta-press); } +.vb-landing .llink svg { transition: transform .15s; } +.vb-landing .llink:hover svg { transform: translateX(3px); } +.vb-landing .l-ctarow { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; margin-top: 32px; } + +/* ---- Nav ------------------------------------------------------------------ */ +.vb-landing .l-nav { position: sticky; top: 0; z-index: 50; background: rgba(246, 244, 236, 0.82); backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); } +.vb-landing .l-nav__in { max-width: 1140px; margin: 0 auto; padding: 14px 28px; display: flex; align-items: center; gap: 14px; } +.vb-landing .l-brand { display: flex; align-items: center; gap: 9px; font-size: 16px; font-weight: 700; letter-spacing: -0.01em; color: var(--ink-1); text-decoration: none; } +.vb-landing .l-brand__beat { color: var(--green-ink); } +.vb-landing .l-brand__mark { width: 22px; height: 22px; flex-shrink: 0; } +.vb-landing .l-nav__spacer { margin-left: auto; } +.vb-landing .l-nav__links { display: flex; align-items: center; gap: 22px; } +.vb-landing .l-nav__link { font-size: 14px; font-weight: 500; color: var(--ink-2); text-decoration: none; } +.vb-landing .l-nav__link:hover { color: var(--ink-1); } +@media (max-width: 720px) { .vb-landing .l-nav__links { display: none; } } + +/* ---- §1 Hero -------------------------------------------------------------- */ +.vb-landing .l-hero { position: relative; overflow: hidden; } +.vb-landing .l-hero__grid { display: grid; grid-template-columns: 1.05fr 0.95fr; gap: 56px; align-items: center; padding: 76px 0 84px; } +.vb-landing .l-hero__pizzawrap { position: relative; display: flex; flex-direction: column; align-items: center; gap: 16px; } +.vb-landing .l-hero__glow { position: absolute; inset: -8% -6%; background: radial-gradient(circle at 50% 45%, rgba(58, 158, 128, 0.20), rgba(182, 234, 92, 0.07) 38%, transparent 66%); pointer-events: none; z-index: 0; } +.vb-landing .l-hero__pizza { position: relative; z-index: 1; } +.vb-landing .l-hero__cap { position: relative; z-index: 1; font-size: 13px; color: var(--ink-3); display: inline-flex; align-items: center; gap: 8px; } +.vb-landing .l-hero__cap b { color: var(--ink-2); font-weight: 600; } +.vb-landing .l-scroll { display: inline-flex; align-items: center; gap: 7px; font-size: 14px; font-weight: 600; color: var(--ink-2); text-decoration: none; } +.vb-landing .l-scroll:hover { color: var(--ink-1); } +.vb-landing .l-scroll svg { animation: lbob 1.8s ease-in-out infinite; } +@media (prefers-reduced-motion: reduce) { .vb-landing .l-scroll svg { animation: none; } } +@keyframes lbob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(3px); } } + +/* ---- §2 The gap + lineage ------------------------------------------------- */ +.vb-landing .l-lineage { display: flex; align-items: center; gap: 12px; margin-top: 36px; flex-wrap: wrap; } +.vb-landing .l-lin { display: inline-flex; align-items: center; gap: 9px; padding: 9px 15px; border-radius: 999px; border: 1px solid var(--line-2); background: var(--paper-2); font-size: 14px; font-weight: 600; color: var(--ink-3); } +.vb-landing .l-lin.is-now { color: var(--ink-1); border-color: var(--green-ink); background: rgba(12, 138, 111, 0.07); } +.vb-landing .l-lin__dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; opacity: .55; } +.vb-landing .l-lin.is-now .l-lin__dot { background: var(--green-ink); opacity: 1; } +.vb-landing .l-lineage__arrow { color: var(--ink-3); } +.vb-landing .l-pull { margin-top: 28px; font-size: 21px; line-height: 1.45; font-weight: 600; color: var(--ink-1); letter-spacing: -0.01em; max-width: 760px; text-wrap: pretty; } +.vb-landing .l-pull .hl { color: var(--green-ink); } + +/* ---- §3 Failure modes ----------------------------------------------------- */ +.vb-landing .l-cards2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 44px; } +.vb-landing .l-fcard { background: var(--paper-2); border: 1px solid var(--line); border-radius: 16px; padding: 28px; box-shadow: var(--shadow-card); } +.vb-landing .l-fcard__ico { width: 46px; height: 46px; border-radius: 12px; display: flex; align-items: center; justify-content: center; margin-bottom: 18px; } +.vb-landing .l-fcard__ico--safety { background: rgba(196, 96, 68, 0.12); color: var(--vb-red-ink); } +.vb-landing .l-fcard__ico--live { background: rgba(207, 154, 58, 0.15); color: var(--vb-yellow-ink); } +.vb-landing .l-fcard__tag { font-size: 12px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; margin: 0 0 6px; } +.vb-landing .l-fcard__tag--safety { color: var(--vb-red-ink); } +.vb-landing .l-fcard__tag--live { color: var(--vb-yellow-ink); } +.vb-landing .l-fcard__desc { font-size: 15.5px; line-height: 1.6; color: var(--ink-2); margin: 8px 0 0; } +.vb-landing .l-note { margin-top: 28px; font-size: 16px; line-height: 1.6; color: var(--ink-2); max-width: 820px; } +.vb-landing .l-note strong { color: var(--ink-1); font-weight: 600; } + +/* ---- §4 How VB reads an operator ------------------------------------------ */ +.vb-landing .l-stages { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 40px; } +.vb-landing .l-stagecard { border-radius: 16px; padding: 22px; border: 1px solid; } +.vb-landing .l-stagecard--0 { background: rgba(196, 96, 68, 0.09); border-color: rgba(196, 96, 68, 0.38); } +.vb-landing .l-stagecard--1 { background: rgba(207, 154, 58, 0.10); border-color: rgba(207, 154, 58, 0.42); } +.vb-landing .l-stagecard--2 { background: rgba(58, 158, 128, 0.11); border-color: rgba(44, 122, 100, 0.42); } +.vb-landing .l-stagecard__top { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; } +.vb-landing .l-stagebadge { width: 40px; height: 40px; border-radius: 11px; display: flex; align-items: center; justify-content: center; font-size: 17px; font-weight: 800; color: #fff; } +.vb-landing .l-stagebadge--0 { background: var(--vb-red-ink); } +.vb-landing .l-stagebadge--1 { background: #b8862a; } +.vb-landing .l-stagebadge--2 { background: var(--cta); } +.vb-landing .l-stagecard__name { font-size: 18px; font-weight: 700; color: var(--ink-1); } +.vb-landing .l-stagecard__kind { font-size: 13px; font-weight: 700; } +.vb-landing .l-stagecard--0 .l-stagecard__kind { color: var(--vb-red-ink); } +.vb-landing .l-stagecard--1 .l-stagecard__kind { color: var(--vb-yellow-ink); } +.vb-landing .l-stagecard--2 .l-stagecard__kind { color: var(--green-ink); } +.vb-landing .l-stagecard__desc { font-size: 15px; line-height: 1.55; color: var(--ink-2); margin: 0; } + +.vb-landing .l-read { display: grid; grid-template-columns: 360px 1fr; gap: 48px; align-items: center; margin-top: 40px; } +.vb-landing .l-read__pizza { display: flex; flex-direction: column; align-items: center; gap: 14px; } +.vb-landing .l-slices { display: grid; grid-template-columns: 1fr 1fr; gap: 16px 22px; } +.vb-landing .l-slice__h { display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 700; color: var(--ink-1); } +.vb-landing .l-slice__dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } +.vb-landing .l-slice__d { font-size: 13.5px; line-height: 1.5; color: var(--ink-3); margin: 4px 0 0; } +.vb-landing .l-read__foot { margin-top: 34px; font-size: 16px; line-height: 1.6; color: var(--ink-2); } +.vb-landing .l-read__dis { margin-top: 14px; font-size: 13.5px; font-style: italic; color: var(--ink-3); } + +/* ---- §5 Both sides -------------------------------------------------------- */ +.vb-landing .l-sides { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; margin-top: 44px; } +.vb-landing .l-side { background: var(--paper-2); border: 1px solid var(--line); border-radius: 16px; padding: 30px; box-shadow: var(--shadow-card); display: flex; flex-direction: column; } +.vb-landing .l-side__k { font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--green-ink); margin: 0 0 12px; } +.vb-landing .l-side__h { font-size: 22px; font-weight: 700; letter-spacing: -0.01em; color: var(--ink-1); margin: 0 0 12px; } +.vb-landing .l-side__p { font-size: 15.5px; line-height: 1.6; color: var(--ink-2); margin: 0; } +.vb-landing .l-side__cta { margin-top: 22px; } + +/* ---- §6 valOS ------------------------------------------------------------- */ +.vb-landing .l-valos { display: grid; grid-template-columns: 1fr 320px; gap: 48px; align-items: center; margin-top: 8px; } +.vb-landing .l-whohow { display: flex; flex-direction: column; gap: 12px; } +.vb-landing .l-whohow__box { border: 1px solid var(--line-2); border-radius: 14px; padding: 18px 20px; background: var(--paper-2); } +.vb-landing .l-whohow__k { font-size: 12px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-3); } +.vb-landing .l-whohow__v { font-size: 16px; font-weight: 600; color: var(--ink-1); margin-top: 4px; } +.vb-landing .l-whohow__v b { color: var(--green-ink); } +.vb-landing .l-whohow__arrow { display: flex; justify-content: center; color: var(--ink-3); } + +/* ---- §7 Neutral standard -------------------------------------------------- */ +.vb-landing .l-authors { display: flex; align-items: center; gap: 14px; margin-top: 30px; flex-wrap: wrap; } +.vb-landing .l-author { display: inline-flex; align-items: center; gap: 9px; padding: 11px 18px; border-radius: 12px; border: 1px solid var(--line-2); background: var(--paper-2); font-size: 15px; font-weight: 700; color: var(--ink-1); } +.vb-landing .l-author__dot { width: 9px; height: 9px; border-radius: 50%; background: var(--green-ink); } +.vb-landing .l-author__dot--lido { background: #00a3ff; } + +/* ---- §8 Closing CTA (deep evergreen brand band — intentional bookend) ----- */ +.vb-landing .l-closing { background: #14302a; border-top: 1px solid #1f463b; } +.vb-landing .l-closing .l-wrap { text-align: center; padding-top: 92px; padding-bottom: 92px; } +.vb-landing .l-closing__h { font-size: 40px; line-height: 1.12; letter-spacing: -0.025em; font-weight: 700; color: #f1f5f0; margin: 0 auto; max-width: 16ch; text-wrap: balance; } +.vb-landing .l-closing__row { display: flex; align-items: center; justify-content: center; gap: 22px; flex-wrap: wrap; margin-top: 34px; } +.vb-landing .l-closing__link { font-size: 15px; font-weight: 600; color: #9dc4b8; text-decoration: none; } +.vb-landing .l-closing__link:hover { color: #d8ebe3; } +.vb-landing .l-closing__sep { color: #3a5e54; } + +/* ---- Footer --------------------------------------------------------------- */ +.vb-landing .l-foot { background: #0f1a16; } +.vb-landing .l-foot .l-wrap { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; padding-top: 26px; padding-bottom: 26px; } +.vb-landing .l-foot__brand { font-size: 14px; font-weight: 700; color: #d2e2dc; } +.vb-landing .l-foot__brand b { color: #6fceac; } +.vb-landing .l-foot__note { font-size: 12.5px; color: #6a7e78; } +.vb-landing .l-foot__spacer { margin-left: auto; } +.vb-landing .l-foot__link { font-size: 13px; color: #93aaa3; text-decoration: none; } +.vb-landing .l-foot__link:hover { color: #d2e2dc; } + +/* ---- Reused Pizza tweaks (scoped so the app pizza is untouched) ------------ */ +.vb-landing .vbpizza__lbl { font-weight: 700; } + +/* Muted wedge fills, scoped to just the landing pizzas — overrides the signal + tokens for the SVG inside these wrappers only. The heartbeat colors and the + slice-legend dots keep the brighter set (they read better at small size). */ +.vb-landing .l-hero__pizza, +.vb-landing .l-read__pizza { + --vb-green: #4f8c78; + --vb-yellow: #c0974f; + --vb-red: #b46552; +} + +/* ---- Responsive ----------------------------------------------------------- */ +@media (max-width: 900px) { + .vb-landing .l-hero__grid { grid-template-columns: 1fr; gap: 40px; padding: 52px 0 64px; } + .vb-landing .l-hero__pizzawrap { order: -1; } + .vb-landing .l-read { grid-template-columns: 1fr; gap: 32px; } + .vb-landing .l-read__pizza { order: -1; } + .vb-landing .l-valos { grid-template-columns: 1fr; gap: 28px; } +} +@media (max-width: 760px) { + .vb-landing .l-section { padding: 62px 0; } + .vb-landing .l-h1 { font-size: 40px; } + .vb-landing .l-h2 { font-size: 30px; } + .vb-landing .l-cards2, .vb-landing .l-stages, .vb-landing .l-sides, .vb-landing .l-slices { grid-template-columns: 1fr; } + .vb-landing .l-closing__h { font-size: 30px; } +} + +/* ============================================================================ + DARK MODE — map the landing tokens to the assessment app's dark-teal palette + so the two surfaces feel cohesive in dark (they diverge only in light). + ========================================================================== */ +[data-theme="dark"] .vb-landing { + /* surfaces */ + --paper: #091011; + --paper-2: #111f22; + --band: #101c1f; + /* ink ramp */ + --ink-1: #e1e9eb; + --ink-2: #97b2b8; + --ink-3: #667a80; + /* hairlines */ + --line: #2d4d53; + --line-2: #3a5f66; + /* primary button — bright mint with dark ink */ + --cta: #2fe4ab; + --cta-press: #26c08f; + --cta-ink: #091011; + --green-ink: #2fe4ab; + --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.3), 0 10px 30px rgba(0, 0, 0, 0.35); + --shadow-cta: 0 2px 10px rgba(47, 228, 171, 0.22); + /* functional signal colors — bright set (matches the app) */ + --vb-green: #2fe4ab; + --vb-yellow: #e8b339; + --vb-red: #dd603c; + --vb-yellow-ink: #e8b339; + --vb-red-ink: #dd603c; + /* pizza internals — app dark */ + --vb-plate: #0a1214; + --vb-empty: #16282d; + --vb-empty-stroke: #2d4d53; + --vb-ink: #e1e9eb; + --theme-risk-green-ring: #6ff0c8; + --theme-risk-yellow-ring: #f0c95f; + --theme-risk-red-ring: #e8856a; + --fg-1: #e1e9eb; + --fg-3: #97b2b8; +} +/* the landing pizzas use the bright app set in dark (override the light muted scope) */ +[data-theme="dark"] .vb-landing .l-hero__pizza, +[data-theme="dark"] .vb-landing .l-read__pizza { + --vb-green: #2fe4ab; + --vb-yellow: #e8b339; + --vb-red: #dd603c; +} +/* sticky nav blur → dark translucent */ +[data-theme="dark"] .vb-landing .l-nav { background: rgba(9, 16, 17, 0.82); } +/* hero radial glow → cooler/dimmer on dark */ +[data-theme="dark"] .vb-landing .l-hero__glow { + background: radial-gradient(circle at 50% 45%, rgba(47, 228, 171, 0.16), rgba(47, 228, 171, 0.05) 38%, transparent 66%); +} + +/* ---- Theme toggle in the nav (re-themes via landing tokens) --------------- */ +.vb-landing .l-nav__toggle button { + color: var(--ink-2); + border-color: var(--line-2); + background: transparent; +} +.vb-landing .l-nav__toggle button:hover { + color: var(--ink-1); + background: var(--paper-2); +} From 8df1010a865b9259c065e6dd2968f88212c64b66 Mon Sep 17 00:00:00 2001 From: Kody Sale Date: Tue, 9 Jun 2026 17:51:30 -0700 Subject: [PATCH 2/3] Rename the Infrastructure slice to Provider. --- lib/rubric/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rubric/index.ts b/lib/rubric/index.ts index 806a924..83fa9b4 100644 --- a/lib/rubric/index.ts +++ b/lib/rubric/index.ts @@ -21,8 +21,8 @@ export const SLICES: SliceMeta[] = [ }, { id: "infraDiversity", - label: "Infrastructure", - short: "Infra", + label: "Provider", + short: "Provider", why: "One hosting provider's outage or compromise can take every validator hosted there with it.", }, { From 7d2d53373f403f7763a0958f48977693fa881db8 Mon Sep 17 00:00:00 2001 From: HananINouman Date: Wed, 10 Jun 2026 14:02:00 +0300 Subject: [PATCH 3/3] Refactor landing page to obol-ui and document styling conventions. Replace custom landing.css with Box/Text and shared assessment tokens, add Radix icons, and note future shared chrome UX plus obol-ui rules in README and CLAUDE.md. Co-authored-by: Cursor --- CLAUDE.md | 16 + README.md | 17 +- components/landing/Landing.tsx | 1238 +++++++++++++++++++------------- components/landing/icons.tsx | 38 + pages/_app.tsx | 1 - styles/landing.css | 340 --------- 6 files changed, 820 insertions(+), 830 deletions(-) create mode 100644 components/landing/icons.tsx delete mode 100644 styles/landing.css diff --git a/CLAUDE.md b/CLAUDE.md index fce858d..dd69717 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,6 +82,22 @@ Pre-commit (husky) runs `yarn test` + `yarn lint`. Lint-staged additionally runs - Copy lives close to the rubric: question text in `lib/assessment/questions.ts`, slice `why` and remediation tips in `lib/rubric/index.ts`. Methodology page reads from `SLICES` so descriptions stay in sync. - Styling is a hybrid: `@obolnetwork/obol-ui` Stitches components (see `components/assessment/stitches.ts`) for the assessment shell, plus plain CSS files in `styles/` (CSS custom properties in `theme-tokens.css` are the palette source of truth — if you change pizza slice colors there, also update `lib/theme/tokens.ts`). +### UI and styling (use obol-ui) + +**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. +- 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. + +**Exceptions (plain CSS or inline SVG is fine):** + +- **Complex animations** (keyframes, multi-step pulses) — e.g. a hero “heartbeat” effect; a small block in an existing global stylesheet or component-level `@keyframes` in `css` is OK if Stitches/obol-ui makes it awkward. +- **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. + ## Deferred work (don't build yet unless asked) `data/operators/`, `content/methodology/`, `components/operators/`, and `lib/schemas/` are placeholder dirs for the **v1.2 operator registry** (YAML profiles + Zod validation + a `/operators` summary table). `scripts/validate-operators.ts` and `scripts/import-survey-csv.ts` are scaffolding for that phase. v0.1 deliberately does not ship any of this — the `/operators` route currently redirects to `/`. diff --git a/README.md b/README.md index 9d58c98..5300a1f 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,26 @@ Every push to `main` runs CI and, on success, deploys the static `out/` folder t **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). +## Future UX: shared chrome across landing and assessment + +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.” + +**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. + ## v0.1 routes | Route | Purpose | |-------|---------| -| `/` | Self-assessment (intro → 6 slices → results) | +| `/` | Marketing landing page | +| `/assess/` | Self-assessment (intro → 6 slices → results) | | `/GYRYGG` | Open a shared result (729 static pages + OG preview image) | | `/GYRYGG?n=Kody%27s%20Cluster` | Same, with optional display name (not in link preview) | | `/methodology/` | Framework and stage definitions | diff --git a/components/landing/Landing.tsx b/components/landing/Landing.tsx index 433f0ce..0d20854 100644 --- a/components/landing/Landing.tsx +++ b/components/landing/Landing.tsx @@ -1,115 +1,334 @@ -import Link from "next/link"; +import { Box, Text } from "@obolnetwork/obol-ui"; +import NextLink from "next/link"; import { Pizza } from "@components/pizza/Pizza"; +import { + BrandAccent, + BrandLink, + Card, + Eyebrow, + HeroCard, + RiskDot, + TopNavLink, + TopSpacer, + risk, +} from "@components/assessment/stitches"; import { ThemeToggle } from "@components/assessment/ThemeToggle"; import type { Answers, SliceId } from "@lib/rubric/types"; - -/** - * Marketing landing page — the public entry point in front of the assessment. - * Ported from the approved design handoff (light/cream, ecosystem-neutral). - * Scoped under .vb-landing; reuses the shared Pizza component, which reads the - * cream-tuned CSS tokens defined in styles/landing.css. - */ +import type { CSS } from "@stitches/react"; +import { + IconArrowDown, + IconArrowRight, + IconExternalLink, + IconLiveness, + IconSafety, +} from "./icons"; const ASSESS = "/assess/"; const METHODOLOGY = "/methodology/"; const VALOS = "https://lidofinance.github.io/valos/valos-spec.html"; -/* muted signal hexes for the static slice dots (match the cream tokens) */ -const FUNC = { green: "#3a9e80", yellow: "#cf9a3a", red: "#c46044" } as const; - -/* ---- tiny line-icon set (single-color, currentColor) --------------------- */ -type IconProps = { size?: number; sw?: number; children?: React.ReactNode }; -const Icon = ({ size = 22, sw = 1.8, children }: IconProps) => ( - -); -const IArrowR = (p: IconProps) => ( - - - - -); -const IArrowD = (p: IconProps) => ( - - - - -); -const IExt = (p: IconProps) => ( - - - - - -); -const ISlash = (p: IconProps) => ( - - - - - -); -const IPulse = (p: IconProps) => ( - - - -); +const wrap: CSS = { maxWidth: 1140, margin: "0 auto", padding: "0 28px" }; +const section: CSS = { padding: "88px 0", "@media (max-width: 760px)": { padding: "62px 0" } }; +const sectionBand: CSS = { + backgroundColor: "$bg03", + borderTop: "1px solid $bg05", + borderBottom: "1px solid $bg05", +}; +const sectionHead: CSS = { maxWidth: 760 }; +const h1: CSS = { + fontSize: 56, + lineHeight: 1.04, + letterSpacing: "-0.03em", + fontWeight: "$bold", + color: "$body", + margin: 0, + textWrap: "balance", + "@media (max-width: 760px)": { fontSize: 40 }, +}; +const h2: CSS = { + fontSize: 38, + lineHeight: 1.1, + letterSpacing: "-0.025em", + fontWeight: "$bold", + color: "$body", + margin: 0, + textWrap: "balance", + "@media (max-width: 760px)": { fontSize: 30 }, +}; +const h3: CSS = { + fontSize: 20, + lineHeight: 1.25, + letterSpacing: "-0.01em", + fontWeight: "$bold", + color: "$body", + margin: 0, +}; +const lede: CSS = { + fontSize: "$4", + lineHeight: 1.6, + color: "$textMiddle", + marginTop: 20, + textWrap: "pretty", + "& strong": { color: "$body", fontWeight: "$semibold" }, +}; +const prose: CSS = { + fontSize: "$3", + lineHeight: 1.65, + color: "$textMiddle", + marginTop: 18, + textWrap: "pretty", + "& strong": { color: "$body", fontWeight: "$semibold" }, +}; +const pullQuote: CSS = { + marginTop: 28, + fontSize: 21, + lineHeight: 1.45, + fontWeight: "$semibold", + color: "$body", + letterSpacing: "-0.01em", + maxWidth: 760, + textWrap: "pretty", +}; +const ctaRow: CSS = { + display: "flex", + alignItems: "center", + gap: 18, + flexWrap: "wrap", + marginTop: 32, +}; +const primaryLink: 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)" }, +}; +const primaryLinkLg: CSS = { ...primaryLink, fontSize: "$4", padding: "14px 28px" }; +const ghostLink: CSS = { + display: "inline-flex", + alignItems: "center", + gap: 8, + fontSize: "$3", + fontWeight: "$semibold", + textDecoration: "none", + whiteSpace: "nowrap", + borderRadius: "$3", + padding: "12px 22px", + backgroundColor: "transparent", + color: "$body", + border: "1px solid $bg05", + "&:hover": { backgroundColor: "$bg04" }, +}; +const textLink: CSS = { + display: "inline-flex", + alignItems: "center", + gap: 6, + fontSize: "$3", + fontWeight: "$semibold", + color: "var(--theme-brand)", + textDecoration: "none", + "&:hover": { color: "var(--theme-brand-hover)" }, + "& svg": { transition: "transform 0.15s" }, + "&:hover svg": { transform: "translateX(3px)" }, +}; +const scrollLink: CSS = { + display: "inline-flex", + alignItems: "center", + gap: 6, + fontSize: "$2", + fontWeight: "$semibold", + color: "$textMiddle", + textDecoration: "none", + "&:hover": { color: "$body" }, +}; +const stickyNav: CSS = { + position: "sticky", + top: 0, + zIndex: 50, + borderBottom: "1px solid $bg05", + backgroundColor: "$bg01", + backdropFilter: "blur(10px)", +}; +const navInner: CSS = { + ...wrap, + display: "flex", + alignItems: "center", + gap: 14, + paddingTop: 14, + paddingBottom: 14, +}; +const navLinks: CSS = { + display: "flex", + alignItems: "center", + gap: 22, + "@media (max-width: 720px)": { display: "none" }, +}; +const heroGrid: CSS = { + display: "grid", + gridTemplateColumns: "1.05fr 0.95fr", + gap: 56, + alignItems: "center", + padding: "76px 0 84px", + "@media (max-width: 900px)": { gridTemplateColumns: "1fr", gap: 40, padding: "52px 0 64px" }, +}; +const heroPizzaCol: CSS = { + position: "relative", + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: 16, + "@media (max-width: 900px)": { order: -1 }, +}; +const cards2: CSS = { + display: "grid", + gridTemplateColumns: "1fr 1fr", + gap: 20, + marginTop: 44, + "@media (max-width: 760px)": { gridTemplateColumns: "1fr" }, +}; +const cardPad: CSS = { padding: 28 }; +const stagesGrid: CSS = { + display: "grid", + gridTemplateColumns: "repeat(3, 1fr)", + gap: 18, + marginTop: 40, + "@media (max-width: 760px)": { gridTemplateColumns: "1fr" }, +}; +const readGrid: CSS = { + display: "grid", + gridTemplateColumns: "360px 1fr", + gap: 48, + alignItems: "center", + marginTop: 40, + "@media (max-width: 900px)": { gridTemplateColumns: "1fr", gap: 32 }, +}; +const readPizzaCol: CSS = { + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: 14, + "@media (max-width: 900px)": { order: -1 }, +}; +const sliceGrid: CSS = { + display: "grid", + gridTemplateColumns: "1fr 1fr", + gap: "16px 22px", + "@media (max-width: 760px)": { gridTemplateColumns: "1fr" }, +}; +const sidesGrid: CSS = { + display: "grid", + gridTemplateColumns: "1fr 1fr", + gap: 22, + marginTop: 44, + "@media (max-width: 760px)": { gridTemplateColumns: "1fr" }, +}; +const valosGrid: CSS = { + display: "grid", + gridTemplateColumns: "1fr 320px", + gap: 48, + alignItems: "center", + marginTop: 8, + "@media (max-width: 900px)": { gridTemplateColumns: "1fr", gap: 28 }, +}; +const lineage: CSS = { + display: "flex", + alignItems: "center", + gap: 12, + marginTop: 36, + flexWrap: "wrap", +}; +const lineagePill: CSS = { + display: "inline-flex", + alignItems: "center", + gap: 9, + padding: "9px 15px", + borderRadius: "$pill", + border: "1px solid $bg05", + backgroundColor: "$bg02", + fontSize: "$2", + fontWeight: "$semibold", + color: "$textMiddle", +}; +const lineagePillActive: CSS = { + ...lineagePill, + color: "$body", + borderColor: "var(--theme-brand)", + backgroundColor: "$bg03", +}; +const failIcon: CSS = { + width: 46, + height: 46, + borderRadius: "$3", + display: "flex", + alignItems: "center", + justifyContent: "center", + marginBottom: 18, +}; -const Mark = () => ( - -); +const s = { + wrap, + section, + sectionBand, + sectionHead, + h1, + h2, + h3, + lede, + prose, + pullQuote, + ctaRow, + primaryLink, + primaryLinkLg, + ghostLink, + textLink, + scrollLink, + stickyNav, + navInner, + navLinks, + heroGrid, + heroPizzaCol, + cards2, + cardPad, + stagesGrid, + readGrid, + readPizzaCol, + sliceGrid, + sidesGrid, + valosGrid, + lineage, + lineagePill, + lineagePillActive, + failIcon, +}; -/* ---- §0 Nav --------------------------------------------------------------- */ -const LNav = () => ( - -); +function Mark() { + return ( + + ); +} -/* ---- §1 Hero -------------------------------------------------------------- */ const HERO_SAMPLE: Answers = { keyCustody: "green", clientDiversity: "green", @@ -118,138 +337,16 @@ const HERO_SAMPLE: Answers = { cpuDiversity: "red", geoDiversity: "yellow", }; -const LHero = () => ( -
-
-
-
-

A simple view into validator operations

-

- The validator ecosystem is missing its{" "} - - beat - . -

-

- When you stake ETH, you always (sometimes unknowingly) pick an operator. You{" "} - cannot see how they hold their keys, which clients they run, or how - correlated their setup is with everyone else securing the network. Validator Beat makes - that visible. -

-
- - Assess your validator - - - See how it works - -
-
-
-
-
- -
- - An example operator profile - -
-
-
-
-); -/* ---- §2 The gap ----------------------------------------------------------- */ -const LGap = () => ( -
-
-
-

The gap

-

- Transparency has been built into every layer, except the validators. -

-
-
- - L2Beat - - - - - - Walletbeat - - - - - - Validator Beat - -
-

- Rollups have L2Beat. Wallets have Walletbeat. The validator operators securing tens of - millions of staked ETH have had no way to differentiate themselves outside of performance - and reputation. Stakers, from ETH holders to institutional allocators, choose an operator or - protocol and forget about it. Compliance attestations like SOC 2 cover process and custody, - but they say nothing about the choices that actually determine whether a validator stays safe - and online: how signing keys are split, whether the operator runs a single client - implementation, how concentrated their infrastructure is, and how much of that overlaps with - the rest of the network. -

-

- 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. -

-
-
-); - -/* ---- §3 How validators actually fail -------------------------------------- */ -const LFail = () => ( -
-
-
-

Why it matters

-

How validators actually fail

-

- There are two ways a validator fails, and decentralization is the insurance against both. -

-
-
-
-
- -
-

Safety failure · slashing

-

It signs something it never should have.

-

- A double attestation, or a supermajority vote on the wrong chain. The protocol slashes - the stake. This is the expensive failure. -

-
-
-
- -
-

Liveness failure · downtime

-

It goes offline and misses its duties.

-

- No slashing, but no rewards either, and a weaker network. The validator simply isn't - there when it's needed. -

-
-
-

- Most safety and liveness issues trace back to a single point of failure: - one machine, one team member, one client, one provider, one region. Validator Beat measures - how many of those single points of failure an operator has removed. -

-
-
-); +const READ_SAMPLE: Answers = { + keyCustody: "green", + clientDiversity: "green", + infraDiversity: "yellow", + osDiversity: "green", + cpuDiversity: "green", + geoDiversity: "yellow", +}; -/* ---- §4 How VB reads an operator ------------------------------------------ */ const SLICE_DESC: Record = { keyCustody: "How signing keys are held, and how many independent parties must cooperate to sign.", @@ -257,9 +354,9 @@ const SLICE_DESC: Record = { infraDiversity: "Concentration on a single cloud or hosting provider.", osDiversity: "Correlated risk from running a single operating system across the fleet.", cpuDiversity: "Correlated risk from running a single chipset across the fleet.", - geoDiversity: - "Concentration in one region and exposure to power failure or natural disaster.", + geoDiversity: "Concentration in one region and exposure to power failure or natural disaster.", }; + const READ_LABEL: Record = { keyCustody: "Key Custody", clientDiversity: "Client Diversity", @@ -268,14 +365,7 @@ const READ_LABEL: Record = { cpuDiversity: "CPU Architecture", geoDiversity: "Geographic Diversity", }; -const READ_SAMPLE: Answers = { - keyCustody: "green", - clientDiversity: "green", - infraDiversity: "yellow", - osDiversity: "green", - cpuDiversity: "green", - geoDiversity: "yellow", -}; + const ORDER: SliceId[] = [ "keyCustody", "clientDiversity", @@ -284,260 +374,432 @@ const ORDER: SliceId[] = [ "osDiversity", "cpuDiversity", ]; -const LRead = () => ( -
-
-
-

The assessment

-

How Validator Beat assesses an operator

-

- Most operators start exposed with two stages to climb and a six-slice risk profile. -

-
-
-
-
- 0 -
-
Stage 0 — Exposed
-
Starting point
-
-
-

- At least one single point of failure remains. Most operators start here, it's the - baseline. -

-
-
-
- 1 -
-
Stage 1 — Slashing-averse
-
Safety
-
-
-

- No single compromise of one machine, one team member, or one signer can produce a - slashable message. -

-
-
-
- 2 -
-
Stage 2 — Downtime-averse
-
Liveness
-
-
-

- No single point of failure in the operator's infrastructure can take the validator - offline. This is the end game. -

-
-
-
-
- - - Each slice scored green,{" "} - yellow, or red - -
-
- {ORDER.map((id) => ( -
-
- {" "} - {READ_LABEL[id]} -
-

{SLICE_DESC[id]}

-
+ +const STAGES = [ + { + stage: "0" as const, + tone: "red" as const, + num: "0", + name: "Stage 0 — Exposed", + kind: "Starting point", + desc: "At least one single point of failure remains. Most operators start here — the baseline.", + }, + { + stage: "1" as const, + tone: "yellow" as const, + num: "1", + name: "Stage 1 — Slashing-averse", + kind: "Safety", + desc: "No single compromise of one machine, one team member, or one signer can produce a slashable message.", + }, + { + stage: "2" as const, + tone: "green" as const, + num: "2", + name: "Stage 2 — Downtime-averse", + kind: "Liveness", + desc: "No single point of failure in the operator's infrastructure can take the validator offline. This is the end game.", + }, +]; + +function LNav() { + return ( + + + + Validator Beat + + + + + How it works + + Methodology + + valOS + + + + + Assess your validator + + + + ); +} + +function LHero() { + return ( + + + + + + A simple view into validator operations + + + The validator ecosystem is missing its beat. + + + When you stake ETH, you always (sometimes unknowingly) pick an operator. You{" "} + cannot see how they hold their keys, which clients they run, or how + correlated their setup is with everyone else securing the network. Validator Beat makes + that visible. + + + + Assess your validator + + + See how it works + + + + + + + + An example operator profile + + + + + + + ); +} + +function LGap() { + return ( + + + + The gap + + Transparency has been built into every layer, except the validators. + + + + {["L2Beat", "Walletbeat"].map((name) => ( + + + + {name} + + + + + ))} -
-
-

- This can be read at a glance. For the exact thresholds and rubric, see the{" "} - - methodology - - . To see where your own validator lands,{" "} - - take the assessment - - . -

-

Validator Beat is self-reported and consent-based.

-
-
-); + + + Validator Beat + + + + Rollups have L2Beat. Wallets have Walletbeat. The validator operators securing tens of + millions of staked ETH have had no way to differentiate themselves outside of performance + and reputation. Stakers, from ETH holders to institutional allocators, choose an operator or + protocol and forget about it. Compliance attestations like SOC 2 cover process and custody, + but they say nothing about the choices that actually determine whether a validator stays safe + and online: how signing keys are split, whether the operator runs a single client + implementation, how concentrated their infrastructure is, and how much of that overlaps with + the rest of the network. + + + 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. + + + + ); +} -/* ---- §5 Both sides -------------------------------------------------------- */ -const LSides = () => ( -
-
-
-

Who it's for

-

Built for both sides of the stake

-
-
-
-

If you delegate your ETH

-

Read any operator's profile in five seconds.

-

- Before you stake, not after an incident. Two stages and six colors give you insight into - validator operations that the marketing page never will. -

-
-
-

If you're an operatooor

-

Proof of work.

-

- Run the assessment, earn your stages, and show off your pizza wherever you list your - product. Operators who have removed their single points of failure now have a way to show - it. -

-
- - Assess your validator - -
-
-
-
-
-); +function LFail() { + return ( + + + + Why it matters + How validators actually fail + + There are two ways a validator fails, and decentralization is the insurance against both. + + + + + + + + + Safety failure · slashing + + It signs something it never should have. + + A double attestation, or a supermajority vote on the wrong chain. The protocol slashes + the stake. This is the expensive failure. + + + + + + + + Liveness failure · downtime + + It goes offline and misses its duties. + + No slashing, but no rewards either, and a weaker network. The validator simply isn't + there when it's needed. + + + + + Most safety and liveness issues trace back to a single point of failure: + one machine, one team member, one client, one provider, one region. Validator Beat measures + how many of those single points of failure an operator has removed. + + + + ); +} + +function LRead() { + return ( + + + + The assessment + How Validator Beat assesses an operator + + Most operators start exposed with two stages to climb and a six-slice risk profile. + + + + {STAGES.map(({ tone, num, name, kind, desc }) => ( + + + + {num} + + + {name} + {kind} + + + {desc} + + ))} + + + + + + Each slice scored{" "} + green,{" "} + yellow, or{" "} + red + + + + {ORDER.map((id) => ( + + + + {READ_LABEL[id]} + + + {SLICE_DESC[id]} + + + ))} + + + + This can be read at a glance. For the exact thresholds and rubric, see the{" "} + methodology. To see where your + own validator lands,{" "} + + take the assessment + + . + + + Validator Beat is self-reported and consent-based. + + + + ); +} + +function LSides() { + return ( + + + + Who it's for + Built for both sides of the stake + + + + If you delegate your ETH + + Read any operator's profile in five seconds. + + + Before you stake, not after an incident. Two stages and six colors give you insight into + validator operations that the marketing page never will. + + + + If you're an operatooor + Proof of work. + + Run the assessment, earn your stages, and show off your pizza wherever you list your + product. Operators who have removed their single points of failure now have a way to show + it. + + + + Assess your validator + + + + + + + ); +} + +function LValos() { + return ( + + + + + The standard behind the score + Validator Beat and valOS + + Validator Beat is the public-facing who is running validators. valOS, + the Validator Operating Standard, is the technical how: a deep catalog + of the controls and mitigations behind professional validator operations. + + + A staker can quickly read an operator's stages and profile here. An operator doing + the hard work and implementing the mitigations should dig into valOS. Follow valOS, and + you'll end up at stage 2. + + + + Learn more about valOS + + + + + + Validator Beat + + The who — the public dashboard + + + + + + + valOS + + The how — the operating standard + + + + + + + ); +} -/* ---- §6 valOS ------------------------------------------------------------- */ -const LValos = () => ( -
-
-
-
-

The standard behind the score

-

Validator Beat and valOS

-

- Validator Beat is the public-facing who is running validators. valOS, - the Validator Operating Standard, is the technical how: a deep catalog - of the controls and mitigations behind professional validator operations. -

-

- A staker can quickly read an operator's stages and profile here. An operator doing - the hard work and implementing the mitigations should dig into valOS. Follow valOS, and - you'll end up at stage 2. -

- -
-
-
-
Validator Beat
-
- The who — the public dashboard -
-
-
- -
-
-
valOS
-
- The how — the operating standard -
-
-
-
-
-
-); +function LNeutral() { + return ( + + + + Credibility + A neutral dashboard, built in the open + + Validator Beat is co-authored by Obol and Lido. It is deliberately neutral and no single + team owns the rubric. The methodology is public and is meant to be adopted, challenged, + and improved by the whole ecosystem. + + + The goal is a race to the top, where transparency about validator risk + becomes the default expectation. + + + {[ + { name: "Obol", color: "var(--theme-brand)" }, + { name: "Lido", color: "#00a3ff" }, + ].map(({ name, color }) => ( + + + {name} + + ))} + + + + + ); +} -/* ---- §7 Neutral standard -------------------------------------------------- */ -const LNeutral = () => ( -
-
-
-

Credibility

-

A neutral dashboard, built in the open

-

- Validator Beat is co-authored by Obol and Lido. It is deliberately neutral and no single - team owns the rubric. The methodology is public and is meant to be adopted, challenged, - and improved by the whole ecosystem. -

-

- The goal is a race to the top, where transparency about validator risk - becomes the default expectation. -

-
- - Obol - - - Lido - -
-
-
-
-); +function LClosing() { + return ( + + + + Find out how secure your validator really is. + + + + Assess your validator + + + + Read the methodology + · + + Explore valOS + + + + + ); +} -/* ---- §8 Closing CTA + footer ---------------------------------------------- */ -const LClosing = () => ( -
-
-

Find out how secure your validator really is.

-
- - Assess your validator - -
-
- - Read the methodology - - · - - Explore valOS - -
-
-
-); -const LFooter = () => ( -
-
- - Validator Beat - - - A simple view into validator operations - - - - Methodology - - - valOS - - - Assess - -
-
-); +function LFooter() { + return ( + + + + Validator Beat + + + A simple view into validator operations + + + Methodology + + valOS + + Assess + + + ); +} export function Landing() { return ( -
+ @@ -548,6 +810,6 @@ export function Landing() { -
+
); } diff --git a/components/landing/icons.tsx b/components/landing/icons.tsx new file mode 100644 index 0000000..0cfd1c5 --- /dev/null +++ b/components/landing/icons.tsx @@ -0,0 +1,38 @@ +import { + ActivityLogIcon, + ArrowDownIcon, + ArrowRightIcon, + ExclamationTriangleIcon, + ExternalLinkIcon, +} from "@radix-ui/react-icons"; +import type { ComponentProps } from "react"; + +type IconSize = { size?: number }; + +const iconStyle = (size: number): ComponentProps<"svg">["style"] => ({ + width: size, + height: size, + flexShrink: 0, +}); + +export function IconArrowRight({ size = 16 }: IconSize) { + return ; +} + +export function IconArrowDown({ size = 16 }: IconSize) { + return ; +} + +export function IconExternalLink({ size = 16 }: IconSize) { + return ; +} + +/** Safety / slashing failure */ +export function IconSafety({ size = 24 }: IconSize) { + return ; +} + +/** Liveness / downtime */ +export function IconLiveness({ size = 24 }: IconSize) { + return ; +} diff --git a/pages/_app.tsx b/pages/_app.tsx index 02a0f31..7dc44e8 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -4,7 +4,6 @@ import "@styles/colors_and_type.css"; import "@styles/obol-bridge.css"; import "@styles/pizza.css"; import "@styles/methodology.css"; -import "@styles/landing.css"; import { SITE_DESCRIPTION, SITE_NAME } from "@constants/index"; import type { AppProps } from "next/app"; import Head from "next/head"; diff --git a/styles/landing.css b/styles/landing.css deleted file mode 100644 index c255467..0000000 --- a/styles/landing.css +++ /dev/null @@ -1,340 +0,0 @@ -/* ============================================================================ - Validator Beat — LANDING PAGE theme (light / cream, ecosystem-neutral). - Deliberately non-Obol coalition surface: warm cream paper, sage accents, - the shared pizza/stage visual language. Ported from the approved design - handoff. EVERYTHING is scoped under .vb-landing so the token overrides - (--vb-*, --fg-*, etc.) never leak into the dark-teal assessment app. - Dark mode (the app's existing palette) is mapped under [data-theme="dark"]. - ========================================================================== */ -.vb-landing { - /* fonts */ - --font-sans: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - - /* surfaces — warm cream */ - --paper: #f6f4ec; - --paper-2: #fffefa; - --band: #edf1e7; - /* ink ramp — near-black teal → muted */ - --ink-1: #0e1b1d; - --ink-2: #45565a; - --ink-3: #7d8a86; - /* hairlines */ - --line: #e6e1d1; - --line-2: #d8d2bf; - /* primary button — muted deep sage-green */ - --cta: #2c7a64; - --cta-press: #235f4e; - --cta-ink: #fff; - --green-ink: #20705c; - --mint: #2fe4ab; - --lime: #b6ea5c; - --shadow-card: 0 1px 2px rgba(20, 35, 28, 0.05), 0 10px 30px rgba(20, 45, 33, 0.07); - --shadow-cta: 0 2px 8px rgba(44, 122, 100, 0.24); - /* functional signal colors — muted earthy variants tuned for cream */ - --vb-green: #3a9e80; - --vb-yellow: #cf9a3a; - --vb-red: #c46044; - --vb-yellow-ink: #9a7012; - --vb-red-ink: #b0492f; - - /* pizza internals consumed by the reused Pizza component (cream-tuned) */ - --vb-plate: #fffdf7; - --vb-empty: #e9e5d8; - --vb-empty-stroke: #d2cbb8; - --vb-ink: #0e1b1d; - --theme-risk-green-ring: #1f7a5e; - --theme-risk-yellow-ring: #a8761f; - --theme-risk-red-ring: #a64428; - /* Pizza reads --fg-1 / --fg-3 for its wedge labels */ - --fg-1: #0e1b1d; - --fg-3: #5e6e69; - - min-height: 100vh; - background: var(--paper); - color: var(--ink-2); - font-family: var(--font-sans); - -webkit-font-smoothing: antialiased; - line-height: 1.55; - scroll-behavior: smooth; -} -@media (prefers-reduced-motion: reduce) { - .vb-landing { scroll-behavior: auto; } -} - -.vb-landing .l-wrap { max-width: 1140px; margin: 0 auto; padding: 0 28px; } -.vb-landing .l-section { padding: 88px 0; } -.vb-landing .l-band { background: var(--band); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } - -/* ---- Type ----------------------------------------------------------------- */ -.vb-landing .l-eyebrow { font-size: 12px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--green-ink); margin: 0 0 16px; } -.vb-landing .l-h1 { font-size: 56px; line-height: 1.04; letter-spacing: -0.03em; font-weight: 700; color: var(--ink-1); margin: 0; text-wrap: balance; } - -/* "beat" — a heartbeat pulse cycling the three signal colors (green → yellow → red). */ -.vb-landing .l-beat { position: relative; display: inline-block; font-weight: 800; - isolation: isolate; /* contain the negative-z pulse above the section background */ - animation: l-beat-color 3s cubic-bezier(.16,1,.3,1) infinite; } -.vb-landing .l-beat__pulse { position: absolute; left: 50%; top: 54%; width: 1.55em; height: 1.55em; - transform: translate(-50%, -50%) scale(0.6); border-radius: 50%; - z-index: -1; opacity: 0; pointer-events: none; - animation: l-beat-ring 3s cubic-bezier(.16,1,.3,1) infinite; } -@keyframes l-beat-color { - 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); } -} -@keyframes l-beat-ring { - 0% { opacity: 0; transform: translate(-50%,-50%) scale(0.6); box-shadow: 0 0 0 0 var(--vb-green); } - 6% { opacity: .5; transform: translate(-50%,-50%) scale(0.92); box-shadow: 0 0 22px 5px var(--vb-green); } - 14% { opacity: .18; transform: translate(-50%,-50%) scale(1.04); } - 20% { opacity: .55; transform: translate(-50%,-50%) scale(1.0); box-shadow: 0 0 26px 7px var(--vb-green); } - 30% { opacity: 0; transform: translate(-50%,-50%) scale(1.12); box-shadow: 0 0 0 0 var(--vb-yellow); } - 39% { opacity: .5; transform: translate(-50%,-50%) scale(0.92); box-shadow: 0 0 22px 5px var(--vb-yellow); } - 47% { opacity: .18; transform: translate(-50%,-50%) scale(1.04); } - 53% { opacity: .55; transform: translate(-50%,-50%) scale(1.0); box-shadow: 0 0 26px 7px var(--vb-yellow); } - 63% { opacity: 0; transform: translate(-50%,-50%) scale(1.12); box-shadow: 0 0 0 0 var(--vb-red); } - 72% { opacity: .5; transform: translate(-50%,-50%) scale(0.92); box-shadow: 0 0 22px 5px var(--vb-red); } - 80% { opacity: .18; transform: translate(-50%,-50%) scale(1.04); } - 86% { opacity: .55; transform: translate(-50%,-50%) scale(1.0); box-shadow: 0 0 26px 7px var(--vb-red); } - 100% { opacity: 0; transform: translate(-50%,-50%) scale(1.12); box-shadow: 0 0 0 0 var(--vb-green); } -} -@media (prefers-reduced-motion: reduce) { - .vb-landing .l-beat { animation: none; color: var(--green-ink); } - .vb-landing .l-beat__pulse { animation: none; display: none; } -} -.vb-landing .l-h2 { font-size: 38px; line-height: 1.1; letter-spacing: -0.025em; font-weight: 700; color: var(--ink-1); margin: 0; text-wrap: balance; } -.vb-landing .l-h3 { font-size: 20px; line-height: 1.25; letter-spacing: -0.01em; font-weight: 700; color: var(--ink-1); margin: 0; } -.vb-landing .l-lede { font-size: 18px; line-height: 1.6; color: var(--ink-2); margin: 20px 0 0; text-wrap: pretty; } -.vb-landing .l-prose { font-size: 16.5px; line-height: 1.65; color: var(--ink-2); margin: 18px 0 0; text-wrap: pretty; } -.vb-landing .l-prose strong, .vb-landing .l-lede strong { color: var(--ink-1); font-weight: 600; } -.vb-landing .l-section__head { max-width: 760px; } - -/* ---- Buttons / links ------------------------------------------------------ */ -.vb-landing .lbtn { display: inline-flex; align-items: center; gap: 9px; font-family: var(--font-sans); font-size: 15px; font-weight: 600; border: 1px solid transparent; border-radius: 11px; padding: 13px 22px; cursor: pointer; text-decoration: none; white-space: nowrap; transition: background .15s, transform .15s, box-shadow .15s, border-color .15s; } -.vb-landing .lbtn--primary { background: var(--cta); color: var(--cta-ink); box-shadow: var(--shadow-cta); } -.vb-landing .lbtn--primary:hover { background: var(--cta-press); transform: translateY(-1px); } -.vb-landing .lbtn--lg { font-size: 16px; padding: 16px 28px; border-radius: 12px; } -.vb-landing .lbtn--ghost { background: transparent; color: var(--ink-1); border-color: var(--line-2); } -.vb-landing .lbtn--ghost:hover { background: var(--paper-2); border-color: var(--ink-3); } -.vb-landing .lbtn--onDark { background: #5cc6a6; color: #08110f; box-shadow: none; } -.vb-landing .lbtn--onDark:hover { background: #6fd4b6; transform: translateY(-1px); } -.vb-landing .llink { display: inline-flex; align-items: center; gap: 7px; font-size: 15px; font-weight: 600; color: var(--green-ink); text-decoration: none; cursor: pointer; } -.vb-landing .llink:hover { color: var(--cta-press); } -.vb-landing .llink svg { transition: transform .15s; } -.vb-landing .llink:hover svg { transform: translateX(3px); } -.vb-landing .l-ctarow { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; margin-top: 32px; } - -/* ---- Nav ------------------------------------------------------------------ */ -.vb-landing .l-nav { position: sticky; top: 0; z-index: 50; background: rgba(246, 244, 236, 0.82); backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); } -.vb-landing .l-nav__in { max-width: 1140px; margin: 0 auto; padding: 14px 28px; display: flex; align-items: center; gap: 14px; } -.vb-landing .l-brand { display: flex; align-items: center; gap: 9px; font-size: 16px; font-weight: 700; letter-spacing: -0.01em; color: var(--ink-1); text-decoration: none; } -.vb-landing .l-brand__beat { color: var(--green-ink); } -.vb-landing .l-brand__mark { width: 22px; height: 22px; flex-shrink: 0; } -.vb-landing .l-nav__spacer { margin-left: auto; } -.vb-landing .l-nav__links { display: flex; align-items: center; gap: 22px; } -.vb-landing .l-nav__link { font-size: 14px; font-weight: 500; color: var(--ink-2); text-decoration: none; } -.vb-landing .l-nav__link:hover { color: var(--ink-1); } -@media (max-width: 720px) { .vb-landing .l-nav__links { display: none; } } - -/* ---- §1 Hero -------------------------------------------------------------- */ -.vb-landing .l-hero { position: relative; overflow: hidden; } -.vb-landing .l-hero__grid { display: grid; grid-template-columns: 1.05fr 0.95fr; gap: 56px; align-items: center; padding: 76px 0 84px; } -.vb-landing .l-hero__pizzawrap { position: relative; display: flex; flex-direction: column; align-items: center; gap: 16px; } -.vb-landing .l-hero__glow { position: absolute; inset: -8% -6%; background: radial-gradient(circle at 50% 45%, rgba(58, 158, 128, 0.20), rgba(182, 234, 92, 0.07) 38%, transparent 66%); pointer-events: none; z-index: 0; } -.vb-landing .l-hero__pizza { position: relative; z-index: 1; } -.vb-landing .l-hero__cap { position: relative; z-index: 1; font-size: 13px; color: var(--ink-3); display: inline-flex; align-items: center; gap: 8px; } -.vb-landing .l-hero__cap b { color: var(--ink-2); font-weight: 600; } -.vb-landing .l-scroll { display: inline-flex; align-items: center; gap: 7px; font-size: 14px; font-weight: 600; color: var(--ink-2); text-decoration: none; } -.vb-landing .l-scroll:hover { color: var(--ink-1); } -.vb-landing .l-scroll svg { animation: lbob 1.8s ease-in-out infinite; } -@media (prefers-reduced-motion: reduce) { .vb-landing .l-scroll svg { animation: none; } } -@keyframes lbob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(3px); } } - -/* ---- §2 The gap + lineage ------------------------------------------------- */ -.vb-landing .l-lineage { display: flex; align-items: center; gap: 12px; margin-top: 36px; flex-wrap: wrap; } -.vb-landing .l-lin { display: inline-flex; align-items: center; gap: 9px; padding: 9px 15px; border-radius: 999px; border: 1px solid var(--line-2); background: var(--paper-2); font-size: 14px; font-weight: 600; color: var(--ink-3); } -.vb-landing .l-lin.is-now { color: var(--ink-1); border-color: var(--green-ink); background: rgba(12, 138, 111, 0.07); } -.vb-landing .l-lin__dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; opacity: .55; } -.vb-landing .l-lin.is-now .l-lin__dot { background: var(--green-ink); opacity: 1; } -.vb-landing .l-lineage__arrow { color: var(--ink-3); } -.vb-landing .l-pull { margin-top: 28px; font-size: 21px; line-height: 1.45; font-weight: 600; color: var(--ink-1); letter-spacing: -0.01em; max-width: 760px; text-wrap: pretty; } -.vb-landing .l-pull .hl { color: var(--green-ink); } - -/* ---- §3 Failure modes ----------------------------------------------------- */ -.vb-landing .l-cards2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 44px; } -.vb-landing .l-fcard { background: var(--paper-2); border: 1px solid var(--line); border-radius: 16px; padding: 28px; box-shadow: var(--shadow-card); } -.vb-landing .l-fcard__ico { width: 46px; height: 46px; border-radius: 12px; display: flex; align-items: center; justify-content: center; margin-bottom: 18px; } -.vb-landing .l-fcard__ico--safety { background: rgba(196, 96, 68, 0.12); color: var(--vb-red-ink); } -.vb-landing .l-fcard__ico--live { background: rgba(207, 154, 58, 0.15); color: var(--vb-yellow-ink); } -.vb-landing .l-fcard__tag { font-size: 12px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; margin: 0 0 6px; } -.vb-landing .l-fcard__tag--safety { color: var(--vb-red-ink); } -.vb-landing .l-fcard__tag--live { color: var(--vb-yellow-ink); } -.vb-landing .l-fcard__desc { font-size: 15.5px; line-height: 1.6; color: var(--ink-2); margin: 8px 0 0; } -.vb-landing .l-note { margin-top: 28px; font-size: 16px; line-height: 1.6; color: var(--ink-2); max-width: 820px; } -.vb-landing .l-note strong { color: var(--ink-1); font-weight: 600; } - -/* ---- §4 How VB reads an operator ------------------------------------------ */ -.vb-landing .l-stages { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 40px; } -.vb-landing .l-stagecard { border-radius: 16px; padding: 22px; border: 1px solid; } -.vb-landing .l-stagecard--0 { background: rgba(196, 96, 68, 0.09); border-color: rgba(196, 96, 68, 0.38); } -.vb-landing .l-stagecard--1 { background: rgba(207, 154, 58, 0.10); border-color: rgba(207, 154, 58, 0.42); } -.vb-landing .l-stagecard--2 { background: rgba(58, 158, 128, 0.11); border-color: rgba(44, 122, 100, 0.42); } -.vb-landing .l-stagecard__top { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; } -.vb-landing .l-stagebadge { width: 40px; height: 40px; border-radius: 11px; display: flex; align-items: center; justify-content: center; font-size: 17px; font-weight: 800; color: #fff; } -.vb-landing .l-stagebadge--0 { background: var(--vb-red-ink); } -.vb-landing .l-stagebadge--1 { background: #b8862a; } -.vb-landing .l-stagebadge--2 { background: var(--cta); } -.vb-landing .l-stagecard__name { font-size: 18px; font-weight: 700; color: var(--ink-1); } -.vb-landing .l-stagecard__kind { font-size: 13px; font-weight: 700; } -.vb-landing .l-stagecard--0 .l-stagecard__kind { color: var(--vb-red-ink); } -.vb-landing .l-stagecard--1 .l-stagecard__kind { color: var(--vb-yellow-ink); } -.vb-landing .l-stagecard--2 .l-stagecard__kind { color: var(--green-ink); } -.vb-landing .l-stagecard__desc { font-size: 15px; line-height: 1.55; color: var(--ink-2); margin: 0; } - -.vb-landing .l-read { display: grid; grid-template-columns: 360px 1fr; gap: 48px; align-items: center; margin-top: 40px; } -.vb-landing .l-read__pizza { display: flex; flex-direction: column; align-items: center; gap: 14px; } -.vb-landing .l-slices { display: grid; grid-template-columns: 1fr 1fr; gap: 16px 22px; } -.vb-landing .l-slice__h { display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 700; color: var(--ink-1); } -.vb-landing .l-slice__dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } -.vb-landing .l-slice__d { font-size: 13.5px; line-height: 1.5; color: var(--ink-3); margin: 4px 0 0; } -.vb-landing .l-read__foot { margin-top: 34px; font-size: 16px; line-height: 1.6; color: var(--ink-2); } -.vb-landing .l-read__dis { margin-top: 14px; font-size: 13.5px; font-style: italic; color: var(--ink-3); } - -/* ---- §5 Both sides -------------------------------------------------------- */ -.vb-landing .l-sides { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; margin-top: 44px; } -.vb-landing .l-side { background: var(--paper-2); border: 1px solid var(--line); border-radius: 16px; padding: 30px; box-shadow: var(--shadow-card); display: flex; flex-direction: column; } -.vb-landing .l-side__k { font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--green-ink); margin: 0 0 12px; } -.vb-landing .l-side__h { font-size: 22px; font-weight: 700; letter-spacing: -0.01em; color: var(--ink-1); margin: 0 0 12px; } -.vb-landing .l-side__p { font-size: 15.5px; line-height: 1.6; color: var(--ink-2); margin: 0; } -.vb-landing .l-side__cta { margin-top: 22px; } - -/* ---- §6 valOS ------------------------------------------------------------- */ -.vb-landing .l-valos { display: grid; grid-template-columns: 1fr 320px; gap: 48px; align-items: center; margin-top: 8px; } -.vb-landing .l-whohow { display: flex; flex-direction: column; gap: 12px; } -.vb-landing .l-whohow__box { border: 1px solid var(--line-2); border-radius: 14px; padding: 18px 20px; background: var(--paper-2); } -.vb-landing .l-whohow__k { font-size: 12px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-3); } -.vb-landing .l-whohow__v { font-size: 16px; font-weight: 600; color: var(--ink-1); margin-top: 4px; } -.vb-landing .l-whohow__v b { color: var(--green-ink); } -.vb-landing .l-whohow__arrow { display: flex; justify-content: center; color: var(--ink-3); } - -/* ---- §7 Neutral standard -------------------------------------------------- */ -.vb-landing .l-authors { display: flex; align-items: center; gap: 14px; margin-top: 30px; flex-wrap: wrap; } -.vb-landing .l-author { display: inline-flex; align-items: center; gap: 9px; padding: 11px 18px; border-radius: 12px; border: 1px solid var(--line-2); background: var(--paper-2); font-size: 15px; font-weight: 700; color: var(--ink-1); } -.vb-landing .l-author__dot { width: 9px; height: 9px; border-radius: 50%; background: var(--green-ink); } -.vb-landing .l-author__dot--lido { background: #00a3ff; } - -/* ---- §8 Closing CTA (deep evergreen brand band — intentional bookend) ----- */ -.vb-landing .l-closing { background: #14302a; border-top: 1px solid #1f463b; } -.vb-landing .l-closing .l-wrap { text-align: center; padding-top: 92px; padding-bottom: 92px; } -.vb-landing .l-closing__h { font-size: 40px; line-height: 1.12; letter-spacing: -0.025em; font-weight: 700; color: #f1f5f0; margin: 0 auto; max-width: 16ch; text-wrap: balance; } -.vb-landing .l-closing__row { display: flex; align-items: center; justify-content: center; gap: 22px; flex-wrap: wrap; margin-top: 34px; } -.vb-landing .l-closing__link { font-size: 15px; font-weight: 600; color: #9dc4b8; text-decoration: none; } -.vb-landing .l-closing__link:hover { color: #d8ebe3; } -.vb-landing .l-closing__sep { color: #3a5e54; } - -/* ---- Footer --------------------------------------------------------------- */ -.vb-landing .l-foot { background: #0f1a16; } -.vb-landing .l-foot .l-wrap { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; padding-top: 26px; padding-bottom: 26px; } -.vb-landing .l-foot__brand { font-size: 14px; font-weight: 700; color: #d2e2dc; } -.vb-landing .l-foot__brand b { color: #6fceac; } -.vb-landing .l-foot__note { font-size: 12.5px; color: #6a7e78; } -.vb-landing .l-foot__spacer { margin-left: auto; } -.vb-landing .l-foot__link { font-size: 13px; color: #93aaa3; text-decoration: none; } -.vb-landing .l-foot__link:hover { color: #d2e2dc; } - -/* ---- Reused Pizza tweaks (scoped so the app pizza is untouched) ------------ */ -.vb-landing .vbpizza__lbl { font-weight: 700; } - -/* Muted wedge fills, scoped to just the landing pizzas — overrides the signal - tokens for the SVG inside these wrappers only. The heartbeat colors and the - slice-legend dots keep the brighter set (they read better at small size). */ -.vb-landing .l-hero__pizza, -.vb-landing .l-read__pizza { - --vb-green: #4f8c78; - --vb-yellow: #c0974f; - --vb-red: #b46552; -} - -/* ---- Responsive ----------------------------------------------------------- */ -@media (max-width: 900px) { - .vb-landing .l-hero__grid { grid-template-columns: 1fr; gap: 40px; padding: 52px 0 64px; } - .vb-landing .l-hero__pizzawrap { order: -1; } - .vb-landing .l-read { grid-template-columns: 1fr; gap: 32px; } - .vb-landing .l-read__pizza { order: -1; } - .vb-landing .l-valos { grid-template-columns: 1fr; gap: 28px; } -} -@media (max-width: 760px) { - .vb-landing .l-section { padding: 62px 0; } - .vb-landing .l-h1 { font-size: 40px; } - .vb-landing .l-h2 { font-size: 30px; } - .vb-landing .l-cards2, .vb-landing .l-stages, .vb-landing .l-sides, .vb-landing .l-slices { grid-template-columns: 1fr; } - .vb-landing .l-closing__h { font-size: 30px; } -} - -/* ============================================================================ - DARK MODE — map the landing tokens to the assessment app's dark-teal palette - so the two surfaces feel cohesive in dark (they diverge only in light). - ========================================================================== */ -[data-theme="dark"] .vb-landing { - /* surfaces */ - --paper: #091011; - --paper-2: #111f22; - --band: #101c1f; - /* ink ramp */ - --ink-1: #e1e9eb; - --ink-2: #97b2b8; - --ink-3: #667a80; - /* hairlines */ - --line: #2d4d53; - --line-2: #3a5f66; - /* primary button — bright mint with dark ink */ - --cta: #2fe4ab; - --cta-press: #26c08f; - --cta-ink: #091011; - --green-ink: #2fe4ab; - --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.3), 0 10px 30px rgba(0, 0, 0, 0.35); - --shadow-cta: 0 2px 10px rgba(47, 228, 171, 0.22); - /* functional signal colors — bright set (matches the app) */ - --vb-green: #2fe4ab; - --vb-yellow: #e8b339; - --vb-red: #dd603c; - --vb-yellow-ink: #e8b339; - --vb-red-ink: #dd603c; - /* pizza internals — app dark */ - --vb-plate: #0a1214; - --vb-empty: #16282d; - --vb-empty-stroke: #2d4d53; - --vb-ink: #e1e9eb; - --theme-risk-green-ring: #6ff0c8; - --theme-risk-yellow-ring: #f0c95f; - --theme-risk-red-ring: #e8856a; - --fg-1: #e1e9eb; - --fg-3: #97b2b8; -} -/* the landing pizzas use the bright app set in dark (override the light muted scope) */ -[data-theme="dark"] .vb-landing .l-hero__pizza, -[data-theme="dark"] .vb-landing .l-read__pizza { - --vb-green: #2fe4ab; - --vb-yellow: #e8b339; - --vb-red: #dd603c; -} -/* sticky nav blur → dark translucent */ -[data-theme="dark"] .vb-landing .l-nav { background: rgba(9, 16, 17, 0.82); } -/* hero radial glow → cooler/dimmer on dark */ -[data-theme="dark"] .vb-landing .l-hero__glow { - background: radial-gradient(circle at 50% 45%, rgba(47, 228, 171, 0.16), rgba(47, 228, 171, 0.05) 38%, transparent 66%); -} - -/* ---- Theme toggle in the nav (re-themes via landing tokens) --------------- */ -.vb-landing .l-nav__toggle button { - color: var(--ink-2); - border-color: var(--line-2); - background: transparent; -} -.vb-landing .l-nav__toggle button:hover { - color: var(--ink-1); - background: var(--paper-2); -}