← back to Grant
Build out fit-feed landing as real /landing route (interactive source filter, real grants data model, next/font)
5692f3898050348524e595822318ad3829966d0d · 2026-05-30 09:08:09 -0700 · Steve Abrams
Files touched
A app/landing/landing.cssA app/landing/page.tsxA components/landing/FitFeed.tsx
Diff
commit 5692f3898050348524e595822318ad3829966d0d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat May 30 09:08:09 2026 -0700
Build out fit-feed landing as real /landing route (interactive source filter, real grants data model, next/font)
---
app/landing/landing.css | 109 ++++++++++++++++++++++
app/landing/page.tsx | 32 +++++++
components/landing/FitFeed.tsx | 201 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 342 insertions(+)
diff --git a/app/landing/landing.css b/app/landing/landing.css
new file mode 100644
index 0000000..d4e3b43
--- /dev/null
+++ b/app/landing/landing.css
@@ -0,0 +1,109 @@
+/* ─────────────────────────────────────────────────────────────────────────
+ Fit-Feed public landing — light marketing theme.
+ Every rule is scoped under .ff so it never bleeds into the dark app shell
+ (globals.css sets a near-black body; .ff paints its own light surface).
+ Ported from mockups/02-fit-feed.html and wired to the real grants model.
+ ───────────────────────────────────────────────────────────────────────── */
+.ff {
+ --bg: #f4f7f5;
+ --panel: #ffffff;
+ --ink: #0c1f17;
+ --muted: #5a6b62;
+ --emerald: #047857;
+ --accent: #10b981;
+ --line: #e2ebe5;
+ --fed: #2563eb;
+ --found: #7c3aed;
+ --corp: #ea580c;
+ --local: #0891b2;
+ --font-display: var(--ff-display, 'Space Grotesk', sans-serif);
+ --font-body: var(--ff-body, 'IBM Plex Sans', sans-serif);
+
+ font-family: var(--font-body);
+ background: var(--bg);
+ color: var(--ink);
+ min-height: 100vh;
+ color-scheme: light;
+}
+.ff *,
+.ff *::before,
+.ff *::after { box-sizing: border-box; }
+.ff a { text-decoration: none; color: inherit; }
+.ff button { font: inherit; cursor: pointer; border: none; background: none; }
+.ff .wrap { max-width: 1240px; margin: 0 auto; padding: 0 30px; }
+
+/* ── Nav ───────────────────────────────────────────────────────────────── */
+.ff .nav { background: var(--panel); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 20; }
+.ff .nbar { display: flex; align-items: center; justify-content: space-between; height: 66px; max-width: 1240px; margin: 0 auto; padding: 0 30px; }
+.ff .logo { font-family: var(--font-display); font-weight: 700; font-size: 21px; color: var(--emerald); display: flex; align-items: center; gap: 8px; }
+.ff .logo .lbadge { background: var(--emerald); color: #fff; border-radius: 7px; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; font-size: 15px; }
+.ff .navlinks { display: flex; gap: 26px; font-size: 15px; color: var(--muted); align-items: center; }
+.ff .navlinks a:hover { color: var(--ink); }
+.ff .btn { background: var(--emerald); color: #fff; font-weight: 600; padding: 10px 20px; border-radius: 9px; font-size: 15px; font-family: var(--font-display); display: inline-block; transition: background .15s ease; }
+.ff .btn:hover { background: #036249; }
+
+/* ── Hero ──────────────────────────────────────────────────────────────── */
+.ff .hero { padding: 60px 0 30px; display: grid; grid-template-columns: 1fr 1.05fr; gap: 54px; align-items: center; }
+.ff .pill { display: inline-flex; align-items: center; gap: 8px; background: #e4f5ee; color: var(--emerald); font-size: 13px; font-weight: 600; padding: 7px 14px; border-radius: 999px; margin-bottom: 22px; }
+.ff .pill .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
+.ff h1 { font-family: var(--font-display); font-weight: 700; font-size: clamp(38px, 4.6vw, 58px); line-height: 1.04; letter-spacing: -1.5px; margin: 0 0 20px; }
+.ff h1 span { color: var(--emerald); }
+.ff .sub { font-size: 18px; color: var(--muted); line-height: 1.6; margin: 0 0 28px; max-width: 46ch; }
+.ff .cta-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
+.ff .btn-lg { padding: 14px 26px; font-size: 16px; }
+.ff .btn-out { border: 1.5px solid var(--line); color: var(--ink); background: #fff; padding: 13px 24px; border-radius: 9px; font-weight: 600; font-size: 16px; font-family: var(--font-display); transition: border-color .15s ease; }
+.ff .btn-out:hover { border-color: #b9ccc1; }
+.ff .undr { margin-top: 18px; font-size: 13.5px; color: var(--muted); }
+
+/* ── Feed (live, filterable) ───────────────────────────────────────────── */
+.ff .feed { background: var(--panel); border: 1px solid var(--line); border-radius: 18px; box-shadow: 0 24px 60px -20px rgba(4,120,87,.22); overflow: hidden; }
+.ff .feedhead { padding: 16px 20px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
+.ff .feedhead .t { font-family: var(--font-display); font-weight: 600; font-size: 15px; }
+.ff .filters { display: flex; gap: 6px; flex-wrap: wrap; }
+.ff .chip { font-size: 12px; padding: 5px 11px; border-radius: 999px; background: #eef3f0; color: var(--muted); font-weight: 500; transition: background .12s ease, color .12s ease; }
+.ff .chip:hover { background: #e0ebe5; }
+.ff .chip.on { background: var(--emerald); color: #fff; }
+.ff .feedlist { transition: opacity .12s ease; }
+.ff .gcard { padding: 18px 20px; border-bottom: 1px solid var(--line); display: flex; gap: 16px; align-items: center; }
+.ff .gcard:last-child { border-bottom: none; }
+.ff .score { flex: none; width: 58px; height: 58px; border-radius: 50%; display: flex; flex-direction: column; align-items: center; justify-content: center; font-family: var(--font-display); }
+.ff .score .pct { font-weight: 700; font-size: 19px; line-height: 1; }
+.ff .score .lab { font-size: 9px; text-transform: uppercase; letter-spacing: .5px; opacity: .7; }
+.ff .gmeta { flex: 1; min-width: 0; }
+.ff .gmeta .nm { font-weight: 600; font-size: 15.5px; margin-bottom: 5px; }
+.ff .gmeta .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; font-size: 12.5px; color: var(--muted); }
+.ff .src { font-size: 11px; font-weight: 600; padding: 3px 9px; border-radius: 6px; color: #fff; }
+.ff .src.fed { background: var(--fed); } .ff .src.found { background: var(--found); } .ff .src.corp { background: var(--corp); } .ff .src.local { background: var(--local); }
+.ff .due { font-weight: 600; color: var(--emerald); }
+.ff .due.low { color: #9aa8a0; font-weight: 500; }
+.ff .feedempty { padding: 38px 20px; text-align: center; color: var(--muted); font-size: 14px; }
+
+/* ── Stat strip ────────────────────────────────────────────────────────── */
+.ff .strip { background: var(--panel); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); padding: 26px 0; }
+.ff .strip .wrap { display: flex; justify-content: space-around; flex-wrap: wrap; gap: 20px; align-items: center; }
+.ff .scell { text-align: center; }
+.ff .scell b { font-family: var(--font-display); font-size: 15px; }
+.ff .scell span { display: block; font-size: 13px; color: var(--muted); }
+
+/* ── Why ───────────────────────────────────────────────────────────────── */
+.ff .why { padding: 80px 0; }
+.ff .why h2 { font-family: var(--font-display); font-weight: 700; font-size: 34px; letter-spacing: -1px; text-align: center; margin: 0 0 10px; }
+.ff .why p.l { text-align: center; color: var(--muted); max-width: 50ch; margin: 0 auto 46px; }
+.ff .cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
+.ff .col { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 24px; }
+.ff .col h3 { font-family: var(--font-display); font-size: 18px; margin: 0 0 8px; }
+.ff .col p { color: var(--muted); font-size: 14.5px; line-height: 1.55; margin: 0; }
+.ff .col .k { display: inline-block; font-size: 12px; font-weight: 600; color: var(--emerald); background: #e4f5ee; padding: 4px 10px; border-radius: 6px; margin-bottom: 12px; }
+
+/* ── Closer + footer ───────────────────────────────────────────────────── */
+.ff .closer { background: var(--emerald); color: #fff; text-align: center; padding: 70px 0; }
+.ff .closer h2 { font-family: var(--font-display); font-size: clamp(30px, 4vw, 46px); font-weight: 700; letter-spacing: -1px; margin: 0 0 24px; }
+.ff .closer .btn-w { background: #fff; color: var(--emerald); padding: 15px 32px; border-radius: 10px; font-weight: 600; font-size: 17px; font-family: var(--font-display); display: inline-block; transition: transform .12s ease; }
+.ff .closer .btn-w:hover { transform: translateY(-1px); }
+.ff .foot { padding: 28px 0; text-align: center; color: var(--muted); font-size: 13.5px; }
+
+@media (max-width: 880px) {
+ .ff .hero { grid-template-columns: 1fr; padding-top: 40px; }
+ .ff .cols { grid-template-columns: 1fr; }
+ .ff .navlinks a:not(.btn) { display: none; }
+}
diff --git a/app/landing/page.tsx b/app/landing/page.tsx
new file mode 100644
index 0000000..699a659
--- /dev/null
+++ b/app/landing/page.tsx
@@ -0,0 +1,32 @@
+import type { Metadata } from 'next';
+import { Space_Grotesk, IBM_Plex_Sans } from 'next/font/google';
+import FitFeed from '@/components/landing/FitFeed';
+import './landing.css';
+
+const display = Space_Grotesk({
+ subsets: ['latin'],
+ weight: ['500', '600', '700'],
+ variable: '--ff-display',
+ display: 'swap',
+});
+
+const body = IBM_Plex_Sans({
+ subsets: ['latin'],
+ weight: ['400', '500', '600'],
+ variable: '--ff-body',
+ display: 'swap',
+});
+
+export const metadata: Metadata = {
+ title: 'Grant — One fit-scored feed for every funding source',
+ description:
+ 'Federal, foundation, corporate, and local grants unified into a single feed and ranked by fit against your nonprofit mission. Stop tab-hopping between siloed databases.',
+};
+
+export default function LandingPage() {
+ return (
+ <div className={`${display.variable} ${body.variable}`}>
+ <FitFeed />
+ </div>
+ );
+}
diff --git a/components/landing/FitFeed.tsx b/components/landing/FitFeed.tsx
new file mode 100644
index 0000000..3e7c683
--- /dev/null
+++ b/components/landing/FitFeed.tsx
@@ -0,0 +1,201 @@
+'use client';
+
+import { useMemo, useState } from 'react';
+import Link from 'next/link';
+
+/* The four funding sources Grant unifies. Mirrors the real grants.source_api /
+ funder-type split — federal (Grants.gov + Federal Register), foundation,
+ corporate, and state/local. */
+type Source = 'fed' | 'found' | 'corp' | 'local';
+
+interface FeedItem {
+ /** mirrors grants.title */
+ title: string;
+ /** mirrors grants.funder */
+ funder: string;
+ source: Source;
+ /** mirrors grants.ai_fit_score (0–100) */
+ fit: number;
+ /** rendered from grants.amount_min / amount_max */
+ amount: string;
+ /** rendered from grants.deadline */
+ due: string;
+ /** low-fit row, pushed down by ranking — illustrates the filter */
+ lowFit?: boolean;
+}
+
+const SOURCE_LABEL: Record<Source, string> = {
+ fed: 'Federal',
+ found: 'Foundation',
+ corp: 'Corporate',
+ local: 'State / Local',
+};
+
+/* Ring tint per source, matching the badge colors. */
+const SCORE_TINT: Record<Source, { bg: string; color: string }> = {
+ fed: { bg: '#e4f5ee', color: '#047857' },
+ found: { bg: '#ede9fe', color: '#7c3aed' },
+ corp: { bg: '#ffedd5', color: '#ea580c' },
+ local: { bg: '#cffafe', color: '#0891b2' },
+};
+
+const LOW_TINT = { bg: '#eef3f0', color: '#5a6b62' };
+
+/* Representative preview feed — real, public grant programs shown as an example
+ of what a unified, fit-scored stream looks like. The signed-in app replaces
+ this with the org's live /api/grants feed scored against its mission. */
+const FEED: FeedItem[] = [
+ { title: 'NEA Grants for Arts Projects', funder: 'National Endowment for the Arts', source: 'fed', fit: 94, amount: '$10k–$100k', due: 'Due in 6 days' },
+ { title: 'Kresge Foundation — Health Equity', funder: 'The Kresge Foundation', source: 'found', fit: 90, amount: 'up to $250k', due: 'Due in 19 days' },
+ { title: 'CA Arts Council — Local Impact', funder: 'California Arts Council', source: 'local', fit: 88, amount: '$18k', due: 'Due in 31 days' },
+ { title: 'Bank of America Charitable Fund', funder: 'Bank of America', source: 'corp', fit: 81, amount: '$25k', due: 'Due in 44 days' },
+ { title: 'Mellon Foundation — Humanities in Place', funder: 'Andrew W. Mellon Foundation', source: 'found', fit: 76, amount: 'up to $500k', due: 'Due in 52 days' },
+ { title: 'Rural Broadband Infrastructure Pilot', funder: 'USDA Rural Development', source: 'fed', fit: 38, amount: '$1M+', due: 'Low fit — filtered down', lowFit: true },
+];
+
+const FILTERS: { key: 'all' | Source; label: string }[] = [
+ { key: 'all', label: 'All' },
+ { key: 'fed', label: 'Federal' },
+ { key: 'found', label: 'Foundation' },
+ { key: 'corp', label: 'Corporate' },
+ { key: 'local', label: 'Local' },
+];
+
+function FeedCard({ item }: { item: FeedItem }) {
+ const tint = item.lowFit ? LOW_TINT : SCORE_TINT[item.source];
+ return (
+ <div className="gcard">
+ <div className="score" style={{ background: tint.bg, color: tint.color }}>
+ <span className="pct">{item.fit}%</span>
+ <span className="lab">fit</span>
+ </div>
+ <div className="gmeta">
+ <div className="nm">{item.title}</div>
+ <div className="row">
+ <span className={`src ${item.source}`}>{SOURCE_LABEL[item.source]}</span>
+ <span>{item.amount}</span>
+ <span className={item.lowFit ? 'due low' : 'due'}>{item.due}</span>
+ </div>
+ </div>
+ </div>
+ );
+}
+
+export default function FitFeed() {
+ const [filter, setFilter] = useState<'all' | Source>('all');
+
+ const visible = useMemo(
+ () => (filter === 'all' ? FEED : FEED.filter((g) => g.source === filter)),
+ [filter],
+ );
+
+ return (
+ <div className="ff">
+ {/* Nav */}
+ <nav className="nav">
+ <div className="nbar">
+ <div className="logo"><span className="lbadge">G</span>Grant</div>
+ <div className="navlinks">
+ <a href="#feed">Feed</a>
+ <a href="#why">Fit scores</a>
+ <a href="#why">Proposals</a>
+ <Link href="/login">Sign in</Link>
+ <Link className="btn" href="/login">Start free</Link>
+ </div>
+ </div>
+ </nav>
+
+ {/* Hero + live feed */}
+ <section className="wrap hero" id="feed">
+ <div>
+ <span className="pill"><span className="dot" />One feed, every funding source</span>
+ <h1>Stop tab-hopping. <span>Every grant, ranked by fit.</span></h1>
+ <p className="sub">
+ Federal, foundation, corporate, and local grants — siloed everywhere
+ else — unified into a single feed and scored against your mission.
+ See the 94%-fit opportunity before the 40% noise.
+ </p>
+ <div className="cta-row">
+ <Link className="btn btn-lg" href="/login">See your fit scores</Link>
+ <a className="btn-out" href="#feed">Browse the feed</a>
+ </div>
+ <p className="undr">
+ Rivals are siloed — Grants.gov is federal-only, Candid is
+ foundation-centric. Grant scores them all together.
+ </p>
+ </div>
+
+ <div className="feed">
+ <div className="feedhead">
+ <div className="t">Your fit-scored feed</div>
+ <div className="filters" role="tablist" aria-label="Filter feed by source">
+ {FILTERS.map((f) => (
+ <button
+ key={f.key}
+ role="tab"
+ aria-selected={filter === f.key}
+ className={filter === f.key ? 'chip on' : 'chip'}
+ onClick={() => setFilter(f.key)}
+ >
+ {f.label}
+ </button>
+ ))}
+ </div>
+ </div>
+ <div className="feedlist">
+ {visible.length === 0 ? (
+ <div className="feedempty">No {filter} opportunities in this sample feed.</div>
+ ) : (
+ visible.map((item) => <FeedCard key={item.title} item={item} />)
+ )}
+ </div>
+ </div>
+ </section>
+
+ {/* Stat strip */}
+ <div className="strip">
+ <div className="wrap">
+ <div className="scell"><b>4 source types</b><span>federal · foundation · corporate · local</span></div>
+ <div className="scell"><b>1 fit score</b><span>tuned to your org profile</span></div>
+ <div className="scell"><b>No double-tooling</b><span>free federal feed built in</span></div>
+ <div className="scell"><b>Individuals welcome</b><span>not just funded dev teams</span></div>
+ </div>
+ </div>
+
+ {/* Why */}
+ <section className="why wrap" id="why">
+ <h2>Precision the firehose can’t give you</h2>
+ <p className="l">GrantWatch lists everything. Candid has the data. Neither ranks it for <em>you</em>.</p>
+ <div className="cols">
+ <div className="col">
+ <span className="k">Source-unified</span>
+ <h3>One ranked stream</h3>
+ <p>Federal + foundation + corporate + local + individual in one ranked stream — no more paying for one tool and still tabbing over to Grants.gov.</p>
+ </div>
+ <div className="col">
+ <span className="k">Fit scoring</span>
+ <h3>Ranked, not listed</h3>
+ <p>Every opportunity scored against your mission, budget, and history. The 38%-fit gets pushed down; the 94% rises to the top.</p>
+ </div>
+ <div className="col">
+ <span className="k">Action-first</span>
+ <h3>Discovery into draft</h3>
+ <p>Click a high-fit grant and draft the LOI in the same screen. Discovery flows straight into a proposal and outreach.</p>
+ </div>
+ </div>
+ </section>
+
+ {/* Closer */}
+ <section className="closer">
+ <div className="wrap">
+ <h2>See what actually fits your mission.</h2>
+ <Link className="btn-w" href="/login">See your fit scores</Link>
+ </div>
+ </section>
+
+ <footer className="foot">
+ Grant — Deadline River · One fit-scored feed for under-resourced nonprofits · Federal data via the U.S. Federal Register API
+ </footer>
+ </div>
+ );
+}
← 328ab89 Add 6 Grant public-landing mockups
·
back to Grant
·
Serve fit-feed landing at / for logged-out visitors; AppShel f42a326 →