Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/images/og.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/textures/earth-black-marble.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 3 additions & 4 deletions skills/context/progress-tracker.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,9 @@ All seven homepage sections are built, wired, and animated (scroll-driven entran

## Next Steps

1. Performance review and optimization.
2. og image and description.
3. SEO, tracking validation, and launch metadata.
4. Staking page (remove `TEMP(staking-page)` comments when shipping).
1. og image and description.
2. launch metadata.
3. Staking page (remove `TEMP(staking-page)` comments when shipping).

## Latest Handoff

Expand Down
48 changes: 44 additions & 4 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ import {
SITE_HEADER_SCROLL_BOOTSTRAP_SCRIPT,
SITE_HEADER_SCROLL_CRITICAL_CSS,
} from "@/lib/layout/site-header-scroll";
import {
OG_IMAGE,
OG_PAGE_URL,
SITE_DESCRIPTION,
SITE_NAME,
SITE_TAGLINE,
SITE_URL,
TWITTER_HANDLE,
} from "@/lib/site";
import "./globals.css";

const monaSans = Mona_Sans({
Expand All @@ -19,16 +28,47 @@ const monaSans = Mona_Sans({
display: "swap",
});

// `optional` avoids the late font-swap repaint that made the hero subtitle's
// LCP land at font-arrival time; the preloaded font still wins on fast loads.
const openSans = Open_Sans({
variable: "--font-open-sans",
subsets: ["latin"],
display: "swap",
display: "optional",
});

const openGraphTitle = `${SITE_NAME} — ${SITE_TAGLINE}`;

export const metadata: Metadata = {
title: "Cortex Global",
description:
"A global network of local hubs, education, events, services, and real projects around emerging technology.",
metadataBase: new URL(SITE_URL),
title: {
default: SITE_NAME,
template: `%s | ${SITE_NAME}`,
},
description: SITE_DESCRIPTION,
applicationName: SITE_NAME,
openGraph: {
type: "website",
locale: "en_US",
url: OG_PAGE_URL,
siteName: SITE_NAME,
title: openGraphTitle,
description: SITE_DESCRIPTION,
images: [
{
url: OG_IMAGE.url,
width: OG_IMAGE.width,
height: OG_IMAGE.height,
alt: OG_IMAGE.alt,
},
],
},
twitter: {
card: "summary_large_image",
site: `@${TWITTER_HANDLE}`,
title: openGraphTitle,
description: SITE_DESCRIPTION,
images: [OG_IMAGE.url],
},
};

// HeaderUpcomingEvent filters by UTC date at render; hourly ISR keeps the promo fresh.
Expand Down
14 changes: 10 additions & 4 deletions src/components/sections/mission/mission-cards-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
MissionIllustrationFallback,
} from "@/components/illustrations/mission-illustration";
import { DESKTOP_MQL, useIsDesktop } from "@/hooks/use-is-desktop";
import { useInView } from "@/hooks/use-in-view";
import { useReducedMotion } from "@/hooks/use-reduced-motion";
import { gsap, ScrollTrigger, useGSAP } from "@/lib/gsap-setup";
import { cn } from "@/lib/utils";
Expand Down Expand Up @@ -140,6 +141,7 @@ export function MissionDesktopStack({
const [activeIndex, setActiveIndex] = useState(0);
const [entranceComplete, setEntranceComplete] = useState(false);
const reducedMotion = useReducedMotion();
const { ref: inViewRef, inView } = useInView();

const markEntranceComplete = useCallback(() => {
entranceCompleteRef.current = true;
Expand Down Expand Up @@ -265,7 +267,10 @@ export function MissionDesktopStack({

return (
<div ref={pinRef} data-mission-pin className="hidden xl:block">
<div className="site-container flex flex-row items-start gap-11">
<div
ref={inViewRef}
className="site-container flex flex-row items-start gap-11"
>
<div
className="flex shrink-0 flex-col"
style={{
Expand Down Expand Up @@ -304,7 +309,7 @@ export function MissionDesktopStack({
{isActive ? (
<MissionExpandedCard
card={card}
illustrationActive={entranceComplete}
illustrationActive={entranceComplete && inView}
className="h-full border-0"
/>
) : (
Expand Down Expand Up @@ -333,6 +338,7 @@ export function MissionMobileCards({ cards }: MissionCardsProps) {
const carouselRef = useRef<HTMLDivElement | null>(null);
const scrollRafRef = useRef<number | null>(null);
const isDesktop = useIsDesktop();
const { ref: inViewRef, inView } = useInView();

const setCarouselRef = useCallback((node: HTMLDivElement | null) => {
if (node === null && scrollRafRef.current !== null) {
Expand Down Expand Up @@ -368,7 +374,7 @@ export function MissionMobileCards({ cards }: MissionCardsProps) {
}, []);

return (
<div className="mt-6 md:mt-8 xl:hidden">
<div ref={inViewRef} className="mt-6 md:mt-8 xl:hidden">
<div
ref={setCarouselRef}
onScroll={handleCarouselScroll}
Expand All @@ -378,7 +384,7 @@ export function MissionMobileCards({ cards }: MissionCardsProps) {
<MissionExpandedCard
key={card.id}
card={card}
illustrationActive={!isDesktop && activeIndex === i}
illustrationActive={!isDesktop && inView && activeIndex === i}
className="mission-carousel-card"
/>
))}
Expand Down
5 changes: 4 additions & 1 deletion src/components/webgl/globe/globe-canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,19 @@ export function GlobeCanvas({
reducedMotion,
rotationY,
onReady,
active = true,
}: {
reducedMotion: boolean;
rotationY: number;
onReady?: () => void;
/** When false (hero off-screen), the frameloop idles to save CPU/GPU. */
active?: boolean;
}) {
return (
<Canvas
dpr={resolveGlobeDpr()}
resize={{ scroll: false }}
frameloop={reducedMotion ? "demand" : "always"}
frameloop={reducedMotion || !active ? "demand" : "always"}
gl={{
antialias: true,
alpha: true,
Expand Down
16 changes: 14 additions & 2 deletions src/components/webgl/hero-webgl-background.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type ReactNode,
} from "react";
import dynamic from "next/dynamic";
import { useInView } from "@/hooks/use-in-view";
import { useIsLargeScreen } from "@/hooks/use-is-desktop";
import { useReducedMotion } from "@/hooks/use-reduced-motion";
import { useWebGLSupport } from "@/hooks/use-webgl-support";
Expand Down Expand Up @@ -127,7 +128,13 @@ function StaticGlobeImage() {
);
}

function WebglGlobe({ reducedMotion }: { reducedMotion: boolean }) {
function WebglGlobe({
reducedMotion,
active,
}: {
reducedMotion: boolean;
active: boolean;
}) {
const [ready, setReady] = useState(false);

return (
Expand All @@ -137,6 +144,7 @@ function WebglGlobe({ reducedMotion }: { reducedMotion: boolean }) {
reducedMotion={reducedMotion}
rotationY={DEFAULT_GLOBE_ROTATION_Y}
onReady={() => setReady(true)}
active={active}
/>
</GlobeCanvasErrorBoundary>
);
Expand All @@ -150,20 +158,24 @@ export const HeroWebglBackground = memo(function HeroWebglBackground() {
const reducedMotion = useReducedMotion();
const isLargeScreen = useIsLargeScreen();
const webgl = useWebGLSupport();
const { ref: inViewRef, inView } = useInView();

const showLiveGlobe = isLargeScreen && webgl;
const showStaticGlobe = !showLiveGlobe;

return (
<div
ref={inViewRef}
aria-hidden="true"
className="absolute inset-0 -z-10 bg-bg-canvas"
style={{ pointerEvents: "none" }}
data-hero-webgl-background
>
{showStaticGlobe ? <StaticGlobeImage /> : null}

{showLiveGlobe ? <WebglGlobe reducedMotion={reducedMotion} /> : null}
{showLiveGlobe ? (
<WebglGlobe reducedMotion={reducedMotion} active={inView} />
) : null}

{/* Ambient spill — warm glow rising from the bottom-anchored globe */}
<div
Expand Down
38 changes: 38 additions & 0 deletions src/hooks/use-in-view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";

import { useCallback, useState } from "react";

type UseInViewResult = {
/** Attach to the element whose viewport visibility should be tracked. */
ref: (node: Element | null) => (() => void) | undefined;
inView: boolean;
};

/**
* Tracks whether an element intersects the viewport, for pausing ambient
* animation loops (WebGL frameloop, infinite GSAP timelines) off-screen.
*
* `inView` flips to true as soon as 1px is visible and back to false only
* when the element is fully out of view, so loops never pause mid-frame
* while partially visible. Defaults to true (SSR and pre-observe), so
* content is never blocked from animating before the observer fires.
*/
export function useInView(): UseInViewResult {
const [inView, setInView] = useState(true);

const ref = useCallback((node: Element | null) => {
if (!node || typeof IntersectionObserver === "undefined") return;

const observer = new IntersectionObserver(([entry]) => {
setInView(entry.isIntersecting);
});
observer.observe(node);

return () => {
observer.disconnect();
setInView(true);
};
}, []);

return { ref, inView };
}
25 changes: 25 additions & 0 deletions src/lib/site.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const SITE_NAME = "Cortex Global" as const;

export const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL ?? "https://www.cortexglobal.xyz";

export const SITE_TAGLINE = "Local Service. Global Impact." as const;

export const SITE_DESCRIPTION =
"Cortex serves all individual, business, and community needs through our globally distributed professional network — local citizens providing expertise, guidance, and connection to the top industry professionals in all verticals. Everything for Everyone. Everywhere." as const;

export const TWITTER_HANDLE = "Cortex_Global_" as const;

export const OG_IMAGE_PATH = "/images/og.png" as const;

/** Bump to bust social-platform image caches without changing routes or image paths. */
export const OG_CACHE_VERSION = "3" as const;

export const OG_PAGE_URL = `/?v=${OG_CACHE_VERSION}` as const;

export const OG_IMAGE = {
url: `${OG_IMAGE_PATH}?v=${OG_CACHE_VERSION}`,
width: 2170,
height: 1139,
alt: `${SITE_NAME} — ${SITE_TAGLINE}`,
} as const;
Loading