Southpaw Boxing Club — build guide · plain HTML/CSS/JS · GSAP · Higgsfield imagery
Gritty-premium fight-club energy. The whole site is art-directed like a vintage boxing bill reset with a modern grotesque: near-black rooms, one harsh light source, chalk dust, and huge ultra-condensed headlines stacked like a fight poster. Two texture cues sell the world — a living 35mm film-grain overlay on the whole page and tape stickers (mono type on an off-white strip, slightly rotated) used as annotations everywhere a poster would have a pasted-on correction.
The single most important rule: nothing floats. Every reveal snaps in hard and stops dead, like a punch landing. No soft fades, no gentle drifts.
Red is used at two temperatures: #B1252B for surfaces (buttons, the ticker band, the
featured pricing card) and #D92C33 only for small hot accents — the word that "hits",
meta labels, stat suffixes. Body copy never sits on pure red except inside the featured card,
where it switches to canvas at 75–90% opacity for contrast.
| Role | Face | Treatment |
|---|---|---|
| Display | Big Shoulders Display 700–900 | Uppercase, line-height 0.86–0.9, sizes up to clamp(72px, 15.5vw, 250px). One line per beat, stacked like a fight card. Outline variant via -webkit-text-stroke on a transparent fill. |
| Labels / numbers | IBM Plex Mono 400–500 | 10–12px, letter-spacing 0.14–0.22em, uppercase. All tape stickers, schedule times, fight records, footer meta. |
| Body | Archivo 400/600 | 14–18px on #C9BFA9 (dimmed canvas) so the display type keeps the volume. |
All eight images were generated with Higgsfield Soul v2 (photorealistic model), then compressed with ffmpeg to ~120–250 KB JPEGs. Each frame came from a carefully art-directed brief — lighting, lens, palette, composition and mood specified shot by shot, with the same harsh single-source lighting grammar repeated across the whole set so eight generations read as one shoot — and iterated until every image landed inside the club's gritty-premium world.
ffmpeg -i raw.png -vf "scale=1920:-2" -q:v 4 hero-boxer.jpg # heroes / wides
ffmpeg -i raw.png -vf "scale=1100:-2" -q:v 4 coach-ray.jpg # portraits / details
GSAP 3 + ScrollTrigger from CDN. One house ease — power4.out (steep in, dead stop) —
and four reveal flavors, each mapped to a data-reveal attribute so the HTML stays declarative:
slide — section titles jab in from the left with a −6° skew that resolves on landing.up — short 44px vertical throw, 0.4s, dead stop. The workhorse.pop — scale 0.5 → 1 with back.out(2.2) overshoot. Buttons and tape stickers only.row / card — program rows jab from the left; cards drop 70px with a ±0.8° rotation so each lands slightly imperfect, like a pasted poster.overflow: clip wrapper
and translates up from yPercent: 118 with a 4° skew, staggered 0.11s — a one-two-three combination.
When the last line lands, an impact shake fires on the whole headline.power2.in — they
accelerate into the final value instead of easing out of it, then shake on arrival.
Backwards from every tutorial, and that's exactly why it feels like a hit.tl.to(proxy, {
v: target,
ease: "power2.in", // accelerate INTO the number — the slam
onUpdate: () => el.textContent = Math.round(proxy.v),
onComplete: () => impact(stat) // ±6px decaying shake, 0.28s
});
aria-hidden), then translateX(-50%) loops seamlessly. Pauses on hover.feTurbulence data-URI on a fixed
body::before, stepped 4-frame jitter at 7% opacity..is-armed class), and prefers-reduced-motion exits before any
animation is registered — counters just print their final values.<table> (7 columns) inside an
overflow-x: auto region so mobile scrolls the grid without breaking the page.overflow-x: clip on html, body;
the only intentionally overflowing element (ticker track) lives inside its own clipped band.southpaw-boxing folder and drop it on
app.netlify.com/drop. Done — it's static.npm i -g netlify-cli
cd southpaw-boxing
netlify deploy --prod --dir .
No build step, no environment variables. The only external requests are Google Fonts and the two GSAP CDN scripts; everything else (images, CSS, JS) is local and relative-pathed, so the site works from any subdirectory.