From 103e8c0c4f7d900bdb5c77f63cf62e250f95d548 Mon Sep 17 00:00:00 2001 From: adamsoffer Date: Wed, 15 Apr 2026 09:42:57 -0400 Subject: [PATCH 01/21] =?UTF-8?q?feat(foundation):=20rebuild=20/foundation?= =?UTF-8?q?=20page=20as=20Linear=20Method=E2=80=93style=20essay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuilds the /foundation page from scratch using Linear Method's essay format as heavy inspiration. Copy is sourced verbatim from the doc brief (Steph Alinsug, April 2026): hero + four chapters (About The Network, About The Foundation, What We Do, The Team). Hero: - Serif display headline (Instrument Serif, added to lib/fonts.ts) paired with Favorit Pro for body - "Coordinating the ecosystem" centered in a full-viewport hero - Venn-diagram graphic below: two dashed overlapping circles with stroked gradient "shooting star" comets revolving around each, diagonal stripes in the lens intersection with a small inset - Concentric arc geometry (radius 49.9 in 100-unit viewBox) so the comets trace exactly along the dashed borders Chapters: - Each chapter has a numbered breadcrumb (01–04), serif chapter title, and (for 01–03) a thematic animated SVG banner sandwiched between the heading and body prose - 01 The Network: distributed dot-grid with connection pulses - 02 The Foundation: same grid with a ripple radiating from a centered anchor (visual echo to reinforce Foundation as in-service-of-network) - 03 What We Do: three streams converging on a shared focal point, viewBox widened to 800×200 so streams span the full text-column width while keeping circles perfectly round (preserveAspectRatio="xMinYMid meet") - 04 The Team: no graphic — the roster of names carries the section Nav + Footer: - Added Foundation to Resources dropdown in NAV_ITEMS (lib/constants.ts) - Added Foundation to the Resources column in the global Footer Internal link: - "Read the original announcement" now points to /blog/introducing- the-livepeer-foundation (internal) rather than external blog URL Other: - Resolved stray merge-conflict markers in pnpm-lock.yaml Co-Authored-By: Claude Opus 4.6 (1M context) --- app/foundation/layout.tsx | 12 +- app/foundation/page.tsx | 1767 ++++++++++++++-------------------- app/globals.css | 1 + app/layout.tsx | 4 +- components/layout/Footer.tsx | 1 + lib/constants.ts | 3 +- lib/fonts.ts | 12 +- pnpm-lock.yaml | 600 ------------ 8 files changed, 754 insertions(+), 1646 deletions(-) diff --git a/app/foundation/layout.tsx b/app/foundation/layout.tsx index 3d0d585..33bdbda 100644 --- a/app/foundation/layout.tsx +++ b/app/foundation/layout.tsx @@ -1,19 +1,19 @@ import type { Metadata } from "next"; +const description = + "The Livepeer Foundation exists to serve the Livepeer network \u2014 making it easier to build on, establishing it as open infrastructure for real-time AI video, and holding strategic decisions to the highest standard of accountability."; + export const metadata: Metadata = { title: "Foundation | Livepeer", - description: - "The Livepeer Foundation is a neutral, non-profit steward of the Livepeer protocol, coordinating long-term strategy, core development and ecosystem growth.", + description, openGraph: { title: "Foundation | Livepeer", - description: - "The Livepeer Foundation is a neutral, non-profit steward of the Livepeer protocol, coordinating long-term strategy, core development and ecosystem growth.", + description, }, twitter: { card: "summary_large_image", title: "Foundation | Livepeer", - description: - "The Livepeer Foundation is a neutral, non-profit steward of the Livepeer protocol, coordinating long-term strategy, core development and ecosystem growth.", + description, }, }; diff --git a/app/foundation/page.tsx b/app/foundation/page.tsx index fb1767e..90ca421 100644 --- a/app/foundation/page.tsx +++ b/app/foundation/page.tsx @@ -1,1153 +1,848 @@ "use client"; -import { useEffect, useRef, useState, useCallback } from "react"; -import { motion, useScroll, useTransform } from "framer-motion"; +import { motion } from "framer-motion"; import Container from "@/components/ui/Container"; -import SectionHeader from "@/components/ui/SectionHeader"; const fadeUp = { hidden: { opacity: 0, y: 24 }, visible: { opacity: 1, y: 0 }, }; -const PILLARS = [ +const WORK = [ { - num: "01", - title: "Strategic Alignment", - description: - "Co-creating the long-term, shared vision with key stakeholders across the Livepeer ecosystem.", + label: "Strategy", + text: "We set the strategic direction of the network and align stakeholders around a long-term roadmap of priority work.", }, { - num: "02", - title: "Core Development", - description: - "Coordinating research, development, and upgrades that strengthen the Livepeer network and protocol.", + label: "Coordination", + text: "We coordinate technical development across independent teams building on and for the network.", }, { - num: "03", - title: "Ecosystem Growth", - description: - "Driving awareness of the Livepeer project and onboarding new developers and companies to drive demand to the network.", + label: "Support", + text: "We support builders through funding, connections and tools that lower the barrier to building.", }, ]; -const VALUES = [ - { - title: "Transparency & Accountability", - description: - "Our funding, decisions and deliverables follow open processes with clear reporting, milestones and public oversight.", - }, - { - title: "Efficiency & Execution", - description: - "We drive forward priority work with focus, discipline and speed to fuel network demand and innovation.", - }, - { - title: "Co-creation & Inclusivity", - description: - "We\u2019re a vehicle for ecosystem participation that co-creates the future of the network through community inclusion and open feedback cycles.", - }, +const TEAM = [ + { name: "Steph Alinsug", role: "Narrative" }, + { name: "Ben Perez", role: "Operations" }, + { name: "Rick Staa", role: "Technical" }, + { name: "Rich O'Grady", role: "Ecosystem & Trust" }, + { name: "Mehrdad Sadeghi", role: "Ops Engineer" }, + { name: "Joe Birch", role: "Storyteller" }, ]; -/* ------------------------------------------------------------------ */ -/* Card illustrations (bold B&W, Linear-inspired) */ -/* ------------------------------------------------------------------ */ - -function IllustrationStrategy() { - // Compass / convergence — multiple arrows pointing inward to a center target +/* ================================================================== */ +/* Hero graphic — Venn-style overlapping circles, modeled exactly on */ +/* Linear Method's graphic. */ +/* Two dashed-border circle DIVs overlap. Inside each, a "spinner" */ +/* div rotates 10s linear, carrying a gradient comet SVG at the top */ +/* edge. The right circle's container is rotated 180° so the two */ +/* comets are 180° out of phase. Stripes fill the lens intersection. */ +/* ================================================================== */ + +function CometSvg() { + // Concentric 50° arc centered at the viewBox center. The containing + // spinner is extended via `inset: -1px` so the SVG's viewBox maps to + // the FULL circle (including its 1px border area), not just the inner + // content box. This lets the arc radius of 49.9 land directly on the + // dashed border (at ~49.91 viewBox units from center), so the comet + // traces along the arc edge exactly rather than floating inside. + // + // Arc from angle 270° (top) to angle 320° (50° clockwise). + // Start: (50 + 49.9·cos(270°), 50 + 49.9·sin(270°)) = (50, 0.1) + // End: (50 + 49.9·cos(320°), 50 + 49.9·sin(320°)) = (88.23, 17.92) return ( ); } -function IllustrationDevelopment() { - // Layered blocks with code-like horizontal lines inside +function HeroVenn() { + // Geometry: + // D = circle diameter = min(40vw, 560px) + // Circles overlap by 42% of D (each translated 21% inward) + // Bounding width = 2D - 0.42D = 1.58D + // Inside the bounds, circles sit against the left and right edges. return ( - +
+
+ {/* Left circle — anchored to left edge of bounds */} +
+
+ +
+
+ + {/* Right circle — anchored to right edge, rotated 180° so its + comet is phase-offset */} +
+
+ +
+
+ + {/* Stripes inside the lens intersection */} + +
+ + +
); } -function IllustrationGrowth() { - // Expanding network — central node with radiating connections and outer nodes +// Diagonal stripes clipped to the lens = intersection of the two circles. +// Uses nested SVG clipPaths (inside-both) to isolate the lens region. +function HeroStripes() { + // viewBox 158 × 100 matches the 1.58:1 bounding box. Each circle has + // diameter 100 and radius 50. Left circle center (50, 50), right (108, 50). return ( ); } -function IllustrationTransparency() { - // Open window / magnifying lens with visible grid and data points +/* ================================================================== */ +/* Chapter graphics — wide rectangular SVG banners */ +/* Designed to match the width of body text and sit tight between */ +/* the heading and the prose. Inspired by Linear Method. */ +/* ================================================================== */ + +// 01 — Network: distributed grid of nodes with connection pulses +function NetworkGraphic() { + // Build an 11 × 5 lattice of node positions (rectangular, wide). + const cols = 11; + const rows = 5; + const gapX = 68; + const gapY = 40; + const nodes: { x: number; y: number }[] = []; + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + nodes.push({ + x: -gapX * (cols - 1) / 2 + c * gapX, + y: -gapY * (rows - 1) / 2 + r * gapY, + }); + } + } + // Pick a scattered set of nodes to highlight (green) + const highlighted = new Set([12, 27, 38, 49, 14, 33, 5, 42]); + // Pick paths to animate as connections + const connections: Array<[number, number, number]> = [ + // [fromIndex, toIndex, animationDelay] + [12, 27, 0], + [27, 38, 1.4], + [38, 49, 2.8], + [5, 14, 0.6], + [14, 33, 2.0], + [33, 42, 3.4], + ]; return ( ); } -function IllustrationEfficiency() { - // Funnel / pipeline narrowing from many inputs to focused output +// 02 — Foundation: rectangular grid of dots with a ripple-wave animation +// that sweeps outward from the center. Each dot lights up green as the +// wave passes through its radius. Fills the full rectangle. +function FoundationGraphic() { + const cols = 21; + const rows = 7; + const gapX = 34; + const gapY = 24; + const totalW = (cols - 1) * gapX; + const totalH = (rows - 1) * gapY; + + type Dot = { x: number; y: number; delay: number }; + const dots: Dot[] = []; + // Find max distance to normalize the radial delay + const cx = 0; + const cy = 0; + let maxDist = 0; + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const x = -totalW / 2 + c * gapX; + const y = -totalH / 2 + r * gapY; + const d = Math.hypot(x - cx, y - cy); + if (d > maxDist) maxDist = d; + } + } + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const x = -totalW / 2 + c * gapX; + const y = -totalH / 2 + r * gapY; + const d = Math.hypot(x - cx, y - cy); + // Closer dots light up first; delay proportional to distance + const delay = (d / maxDist) * 2.5; // 0..2.5s + dots.push({ x, y, delay }); + } + } + return ( ); } -function IllustrationCocreation() { - // Three overlapping circles with people-like dots forming a collaborative pattern +// 03 — Work: three horizontal streams with dashed paths, bright animated +// comet pulses flowing left-to-right, and a shared focal convergence point +// on the right. Represents Strategy / Coordination / Support working in +// parallel toward a common outcome. Left-aligned, bold, continuously alive. +function WorkGraphic() { + const streams = [ + { label: "Strategy", y: -62, delay: 0 }, + { label: "Coordination", y: 0, delay: 0.55 }, + { label: "Support", y: 62, delay: 1.1 }, + ]; + const startX = 0; + const pathEndX = 700; + const focalX = 760; + return ( ); } -const PILLAR_ILLUSTRATIONS = [ - IllustrationStrategy, - IllustrationDevelopment, - IllustrationGrowth, -]; -const VALUE_ILLUSTRATIONS = [ - IllustrationTransparency, - IllustrationEfficiency, - IllustrationCocreation, -]; - -/* ------------------------------------------------------------------ */ -/* Scroll-opacity mission text */ -/* ------------------------------------------------------------------ */ - -const MISSION_TEXT: { - word: string; - bold?: boolean; - color?: string; - num?: string; -}[] = [ - { word: "Drive" }, - { word: "the" }, - { word: "strategy,", bold: true, color: "text-gradient", num: "01" }, - { word: "core", bold: true, color: "text-gradient", num: "02" }, - { word: "development", bold: true, color: "text-gradient" }, - { word: "and" }, - { word: "ecosystem", bold: true, color: "text-gradient", num: "03" }, - { word: "growth", bold: true, color: "text-gradient" }, - { word: "of" }, - { word: "the" }, - { word: "Livepeer" }, - { word: "project." }, -]; +/* ================================================================== */ +/* Chapter header — number / crumb + serif title + optional graphic */ +/* Graphic sandwiched tight between heading and body text. */ +/* ================================================================== */ -function ScrollRevealMission() { - const containerRef = useRef(null); - const { scrollYProgress } = useScroll({ - target: containerRef, - offset: ["start 0.9", "start 0.25"], - }); - - return ( -
-

- {MISSION_TEXT.map((item, i) => ( - - ))} -

-
- ); -} - -function ScrollWord({ - word, - bold, - color, +function ChapterHeader({ num, - index, - total, - progress, + crumb, + title, + graphic, + graphicAlign = "full", }: { - word: string; - bold?: boolean; - color?: string; - num?: string; - index: number; - total: number; - progress: ReturnType["scrollYProgress"]; + num: string; + crumb: string; + title: string; + graphic?: React.ReactNode; + graphicAlign?: "full" | "left"; }) { - const start = index / total; - const end = start + 1 / total; - const opacity = useTransform(progress, [start, end], [0.15, 1]); - return ( - - {num && ( - - {num} - - )} - {word}{" "} - - ); -} - -/* ------------------------------------------------------------------ */ -/* Animated grain canvas (like Linear's GrainCanvas) */ -/* ------------------------------------------------------------------ */ - -function GrainCanvas() { - const canvasRef = useRef(null); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - const ctx = canvas.getContext("2d"); - if (!ctx) return; - - let raf: number; - - const resize = () => { - const dpr = window.devicePixelRatio || 1; - const rect = canvas.parentElement?.getBoundingClientRect(); - if (!rect) return; - canvas.width = rect.width * dpr; - canvas.height = rect.height * dpr; - }; - - resize(); - window.addEventListener("resize", resize); - - const frame = () => { - const w = canvas.width; - const h = canvas.height; - if (w === 0 || h === 0) { - raf = requestAnimationFrame(frame); - return; - } - const imageData = ctx.createImageData(w, h); - const d = imageData.data; - for (let i = 0; i < d.length; i += 4) { - const v = (Math.random() * 255) | 0; - d[i] = v; - d[i + 1] = v; - d[i + 2] = v; - d[i + 3] = 255; - } - ctx.putImageData(imageData, 0, 0); - raf = requestAnimationFrame(frame); - }; - - raf = requestAnimationFrame(frame); - return () => { - cancelAnimationFrame(raf); - window.removeEventListener("resize", resize); - }; - }, []); - - return ( -