Update hero alumni strip, 2027 dates, and Antebas font loading.
Some checks failed
Deploy to Cloudflare Workers (OpenNext) / deploy (push) Has been cancelled

Add a bottom cut-out panel with larger winner cards (four visible), move summit dates to Feb 21–22 2027, and limit demo Antebas to letters so symbols render via DM Sans.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
kirukib 2026-06-10 00:25:43 +03:00
parent f950545ae0
commit 03d439e97b
15 changed files with 294 additions and 114 deletions

53
app/antebas.css Normal file
View File

@ -0,0 +1,53 @@
/**
* Antebas (Fontspring DEMO) letters only.
* Demo builds replace numerals/punctuation with watermark glyphs; DM Sans covers the rest.
*/
@font-face {
font-family: Antebas;
src: url("/fonts/Fontspring-DEMO-antebas-light.otf") format("opentype");
font-weight: 300;
font-style: normal;
font-display: swap;
unicode-range:
U+0041-005A, U+0061-007A, U+00C0-00D6, U+00D8-00F6, U+00F8-00FF, U+0100-017F;
}
@font-face {
font-family: Antebas;
src: url("/fonts/Fontspring-DEMO-antebas-regular.otf") format("opentype");
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range:
U+0041-005A, U+0061-007A, U+00C0-00D6, U+00D8-00F6, U+00F8-00FF, U+0100-017F;
}
@font-face {
font-family: Antebas;
src: url("/fonts/Fontspring-DEMO-antebas-medium.otf") format("opentype");
font-weight: 500;
font-style: normal;
font-display: swap;
unicode-range:
U+0041-005A, U+0061-007A, U+00C0-00D6, U+00D8-00F6, U+00F8-00FF, U+0100-017F;
}
@font-face {
font-family: Antebas;
src: url("/fonts/Fontspring-DEMO-antebas-bold.otf") format("opentype");
font-weight: 700;
font-style: normal;
font-display: swap;
unicode-range:
U+0041-005A, U+0061-007A, U+00C0-00D6, U+00D8-00F6, U+00F8-00FF, U+0100-017F;
}
@font-face {
font-family: Antebas;
src: url("/fonts/Fontspring-DEMO-antebas-black.otf") format("opentype");
font-weight: 900;
font-style: normal;
font-display: swap;
unicode-range:
U+0041-005A, U+0061-007A, U+00C0-00D6, U+00D8-00F6, U+00F8-00FF, U+0100-017F;
}

View File

@ -1,4 +1,5 @@
@import "tailwindcss";
@import "./antebas.css";
@import "@fontsource-variable/google-sans-flex/wght.css";
@custom-variant dark (&:is(.dark *));
@ -26,8 +27,8 @@
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--font-sans: var(--font-antebas);
--font-display: var(--font-antebas);
--font-sans: var(--font-site);
--font-display: var(--font-site);
--font-wordmark: "Google Sans Flex Variable", system-ui, sans-serif;
--font-hero-serif: var(--font-hero-serif);
--color-brand-primary: #37a47a;
@ -77,6 +78,7 @@
--hero: var(--brand-tertiary);
--section-muted: var(--brand-surface-muted);
--section-inverse: var(--brand-primary);
--font-site: Antebas, var(--font-hero-body), system-ui, sans-serif;
}
@layer base {
@ -85,13 +87,13 @@
}
body {
@apply bg-background text-foreground antialiased;
font-family: var(--font-antebas), system-ui, sans-serif;
font-family: var(--font-site);
}
h1,
h2,
h3,
h4 {
font-family: var(--font-antebas), system-ui, sans-serif;
font-family: var(--font-site);
letter-spacing: -0.02em;
}
@ -238,6 +240,18 @@
.marquee-winners {
animation: marquee 55s linear infinite;
}
/* Hero bottom cut-out — fits four large winner marks in the visible strip */
.hero-winners-viewport {
width: 100%;
max-width: min(100%, calc(var(--winners-visible, 4) * 13.25rem + (var(--winners-visible, 4) - 1) * 1rem));
}
@media (max-width: 639px) {
.hero-winners-viewport {
max-width: min(100%, calc(var(--winners-visible, 4) * 10.5rem + (var(--winners-visible, 4) - 1) * 0.75rem));
}
}
@keyframes marquee {
from {
transform: translateX(0);

View File

@ -1,4 +1,3 @@
import { antebas } from "@/lib/fonts/antebas";
import { heroFontVariables } from "@/lib/fonts/hero";
import { RiftPageFlow } from "@/components/brand/RiftPageFlow";
import { JsonLd } from "@/components/seo/JsonLd";
@ -16,11 +15,8 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html
lang="en"
className={`${antebas.variable} ${heroFontVariables}`}
>
<body className={`${antebas.className} min-h-screen flex flex-col`}>
<html lang="en" className={heroFontVariables}>
<body className="min-h-screen flex flex-col">
<JsonLd />
<SiteHeader />
<main className="relative flex-1">

View File

@ -74,6 +74,10 @@ export function Hero() {
hoverY={hoverPoint.y}
className="absolute inset-0"
/>
<div
className="pointer-events-none absolute inset-x-0 bottom-0 z-[4] h-28 bg-gradient-to-t from-[#e8f2ec]/95 via-[#e8f2ec]/40 to-transparent md:h-36"
aria-hidden
/>
<TopoCurvyExtend />
<HeroRiftParticles
active={!reduceMotion}
@ -81,7 +85,8 @@ export function Hero() {
/>
<TopoSectionProvider tone="light">
<div className="topo-content-layer topo-content-readable relative z-20 mx-auto flex min-h-[min(100svh,900px)] w-full max-w-6xl flex-col items-center justify-center px-4 py-24 text-center md:px-6">
<div className="topo-content-layer topo-content-readable relative z-20 mx-auto flex min-h-[min(100svh,900px)] w-full max-w-6xl flex-col px-4 pt-24 pb-0 text-center md:px-6">
<div className="flex flex-1 flex-col items-center justify-center pb-8">
<TopoProseSurface className="mx-auto flex w-full max-w-4xl flex-col items-center text-center">
<ScrollReveal immediate variant="fade-up" delay={0}>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-[#37a47a]/80 md:text-sm">
@ -131,8 +136,9 @@ export function Hero() {
</p>
</ScrollReveal>
</TopoProseSurface>
</div>
<LastYearWinnersScroll variant="on-light" className="w-full max-w-5xl" />
<LastYearWinnersScroll variant="on-light" size="large" visibleCount={4} className="w-full" />
</div>
</TopoSectionProvider>
</section>

View File

@ -18,6 +18,7 @@ type Props = {
onUnpin: () => void;
onHover: (key: string | null) => void;
variant?: "on-green" | "on-light";
size?: "default" | "large";
className?: string;
};
@ -30,6 +31,7 @@ export function LastYearWinnerMark({
onUnpin,
onHover,
variant = "on-light",
size = "default",
className,
}: Props) {
const leaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
@ -38,6 +40,8 @@ export function LastYearWinnerMark({
const showImage = !failed && src;
const logoOnly = !company.name;
const onLight = variant === "on-light";
const isLarge = size === "large";
const logoPx = isLarge ? 48 : 28;
const clearLeaveTimer = useCallback(() => {
if (leaveTimer.current) {
@ -96,11 +100,14 @@ export function LastYearWinnerMark({
>
<div
className={cn(
"flex h-10 shrink-0 items-center gap-2 rounded-lg border px-2.5 shadow-sm transition-colors",
"flex shrink-0 items-center gap-2.5 rounded-xl border shadow-sm transition-colors",
isLarge
? "h-14 min-w-[10.5rem] gap-2.5 px-3 sm:h-16 sm:min-w-[12.75rem] sm:gap-3 sm:px-3.5 md:h-[4.5rem] md:min-w-[13.25rem] md:px-4"
: "h-10 gap-2 rounded-lg px-2.5",
onLight
? "border-[#37a47a]/15 bg-white/95 hover:border-[#37a47a]/30 hover:bg-white"
: "border-white/25 bg-white/90 hover:border-white/50 hover:bg-white",
logoOnly && "px-2",
logoOnly && (isLarge ? "px-3" : "px-2"),
isOpen &&
(onLight
? "border-[#37a47a]/40 ring-2 ring-[#37a47a]/15"
@ -109,32 +116,49 @@ export function LastYearWinnerMark({
className
)}
>
<div className="relative flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-md bg-white">
<div
className={cn(
"relative flex shrink-0 items-center justify-center overflow-hidden rounded-lg bg-white",
isLarge ? "size-11 md:size-12" : "size-7 rounded-md"
)}
>
{showImage ? (
<Image
src={src}
alt=""
width={28}
height={28}
className="size-7 object-contain p-0.5"
width={logoPx}
height={logoPx}
className={cn("object-contain p-0.5", isLarge ? "size-11 md:size-12" : "size-7")}
onError={() => setFailed(true)}
/>
) : company.initials ? (
<span className="text-[10px] font-bold leading-none text-[#37a47a]">
<span
className={cn(
"font-bold leading-none text-[#37a47a]",
isLarge ? "text-sm md:text-base" : "text-[10px]"
)}
>
{company.initials}
</span>
) : (
<Image
src={GRV_LOGO}
alt=""
width={28}
height={28}
className="size-7 object-contain p-0.5"
width={logoPx}
height={logoPx}
className={cn("object-contain p-0.5", isLarge ? "size-11 md:size-12" : "size-7")}
/>
)}
</div>
{company.name ? (
<span className="max-w-[7.5rem] truncate text-[11px] font-medium text-[#37a47a]">
<span
className={cn(
"truncate font-medium text-[#37a47a]",
isLarge
? "max-w-[8.5rem] text-sm md:max-w-[9.5rem] md:text-base"
: "max-w-[7.5rem] text-[11px]"
)}
>
{company.name}
</span>
) : null}
@ -148,7 +172,10 @@ export function LastYearWinnerMark({
sideOffset={8}
collisionPadding={12}
data-winner-interactive
className="z-50 w-72 max-w-[calc(100vw-2rem)] shrink-0 overflow-hidden border-[#37a47a]/15 bg-white p-0 shadow-lg"
className={cn(
"z-50 max-w-[calc(100vw-2rem)] shrink-0 overflow-hidden border-[#37a47a]/15 bg-white p-0 shadow-lg",
isLarge ? "w-[22rem] md:w-96" : "w-72"
)}
onMouseEnter={handleEnter}
onMouseLeave={handleLeave}
onOpenAutoFocus={(e) => e.preventDefault()}
@ -159,7 +186,12 @@ export function LastYearWinnerMark({
if (isPinned) e.preventDefault();
}}
>
<LastYearWinnerTip company={company} onClose={onUnpin} showClose={isPinned} />
<LastYearWinnerTip
company={company}
onClose={onUnpin}
showClose={isPinned}
size={size}
/>
</PopoverContent>
) : null}
</Popover>

View File

@ -13,13 +13,20 @@ type Props = {
className?: string;
showClose?: boolean;
onClose?: () => void;
size?: "default" | "large";
};
function externalLinkProps(href: string) {
return href.startsWith("http") ? { target: "_blank" as const, rel: "noopener noreferrer" } : {};
}
function FounderPhoto({ company }: { company: LastYearWinner }) {
function FounderPhoto({
company,
large,
}: {
company: LastYearWinner;
large?: boolean;
}) {
const [failed, setFailed] = useState(false);
const src = company.founderImageSrc;
@ -28,9 +35,12 @@ function FounderPhoto({ company }: { company: LastYearWinner }) {
<Image
src={src}
alt=""
width={48}
height={48}
className="size-12 shrink-0 rounded-full border-2 border-[#37a47a]/15 object-cover"
width={large ? 56 : 48}
height={large ? 56 : 48}
className={cn(
"shrink-0 rounded-full border-2 border-[#37a47a]/15 object-cover",
large ? "size-14" : "size-12"
)}
onError={() => setFailed(true)}
/>
);
@ -49,7 +59,10 @@ function FounderPhoto({ company }: { company: LastYearWinner }) {
return (
<div
className="flex size-12 shrink-0 items-center justify-center rounded-full border-2 border-[#37a47a]/15 bg-[#e8f2ec] text-sm font-bold text-[#37a47a]"
className={cn(
"flex shrink-0 items-center justify-center rounded-full border-2 border-[#37a47a]/15 bg-[#e8f2ec] font-bold text-[#37a47a]",
large ? "size-14 text-base" : "size-12 text-sm"
)}
aria-hidden
>
{initials}
@ -57,7 +70,14 @@ function FounderPhoto({ company }: { company: LastYearWinner }) {
);
}
export function LastYearWinnerTip({ company, className, showClose, onClose }: Props) {
export function LastYearWinnerTip({
company,
className,
showClose,
onClose,
size = "default",
}: Props) {
const isLarge = size === "large";
const impact = getWinnerImpact(company);
const headline = impact.metrics.find((m) => m.highlight) ?? impact.metrics[0];
const supporting = impact.metrics.filter((m) => m !== headline).slice(0, 2);
@ -68,28 +88,66 @@ export function LastYearWinnerTip({ company, className, showClose, onClose }: Pr
const showFooter = showClose || viewHref || donateHref;
return (
<div className={cn("box-border w-72 max-w-full shrink-0 overflow-hidden", className)}>
<div className="flex min-w-0 gap-2.5 px-3 pt-3 pb-2">
<FounderPhoto company={company} />
<div
className={cn(
"box-border max-w-full shrink-0 overflow-hidden",
isLarge ? "w-full" : "w-72",
className
)}
>
<div className={cn("flex min-w-0 px-3 pt-3 pb-2", isLarge ? "gap-3.5" : "gap-2.5")}>
<FounderPhoto company={company} large={isLarge} />
<div className="min-w-0 flex-1">
<p className="truncate text-[11px] font-semibold uppercase tracking-wider text-[#37a47a]">
<p
className={cn(
"truncate font-semibold uppercase tracking-wider text-[#37a47a]",
isLarge ? "text-xs md:text-sm" : "text-[11px]"
)}
>
{name}
</p>
{company.founderName ? (
<p className="truncate text-[10px] text-[#5b5b5b]/80">{company.founderName}</p>
<p
className={cn(
"truncate text-[#5b5b5b]/80",
isLarge ? "text-xs" : "text-[10px]"
)}
>
{company.founderName}
</p>
) : null}
<p className="mt-1 line-clamp-2 text-[11px] leading-snug text-[#5b5b5b]">
<p
className={cn(
"mt-1 line-clamp-2 leading-snug text-[#5b5b5b]",
isLarge ? "text-xs md:text-sm" : "text-[11px]"
)}
>
{impact.summary}
</p>
</div>
</div>
{headline ? (
<div className="mx-3 mb-2 overflow-hidden rounded-lg border border-[#37a47a]/12 bg-[#e8f2ec] px-2.5 py-2 text-center">
<p className="truncate text-[10px] font-bold uppercase tracking-wider text-[#37a47a]/75">
<div
className={cn(
"mx-3 mb-2 overflow-hidden rounded-lg border border-[#37a47a]/12 bg-[#e8f2ec] text-center",
isLarge ? "px-3 py-3" : "px-2.5 py-2"
)}
>
<p
className={cn(
"truncate font-bold uppercase tracking-wider text-[#37a47a]/75",
isLarge ? "text-xs" : "text-[10px]"
)}
>
{headline.label}
</p>
<p className="winner-impact-value mt-0.5 truncate font-display text-xl font-extrabold tracking-tight text-[#30614c]">
<p
className={cn(
"winner-impact-value mt-0.5 truncate font-display font-extrabold tracking-tight text-[#30614c]",
isLarge ? "text-2xl md:text-3xl" : "text-xl"
)}
>
{headline.value}
</p>
</div>

View File

@ -1,18 +1,27 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useState, type CSSProperties } from "react";
import { lastYearWinners, lastYearWinnersCopy } from "@/content/last-year-winners";
import { LastYearWinnerMark } from "@/components/home/LastYearWinnerMark";
import { cn } from "@/lib/utils";
type Props = {
variant?: "on-green" | "on-light";
size?: "default" | "large";
/** How many company marks fit in the visible strip (marquee viewport). */
visibleCount?: number;
className?: string;
};
export function LastYearWinnersScroll({ variant = "on-green", className }: Props) {
export function LastYearWinnersScroll({
variant = "on-green",
size = "default",
visibleCount = 4,
className,
}: Props) {
const items = [...lastYearWinners, ...lastYearWinners];
const onLight = variant === "on-light";
const isLarge = size === "large";
const [hoverKey, setHoverKey] = useState<string | null>(null);
const [pinnedKey, setPinnedKey] = useState<string | null>(null);
@ -57,53 +66,65 @@ export function LastYearWinnersScroll({ variant = "on-green", className }: Props
};
}, [pinnedKey]);
return (
<div
className={cn(
"pt-8",
onLight ? "mt-8 border-t border-[#37a47a]/12" : "mt-10 border-t border-white/15",
className
)}
>
const content = (
<>
<p
className={cn(
"text-center text-[10px] font-semibold uppercase tracking-[0.2em] text-[#37a47a]"
"text-center font-semibold uppercase tracking-[0.2em] text-[#37a47a]",
isLarge ? "text-xs md:text-sm" : "text-[10px]"
)}
>
{lastYearWinnersCopy.eyebrow}
</p>
<p
className={cn(
"mt-2 text-center text-lg font-bold tracking-tight md:text-xl",
"mt-2 text-center font-bold tracking-tight",
isLarge ? "text-xl md:text-2xl" : "text-lg md:text-xl",
onLight ? "text-[#30614c]" : "text-white"
)}
>
{lastYearWinnersCopy.headline}
</p>
<p className="mt-2 text-center">
<span className="inline-block rounded-full bg-white px-3 py-1 text-[11px] font-medium text-[#37a47a] shadow-sm">
<span
className={cn(
"inline-block rounded-full bg-white font-medium text-[#37a47a] shadow-sm",
isLarge ? "px-4 py-1.5 text-xs md:text-sm" : "px-3 py-1 text-[11px]"
)}
>
{lastYearWinnersCopy.hoverHint}
</span>
</p>
<div
className="relative mt-5 overflow-hidden"
className={cn(
"relative mt-5 overflow-hidden",
isLarge && "hero-winners-viewport mx-auto"
)}
style={
isLarge
? ({
"--winners-visible": visibleCount,
} as CSSProperties)
: undefined
}
aria-label="Companies supported at last year's summit"
>
<div
className={cn(
"pointer-events-none absolute inset-y-0 left-0 z-10 w-10 bg-gradient-to-r to-transparent",
onLight ? "from-white" : "from-[#37a47a]"
"pointer-events-none absolute inset-y-0 left-0 z-10 w-12 bg-gradient-to-r to-transparent md:w-16",
onLight ? "from-[#e8f2ec]" : "from-[#37a47a]"
)}
/>
<div
className={cn(
"pointer-events-none absolute inset-y-0 right-0 z-10 w-10 bg-gradient-to-l to-transparent",
onLight ? "from-white" : "from-[#37a47a]"
"pointer-events-none absolute inset-y-0 right-0 z-10 w-12 bg-gradient-to-l to-transparent md:w-16",
onLight ? "from-[#e8f2ec]" : "from-[#37a47a]"
)}
/>
<div
className={cn(
"marquee-winners pointer-events-auto flex w-max shrink-0 items-center gap-3 py-1",
"marquee-winners pointer-events-auto flex w-max shrink-0 items-center py-1",
isLarge ? "gap-3 md:gap-4" : "gap-3",
isPaused && "marquee-winners-paused"
)}
>
@ -118,6 +139,7 @@ export function LastYearWinnersScroll({ variant = "on-green", className }: Props
tipKey={tipKey}
company={company}
variant={variant}
size={size}
isOpen={isOpen}
isPinned={isPinned}
onPin={() => pinTip(tipKey)}
@ -128,6 +150,36 @@ export function LastYearWinnersScroll({ variant = "on-green", className }: Props
})}
</div>
</div>
</>
);
if (onLight && isLarge) {
return (
<div className={cn("relative mt-auto w-full", className)}>
<div className="hero-winners-cutout relative mx-auto w-full max-w-6xl">
<div
className={cn(
"rounded-t-[1.75rem] border border-[#37a47a]/12 border-b-0",
"bg-[#e8f2ec]/95 px-4 pt-7 pb-6 shadow-[0_-10px_40px_rgba(48,97,76,0.08)]",
"md:rounded-t-[2.25rem] md:px-8 md:pt-8 md:pb-7"
)}
>
{content}
</div>
</div>
</div>
);
}
return (
<div
className={cn(
"pt-8",
onLight ? "mt-8 border-t border-[#37a47a]/12" : "mt-10 border-t border-white/15",
className
)}
>
{content}
</div>
);
}

View File

@ -15,7 +15,7 @@ export const faqs: FaqItem[] = [
id: "when",
question: "When and where does the summit take place?",
answer:
"The inaugural summit was held 31 January 1 February 2025 at Skylight Hotel, Bole, Addis Ababa, Ethiopia. Future edition dates will be announced on this site.",
"The summit takes place 2122 February 2027 at Skylight Hotel, Bole, Addis Ababa, Ethiopia.",
},
{
id: "who",

View File

@ -3,7 +3,7 @@ export const pageSeo = {
home: {
title: "Great Rift Valley Innovation Summit",
description:
"Ethiopia's premier innovation summit for agriculture, healthcare, and education. 31 Jan 01 Feb 2025 at Skylight Hotel, Addis Ababa. Tickets, pitch grants, and partnerships.",
"Ethiopia's premier innovation summit for agriculture, healthcare, and education. 21 Feb 22 Feb 2027 at Skylight Hotel, Addis Ababa. Tickets, pitch grants, and partnerships.",
path: "/",
},
program: {

View File

@ -24,7 +24,7 @@ const placeholderRow = (count: number) =>
Array.from({ length: count }, (_, i) => placeholder(i + 1));
export const partnersIntro = {
eyebrow: "Partners 2025",
eyebrow: "Partners 2027",
headline: "Meet the organizations that make GRV Summit possible",
subheadline:
"Partner logos and profiles below are placeholders — your brand could be featured here. Get in touch to secure a slot.",

View File

@ -10,7 +10,7 @@ export type ProgramDay = {
export const programDays: ProgramDay[] = [
{
id: "day-1",
date: "31 Jan 2025",
date: "21 Feb 2027",
title: "Workshops & Panel Discussions",
description:
"Curated sessions offering valuable insights for innovators and professionals at every career stage—from newcomers to seasoned executives.",
@ -23,7 +23,7 @@ export const programDays: ProgramDay[] = [
},
{
id: "day-2",
date: "01 Feb 2025",
date: "22 Feb 2027",
title: "Exhibition & Pitch Finals",
description:
"Connect with investors, companies, and startups in the exhibitor hall. Watch finalists compete for Africa's largest non-dilutive grant pool.",

View File

@ -5,9 +5,9 @@ export const site = {
"Ethiopia's premier gathering for tech-enabled innovation in agriculture, healthcare, and education.",
presentedBy: "Ethiopian Diaspora Trust Fund (EDTF)",
dates: {
label: "31 Jan 01 Feb 2025",
start: "2025-01-31",
end: "2025-02-01",
label: "21 Feb 22 Feb 2027",
start: "2027-02-21",
end: "2027-02-22",
},
venue: {
name: "Skylight Hotel",

View File

@ -4,7 +4,7 @@ export type TicketTier = {
description: string;
priceUsd: number;
priceLabel?: string;
/** e.g. "Day 2 — 01 Feb" for single-day passes */
/** e.g. "Day 2 — 22 Feb" for single-day passes */
scheduleLabel?: string;
features: string[];
soldOut?: boolean;
@ -38,7 +38,7 @@ export const ticketTiers: TicketTier[] = [
{
id: "cocktail-pass",
name: "Cocktail Pass",
scheduleLabel: "Day 2 — 01 Feb 2025",
scheduleLabel: "Day 2 — 22 Feb 2027",
description:
"Join the summit cocktail reception and evening networking on the second day at Skylight Hotel.",
priceUsd: 75,

View File

@ -1,8 +1,8 @@
import { site } from "@/content/site";
/** Event times in Addis Ababa (EAT, UTC+3) */
const EVENT_START = "20250131T080000";
const EVENT_END = "20250201T180000";
const EVENT_START = "20270221T080000";
const EVENT_END = "20270222T180000";
function formatIcsDate(iso: string) {
return iso.replace(/[-:]/g, "").split(".")[0] + "Z";
@ -25,8 +25,8 @@ export function buildOutlookCalendarUrl() {
path: "/calendar/action/compose",
rru: "addevent",
subject: site.name,
startdt: "2025-01-31T08:00:00",
enddt: "2025-02-01T18:00:00",
startdt: "2027-02-21T08:00:00",
enddt: "2027-02-22T18:00:00",
body: site.tagline,
location: `${site.venue.name}, ${site.venue.address}`,
});
@ -46,8 +46,8 @@ export function buildIcsFileContent() {
"BEGIN:VEVENT",
`UID:${uid}`,
`DTSTAMP:${now}`,
`DTSTART;TZID=Africa/Addis_Ababa:20250131T080000`,
`DTEND;TZID=Africa/Addis_Ababa:20250201T180000`,
`DTSTART;TZID=Africa/Addis_Ababa:20270221T080000`,
`DTEND;TZID=Africa/Addis_Ababa:20270222T180000`,
`SUMMARY:${site.name}`,
`DESCRIPTION:${site.tagline.replace(/\n/g, "\\n")}`,
`LOCATION:${site.venue.name}, ${site.venue.address}`,

View File

@ -1,39 +1,8 @@
import localFont from "next/font/local";
/** Antebas — primary site typeface (self-hosted from public/fonts). */
export const antebas = localFont({
src: [
{
path: "../../public/fonts/Fontspring-DEMO-antebas-thin.otf",
weight: "100",
style: "normal",
},
{
path: "../../public/fonts/Fontspring-DEMO-antebas-light.otf",
weight: "300",
style: "normal",
},
{
path: "../../public/fonts/Fontspring-DEMO-antebas-regular.otf",
weight: "400",
style: "normal",
},
{
path: "../../public/fonts/Fontspring-DEMO-antebas-medium.otf",
weight: "500",
style: "normal",
},
{
path: "../../public/fonts/Fontspring-DEMO-antebas-bold.otf",
weight: "700",
style: "normal",
},
{
path: "../../public/fonts/Fontspring-DEMO-antebas-black.otf",
weight: "900",
style: "normal",
},
],
variable: "--font-antebas",
display: "swap",
});
/**
* Antebas letterforms are declared in app/antebas.css (unicode-range limited).
* DM Sans (--font-hero-body) renders numerals, punctuation, and symbols.
* Replace DEMO .otf files with licensed Antebas to use the full glyph set.
*/
/** Site font stack — Antebas letters + DM Sans for numerals/symbols. */
export const ANTEBAS_FONT_STACK =
"Antebas, var(--font-hero-body), system-ui, sans-serif";