)
}
-function AppPasswordIllustration() {
- const items = [
+function CommunityVoteUI() {
+ const steps = [
{
- label: "App-password only",
- icon: (
-
- ),
+ label: "Proposal",
+ text: "A change to ranking weights or rules is made visible before it affects the feed.",
},
{
- label: "Revoke anytime, one click",
- icon: (
- <>
-
- >
- ),
+ label: "Vote",
+ text: "Community votes are aggregated into the next set of governance weights.",
},
{
- label: "No main password, ever",
- icon: (
-
- ),
+ label: "Epoch transition",
+ text: "The feed applies the new weights at the boundary instead of changing silently midstream.",
+ },
+ {
+ label: "Receipt",
+ text: "Weights and post-score explanations remain inspectable after the change.",
},
]
+
return (
-
@@ -84,7 +93,7 @@ export function FooterSection() {
{/* Copyright bar */}
- © 2025 Corgi. Built on Bluesky.
+ © 2026 Corgi. Built on Bluesky.
Open source · No black box
diff --git a/web-next/components/get-started-section.tsx b/web-next/components/get-started-section.tsx
index f15f329d..737a0420 100644
--- a/web-next/components/get-started-section.tsx
+++ b/web-next/components/get-started-section.tsx
@@ -1,59 +1,63 @@
-import Link from "next/link"
-import { Button } from "@/components/ui/button"
+import { DemoCTA, SignInCTA } from "@/components/landing-ctas"
const steps = [
{
number: "01",
- heading: "Connect your Bluesky account",
- body: "Generate an app-password in your Bluesky settings and paste it into Corgi. Your main password is never touched.",
+ heading: "Explore the reviewer demo",
+ body: "Start with the read-only demo: live snapshot totals, anonymized receipt rows, and the ranking explanation without signing in.",
},
{
number: "02",
- heading: "Join or start a community feed",
- body: "Find an existing Corgi feed for your community, or create a new one. Invite members with a single link.",
+ heading: "Inspect how ranking works",
+ body: "Follow the weight, score, epoch, and receipt views before trusting the feed. The public path is built for review first.",
},
{
number: "03",
- heading: "Cast your first vote",
- body: "Vote on how the current weights should change. Every vote counts equally. The feed reranks at the end of the epoch.",
+ heading: "Sign in when ready",
+ body: "Use a Bluesky app password only when you want to participate. Your main password stays outside Corgi.",
},
]
export function GetStartedSection() {
return (
-
-
-
- Up and running in minutes
-
-
- No code required. No waiting list. Just a Bluesky account.
-
+
+
+
+
+ Start with the read-only demo.
+
+
+
+
+ Inspect the live snapshot and receipt trail first. Account connection stays available for participation, but the reviewer path does not depend on it.
+
+
-
- {/* Connector line spanning all three cards on desktop */}
-
-
- {steps.map((step) => (
-
-
- {step.number}
-
-
-
{step.heading}
-
{step.body}
-
+
+ {steps.map((step, i) => (
+
0 ? "md:pl-10" : ""} ${i < steps.length - 1 ? "md:pr-10" : ""}`}
+ >
+
{step.number}
+
+
{step.heading}
+
{step.body}
- ))}
-
+
+ ))}
-
-
-
+
+
+
+
No account needed for the reviewer demo.
+
)
}
diff --git a/web-next/components/hero-section.tsx b/web-next/components/hero-section.tsx
index 28eb602d..a81a7931 100644
--- a/web-next/components/hero-section.tsx
+++ b/web-next/components/hero-section.tsx
@@ -2,38 +2,39 @@ import React from "react"
import { Button } from "@/components/ui/button"
import Link from "next/link"
import { LIVE_METRICS_SNAPSHOT } from "@/lib/live-metrics-snapshot"
+import { DemoCTA } from "@/components/landing-ctas"
export function HeroSection() {
return (
{/* SVG Background — warm cream grid */}
@@ -124,7 +124,7 @@ export function HeroSection() {
{/* Eyebrow — GitHub star badge */}
- Star us
+ View code
{/* Hero copy */}
@@ -153,11 +153,7 @@ export function HeroSection() {
{/* CTAs */}
-
-
-
+
{/* Trust line */}
- Free · App-password secure · Leave anytime
+ Read-only demo first · account connection stays optional
Live snapshot, {LIVE_METRICS_SNAPSHOT.collectedAtLabel}: {LIVE_METRICS_SNAPSHOT.scoredPosts.toLocaleString("en-US")} scored posts · {LIVE_METRICS_SNAPSHOT.uniqueAuthors.toLocaleString("en-US")} authors · epoch {LIVE_METRICS_SNAPSHOT.epochId} active
diff --git a/web-next/components/landing-ctas.tsx b/web-next/components/landing-ctas.tsx
new file mode 100644
index 00000000..4a420f22
--- /dev/null
+++ b/web-next/components/landing-ctas.tsx
@@ -0,0 +1,40 @@
+import Link from "next/link"
+import { Button } from "@/components/ui/button"
+import { cn } from "@/lib/utils"
+
+interface LandingCTAProps {
+ className?: string
+}
+
+export function DemoCTA({ className }: LandingCTAProps) {
+ return (
+
+ )
+}
+
+export function SignInCTA({ className }: LandingCTAProps) {
+ return (
+
+ )
+}
diff --git a/web-next/components/sign-in-dialog.tsx b/web-next/components/sign-in-dialog.tsx
index a1e95aea..9fd63ae9 100644
--- a/web-next/components/sign-in-dialog.tsx
+++ b/web-next/components/sign-in-dialog.tsx
@@ -2,7 +2,8 @@
import { useEffect, useRef, useState } from "react"
import Image from "next/image"
-import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
+import Link from "next/link"
+import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
@@ -76,9 +77,9 @@ export function SignInDialog({ open, onOpenChange }: SignInDialogProps) {
Sign in to vote
-
+
Connect your Bluesky account to participate in feed governance. You'll need an app password from your Bluesky settings.
-
+
@@ -154,13 +155,13 @@ export function SignInDialog({ open, onOpenChange }: SignInDialogProps) {
By signing in, you agree to our{" "}
-
+
Terms of Service
- {" "}
+ {" "}
and{" "}
-
+
Privacy Policy
-
+
.
diff --git a/web-next/components/social-proof.tsx b/web-next/components/social-proof.tsx
index 3f83dfcc..bc935938 100644
--- a/web-next/components/social-proof.tsx
+++ b/web-next/components/social-proof.tsx
@@ -10,10 +10,10 @@ const pillars = [
),
- heading: "MIT licensed",
- body: "Read every line. Fork it. Self-host it. The code is yours.",
- href: "https://github.com/corgi-feed/corgi",
- linkLabel: "View on GitHub",
+ heading: "Open source",
+ body: "Read the implementation behind the feed, governance routes, and receipt surfaces.",
+ href: "https://github.com/andrewnordstrom-eng/bluesky-community-feed",
+ linkLabel: "View source",
},
{
icon: (
@@ -22,10 +22,10 @@ const pillars = [
),
- heading: "Self-hostable",
- body: "Run Corgi on your own infrastructure. No dependency on our servers.",
- href: "https://github.com/corgi-feed/corgi#self-hosting",
- linkLabel: "Hosting docs",
+ heading: "Reviewer-safe",
+ body: "Inspect the public demo without connecting an account or exposing raw production identifiers.",
+ href: "/demo",
+ linkLabel: "Open demo",
},
{
icon: (
@@ -35,10 +35,10 @@ const pillars = [
),
- heading: "Community-built",
- body: "PRs welcome. If something bothers you about the algorithm, you can fix it.",
- href: "https://github.com/corgi-feed/corgi/contribute",
- linkLabel: "Contribute",
+ heading: "Receipt-first",
+ body: "Public copy stays tied to anonymized score receipts, live snapshot totals, and auditable epochs.",
+ href: "#features-section",
+ linkLabel: "See receipts",
},
]
@@ -60,14 +60,23 @@ export function SocialProof() {
{p.heading}
{p.body}
-
- {p.linkLabel} →
-
+ {p.href.startsWith("https://") ? (
+
+ {p.linkLabel} →
+
+ ) : (
+
+ {p.linkLabel} →
+
+ )}
))}
diff --git a/web-next/lib/percent.ts b/web-next/lib/percent.ts
new file mode 100644
index 00000000..ea64e496
--- /dev/null
+++ b/web-next/lib/percent.ts
@@ -0,0 +1,27 @@
+export function clampPercentValue(value: number): number {
+ if (!Number.isFinite(value)) {
+ return 0
+ }
+
+ return Math.min(100, Math.max(0, Math.round(value)))
+}
+
+export function unitIntervalToPercentValue(value: number): number {
+ if (!Number.isFinite(value)) {
+ return 0
+ }
+
+ return clampPercentValue(value * 100)
+}
+
+export function absoluteUnitScoreToPercentValue(value: number): number {
+ if (!Number.isFinite(value)) {
+ return 0
+ }
+
+ return unitIntervalToPercentValue(Math.abs(value))
+}
+
+export function formatUnitIntervalPercent(value: number): string {
+ return `${unitIntervalToPercentValue(value)}%`
+}
diff --git a/web-next/lib/score.ts b/web-next/lib/score.ts
new file mode 100644
index 00000000..e98e2a6a
--- /dev/null
+++ b/web-next/lib/score.ts
@@ -0,0 +1,18 @@
+export function normalizeScoreValue(value: number): number {
+ if (!Number.isFinite(value)) {
+ return 0
+ }
+
+ return value
+}
+
+export function formatSignedScore(value: number): string {
+ const normalizedValue = normalizeScoreValue(value)
+ const sign = normalizedValue >= 0 ? "+" : "-"
+
+ return `${sign}${Math.abs(normalizedValue).toFixed(2)}`
+}
+
+export function isNonNegativeScore(value: number): boolean {
+ return normalizeScoreValue(value) >= 0
+}