diff --git a/app/(marketing)/page.tsx b/app/(marketing)/page.tsx
index 6ea5dbe..e1ec378 100644
--- a/app/(marketing)/page.tsx
+++ b/app/(marketing)/page.tsx
@@ -1,10 +1,15 @@
import type { Metadata } from "next";
-import { BuilderJourney } from "@/components/patterns/builder-journey";
-import { CtaBanner } from "@/components/patterns/cta-banner";
-import { LandingAbout } from "@/components/patterns/landing-about";
-import { LandingHero } from "@/components/patterns/landing-hero";
-import { TrustImpact } from "@/components/patterns/trust-impact";
+import { AudienceSections } from "@/components/landing/audience-sections";
+import { GetStarted } from "@/components/landing/get-started";
+import { LandingAbout } from "@/components/landing/landing-about";
+import { LandingBanner } from "@/components/landing/landing-banner";
+import { LandingHero } from "@/components/landing/landing-hero";
+import { LatestPosts } from "@/components/landing/latest-posts";
+import { OpenInfrastructure } from "@/components/landing/open-infrastructure";
+import { PartnerStrip } from "@/components/landing/partner-strip";
+import { PlatformGrid } from "@/components/landing/platform-grid";
+import { TrustBand } from "@/components/landing/trust-band";
import { pageOpenGraph } from "@/lib/seo/metadata";
export const metadata: Metadata = {
@@ -16,18 +21,24 @@ export const metadata: Metadata = {
openGraph: pageOpenGraph("/"),
};
+/*
+ * The landing page follows one continuous frame: every section's content
+ * sits inside `.lp-frame`, whose hairline side rules run the full height
+ * of the page, with horizontal rules marking where one idea ends.
+ */
export default function LandingPage() {
return (
-
+
-
+
+
+
+
-
-
-
-
-
-
+
+
+
+
);
}
diff --git a/app/globals.css b/app/globals.css
index 76cdc21..1da0875 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -47,6 +47,17 @@
@layer base {
:root {
--site-header-height: 4rem;
+ /* The landing page frame (and the header aligned to it): a hairline-ruled
+ column with a narrow edge outside the rules and a gutter inside them. */
+ --frame-max: 1440px;
+ --frame-edge: clamp(12px, 2vw, 32px);
+ --frame-gutter: clamp(16px, 1.5vw, 24px);
+ }
+ @media (max-width: 1023px) {
+ :root {
+ --frame-edge: 0px;
+ --frame-gutter: clamp(16px, 3vw, 24px);
+ }
}
html {
color-scheme: light;
@@ -167,7 +178,7 @@
.how-it-works-heading-block {
min-width: 0;
}
- /* Home page stars (see .journey-star), flanking the centered heading block. */
+ /* Brand stars flanking the centered heading block. */
.how-it-works-star {
position: absolute;
width: clamp(36px, 3.6vw, 56px);
@@ -202,7 +213,7 @@
letter-spacing: 0.18em;
text-transform: uppercase;
}
- /* Same scale as the home page hero heading (.hero-heading), at every breakpoint below. */
+ /* Display-scale page heading, resized at the breakpoints further down. */
.how-it-works-title {
max-width: 1100px;
font-size: clamp(2.75rem, 4.6vw, 5.5rem);
@@ -310,174 +321,6 @@
}
}
- /* Editorial three-column section: copy + pill photos flanking an arced centerpiece. */
- .builder-journey {
- display: grid;
- grid-template-columns: minmax(0, 1fr) min(29vw, 416px) minmax(0, 1fr);
- gap: clamp(40px, 5vw, 72px);
- padding-block: clamp(48px, 6vw, 88px);
- }
- .journey-column {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- gap: 40px;
- }
- .journey-copy {
- position: relative;
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- gap: 32px;
- max-width: 356px;
- }
- .journey-copy-right {
- gap: 28px;
- max-width: 360px;
- margin-inline-start: clamp(0px, 1.8vw, 24px);
- }
- .journey-final-row {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: clamp(16px, 1.6vw, 24px);
- }
- .journey-display {
- color: var(--color-ink);
- font-family: var(--font-display);
- font-size: clamp(28px, 2.6vw, 40px);
- font-weight: 600;
- letter-spacing: -0.03em;
- line-height: 1.15;
- }
- .journey-heading-mobile {
- display: none;
- }
- .journey-text {
- color: var(--color-body-copy);
- font-family: var(--font-body);
- font-size: clamp(15px, 1vw, 17px);
- line-height: 1.55;
- }
- .journey-action {
- position: relative;
- isolation: isolate;
- display: inline-flex;
- align-items: center;
- gap: 8px;
- overflow: hidden;
- padding: clamp(12px, 1vw, 14px) clamp(20px, 1.6vw, 26px);
- border-radius: 999px;
- background: var(--color-brand);
- color: var(--color-brand-ink);
- font-family: var(--font-display);
- font-size: clamp(14px, 0.85vw, 16px);
- font-weight: 600;
- line-height: 1;
- --btn-fill-color: var(--color-brand-soft);
- }
- .journey-pair {
- display: flex;
- align-items: flex-start;
- gap: clamp(12px, 1.4vw, 22px);
- }
- .journey-pill {
- position: relative;
- overflow: hidden;
- border-radius: 999px;
- background: var(--color-surface);
- }
- .journey-pair > .journey-pill {
- flex: 1 1 0;
- max-width: 184px;
- aspect-ratio: 176 / 298;
- }
- .journey-pair-left > .journey-pill:first-child {
- margin-top: clamp(32px, 4.6vw, 66px);
- }
- .journey-pair-right > .journey-pill:last-child {
- margin-top: clamp(48px, 7.3vw, 105px);
- }
- .journey-center {
- position: relative;
- align-self: center;
- aspect-ratio: 416 / 709;
- }
- .journey-pill-center {
- position: absolute;
- top: 9.73%;
- left: 5.05%;
- width: 89.9%;
- height: 90.27%;
- }
- .journey-arc {
- position: absolute;
- inset: 0;
- width: 100%;
- height: 100%;
- overflow: visible;
- fill: var(--color-ink);
- font-family: var(--font-display);
- font-size: 22px;
- font-weight: 500;
- letter-spacing: 0.02em;
- }
- .journey-star {
- position: absolute;
- z-index: 1;
- width: clamp(36px, 3.6vw, 56px);
- height: auto;
- fill: var(--color-brand);
- }
- .journey-star-left {
- top: clamp(28px, 3.4vw, 48px);
- left: calc(100% - 24px);
- }
- .journey-star-right {
- top: calc(-1 * clamp(44px, 5vw, 68px));
- left: 0;
- }
- @media (max-width: 1023px) {
- .builder-journey {
- grid-template-columns: minmax(0, 1fr);
- gap: 48px;
- }
- .journey-copy,
- .journey-copy-right {
- max-width: 560px;
- margin-inline-start: 0;
- }
- .journey-pair {
- justify-content: center;
- }
- .journey-center {
- justify-self: center;
- width: min(100%, 416px);
- }
- .journey-star {
- display: none;
- }
- }
-
- @media (max-width: 599px) {
- .journey-column-left .journey-copy {
- align-items: center;
- text-align: center;
- }
- .journey-heading-desktop {
- display: none;
- }
- .journey-heading-mobile {
- display: inline;
- font-size: clamp(20px, 6vw, 30px);
- white-space: nowrap;
- }
- .journey-text {
- align-self: center;
- text-align: center;
- }
- }
-
/*
* Final CTA banner: a solid logo-navy card split in half, the platform's
* promise and a working newsletter signup on the left, and a photo that
@@ -648,255 +491,28 @@
font-weight: 600;
}
- /*
- * Trust & Impact — sits between How It Works and the newsletter banner.
- * Built from the reference frame's arrangement (text column, photo group,
- * photo strip; 500x688 photo, 200x262 strip photos), with the photo
- * columns trimmed so the images sit smaller in a full-width section.
- * Everything scales as one piece:
- * - columns are fr units (text 480 : photo group 560 : strip 150),
- * - type, padding and card sizes are container units (cqi) of the
- * section's own width, so text scales with the photos instead of
- * staying viewport-sized.
- * Full width like every other section (site-container gutters only).
- * Stat cards run beneath the text and photo, not under the strip.
- */
- .trust-impact {
- container-type: inline-size;
- display: grid;
- grid-template-columns: minmax(0, 480fr) minmax(0, 560fr) minmax(0, 150fr);
- grid-template-rows: auto auto;
- column-gap: 3.2%;
- align-items: start;
- padding-block: clamp(48px, 5vw, 80px);
- }
- .trust-impact-content {
- grid-column: 1;
- grid-row: 1;
- align-self: center;
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- gap: clamp(16px, 2.4cqi, 32px);
- }
- .trust-impact-eyebrow {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: calc(-1 * clamp(8px, 1.8cqi, 24px));
- font-family: var(--font-body);
- font-size: clamp(13px, 1.37cqi, 18px);
- font-weight: 500;
- color: var(--color-ink);
- }
- .trust-impact-eyebrow span {
- color: var(--color-brand);
- }
- .trust-impact-heading {
- font-family: var(--font-display);
- font-weight: 600;
- font-size: clamp(28px, 4.27cqi, 56px);
- line-height: 1.2;
- letter-spacing: -0.01em;
- color: var(--color-ink);
- }
- .trust-impact-text {
- font-size: clamp(14px, 1.3cqi, 17px);
- line-height: 1.55;
- color: var(--color-body-copy);
- }
- .trust-impact-visual {
- position: relative;
- isolation: isolate;
- grid-column: 2;
- grid-row: 1;
- }
- .trust-impact-star {
- position: absolute;
- z-index: 0;
- top: 3%;
- left: -12.5%;
- width: 21%;
- height: auto;
- aspect-ratio: 139 / 150;
- fill: var(--color-brand);
- }
- .trust-impact-frame {
- position: relative;
- z-index: 1;
- width: 70%;
- margin-left: 10%;
- aspect-ratio: 500 / 688;
- /* Matte: the photo sits inset inside a larger arch outline. Padding
- percentages resolve against the photo group's width, sized so the
- insets keep the reference's ratio to the frame (about 6% / 9%). */
- padding: 6% 4.6% 4.6% 4.3%;
- border: 1px solid var(--color-ink);
- /* Arch: browsers cap a too-large radius at 50% of the box, so the top
- corners always resolve to a perfect half-circle regardless of width. */
- border-radius: 999px 999px 0 0;
- }
- .trust-impact-photo {
- position: relative;
- width: 100%;
- height: 100%;
- overflow: hidden;
- border-radius: 999px 999px 0 0;
- }
- .trust-float-card {
- position: absolute;
- z-index: 2;
- display: flex;
- align-items: center;
- gap: clamp(8px, 1.2cqi, 16px);
- padding: clamp(10px, 1.83cqi, 24px);
- border: 1px solid var(--color-ink);
- border-radius: clamp(10px, 1.2cqi, 16px);
- background: color-mix(in srgb, var(--color-surface) 80%, transparent);
- backdrop-filter: blur(8px);
- }
- .trust-float-card p {
- margin: 0;
- font-size: clamp(11px, 1.07cqi, 14px);
- line-height: 1.25;
- color: var(--color-ink);
- }
- .trust-float-icon {
- display: flex;
- flex-shrink: 0;
- align-items: center;
- justify-content: center;
- width: clamp(28px, 3.66cqi, 48px);
- height: clamp(28px, 3.66cqi, 48px);
- border-radius: 999px;
- background: var(--color-brand-soft);
- color: var(--color-ink);
- }
- .trust-float-icon svg {
- width: 45%;
- height: 45%;
- }
- .trust-float-card-top {
- top: 24%;
- left: 56.5%;
- width: 40.8%;
- }
- .trust-float-card-bottom {
- top: 45.5%;
- left: -0.75%;
- width: 39.2%;
- }
- .trust-impact-strip {
- grid-column: 3;
- grid-row: 1 / span 2;
- display: flex;
- flex-direction: column;
- gap: clamp(10px, 1.83cqi, 24px);
- }
- .trust-strip-photo {
- position: relative;
- aspect-ratio: 200 / 262;
- overflow: hidden;
- border: 1px solid var(--color-ink);
- border-radius: clamp(16px, 2.44cqi, 32px);
- }
- .trust-stats {
- grid-column: 1 / span 2;
- grid-row: 2;
- display: flex;
- gap: clamp(14px, 2.44cqi, 32px);
- width: 90%;
- margin: clamp(24px, 3.66cqi, 48px) 0 0;
- padding: 0;
- list-style: none;
- }
- .trust-stat {
- flex: 1 1 0;
- min-width: 0;
- display: flex;
- align-items: center;
- gap: clamp(10px, 1.22cqi, 16px);
- min-height: clamp(60px, 6.3cqi, 83px);
- padding: clamp(10px, 1.22cqi, 16px);
- /* Logo navy fill + logo bright-blue hard shadow. brand-ink stays navy in
- both themes (ink flips light in dark mode), so the cards keep their
- fill whichever theme is active. */
- border: 1px solid var(--color-brand-ink);
- border-radius: clamp(10px, 1.22cqi, 16px);
- background: var(--color-brand-ink);
- box-shadow: 6px 6px 0 var(--color-brand);
- }
- .trust-stat-value {
- flex-shrink: 0;
- font-family: var(--font-display);
- font-size: clamp(17px, 1.68cqi, 22px);
- font-weight: 600;
- color: #ffffff;
- }
- .trust-stat-label {
- font-size: clamp(11px, 1.07cqi, 14px);
- line-height: 1.25;
- color: rgb(255 255 255 / 80%);
- }
- @media (max-width: 1023px) {
- .trust-impact {
- grid-template-columns: minmax(0, 1fr);
- grid-template-rows: auto;
- row-gap: clamp(32px, 5vw, 48px);
- }
- .trust-impact-content,
- .trust-impact-visual,
- .trust-impact-strip,
- .trust-stats {
- grid-column: 1;
- grid-row: auto;
- }
- /* Mobile reads as a stacked, centered story: eyebrow, heading, copy and
- CTA all align to the middle instead of hugging the left edge. */
- .trust-impact-content {
- align-items: center;
- text-align: center;
- }
- .trust-impact-visual {
- width: min(100%, 460px);
- margin-inline: auto;
- }
- .trust-impact-strip {
- flex-direction: row;
- }
- .trust-strip-photo {
- flex: 1 1 0;
- min-width: 0;
- }
- .trust-stats {
- width: 100%;
- margin-top: 0;
- }
- }
- @media (max-width: 599px) {
- .trust-float-card {
- display: none;
- }
- .trust-stats {
- flex-direction: column;
- }
- }
-
+ /* Left-aligned bar: logo, then the links beside it, actions pushed right. */
.site-header-grid {
display: grid;
- grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
+ grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
height: var(--site-header-height);
gap: 16px;
}
+ /* On the landing page the bar lines up with the ruled frame below it. */
+ body:has(.lp) .site-header-grid {
+ width: min(100% - 2 * var(--frame-edge), var(--frame-max));
+ padding-inline: var(--frame-gutter);
+ }
.site-header-links,
.site-header-actions {
display: flex;
align-items: center;
}
.site-header-links {
- justify-content: center;
+ justify-content: flex-start;
gap: 28px;
+ margin-left: clamp(8px, 1.6vw, 24px);
}
/* From 1024px (where the inline links return) to 1279px, the six links
and the actions are tight, so the links sit closer rather than overlap. */
@@ -1221,78 +837,6 @@
padding-inline: clamp(1rem, 3vw, 3.5rem);
}
- .landing-hero {
- --collage-gap: clamp(0.5rem, 1.65vw, 1.75rem);
- display: flex;
- flex-direction: column;
- width: 100%;
- height: calc(100svh - var(--site-header-height));
- min-height: 620px;
- padding: clamp(1rem, 2.5vh, 2rem) clamp(1rem, 3vw, 3.5rem) 1.75rem;
- gap: clamp(1.25rem, 3vh, 2.5rem);
- }
-
- .hero-intro {
- position: relative;
- flex-shrink: 0;
- }
- .hero-copy {
- position: relative;
- z-index: 1;
- max-width: 780px;
- margin-inline: auto;
- text-align: center;
- }
- /* Home page stars (see .journey-star), just outside the centered hero copy. */
- .hero-star {
- position: absolute;
- width: clamp(36px, 3.6vw, 56px);
- height: auto;
- fill: var(--color-brand);
- pointer-events: none;
- }
- .hero-star-left {
- top: 26%;
- right: calc(100% + clamp(24px, 3vw, 56px));
- }
- .hero-star-right {
- top: 60%;
- left: calc(100% + clamp(24px, 3vw, 56px));
- }
- @media (max-width: 1023px) {
- .hero-star {
- display: none;
- }
- }
- .hero-eyebrow {
- font-size: 10px;
- font-weight: 600;
- letter-spacing: 0.19em;
- text-transform: uppercase;
- color: var(--color-ink-soft);
- }
- .hero-heading {
- margin-top: 12px;
- font-size: clamp(2.75rem, 4.6vw, 5.5rem);
- line-height: 1.06;
- letter-spacing: -0.045em;
- font-weight: 700;
- }
- .hero-heading span {
- color: var(--color-ink-soft);
- }
- .hero-description {
- max-width: 540px;
- margin: 18px auto 0;
- font-size: 14px;
- line-height: 1.7;
- color: var(--color-body-copy);
- text-wrap: pretty;
- }
- .hero-description strong {
- color: var(--color-ink);
- font-weight: 600;
- }
.hero-actions {
display: flex;
justify-content: center;
@@ -1330,82 +874,13 @@
.hero-action-secondary:hover {
color: var(--color-ink-soft);
}
- .hero-collage {
- display: grid;
- grid-template-columns: 0.7fr 1fr 1.13fr 1fr 0.7fr;
- gap: var(--collage-gap);
- flex: 1;
- min-height: 0;
- }
- .hero-column {
- display: flex;
- flex-direction: column;
- min-height: 0;
- gap: var(--collage-gap);
- }
- .hero-photo {
- position: relative;
- flex: 1;
- min-height: 0;
- overflow: hidden;
- border-radius: clamp(10px, 1.2vw, 22px);
- background: #deedf3;
- }
- .hero-photo-center img {
- object-position: center 35%;
- }
- .hero-column-left {
- padding-bottom: 7%;
- }
- .hero-column-right {
- padding-top: 14%;
- }
- .hero-column-edge-left {
- padding-bottom: 36%;
- }
- .hero-column-edge-right {
- padding-top: 42%;
- }
- .hero-side-note {
- padding: 0 0 12px;
- font-size: clamp(14px, 1.65vw, 29px);
- line-height: 1.75;
- font-weight: 500;
- }
- .hero-side-note::after {
- content: "";
- display: block;
- margin-top: 14px;
- height: 3px;
- width: 65%;
- background: var(--color-ink);
- }
- .hero-column-edge-right .hero-side-note {
- align-self: flex-end;
- padding-top: 5px;
- padding-bottom: 0;
- }
-
@media (min-width: 1024px) and (min-height: 900px) {
- .landing-hero {
- padding-top: 4vh;
- padding-bottom: 3vh;
- gap: 4vh;
- }
- .hero-heading,
.how-it-works-title {
font-size: clamp(4rem, 5vw, 6rem);
}
- .hero-description {
- font-size: 16px;
- }
}
@media (max-width: 1023px) {
- .hero-copy {
- max-width: 70%;
- }
- .hero-heading,
.how-it-works-title {
font-size: clamp(2.5rem, 5.3vw, 3.4rem);
}
@@ -1422,34 +897,10 @@
}
@media (max-width: 639px) {
- .landing-hero {
- min-height: 464px;
- padding: 20px 14px 22px;
- gap: 22px;
- }
- .hero-copy {
- max-width: 100%;
- }
- .hero-eyebrow {
- font-size: 8px;
- letter-spacing: 0.16em;
- }
- .hero-heading {
- margin-top: 13px;
- font-size: clamp(2rem, 8.8vw, 3.5rem);
- line-height: 1.1;
- }
.how-it-works-title {
font-size: clamp(2rem, 8.8vw, 3.5rem);
line-height: 1.1;
}
- .hero-description {
- max-width: 360px;
- margin-top: 14px;
- font-size: 12px;
- line-height: 1.65;
- padding-inline: 8px;
- }
.hero-actions {
flex-direction: column;
gap: 0;
@@ -1464,47 +915,6 @@
min-height: 44px;
font-size: 11px;
}
- .hero-collage {
- gap: 7px;
- grid-template-columns: 0.6fr 1fr 1.3fr 1fr 0.6fr;
- max-height: 360px;
- margin-block: auto;
- }
- .hero-column {
- gap: 7px;
- }
- .hero-column-edge-left {
- padding-top: 70%;
- padding-bottom: 85%;
- }
- .hero-column-edge-right {
- padding-top: 100%;
- padding-bottom: 45%;
- }
- .hero-column-left {
- padding-bottom: 25%;
- }
- .hero-column-right {
- padding-top: 28%;
- }
- .hero-side-note {
- display: none;
- }
- .hero-photo {
- border-radius: 10px;
- }
- }
-
- /* Let short landscape windows and zoomed text scroll without clipping content. */
- @media (max-height: 600px) and (min-width: 640px) {
- .landing-hero {
- height: auto;
- min-height: calc(100svh - var(--site-header-height));
- }
- .hero-collage {
- flex: none;
- height: 300px;
- }
}
/* Long-form legal document typography (Privacy Policy, Terms of Service) —
@@ -1841,3 +1251,1746 @@
color: rgb(255 255 255 / 0.88);
}
}
+
+/*
+ * Landing page (app/(marketing)/page.tsx, components/landing/*).
+ * Structure borrowed from the classic fintech landing grid: one continuous
+ * frame whose hairline side rules run the height of the page, full-bleed
+ * rules around the track strip and bands, two-tone statement headings, and
+ * product cards carrying small mock screens. Colours and type stay ours.
+ */
+@layer components {
+ .lp {
+ --lp-line: color-mix(in srgb, var(--color-ink) 10%, transparent);
+ --lp-quiet: color-mix(in srgb, var(--color-body-copy) 78%, var(--color-ink-soft));
+ --lp-tint: color-mix(in srgb, var(--color-brand) 6%, var(--color-paper));
+ /* Fixed brand blues for gradient art, which must not flip with the theme. */
+ --lp-blue-deep: #0272d4;
+ --lp-navy: #000092;
+ --lp-gutter: var(--frame-gutter);
+ --lp-space: clamp(56px, 7vw, 96px);
+ overflow-x: clip;
+ }
+
+ .btn-pill-inverse {
+ --btn-fill-color: color-mix(in srgb, var(--color-on-inverse) 12%, transparent);
+ }
+
+ .lp-frame {
+ position: relative;
+ width: min(100% - 2 * var(--frame-edge), var(--frame-max));
+ margin-inline: auto;
+ padding-inline: var(--lp-gutter);
+ border-inline: 1px solid var(--lp-line);
+ }
+ .lp-block {
+ padding-block: var(--lp-space);
+ }
+ .lp-block-tight {
+ padding-top: 0;
+ }
+ .lp-block-flush {
+ padding-bottom: var(--lp-space);
+ }
+ .lp-divided {
+ border-top: 1px solid var(--lp-line);
+ }
+
+ /* Two-tone statement heading: the claim in ink, the detail in a quieter tone. */
+ .lp-statement {
+ max-width: 780px;
+ font-family: var(--font-display);
+ font-size: clamp(26px, 2.6vw, 36px);
+ font-weight: 500;
+ line-height: 1.2;
+ letter-spacing: -0.02em;
+ color: var(--color-ink);
+ text-wrap: pretty;
+ }
+ .lp-statement span {
+ color: var(--lp-quiet);
+ }
+ .lp-statement-sm {
+ max-width: 700px;
+ font-size: clamp(21px, 1.9vw, 26px);
+ line-height: 1.3;
+ }
+ .lp-statement-stack span {
+ display: block;
+ }
+
+ .lp-text-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+ color: var(--color-ink-soft);
+ font-size: 14.5px;
+ font-weight: 600;
+ transition: color 150ms ease;
+ }
+ .lp-text-link:hover {
+ color: var(--color-ink);
+ }
+ .lp-text-link-chevron {
+ width: 15px;
+ height: 15px;
+ transition: transform 150ms ease;
+ }
+ .lp-text-link:hover .lp-text-link-chevron {
+ transform: translateX(3px);
+ }
+ .lp-text-link-inverse {
+ color: var(--color-brand-soft);
+ }
+ .lp-text-link-inverse:hover {
+ color: var(--color-on-inverse);
+ }
+
+ /* ---- Hero ---- */
+ .lp-hero {
+ position: relative;
+ overflow: hidden;
+ }
+ .lp-ribbon {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: min(64vw, 1000px);
+ height: 100%;
+ pointer-events: none;
+ }
+ .lp-hero-inner {
+ padding-block: clamp(72px, 10vw, 148px) clamp(64px, 8vw, 112px);
+ }
+ .lp-hero-ticker {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--color-ink);
+ }
+ .lp-hero-ticker span {
+ margin-left: 4px;
+ color: var(--color-ink-soft);
+ font-variant-numeric: tabular-nums;
+ font-weight: 600;
+ }
+ .lp-hero-heading {
+ position: relative;
+ max-width: 900px;
+ margin-top: 28px;
+ font-family: var(--font-display);
+ font-size: clamp(32px, 3.6vw, 52px);
+ font-weight: 500;
+ line-height: 1.1;
+ letter-spacing: -0.03em;
+ color: var(--color-ink);
+ text-wrap: pretty;
+ }
+ .lp-hero-heading span {
+ color: var(--lp-quiet);
+ }
+ .lp-hero-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-top: 36px;
+ }
+
+ /* ---- Partner logo strip ---- */
+ .lp-tracks {
+ border-block: 1px solid var(--lp-line);
+ }
+ .lp-tracks-frame {
+ overflow: hidden;
+ padding-inline: 0;
+ mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
+ }
+ .lp-tracks-row {
+ display: flex;
+ width: max-content;
+ animation: lp-marquee 48s linear infinite;
+ }
+ .lp-tracks-row:hover {
+ animation-play-state: paused;
+ }
+ .lp-partner {
+ display: flex;
+ flex-shrink: 0;
+ align-items: center;
+ height: 96px;
+ padding-inline: clamp(40px, 5vw, 72px);
+ }
+ .lp-partner-logo {
+ width: auto;
+ height: 40px;
+ }
+ /* Technetium's mark carries a tagline under the wordmark, so it runs taller. */
+ .lp-partner-logo.lp-partner-technetium {
+ height: 52px;
+ }
+ .lp-partner-logo.lp-partner-skillssync {
+ height: 38px;
+ }
+ @keyframes lp-marquee {
+ to {
+ transform: translateX(-50%);
+ }
+ }
+
+ /* ---- Product cards (bento) ---- */
+ .lp-bento {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 16px;
+ margin-top: clamp(40px, 5vw, 64px);
+ }
+ .lp-bento-wide {
+ grid-column: span 2;
+ }
+ .lp-bento-full {
+ grid-column: 1 / -1;
+ }
+ .lp-card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ min-height: 440px;
+ overflow: hidden;
+ border: 1px solid var(--lp-line);
+ border-radius: var(--radius-card);
+ background: var(--color-surface);
+ transition: border-color 200ms ease, box-shadow 200ms ease;
+ }
+ .lp-card:hover {
+ border-color: color-mix(in srgb, var(--color-brand) 45%, transparent);
+ box-shadow: var(--shadow-card);
+ }
+ .lp-card:has(.lp-card-link:focus-visible) {
+ outline: 2px solid var(--color-brand);
+ outline-offset: 2px;
+ }
+ .lp-card-head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 24px 24px 0;
+ }
+ .lp-card-title {
+ max-width: 22ch;
+ font-family: var(--font-display);
+ font-size: 20px;
+ font-weight: 500;
+ line-height: 1.3;
+ letter-spacing: -0.01em;
+ color: var(--color-ink);
+ }
+ .lp-card-link {
+ outline: none;
+ }
+ .lp-card-link::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+ }
+ .lp-card-chip {
+ display: grid;
+ flex-shrink: 0;
+ place-items: center;
+ width: 32px;
+ height: 32px;
+ border-radius: var(--radius-control);
+ background: color-mix(in srgb, var(--color-brand) 12%, transparent);
+ color: var(--color-ink-soft);
+ transition: background 200ms ease, color 200ms ease;
+ }
+ .lp-card:hover .lp-card-chip {
+ background: var(--color-brand);
+ color: var(--color-brand-ink);
+ }
+ .lp-card-visual {
+ position: relative;
+ display: flex;
+ flex: 1;
+ align-items: flex-end;
+ justify-content: center;
+ margin-top: 24px;
+ padding: 24px;
+ background:
+ radial-gradient(120% 90% at 100% 100%, color-mix(in srgb, var(--color-brand) 26%, transparent), transparent 62%),
+ radial-gradient(90% 70% at 0% 100%, color-mix(in srgb, var(--color-brand-soft) 30%, transparent), transparent 60%);
+ }
+ .lp-bento-full {
+ display: grid;
+ grid-template-columns: 260px minmax(0, 1fr);
+ min-height: 0;
+ }
+ .lp-bento-full .lp-card-head {
+ padding-bottom: 24px;
+ }
+ .lp-bento-full .lp-card-chip {
+ position: absolute;
+ top: 24px;
+ right: 24px;
+ z-index: 1;
+ }
+ .lp-bento-full .lp-card-visual {
+ margin-top: 0;
+ padding: 56px 32px 0;
+ }
+
+ /* Mock screens inside the cards: sample data, real rules. */
+ .lp-mock {
+ width: 100%;
+ padding: 16px;
+ border: 1px solid var(--lp-line);
+ border-radius: 10px;
+ background: var(--color-surface);
+ box-shadow: var(--shadow-card);
+ color: var(--color-body-copy);
+ font-size: 12px;
+ line-height: 1.45;
+ text-align: left;
+ }
+ .lp-mock-kicker {
+ color: var(--color-muted);
+ font-size: 10.5px;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ }
+ .lp-mock-title {
+ margin-top: 6px;
+ color: var(--color-ink);
+ font-size: 13px;
+ font-weight: 600;
+ }
+ .lp-mock-amount {
+ margin-top: 4px;
+ color: var(--color-ink);
+ font-family: var(--font-display);
+ font-size: 24px;
+ font-weight: 600;
+ letter-spacing: -0.02em;
+ font-variant-numeric: tabular-nums;
+ }
+ .lp-mock-pair {
+ display: grid;
+ grid-template-columns: minmax(0, 0.8fr) minmax(0, 1fr);
+ align-items: end;
+ gap: 16px;
+ width: 100%;
+ }
+ .lp-mock-progress {
+ height: 6px;
+ margin-top: 14px;
+ overflow: hidden;
+ border-radius: 999px;
+ background: var(--lp-line);
+ }
+ .lp-mock-progress span {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+ background: linear-gradient(90deg, var(--color-brand-soft), var(--color-brand));
+ }
+ .lp-mock-row {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 8px;
+ }
+ .lp-mock-row strong {
+ color: var(--color-ink);
+ }
+ .lp-mock-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ margin-top: 14px;
+ padding: 3px 9px;
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--color-success) 12%, transparent);
+ color: var(--color-success);
+ font-size: 11px;
+ font-weight: 600;
+ }
+ .lp-mock-browser {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin: -4px 0 14px;
+ }
+ .lp-mock-browser span {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--lp-line);
+ }
+ .lp-mock-browser em {
+ margin-left: 10px;
+ padding: 2px 10px;
+ border-radius: 999px;
+ background: var(--lp-tint);
+ color: var(--color-muted);
+ font-size: 10px;
+ font-style: normal;
+ }
+ .lp-mock-lines {
+ margin-top: 8px;
+ }
+ .lp-mock-lines div {
+ display: flex;
+ justify-content: space-between;
+ padding-block: 7px;
+ border-bottom: 1px dashed var(--lp-line);
+ }
+ .lp-mock-lines dd {
+ color: var(--color-ink);
+ font-variant-numeric: tabular-nums;
+ }
+ .lp-mock-lines .lp-mock-total {
+ border-bottom: 0;
+ font-weight: 600;
+ }
+ .lp-mock-lines .lp-mock-total dt {
+ color: var(--color-ink);
+ }
+ .lp-mock-methods {
+ display: flex;
+ gap: 6px;
+ margin-top: 8px;
+ }
+ .lp-mock-method {
+ padding: 4px 10px;
+ border: 1px solid var(--lp-line);
+ border-radius: 6px;
+ font-size: 11px;
+ }
+ .lp-mock-method-on {
+ border-color: var(--color-brand);
+ background: color-mix(in srgb, var(--color-brand) 10%, transparent);
+ color: var(--color-ink);
+ font-weight: 600;
+ }
+ .lp-mock-locked {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 12px;
+ color: var(--color-ink-soft);
+ font-size: 11px;
+ font-weight: 500;
+ }
+ .lp-mock-payout {
+ max-width: 300px;
+ }
+ .lp-mock-tranches {
+ display: grid;
+ gap: 12px;
+ margin-top: 14px;
+ }
+ .lp-mock-tranches li {
+ display: flex;
+ gap: 10px;
+ }
+ .lp-mock-tranches strong {
+ display: block;
+ color: var(--color-ink);
+ font-size: 12.5px;
+ }
+ .lp-mock-tranches span:not(.lp-mock-dot) {
+ font-size: 11px;
+ }
+ .lp-mock-dot {
+ display: grid;
+ flex-shrink: 0;
+ place-items: center;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: var(--lp-line);
+ color: var(--color-muted);
+ }
+ .lp-mock-tranche-done .lp-mock-dot {
+ background: var(--color-success);
+ color: #fff;
+ }
+ .lp-mock-split {
+ display: flex;
+ height: 26px;
+ margin-top: 16px;
+ overflow: hidden;
+ border-radius: 6px;
+ font-size: 10.5px;
+ font-weight: 600;
+ }
+ .lp-mock-split span {
+ display: grid;
+ place-items: center;
+ }
+ .lp-mock-split span:first-child {
+ background: var(--color-brand);
+ color: var(--color-brand-ink);
+ }
+ .lp-mock-split span:last-child {
+ flex: 1;
+ background: var(--lp-line);
+ color: var(--color-body-copy);
+ }
+ .lp-mock-rubric {
+ display: grid;
+ gap: 8px;
+ margin-top: 12px;
+ }
+ .lp-mock-rubric li {
+ display: grid;
+ grid-template-columns: 64px minmax(0, 1fr) 14px;
+ align-items: center;
+ gap: 10px;
+ }
+ .lp-mock-rubric strong {
+ color: var(--color-ink);
+ text-align: right;
+ }
+ .lp-mock-meter {
+ height: 6px;
+ overflow: hidden;
+ border-radius: 999px;
+ background: var(--lp-line);
+ }
+ .lp-mock-meter span {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+ background: linear-gradient(90deg, var(--color-brand-soft), var(--color-brand));
+ }
+ .lp-mock-feedback {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 14px;
+ color: var(--color-success);
+ font-size: 11.5px;
+ font-weight: 500;
+ }
+ .lp-mock-person {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+ .lp-mock-person strong {
+ display: block;
+ color: var(--color-ink);
+ font-size: 13px;
+ }
+ .lp-mock-person span:not(.lp-mock-avatar) {
+ font-size: 11px;
+ color: var(--color-muted);
+ }
+ .lp-mock-avatar {
+ display: grid;
+ flex-shrink: 0;
+ place-items: center;
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background: var(--color-ink);
+ color: var(--color-paper);
+ font-family: var(--font-display);
+ font-size: 13px;
+ font-weight: 700;
+ }
+ .lp-mock-pills {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-top: 12px;
+ }
+ .lp-mock-pills li {
+ padding: 3px 9px;
+ border: 1px solid var(--lp-line);
+ border-radius: 999px;
+ color: var(--color-ink);
+ font-size: 10.5px;
+ font-weight: 500;
+ }
+ .lp-mock-graph {
+ display: grid;
+ grid-template-columns: repeat(14, minmax(0, 1fr));
+ gap: 3px;
+ margin-top: 14px;
+ }
+ .lp-mock-graph span {
+ aspect-ratio: 1;
+ border-radius: 2px;
+ background: var(--lp-line);
+ }
+ .lp-mock-graph span[data-level="1"] {
+ background: color-mix(in srgb, var(--color-brand-soft) 60%, transparent);
+ }
+ .lp-mock-graph span[data-level="2"] {
+ background: var(--color-brand);
+ }
+ .lp-mock-graph span[data-level="3"] {
+ background: var(--color-ink-soft);
+ }
+ .lp-mock-ledger ul {
+ margin-top: 8px;
+ }
+ .lp-mock-ledger li {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto auto;
+ align-items: center;
+ gap: 10px;
+ padding-block: 8px;
+ border-bottom: 1px solid var(--lp-line);
+ font-size: 11px;
+ }
+ .lp-mock-ledger li:last-child {
+ border-bottom: 0;
+ }
+ .lp-mock-ledger code {
+ overflow: hidden;
+ color: var(--color-ink-soft);
+ font-size: 11px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .lp-mock-ledger strong {
+ color: var(--color-ink);
+ font-variant-numeric: tabular-nums;
+ }
+ .lp-mock-dashboard {
+ max-width: 880px;
+ padding-bottom: 0;
+ border-bottom: 0;
+ border-radius: 10px 10px 0 0;
+ }
+ .lp-mock-dash-body {
+ display: grid;
+ grid-template-columns: 160px minmax(0, 1fr);
+ }
+ .lp-mock-dash-nav {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ padding: 0 14px 16px 0;
+ border-right: 1px solid var(--lp-line);
+ font-size: 11.5px;
+ }
+ .lp-mock-dash-nav strong {
+ margin-bottom: 8px;
+ color: var(--color-ink);
+ font-size: 12px;
+ }
+ .lp-mock-dash-nav span {
+ padding: 4px 8px;
+ border-radius: 6px;
+ }
+ .lp-mock-dash-nav .lp-mock-dash-active {
+ background: color-mix(in srgb, var(--color-brand) 12%, transparent);
+ color: var(--color-ink);
+ font-weight: 600;
+ }
+ .lp-mock-dash-main {
+ padding-left: 16px;
+ }
+ .lp-mock-dash-stats {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 10px;
+ }
+ .lp-mock-dash-stats div {
+ padding: 10px;
+ border: 1px solid var(--lp-line);
+ border-radius: 8px;
+ }
+ .lp-mock-dash-stats span {
+ display: block;
+ font-size: 10.5px;
+ }
+ .lp-mock-dash-stats strong {
+ color: var(--color-ink);
+ font-family: var(--font-display);
+ font-size: 18px;
+ font-weight: 600;
+ }
+ .lp-mock-table {
+ width: 100%;
+ margin-top: 14px;
+ font-size: 11.5px;
+ border-collapse: collapse;
+ }
+ .lp-mock-table th {
+ padding: 6px 0;
+ border-bottom: 1px solid var(--lp-line);
+ font-weight: 500;
+ text-align: left;
+ }
+ .lp-mock-table td {
+ padding: 8px 0;
+ border-bottom: 1px solid var(--lp-line);
+ color: var(--color-ink);
+ }
+ .lp-mock-status {
+ padding: 2px 8px;
+ border-radius: 999px;
+ font-size: 10.5px;
+ font-weight: 600;
+ }
+ .lp-mock-status-done {
+ background: color-mix(in srgb, var(--color-success) 13%, transparent);
+ color: var(--color-success);
+ }
+ .lp-mock-status-idle {
+ background: var(--lp-line);
+ color: var(--color-body-copy);
+ }
+
+ /* ---- Feature banner ---- */
+ .lp-banner {
+ position: relative;
+ min-height: 320px;
+ aspect-ratio: 1216 / 400;
+ overflow: hidden;
+ border-radius: var(--radius-card);
+ isolation: isolate;
+ }
+ .lp-banner-scrim {
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(
+ 90deg,
+ rgb(0 0 70 / 0.85) 0%,
+ rgb(0 0 70 / 0.5) 45%,
+ rgb(0 0 70 / 0.05) 80%
+ );
+ }
+ /* Vertically centred on the left, over the darkest part of the scrim. */
+ .lp-banner-content {
+ position: absolute;
+ inset: 0 24px 0 clamp(24px, 3.5vw, 44px);
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+ }
+ .lp-banner-heading {
+ color: #fff;
+ font-family: var(--font-display);
+ font-size: clamp(28px, 3.3vw, 46px);
+ font-weight: 500;
+ line-height: 1.1;
+ letter-spacing: -0.025em;
+ }
+ .lp-banner-mark {
+ position: absolute;
+ right: clamp(20px, 3vw, 36px);
+ bottom: clamp(16px, 2.5vw, 28px);
+ color: #fff;
+ font-family: var(--font-display);
+ font-size: 20px;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ }
+ .lp-banner-mark span {
+ font-weight: 400;
+ opacity: 0.8;
+ }
+
+ /* ---- Tinted bands ---- */
+ .lp-band {
+ background: linear-gradient(180deg, var(--color-paper) 0%, var(--lp-tint) 22%, var(--lp-tint) 100%);
+ }
+ .lp-band-frame {
+ padding-top: var(--lp-space);
+ }
+ .lp-band-heading {
+ font-family: var(--font-display);
+ font-size: clamp(30px, 3.4vw, 48px);
+ font-weight: 500;
+ line-height: 1.1;
+ letter-spacing: -0.025em;
+ color: var(--color-ink);
+ text-align: center;
+ }
+ /* Stats menu: one stat active at a time, marked by a gradient indicator
+ that slides along both rules (components/landing/trust-stats.tsx). */
+ .lp-stats {
+ position: relative;
+ margin-top: clamp(40px, 5vw, 64px);
+ margin-inline: calc(-1 * var(--lp-gutter));
+ border-block: 1px solid var(--lp-line);
+ }
+ .lp-stats-list {
+ display: grid;
+ grid-template-columns: repeat(var(--lp-count, 4), minmax(0, 1fr));
+ }
+ .lp-stats-indicator {
+ position: absolute;
+ left: 0;
+ width: calc(100% / var(--lp-count, 4));
+ height: 1px;
+ background: linear-gradient(90deg, transparent, var(--color-brand) 30%, var(--color-brand-soft) 70%, transparent);
+ transform: translateX(calc(var(--lp-active, 0) * 100%));
+ transition: transform 600ms cubic-bezier(0.65, 0, 0.35, 1);
+ pointer-events: none;
+ }
+ .lp-stats-indicator-top {
+ top: -1px;
+ }
+ .lp-stats-indicator-bottom {
+ bottom: -1px;
+ }
+ .lp-stat {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 28px 16px 20px;
+ text-align: center;
+ }
+ .lp-stat-button {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 6px;
+ border-radius: var(--radius-control);
+ cursor: pointer;
+ }
+ .lp-stat-value {
+ color: var(--lp-quiet);
+ font-family: var(--font-display);
+ font-size: clamp(34px, 3.2vw, 46px);
+ font-weight: 500;
+ line-height: 1;
+ letter-spacing: -0.03em;
+ transition: color 300ms ease;
+ }
+ .lp-stat-label {
+ max-width: 20ch;
+ color: var(--color-body-copy);
+ font-size: 13.5px;
+ line-height: 1.45;
+ }
+ .lp-stat-button:hover .lp-stat-value,
+ .lp-stat-active .lp-stat-value {
+ color: var(--color-ink);
+ }
+ .lp-stat-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+ margin-top: 10px;
+ color: var(--color-ink-soft);
+ font-size: 13px;
+ font-weight: 600;
+ opacity: 0;
+ transform: translateY(4px);
+ transition: opacity 300ms ease, transform 300ms ease;
+ pointer-events: none;
+ }
+ .lp-stat-active .lp-stat-link {
+ opacity: 1;
+ transform: none;
+ pointer-events: auto;
+ }
+ .lp-stat-link:hover {
+ color: var(--color-ink);
+ }
+ .lp-burst {
+ display: block;
+ width: calc(100% + 2 * var(--lp-gutter));
+ height: clamp(220px, 26vw, 340px);
+ margin-inline: calc(-1 * var(--lp-gutter));
+ cursor: crosshair;
+ touch-action: pan-y;
+ }
+
+ /* ---- Split rows (heading + action | paragraph) ---- */
+ .lp-split {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+ align-items: start;
+ gap: clamp(24px, 6vw, 96px);
+ padding-bottom: clamp(32px, 4vw, 48px);
+ }
+ .lp-split-solo {
+ padding-bottom: var(--lp-space);
+ }
+ .lp-split-heading {
+ max-width: 22ch;
+ font-family: var(--font-display);
+ font-size: clamp(22px, 2vw, 28px);
+ font-weight: 500;
+ line-height: 1.25;
+ letter-spacing: -0.015em;
+ color: var(--color-ink);
+ }
+ .lp-split-text {
+ max-width: 52ch;
+ color: var(--color-body-copy);
+ font-size: 16px;
+ line-height: 1.65;
+ }
+ .lp-split-text strong {
+ color: var(--color-ink);
+ }
+
+ /* ---- Case study ---- */
+ .lp-story-head {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 16px;
+ }
+ .lp-story-head p {
+ flex: 1;
+ color: var(--color-ink);
+ font-family: var(--font-display);
+ font-size: 18px;
+ font-weight: 500;
+ }
+ .lp-story-mark {
+ display: grid;
+ flex-shrink: 0;
+ place-items: center;
+ width: 32px;
+ height: 32px;
+ border-radius: var(--radius-control);
+ background: var(--color-brand);
+ color: var(--color-brand-ink);
+ }
+ .lp-story-link {
+ padding: 6px 14px;
+ border: 1px solid var(--lp-line);
+ border-radius: 999px;
+ font-size: 13.5px;
+ }
+ .lp-story-photo {
+ position: relative;
+ aspect-ratio: 1216 / 440;
+ overflow: hidden;
+ border-radius: var(--radius-card);
+ }
+ .lp-story-facts {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 16px;
+ margin: 16px 0 32px;
+ }
+ .lp-story-facts div {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: 6px;
+ }
+ .lp-story-facts dt {
+ color: var(--color-ink);
+ font-family: var(--font-display);
+ font-size: 15px;
+ font-weight: 600;
+ }
+ .lp-story-facts dd {
+ color: var(--color-body-copy);
+ font-size: 13.5px;
+ }
+ .lp-accordion {
+ border-top: 1px solid var(--lp-line);
+ }
+ .lp-accordion-row {
+ border-bottom: 1px solid var(--lp-line);
+ }
+ .lp-accordion-row summary {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding-block: 16px;
+ cursor: pointer;
+ list-style: none;
+ }
+ .lp-accordion-row summary::-webkit-details-marker {
+ display: none;
+ }
+ .lp-accordion-mark {
+ display: grid;
+ flex-shrink: 0;
+ place-items: center;
+ width: 32px;
+ height: 32px;
+ border: 1px solid var(--lp-line);
+ border-radius: var(--radius-control);
+ background: var(--lp-tint);
+ color: var(--color-ink-soft);
+ }
+ .lp-accordion-title {
+ flex: 1;
+ color: var(--color-ink);
+ font-family: var(--font-display);
+ font-size: 17px;
+ font-weight: 500;
+ }
+ .lp-accordion-toggle {
+ display: grid;
+ flex-shrink: 0;
+ place-items: center;
+ width: 28px;
+ height: 28px;
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--color-brand) 12%, transparent);
+ color: var(--color-ink-soft);
+ transition: transform 200ms ease;
+ }
+ .lp-accordion-row[open] .lp-accordion-toggle {
+ transform: rotate(45deg);
+ }
+ .lp-accordion-row p {
+ max-width: 70ch;
+ padding: 0 0 20px 46px;
+ color: var(--color-body-copy);
+ font-size: 15px;
+ line-height: 1.65;
+ }
+
+ /* ---- Journey carousel ---- */
+ .lp-carousel-controls {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ margin-bottom: 16px;
+ }
+ .lp-carousel-button {
+ display: grid;
+ place-items: center;
+ width: 32px;
+ height: 32px;
+ border: 1px solid var(--lp-line);
+ border-radius: var(--radius-control);
+ background: var(--color-surface);
+ color: var(--color-ink-soft);
+ transition: background 150ms ease;
+ }
+ .lp-carousel-button:hover {
+ background: color-mix(in srgb, var(--color-brand) 12%, var(--color-surface));
+ }
+ .lp-carousel-track {
+ display: grid;
+ grid-auto-columns: calc((100% - 3 * 16px) / 3.5);
+ grid-auto-flow: column;
+ gap: 16px;
+ margin-right: calc(-1 * var(--lp-gutter));
+ padding-right: var(--lp-gutter);
+ overflow-x: auto;
+ overscroll-behavior-x: contain;
+ scroll-snap-type: x mandatory;
+ scrollbar-width: none;
+ }
+ .lp-carousel-track::-webkit-scrollbar {
+ display: none;
+ }
+ .lp-journey-card {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+ scroll-snap-align: start;
+ }
+ .lp-journey-photo {
+ position: relative;
+ width: 100%;
+ aspect-ratio: 3 / 4;
+ overflow: hidden;
+ border-radius: var(--radius-card);
+ }
+ .lp-journey-text {
+ color: var(--color-body-copy);
+ font-size: 14.5px;
+ line-height: 1.55;
+ }
+ .lp-journey-text strong {
+ color: var(--color-ink);
+ }
+
+ .lp-promos {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 16px;
+ margin-top: 40px;
+ }
+ .lp-promo {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 16px;
+ min-height: 160px;
+ padding: 24px 44% 24px 24px;
+ overflow: hidden;
+ border: 1px solid var(--lp-line);
+ border-radius: var(--radius-card);
+ background: var(--lp-tint);
+ transition: border-color 200ms ease;
+ }
+ .lp-promo:hover {
+ border-color: color-mix(in srgb, var(--color-brand) 45%, transparent);
+ }
+ .lp-promo p {
+ color: var(--color-body-copy);
+ font-size: 14.5px;
+ line-height: 1.55;
+ }
+ .lp-promo strong {
+ color: var(--color-ink);
+ }
+ .lp-promo-art {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 40%;
+ }
+ .lp-promo-art-a {
+ background: linear-gradient(135deg, var(--color-brand-soft), var(--color-brand) 45%, var(--lp-navy));
+ clip-path: polygon(38% 0, 100% 0, 100% 100%, 0 100%);
+ }
+ .lp-promo-art-b {
+ background: linear-gradient(200deg, var(--lp-blue-deep), var(--color-brand) 50%, var(--color-brand-soft));
+ clip-path: polygon(100% 0, 100% 100%, 0 100%);
+ }
+
+ /* ---- Hiring showcase ---- */
+ .lp-showcase {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 340px;
+ padding: clamp(24px, 5vw, 56px);
+ overflow: hidden;
+ border-radius: var(--radius-card);
+ background:
+ repeating-linear-gradient(115deg, rgb(255 255 255 / 0.1) 0 1px, transparent 1px 7px),
+ linear-gradient(120deg, var(--color-brand-soft), var(--color-brand) 38%, var(--lp-blue-deep) 72%, var(--lp-navy));
+ }
+ .lp-showcase-card {
+ width: min(460px, 100%);
+ padding: 20px;
+ border-radius: var(--radius-card);
+ background: var(--color-surface);
+ box-shadow: 0 24px 48px -12px rgb(0 0 70 / 0.35);
+ color: var(--color-body-copy);
+ font-size: 12.5px;
+ }
+ .lp-showcase-rows {
+ margin-top: 14px;
+ }
+ .lp-showcase-rows li {
+ display: grid;
+ grid-template-columns: 70px minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 10px;
+ padding-block: 10px;
+ border-bottom: 1px solid var(--lp-line);
+ }
+ .lp-showcase-rows strong {
+ color: var(--color-ink);
+ font-weight: 600;
+ }
+ .lp-showcase-rows em {
+ padding: 2px 9px;
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--color-success) 13%, transparent);
+ color: var(--color-success);
+ font-size: 11px;
+ font-style: normal;
+ font-weight: 600;
+ }
+ .lp-showcase-cta {
+ margin-top: 16px;
+ padding: 9px;
+ border-radius: 999px;
+ background: var(--color-brand);
+ color: var(--color-brand-ink);
+ font-weight: 600;
+ text-align: center;
+ }
+
+ .lp-features {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 32px;
+ margin-top: 40px;
+ }
+ .lp-features li {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 10px;
+ }
+ .lp-features p {
+ color: var(--color-body-copy);
+ font-size: 14.5px;
+ line-height: 1.6;
+ }
+ .lp-features strong {
+ color: var(--color-ink);
+ }
+ .lp-feature-icon {
+ display: grid;
+ place-items: center;
+ width: 32px;
+ height: 32px;
+ border: 1px solid var(--lp-line);
+ border-radius: var(--radius-control);
+ background: var(--lp-tint);
+ color: var(--color-ink-soft);
+ }
+
+ .lp-quote {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+ max-width: 760px;
+ margin: clamp(56px, 7vw, 88px) auto 0;
+ text-align: center;
+ }
+ .lp-quote blockquote {
+ color: var(--lp-quiet);
+ font-family: var(--font-display);
+ font-size: clamp(20px, 2vw, 26px);
+ line-height: 1.4;
+ letter-spacing: -0.01em;
+ }
+ .lp-quote figcaption {
+ color: var(--color-body-copy);
+ font-size: 14px;
+ }
+ .lp-quote figcaption strong {
+ color: var(--color-ink);
+ }
+
+ /* ---- Navy infrastructure band ---- */
+ .lp-dark {
+ --lp-line: color-mix(in srgb, var(--color-on-inverse) 14%, transparent);
+ --lp-quiet: color-mix(in srgb, var(--color-on-inverse) 58%, var(--color-brand-soft));
+ background: color-mix(in srgb, var(--lp-navy) 72%, #000);
+ color: var(--color-on-inverse);
+ }
+ /* On the black dark-theme page the band needs its own navy to read as a band. */
+ [data-theme="dark"] .lp-dark {
+ background: color-mix(in srgb, var(--lp-navy) 36%, #000);
+ }
+ .lp-dark .lp-statement {
+ color: var(--color-on-inverse);
+ }
+ .lp-dark-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-top: 32px;
+ }
+ .lp-flow {
+ margin-top: 48px;
+ padding: clamp(28px, 4vw, 48px) clamp(16px, 3vw, 32px);
+ border: 1px solid var(--lp-line);
+ border-radius: var(--radius-card);
+ background-image: radial-gradient(color-mix(in srgb, var(--color-on-inverse) 14%, transparent) 1px, transparent 1px);
+ background-size: 16px 16px;
+ }
+ .lp-flow-row {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ }
+ .lp-flow-node {
+ padding: 10px 14px;
+ border: 1px solid color-mix(in srgb, var(--color-brand) 50%, transparent);
+ border-radius: var(--radius-control);
+ background: color-mix(in srgb, var(--color-brand) 16%, transparent);
+ text-align: center;
+ }
+ .lp-flow-node span {
+ display: block;
+ color: var(--color-on-inverse);
+ font-size: 13px;
+ font-weight: 600;
+ white-space: nowrap;
+ }
+ .lp-flow-node em {
+ display: block;
+ margin-top: 2px;
+ color: var(--lp-quiet);
+ font-size: 11px;
+ font-style: normal;
+ white-space: nowrap;
+ }
+ .lp-flow-node-core {
+ border-color: var(--color-brand);
+ background: var(--color-brand);
+ box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-brand) 22%, transparent);
+ }
+ .lp-flow-node-core span,
+ .lp-flow-node-core em {
+ color: var(--color-brand-ink);
+ }
+ .lp-flow-split {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+ .lp-flow-arrow {
+ flex-shrink: 0;
+ color: var(--lp-quiet);
+ }
+ .lp-flow-ledger {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ gap: 10px 14px;
+ width: fit-content;
+ max-width: 100%;
+ margin: 32px auto 0;
+ padding: 10px 18px;
+ border: 1px dashed var(--lp-line);
+ border-radius: 999px;
+ color: var(--lp-quiet);
+ font-size: 13px;
+ text-align: center;
+ }
+ .lp-flow-ledger strong {
+ color: var(--color-on-inverse);
+ }
+ .lp-flow-tag {
+ padding: 2px 9px;
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--color-brand) 24%, transparent);
+ color: var(--color-brand-soft);
+ font-size: 11px;
+ font-weight: 600;
+ }
+ .lp-flow-ledger-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ color: var(--color-brand-soft);
+ font-weight: 600;
+ }
+ .lp-flow-ledger-link:hover {
+ color: var(--color-on-inverse);
+ }
+ .lp-waves {
+ display: block;
+ width: calc(100% + 2 * var(--lp-gutter));
+ height: clamp(150px, 20vw, 260px);
+ margin: 16px calc(-1 * var(--lp-gutter)) 8px;
+ }
+ .lp-dark-stats {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 24px;
+ }
+ .lp-dark-stats li {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ color: var(--lp-quiet);
+ font-size: 14px;
+ }
+ .lp-dark-stat-value {
+ width: fit-content;
+ background: linear-gradient(90deg, var(--color-brand-soft), var(--color-brand));
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ font-family: var(--font-display);
+ font-size: clamp(40px, 4.4vw, 60px);
+ font-weight: 500;
+ line-height: 1.05;
+ letter-spacing: -0.03em;
+ }
+ .lp-paths {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 16px;
+ margin-top: 40px;
+ }
+ .lp-path {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 14px;
+ }
+ .lp-path p {
+ color: var(--lp-quiet);
+ font-size: 14.5px;
+ line-height: 1.6;
+ }
+ .lp-path strong {
+ color: var(--color-on-inverse);
+ }
+ .lp-path-visual {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 8px;
+ width: 100%;
+ height: 200px;
+ margin: 0;
+ padding: 20px;
+ overflow: hidden;
+ border: 1px solid var(--lp-line);
+ border-radius: var(--radius-card);
+ background: color-mix(in srgb, var(--color-brand) 10%, transparent);
+ }
+ .lp-path-event {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 10px 12px;
+ border: 1px solid var(--lp-line);
+ border-radius: var(--radius-control);
+ background: color-mix(in srgb, var(--color-on-inverse) 7%, transparent);
+ font-size: 12px;
+ font-weight: 500;
+ }
+ .lp-path-event em {
+ display: inline-flex;
+ flex-shrink: 0;
+ align-items: center;
+ gap: 4px;
+ color: var(--color-brand-soft);
+ font-size: 10.5px;
+ font-style: normal;
+ font-weight: 600;
+ }
+ .lp-path-step {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ color: var(--lp-quiet);
+ font-size: 12.5px;
+ }
+ .lp-path-step span {
+ display: grid;
+ place-items: center;
+ width: 22px;
+ height: 22px;
+ border: 1px solid var(--lp-line);
+ border-radius: 50%;
+ font-size: 10.5px;
+ font-weight: 600;
+ }
+ .lp-path-step-done {
+ color: var(--color-on-inverse);
+ }
+ .lp-path-step-done span {
+ border-color: var(--color-brand);
+ background: var(--color-brand);
+ color: var(--color-brand-ink);
+ }
+ .lp-path-code {
+ justify-content: flex-start;
+ color: var(--color-on-inverse);
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 12.5px;
+ line-height: 1.85;
+ white-space: pre;
+ }
+ .lp-code-dim {
+ margin-right: 6px;
+ opacity: 0.4;
+ }
+ .lp-code-key {
+ color: var(--color-brand-soft);
+ }
+ .lp-code-ok {
+ color: #36d399;
+ }
+
+ /* ---- Latest posts ---- */
+ .lp-latest {
+ display: grid;
+ grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
+ gap: 16px 32px;
+ margin-top: 40px;
+ }
+ .lp-latest-feature {
+ position: relative;
+ min-height: 300px;
+ aspect-ratio: 16 / 8;
+ overflow: hidden;
+ border-radius: var(--radius-card);
+ isolation: isolate;
+ }
+ .lp-latest-feature img {
+ transition: transform 500ms ease;
+ }
+ .lp-latest-feature:hover img {
+ transform: scale(1.03);
+ }
+ .lp-latest-scrim {
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(180deg, transparent 35%, rgb(0 0 70 / 0.82));
+ }
+ .lp-latest-category {
+ position: absolute;
+ top: 16px;
+ left: 16px;
+ padding: 3px 10px;
+ border-radius: 999px;
+ background: rgb(255 255 255 / 0.88);
+ color: #000092;
+ font-size: 11px;
+ font-weight: 600;
+ }
+ .lp-latest-title {
+ position: absolute;
+ right: 24px;
+ bottom: 22px;
+ left: 24px;
+ color: #fff;
+ font-family: var(--font-display);
+ font-size: clamp(22px, 2.4vw, 32px);
+ font-weight: 500;
+ line-height: 1.15;
+ letter-spacing: -0.02em;
+ }
+ .lp-latest-list {
+ border-top: 1px solid var(--lp-line);
+ }
+ .lp-latest-list li {
+ border-bottom: 1px solid var(--lp-line);
+ }
+ .lp-latest-item {
+ display: block;
+ padding-block: 18px;
+ }
+ .lp-latest-meta {
+ display: block;
+ color: var(--color-muted);
+ font-size: 12px;
+ font-weight: 500;
+ }
+ .lp-latest-item-title {
+ display: block;
+ margin-top: 4px;
+ color: var(--color-ink);
+ font-family: var(--font-display);
+ font-size: 17px;
+ font-weight: 500;
+ line-height: 1.35;
+ transition: color 150ms ease;
+ }
+ .lp-latest-item:hover .lp-latest-item-title {
+ color: var(--color-ink-soft);
+ }
+ .lp-latest-foot {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 24px;
+ margin-top: 24px;
+ }
+ .lp-latest-foot p {
+ max-width: 64ch;
+ color: var(--color-body-copy);
+ font-size: 15px;
+ line-height: 1.6;
+ }
+ .lp-latest-foot strong {
+ color: var(--color-ink);
+ }
+
+ /* ---- Get started ---- */
+ .lp-band-end {
+ background: var(--lp-tint);
+ }
+ .lp-start {
+ display: grid;
+ grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, 1fr);
+ align-items: start;
+ gap: 40px;
+ }
+ .lp-start-heading {
+ color: var(--color-ink);
+ font-family: var(--font-display);
+ font-size: clamp(26px, 2.6vw, 34px);
+ font-weight: 500;
+ line-height: 1.2;
+ letter-spacing: -0.02em;
+ }
+ .lp-start-text {
+ max-width: 44ch;
+ margin-top: 14px;
+ color: var(--color-body-copy);
+ font-size: 16px;
+ line-height: 1.6;
+ }
+ .lp-start-item {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ padding-top: 8px;
+ color: var(--color-body-copy);
+ font-size: 14.5px;
+ line-height: 1.6;
+ }
+ .lp-start-item-title {
+ margin-top: 6px;
+ color: var(--color-ink);
+ font-weight: 600;
+ }
+
+ /* ---- Responsive ---- */
+ @media (max-width: 1023px) {
+ .lp-frame {
+ border-inline-color: transparent;
+ }
+ .lp-bento {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ .lp-bento-wide {
+ grid-column: 1 / -1;
+ }
+ .lp-bento-full {
+ grid-template-columns: minmax(0, 1fr);
+ }
+ .lp-bento-full .lp-card-visual {
+ padding-top: 0;
+ }
+ .lp-stats-list {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ .lp-stat:nth-child(-n + 2) {
+ border-bottom: 1px solid var(--lp-line);
+ }
+ /* In the 2x2 grid the sliding rule has no single track, so the active
+ stat gets a tint instead. */
+ .lp-stats-indicator {
+ display: none;
+ }
+ .lp-stat-active {
+ background: color-mix(in srgb, var(--color-brand) 7%, transparent);
+ }
+ .lp-split,
+ .lp-latest,
+ .lp-promos {
+ grid-template-columns: minmax(0, 1fr);
+ }
+ .lp-carousel-track {
+ grid-auto-columns: calc((100% - 16px) / 2.3);
+ }
+ .lp-features,
+ .lp-paths {
+ grid-template-columns: minmax(0, 1fr);
+ gap: 32px;
+ }
+ .lp-flow-row {
+ flex-direction: column;
+ }
+ .lp-flow-arrow {
+ transform: rotate(90deg);
+ }
+ .lp-flow-split {
+ flex-direction: row;
+ }
+ .lp-start {
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+ }
+ .lp-start > :first-child {
+ grid-column: 1 / -1;
+ }
+ }
+
+ @media (max-width: 639px) {
+ .lp-ribbon {
+ width: 120vw;
+ height: 58%;
+ right: -30vw;
+ opacity: 0.85;
+ }
+ .lp-hero-inner {
+ padding-top: 42vw;
+ }
+ .lp-hero-actions > * {
+ flex: 1 1 100%;
+ }
+ .lp-bento {
+ grid-template-columns: minmax(0, 1fr);
+ }
+ .lp-card {
+ min-height: 380px;
+ }
+ .lp-mock-pair {
+ grid-template-columns: minmax(0, 1fr);
+ }
+ .lp-mock-vault {
+ display: none;
+ }
+ .lp-mock-dash-body {
+ grid-template-columns: minmax(0, 1fr);
+ }
+ .lp-mock-dash-nav {
+ display: none;
+ }
+ .lp-mock-dash-main {
+ padding-left: 0;
+ }
+ .lp-mock-dash-stats {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ .lp-banner {
+ aspect-ratio: auto;
+ min-height: 360px;
+ }
+ .lp-banner-scrim {
+ background: linear-gradient(180deg, rgb(0 0 70 / 0.85), rgb(0 0 70 / 0.35));
+ }
+ .lp-story-head {
+ flex-wrap: wrap;
+ }
+ .lp-story-head p {
+ flex-basis: calc(100% - 44px);
+ }
+ .lp-story-photo {
+ aspect-ratio: 4 / 3;
+ }
+ .lp-story-facts {
+ grid-template-columns: minmax(0, 1fr);
+ gap: 8px;
+ }
+ .lp-carousel-track {
+ grid-auto-columns: 75%;
+ }
+ .lp-promo {
+ padding-right: 24px;
+ padding-bottom: 96px;
+ }
+ .lp-promo-art {
+ top: auto;
+ width: 100%;
+ height: 72px;
+ }
+ .lp-dark-stats {
+ grid-template-columns: minmax(0, 1fr);
+ }
+ .lp-flow-split {
+ flex-direction: column;
+ }
+ .lp-latest-feature {
+ min-height: 0;
+ aspect-ratio: 4 / 3;
+ }
+ .lp-latest-foot {
+ flex-direction: column;
+ }
+ .lp-start {
+ grid-template-columns: minmax(0, 1fr);
+ }
+ }
+}
diff --git a/components/landing/audience-sections.tsx b/components/landing/audience-sections.tsx
new file mode 100644
index 0000000..c6279d4
--- /dev/null
+++ b/components/landing/audience-sections.tsx
@@ -0,0 +1,336 @@
+import Image from "next/image";
+import Link from "next/link";
+import {
+ BadgeCheck,
+ CalendarCheck,
+ Camera,
+ Handshake,
+ ListChecks,
+ Plus,
+ ShieldCheck,
+ Sparkles,
+ type LucideIcon,
+} from "lucide-react";
+
+import { JourneyCarousel } from "@/components/landing/journey-carousel";
+import { LandingLink, TextLink } from "@/components/landing/landing-link";
+
+const GITHUB_URL = "https://github.com/CodeWithEugene/HackVillage";
+
+const ORGANIZER_FACTS = [
+ { value: "100%", label: "deposited before launch" },
+ { value: "5%", label: "platform fee, paid on top of the pool" },
+ { value: "30 days", label: "for each winner's milestone" },
+] as const;
+
+const ORGANIZER_ROWS: { Icon: LucideIcon; title: string; body: string }[] = [
+ {
+ Icon: ListChecks,
+ title: "Judges score every team against the same rubric.",
+ body: "Invite judges by email, set weighted criteria, and send every team written feedback alongside its scores, so results are something people can check rather than argue about.",
+ },
+ {
+ Icon: Camera,
+ title: "Photos and recaps are due within 48 hours of the close.",
+ body: "Media lands on the hackathon page while people still care, and the deadline counts toward the organizer's public Trust Score.",
+ },
+ {
+ Icon: CalendarCheck,
+ title: "Three months later, winners check in on what they shipped.",
+ body: "Legacy check-ins keep a record of what happened after demo day, and disputes go to the HackVillage team with the money still locked in escrow.",
+ },
+];
+
+const JOURNEY = [
+ {
+ image: "kenyan-developer-building-hackathon-project",
+ alt: "Kenyan software engineer building her hackathon project",
+ title: "Build with a team.",
+ body: "Form a team or join one with an invite code, then ship before the deadline.",
+ href: "/hackathons",
+ link: "Find a hackathon",
+ },
+ {
+ image: "developer-demoing-mobile-app",
+ alt: "Kenyan developer demonstrating the mobile app he built",
+ title: "Demo to real judges.",
+ body: "Every team is scored on the same published rubric and gets written feedback.",
+ href: "/blog/judging-scorecards-teams-trust",
+ link: "How judging works",
+ },
+ {
+ image: "hackathon-winner-with-trophy-and-laptop",
+ alt: "Kenyan hackathon winner holding her trophy and laptop",
+ title: "Win a funded prize.",
+ body: "The prize was deposited in escrow before you wrote a single line of code.",
+ href: "/how-escrow-works",
+ link: "How escrow works",
+ },
+ {
+ image: "developer-receiving-hackathon-prize-payout",
+ alt: "Kenyan developer checking his phone after receiving a prize payout",
+ title: "Get paid in minutes.",
+ body: "Half your prize goes to M-Pesa or your bank the moment results are announced.",
+ href: "/blog/how-winners-get-paid",
+ link: "How winners get paid",
+ },
+ {
+ image: "engineer-presenting-portfolio-to-hiring-manager",
+ alt: "Kenyan engineer presenting her project portfolio to a hiring manager",
+ title: "Get hired on proof.",
+ body: "Hiring teams request intros from your verified Proof-of-Work profile.",
+ href: "/blog/proof-of-work-portfolio",
+ link: "Build your portfolio",
+ },
+] as const;
+
+const HIRING_FEATURES: {
+ Icon: LucideIcon;
+ title: string;
+ body: string;
+ href: string;
+ link: string;
+}[] = [
+ {
+ Icon: BadgeCheck,
+ title: "Verified results.",
+ body: "Every win on a profile is tied to a real hackathon with a funded, escrowed prize.",
+ href: "/how-escrow-works",
+ link: "How results are verified",
+ },
+ {
+ Icon: Sparkles,
+ title: "Judge endorsements.",
+ body: "Judges who scored a project can endorse its builders, on the record and by name.",
+ href: "/blog/proof-of-work-portfolio",
+ link: "Read about Proof-of-Work",
+ },
+ {
+ Icon: Handshake,
+ title: "Warm introductions.",
+ body: "Request an intro from a builder's profile. They decide whether to reply, and when.",
+ href: "/hiring/join",
+ link: "Become a hiring partner",
+ },
+];
+
+function OrganizersBlock() {
+ return (
+ <>
+
+
+
Host a hackathon people trust from day one
+
+ HackVillage For Organizers
+
+
+
+ Deposit the prize pool once through Paystack and HackVillage handles the rest: the Prize
+ Verified badge, judge invitations, results, payouts and a paper trail anyone can check.
+
+
+
+
+
+
+
+
+
+
Every prize pool is funded before registration opens.
+
+ Read how escrow works
+
+
+
+
+
+
+ {ORGANIZER_FACTS.map((fact) => (
+
+
{fact.value}
+ {fact.label}
+
+ ))}
+
+
+ {ORGANIZER_ROWS.map(({ Icon, title, body }) => (
+
+
+
+
+
+ {title}
+
+
+
+
+ {body}
+
+ ))}
+
+
+
+ >
+ );
+}
+
+function BuildersBlock() {
+ return (
+ <>
+
+
+
Build a portfolio that proves what you shipped
+
+ Explore Hackathons
+
+
+
+ Join a team, ship your project and get half your prize the moment you win. Every result,
+ endorsement and payout lands on your public developer profile.
+
+
+
+
+
+ {JOURNEY.map((step) => (
+
+
+
+
+
+ {step.title} {step.body}
+
+ {step.link}
+
+ ))}
+
+
+
+
+ >
+ );
+}
+
+function HiringBlock() {
+ return (
+ <>
+
+
+
Hire from verified Proof-of-Work, not CVs
+
+ Join The Hiring Network
+
+
+
+ Every HackVillage profile is built from real results: projects scored by judges, prizes
+ actually paid, and follow-ups three months on. Find builders by what they shipped.
+
+
+
+
+
+
+
+
WK
+
+ Wanjiku K.
+ Full-stack engineer · Nairobi
+
+
+
+
+ 1st place
+ Nairobi AI Hackathon
+ Paid
+
+
+ Finalist
+ Mombasa Fintech Sprint
+ Endorsed
+
+
+ Winner
+ Kisumu Climate Build
+ Shipped
+
+
+
Request an introduction
+
+
+
+
+ {HIRING_FEATURES.map(({ Icon, title, body, href, link }) => (
+
+
+
+
+
+ {title} {body}
+
+ {link}
+
+ ))}
+
+
+
+
+ “If the prize isn't in the vault, the hackathon doesn't go live. Every
+ payout after that is on the record.”
+
+
+ The HackVillage escrow rule, enforced in code since v1.0
+
+ See the public ledger
+
+
+ >
+ );
+}
+
+export function AudienceSections() {
+ return (
+
+
+
+ Built for everyone in the room.{" "}
+
+ Organizers, builders, judges and hiring teams each get tools that fit how they already
+ work.
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/landing/get-started.tsx b/components/landing/get-started.tsx
new file mode 100644
index 0000000..b1c3175
--- /dev/null
+++ b/components/landing/get-started.tsx
@@ -0,0 +1,49 @@
+import { Code2, ReceiptText } from "lucide-react";
+
+import { LandingLink, TextLink } from "@/components/landing/landing-link";
+
+const CONTACT_EMAIL = "info@hackvillage.xyz";
+
+/** Closing call to action, Stripe's "Ready to get started?" block. */
+export function GetStarted() {
+ return (
+
+
+
+
+ Ready to run a hackathon people trust?
+
+
+ Create an account in minutes, or talk to us about a hackathon series for your community,
+ campus or company.
+
+
+ Sign Up
+
+ Contact Us
+
+
+
+
+
+
+
+
Transparent pricing
+
+ A flat 5% platform fee, paid by the organizer on top of the prize pool. Winners keep
+ every shilling.
+
+
How escrow works
+
+
+
+
+
+
Start contributing
+
Open source and ready to run locally, with seeded demo data, in a few commands.
+
Contributor guide
+
+
+
+ );
+}
diff --git a/components/landing/journey-carousel.tsx b/components/landing/journey-carousel.tsx
new file mode 100644
index 0000000..e6b7d86
--- /dev/null
+++ b/components/landing/journey-carousel.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import { useRef } from "react";
+import { ArrowLeft, ArrowRight } from "lucide-react";
+
+/**
+ * A horizontally scrolling row with Stripe-style prev/next buttons. The
+ * cards are server-rendered children; this only adds the scroll controls,
+ * and the row still scrolls by touch, trackpad or keyboard without them.
+ */
+export function JourneyCarousel({ label, children }: { label: string; children: React.ReactNode }) {
+ const trackRef = useRef
(null);
+
+ const scroll = (direction: 1 | -1) => {
+ const track = trackRef.current;
+ if (!track) return;
+ const card = track.querySelector("li");
+ const step = card ? card.getBoundingClientRect().width + 16 : track.clientWidth * 0.8;
+ track.scrollBy({ left: direction * step, behavior: "smooth" });
+ };
+
+ return (
+
+
+
scroll(-1)}
+ aria-label="Previous"
+ >
+
+
+
scroll(1)}
+ aria-label="Next"
+ >
+
+
+
+
+
+ );
+}
diff --git a/components/landing/landing-about.tsx b/components/landing/landing-about.tsx
new file mode 100644
index 0000000..a9d2439
--- /dev/null
+++ b/components/landing/landing-about.tsx
@@ -0,0 +1,31 @@
+import { LandingLink } from "@/components/landing/landing-link";
+
+/**
+ * A short, plain answer to "What is HackVillage?" for visitors, search
+ * engines and AI answer engines. It names the brand both ways people search
+ * for it and states only what the platform already enforces in code, so an
+ * engine quoting it never repeats something that isn't true.
+ */
+export function LandingAbout() {
+ return (
+
+
+
+
+ What Is HackVillage?
+
+
+ How It Works
+
+
+
+ HackVillage (also written Hack Village) is an open-source hackathon
+ platform for Kenya and Africa. Organizers deposit the full prize pool into escrow before a
+ hackathon goes live, so every prize listed here is funded, and winners are paid half of
+ their prize as soon as results are announced. Every result also builds the winner's
+ Proof-of-Work developer profile.
+
+
+
+ );
+}
diff --git a/components/landing/landing-banner.tsx b/components/landing/landing-banner.tsx
new file mode 100644
index 0000000..cde8997
--- /dev/null
+++ b/components/landing/landing-banner.tsx
@@ -0,0 +1,36 @@
+import Image from "next/image";
+
+import { LandingLink } from "@/components/landing/landing-link";
+
+/** The full-width feature banner: one photo, one promise, one next step. */
+export function LandingBanner() {
+ return (
+
+
+
+
+
+
+
+ Organizers ghost winners.
+
+ We fixed that.
+
+
+ How Escrow Works
+
+
+
+ HackVillage Nairobi
+
+
+
+
+ );
+}
diff --git a/components/landing/landing-hero.tsx b/components/landing/landing-hero.tsx
new file mode 100644
index 0000000..83819f3
--- /dev/null
+++ b/components/landing/landing-hero.tsx
@@ -0,0 +1,110 @@
+import { LandingLink } from "@/components/landing/landing-link";
+import { HOST_HACKATHON_HREF } from "@/lib/auth/signup-links";
+
+/*
+ * The ribbon's two edges, as cubic Béziers from the top of the hero to its
+ * right edge. Strands are drawn by interpolating between them, so they
+ * always sit inside the band and follow its curve.
+ */
+const EDGE_A = [
+ [330, -20],
+ [400, 250],
+ [620, 420],
+ [1020, 430],
+] as const;
+const EDGE_B = [
+ [610, -20],
+ [480, 330],
+ [650, 650],
+ [1020, 720],
+] as const;
+
+function lerpCurve(t: number): string {
+ const p = EDGE_A.map(([ax, ay], i) => {
+ const [bx, by] = EDGE_B[i];
+ return `${(ax + (bx - ax) * t).toFixed(1)} ${(ay + (by - ay) * t).toFixed(1)}`;
+ });
+ return `M ${p[0]} C ${p[1]}, ${p[2]}, ${p[3]}`;
+}
+
+const STRANDS = Array.from({ length: 48 }, (_, i) => lerpCurve(i / 47));
+
+const BAND = `M 330 -20 C 400 250, 620 420, 1020 430 L 1020 720 C 650 650, 480 330, 610 -20 Z`;
+const FOLD = `M 520 -20 C 540 250, 700 470, 1020 560 L 1020 660 C 700 600, 560 330, 600 -20 Z`;
+
+/**
+ * The hero ribbon: a broad band in the brand blues, a darker fold across
+ * it, and combed strands for the silky texture, bleeding off the top-right
+ * corner. Pure SVG, so it themes with the tokens and costs nothing to load.
+ */
+function HeroRibbon() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {STRANDS.map((d) => (
+
+ ))}
+
+
+
+ );
+}
+
+export function LandingHero() {
+ return (
+
+
+
+
+ Prize pools escrowed before a hackathon goes live: 100%
+
+
+ Hackathon infrastructure that pays winners on time.{" "}
+
+ Escrow the full prize pool before launch, pay winners half the moment results land, and
+ turn every submission into a verified Proof-of-Work portfolio.
+
+
+
+ Browse Hackathons
+
+ Host A Hackathon
+
+
+
+
+ );
+}
diff --git a/components/landing/landing-link.tsx b/components/landing/landing-link.tsx
new file mode 100644
index 0000000..3e24891
--- /dev/null
+++ b/components/landing/landing-link.tsx
@@ -0,0 +1,68 @@
+import Link from "next/link";
+import { ArrowUpRight, ChevronRight } from "lucide-react";
+
+import { buttonVariants } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+
+interface LandingLinkProps {
+ href: string;
+ children: React.ReactNode;
+ variant?: "primary" | "secondary" | "inverse";
+ className?: string;
+ external?: boolean;
+}
+
+/**
+ * A navigation link dressed as the pill button, so the landing page keeps the
+ * button design system (sweep fill + nudging arrow) without nesting a
+ * inside an . `inverse` is the outline pill on the navy band.
+ */
+export function LandingLink({
+ href,
+ children,
+ variant = "primary",
+ className,
+ external = false,
+}: LandingLinkProps) {
+ const variantClass =
+ variant === "inverse"
+ ? "btn-pill-inverse border border-on-inverse/30 bg-transparent text-on-inverse"
+ : buttonVariants({ variant, size: "md" });
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+
+/** Stripe-style inline text link: brand colour, a small chevron that slides on hover. */
+export function TextLink({
+ href,
+ children,
+ className,
+}: {
+ href: string;
+ children: React.ReactNode;
+ className?: string;
+}) {
+ return (
+
+ {children}
+
+
+ );
+}
diff --git a/components/landing/latest-posts.tsx b/components/landing/latest-posts.tsx
new file mode 100644
index 0000000..c05c361
--- /dev/null
+++ b/components/landing/latest-posts.tsx
@@ -0,0 +1,62 @@
+import Image from "next/image";
+import Link from "next/link";
+
+import { LandingLink } from "@/components/landing/landing-link";
+import { allPosts, formatPostDate } from "@/lib/blog";
+
+/** Stripe's "What's happening": one featured post, the next few as a list. */
+export function LatestPosts() {
+ const [featured, ...rest] = allPosts();
+ if (!featured) return null;
+ const more = rest.slice(0, 3);
+
+ return (
+
+
+
+ What's happening
+ The latest from the HackVillage blog.
+
+
+
+
+
+
+
{featured.meta.category}
+
{featured.meta.title}
+
+
+ {more.map((post) => (
+
+
+
+ {post.meta.category} · {formatPostDate(post.meta.publishedAt)}
+
+ {post.meta.title}
+
+
+ ))}
+
+
+
+
+
+
+ {featured.meta.category} · {formatPostDate(featured.meta.publishedAt)}.
+ {" "}
+ {featured.meta.excerpt}
+
+
+ Read The Blog
+
+
+
+
+ );
+}
diff --git a/components/landing/open-infrastructure.tsx b/components/landing/open-infrastructure.tsx
new file mode 100644
index 0000000..8de531c
--- /dev/null
+++ b/components/landing/open-infrastructure.tsx
@@ -0,0 +1,249 @@
+import Link from "next/link";
+import { ArrowRight, ArrowUpRight, Check } from "lucide-react";
+
+import { LandingLink, TextLink } from "@/components/landing/landing-link";
+import { HOST_HACKATHON_HREF } from "@/lib/auth/signup-links";
+
+const GITHUB_URL = "https://github.com/CodeWithEugene/HackVillage";
+const BUILD_PLAN_URL = `${GITHUB_URL}/blob/main/docs/BUILD_PLAN.md`;
+
+const STATS = [
+ { value: "50/50", label: "split on every milestone prize" },
+ { value: "1", label: "idempotency key per winner, per tranche" },
+ { value: "100%", label: "open source, from the vault to the UI" },
+] as const;
+
+/** Evenly spaced sine strands for the "scale" visual, drawn once on the server. */
+const WAVES = Array.from({ length: 36 }, (_, i) => {
+ const amp = 30 + i * 2.2;
+ const phase = i * 0.09;
+ const points = Array.from({ length: 61 }, (_, j) => {
+ const x = j * 20;
+ const envelope = Math.sin((Math.PI * j) / 60);
+ const y = 150 + Math.sin(j / 7 + phase) * amp * envelope - i * 1.4 * envelope;
+ return `${j === 0 ? "M" : "L"} ${x} ${y.toFixed(1)}`;
+ });
+ return points.join(" ");
+});
+
+function MoneyFlow() {
+ return (
+
+
+
+ Organizer
+ Deposits the full pool
+
+
+
+
+
+ Paystack
+ M-Pesa, card or bank
+
+
+
+
+
+ PrizeVault
+ Escrow, locked until paid
+
+
+
+
+
+
+ Tranche 1 · 50%
+ At results
+
+
+ Tranche 2 · 50%
+ At milestone
+
+
+
+
+
+
+ Winner
+ M-Pesa or bank
+
+
+
+
Every step
+
+ Deposits, payouts and milestones are attested on the public ledger
+
+
+ Open the ledger
+
+
+
+ );
+}
+
+const PATHS = [
+ {
+ key: "browse",
+ title: "Here to build?",
+ body: "Browse Prize Verified hackathons, join a team and compete for money that is already there.",
+ href: "/hackathons",
+ link: "Browse hackathons",
+ },
+ {
+ key: "host",
+ title: "Here to organize?",
+ body: "Create your hackathon, fund the pool, invite judges and publish when you are ready.",
+ href: HOST_HACKATHON_HREF,
+ link: "Host a hackathon",
+ },
+ {
+ key: "code",
+ title: "Here to contribute?",
+ body: "Run the whole platform locally with seeded demo data and ship your first pull request.",
+ href: "/contribute",
+ link: "Start contributing",
+ },
+] as const;
+
+function PathVisual({ kind }: { kind: (typeof PATHS)[number]["key"] }) {
+ if (kind === "browse") {
+ return (
+
+ {["Nairobi AI Hackathon", "Mombasa Fintech Sprint", "Kisumu Climate Build"].map(
+ (name, i) => (
+
+ {name}
+
+ Prize Verified
+
+
+ ),
+ )}
+
+ );
+ }
+ if (kind === "host") {
+ return (
+
+ {["Create the hackathon", "Fund the prize pool", "Invite judges", "Publish"].map(
+ (step, i) => (
+
+ {i < 2 ? : i + 1}
+ {step}
+
+ ),
+ )}
+
+ );
+ }
+ return (
+
+
+ 1 git clone {" "}
+ HackVillage
+ {"\n"}
+ 2 pnpm install
+ {"\n"}
+ 3 pnpm run
+ db:migrate
+ {"\n"}
+ 4 pnpm run db:seed
+ {"\n"}
+ 5 pnpm dev
+ {"\n"}
+ ✓ Ready on localhost:3000
+
+
+ );
+}
+
+export function OpenInfrastructure() {
+ return (
+
+
+
+ Open, auditable infrastructure for every hackathon.{" "}
+ Read the code, follow the money, and verify every payout yourself.
+
+
+
+ View On GitHub
+
+
+ Read The Build Plan
+
+
+
+
+
+
+ Follow the money.{" "}
+
+ Every shilling takes one path from the organizer's deposit to the winner's
+ account, and nothing leaves the vault without a record.
+
+
+
+
+
+
+
+ Built so retries never double-pay.{" "}
+
+ If a transfer fails, the money stays locked in the vault. No partial payouts, no
+ ambiguous states.
+
+
+
+
+
+
+
+
+
+
+
+
+ {WAVES.map((d, i) => (
+
+ ))}
+
+
+
+ {STATS.map((stat) => (
+
+ {stat.value}
+ {stat.label}
+
+ ))}
+
+
+
+
+
+ Choose how you take part.{" "}
+ Build, organize or contribute. Every path starts in the same open project.
+
+
+
+
+ );
+}
diff --git a/components/landing/partner-strip.tsx b/components/landing/partner-strip.tsx
new file mode 100644
index 0000000..bbebeaf
--- /dev/null
+++ b/components/landing/partner-strip.tsx
@@ -0,0 +1,67 @@
+import Image from "next/image";
+
+interface Partner {
+ name: string;
+ src: string;
+ width: number;
+ height: number;
+}
+
+/* Intrinsic sizes keep each mark's aspect ratio; CSS sets the rendered height. */
+const PARTNERS: Partner[] = [
+ {
+ name: "Salamander Tech Hub",
+ src: "/images/salamander-logo-yellow.svg",
+ width: 359,
+ height: 100,
+ },
+ { name: "Technetium Kenya", src: "/partners/technetium-kenya-logo.png", width: 350, height: 95 },
+ {
+ name: "SkillsSync Global",
+ src: "/partners/skillssync-global-logo.webp",
+ width: 1000,
+ height: 199,
+ },
+];
+
+/* Enough copies of the three marks to overfill the widest frame. */
+const SET = [...PARTNERS, ...PARTNERS];
+
+/**
+ * The logo wall: the organisations HackVillage is built with, drifting
+ * past in a marquee. The row is doubled so the loop has no seam; the
+ * second copy is hidden from assistive tech so each name is read once.
+ */
+export function PartnerStrip() {
+ const row = (hidden: boolean) =>
+ SET.map((partner, i) => {
+ const onceOnly = !hidden && i < PARTNERS.length;
+ return (
+
+
+
+ );
+ });
+
+ return (
+
+
+
+ {row(false)}
+ {row(true)}
+
+
+
+ );
+}
diff --git a/components/landing/platform-grid.tsx b/components/landing/platform-grid.tsx
new file mode 100644
index 0000000..056f6a6
--- /dev/null
+++ b/components/landing/platform-grid.tsx
@@ -0,0 +1,323 @@
+import Link from "next/link";
+import { ArrowUpRight, Check, Lock, ShieldCheck } from "lucide-react";
+
+/*
+ * Stripe-style product cards: a title, a corner arrow, and an illustrative
+ * mock of the real screen underneath. Every figure in the mocks is sample
+ * data, but the rules they show (fee on top, 50/50 tranches, 30-day
+ * milestone, rubric feedback gate, public attestations) are the platform's.
+ */
+
+interface CardProps {
+ title: string;
+ href: string;
+ className?: string;
+ children: React.ReactNode;
+}
+
+function PlatformCard({ title, href, className, children }: CardProps) {
+ return (
+
+
+
+ {/* Stretched link: the whole card is clickable, the title is the accessible name. */}
+
+ {title}
+
+
+
+
+
+
+
+ {children}
+
+
+ );
+}
+
+function VaultMock() {
+ return (
+
+
+
Prize vault
+
Nairobi AI Hackathon
+
KES 500,000
+
+
+
+
+ Funded
+ 100%
+
+
+ Prize Verified
+
+
+
+
+
+
+
+ hackvillage.xyz/organizer/fund
+
+
Deposit summary
+
+
+
Prize pool
+ KES 500,000
+
+
+
Platform fee (5%)
+ KES 25,000
+
+
+
Total deposited
+ KES 525,000
+
+
+
+ M-Pesa
+ Card
+ Bank
+
+
+ Locked in escrow until winners are announced
+
+
+
+ );
+}
+
+function PayoutMock() {
+ return (
+
+
1st place · Team Jua
+
KES 250,000
+
+
+
+
+
+
+ Tranche 1 · 50%
+ Paid to M-Pesa, 12 min after results
+
+
+
+
+
+
+
+ Tranche 2 · 50%
+ Releases at milestone, due in 30 days
+
+
+
+
+ Paid
+ In escrow
+
+
+ );
+}
+
+const RUBRIC = [
+ { label: "Impact", score: 8 },
+ { label: "Execution", score: 9 },
+ { label: "Design", score: 7 },
+ { label: "Pitch", score: 8 },
+];
+
+function JudgingMock() {
+ return (
+
+
Scorecard · Team Jua
+
+ {RUBRIC.map((item) => (
+
+ {item.label}
+
+
+
+ {item.score}
+
+ ))}
+
+
+ Written feedback sent to the team
+
+
+ );
+}
+
+function PortfolioMock() {
+ return (
+
+
+
WK
+
+ Wanjiku K.
+ @wanjiku · Nairobi
+
+
+
+ 1st · Nairobi AI Hackathon
+ Endorsed by 3 judges
+ Prize paid
+ Open to intros
+
+
+ {Array.from({ length: 42 }, (_, i) => (
+
+ ))}
+
+
+ );
+}
+
+const LEDGER = [
+ { hash: "0x7f3a…c21e", kind: "Deposit", amount: "525,000" },
+ { hash: "0x19b2…04af", kind: "Payout · T1", amount: "125,000" },
+ { hash: "0xc4d8…9b10", kind: "Payout · T1", amount: "75,000" },
+ { hash: "0x5e61…a3d7", kind: "Milestone", amount: "125,000" },
+];
+
+function LedgerMock() {
+ return (
+
+
Public attestations
+
+ {LEDGER.map((row) => (
+
+ {row.hash}
+ {row.kind}
+ {row.amount}
+
+ ))}
+
+
+ );
+}
+
+const TEAMS = [
+ { name: "Team Jua", track: "AI", score: "8.4", status: "Paid", tone: "done" },
+ { name: "Shamba Labs", track: "AgriTech", score: "8.1", status: "Paid", tone: "done" },
+ { name: "Matatu OS", track: "Mobility", score: "7.9", status: "Paid", tone: "done" },
+ { name: "Kilimo Data", track: "ClimateTech", score: "7.6", status: "Scored", tone: "idle" },
+ { name: "Soko Pay", track: "Fintech", score: "7.2", status: "Scored", tone: "idle" },
+ { name: "Afya Link", track: "HealthTech", score: "—", status: "Submitted", tone: "idle" },
+] as const;
+
+function DashboardMock() {
+ return (
+
+
+
+
+
+ hackvillage.xyz/organizer
+
+
+
+ Nairobi AI Hackathon
+ Overview
+ Teams
+ Submissions
+ Judging
+ Payouts
+ Media
+
+
+
+
+ Teams
+ 48
+
+
+ Submissions
+ 41
+
+
+ Judges
+ 6
+
+
+ Escrowed
+ KES 500K
+
+
+
+
+
+ Team
+ Track
+ Score
+ Status
+
+
+
+ {TEAMS.map((team) => (
+
+ {team.name}
+ {team.track}
+ {team.score}
+
+
+ {team.status}
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
+
+export function PlatformGrid() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/landing/trust-band.tsx b/components/landing/trust-band.tsx
new file mode 100644
index 0000000..3bc1249
--- /dev/null
+++ b/components/landing/trust-band.tsx
@@ -0,0 +1,43 @@
+import { TrustStats, type TrustStat } from "@/components/landing/trust-stats";
+
+const STATS: TrustStat[] = [
+ {
+ value: "100%",
+ label: "of every prize pool escrowed before launch",
+ href: "/how-escrow-works",
+ link: "How escrow works",
+ },
+ {
+ value: "50%",
+ label: "paid the moment winners are announced",
+ href: "/blog/how-winners-get-paid",
+ link: "How winners get paid",
+ },
+ {
+ value: "1hr",
+ label: "Trust Score target to pay the first half",
+ href: "/trust",
+ link: "See the public ledger",
+ },
+ {
+ value: "0%",
+ label: "taken from winners. Fees are paid on top",
+ href: "/how-escrow-works",
+ link: "Where the fee comes from",
+ },
+];
+
+export function TrustBand() {
+ return (
+
+
+
+ The trust layer
+
+ for African hackathons
+
+
+
+
+ );
+}
diff --git a/components/landing/trust-stats.tsx b/components/landing/trust-stats.tsx
new file mode 100644
index 0000000..f9cc402
--- /dev/null
+++ b/components/landing/trust-stats.tsx
@@ -0,0 +1,305 @@
+"use client";
+
+import Link from "next/link";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { ChevronRight } from "lucide-react";
+
+export interface TrustStat {
+ value: string;
+ label: string;
+ href: string;
+ link: string;
+}
+
+/** How long each stat stays active while the band cycles on its own. */
+const CYCLE_MS = 6000;
+const RAY_COUNT = 140;
+
+interface Ray {
+ angle: number;
+ /** 0–1 share of the fan's reach this ray gets at rest. */
+ reach: number;
+ alpha: number;
+ /** Current (eased) values, drawn each frame. */
+ len: number;
+ lit: number;
+}
+
+function seededRays(): Ray[] {
+ let seed = 7;
+ const rand = () => {
+ seed = (seed * 16807) % 2147483647;
+ return seed / 2147483647;
+ };
+ return Array.from({ length: RAY_COUNT }, (_, i) => {
+ const reach = 0.35 + rand() * 0.65;
+ return {
+ angle: Math.PI * (0.05 + (0.9 * i) / (RAY_COUNT - 1)),
+ reach,
+ alpha: 0.35 + rand() * 0.55,
+ len: reach,
+ lit: 0.6,
+ };
+ });
+}
+
+/**
+ * Each stat drives the fan into a state that shows what the number means:
+ * 100% lights every ray, 50% lights half, 1hr sweeps like a clock hand,
+ * and 0% evens every ray out (nothing is cut from anyone's prize).
+ */
+function targetFor(stat: number, ray: Ray, index: number, time: number) {
+ const t = index / (RAY_COUNT - 1);
+ switch (stat) {
+ case 1:
+ return { len: t < 0.5 ? ray.reach : ray.reach * 0.55, lit: t < 0.5 ? 1 : 0.18 };
+ case 2: {
+ const hand = (time / 2600) % 1;
+ const d = t - hand;
+ const near = Math.exp(-(d * d) / 0.006);
+ return { len: ray.reach * (0.7 + 0.35 * near), lit: 0.22 + 0.78 * near };
+ }
+ case 3:
+ return { len: 0.78, lit: 0.72 };
+ default:
+ return { len: ray.reach, lit: 1 };
+ }
+}
+
+type Rgb = [number, number, number];
+
+function toRgb(value: string, fallback: Rgb): Rgb {
+ const hex = value.trim().replace("#", "");
+ if (!/^[0-9a-f]{6}$/i.test(hex)) return fallback;
+ return [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16)) as Rgb;
+}
+
+const rgba = ([r, g, b]: Rgb, a: number) => `rgba(${r}, ${g}, ${b}, ${a.toFixed(3)})`;
+
+/** The theme tokens are hex, so they convert straight to canvas colours. */
+function readColors(el: HTMLElement) {
+ const css = getComputedStyle(el);
+ return {
+ ray: toRgb(css.getPropertyValue("--color-ink-soft"), [2, 114, 212]),
+ glow: toRgb(css.getPropertyValue("--color-brand"), [4, 161, 241]),
+ soft: toRgb(css.getPropertyValue("--color-brand-soft"), [127, 208, 248]),
+ };
+}
+
+function BurstCanvas({ active }: { active: number }) {
+ const canvasRef = useRef(null);
+ const activeRef = useRef(active);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ const ctx = canvas?.getContext("2d");
+ if (!canvas || !ctx) return;
+
+ const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ const rays = seededRays();
+ let colors = readColors(canvas);
+ let width = 0;
+ let height = 0;
+ let frame = 0;
+ let visible = false;
+ const pointer = { x: 0, y: 0, strength: 0, target: 0 };
+
+ const resize = () => {
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
+ width = canvas.clientWidth;
+ height = canvas.clientHeight;
+ canvas.width = Math.round(width * dpr);
+ canvas.height = Math.round(height * dpr);
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+ };
+
+ const draw = (time: number, ease: number) => {
+ ctx.clearRect(0, 0, width, height);
+ const cx = width / 2;
+ const cy = height;
+ const reachY = height * 0.92;
+ const reachX = Math.min(width * 0.46, reachY * 2.2);
+
+ const glow = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(reachX, reachY));
+ glow.addColorStop(0, rgba(colors.glow, 0.5));
+ glow.addColorStop(0.55, rgba(colors.soft, 0.14));
+ glow.addColorStop(1, rgba(colors.soft, 0));
+ ctx.fillStyle = glow;
+ ctx.fillRect(0, 0, width, height);
+
+ // The pointer in the fan's own terms: which angle it sits over, and how
+ // far out from the root it is (1 = the fan's full reach).
+ const nx = (cx - pointer.x) / reachX;
+ const ny = (cy - pointer.y) / reachY;
+ const pAngle = Math.atan2(ny, nx);
+ const pDist = Math.hypot(nx, ny);
+ pointer.strength += (pointer.target - pointer.strength) * 0.08;
+
+ ctx.lineWidth = 1;
+ rays.forEach((ray, i) => {
+ const target = targetFor(activeRef.current, ray, i, time);
+ let len = target.len;
+ let lit = target.lit;
+ if (pointer.strength > 0.01) {
+ const d = ray.angle - pAngle;
+ const near = Math.exp(-(d * d) / 0.012) * pointer.strength;
+ // Rays under the cursor stretch toward it and brighten.
+ len += Math.max(0, Math.min(1.08, pDist) - len) * near * 0.9 + near * 0.06;
+ lit = Math.min(1, lit + near * 0.8);
+ }
+ ray.len += (len - ray.len) * ease;
+ ray.lit += (lit - ray.lit) * ease;
+
+ const x = cx - Math.cos(ray.angle) * ray.len * reachX;
+ const y = cy - Math.sin(ray.angle) * ray.len * reachY;
+ const alpha = ray.alpha * ray.lit;
+ ctx.strokeStyle = rgba(colors.ray, alpha);
+ ctx.beginPath();
+ ctx.moveTo(cx, cy);
+ ctx.lineTo(x, y);
+ ctx.stroke();
+ ctx.fillStyle = ctx.strokeStyle;
+ ctx.beginPath();
+ ctx.arc(x, y, 1.6 + ray.lit * 0.8, 0, Math.PI * 2);
+ ctx.fill();
+ });
+ };
+
+ const loop = (time: number) => {
+ draw(time, 0.07);
+ frame = visible ? requestAnimationFrame(loop) : 0;
+ };
+ const start = () => {
+ if (!frame && visible && !reduceMotion) frame = requestAnimationFrame(loop);
+ };
+
+ resize();
+ draw(0, 1);
+
+ const resizeObserver = new ResizeObserver(() => {
+ resize();
+ draw(performance.now(), 1);
+ });
+ resizeObserver.observe(canvas);
+
+ const intersection = new IntersectionObserver(([entry]) => {
+ visible = entry?.isIntersecting ?? false;
+ if (visible) start();
+ });
+ intersection.observe(canvas);
+
+ // Re-read the palette when the theme switcher flips data-theme.
+ const themeObserver = new MutationObserver(() => {
+ colors = readColors(canvas);
+ draw(performance.now(), 1);
+ });
+ themeObserver.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ["data-theme"],
+ });
+
+ const onMove = (event: PointerEvent) => {
+ const rect = canvas.getBoundingClientRect();
+ pointer.x = event.clientX - rect.left;
+ pointer.y = event.clientY - rect.top;
+ pointer.target = 1;
+ start();
+ };
+ const onLeave = () => {
+ pointer.target = 0;
+ };
+ canvas.addEventListener("pointermove", onMove);
+ canvas.addEventListener("pointerleave", onLeave);
+
+ // With reduced motion there is no loop, so a stat change redraws once.
+ const redraw = () => draw(performance.now(), 1);
+ canvas.addEventListener("lp-redraw", redraw);
+
+ return () => {
+ cancelAnimationFrame(frame);
+ resizeObserver.disconnect();
+ intersection.disconnect();
+ themeObserver.disconnect();
+ canvas.removeEventListener("pointermove", onMove);
+ canvas.removeEventListener("pointerleave", onLeave);
+ canvas.removeEventListener("lp-redraw", redraw);
+ };
+ }, []);
+
+ useEffect(() => {
+ activeRef.current = active;
+ canvasRef.current?.dispatchEvent(new Event("lp-redraw"));
+ }, [active]);
+
+ return ;
+}
+
+/**
+ * The stats row as a menu, the way the reference site does it: one stat is
+ * active at a time, a gradient indicator slides along the rules to it, the
+ * fan below changes to illustrate it, and the row advances on its own until
+ * someone picks a stat themselves.
+ */
+export function TrustStats({ stats }: { stats: TrustStat[] }) {
+ const [active, setActive] = useState(0);
+ const [auto, setAuto] = useState(true);
+ const [paused, setPaused] = useState(false);
+
+ useEffect(() => {
+ if (!auto || paused) return;
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
+ const timer = window.setTimeout(() => setActive((i) => (i + 1) % stats.length), CYCLE_MS);
+ return () => window.clearTimeout(timer);
+ }, [active, auto, paused, stats.length]);
+
+ const choose = useCallback((index: number) => {
+ setAuto(false);
+ setActive(index);
+ }, []);
+
+ return (
+ setPaused(true)}
+ onPointerLeave={() => setPaused(false)}
+ onFocus={() => setPaused(true)}
+ onBlur={() => setPaused(false)}
+ >
+
+
+
+
+ {stats.map((stat, index) => {
+ const isActive = index === active;
+ return (
+
+ choose(index)}
+ >
+ {stat.value}
+ {stat.label}
+
+
+ {stat.link}
+
+
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/components/patterns/builder-journey.tsx b/components/patterns/builder-journey.tsx
deleted file mode 100644
index 1e3d981..0000000
--- a/components/patterns/builder-journey.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-import Image from "next/image";
-import Link from "next/link";
-import { ArrowUpRight } from "lucide-react";
-import { Sparkle } from "@/components/patterns/sparkle";
-import { HOST_HACKATHON_HREF } from "@/lib/auth/signup-links";
-
-const ARC_TEXT = "Verified. Paid. Proven.";
-
-interface PillPhotoProps {
- name: string;
- alt: string;
- className?: string;
-}
-
-function PillPhoto({ name, alt, className }: PillPhotoProps) {
- return (
-
-
-
- );
-}
-
-export function BuilderJourney() {
- return (
-
-
-
-
-
-
- Build
-
- Ship
-
- Win
-
- Build, Ship, Win
-
-
- Every prize is fully deposited before the first line of code, so builders across
- Africa compete for money that is already there.
-
-
-
-
- Host A Hackathon
-
-
-
-
-
-
-
-
-
-
-
- {ARC_TEXT}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Half your prize lands the moment you win. Ship your milestone and the rest follows, with
- every payout recorded on a public ledger.
-
-
-
- Get Paid
-
- Get Seen
-
- Get Hired
-
-
-
-
- Explore Hackathons
-
-
-
-
-
-
- );
-}
diff --git a/components/patterns/landing-about.tsx b/components/patterns/landing-about.tsx
deleted file mode 100644
index c390036..0000000
--- a/components/patterns/landing-about.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * A short, plain answer to "What is HackVillage?" for visitors, search
- * engines and AI answer engines. It names the brand both ways people search
- * for it and states only what the platform already enforces in code, so an
- * engine quoting it never repeats something that isn't true.
- */
-export function LandingAbout() {
- return (
-
-
-
- What Is HackVillage?
-
-
- HackVillage (also written Hack Village) is an
- open-source hackathon platform for Kenya and Africa. Organizers deposit the full prize
- pool into escrow before a hackathon goes live, so every prize listed here is funded, and
- winners are paid half of their prize as soon as results are announced. Every result also
- builds the winner's Proof-of-Work developer profile.
-
-
-
- );
-}
diff --git a/components/patterns/landing-hero.tsx b/components/patterns/landing-hero.tsx
deleted file mode 100644
index 50f7db5..0000000
--- a/components/patterns/landing-hero.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import Image from "next/image";
-import Link from "next/link";
-import { ArrowUpRight } from "lucide-react";
-
-import { Sparkle } from "@/components/patterns/sparkle";
-import { HOST_HACKATHON_HREF } from "@/lib/auth/signup-links";
-
-function Photo({ name, alt, center = false }: { name: string; alt: string; center?: boolean }) {
- return (
-
-
-
- );
-}
-
-export function LandingHero() {
- return (
-
-
-
-
-
-
Open source. Real people. Real impact.
-
- Great Hackathons.
-
- From Start to Finish.
-
-
- From team formation to judging and payouts, bring your hackathon together in one place.
- Every hackathon is Prize Verified , with funds secured before the building
- begins.
-
-
-
-
-
- Browse Hackathons
-
-
-
- Host A Hackathon
-
-
-
-
-
-
-
-
- Great people.
-
- Bold ideas.
-
- Real rewards.
-
-
-
-
-
-
-
-
-
- Connect.
-
- Create.
-
- Grow.
-
-
-
-
- );
-}
diff --git a/components/patterns/sparkle.tsx b/components/patterns/sparkle.tsx
index 8ca936b..e09dc9b 100644
--- a/components/patterns/sparkle.tsx
+++ b/components/patterns/sparkle.tsx
@@ -1,4 +1,4 @@
-/** The four point brand star used as decoration on the home and How It Works pages. */
+/** The four point brand star used as decoration on the How It Works page. */
export function Sparkle({ className }: { className?: string }) {
return (
diff --git a/components/patterns/trust-impact.tsx b/components/patterns/trust-impact.tsx
deleted file mode 100644
index 71616b6..0000000
--- a/components/patterns/trust-impact.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import Image from "next/image";
-import Link from "next/link";
-import { Fingerprint, Lock } from "lucide-react";
-
-import { Button } from "@/components/ui/button";
-
-const STATS = [
- { value: "100%", label: "Escrowed before a hackathon goes live" },
- { value: "50%", label: "Paid the instant winners are announced" },
- { value: "1hr", label: "Trust Score target to fully settle a prize" },
-] as const;
-
-/**
- * Three zones, matching the reference layout: a narrow text column, a
- * framed hero photo carrying two floating trust callouts, and a tall strip
- * of small photos along the far right edge — with the stat cards running
- * full width beneath the text and photo.
- */
-export function TrustImpact() {
- return (
-
-
-
- ✦ Escrow · Payouts
-
-
- Trust Is Built Into Every Step.
-
-
- From the first deposit to the final payout, every action is verifiable, on the record, and
- impossible to fake.
-
-
-
- How Escrow Works
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Funds locked before Day 1, not promised.
-
-
-
-
-
-
-
Every payout attested on a public ledger.
-
-
-
-
-
-
- {STATS.map((stat) => (
-
- {stat.value}
- {stat.label}
-
- ))}
-
-
- );
-}
diff --git a/public/partners/skillssync-global-logo.webp b/public/partners/skillssync-global-logo.webp
new file mode 100644
index 0000000..40a5444
Binary files /dev/null and b/public/partners/skillssync-global-logo.webp differ
diff --git a/public/partners/technetium-kenya-logo.png b/public/partners/technetium-kenya-logo.png
new file mode 100644
index 0000000..d6dffae
Binary files /dev/null and b/public/partners/technetium-kenya-logo.png differ