Polish home hero: typeface, highlight box, line spacing, map artifacts

- Switch the hook headline to Bebas Neue (font-display family) at heavier
  size with a synthetic webkit-text-stroke fatten, italic, uppercase, and
  tight leading so 'Connecting activists to / unstoppable funding.' reads
  as a single editorial statement.
- Force the orange highlight onto its own line via <br>; tune left/right
  padding and inner text offset so the U sits flush with 'Connecting'
  above while the box extends past the word as a flourish.
- Fix two horizontal slashes across the world map caused by
  antimeridian-crossing rings (Russia, Antarctica): detect any longitude
  step > 180° and close+restart the SVG subpath instead of drawing the
  connecting line.
- Drop the 2008-era left-edge darkening gradient and the bottom
  vignette behind the map.
- Dim the central radial brand-orange glow (~half alpha).
- Tighten the arc-flow dash period and pixel size.
This commit is contained in:
Chad Curtis
2026-05-22 00:42:21 -05:00
parent 8620bb2bc7
commit 53a7c01a9e
7 changed files with 112 additions and 55 deletions
+10
View File
@@ -35,6 +35,7 @@
"@fontsource-variable/nunito": "^5.2.7",
"@fontsource-variable/outfit": "^5.2.8",
"@fontsource-variable/playfair-display": "^5.2.8",
"@fontsource/bebas-neue": "^5.2.7",
"@fontsource/bungee-shade": "^5.2.7",
"@fontsource/caveat": "^5.2.8",
"@fontsource/cherry-bomb-one": "^5.2.7",
@@ -1483,6 +1484,15 @@
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/bebas-neue": {
"version": "5.2.7",
"resolved": "https://registry.npmjs.org/@fontsource/bebas-neue/-/bebas-neue-5.2.7.tgz",
"integrity": "sha512-DsmBrmq55d9BCU0mt4DT4RZDdH8vhWRKEUOfbuNB1EEjMuwbtFvM8N+3gIlkYSFbsb10P8Q19BV5OdpMu2h0fA==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/bungee-shade": {
"version": "5.2.7",
"resolved": "https://registry.npmjs.org/@fontsource/bungee-shade/-/bungee-shade-5.2.7.tgz",
+1
View File
@@ -42,6 +42,7 @@
"@fontsource-variable/nunito": "^5.2.7",
"@fontsource-variable/outfit": "^5.2.8",
"@fontsource-variable/playfair-display": "^5.2.8",
"@fontsource/bebas-neue": "^5.2.7",
"@fontsource/bungee-shade": "^5.2.7",
"@fontsource/caveat": "^5.2.8",
"@fontsource/cherry-bomb-one": "^5.2.7",
+61 -46
View File
@@ -37,11 +37,29 @@ function HeroLightningMapImpl({ className }: { className?: string }) {
const landPaths = useMemo(() => {
return LAND_RINGS.map((ring, idx) => {
// Rings are flat [lng, lat, lng, lat, ...]. Convert to an SVG path.
//
// Antimeridian handling: a few rings (notably Russia and Antarctica
// in the Natural Earth source) cross the ±180° seam. The data stores
// those rings as a single polygon whose longitude jumps from +180 to
// -180 (or vice versa) in one step. Drawn naively with a continuous
// `L` command, that jump renders as a long horizontal slash spanning
// the whole equirectangular viewBox — the "two lines" sitting at
// ~lat 41 and ~lat 77 across the map are exactly Russia's bounding
// edges drawn by such a connection.
//
// Detect any longitude step > 180° and close + restart the subpath
// with `M` instead, so the two halves of the country render in their
// actual hemispheres without a connecting line through the middle.
let d = '';
let prevLng: number | null = null;
for (let i = 0; i < ring.length; i += 2) {
const lng = ring[i];
const lat = ring[i + 1];
d += i === 0 ? `M${lng.toFixed(2)} ${(-lat).toFixed(2)}` : `L${lng.toFixed(2)} ${(-lat).toFixed(2)}`;
const isFirst = i === 0;
const wraps = prevLng !== null && Math.abs(lng - prevLng) > 180;
const cmd = isFirst || wraps ? 'M' : 'L';
d += `${cmd}${lng.toFixed(2)} ${(-lat).toFixed(2)}`;
prevLng = lng;
}
d += 'Z';
return <path key={idx} d={d} />;
@@ -74,7 +92,7 @@ function HeroLightningMapImpl({ className }: { className?: string }) {
className="absolute -inset-[10%]"
style={{
background:
'radial-gradient(60% 55% at 62% 45%, hsl(24 100% 55% / 0.32) 0%, hsl(24 95% 50% / 0.18) 28%, hsl(220 30% 8% / 0) 65%)',
'radial-gradient(60% 55% at 62% 45%, hsl(24 100% 55% / 0.12) 0%, hsl(24 95% 50% / 0.07) 28%, hsl(220 30% 8% / 0) 65%)',
}}
/>
@@ -84,11 +102,14 @@ function HeroLightningMapImpl({ className }: { className?: string }) {
className="absolute inset-0 w-full h-full"
>
<defs>
{/* Land fill — very low alpha brand-orange so the continents
read as faint glowing outlines on the dark backdrop. */}
{/* Land fill — brand-orange wash, fully opaque. The transparency
lives on the wrapping <g opacity=…> below so that overlapping
country polygons don't stack their alpha at shared borders
(which is what painted the visible "latitude line" along the
equator and other country seams). */}
<linearGradient id={arcId('land')} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="hsl(24 80% 50%)" stopOpacity="0.12" />
<stop offset="100%" stopColor="hsl(24 70% 45%)" stopOpacity="0.06" />
<stop offset="0%" stopColor="hsl(24 80% 50%)" />
<stop offset="100%" stopColor="hsl(24 70% 45%)" />
</linearGradient>
{/* Arc gradient — bright at midpoint, fading at endpoints, so
@@ -120,14 +141,21 @@ function HeroLightningMapImpl({ className }: { className?: string }) {
</radialGradient>
</defs>
{/* Land — single fill, single stroke, drawn as one group so the
browser batches it. Stroke is what makes the continents legible
on near-black; fill is just a quiet wash. */}
{/* Land. Each country is its own ring; rendered as separate paths
with semi-transparent fill, every shared country border doubles
up where polygons overlap. The most jarring of those overlaps
falls along the equator (Kenya/Tanzania, DRC/Angola, Indonesian
islands) and reads as a horizontal "latitude line."
Fix: paint each country with a fully-opaque fill, then put the
transparency on the wrapping <g opacity=…>. SVG group opacity
renders the children into an offscreen buffer first and then
composites the buffer at the group's alpha, so internal overlaps
don't stack. No stroke for the same reason. */}
<g
fill={`url(#${arcId('land')})`}
stroke="hsl(24 75% 50% / 0.32)"
strokeWidth="0.18"
strokeLinejoin="round"
stroke="none"
opacity="0.18"
>
{landPaths}
</g>
@@ -135,31 +163,39 @@ function HeroLightningMapImpl({ className }: { className?: string }) {
{/* Arcs. Each arc is a quadratic Bézier with the control point
lifted above the great-circle path, giving the curved silhouette
from the reference. Stroke-dasharray + animated stroke-dashoffset
produces the flowing-energy effect. */}
produces the flowing-energy effect.
`vector-effect="non-scaling-stroke"` keeps the stroke at a fixed
pixel width regardless of viewBox-to-screen scale, which is what
kills the line jitter — without it, sub-pixel stroke widths in
user-space combine with the SVG glow filter to shimmer at any
responsive size. */}
<g
fill="none"
stroke={`url(#${arcId('arc')})`}
strokeWidth="0.45"
strokeWidth="1.5"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
filter={`url(#${arcId('glow')})`}
>
{CURATED_ARCS.map((arc, i) => {
const [x1, y1] = [arc.from[0], -arc.from[1]];
const [x2, y2] = [arc.to[0], -arc.to[1]];
// Lift the control point perpendicular to the chord. The lift
// amount scales with chord length so short arcs stay tight
// and trans-oceanic arcs sweep dramatically.
const dx = x2 - x1;
const dy = y2 - y1;
const len = Math.hypot(dx, dy);
const lift = Math.min(45, len * 0.32);
// Lift the control point above the chord, scaled with chord
// length so short arcs stay tight and trans-oceanic arcs
// sweep dramatically.
const len = Math.hypot(x2 - x1, y2 - y1);
const lift = Math.min(60, len * 0.42);
const mx = (x1 + x2) / 2;
const my = (y1 + y2) / 2;
// Control point biased "north" (negative y in SVG space) so
// arcs always curve upward over the equator. Looks more
// intentional than projecting the true great-circle.
// Push the lift toward whichever hemisphere the chord midpoint
// already favors, so equator-crossing arcs sweep clearly into
// their dominant hemisphere instead of all stacking through
// y=0. Pure equatorial midpoints (my≈0) default to lifting
// north (negative y in SVG space).
const direction = my > 0 ? 1 : -1;
const cx = mx;
const cy = my - lift;
const cy = my + lift * direction;
return (
<path
key={i}
@@ -203,27 +239,6 @@ function HeroLightningMapImpl({ className }: { className?: string }) {
})}
</g>
</svg>
{/* Left-edge gradient mask — ensures the headline column always has
a quiet zone behind it regardless of where arcs land. This is
what replaces the old hero-text-shadow: structural legibility,
not a per-character text effect. */}
<div
className="absolute inset-y-0 left-0 w-2/3 lg:w-1/2"
style={{
background:
'linear-gradient(to right, hsl(220 25% 6% / 0.92) 0%, hsl(220 25% 6% / 0.65) 35%, hsl(220 25% 6% / 0) 100%)',
}}
/>
{/* Bottom vignette — anchors the hero to the page below and softens
the map texture as it meets the next section. */}
<div
className="absolute inset-x-0 bottom-0 h-24"
style={{
background: 'linear-gradient(to bottom, hsl(220 25% 6% / 0) 0%, hsl(220 25% 6% / 0.85) 100%)',
}}
/>
</div>
);
}
+7 -6
View File
@@ -720,16 +720,17 @@
.hero-pan-left { animation: heroPanLeft 28s ease-in-out infinite; }
/* Lightning-arc flow on the home hero. Each path is dashed and the
dash offset animates negative, producing the illusion of energy
traveling along the arc. Dash size and animation length are tuned
so the flow reads as continuous (not blinky) at any arc length. */
dash offset animates by exactly one full dash period, so the loop
point is visually identical to the start point — no snap, no
visible restart. Dash sizes are in screen pixels because the arcs
use `vector-effect: non-scaling-stroke`. */
@keyframes heroArcFlow {
0% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: -36; }
100% { stroke-dashoffset: -16; }
}
.hero-arc-flow {
stroke-dasharray: 1.2 4.5;
animation: heroArcFlow 4.5s linear infinite;
stroke-dasharray: 6 10;
animation: heroArcFlow 2.4s linear infinite;
}
/* City-node pulse used on the home hero. The dot itself doesn't move;
+1
View File
@@ -25,6 +25,7 @@ import './index.css';
import './i18n';
import '@fontsource-variable/inter';
import '@fontsource/bebas-neue/400.css';
// ─── Native status bar theming (Android APK / iOS) ───────────────────────────
// Keeps the OS top chrome in sync with the active app theme.
+31 -3
View File
@@ -164,9 +164,37 @@ export function CampaignsPage() {
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 py-16 lg:py-24 min-h-[440px] sm:min-h-[480px] lg:min-h-[520px] flex flex-col justify-center">
<div className="space-y-6 max-w-2xl">
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight leading-[1.05]">
Connecting activists to{' '}
<span className="text-primary">unstoppable funding.</span>
<h1
className="font-display italic text-6xl sm:text-7xl lg:text-8xl font-normal tracking-wide leading-none uppercase"
style={{
// Bebas Neue only ships at weight 400. Paint a stroke the
// same color as the fill to fatten the letterforms without
// the fuzz a synthetic-bold transform would produce.
WebkitTextStroke: '0.022em currentColor',
}}
>
Connecting activists to
{/* "unstoppable" gets a solid brand-orange highlighter
block on its own line. The negative left margin
(`-ml-1.5`) pulls the box's left edge back by exactly
the box's own horizontal padding so the U sits flush
with the column's left edge instead of being inset by
the highlighter's padding. */}
<br />
{/* Asymmetric padding: zero on the left so "unstoppable"'s
U sits flush with the column edge (matching the row
above), but extra padding on the right so the orange
box extends past the word's trailing edge as a
deliberate visual flourish. The inner text is then
nudged slightly leftward (negative left margin on the
inner element) so the U optically aligns with the
"C" in "Connecting" — Bebas Neue's italic skew shifts
the visual left edge of the U rightward of its
geometric box. */}
<span className="inline-block w-fit pl-0 pr-3 bg-primary text-white leading-[0.95] align-baseline">
<span className="-ml-1 inline-block">unstoppable</span>
</span>{' '}
funding.
</h1>
<p className="text-base sm:text-lg text-white/80 max-w-xl">
Raise Bitcoin directly from supporters around the world. Every donation
+1
View File
@@ -32,6 +32,7 @@ export default {
},
fontFamily: {
sans: ['Inter Variable', 'Inter', 'system-ui', 'sans-serif'],
display: ['Bebas Neue', 'Impact', 'Inter Variable', 'system-ui', 'sans-serif'],
emoji: ['Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', 'Twemoji Mozilla', 'Android Emoji', 'EmojiSymbols', 'sans-serif'],
},
colors: {