+
{
+ if (
+ event.currentTarget !== event.target ||
+ event.propertyName !== "transform" ||
+ isMenuOpen ||
+ !isMenuClosing
+ ) {
+ return;
+ }
+
+ setIsMenuClosing(false);
+ }}
+ />
+
+
+
+ {socialLinks.length > 0 ? (
+
+ {socialLinks.map((link) => (
+
+
+
+ ))}
+
+ ) : null}
+
+
+
+
+ >
+ );
+}
+
+function SocialIcon({ icon }: { icon: NavbarSocialLink["icon"] }) {
+ if (icon === "instagram") {
+ return
;
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/khix/src/app/_components/navbar/index.ts b/apps/khix/src/app/_components/navbar/index.ts
new file mode 100644
index 000000000..385c652e5
--- /dev/null
+++ b/apps/khix/src/app/_components/navbar/index.ts
@@ -0,0 +1,2 @@
+export { Navbar } from "./Navbar";
+export type { NavbarLink, NavbarSocialLink } from "./Navbar";
diff --git a/apps/khix/src/app/_components/navbar/site-navigation.ts b/apps/khix/src/app/_components/navbar/site-navigation.ts
new file mode 100644
index 000000000..4cec41a4a
--- /dev/null
+++ b/apps/khix/src/app/_components/navbar/site-navigation.ts
@@ -0,0 +1,28 @@
+import type { NavbarLink, NavbarSocialLink } from "./Navbar";
+
+export const KHIX_HOME_NAV_LINKS = [
+ { label: "About", href: "#about" },
+ { label: "Tracks", href: "#tracks" },
+ { label: "Speakers", href: "#speakers" },
+ { label: "Sponsors", href: "#sponsors" },
+ { label: "Team", href: "#team" },
+ { label: "FAQ", href: "#faq" },
+] satisfies NavbarLink[];
+
+export const KHIX_SITE_NAV_LINKS = KHIX_HOME_NAV_LINKS.map((link) => ({
+ ...link,
+ href: link.href === "#home" ? "/" : `/${link.href}`,
+})) satisfies NavbarLink[];
+
+export const KHIX_SOCIAL_LINKS = [
+ {
+ label: "Join Knight Hacks on Discord",
+ href: "https://discord.knighthacks.org/",
+ icon: "discord",
+ },
+ {
+ label: "Knight Hacks on Instagram",
+ href: "https://www.instagram.com/knighthacks/",
+ icon: "instagram",
+ },
+] satisfies NavbarSocialLink[];
diff --git a/apps/khix/src/app/_components/not-found/KhixPublicNotFound.module.css b/apps/khix/src/app/_components/not-found/KhixPublicNotFound.module.css
new file mode 100644
index 000000000..9f6464959
--- /dev/null
+++ b/apps/khix/src/app/_components/not-found/KhixPublicNotFound.module.css
@@ -0,0 +1,191 @@
+.notFoundPage {
+ position: relative;
+ min-height: 100vh;
+ min-height: 100svh;
+ overflow: hidden;
+ background: #07100f;
+ color: rgb(255 252 244 / 0.96);
+ isolation: isolate;
+}
+
+.heroScene {
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ overflow: hidden;
+ pointer-events: none;
+}
+
+.heroScene > :first-child {
+ min-height: 100vh;
+ min-height: 100svh;
+}
+
+.heroScene :global([data-hero-title]) {
+ display: none;
+}
+
+.sceneMist {
+ position: absolute;
+ inset: 0;
+ z-index: 20;
+ background:
+ linear-gradient(
+ 180deg,
+ rgb(238 246 207 / 0.08) 0%,
+ rgb(7 16 15 / 0.04) 32%,
+ rgb(7 16 15 / 0.58) 100%
+ ),
+ radial-gradient(ellipse at 18% 72%, rgb(7 16 15 / 0.58), transparent 31rem),
+ radial-gradient(
+ ellipse at 80% 20%,
+ rgb(238 246 207 / 0.12),
+ transparent 28rem
+ ),
+ linear-gradient(90deg, rgb(7 16 15 / 0.54), transparent 58%);
+}
+
+.content {
+ position: absolute;
+ top: 29%;
+ left: 14%;
+ z-index: 2;
+ width: min(78vw, 90rem);
+ padding: 0;
+}
+
+.copy {
+ width: min(42rem, 100%);
+}
+
+.title {
+ margin: 0;
+ max-width: none;
+ color: rgb(255 252 244 / 0.98);
+ font-family: var(--font-cormorant-garamond, Georgia), Georgia, serif;
+ font-size: clamp(3.3rem, 7.3vw, 6.35rem);
+ font-weight: 400;
+ letter-spacing: 0;
+ line-height: 0.82;
+ white-space: nowrap;
+ text-shadow:
+ 0 0 0.6rem rgb(210 211 131 / 0.2),
+ 0 1.4rem 4rem rgb(7 16 15 / 0.82);
+}
+
+.body {
+ width: min(28rem, 100%);
+ margin: clamp(0.8rem, 1.8vh, 1.15rem) 0 0;
+ color: rgb(255 252 244 / 0.82);
+ font-size: clamp(1rem, 2vw, 1.18rem);
+ font-weight: 680;
+ line-height: 1.35;
+ text-shadow: 0 0.35rem 1.1rem rgb(7 16 15 / 0.58);
+}
+
+.returnLink {
+ display: inline-flex;
+ min-height: 2.65rem;
+ align-items: center;
+ gap: 0.58rem;
+ justify-content: flex-start;
+ margin-top: clamp(0.85rem, 2vh, 1.2rem);
+ color: rgb(255 255 255 / 0.92);
+ text-decoration: none;
+ transition:
+ color 180ms ease,
+ opacity 180ms ease,
+ transform 180ms ease;
+}
+
+.returnLink:hover,
+.returnLink:focus-visible {
+ color: #ffffff;
+ opacity: 1;
+ outline: none;
+ transform: translateX(-0.18rem);
+}
+
+.returnLink:focus-visible {
+ outline: 2px solid rgb(255 255 255 / 0.72);
+ outline-offset: 0.18rem;
+}
+
+.returnIcon {
+ width: 1.55rem;
+ height: 1.55rem;
+ flex: 0 0 auto;
+ stroke-width: 1.55;
+}
+
+.returnText {
+ color: rgb(255 255 255 / 0.84);
+ font-size: clamp(0.82rem, 1.55vw, 0.98rem);
+ font-weight: 300;
+ line-height: 1;
+ text-shadow: 0 0.3rem 1rem rgb(7 16 15 / 0.5);
+}
+
+@media (max-width: 760px) {
+ .sceneMist {
+ background:
+ linear-gradient(
+ 180deg,
+ rgb(238 246 207 / 0.08) 0%,
+ rgb(7 16 15 / 0.08) 38%,
+ rgb(7 16 15 / 0.72) 100%
+ ),
+ radial-gradient(
+ ellipse at 24% 82%,
+ rgb(7 16 15 / 0.72),
+ transparent 20rem
+ ),
+ linear-gradient(90deg, rgb(7 16 15 / 0.48), rgb(7 16 15 / 0.16));
+ }
+
+ .content {
+ top: 20%;
+ left: 6%;
+ width: 88vw;
+ }
+
+ .copy {
+ width: 100%;
+ }
+
+ .title {
+ font-size: clamp(2.75rem, 12vw, 3.05rem);
+ line-height: 0.82;
+ }
+
+ .body {
+ font-size: clamp(0.92rem, 4vw, 1.02rem);
+ line-height: 1.44;
+ }
+
+ .returnLink {
+ min-height: 2.35rem;
+ gap: 0.48rem;
+ margin-top: 0.82rem;
+ }
+
+ .returnIcon {
+ width: 1.35rem;
+ height: 1.35rem;
+ }
+
+ .returnText {
+ font-size: clamp(0.74rem, 3.4vw, 0.88rem);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .returnLink {
+ transition: none;
+ }
+
+ .returnLink:hover,
+ .returnLink:focus-visible {
+ transform: none;
+ }
+}
diff --git a/apps/khix/src/app/_components/not-found/KhixPublicNotFound.tsx b/apps/khix/src/app/_components/not-found/KhixPublicNotFound.tsx
new file mode 100644
index 000000000..7104644f8
--- /dev/null
+++ b/apps/khix/src/app/_components/not-found/KhixPublicNotFound.tsx
@@ -0,0 +1,48 @@
+import Link from "next/link";
+import { ArrowLeft } from "lucide-react";
+
+import { Footer } from "../footer";
+import { Navbar } from "../navbar";
+import {
+ KHIX_SITE_NAV_LINKS,
+ KHIX_SOCIAL_LINKS,
+} from "../navbar/site-navigation";
+import Hero from "../sections/hero";
+import styles from "./KhixPublicNotFound.module.css";
+
+export function KhixPublicNotFound() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ Page not found
+
+
You've ventured out of the forest.
+
+
+
+ Return to the enchanted forest
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/khix/src/app/_components/not-found/index.ts b/apps/khix/src/app/_components/not-found/index.ts
new file mode 100644
index 000000000..8a6e40991
--- /dev/null
+++ b/apps/khix/src/app/_components/not-found/index.ts
@@ -0,0 +1 @@
+export { KhixPublicNotFound } from "./KhixPublicNotFound";
diff --git a/apps/khix/src/app/_components/sections/about/AboutCorruption.module.css b/apps/khix/src/app/_components/sections/about/AboutCorruption.module.css
new file mode 100644
index 000000000..42972a14e
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/about/AboutCorruption.module.css
@@ -0,0 +1,43 @@
+.corruption {
+ position: absolute;
+ inset: 0;
+ z-index: auto;
+ overflow: visible;
+ pointer-events: none;
+}
+
+.front {
+ position: absolute;
+ display: block;
+ pointer-events: none;
+}
+
+.front {
+ top: var(--khix-front-top);
+ right: 0;
+ left: 0;
+ z-index: 0;
+ height: var(--khix-front-height);
+ background-image: url("https://assets.knighthacks.org/khix/extended-front.webp");
+ background-repeat: no-repeat;
+ background-position: center top;
+ background-size: 100% auto;
+}
+
+@media (min-width: 761px) and (max-width: 1180px) {
+ .front {
+ bottom: 0;
+ height: auto;
+ background-size: cover;
+ }
+}
+
+@media (max-width: 760px) {
+ .front {
+ bottom: 0;
+ height: auto;
+ background-image: url("https://assets.knighthacks.org/khix/extended-front-mobile.webp");
+ background-position: center -25vw;
+ background-size: 120% auto;
+ }
+}
diff --git a/apps/khix/src/app/_components/sections/about/AboutCorruption.tsx b/apps/khix/src/app/_components/sections/about/AboutCorruption.tsx
new file mode 100644
index 000000000..444b9acaf
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/about/AboutCorruption.tsx
@@ -0,0 +1,9 @@
+import styles from "./AboutCorruption.module.css";
+
+export function AboutCorruption() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/khix/src/app/_components/sections/about/AboutGallery.tsx b/apps/khix/src/app/_components/sections/about/AboutGallery.tsx
new file mode 100644
index 000000000..c3ae9e33a
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/about/AboutGallery.tsx
@@ -0,0 +1,145 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { ArrowBigLeft, ArrowBigRight } from "lucide-react";
+
+import styles from "./AboutSection.module.css";
+
+const AUTO_ADVANCE_DELAY_MS = 4600;
+
+const GALLERY_IMAGES = [
+ {
+ src: "https://assets.knighthacks.org/khix/about-gallery-community.webp",
+ srcSet:
+ "https://assets.knighthacks.org/khix/about-gallery-community-640.webp 640w, https://assets.knighthacks.org/khix/about-gallery-community-1280.webp 1280w",
+ alt: "Hackers gathering in the UCF Student Union during Knight Hacks",
+ },
+ {
+ src: "https://assets.knighthacks.org/khix/about-gallery-crowd.webp",
+ srcSet:
+ "https://assets.knighthacks.org/khix/about-gallery-crowd-640.webp 640w, https://assets.knighthacks.org/khix/about-gallery-crowd-1280.webp 1280w",
+ alt: "Knight Hacks participants filling the UCF Student Union",
+ },
+ {
+ src: "https://assets.knighthacks.org/khix/about-gallery-team.webp",
+ srcSet:
+ "https://assets.knighthacks.org/khix/about-gallery-team-640.webp 640w, https://assets.knighthacks.org/khix/about-gallery-team-1280.webp 1280w",
+ alt: "Knight Hacks organizers and attendees holding a signed event banner",
+ },
+] as const;
+
+export function AboutGallery() {
+ const [activeImageIndex, setActiveImageIndex] = useState(0);
+ const [isPaused, setIsPaused] = useState(false);
+ const [loadedImageIndexes, setLoadedImageIndexes] = useState(
+ () => new Set([0, 1]),
+ );
+
+ const loadImage = (index: number) => {
+ setLoadedImageIndexes((currentIndexes) => {
+ if (currentIndexes.has(index)) return currentIndexes;
+
+ return new Set(currentIndexes).add(index);
+ });
+ };
+
+ const showImage = (index: number) => {
+ loadImage(index);
+ setActiveImageIndex(index);
+ loadImage((index + 1) % GALLERY_IMAGES.length);
+ };
+
+ const showPreviousImage = () => {
+ showImage(
+ (activeImageIndex - 1 + GALLERY_IMAGES.length) % GALLERY_IMAGES.length,
+ );
+ };
+
+ const showNextImage = () => {
+ showImage((activeImageIndex + 1) % GALLERY_IMAGES.length);
+ };
+
+ useEffect(() => {
+ if (isPaused) return;
+
+ const timeoutId = window.setTimeout(showNextImage, AUTO_ADVANCE_DELAY_MS);
+
+ return () => window.clearTimeout(timeoutId);
+ }, [activeImageIndex, isPaused]);
+
+ return (
+
setIsPaused(true)}
+ onMouseLeave={() => setIsPaused(false)}
+ onFocusCapture={() => setIsPaused(true)}
+ onBlurCapture={(event) => {
+ if (!event.currentTarget.contains(event.relatedTarget)) {
+ setIsPaused(false);
+ }
+ }}
+ >
+
+ {GALLERY_IMAGES.map((image, index) =>
+ loadedImageIndexes.has(index) ? (
+
+ ) : null,
+ )}
+
+
+
+
+ loadImage(
+ (activeImageIndex - 1 + GALLERY_IMAGES.length) %
+ GALLERY_IMAGES.length,
+ )
+ }
+ onPointerEnter={() =>
+ loadImage(
+ (activeImageIndex - 1 + GALLERY_IMAGES.length) %
+ GALLERY_IMAGES.length,
+ )
+ }
+ >
+
+
+
+ {String(activeImageIndex + 1).padStart(2, "0")} /{" "}
+ {String(GALLERY_IMAGES.length).padStart(2, "0")}
+
+
+ loadImage((activeImageIndex + 1) % GALLERY_IMAGES.length)
+ }
+ onPointerEnter={() =>
+ loadImage((activeImageIndex + 1) % GALLERY_IMAGES.length)
+ }
+ >
+
+
+
+
+ );
+}
diff --git a/apps/khix/src/app/_components/sections/about/AboutSection.module.css b/apps/khix/src/app/_components/sections/about/AboutSection.module.css
new file mode 100644
index 000000000..241714aa9
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/about/AboutSection.module.css
@@ -0,0 +1,609 @@
+.about {
+ position: relative;
+ z-index: 2;
+ display: grid;
+ min-height: 160vh;
+ min-height: 160svh;
+ padding: clamp(10rem, 24svh, 18rem) clamp(1.5rem, 8vw, 8rem)
+ clamp(20rem, 38svh, 32rem);
+ align-content: center;
+ justify-items: center;
+ isolation: isolate;
+}
+
+.leftRune {
+ --asset-credit-width: clamp(15rem, 24vw, 27rem);
+
+ position: absolute;
+ top: 50%;
+ left: clamp(-3rem, -2vw, -0.75rem);
+ z-index: 4;
+ width: var(--asset-credit-width);
+ aspect-ratio: 518 / 840;
+ pointer-events: none;
+ transform: translateY(-50%);
+}
+
+.rockImage,
+.glowImage {
+ object-fit: contain;
+ object-position: center;
+ pointer-events: none;
+ user-select: none;
+}
+
+.rockImage {
+ z-index: 1;
+}
+
+.glowImage {
+ z-index: 2;
+ filter: brightness(1.12) saturate(1.18)
+ drop-shadow(0 0 0.42rem rgb(0 228 255 / 0.82))
+ drop-shadow(0 0 1.15rem rgb(0 195 255 / 0.48));
+ opacity: 0.88;
+ animation: aboutRuneGlowPulse 2.8s ease-in-out infinite alternate;
+ will-change: filter, opacity;
+}
+
+.content {
+ position: relative;
+ z-index: 6;
+ display: grid;
+ width: min(64rem, calc(100% - clamp(14rem, 24vw, 22rem)));
+ justify-items: center;
+ justify-self: end;
+ gap: clamp(3rem, 7svh, 6rem);
+ margin-right: clamp(0rem, 2vw, 2rem);
+}
+
+.metricsPanel {
+ display: grid;
+ width: 100%;
+ justify-items: center;
+ gap: clamp(1.6rem, 3svh, 2.4rem);
+}
+
+.copy {
+ position: relative;
+ width: 100%;
+ margin: 0;
+ color: rgba(249, 255, 246, 0.94);
+ font-family:
+ var(--font-geist-sans, ui-sans-serif),
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ sans-serif;
+ font-size: clamp(1rem, 0.72vw + 0.72rem, 1.2rem);
+ line-height: 1.58;
+ text-align: left;
+ text-shadow:
+ 0 0.1rem 0.75rem rgba(0, 0, 0, 0.58),
+ 0 0 1.2rem rgba(13, 38, 32, 0.48);
+ transition:
+ color 220ms ease,
+ text-shadow 260ms ease,
+ transform 220ms ease;
+}
+
+.storyTitle,
+.storySubtitle {
+ margin: 0;
+ color: rgba(255, 252, 245, 0.96);
+ font-family: var(--font-khix), Georgia, "Times New Roman", serif;
+ font-weight: 700;
+ line-height: 1.05;
+ text-shadow:
+ 0 0 0.38rem rgba(235, 251, 255, 0.42),
+ 0 0 1.4rem rgba(129, 225, 255, 0.22);
+ text-wrap: balance;
+}
+
+.storyTitle {
+ font-size: clamp(2.15rem, 3.4vw, 3.3rem);
+}
+
+.storySubtitle {
+ margin-top: clamp(2rem, 4svh, 3rem);
+ font-size: clamp(1.75rem, 2.6vw, 2.5rem);
+}
+
+.copy p {
+ margin: 0;
+}
+
+.copy p + p {
+ margin-top: 1rem;
+}
+
+.storyTitle + p,
+.storySubtitle + p {
+ margin-top: 1rem;
+}
+
+.storyRow {
+ display: grid;
+ grid-template-columns: minmax(24rem, 1.2fr) minmax(20rem, 0.8fr);
+ width: 100%;
+ align-items: center;
+ gap: clamp(2rem, 5vw, 5.5rem);
+}
+
+.rightStump {
+ --asset-credit-width: clamp(18rem, 34vw, 38rem);
+
+ position: absolute;
+ top: clamp(10rem, 22svh, 17rem);
+ right: 0;
+ z-index: 5;
+ display: block;
+ width: var(--asset-credit-width);
+ opacity: 0.96;
+ filter: drop-shadow(0 1.2rem 1.6rem rgb(0 0 0 / 0.38));
+ pointer-events: none;
+}
+
+.rightStump img {
+ display: block;
+ width: 100%;
+ height: auto;
+}
+
+.gallery {
+ position: relative;
+ min-width: 0;
+ isolation: isolate;
+}
+
+.galleryViewport {
+ position: relative;
+ width: 100%;
+ aspect-ratio: 3 / 2;
+ overflow: hidden;
+ border: 1px solid rgb(207 255 224 / 0.24);
+ border-radius: 0.5rem;
+ background: #112522;
+ box-shadow:
+ 0 1.2rem 3rem rgb(0 0 0 / 0.32),
+ 0 0 2.5rem rgb(73 255 196 / 0.09);
+}
+
+.galleryViewport::after {
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+ pointer-events: none;
+ content: "";
+ background: linear-gradient(180deg, transparent 52%, rgb(2 18 15 / 0.7));
+}
+
+.galleryImage {
+ position: absolute;
+ inset: 0;
+ z-index: 1;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: center;
+ opacity: 0;
+ transform: scale(1.045);
+ transition:
+ opacity 560ms ease,
+ transform 4.6s ease;
+}
+
+.galleryImage[data-active="true"] {
+ opacity: 1;
+ transform: scale(1);
+}
+
+.galleryControls {
+ position: absolute;
+ right: clamp(0.55rem, 1.35vw, 1rem);
+ bottom: clamp(0.55rem, 1.35vw, 1rem);
+ z-index: 3;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.galleryArrow {
+ display: inline-grid;
+ width: 2.15rem;
+ aspect-ratio: 1;
+ place-items: center;
+ border: 0;
+ color: rgb(255 255 255 / 0.96);
+ background: rgb(7 20 17 / 0.68);
+ cursor: pointer;
+ transition:
+ background 180ms ease,
+ color 180ms ease,
+ transform 180ms ease;
+}
+
+.galleryArrow svg {
+ width: 1.65rem;
+ height: 1.65rem;
+ fill: currentColor;
+ stroke: currentColor;
+ stroke-width: 1.2;
+}
+
+.galleryArrow:hover,
+.galleryArrow:focus-visible {
+ color: #d7ff76;
+ background: rgb(7 20 17 / 0.9);
+ outline: 1px solid rgb(215 255 118 / 0.74);
+ outline-offset: 2px;
+ transform: translateY(-1px);
+}
+
+.galleryCount {
+ min-width: 3.4rem;
+ margin: 0;
+ color: rgb(255 255 255 / 0.82);
+ font-size: 0.72rem;
+ font-weight: 800;
+ letter-spacing: 0.08em;
+ text-align: center;
+}
+
+.successTitle {
+ margin: 0;
+ color: rgba(255, 252, 245, 0.98);
+ font-size: clamp(1.7rem, 2.8vw, 2.8rem);
+ font-weight: 700;
+ line-height: 1;
+ text-align: center;
+ text-shadow:
+ 0 0 0.38rem rgba(235, 251, 255, 0.42),
+ 0 0 1.4rem rgba(129, 225, 255, 0.22);
+ text-wrap: balance;
+}
+
+.metrics {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ width: min(64rem, 100%);
+ align-items: flex-start;
+ gap: clamp(2rem, 5svh, 3.75rem) clamp(2rem, 8vw, 7rem);
+ margin: 0;
+}
+
+.metric {
+ display: grid;
+ min-width: 0;
+ justify-items: center;
+ gap: 0.55rem;
+ color: rgba(255, 252, 245, 0.98);
+ text-align: center;
+ text-shadow:
+ 0 0.12rem 0.8rem rgba(0, 0, 0, 0.62),
+ 0 0 1.2rem rgba(20, 94, 89, 0.35);
+}
+
+.metric dt {
+ margin: 0;
+ font-size: clamp(2.25rem, 3.6vw, 4rem);
+ font-weight: 800;
+ line-height: 0.9;
+ white-space: nowrap;
+}
+
+.metricNumber {
+ color: #ca96f8;
+ text-shadow:
+ 0 0.12rem 0.8rem rgba(0, 0, 0, 0.62),
+ 0 0 0.42rem rgb(202 150 248 / 0.58),
+ 0 0 1.05rem rgb(202 150 248 / 0.38);
+ animation: aboutMetricGlowPulse 2.8s ease-in-out infinite alternate;
+}
+
+.metric dd {
+ margin: 0;
+ font-size: clamp(0.92rem, 1.25vw, 1.18rem);
+ font-weight: 800;
+ line-height: 1.08;
+ text-wrap: balance;
+}
+
+@media (min-width: 1200px) {
+ .about {
+ --about-closing-offset: clamp(3.75rem, 10svh, 6.5rem);
+ --about-gallery-growth: clamp(2rem, 4vw, 4rem);
+ --about-row-spread: clamp(2rem, 8svh, 5rem);
+
+ padding-top: clamp(9rem, 16svh, 11rem);
+ padding-right: clamp(3rem, 4vw, 5rem);
+ padding-bottom: calc(
+ clamp(20rem, 38svh, 32rem) - var(--about-row-spread) -
+ var(--about-row-spread) - var(--about-closing-offset) -
+ var(--about-gallery-growth)
+ );
+ padding-left: clamp(3rem, 4vw, 5rem);
+ align-content: start;
+ }
+
+ .content {
+ grid-template-columns: repeat(12, minmax(0, 1fr));
+ width: 100%;
+ justify-self: stretch;
+ column-gap: clamp(1.25rem, 2vw, 2.5rem);
+ row-gap: calc(clamp(4rem, 8svh, 7rem) + var(--about-row-spread));
+ margin-right: 0;
+ }
+
+ .leftRune {
+ top: 54%;
+ }
+
+ .metricsPanel {
+ grid-row: 2;
+ grid-column: 6 / 12;
+ box-sizing: border-box;
+ width: 100%;
+ margin-right: 0;
+ padding-left: clamp(1rem, 2vw, 2.5rem);
+ }
+
+ .storyRow,
+ .copy {
+ display: contents;
+ }
+
+ .introStory {
+ grid-row: 1;
+ grid-column: 1 / -1;
+ width: min(54rem, 100%);
+ justify-self: start;
+ /* Follow the desktop navbar logo's responsive left edge. */
+ margin-left: max(0rem, calc(46vw - 39.25rem));
+ }
+
+ .secondaryStory {
+ grid-row: 3;
+ grid-column: 1 / 8;
+ width: min(42rem, 100%);
+ align-self: center;
+ justify-self: start;
+ margin-top: var(--about-closing-offset);
+ margin-left: max(0rem, calc(46vw - 39.25rem));
+ }
+
+ .secondaryStory .storySubtitle {
+ margin-top: 0;
+ }
+
+ .gallery {
+ grid-row: 3;
+ grid-column: 9 / -1;
+ width: min(34rem, 100%);
+ align-self: start;
+ justify-self: end;
+ margin-top: var(--about-closing-offset);
+ }
+
+ .metrics {
+ column-gap: clamp(1rem, 3vw, 3rem);
+ }
+
+ .metric dt {
+ font-size: clamp(2.25rem, 3vw, 4rem);
+ }
+}
+
+@media (min-width: 1500px) {
+ .introStory,
+ .secondaryStory {
+ margin-left: max(
+ 0rem,
+ calc(
+ 46vw - clamp(40.5rem, 39vw, 58rem) + clamp(1.5rem, 1.51vw, 2.02rem) -
+ 0.25rem
+ )
+ );
+ }
+}
+
+@media (max-width: 1100px) and (min-width: 761px) {
+ .content {
+ width: min(52rem, calc(100% - 14rem));
+ }
+
+ .storyRow {
+ grid-template-columns: 1fr;
+ width: min(44rem, 100%);
+ }
+
+ .gallery {
+ width: min(40rem, 100%);
+ justify-self: center;
+ }
+
+ .metrics {
+ column-gap: clamp(1rem, 4vw, 2.5rem);
+ }
+}
+
+@media (max-width: 900px) and (min-width: 761px) {
+ .metrics {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 760px) {
+ .about {
+ min-height: auto;
+ padding: clamp(3rem, 6svh, 4rem) clamp(1.35rem, 8vw, 2rem)
+ clamp(4rem, 10svh, 7rem);
+ align-content: start;
+ justify-items: center;
+ }
+
+ .rightStump {
+ top: clamp(7rem, 15svh, 10rem);
+ width: clamp(15rem, 62vw, 22rem);
+ opacity: 0.9;
+ }
+
+ .content {
+ width: 100%;
+ justify-self: center;
+ gap: clamp(2.5rem, 7svh, 3.5rem);
+ margin-right: 0;
+ }
+
+ .metricsPanel {
+ gap: clamp(1.4rem, 4svh, 2rem);
+ }
+
+ .copy {
+ width: 100%;
+ font-size: clamp(0.92rem, 3.5vw, 1rem);
+ line-height: 1.5;
+ text-align: left;
+ }
+
+ .storyRow {
+ grid-template-columns: 1fr;
+ width: min(32rem, 100%);
+ gap: clamp(1.8rem, 5svh, 2.5rem);
+ }
+
+ .leftRune {
+ top: 58%;
+ left: clamp(-3.25rem, -10vw, -2rem);
+ width: clamp(10rem, 48vw, 13rem);
+ }
+
+ .storyTitle {
+ font-size: clamp(2rem, 9vw, 2.75rem);
+ }
+
+ .storySubtitle {
+ font-size: clamp(1.65rem, 7.5vw, 2.2rem);
+ }
+
+ .successTitle {
+ font-size: clamp(1.65rem, 8vw, 2.25rem);
+ }
+
+ .metrics {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ width: 100%;
+ gap: clamp(1.4rem, 6vw, 2rem) 0.85rem;
+ }
+
+ .metric {
+ min-width: 0;
+ }
+
+ .metric dt {
+ font-size: clamp(1.9rem, 9vw, 2.5rem);
+ }
+
+ .metric dd {
+ font-size: clamp(0.78rem, 3.6vw, 0.94rem);
+ }
+}
+
+@media (hover: hover) and (pointer: fine) {
+ .copy:hover {
+ color: white;
+ text-shadow:
+ 0 0.08rem 0.7rem rgba(0, 0, 0, 0.58),
+ 0 0 0.38rem rgb(238 255 255 / 0.6),
+ 0 0 1.35rem rgb(49 222 255 / 0.48);
+ transform: translateY(-0.18rem);
+ }
+
+ .copy:hover::after {
+ opacity: 0.9;
+ transform: scaleX(1);
+ }
+}
+
+@media (max-width: 760px) {
+ .copy {
+ animation: aboutCopyGlow 3.8s ease-in-out infinite alternate;
+ }
+
+ .copy::after {
+ animation: aboutCopyLineGlow 3.8s ease-in-out infinite alternate;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .glowImage,
+ .copy,
+ .copy::after,
+ .galleryImage,
+ .metricNumber {
+ animation: none;
+ transition: none;
+ }
+}
+
+@keyframes aboutRuneGlowPulse {
+ from {
+ filter: brightness(1.02) saturate(1.08)
+ drop-shadow(0 0 0.28rem rgb(0 228 255 / 0.58))
+ drop-shadow(0 0 0.78rem rgb(0 195 255 / 0.34));
+ opacity: 0.72;
+ }
+
+ to {
+ filter: brightness(1.2) saturate(1.28)
+ drop-shadow(0 0 0.55rem rgb(0 238 255 / 0.94))
+ drop-shadow(0 0 1.45rem rgb(0 195 255 / 0.58));
+ opacity: 1;
+ }
+}
+
+@keyframes aboutCopyGlow {
+ from {
+ color: rgba(249, 255, 246, 0.9);
+ text-shadow:
+ 0 0.1rem 0.75rem rgba(0, 0, 0, 0.58),
+ 0 0 0.5rem rgb(49 222 255 / 0.12);
+ }
+
+ to {
+ color: white;
+ text-shadow:
+ 0 0.08rem 0.7rem rgba(0, 0, 0, 0.58),
+ 0 0 0.4rem rgb(238 255 255 / 0.48),
+ 0 0 1.15rem rgb(49 222 255 / 0.34);
+ }
+}
+
+@keyframes aboutCopyLineGlow {
+ from {
+ opacity: 0.24;
+ transform: scaleX(0.35);
+ }
+
+ to {
+ opacity: 0.78;
+ transform: scaleX(0.9);
+ }
+}
+
+@keyframes aboutMetricGlowPulse {
+ from {
+ text-shadow:
+ 0 0.12rem 0.8rem rgba(0, 0, 0, 0.62),
+ 0 0 0.3rem rgb(202 150 248 / 0.48),
+ 0 0 0.82rem rgb(202 150 248 / 0.3);
+ }
+
+ to {
+ text-shadow:
+ 0 0.12rem 0.8rem rgba(0, 0, 0, 0.62),
+ 0 0 0.6rem rgb(202 150 248 / 0.98),
+ 0 0 1.55rem rgb(202 150 248 / 0.72);
+ }
+}
diff --git a/apps/khix/src/app/_components/sections/about/AboutSection.tsx b/apps/khix/src/app/_components/sections/about/AboutSection.tsx
new file mode 100644
index 000000000..759656313
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/about/AboutSection.tsx
@@ -0,0 +1,130 @@
+import Image from "next/image";
+
+import { AssetCredit } from "../../assets";
+import { AboutGallery } from "./AboutGallery";
+import styles from "./AboutSection.module.css";
+
+export function AboutSection() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Join us at Florida’s best hackathon
+
+
+ From October 9–11, step into Knight Hacks IX, a 36-hour
+ experience built around curiosity, creativity, and community.
+ Spend the weekend chasing ideas, meeting new people, and
+ discovering what you can create when everyone around you is
+ building too.
+
+
+ Explore workshops, mentor sessions, sponsor challenges,
+ late-night activities, and focused time with your team. Bring an
+ idea or find one along the way. Knight Hacks gives you the
+ people, energy, and space to make it happen.
+
+
+ The event is free for accepted hackers, with meals, snacks, and
+ drinks provided throughout the weekend.
+
+
+
+
+
Start wherever you are
+
+ You don’t need a polished idea, a complete team, or years of
+ experience. First-time and returning hackers alike will find a
+ place to take creative risks, learn by doing, and contribute in
+ their own way.
+
+
+ Bring your curiosity and your perspective. Leave with a project
+ you’re proud of, people you’ll want to build with again, and a
+ weekend you won’t forget.
+
+
+
+
+
+
+
+
Last year at Knight Hacks
+
+
+
1,000+
+ Hackers
+
+
+
188
+ Projects
+
+
+
75
+ Activities
+
+
+
$60,000
+ in Prizes
+
+
+
Awarded
+ UCF's Best Large-Scale Event
+
+
+
9.2/10
+ Average Hacker Rating
+
+
+
+
+
+ );
+}
diff --git a/apps/khix/src/app/_components/sections/about/index.ts b/apps/khix/src/app/_components/sections/about/index.ts
new file mode 100644
index 000000000..d0242f555
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/about/index.ts
@@ -0,0 +1,2 @@
+export { AboutSection } from "./AboutSection";
+export { AboutCorruption } from "./AboutCorruption";
diff --git a/apps/khix/src/app/_components/sections/faq/LennyBlink.module.css b/apps/khix/src/app/_components/sections/faq/LennyBlink.module.css
new file mode 100644
index 000000000..42295a2c4
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/faq/LennyBlink.module.css
@@ -0,0 +1,27 @@
+.lenny {
+ cursor: default;
+}
+
+.frameStack {
+ position: relative;
+ display: block;
+ aspect-ratio: 2667 / 3318;
+ width: 100%;
+ filter: drop-shadow(0 0.6rem 0.5rem rgba(5, 8, 24, 0.35));
+ transform: translateZ(0);
+}
+
+.frame {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ opacity: 0;
+ user-select: none;
+ transform: translateZ(0);
+}
+
+.frame[data-active="true"] {
+ opacity: 1;
+}
diff --git a/apps/khix/src/app/_components/sections/faq/LennyBlink.tsx b/apps/khix/src/app/_components/sections/faq/LennyBlink.tsx
new file mode 100644
index 000000000..8e43df57a
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/faq/LennyBlink.tsx
@@ -0,0 +1,39 @@
+import Image from "next/image";
+
+import { AssetCredit } from "../../assets";
+import styles from "./LennyBlink.module.css";
+
+interface LennyBlinkProps {
+ className?: string;
+}
+
+export function LennyBlink({ className }: LennyBlinkProps) {
+ const rootClassName = className
+ ? `${styles.lenny} ${className}`
+ : styles.lenny;
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/apps/khix/src/app/_components/sections/faq/faq.module.css b/apps/khix/src/app/_components/sections/faq/faq.module.css
new file mode 100644
index 000000000..4a0743cf3
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/faq/faq.module.css
@@ -0,0 +1,606 @@
+.faq {
+ --faq-motion-scale: 0.72;
+ --faq-body-font: var(--font-khix), Georgia, "Times New Roman", serif;
+ --faq-answer-link-color: #ca96f8;
+
+ position: relative;
+ width: 100%;
+ max-width: 100vw;
+ min-height: 100vh;
+ min-height: 100svh;
+ overflow: hidden;
+ overflow-x: clip;
+ overflow-anchor: none;
+ isolation: isolate;
+ background: #15192d;
+}
+
+.motionLayer {
+ --faq-pointer-x: var(--faq-motion-pointer-x, 0);
+ --faq-pointer-y: var(--faq-motion-pointer-y, 0);
+
+ display: contents;
+}
+
+.background {
+ position: absolute;
+ inset: -4rem -2rem;
+ z-index: 0;
+ background-image: var(--faq-background-image, none);
+ background-repeat: no-repeat;
+ background-position: center top;
+ background-size: cover;
+ filter: saturate(1.08) brightness(0.86);
+ transform: translate3d(
+ calc(var(--faq-pointer-x) * -18 * var(--faq-motion-scale) * 1px),
+ calc(var(--faq-pointer-y) * -10 * var(--faq-motion-scale) * 1px),
+ 0
+ );
+ backface-visibility: hidden;
+ will-change: transform;
+}
+
+.atmosphereVeil {
+ position: absolute;
+ inset: 0;
+ z-index: 4;
+ pointer-events: none;
+ background:
+ radial-gradient(
+ ellipse at 52% 12%,
+ rgba(139, 249, 255, 0.16),
+ transparent 38%
+ ),
+ linear-gradient(
+ 180deg,
+ rgba(6, 7, 16, 0.34),
+ rgba(10, 13, 29, 0.1) 42%,
+ rgba(5, 6, 14, 0.5)
+ );
+}
+
+.content {
+ --faq-content-align-x: clamp(-1.25rem, -1.5vw, -0.75rem);
+ --faq-content-padding-top: clamp(8rem, 13svh, 11rem);
+ --faq-content-padding-inline: clamp(1rem, 4vw, 2.5rem);
+
+ position: relative;
+ z-index: 10;
+ display: flex;
+ min-height: 100vh;
+ min-height: 100svh;
+ width: min(100%, 58rem);
+ margin: 0 auto;
+ padding: var(--faq-content-padding-top) var(--faq-content-padding-inline)
+ clamp(18rem, 34svh, 28rem);
+ flex-direction: column;
+ align-items: center;
+}
+
+.dividerTitle {
+ position: absolute;
+ top: 46%;
+ left: 50%;
+ z-index: 1;
+ margin: 0;
+ color: rgba(255, 252, 245, 0.96);
+ font-family: var(--font-cormorant-garamond, Georgia), Georgia, serif;
+ font-size: clamp(3.4rem, 8vw, 6.8rem);
+ font-weight: 400;
+ line-height: 0.84;
+ letter-spacing: 0;
+ text-shadow:
+ 0 0 0.5rem rgba(235, 251, 255, 0.54),
+ 0 0 2.2rem rgba(129, 225, 255, 0.3);
+ text-align: center;
+ white-space: nowrap;
+ translate: -50% -50%;
+}
+
+.faqSeparatorCredit {
+ --asset-credit-top: auto;
+ --asset-credit-right: clamp(1rem, 3vw, 2.5rem);
+ --asset-credit-bottom: clamp(1rem, 2vw, 1.5rem);
+
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+}
+
+.dividerTitleQ {
+ /* Bagnard kerns F/A but not A/Q, so balance only the unkerned pair. */
+ display: inline-block;
+ margin-inline-start: -0.05em;
+}
+
+.categoryList {
+ display: grid;
+ width: min(100%, 47rem);
+ margin-inline: auto;
+ margin-top: clamp(1.25rem, 2.8svh, 2.15rem);
+ align-self: center;
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ gap: clamp(0.35rem, 0.9vw, 0.75rem);
+ translate: var(--faq-content-align-x) 0;
+}
+
+.gemButton {
+ --gem-label-x: 0;
+ --gem-label-y: -0.02rem;
+
+ position: relative;
+ display: grid;
+ min-width: 0;
+ min-height: 0;
+ aspect-ratio: 364 / 202;
+ padding: 0;
+ place-items: center;
+ border: 0;
+ cursor: pointer;
+ color: rgba(255, 255, 255, 0.96);
+ font-family: var(--font-cormorant-garamond, Georgia), Georgia, serif;
+ font-size: clamp(0.78rem, 1.25vw, 1.02rem);
+ font-weight: 850;
+ line-height: 1.08;
+ letter-spacing: 0;
+ text-align: center;
+ background: transparent;
+ isolation: isolate;
+ transition: transform 160ms ease;
+}
+
+.gemButton[data-active="true"] {
+ --gem-label-y: -0.08rem;
+}
+
+.gemButton:active {
+ transform: translateY(0.08rem) scale(0.98);
+}
+
+.gemButton:focus-visible {
+ outline: 0.14rem solid rgba(255, 255, 255, 0.86);
+ outline-offset: -0.9rem;
+}
+
+.gemImage {
+ position: absolute;
+ inset: 50% auto auto 50%;
+ width: 100%;
+ height: 100%;
+ max-height: none;
+ object-fit: contain;
+ transform: translate(-50%, -50%);
+ user-select: none;
+ pointer-events: none;
+}
+
+.gemButton span {
+ position: relative;
+ z-index: 1;
+ display: block;
+ max-width: 6.2rem;
+ text-wrap: balance;
+ transform: translate(var(--gem-label-x), var(--gem-label-y));
+}
+
+.questionStack {
+ --faq-answer-overflow-reserve: 0rem;
+
+ display: grid;
+ width: min(100%, 56rem);
+ min-height: 0;
+ margin-inline: auto;
+ margin-top: clamp(1.1rem, 2.2svh, 1.65rem);
+ align-self: center;
+ align-content: start;
+ grid-template-columns: minmax(0, 1fr);
+ gap: clamp(0.82rem, 1.8vw, 1.25rem);
+ translate: var(--faq-content-align-x) 0;
+}
+
+.questionStack[data-height-locked="true"] {
+ height: calc(
+ var(--faq-resting-stack-height) + var(--faq-answer-overflow-reserve)
+ );
+}
+
+.questionItem {
+ display: grid;
+ min-width: 0;
+ grid-template-columns: minmax(0, 1fr);
+ justify-items: center;
+ width: 100%;
+}
+
+.questionButton {
+ position: relative;
+ z-index: 1;
+ display: grid;
+ width: min(100%, 54rem);
+ min-height: clamp(5.8rem, 9vw, 8.25rem);
+ aspect-ratio: 1034 / 140;
+ padding: clamp(0.74rem, 1.3vw, 1rem) clamp(4.6rem, 10vw, 8rem)
+ clamp(0.9rem, 1.6vw, 1.15rem);
+ place-items: center;
+ border: 0;
+ cursor: pointer;
+ color: rgba(255, 255, 255, 0.96);
+ font-family: var(--font-cormorant-garamond, Georgia), Georgia, serif;
+ font-size: clamp(1.18rem, 1.48vw, 1.38rem);
+ font-weight: 700;
+ line-height: 1.14;
+ letter-spacing: 0;
+ text-align: center;
+ background: transparent;
+ transition: transform 180ms ease;
+}
+
+.questionButton[aria-expanded="true"] {
+ transform: none;
+}
+
+.questionButton:focus-visible {
+ outline: 0.14rem solid rgba(255, 255, 255, 0.84);
+ outline-offset: -0.7rem;
+}
+
+.questionRock {
+ position: absolute;
+ inset: 50% auto auto 50%;
+ width: 100%;
+ height: 100%;
+ object-fit: fill;
+ transform: translate(-50%, -50%) translateZ(0);
+ backface-visibility: hidden;
+ user-select: none;
+ pointer-events: none;
+}
+
+.questionButton span {
+ position: relative;
+ z-index: 1;
+ max-width: min(100%, 39rem);
+ top: -0.1rem;
+ color: rgb(3 19 25 / 0.58);
+ text-wrap: balance;
+ transition: color 180ms ease;
+}
+
+.questionButton:hover span,
+.questionButton:focus-visible span,
+.questionButton[aria-expanded="true"] span {
+ color: rgb(3 19 25 / 0.72);
+}
+
+.answerWrap {
+ width: min(100%, 52rem);
+ margin-top: clamp(-1.8rem, -1.6vw, -0.65rem);
+ overflow: hidden;
+ transform-origin: top center;
+}
+
+.answerRock {
+ position: relative;
+ display: grid;
+ width: min(100%, 52rem);
+ margin: 0 auto 0.2rem;
+ padding: clamp(2.35rem, 4vw, 3.1rem) clamp(4.5rem, 9vw, 6.6rem)
+ clamp(2.25rem, 4.2vw, 3.2rem);
+ place-items: center;
+ color: rgba(255, 255, 255, 0.96);
+ font-family: var(--faq-body-font);
+}
+
+.answerRockImage {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: fill;
+ backface-visibility: hidden;
+ user-select: none;
+ pointer-events: none;
+ transform: translateZ(0);
+}
+
+.answerRock p {
+ position: relative;
+ z-index: 1;
+ margin: 0;
+ max-width: 39rem;
+ font-family: var(--font-cormorant-garamond, Georgia), Georgia, serif;
+ font-size: clamp(0.94rem, 1.28vw, 1.12rem);
+ font-weight: 600;
+ line-height: 1.12;
+ letter-spacing: 0;
+ top: clamp(-0.55rem, -0.65vw, -0.2rem);
+ color: rgb(240 244 250 / 0.96);
+ text-align: center;
+ text-wrap: pretty;
+}
+
+.answerRock a {
+ color: var(--faq-answer-link-color);
+ text-decoration-color: currentColor;
+ text-underline-offset: 0.12em;
+}
+
+.floatingAsset {
+ --faq-layer-depth-x: 0;
+ --faq-layer-depth-y: 0;
+
+ position: absolute;
+ z-index: 6;
+ pointer-events: none;
+ transform: translate3d(
+ calc(
+ var(--faq-pointer-x) * var(--faq-layer-depth-x) *
+ var(--faq-motion-scale) * 1px
+ ),
+ calc(
+ var(--faq-pointer-y) * var(--faq-layer-depth-y) *
+ var(--faq-motion-scale) * 1px
+ ),
+ 0
+ );
+ backface-visibility: hidden;
+ will-change: transform;
+}
+
+.floatingAsset img {
+ display: block;
+ width: 100%;
+ height: auto;
+ user-select: none;
+ filter: drop-shadow(0 0 1.15rem rgba(92, 226, 255, 0.18));
+}
+
+.gemHotspot {
+ position: absolute;
+ z-index: 2;
+ padding: 0;
+ cursor: pointer;
+ border: 0;
+ clip-path: var(--hotspot-shape);
+ background: transparent;
+ pointer-events: auto;
+}
+
+.gemHotspot:focus-visible {
+ outline: 0.16rem solid rgba(255, 255, 255, 0.96);
+ outline-offset: 0.1rem;
+}
+
+.caveCeilingPrimary {
+ top: -8%;
+ left: -13%;
+ z-index: 2;
+ width: min(84rem, 132vw);
+ opacity: 0.96;
+}
+
+.caveCeilingSecondary {
+ top: -12%;
+ right: -18%;
+ z-index: 2;
+ width: min(70rem, 108vw);
+ opacity: 0.7;
+}
+
+.caveCeilingSecondary img {
+ transform: scaleX(-1);
+}
+
+.leftColumn {
+ top: 0;
+ left: 0;
+ width: clamp(21rem, 36vw, 35rem);
+ animation: faqWallBreathe 7.1s ease-in-out infinite;
+}
+
+.rightColumn {
+ top: 0;
+ right: 0;
+ width: clamp(27rem, 46vw, 45rem);
+ animation: faqWallBreathe 7.4s ease-in-out 0.3s infinite reverse;
+}
+
+@media (hover: hover) and (pointer: fine) {
+ .questionButton:hover {
+ transform: translateY(-0.08rem) scale(1.045);
+ }
+}
+
+@media (hover: none) and (pointer: coarse) {
+ .questionButton span {
+ transition: none;
+ }
+}
+
+@media (min-width: 1800px) {
+ .background {
+ background-color: #15192d;
+ background-image:
+ linear-gradient(
+ 90deg,
+ #15192d 0,
+ #15192d calc(50% - 62rem),
+ transparent calc(50% - 50rem),
+ transparent calc(50% + 50rem),
+ #15192d calc(50% + 62rem),
+ #15192d 100%
+ ),
+ var(--faq-background-image, none);
+ background-position:
+ center,
+ center top;
+ background-size:
+ 100% 100%,
+ min(100%, 124rem) auto;
+ }
+}
+
+@media (min-width: 821px) {
+ .questionStack {
+ padding-bottom: clamp(2.5rem, 3.5vw, 4rem);
+ }
+}
+
+@media (max-width: 820px) {
+ .faq {
+ --faq-motion-scale: 0;
+ }
+
+ .background,
+ .floatingAsset {
+ transform: none;
+ will-change: auto;
+ }
+
+ .background {
+ background-position: center top;
+ background-size: 100% auto;
+ }
+
+ .caveCeilingPrimary {
+ top: -4.5rem;
+ }
+
+ .caveCeilingSecondary {
+ top: -6.75rem;
+ }
+
+ .content {
+ --faq-content-align-x: 0rem;
+ --faq-content-padding-top: clamp(5.5rem, 9svh, 7rem);
+
+ width: min(100%, 40rem);
+ padding-bottom: clamp(12.5rem, 26svh, 14rem);
+ }
+
+ .categoryList {
+ grid-template-columns: repeat(6, minmax(0, 1fr));
+ width: min(100%, 32rem);
+ margin-top: 1rem;
+ gap: 0.4rem 0.45rem;
+ }
+
+ .gemButton {
+ grid-column: span 2;
+ font-size: clamp(0.72rem, 3.1vw, 0.88rem);
+ }
+
+ .categoryList .gemButton:nth-child(4) {
+ grid-column: 2 / span 2;
+ }
+
+ .categoryList .gemButton:nth-child(5) {
+ grid-column: 4 / span 2;
+ }
+
+ .questionStack {
+ min-height: 0;
+ margin-top: 1rem;
+ gap: 0.65rem;
+ }
+
+ .questionStack[data-height-locked="true"] {
+ height: auto;
+ }
+
+ .questionButton {
+ min-height: 4.5rem;
+ padding-inline: clamp(2rem, 8vw, 3.25rem);
+ font-size: clamp(0.92rem, 3.8vw, 1.05rem);
+ }
+
+ .answerWrap {
+ width: min(100%, 37rem);
+ margin-top: -0.8rem;
+ }
+
+ .leftColumn {
+ left: -10%;
+ width: 62vw;
+ animation: none;
+ }
+
+ .rightColumn {
+ right: -14%;
+ width: 78vw;
+ animation: none;
+ }
+}
+
+@media (max-width: 520px) {
+ .faq {
+ cursor: auto;
+ }
+
+ .background {
+ inset: -1.25rem;
+ }
+
+ .dividerTitle {
+ font-size: clamp(3.5rem, 16vw, 4.5rem);
+ }
+
+ .questionStack {
+ min-height: 0;
+ }
+
+ .questionButton {
+ min-height: 4.35rem;
+ padding-inline: clamp(1.65rem, 7vw, 2.25rem);
+ }
+
+ .questionButton span {
+ max-width: 17rem;
+ top: -0.06rem;
+ }
+
+ .answerWrap {
+ margin-top: -0.85rem;
+ }
+
+ .answerRock {
+ padding: 2.45rem 2rem 2.3rem;
+ }
+
+ .answerRock p {
+ max-width: 19rem;
+ font-size: 1rem;
+ font-weight: 600;
+ line-height: 1.22;
+ top: -0.2rem;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .faq {
+ cursor: auto;
+ }
+
+ .background,
+ .floatingAsset {
+ transform: none !important;
+ }
+
+ .leftColumn,
+ .rightColumn {
+ animation: none;
+ }
+}
+
+@keyframes faqWallBreathe {
+ 0%,
+ 100% {
+ translate: 0 0;
+ scale: 1;
+ }
+
+ 50% {
+ translate: 0 -0.38rem;
+ scale: 1.012;
+ }
+}
diff --git a/apps/khix/src/app/_components/sections/faq/faq.tsx b/apps/khix/src/app/_components/sections/faq/faq.tsx
new file mode 100644
index 000000000..9937e3fdd
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/faq/faq.tsx
@@ -0,0 +1,856 @@
+"use client";
+
+import type { CSSProperties } from "react";
+import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
+import Image from "next/image";
+import { AnimatePresence, motion } from "framer-motion";
+
+import { AssetCredit } from "../../assets";
+import styles from "./faq.module.css";
+import { useFaqMotion } from "./useFaqMotion";
+
+const faqSections = [
+ {
+ id: "general",
+ label: "General",
+ note: 174.61,
+ questions: [
+ {
+ question: "What is a hackathon?",
+ answer:
+ "A hackathon is a weekend where students team up to learn, experiment, and build something new. At Knight Hacks IX, you’ll have workshops, mentors, sponsor events, food, swag, and plenty of time to create.",
+ },
+ {
+ question: "What is Knight Hacks IX?",
+ answer:
+ "Knight Hacks IX is a 36-hour hackathon happening October 9-11, 2026 at the University of Central Florida.",
+ },
+ {
+ question: "Who can attend?",
+ answer:
+ "If you're currently a college student or have graduated in the past year, you can attend! Beginners are absolutely welcome!",
+ },
+ {
+ question: "How much experience do I need?",
+ answer:
+ "None! Knight Hacks is beginner-friendly, whether this is your first hackathon or you've been building projects for years. We'll have workshops, mentors, team formation support, and a welcoming environment to help you learn and build.",
+ },
+ {
+ question: "How much does it cost?",
+ answer: "Nothing! Knight Hacks is free for accepted hackers to attend.",
+ },
+ ],
+ },
+ {
+ id: "event",
+ label: "Event",
+ note: 220,
+ questions: [
+ {
+ question: "When is check-in?",
+ answer:
+ "Check-in begins at 4:00 PM on Friday, October 9, 2026. Check-in will be in Engineering II 102. Please check the Hackers Guide closer to the event for the specific arrival instructions.",
+ },
+ {
+ question: "When and where is opening ceremony?",
+ answer:
+ "Opening ceremony starts at 6:30 PM on Friday, October 9, 2026 on Memory Mall.",
+ },
+ {
+ question: "When and where is the career fair?",
+ answer:
+ "The career fair will take place at 9:00 PM in Student Union Cape Florida Ballroom.",
+ },
+ {
+ question: "When is closing ceremony?",
+ answer:
+ "Closing ceremony will happen asynchronously in Student Union Cape Florida Ballroom and Key West Ballroom. More details will be shared in the Hackers Guide and event announcements.",
+ },
+ {
+ question: "Do I have to stay the whole time?",
+ answer:
+ "Nope! We encourage you to stay for as much of the event as you can so you don't miss workshops, sponsor events, judging, and community activities, but you're free to come and go as needed.",
+ },
+ ],
+ },
+ {
+ id: "logistics",
+ label: "Logistics",
+ note: 261.63,
+ questions: [
+ {
+ question: "Where can I sleep?",
+ answer:
+ "We'll provide designated quiet/rest areas for attendees during the event. Bring anything that helps you rest comfortably, like a blanket, hoodie, or sleeping bag, and make sure to take care of yourself throughout the weekend.",
+ },
+ {
+ question: "What should I bring?",
+ answer:
+ "Bring your laptop, charger, student ID or government ID, any hardware you want to use, a reusable water bottle, toiletries if you're staying overnight, and anything you need to stay comfortable.",
+ },
+ {
+ question: "Is food provided?",
+ answer:
+ "Yes! Knight Hacks provides free meals, snacks, and drinks throughout the event. Food will be given out in Room 119 –– we are not delivering food to individual hackers.",
+ },
+ {
+ question: "Are there showers available?",
+ answer:
+ "Showers will be available at the Recreation and Wellness Center. More details will be shared in the Hackers Guide closer to the event.",
+ },
+ {
+ question: "Is parking available?",
+ answer:
+ "Parking information will be shared in the Hackers Guide closer to the event, including any garage/pass instructions.",
+ },
+ ],
+ },
+ {
+ id: "projects",
+ label: "Projects",
+ note: 293.66,
+ questions: [
+ {
+ question: "What can I build?",
+ answer:
+ "Anything your heart desires! You can build a web app, mobile app, game, AI/ML project, hardware project, embedded project, or anything else you're excited about. Sponsor challenges and tracks may also be available to help inspire your project.",
+ },
+ {
+ question: "How does project submission and judging work?",
+ answer:
+ "Projects must be submitted through Devpost and use GitHub for version control. During judging, teams will present their projects to general judges and sponsor judges. Final judging details will be shared in the Hackers Guide and event announcements.",
+ },
+ {
+ question: "Can I use a past project or something I've built before?",
+ answer:
+ "No. Projects must be started after hacking begins. You're welcome to brainstorm ideas or learn tools ahead of time, but actual project work should happen during the hackathon to keep the competition fair.",
+ },
+ {
+ question: "Should I come with a project idea already planned?",
+ answer:
+ "It's up to you! Some hackers come in with ideas, while others get inspired by workshops, sponsor challenges, tracks, or teammates they meet during the event. Both approaches are totally valid.",
+ },
+ {
+ question: "What if I don't have a laptop or it's not powerful enough?",
+ answer: (
+ <>
+ Don't let that stop you from participating. Reach out to{" "}
+
hack@knighthacks.org
+ {" and "}we'll do our best to help you find a solution. You can
+ also team up with others who have the hardware or setup your project
+ needs.
+ >
+ ),
+ },
+ ],
+ },
+ {
+ id: "community",
+ label: "Community",
+ note: 329.63,
+ questions: [
+ {
+ question: "Do I need a team?",
+ answer:
+ "Nope! You can come solo, bring a team, or find teammates at the event. Teams may have up to four people, and we'll help hackers connect with potential teammates.",
+ },
+ {
+ question: "How do I find teammates?",
+ answer:
+ "Join the Knight Hacks Discord to meet other hackers before the event. We'll also have opportunities during the hackathon to help hackers form teams in person.",
+ },
+ {
+ question: "My question wasn't answered. Where can I ask?",
+ answer: (
+ <>
+ Check the Hackers Guide for the most up-to-date information. If you
+ still have questions, ask in the Knight Hacks Discord or email{" "}
+
hack@knighthacks.org .
+ >
+ ),
+ },
+ {
+ question: "How can I become a sponsor?",
+ answer: (
+ <>
+ We'd love to have your organization sponsor Knight Hacks!
+ Sponsorship gives companies the opportunity to connect with talented
+ students, host challenges or workshops, and support Central
+ Florida's builder community. Visit{" "}
+
+ https://blade.knighthacks.org/sponsor
+ {" "}
+ or contact Knight Hacks for more information.
+ >
+ ),
+ },
+ {
+ question: "Can I volunteer at Knight Hacks?",
+ answer: (
+ <>
+ Yes! Volunteers help with check-in, logistics, event support, and
+ making the weekend run smoothly. If you're interested in
+ volunteering, reach out to{" "}
+
hack@knighthacks.org .
+ >
+ ),
+ },
+ ],
+ },
+] as const;
+
+interface GemHotspot {
+ id: string;
+ label: string;
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ clipPath: string;
+ note: number;
+}
+
+interface FloatingAsset {
+ src: string;
+ alt: string;
+ className: string;
+ width: number;
+ height: number;
+ depth: number;
+ hotspots?: readonly GemHotspot[];
+}
+
+function faqClass(name: string) {
+ const className = styles[name];
+
+ if (!className) {
+ throw new Error(`Missing FAQ style: ${name}`);
+ }
+
+ return className;
+}
+
+const floatingAssets = [
+ {
+ src: "https://assets.knighthacks.org/khix/faq-FAQ cave ceiling - Thomas Ha.webp",
+ alt: "",
+ className: faqClass("caveCeilingPrimary"),
+ width: 4500,
+ height: 3000,
+ depth: -18,
+ },
+ {
+ src: "https://assets.knighthacks.org/khix/FAQ cave ceiling.webp",
+ alt: "",
+ className: faqClass("caveCeilingSecondary"),
+ width: 4500,
+ height: 3000,
+ depth: -12,
+ },
+ {
+ src: "https://assets.knighthacks.org/khix/faq-leftcol.webp",
+ alt: "",
+ className: faqClass("leftColumn"),
+ width: 698,
+ height: 2291,
+ depth: 16,
+ hotspots: [
+ {
+ id: "left-small-mushroom",
+ label: "Play the small blue mushroom",
+ x: 6,
+ y: 17,
+ width: 29,
+ height: 8,
+ clipPath:
+ "polygon(8% 18%, 54% 0, 94% 22%, 78% 52%, 58% 58%, 64% 100%, 42% 100%, 38% 58%, 12% 52%)",
+ note: 392,
+ },
+ {
+ id: "left-tall-crystal",
+ label: "Play the tall violet crystal",
+ x: 19,
+ y: 12,
+ width: 39,
+ height: 17,
+ clipPath:
+ "polygon(45% 0, 68% 32%, 100% 72%, 72% 100%, 18% 88%, 0 55%, 25% 35%)",
+ note: 261.63,
+ },
+ {
+ id: "left-middle-mushroom",
+ label: "Play the middle cyan mushroom",
+ x: 19,
+ y: 38,
+ width: 49,
+ height: 11,
+ clipPath:
+ "polygon(9% 30%, 45% 0, 82% 8%, 100% 35%, 82% 55%, 58% 58%, 66% 100%, 38% 100%, 43% 58%, 12% 55%)",
+ note: 329.63,
+ },
+ {
+ id: "left-large-mushroom",
+ label: "Play the large glowing mushroom",
+ x: 0,
+ y: 51,
+ width: 78,
+ height: 14,
+ clipPath:
+ "polygon(0 28%, 22% 6%, 57% 0, 88% 18%, 100% 42%, 80% 58%, 58% 61%, 62% 100%, 30% 100%, 37% 61%, 8% 55%)",
+ note: 220,
+ },
+ {
+ id: "left-bottom-crystals",
+ label: "Play the lower violet crystal cluster",
+ x: 0,
+ y: 68,
+ width: 80,
+ height: 17,
+ clipPath:
+ "polygon(0 38%, 18% 20%, 32% 30%, 54% 5%, 66% 34%, 100% 0, 86% 54%, 55% 82%, 20% 100%, 0 82%)",
+ note: 174.61,
+ },
+ ],
+ },
+ {
+ src: "https://assets.knighthacks.org/khix/faq-rightcol.webp",
+ alt: "",
+ className: faqClass("rightColumn"),
+ width: 982,
+ height: 2140,
+ depth: 16,
+ hotspots: [
+ {
+ id: "right-top-crystal",
+ label: "Play the hanging violet crystal",
+ x: 58,
+ y: 5,
+ width: 42,
+ height: 24,
+ clipPath:
+ "polygon(0 12%, 30% 0, 100% 4%, 100% 78%, 76% 100%, 34% 86%, 18% 55%)",
+ note: 293.66,
+ },
+ {
+ id: "right-pink-crystals",
+ label: "Play the pink crystal outcrop",
+ x: 73,
+ y: 27,
+ width: 27,
+ height: 15,
+ clipPath:
+ "polygon(18% 0, 42% 18%, 63% 8%, 100% 22%, 100% 100%, 10% 88%, 0 42%)",
+ note: 349.23,
+ },
+ {
+ id: "right-large-mushroom",
+ label: "Play the large blue mushroom",
+ x: 69,
+ y: 44,
+ width: 28,
+ height: 12,
+ clipPath:
+ "polygon(4% 28%, 45% 0, 88% 18%, 100% 42%, 77% 58%, 57% 60%, 65% 100%, 39% 100%, 43% 60%, 10% 55%)",
+ note: 440,
+ },
+ {
+ id: "right-mushroom-cluster",
+ label: "Play the glowing mushroom cluster",
+ x: 70,
+ y: 51,
+ width: 30,
+ height: 16,
+ clipPath:
+ "polygon(0 8%, 28% 0, 45% 22%, 66% 3%, 100% 18%, 100% 100%, 12% 92%)",
+ note: 523.25,
+ },
+ {
+ id: "right-lower-mushroom",
+ label: "Play the lower blue mushroom",
+ x: 78,
+ y: 63,
+ width: 18,
+ height: 8,
+ clipPath:
+ "polygon(0 22%, 45% 0, 100% 28%, 84% 55%, 62% 60%, 72% 100%, 40% 100%, 45% 60%, 12% 52%)",
+ note: 587.33,
+ },
+ {
+ id: "right-bottom-crystals",
+ label: "Play the lower pink crystal cluster",
+ x: 70,
+ y: 73,
+ width: 30,
+ height: 15,
+ clipPath:
+ "polygon(0 18%, 34% 32%, 48% 0, 67% 38%, 100% 22%, 100% 100%, 22% 88%)",
+ note: 466.16,
+ },
+ ],
+ },
+] satisfies readonly FloatingAsset[];
+
+type FaqSectionId = (typeof faqSections)[number]["id"];
+
+const FAQ_ASSET_PRELOAD_MARGIN = "2000px 0px";
+const FAQ_BACKGROUND_IMAGE =
+ 'url("https://assets.knighthacks.org/khix/faq-background-1440.webp")';
+const FAQ_MOBILE_BACKGROUND_IMAGE =
+ 'url("https://assets.knighthacks.org/khix/faq-background-768.webp")';
+const FAQ_SEPARATOR_IMAGE =
+ 'url("https://assets.knighthacks.org/khix/separator-rocks-faq-1920.webp")';
+const FAQ_MOBILE_SEPARATOR_IMAGE =
+ 'url("https://assets.knighthacks.org/khix/separator-rocks-faq-768.webp")';
+
+function useDeferredFaqAssets
() {
+ const elementRef = useRef(null);
+ const [shouldLoad, setShouldLoad] = useState(false);
+
+ useEffect(() => {
+ const element = elementRef.current;
+
+ if (!element || !("IntersectionObserver" in window)) {
+ const frameId = window.requestAnimationFrame(() => setShouldLoad(true));
+ return () => window.cancelAnimationFrame(frameId);
+ }
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (!entry?.isIntersecting) return;
+
+ setShouldLoad(true);
+ observer.disconnect();
+ },
+ { rootMargin: FAQ_ASSET_PRELOAD_MARGIN },
+ );
+
+ observer.observe(element);
+
+ return () => observer.disconnect();
+ }, []);
+
+ return [elementRef, shouldLoad] as const;
+}
+
+function useFaqResponsiveAsset(desktopAsset: string, mobileAsset: string) {
+ const [asset, setAsset] = useState(desktopAsset);
+
+ useEffect(() => {
+ const mediaQuery = window.matchMedia("(max-width: 820px)");
+ const updateAsset = () => {
+ setAsset(mediaQuery.matches ? mobileAsset : desktopAsset);
+ };
+
+ updateAsset();
+ mediaQuery.addEventListener("change", updateAsset);
+
+ return () => mediaQuery.removeEventListener("change", updateAsset);
+ }, [desktopAsset, mobileAsset]);
+
+ return asset;
+}
+
+const getAudioContext = () => {
+ return new AudioContext();
+};
+
+function playCaveNote(frequency: number) {
+ const context = getAudioContext();
+
+ const now = context.currentTime;
+ const primary = context.createOscillator();
+ const shimmer = context.createOscillator();
+ const rumble = context.createOscillator();
+ const primaryGain = context.createGain();
+ const shimmerGain = context.createGain();
+ const rumbleGain = context.createGain();
+ const filter = context.createBiquadFilter();
+ const delay = context.createDelay();
+ const feedback = context.createGain();
+ const wetGain = context.createGain();
+ const output = context.createGain();
+
+ primary.type = "sine";
+ primary.frequency.setValueAtTime(frequency, now);
+ primary.frequency.exponentialRampToValueAtTime(frequency * 0.985, now + 1.1);
+
+ shimmer.type = "triangle";
+ shimmer.frequency.setValueAtTime(frequency * 2.01, now);
+
+ rumble.type = "sine";
+ rumble.frequency.setValueAtTime(frequency / 2, now);
+
+ filter.type = "lowpass";
+ filter.frequency.setValueAtTime(1180, now);
+ filter.Q.setValueAtTime(5, now);
+
+ delay.delayTime.setValueAtTime(0.19, now);
+ feedback.gain.setValueAtTime(0.32, now);
+ wetGain.gain.setValueAtTime(0.2, now);
+
+ primaryGain.gain.setValueAtTime(0.0001, now);
+ primaryGain.gain.exponentialRampToValueAtTime(0.23, now + 0.035);
+ primaryGain.gain.exponentialRampToValueAtTime(0.0001, now + 1.35);
+
+ shimmerGain.gain.setValueAtTime(0.0001, now);
+ shimmerGain.gain.exponentialRampToValueAtTime(0.06, now + 0.02);
+ shimmerGain.gain.exponentialRampToValueAtTime(0.0001, now + 0.72);
+
+ rumbleGain.gain.setValueAtTime(0.0001, now);
+ rumbleGain.gain.exponentialRampToValueAtTime(0.04, now + 0.08);
+ rumbleGain.gain.exponentialRampToValueAtTime(0.0001, now + 1.15);
+
+ output.gain.setValueAtTime(0.62, now);
+ output.gain.exponentialRampToValueAtTime(0.0001, now + 1.55);
+
+ primary.connect(primaryGain);
+ shimmer.connect(shimmerGain);
+ rumble.connect(rumbleGain);
+ primaryGain.connect(filter);
+ shimmerGain.connect(filter);
+ rumbleGain.connect(filter);
+ filter.connect(output);
+ filter.connect(delay);
+ delay.connect(feedback);
+ feedback.connect(delay);
+ delay.connect(wetGain);
+ wetGain.connect(output);
+ output.connect(context.destination);
+
+ primary.start(now);
+ shimmer.start(now);
+ rumble.start(now);
+ primary.stop(now + 1.6);
+ shimmer.stop(now + 1.6);
+ rumble.stop(now + 1.6);
+}
+
+export default function FAQ() {
+ const [activeSectionId, setActiveSectionId] =
+ useState("general");
+ const [openQuestion, setOpenQuestion] = useState(null);
+ const [faqRef, shouldLoadFaqAssets] = useDeferredFaqAssets();
+ const faqBackgroundImage = useFaqResponsiveAsset(
+ FAQ_BACKGROUND_IMAGE,
+ FAQ_MOBILE_BACKGROUND_IMAGE,
+ );
+ const { motionLayerRef, handlePointerMove, handlePointerLeave } =
+ useFaqMotion();
+ const activeSection = useMemo(
+ () => faqSections.find((section) => section.id === activeSectionId),
+ [activeSectionId],
+ );
+ const questionStackRef = useStableFaqStack(activeSectionId);
+
+ if (!activeSection) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {floatingAssets.map((asset) => (
+
+ ))}
+
+
+
+
+
+
+ {faqSections.map((section) => (
+ {
+ setActiveSectionId(section.id);
+ setOpenQuestion(null);
+ playCaveNote(section.note);
+ }}
+ />
+ ))}
+
+
+
+ {activeSection.questions.map((item, index) => (
+
+ setOpenQuestion((currentQuestion) =>
+ currentQuestion === index ? null : index,
+ )
+ }
+ />
+ ))}
+
+
+
+ );
+}
+
+export function FAQTitle({ className }: { className?: string }) {
+ const [titleRef, shouldLoadSeparator] =
+ useDeferredFaqAssets();
+ const faqSeparatorImage = useFaqResponsiveAsset(
+ FAQ_SEPARATOR_IMAGE,
+ FAQ_MOBILE_SEPARATOR_IMAGE,
+ );
+
+ return (
+
+ );
+}
+
+function ParallaxAsset({ asset }: { asset: FloatingAsset }) {
+ return (
+
+
+ {asset.hotspots?.map((hotspot) => (
+ playCaveNote(hotspot.note)}
+ />
+ ))}
+
+ );
+}
+
+function useStableFaqStack(activeSectionId: FaqSectionId) {
+ const questionStackRef = useRef(null);
+
+ useLayoutEffect(() => {
+ const questionStack = questionStackRef.current;
+
+ if (!questionStack) {
+ return;
+ }
+
+ let measurementFrame = 0;
+ let measuredWidth = questionStack.getBoundingClientRect().width;
+
+ const lockRestingHeight = () => {
+ // Measure without the lock, then remove the open answer's contribution so
+ // a resize while expanded still preserves the collapsed stack height.
+ questionStack.dataset.heightLocked = "false";
+
+ const expandedHeight = questionStack.getBoundingClientRect().height;
+ const answer = questionStack.querySelector(
+ `.${styles.answerWrap}`,
+ );
+ const answerStyle = answer ? window.getComputedStyle(answer) : null;
+ const answerContribution = answer
+ ? answer.getBoundingClientRect().height +
+ Number.parseFloat(answerStyle?.marginTop ?? "0") +
+ Number.parseFloat(answerStyle?.marginBottom ?? "0")
+ : 0;
+ const restingHeight = Math.max(0, expandedHeight - answerContribution);
+
+ questionStack.style.setProperty(
+ "--faq-resting-stack-height",
+ `${restingHeight}px`,
+ );
+ questionStack.dataset.heightLocked = "true";
+ };
+
+ const scheduleMeasurement = () => {
+ window.cancelAnimationFrame(measurementFrame);
+ measurementFrame = window.requestAnimationFrame(lockRestingHeight);
+ };
+
+ lockRestingHeight();
+
+ const resizeObserver = new ResizeObserver(([entry]) => {
+ const width = entry?.contentRect.width ?? 0;
+
+ if (Math.abs(width - measuredWidth) < 0.5) {
+ return;
+ }
+
+ measuredWidth = width;
+ scheduleMeasurement();
+ });
+
+ resizeObserver.observe(questionStack);
+
+ return () => {
+ resizeObserver.disconnect();
+ window.cancelAnimationFrame(measurementFrame);
+ };
+ }, [activeSectionId]);
+
+ return questionStackRef;
+}
+
+function GemstoneButton({
+ section,
+ isActive,
+ onSelect,
+}: {
+ section: (typeof faqSections)[number];
+ isActive: boolean;
+ onSelect: () => void;
+}) {
+ return (
+
+
+ {section.label}
+
+ );
+}
+
+function FaqQuestion({
+ item,
+ isOpen,
+ onToggle,
+}: {
+ item: (typeof faqSections)[number]["questions"][number];
+ isOpen: boolean;
+ onToggle: () => void;
+}) {
+ return (
+
+
+
+ {item.question}
+
+
+
+ {/* Transforming text-bearing layers produces glyph artifacts in iOS
+ Safari, so the rock reveal animates by clipping its height only. */}
+ {isOpen ? (
+
+
+
+ ) : null}
+
+
+ );
+}
diff --git a/apps/khix/src/app/_components/sections/faq/useFaqMotion.ts b/apps/khix/src/app/_components/sections/faq/useFaqMotion.ts
new file mode 100644
index 000000000..bfe420b53
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/faq/useFaqMotion.ts
@@ -0,0 +1,118 @@
+import type { PointerEvent, RefObject } from "react";
+import { useEffect, useRef } from "react";
+
+interface FaqMotion {
+ motionLayerRef: RefObject;
+ handlePointerMove: (event: PointerEvent) => void;
+ handlePointerLeave: () => void;
+}
+
+const setMotionNumber = (
+ element: HTMLElement | null,
+ property: string,
+ value: number,
+) => {
+ element?.style.setProperty(property, value.toFixed(4));
+};
+
+export function useFaqMotion(): FaqMotion {
+ const motionLayerRef = useRef(null);
+ const pointerFrameRef = useRef(0);
+ const pendingPointerRef = useRef({ x: 0, y: 0 });
+ const shouldReduceMotionRef = useRef(false);
+ const motionValuesRef = useRef({
+ pointerX: 0,
+ pointerY: 0,
+ });
+
+ const setPointerVars = (x: number, y: number) => {
+ const motionValues = motionValuesRef.current;
+
+ if (
+ Math.abs(motionValues.pointerX - x) < 0.001 &&
+ Math.abs(motionValues.pointerY - y) < 0.001
+ ) {
+ return;
+ }
+
+ motionValues.pointerX = x;
+ motionValues.pointerY = y;
+ setMotionNumber(motionLayerRef.current, "--faq-motion-pointer-x", x);
+ setMotionNumber(motionLayerRef.current, "--faq-motion-pointer-y", y);
+ };
+
+ const cancelPointerFrame = () => {
+ if (!pointerFrameRef.current) {
+ return;
+ }
+
+ window.cancelAnimationFrame(pointerFrameRef.current);
+ pointerFrameRef.current = 0;
+ };
+
+ const schedulePointerVars = (x: number, y: number) => {
+ pendingPointerRef.current = { x, y };
+
+ if (pointerFrameRef.current) {
+ return;
+ }
+
+ pointerFrameRef.current = window.requestAnimationFrame(() => {
+ pointerFrameRef.current = 0;
+ setPointerVars(pendingPointerRef.current.x, pendingPointerRef.current.y);
+ });
+ };
+
+ useEffect(() => {
+ const reduceMotionQuery = window.matchMedia(
+ "(prefers-reduced-motion: reduce)",
+ );
+
+ shouldReduceMotionRef.current = reduceMotionQuery.matches;
+ setPointerVars(0, 0);
+
+ const handleReducedMotionChange = () => {
+ shouldReduceMotionRef.current = reduceMotionQuery.matches;
+
+ if (reduceMotionQuery.matches) {
+ cancelPointerFrame();
+ setPointerVars(0, 0);
+ }
+ };
+
+ reduceMotionQuery.addEventListener("change", handleReducedMotionChange);
+
+ return () => {
+ reduceMotionQuery.removeEventListener(
+ "change",
+ handleReducedMotionChange,
+ );
+ cancelPointerFrame();
+ setPointerVars(0, 0);
+ };
+ }, []);
+
+ const handlePointerMove = (event: PointerEvent) => {
+ if (shouldReduceMotionRef.current) {
+ cancelPointerFrame();
+ setPointerVars(0, 0);
+ return;
+ }
+
+ const bounds = event.currentTarget.getBoundingClientRect();
+ const nextX = ((event.clientX - bounds.left) / bounds.width - 0.5) * 2;
+ const nextY = ((event.clientY - bounds.top) / bounds.height - 0.5) * 2;
+
+ schedulePointerVars(nextX, nextY);
+ };
+
+ const handlePointerLeave = () => {
+ schedulePointerVars(0, 0);
+ };
+
+ return {
+ motionLayerRef,
+ handlePointerMove,
+ handlePointerLeave,
+ };
+}
diff --git a/apps/khix/src/app/_components/sections/hero/FallingLeaves.tsx b/apps/khix/src/app/_components/sections/hero/FallingLeaves.tsx
new file mode 100644
index 000000000..82144b005
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/hero/FallingLeaves.tsx
@@ -0,0 +1,153 @@
+import type { CSSProperties } from "react";
+
+import styles from "./Hero.module.css";
+
+interface FallingLeaf {
+ delay: string;
+ drift: string;
+ duration: string;
+ fill: string;
+ opacity: number;
+ rotate: string;
+ size: string;
+ x: string;
+}
+
+interface LeafStyle extends CSSProperties {
+ "--leaf-delay": string;
+ "--leaf-drift": string;
+ "--leaf-duration": string;
+ "--leaf-fill": string;
+ "--leaf-opacity": number;
+ "--leaf-rotate": string;
+ "--leaf-size": string;
+ "--leaf-x": string;
+}
+
+const getLeafStyle = (leaf: FallingLeaf): LeafStyle => ({
+ "--leaf-delay": leaf.delay,
+ "--leaf-drift": leaf.drift,
+ "--leaf-duration": leaf.duration,
+ "--leaf-fill": leaf.fill,
+ "--leaf-opacity": leaf.opacity,
+ "--leaf-rotate": leaf.rotate,
+ "--leaf-size": leaf.size,
+ "--leaf-x": leaf.x,
+});
+
+const FALLING_LEAVES: FallingLeaf[] = [
+ {
+ x: "52%",
+ size: "22px",
+ duration: "9.6s",
+ delay: "-1.8s",
+ opacity: 0.5,
+ fill: "#b987ff",
+ drift: "-38px",
+ rotate: "280deg",
+ },
+ {
+ x: "61%",
+ size: "18px",
+ duration: "11.2s",
+ delay: "-6.4s",
+ opacity: 0.42,
+ fill: "#9b6dff",
+ drift: "51px",
+ rotate: "340deg",
+ },
+ {
+ x: "70%",
+ size: "25px",
+ duration: "10.4s",
+ delay: "-3.2s",
+ opacity: 0.46,
+ fill: "#d19aff",
+ drift: "-61px",
+ rotate: "390deg",
+ },
+ {
+ x: "79%",
+ size: "20px",
+ duration: "12.8s",
+ delay: "-8.5s",
+ opacity: 0.38,
+ fill: "#8752f0",
+ drift: "45px",
+ rotate: "320deg",
+ },
+ {
+ x: "86%",
+ size: "18px",
+ duration: "8.9s",
+ delay: "-4.9s",
+ opacity: 0.44,
+ fill: "#c08cff",
+ drift: "-29px",
+ rotate: "250deg",
+ },
+ {
+ x: "47%",
+ size: "19px",
+ duration: "10.8s",
+ delay: "-7.2s",
+ opacity: 0.4,
+ fill: "#a66cff",
+ drift: "35px",
+ rotate: "310deg",
+ },
+ {
+ x: "57%",
+ size: "26px",
+ duration: "13.4s",
+ delay: "-10.6s",
+ opacity: 0.34,
+ fill: "#cf95ff",
+ drift: "-48px",
+ rotate: "420deg",
+ },
+ {
+ x: "66%",
+ size: "18px",
+ duration: "9.2s",
+ delay: "-0.9s",
+ opacity: 0.46,
+ fill: "#8f5bf5",
+ drift: "59px",
+ rotate: "270deg",
+ },
+ {
+ x: "74%",
+ size: "23px",
+ duration: "12.1s",
+ delay: "-5.7s",
+ opacity: 0.37,
+ fill: "#b47aff",
+ drift: "-43px",
+ rotate: "365deg",
+ },
+ {
+ x: "91%",
+ size: "21px",
+ duration: "10s",
+ delay: "-2.6s",
+ opacity: 0.43,
+ fill: "#d3a2ff",
+ drift: "30px",
+ rotate: "295deg",
+ },
+];
+
+export function FallingLeaves() {
+ return (
+
+ {FALLING_LEAVES.map((leaf, index) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/khix/src/app/_components/sections/hero/Hero.module.css b/apps/khix/src/app/_components/sections/hero/Hero.module.css
new file mode 100644
index 000000000..f2272cb8d
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/hero/Hero.module.css
@@ -0,0 +1,949 @@
+@property --khix-scroll-progress {
+ syntax: "";
+ inherits: true;
+ initial-value: 0;
+}
+
+@property --khix-apply-glow-angle {
+ syntax: "";
+ inherits: true;
+ initial-value: 0deg;
+}
+
+@property --khix-apply-orbit-angle {
+ syntax: "";
+ inherits: false;
+ initial-value: 0deg;
+}
+
+.hero {
+ --khix-hero-height: 112svh;
+ --khix-hero-pointer-x: 0;
+ --khix-hero-pointer-y: 0;
+ --khix-hero-scroll-progress: 0;
+
+ position: relative;
+ z-index: 1;
+ min-height: 112vh;
+ min-height: var(--khix-hero-height);
+ overflow-x: hidden;
+ overflow-x: clip;
+ overflow-anchor: none;
+ background: #eef6cf;
+}
+
+.stage {
+ --khix-pointer-x: var(--khix-hero-pointer-x, 0);
+ --khix-pointer-y: var(--khix-hero-pointer-y, 0);
+ --khix-scroll-progress: var(--khix-hero-scroll-progress, 0);
+
+ position: relative;
+ width: 100%;
+ height: 112vh;
+ height: var(--khix-hero-height);
+ overflow: hidden;
+ overflow-anchor: none;
+ isolation: isolate;
+ background:
+ linear-gradient(
+ 180deg,
+ rgba(247, 253, 213, 0.96),
+ rgba(219, 240, 226, 0.9)
+ ),
+ #eef6cf;
+}
+
+.art {
+ --khix-art-motion-scale: 0.46;
+
+ position: absolute;
+ inset: -4svh -3vw;
+ z-index: 1;
+ pointer-events: auto;
+ overflow-anchor: none;
+}
+
+.desktopHeroLayers {
+ display: contents;
+}
+
+.mobileHeroLayers {
+ position: relative;
+ display: none;
+}
+
+.introVeil {
+ position: absolute;
+ inset: 0;
+ z-index: 60;
+ pointer-events: none;
+ background:
+ linear-gradient(
+ 180deg,
+ rgba(228, 238, 214, 0.58),
+ rgba(191, 215, 211, 0.34)
+ ),
+ rgba(42, 56, 70, 0.08);
+ backdrop-filter: blur(26px) saturate(1.04);
+ animation: intro-veil-release 1.45s ease-out forwards;
+}
+
+.layer {
+ --khix-layer-depth-x: 0;
+ --khix-layer-depth-y: 0;
+ --khix-layer-max-x: clamp(0.65rem, 1.65vw, 1.85rem);
+ --khix-layer-max-y: clamp(0.65rem, 1.8svh, 1.7rem);
+ --khix-layer-scale: 1;
+ --khix-layer-scroll-y: 0px;
+
+ position: absolute;
+ inset: clamp(-2.35rem, -2.4vw, -0.8rem);
+ display: block;
+ overflow: hidden;
+ overflow-anchor: none;
+ transform: translate3d(
+ clamp(
+ calc(var(--khix-layer-max-x) * -1),
+ calc(
+ var(--khix-pointer-x) * var(--khix-layer-depth-x) *
+ var(--khix-art-motion-scale) * 1px
+ ),
+ var(--khix-layer-max-x)
+ ),
+ clamp(
+ calc(var(--khix-layer-max-y) * -1),
+ calc(
+ (
+ var(--khix-pointer-y) * var(--khix-layer-depth-y) *
+ var(--khix-art-motion-scale) * 1px
+ ) +
+ (
+ var(--khix-scroll-progress) * var(--khix-layer-scroll-y) *
+ var(--khix-art-motion-scale)
+ )
+ ),
+ var(--khix-layer-max-y)
+ ),
+ 0
+ )
+ scale(var(--khix-layer-scale));
+ backface-visibility: hidden;
+ will-change: transform;
+}
+
+.layerImage {
+ object-fit: cover;
+ object-position: right center;
+ user-select: none;
+ pointer-events: none;
+}
+
+.distantLayer .layerImage {
+ opacity: 0.92;
+}
+
+.foregroundLayer {
+ inset: 0;
+ z-index: 12;
+ overflow: hidden;
+ pointer-events: none;
+ transform: none;
+ will-change: auto;
+}
+
+.foregroundImage {
+ position: absolute;
+ top: calc(var(--khix-hero-height) - 60vw);
+ left: 0;
+ width: 100%;
+ max-width: 100%;
+ height: auto;
+ transform-origin: 50% 100%;
+}
+
+.glowLayer {
+ --khix-hero-glow-opacity: 0.14;
+
+ opacity: var(--khix-hero-glow-opacity);
+ filter: drop-shadow(0 0 0.24rem rgb(108 246 255 / 0.22));
+ animation: desktopHeroGlowFlicker 6.6s steps(1, end) infinite;
+ will-change: opacity, filter;
+}
+
+.waterLayer {
+ overflow: hidden;
+}
+
+.pondLayer .layerImage {
+ transform-origin: 52% 78%;
+}
+
+.pondSourceImage {
+ opacity: 1;
+}
+
+.pondFrameSequence {
+ position: absolute;
+ inset: 0;
+ display: block;
+ pointer-events: none;
+ background-image: url("https://assets.knighthacks.org/khix/hero-pond-animated.webp");
+ background-repeat: no-repeat;
+ background-position: right center;
+ background-size: cover;
+ opacity: 0.5;
+ transform-origin: 52% 78%;
+}
+
+.waterfallLayer .layerImage {
+ z-index: 1;
+ transform-origin: 4% 56%;
+}
+
+.waterfallSourceImage {
+ opacity: 0;
+}
+
+.waterfallFrameSequence {
+ position: absolute;
+ inset: 0;
+ z-index: 1;
+ display: block;
+ pointer-events: none;
+ background-image: url("https://assets.knighthacks.org/khix/hero-waterfall-animated.webp");
+ background-repeat: no-repeat;
+ background-position: right center;
+ background-size: cover;
+ transform-origin: 4% 56%;
+}
+
+.leafField {
+ position: absolute;
+ inset: 0;
+ z-index: 40;
+ overflow: hidden;
+ pointer-events: none;
+}
+
+.leaf {
+ position: absolute;
+ top: -9svh;
+ left: var(--leaf-x);
+ width: var(--leaf-size);
+ aspect-ratio: 0.62;
+ border-radius: 100% 0 100% 0;
+ background:
+ linear-gradient(135deg, rgba(239, 217, 255, 0.44), transparent 38%),
+ var(--leaf-fill);
+ box-shadow: inset -1px -2px 3px rgba(58, 31, 93, 0.22);
+ opacity: 0;
+ transform: translate3d(0, -8svh, 0) rotate(-18deg);
+ animation: leaf-fall var(--leaf-duration) linear var(--leaf-delay) infinite;
+}
+
+.leaf::after {
+ position: absolute;
+ inset: 13% 46% 8% auto;
+ width: 1px;
+ content: "";
+ background: rgba(64, 38, 102, 0.3);
+ transform: rotate(10deg);
+ transform-origin: bottom;
+}
+
+.shade {
+ position: absolute;
+ inset: 0;
+ z-index: 10;
+ pointer-events: none;
+ background:
+ linear-gradient(
+ 180deg,
+ rgba(12, 20, 35, 0.15),
+ rgba(35, 50, 58, 0.04) 46%,
+ rgba(5, 18, 24, 0.1)
+ ),
+ radial-gradient(
+ ellipse at 60% 18%,
+ rgba(208, 224, 236, 0.1),
+ transparent 42%
+ );
+ box-shadow:
+ inset 0 -24svh 34svh rgba(7, 28, 23, 0.2),
+ inset 0 18svh 30svh rgba(247, 253, 213, 0.16);
+}
+
+.titleLockup {
+ --khix-title-aura-opacity: 0.42;
+ --khix-title-glow-opacity: 0.62;
+
+ position: absolute;
+ top: 29%;
+ left: 10%;
+ z-index: 30;
+ display: flex;
+ width: clamp(28rem, 39vw, 47rem);
+ flex-direction: column;
+ align-items: flex-start;
+ gap: clamp(0.9rem, 1.8svh, 1.28rem);
+ pointer-events: auto;
+ opacity: 1;
+ transform: translate3d(0, 0, 0) scale(1);
+ transform-origin: 50% 55%;
+}
+
+.titleLockup::before {
+ position: absolute;
+ inset: -18% -8% -10%;
+ z-index: 0;
+ pointer-events: none;
+ content: "";
+ background:
+ radial-gradient(
+ ellipse at 42% 52%,
+ rgb(203 255 224 / calc(var(--khix-title-aura-opacity) * 0.38)),
+ transparent 48%
+ ),
+ radial-gradient(
+ ellipse at 68% 48%,
+ rgb(133 234 255 / calc(var(--khix-title-aura-opacity) * 0.2)),
+ transparent 54%
+ );
+ filter: blur(18px);
+ opacity: var(--khix-title-aura-opacity);
+}
+
+.titleLogoWrap {
+ position: relative;
+ z-index: 1;
+ width: 100%;
+}
+
+.titleLogo {
+ position: relative;
+ z-index: 1;
+ display: block;
+ width: 100%;
+ height: auto;
+ user-select: none;
+ filter: brightness(1.06)
+ drop-shadow(0 0 0.1rem rgb(255 255 255 / var(--khix-title-glow-opacity)))
+ drop-shadow(
+ 0 0 0.42rem rgb(235 249 255 / calc(var(--khix-title-glow-opacity) * 0.58))
+ )
+ drop-shadow(
+ 0 0 0.9rem rgb(103 228 255 / calc(var(--khix-title-glow-opacity) * 0.3))
+ )
+ drop-shadow(
+ 0 0 1.65rem rgb(103 228 255 / calc(var(--khix-title-glow-opacity) * 0.13))
+ );
+}
+
+.eventDetails {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ align-self: center;
+ flex-direction: column;
+ align-items: center;
+ gap: clamp(0.14rem, 0.35svh, 0.24rem);
+ margin: 0;
+ color: rgb(255 255 255 / 0.96);
+ font-family: var(--font-khix), Georgia, "Times New Roman", serif;
+ font-size: clamp(1rem, 1.15vw, 1.35rem);
+ font-weight: 650;
+ line-height: 1.3;
+ text-align: center;
+ text-shadow:
+ 0 1px 0.2rem rgb(5 24 31 / 0.84),
+ 0 0 0.08rem rgb(255 255 255 / 0.9),
+ 0 0 0.42rem rgb(235 249 255 / 0.62),
+ 0 0 0.9rem rgb(103 228 255 / 0.28);
+}
+
+.heroApplyButton {
+ --khix-apply-mote-opacity: 0.82;
+ --khix-apply-mote-core: rgb(255 255 255 / 0.9);
+ --khix-apply-mote-mid: rgb(210 255 226 / 0.62);
+ --khix-apply-mote-far: rgb(135 238 255 / 0.42);
+
+ position: relative;
+ z-index: 2;
+ isolation: isolate;
+ display: inline-flex;
+ align-self: center;
+ min-height: clamp(2.15rem, 3.9svh, 2.55rem);
+ align-items: center;
+ justify-content: center;
+ overflow: visible;
+ border: 1px solid rgb(255 255 255 / 0.42);
+ border-radius: 0.5rem;
+ padding: 0 clamp(2.8rem, 6vw, 4.3rem);
+ color: rgb(255 255 255 / 0.96);
+ font-family: var(--font-khix), Georgia, "Times New Roman", serif;
+ font-size: clamp(0.84rem, 1.05vw, 0.96rem);
+ font-weight: 650;
+ letter-spacing: 0;
+ text-decoration: none;
+ text-shadow: 0 0 0.42rem rgb(255 255 255 / 0.36);
+ background:
+ radial-gradient(circle at 20% 0%, rgb(209 255 232 / 0.1), transparent 42%),
+ rgb(255 255 255 / 0.035);
+ box-shadow:
+ inset 0 0 0.45rem rgb(255 255 255 / 0.055),
+ 0 0.45rem 1rem rgb(9 30 30 / 0.12),
+ 0 0 0.65rem rgb(169 247 255 / 0.07);
+ backdrop-filter: blur(2px) saturate(1.03);
+ transition:
+ color 180ms ease,
+ border-color 180ms ease,
+ background 180ms ease,
+ box-shadow 220ms ease,
+ transform 180ms ease;
+}
+
+.titleApplyButton {
+ --khix-apply-pointer-x: 50%;
+ --khix-apply-pointer-y: 50%;
+ --khix-apply-glow-angle: 0deg;
+
+ width: clamp(15rem, 18vw, 20rem);
+ min-height: clamp(3.25rem, 6.4svh, 4rem);
+ padding-inline: clamp(2rem, 4vw, 3rem);
+ font-size: clamp(1.05rem, 1.25vw, 1.2rem);
+}
+
+.titleApplyButton[data-cursor-active="true"] {
+ background:
+ radial-gradient(
+ circle at var(--khix-apply-pointer-x) var(--khix-apply-pointer-y),
+ rgb(255 255 255 / 0.24),
+ rgb(218 250 255 / 0.13) 22%,
+ transparent 52%
+ ),
+ radial-gradient(circle at 20% 0%, rgb(209 255 232 / 0.1), transparent 42%),
+ rgb(255 255 255 / 0.035);
+ box-shadow:
+ inset 0 0 0.45rem rgb(255 255 255 / 0.08),
+ 0 0.45rem 1rem rgb(9 30 30 / 0.12),
+ 0 0 1rem rgb(255 255 255 / 0.14),
+ 0 0 1.6rem rgb(169 247 255 / 0.08);
+}
+
+.titleApplyButton::before {
+ --khix-apply-orbit-angle: 0deg;
+
+ position: absolute;
+ inset: -0.28rem;
+ z-index: 0;
+ padding: 0.16rem;
+ pointer-events: none;
+ content: "";
+ border-radius: 0.78rem;
+ background: conic-gradient(
+ from calc(var(--khix-apply-glow-angle) + var(--khix-apply-orbit-angle)),
+ transparent 0deg 210deg,
+ rgb(255 255 255 / 0.12) 232deg,
+ rgb(255 255 255 / 0.98) 270deg,
+ rgb(225 250 255 / 0.34) 306deg,
+ transparent 330deg 360deg
+ );
+ -webkit-mask:
+ linear-gradient(#fff 0 0) content-box,
+ linear-gradient(#fff 0 0);
+ mask:
+ linear-gradient(#fff 0 0) content-box,
+ linear-gradient(#fff 0 0);
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+ filter: drop-shadow(0 0 0.25rem rgb(255 255 255 / 0.78))
+ drop-shadow(0 0 0.72rem rgb(210 248 255 / 0.42));
+ opacity: 0;
+ transition: opacity 180ms ease;
+}
+
+.titleApplyButton[data-cursor-active="true"]::before,
+.titleApplyButton:focus-visible::before {
+ opacity: 0.94;
+}
+
+.titleApplyButton[data-cursor-idle="true"]::before,
+.titleApplyButton:focus-visible::before {
+ animation: khixHeroApplyGlowOrbit 2.1s linear infinite;
+}
+
+.mobileHeroApplyButton {
+ display: none;
+}
+
+.heroApplyButton::after {
+ position: absolute;
+ inset: -0.7rem;
+ z-index: 0;
+ pointer-events: none;
+ content: "";
+ border-radius: calc(0.5rem + 0.7rem);
+ background:
+ radial-gradient(
+ circle at 12% 50%,
+ var(--khix-apply-mote-core) 0 0.045rem,
+ var(--khix-apply-mote-mid) 0.075rem 0.17rem,
+ transparent 0.36rem
+ ),
+ radial-gradient(
+ circle at 28% 10%,
+ var(--khix-apply-mote-core) 0 0.04rem,
+ var(--khix-apply-mote-far) 0.07rem 0.16rem,
+ transparent 0.34rem
+ ),
+ radial-gradient(
+ circle at 74% 8%,
+ var(--khix-apply-mote-core) 0 0.04rem,
+ var(--khix-apply-mote-mid) 0.07rem 0.16rem,
+ transparent 0.34rem
+ ),
+ radial-gradient(
+ circle at 91% 48%,
+ var(--khix-apply-mote-core) 0 0.045rem,
+ var(--khix-apply-mote-far) 0.08rem 0.18rem,
+ transparent 0.38rem
+ ),
+ radial-gradient(
+ circle at 68% 93%,
+ var(--khix-apply-mote-core) 0 0.04rem,
+ var(--khix-apply-mote-mid) 0.07rem 0.16rem,
+ transparent 0.34rem
+ ),
+ radial-gradient(
+ circle at 22% 90%,
+ var(--khix-apply-mote-core) 0 0.042rem,
+ var(--khix-apply-mote-far) 0.075rem 0.17rem,
+ transparent 0.36rem
+ );
+ background-repeat: no-repeat;
+ mix-blend-mode: screen;
+ opacity: 0;
+ transform: scale(0.96);
+ transition:
+ opacity 180ms ease,
+ transform 440ms ease;
+}
+
+.heroApplyButton > span {
+ position: relative;
+ z-index: 1;
+}
+
+.heroApplyButton:hover,
+.heroApplyButton:focus-visible {
+ color: white;
+ border-color: rgb(255 255 255 / 0.58);
+ transform: translate3d(0, -1px, 0);
+}
+
+.heroApplyButton:hover::after,
+.heroApplyButton:focus-visible::after {
+ opacity: var(--khix-apply-mote-opacity);
+ transform: scale(1);
+ animation: khixHeroApplyButtonMotes 1.9s ease-in-out infinite alternate;
+}
+
+.heroApplyButton:focus-visible {
+ outline: 2px solid rgb(255 255 255 / 0.7);
+ outline-offset: 0.22rem;
+}
+
+@media (max-width: 1440px) and (min-width: 701px) {
+ .art {
+ --khix-art-motion-scale: 0.38;
+ inset: -3.4svh -4vw;
+ }
+
+ .titleLockup {
+ top: clamp(13.5rem, 30svh, 18rem);
+ left: clamp(2rem, 8vw, 5rem);
+ width: clamp(22rem, 45vw, 31rem);
+ }
+
+ .titleApplyButton {
+ width: clamp(13rem, 22vw, 17rem);
+ min-height: clamp(3rem, 6svh, 3.75rem);
+ padding-inline: clamp(1.75rem, 4vw, 2.5rem);
+ font-size: clamp(1rem, 1.4vw, 1.1rem);
+ }
+}
+
+@media (max-width: 760px) {
+ .foregroundLayer {
+ display: none;
+ }
+}
+
+@media (max-width: 700px) {
+ .hero {
+ --khix-hero-height: 108svh;
+ z-index: auto;
+ min-height: 108vh;
+ min-height: var(--khix-hero-height);
+ }
+
+ .art {
+ --khix-art-motion-scale: 0.42;
+ inset: -1.4svh -2vw;
+ }
+
+ .leaf {
+ width: clamp(0.625rem, 3vw, var(--leaf-size));
+ }
+
+ .desktopHeroLayers {
+ display: none;
+ }
+
+ .mobileHeroLayers {
+ position: absolute;
+ top: 0;
+ right: 0;
+ z-index: 1;
+ display: block;
+ width: max(100vw, min(130vw, calc(var(--khix-hero-height) * 9 / 16)));
+ aspect-ratio: 9 / 16;
+ overflow: hidden;
+ pointer-events: none;
+ }
+
+ .mobileHeroLayer {
+ --khix-mobile-layer-depth-x: 0;
+ --khix-mobile-layer-depth-y: 0;
+ --khix-mobile-layer-scroll-y: 0px;
+ --khix-mobile-layer-max-x: clamp(0.35rem, 2vw, 0.75rem);
+ --khix-mobile-layer-max-y: clamp(0.35rem, 1.6svh, 0.85rem);
+ --khix-mobile-layer-overscan: 1.035;
+
+ object-fit: fill;
+ transform: translate3d(
+ clamp(
+ calc(var(--khix-mobile-layer-max-x) * -1),
+ calc(
+ var(--khix-pointer-x) * var(--khix-mobile-layer-depth-x) *
+ var(--khix-art-motion-scale) * 1px
+ ),
+ var(--khix-mobile-layer-max-x)
+ ),
+ clamp(
+ calc(var(--khix-mobile-layer-max-y) * -1),
+ calc(
+ (
+ var(--khix-pointer-y) * var(--khix-mobile-layer-depth-y) *
+ var(--khix-art-motion-scale) * 1px
+ ) +
+ (
+ var(--khix-scroll-progress) *
+ var(--khix-mobile-layer-scroll-y) *
+ var(--khix-art-motion-scale)
+ )
+ ),
+ var(--khix-mobile-layer-max-y)
+ ),
+ 0
+ )
+ scale(var(--khix-mobile-layer-overscan));
+ transform-origin: 50% 0;
+ user-select: none;
+ pointer-events: none;
+ will-change: transform;
+ }
+
+ .mobileHeroLayerSeven {
+ --khix-mobile-layer-overscan: 1.02;
+ z-index: 1;
+ }
+
+ .mobileHeroLayerSix {
+ --khix-mobile-layer-overscan: 1.025;
+ z-index: 2;
+ }
+
+ .mobileHeroLayerFive {
+ --khix-mobile-layer-overscan: 1.03;
+ z-index: 3;
+ }
+
+ .mobileHeroLayerFour {
+ --khix-mobile-layer-overscan: 1.035;
+ z-index: 4;
+ }
+
+ .mobileHeroPondAnimationLayer {
+ --khix-mobile-layer-overscan: 1.035;
+ z-index: 4;
+ opacity: 0.68;
+ }
+
+ .mobileHeroLayerThree {
+ --khix-mobile-layer-overscan: 1.04;
+ z-index: 5;
+ }
+
+ .mobileHeroLayerTwo {
+ --khix-mobile-layer-overscan: 1.04;
+ z-index: 6;
+ opacity: 0.82;
+ mix-blend-mode: normal;
+ filter: drop-shadow(0 0 0.35rem rgb(92 252 255 / 0.34));
+ animation: mobileHeroGlowFlicker 5.8s steps(1, end) infinite;
+ will-change: transform, opacity, filter;
+ }
+
+ .layerImage {
+ object-position: 94% 50%;
+ }
+
+ .foregroundImage {
+ top: calc(var(--khix-hero-height) - 66vw - 15svh);
+ left: 0;
+ width: 100%;
+ max-width: 100%;
+ transform: none;
+ }
+
+ .pondFrameSequence,
+ .waterfallFrameSequence {
+ background-position: 94% 50%;
+ }
+
+ .titleLockup {
+ --khix-title-aura-opacity: 0.34;
+ --khix-title-glow-opacity: 0.7;
+ --khix-mobile-title-top: clamp(8.5rem, 16svh, 10rem);
+
+ top: var(--khix-mobile-title-top);
+ left: 50%;
+ display: flex;
+ width: min(78vw, 20rem);
+ flex-direction: column;
+ align-items: center;
+ gap: clamp(0.58rem, 1.5svh, 0.78rem);
+ transform: translate3d(-50%, 0, 0) scale(1);
+ transform-origin: 50% 45%;
+ }
+
+ .titleLockup::before {
+ inset: -32% -24% -48% -18%;
+ filter: blur(16px);
+ }
+
+ .eventDetails {
+ text-shadow:
+ 0 0 0.04rem rgb(255 255 255 / 0.72),
+ 0 0 0.18rem rgb(235 249 255 / 0.38),
+ 0 0 0.42rem rgb(139 232 255 / 0.2);
+ font-size: clamp(1rem, 4.1vw, 1.15rem);
+ }
+
+ .titleApplyButton {
+ display: none;
+ }
+
+ .mobileHeroApplyButton {
+ position: absolute;
+ top: 90svh;
+ right: 0;
+ left: 0;
+ z-index: 30;
+ display: inline-flex;
+ width: min(68vw, 18rem);
+ min-height: clamp(3rem, 6svh, 3.35rem);
+ margin-inline: auto;
+ padding-inline: clamp(1.5rem, 6vw, 2.25rem);
+ font-size: clamp(1rem, 4.2vw, 1.1rem);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .hero {
+ min-height: var(--khix-hero-height);
+ }
+
+ .stage {
+ position: relative;
+ }
+
+ .introVeil {
+ display: none;
+ }
+
+ .layer {
+ transform: none !important;
+ }
+
+ .glowLayer {
+ opacity: var(--khix-hero-glow-opacity);
+ filter: none;
+ animation: none;
+ will-change: auto;
+ }
+
+ .mobileHeroLayer {
+ transform: none !important;
+ will-change: auto;
+ }
+
+ .mobileHeroLayerTwo {
+ opacity: 0.86;
+ filter: none;
+ animation: none;
+ }
+
+ .mobileHeroPondAnimationLayer {
+ display: none;
+ }
+
+ .heroApplyButton,
+ .heroApplyButton::after,
+ .titleApplyButton::before {
+ transition: none;
+ }
+
+ .heroApplyButton::after {
+ animation: none;
+ }
+
+ .titleApplyButton[data-cursor-idle="true"]::before,
+ .titleApplyButton:focus-visible::before {
+ animation: none;
+ }
+
+ .leaf {
+ display: none;
+ }
+
+ .pondFrameSequence,
+ .waterfallFrameSequence {
+ display: none;
+ }
+
+ .pondSourceImage,
+ .waterfallSourceImage {
+ opacity: 1;
+ }
+}
+
+@keyframes khixHeroApplyButtonMotes {
+ from {
+ background-position:
+ 0 0,
+ 0 0,
+ 0 0,
+ 0 0,
+ 0 0,
+ 0 0;
+ }
+
+ to {
+ background-position:
+ 0.18rem -0.1rem,
+ -0.14rem 0.16rem,
+ 0.12rem 0.14rem,
+ -0.18rem -0.08rem,
+ 0.16rem 0.06rem,
+ -0.1rem -0.12rem;
+ }
+}
+
+@keyframes khixHeroApplyGlowOrbit {
+ to {
+ --khix-apply-orbit-angle: 360deg;
+ }
+}
+
+@keyframes desktopHeroGlowFlicker {
+ 0%,
+ 8%,
+ 27%,
+ 48%,
+ 71%,
+ 100% {
+ opacity: var(--khix-hero-glow-opacity);
+ filter: drop-shadow(0 0 0.24rem rgb(108 246 255 / 0.22));
+ }
+
+ 9%,
+ 29%,
+ 50% {
+ opacity: calc(var(--khix-hero-glow-opacity) * 0.52);
+ filter: drop-shadow(0 0 0.12rem rgb(108 246 255 / 0.12));
+ }
+
+ 12%,
+ 32%,
+ 54%,
+ 75% {
+ opacity: calc(var(--khix-hero-glow-opacity) * 1.78);
+ filter: drop-shadow(0 0 0.4rem rgb(130 255 255 / 0.38));
+ }
+}
+
+@keyframes mobileHeroGlowFlicker {
+ 0%,
+ 11%,
+ 23%,
+ 47%,
+ 62%,
+ 88%,
+ 100% {
+ opacity: 0.82;
+ filter: drop-shadow(0 0 0.35rem rgb(92 252 255 / 0.34));
+ }
+
+ 12%,
+ 24%,
+ 49% {
+ opacity: 0.58;
+ filter: drop-shadow(0 0 0.16rem rgb(92 252 255 / 0.18));
+ }
+
+ 15%,
+ 29%,
+ 52%,
+ 67% {
+ opacity: 0.94;
+ filter: drop-shadow(0 0 0.55rem rgb(115 255 255 / 0.5));
+ }
+}
+
+@keyframes intro-veil-release {
+ 0% {
+ opacity: 1;
+ backdrop-filter: blur(26px) saturate(1.04);
+ }
+
+ 100% {
+ opacity: 0;
+ backdrop-filter: blur(0) saturate(1);
+ visibility: hidden;
+ }
+}
+
+@keyframes leaf-fall {
+ 0% {
+ opacity: 0;
+ transform: translate3d(0, -8svh, 0) rotate(-18deg) scale(0.86);
+ }
+
+ 12% {
+ opacity: var(--leaf-opacity);
+ }
+
+ 58% {
+ opacity: var(--leaf-opacity);
+ transform: translate3d(var(--leaf-drift), 54svh, 0)
+ rotate(var(--leaf-rotate)) scale(1);
+ }
+
+ 100% {
+ opacity: 0;
+ transform: translate3d(var(--leaf-drift), 106svh, 0)
+ rotate(var(--leaf-rotate)) scale(0.9);
+ }
+}
diff --git a/apps/khix/src/app/_components/sections/hero/Hero.tsx b/apps/khix/src/app/_components/sections/hero/Hero.tsx
new file mode 100644
index 000000000..b18aef3d1
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/hero/Hero.tsx
@@ -0,0 +1,152 @@
+"use client";
+
+import type { CSSProperties } from "react";
+import { useEffect, useState } from "react";
+import Image from "next/image";
+
+import { FallingLeaves } from "./FallingLeaves";
+import styles from "./Hero.module.css";
+import { HeroLayerImage } from "./HeroLayerImage";
+import { HeroApplyButton, HeroTitle } from "./HeroTitle";
+import { HERO_LAYERS } from "./layers";
+import { useHeroMotion } from "./useHeroMotion";
+
+export default function Hero() {
+ const { sectionRef, stageRef, handlePointerMove, handlePointerLeave } =
+ useHeroMotion();
+ const [viewport, setViewport] = useState<"desktop" | "mobile" | null>(null);
+
+ useEffect(() => {
+ const mediaQuery = window.matchMedia("(max-width: 700px)");
+ const updateViewport = () => {
+ setViewport(mediaQuery.matches ? "mobile" : "desktop");
+ };
+
+ updateViewport();
+ mediaQuery.addEventListener("change", updateViewport);
+
+ return () => mediaQuery.removeEventListener("change", updateViewport);
+ }, []);
+
+ return (
+
+
+
+ Knight Hacks IX
+
+
+ {viewport === "desktop" ? (
+
+ {HERO_LAYERS.map((layer, index) => (
+
+ ))}
+
+ ) : null}
+ {viewport === "mobile" ?
: null}
+
+
+
+
+
+
+
+
+ );
+}
+
+const MOBILE_HERO_LAYERS = [
+ {
+ src: "https://assets.knighthacks.org/khix/hero-mobile-7bg.webp",
+ className: styles.mobileHeroLayerSeven,
+ depthX: -6,
+ depthY: -2,
+ scrollY: -10,
+ },
+ {
+ src: "https://assets.knighthacks.org/khix/hero-mobile-6.webp",
+ className: styles.mobileHeroLayerSix,
+ depthX: -5,
+ depthY: -1.6,
+ scrollY: -8,
+ },
+ {
+ src: "https://assets.knighthacks.org/khix/hero-mobile-5.webp",
+ className: styles.mobileHeroLayerFive,
+ depthX: -3,
+ depthY: -1,
+ scrollY: -5,
+ },
+ {
+ src: "https://assets.knighthacks.org/khix/hero-mobile-4.webp",
+ className: styles.mobileHeroLayerFour,
+ depthX: 3,
+ depthY: 1,
+ scrollY: 5,
+ },
+ {
+ src: "https://assets.knighthacks.org/khix/hero-mobile-pond-animated.webp",
+ className: styles.mobileHeroPondAnimationLayer,
+ depthX: 3,
+ depthY: 1,
+ scrollY: 5,
+ },
+ {
+ src: "https://assets.knighthacks.org/khix/hero-mobile-3.webp",
+ className: styles.mobileHeroLayerThree,
+ depthX: 5,
+ depthY: 1.8,
+ scrollY: 8,
+ },
+ {
+ src: "https://assets.knighthacks.org/khix/hero-mobile-2.webp",
+ className: styles.mobileHeroLayerTwo,
+ depthX: 5,
+ depthY: 1.8,
+ scrollY: 8,
+ },
+];
+
+function MobileHeroLayers() {
+ return (
+
+ {MOBILE_HERO_LAYERS.map((layer, index) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/khix/src/app/_components/sections/hero/HeroLayerImage.tsx b/apps/khix/src/app/_components/sections/hero/HeroLayerImage.tsx
new file mode 100644
index 000000000..26546c12d
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/hero/HeroLayerImage.tsx
@@ -0,0 +1,116 @@
+import type { CSSProperties } from "react";
+import Image from "next/image";
+
+import type { HeroLayer } from "./layers";
+import styles from "./Hero.module.css";
+import { HERO_ASSET_BASE_PATH } from "./layers";
+
+interface HeroLayerImageProps {
+ layer: HeroLayer;
+ index: number;
+ zIndex?: number;
+}
+
+export function HeroLayerImage({ layer, index, zIndex }: HeroLayerImageProps) {
+ const isGlow = layer.motionRole === "glow";
+ const isPond = layer.motionRole === "pond";
+ const isTk = layer.motionRole === "tk";
+ const isWaterfall = layer.motionRole === "waterfall";
+ const isForeground = layer.motionRole === "foreground";
+ const isTreeWaterfall = layer.filename.includes("3_tree_and_waterfall");
+ const isDistant = index < 4;
+ const hasAmbientImageMotion = !(
+ isForeground ||
+ isGlow ||
+ isPond ||
+ isTk ||
+ isWaterfall
+ );
+ const imageClassName = [
+ styles.layerImage,
+ isForeground ? styles.foregroundImage : "",
+ isPond ? styles.pondSourceImage : "",
+ isWaterfall ? styles.waterfallSourceImage : "",
+ ]
+ .filter(Boolean)
+ .join(" ");
+ const layerClassName = [
+ styles.layer,
+ isForeground ? styles.foregroundLayer : "",
+ isDistant ? styles.distantLayer : "",
+ isGlow ? styles.glowLayer : "",
+ isPond || isWaterfall ? styles.waterLayer : "",
+ isPond ? styles.pondLayer : "",
+ isTreeWaterfall ? styles.treeWaterfallLayer : "",
+ isTk ? styles.tkLayer : "",
+ isWaterfall ? styles.waterfallLayer : "",
+ ]
+ .filter(Boolean)
+ .join(" ");
+
+ const layerStyle = {
+ "--khix-layer-depth-x": layer.depthX,
+ "--khix-layer-depth-y": layer.depthY,
+ "--khix-layer-scale": layer.scale,
+ "--khix-layer-scroll-y": `${layer.scrollY}px`,
+ zIndex: zIndex ?? index + 1,
+ } as CSSProperties;
+ const imageDataAttributes = {
+ "data-hero-layer-image": !isWaterfall ? true : undefined,
+ "data-hero-ambient-image": hasAmbientImageMotion ? true : undefined,
+ };
+ const imageSrc = layer.filename.startsWith("/")
+ ? layer.filename
+ : `${HERO_ASSET_BASE_PATH}/${layer.filename}`;
+
+ return (
+
+ {isForeground ? (
+
+ ) : (
+
+ )}
+ {isPond ? (
+
+ ) : null}
+ {isWaterfall ? (
+
+ ) : null}
+
+ );
+}
diff --git a/apps/khix/src/app/_components/sections/hero/HeroTitle.tsx b/apps/khix/src/app/_components/sections/hero/HeroTitle.tsx
new file mode 100644
index 000000000..a33e194f3
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/hero/HeroTitle.tsx
@@ -0,0 +1,119 @@
+"use client";
+
+import type { PointerEvent as ReactPointerEvent } from "react";
+import { useEffect, useRef } from "react";
+import Image from "next/image";
+import Link from "next/link";
+
+import styles from "./Hero.module.css";
+
+interface HeroApplyButtonProps {
+ className?: string;
+ cursorReactive?: boolean;
+}
+
+export function HeroApplyButton({
+ className,
+ cursorReactive = false,
+}: HeroApplyButtonProps) {
+ const cursorIdleTimerRef = useRef(null);
+
+ useEffect(
+ () => () => {
+ if (cursorIdleTimerRef.current !== null) {
+ window.clearTimeout(cursorIdleTimerRef.current);
+ }
+ },
+ [],
+ );
+
+ const clearCursorIdleTimer = () => {
+ if (cursorIdleTimerRef.current === null) {
+ return;
+ }
+
+ window.clearTimeout(cursorIdleTimerRef.current);
+ cursorIdleTimerRef.current = null;
+ };
+
+ const updateCursorGlow = (event: ReactPointerEvent) => {
+ if (!cursorReactive || event.pointerType !== "mouse") {
+ return;
+ }
+
+ const button = event.currentTarget;
+ const bounds = button.getBoundingClientRect();
+ const offsetX = event.clientX - bounds.left;
+ const offsetY = event.clientY - bounds.top;
+ const pointerX = Math.min(100, Math.max(0, (offsetX / bounds.width) * 100));
+ const pointerY = Math.min(
+ 100,
+ Math.max(0, (offsetY / bounds.height) * 100),
+ );
+ const deltaX = offsetX - bounds.width / 2;
+ const deltaY = offsetY - bounds.height / 2;
+ const glowAngle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI + 180;
+
+ button.style.setProperty("--khix-apply-pointer-x", `${pointerX}%`);
+ button.style.setProperty("--khix-apply-pointer-y", `${pointerY}%`);
+ button.style.setProperty("--khix-apply-glow-angle", `${glowAngle}deg`);
+ button.dataset.cursorActive = "true";
+ button.dataset.cursorIdle = "false";
+
+ clearCursorIdleTimer();
+ cursorIdleTimerRef.current = window.setTimeout(() => {
+ if (button.dataset.cursorActive === "true") {
+ button.dataset.cursorIdle = "true";
+ }
+ }, 550);
+ };
+
+ const handlePointerLeave = (event: ReactPointerEvent) => {
+ if (!cursorReactive || event.pointerType !== "mouse") {
+ return;
+ }
+
+ clearCursorIdleTimer();
+ event.currentTarget.dataset.cursorActive = "false";
+ event.currentTarget.dataset.cursorIdle = "false";
+ };
+
+ return (
+
+ Apply
+
+ );
+}
+
+export function HeroTitle() {
+ return (
+
+
+
+
+
+ October 9–11, 2026
+ University of Central Florida
+
+
+
+ );
+}
diff --git a/apps/khix/src/app/_components/sections/hero/index.ts b/apps/khix/src/app/_components/sections/hero/index.ts
new file mode 100644
index 000000000..eada8b792
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/hero/index.ts
@@ -0,0 +1 @@
+export { default } from "./Hero";
diff --git a/apps/khix/src/app/_components/sections/hero/layers.ts b/apps/khix/src/app/_components/sections/hero/layers.ts
new file mode 100644
index 000000000..f2d31fc11
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/hero/layers.ts
@@ -0,0 +1,82 @@
+export const HERO_ASSET_BASE_PATH = "https://assets.knighthacks.org/khix";
+
+export interface HeroLayer {
+ filename: string;
+ width?: number;
+ height?: number;
+ depthX: number;
+ depthY: number;
+ scrollY: number;
+ scale: number;
+ motionRole?: "foreground" | "glow" | "pond" | "tk" | "waterfall";
+}
+
+export const HERO_LAYERS: HeroLayer[] = [
+ {
+ filename: "hero-7_bg.webp",
+ depthX: -10,
+ depthY: -4,
+ scrollY: -16,
+ scale: 1.002,
+ },
+ {
+ filename: "hero-6_bg.webp",
+ depthX: -8,
+ depthY: -3.2,
+ scrollY: -14,
+ scale: 1.002,
+ },
+ {
+ filename: "hero-5_bg.webp",
+ depthX: -5.5,
+ depthY: -2.4,
+ scrollY: -10,
+ scale: 1.002,
+ },
+ {
+ filename: "hero-4_bg.webp",
+ depthX: 5,
+ depthY: 2.4,
+ scrollY: -6,
+ scale: 1.001,
+ },
+ {
+ filename: "hero-pond.webp",
+ depthX: 14,
+ depthY: 7,
+ scrollY: 12,
+ scale: 1.001,
+ motionRole: "pond",
+ },
+ {
+ filename: "hero-3_tree_and_waterfall.webp",
+ depthX: 18,
+ depthY: 9,
+ scrollY: 16,
+ scale: 1.001,
+ },
+ {
+ filename: "hero-waterfall.webp",
+ depthX: 18,
+ depthY: 9,
+ scrollY: 16,
+ scale: 1.001,
+ motionRole: "waterfall",
+ },
+ {
+ filename: "hero-2_tk.webp",
+ depthX: 24,
+ depthY: 12,
+ scrollY: 20,
+ scale: 1.001,
+ motionRole: "tk",
+ },
+ {
+ filename: "hero-tk_glow_alone.webp",
+ depthX: 24,
+ depthY: 12,
+ scrollY: 20,
+ scale: 1.001,
+ motionRole: "glow",
+ },
+];
diff --git a/apps/khix/src/app/_components/sections/hero/useHeroMotion.ts b/apps/khix/src/app/_components/sections/hero/useHeroMotion.ts
new file mode 100644
index 000000000..692104269
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/hero/useHeroMotion.ts
@@ -0,0 +1,207 @@
+import type { PointerEvent, RefObject } from "react";
+import { useEffect, useRef } from "react";
+
+interface HeroMotion {
+ sectionRef: RefObject;
+ stageRef: RefObject;
+ handlePointerMove: (event: PointerEvent) => void;
+ handlePointerLeave: () => void;
+}
+
+const clamp = (value: number, min = 0, max = 1) =>
+ Math.min(max, Math.max(min, value));
+
+const getViewportHeight = () =>
+ window.visualViewport?.height ?? window.innerHeight;
+
+const setElementMotionNumber = (
+ element: HTMLElement | null,
+ property: string,
+ value: number,
+) => {
+ element?.style.setProperty(property, value.toFixed(4));
+};
+
+export function useHeroMotion(): HeroMotion {
+ const sectionRef = useRef(null);
+ const stageRef = useRef(null);
+ const pointerFrameRef = useRef(0);
+ const pendingPointerRef = useRef({ x: 0, y: 0 });
+ const shouldReduceMotionRef = useRef(false);
+ const motionValuesRef = useRef({
+ pointerX: 0,
+ pointerY: 0,
+ scrollProgress: 0,
+ });
+
+ const setPointerVars = (x: number, y: number) => {
+ const stage = stageRef.current;
+ const section = sectionRef.current;
+ const motionValues = motionValuesRef.current;
+
+ if (
+ Math.abs(motionValues.pointerX - x) < 0.001 &&
+ Math.abs(motionValues.pointerY - y) < 0.001
+ ) {
+ return;
+ }
+
+ motionValues.pointerX = x;
+ motionValues.pointerY = y;
+ setElementMotionNumber(stage, "--khix-hero-pointer-x", x);
+ setElementMotionNumber(stage, "--khix-hero-pointer-y", y);
+ setElementMotionNumber(section, "--khix-hero-pointer-x", x);
+ setElementMotionNumber(section, "--khix-hero-pointer-y", y);
+ };
+
+ const cancelPointerFrame = () => {
+ if (!pointerFrameRef.current) {
+ return;
+ }
+
+ window.cancelAnimationFrame(pointerFrameRef.current);
+ pointerFrameRef.current = 0;
+ };
+
+ const schedulePointerVars = (x: number, y: number) => {
+ pendingPointerRef.current = { x, y };
+
+ if (pointerFrameRef.current) {
+ return;
+ }
+
+ pointerFrameRef.current = window.requestAnimationFrame(() => {
+ pointerFrameRef.current = 0;
+ setPointerVars(pendingPointerRef.current.x, pendingPointerRef.current.y);
+ });
+ };
+
+ useEffect(() => {
+ const reduceMotionQuery = window.matchMedia(
+ "(prefers-reduced-motion: reduce)",
+ );
+ let frame = 0;
+
+ const setScrollProgressVar = (progress: number) => {
+ const motionValues = motionValuesRef.current;
+
+ if (Math.abs(motionValues.scrollProgress - progress) < 0.001) {
+ return;
+ }
+
+ motionValues.scrollProgress = progress;
+ setElementMotionNumber(
+ stageRef.current,
+ "--khix-hero-scroll-progress",
+ progress,
+ );
+ setElementMotionNumber(
+ sectionRef.current,
+ "--khix-hero-scroll-progress",
+ progress,
+ );
+ };
+
+ const updateScrollProgress = () => {
+ const section = sectionRef.current;
+
+ if (!section || shouldReduceMotionRef.current) {
+ setScrollProgressVar(0);
+ return;
+ }
+
+ const viewportHeight = getViewportHeight();
+ const scrollableDistance = Math.max(1, viewportHeight);
+ const progress = clamp(
+ -section.getBoundingClientRect().top / scrollableDistance,
+ );
+
+ setScrollProgressVar(progress);
+ };
+
+ const scheduleScrollProgress = () => {
+ if (frame) {
+ return;
+ }
+
+ frame = window.requestAnimationFrame(() => {
+ frame = 0;
+ updateScrollProgress();
+ });
+ };
+
+ shouldReduceMotionRef.current = reduceMotionQuery.matches;
+ setPointerVars(0, 0);
+ updateScrollProgress();
+
+ const handleReducedMotionChange = () => {
+ shouldReduceMotionRef.current = reduceMotionQuery.matches;
+
+ if (reduceMotionQuery.matches) {
+ cancelPointerFrame();
+ setPointerVars(0, 0);
+ }
+
+ updateScrollProgress();
+ };
+
+ reduceMotionQuery.addEventListener("change", handleReducedMotionChange);
+ window.addEventListener("scroll", scheduleScrollProgress, {
+ passive: true,
+ });
+ window.addEventListener("resize", scheduleScrollProgress);
+ window.visualViewport?.addEventListener("resize", scheduleScrollProgress);
+
+ return () => {
+ reduceMotionQuery.removeEventListener(
+ "change",
+ handleReducedMotionChange,
+ );
+ window.removeEventListener("scroll", scheduleScrollProgress);
+ window.removeEventListener("resize", scheduleScrollProgress);
+ window.visualViewport?.removeEventListener(
+ "resize",
+ scheduleScrollProgress,
+ );
+ if (frame) {
+ window.cancelAnimationFrame(frame);
+ }
+ cancelPointerFrame();
+ setPointerVars(0, 0);
+ setScrollProgressVar(0);
+ };
+ }, []);
+
+ const handlePointerMove = (event: PointerEvent) => {
+ if (shouldReduceMotionRef.current) {
+ cancelPointerFrame();
+ setPointerVars(0, 0);
+ return;
+ }
+
+ const bounds = event.currentTarget.getBoundingClientRect();
+ const nextX = clamp(
+ ((event.clientX - bounds.left) / bounds.width - 0.5) * 2,
+ -1,
+ 1,
+ );
+ const nextY = clamp(
+ ((event.clientY - bounds.top) / bounds.height - 0.5) * 2,
+ -1,
+ 1,
+ );
+
+ schedulePointerVars(nextX, nextY);
+ };
+
+ const handlePointerLeave = () => {
+ schedulePointerVars(0, 0);
+ };
+
+ return {
+ sectionRef,
+ stageRef,
+ handlePointerMove,
+ handlePointerLeave,
+ };
+}
diff --git a/apps/khix/src/app/_components/sections/speakers/PondAtmosphere.module.css b/apps/khix/src/app/_components/sections/speakers/PondAtmosphere.module.css
new file mode 100644
index 000000000..f25160373
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/speakers/PondAtmosphere.module.css
@@ -0,0 +1,721 @@
+.pondAtmosphere,
+.fireflies,
+.firefly,
+.wisp,
+.wisp::before,
+.wisp::after {
+ pointer-events: none;
+}
+
+.pondAtmosphere {
+ position: absolute;
+ inset: 0;
+ contain: layout paint;
+ overflow: hidden;
+ opacity: 1;
+ backface-visibility: hidden;
+}
+
+.pondAssetCredit {
+ --asset-credit-top: auto;
+ --asset-credit-right: clamp(1rem, 3vw, 2.5rem);
+ --asset-credit-bottom: clamp(1rem, 2vw, 1.5rem);
+}
+
+.fireflies {
+ position: absolute;
+ inset: 0;
+ overflow: hidden;
+ contain: paint;
+}
+
+.firefly {
+ --firefly-left: 50%;
+ --firefly-top: 50%;
+ --firefly-size: 0.24rem;
+ --firefly-opacity: 0.68;
+ --firefly-duration: 20s;
+ --firefly-delay: 0s;
+ --firefly-x-mid: 1rem;
+ --firefly-y-mid: -1rem;
+ --firefly-x-end: 2rem;
+ --firefly-y-end: 0.5rem;
+
+ position: absolute;
+ top: var(--firefly-top);
+ left: var(--firefly-left);
+ width: clamp(1.45rem, calc(var(--firefly-size) + 1.35rem), 2rem);
+ aspect-ratio: 1;
+ border-radius: 999px;
+ background: radial-gradient(
+ circle,
+ rgb(255 255 235) 0 8%,
+ rgb(255 237 116 / 0.94) 16%,
+ rgb(255 207 72 / 0.58) 32%,
+ rgb(248 188 58 / 0.24) 50%,
+ transparent 78%
+ );
+ filter: drop-shadow(0 0 0.55rem rgb(255 222 96 / 0.5))
+ drop-shadow(0 0 1rem rgb(255 196 55 / 0.24));
+ opacity: var(--firefly-opacity);
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden;
+ transform: translate3d(0, 0, 0);
+ transform-origin: center;
+ animation: pondFireflyFloat var(--firefly-duration) ease-in-out
+ var(--firefly-delay) infinite alternate;
+ will-change: transform;
+}
+
+.firefly[data-firefly="1"] {
+ --firefly-left: 5%;
+ --firefly-top: 9%;
+ --firefly-size: 0.22rem;
+ --firefly-duration: 24s;
+ --firefly-delay: -5s;
+ --firefly-opacity: 0.56;
+ --firefly-x-mid: 1.4rem;
+ --firefly-y-mid: -2.2rem;
+ --firefly-x-end: 2.8rem;
+ --firefly-y-end: 0.6rem;
+}
+
+.firefly[data-firefly="2"] {
+ --firefly-left: 14%;
+ --firefly-top: 28%;
+ --firefly-size: 0.3rem;
+ --firefly-duration: 19s;
+ --firefly-delay: -9s;
+ --firefly-opacity: 0.72;
+ --firefly-x-mid: 2.1rem;
+ --firefly-y-mid: 1rem;
+ --firefly-x-end: 3.2rem;
+ --firefly-y-end: -1.5rem;
+}
+
+.firefly[data-firefly="3"] {
+ --firefly-left: 24%;
+ --firefly-top: 12%;
+ --firefly-size: 0.2rem;
+ --firefly-duration: 27s;
+ --firefly-delay: -2s;
+ --firefly-opacity: 0.5;
+ --firefly-x-mid: -1.4rem;
+ --firefly-y-mid: 2.4rem;
+ --firefly-x-end: 1.2rem;
+ --firefly-y-end: 4rem;
+}
+
+.firefly[data-firefly="4"] {
+ --firefly-left: 35%;
+ --firefly-top: 42%;
+ --firefly-size: 0.26rem;
+ --firefly-duration: 21s;
+ --firefly-delay: -13s;
+ --firefly-opacity: 0.66;
+ --firefly-x-mid: 1rem;
+ --firefly-y-mid: -2.8rem;
+ --firefly-x-end: -1.8rem;
+ --firefly-y-end: -4.4rem;
+}
+
+.firefly[data-firefly="5"] {
+ --firefly-left: 46%;
+ --firefly-top: 7%;
+ --firefly-size: 0.24rem;
+ --firefly-duration: 23s;
+ --firefly-delay: -7s;
+ --firefly-opacity: 0.58;
+ --firefly-x-mid: 2.6rem;
+ --firefly-y-mid: 1.5rem;
+ --firefly-x-end: 4.1rem;
+ --firefly-y-end: -0.8rem;
+}
+
+.firefly[data-firefly="6"] {
+ --firefly-left: 58%;
+ --firefly-top: 24%;
+ --firefly-size: 0.32rem;
+ --firefly-duration: 18s;
+ --firefly-delay: -11s;
+ --firefly-opacity: 0.75;
+ --firefly-x-mid: -2.4rem;
+ --firefly-y-mid: -1.2rem;
+ --firefly-x-end: -3.2rem;
+ --firefly-y-end: 1.7rem;
+}
+
+.firefly[data-firefly="7"] {
+ --firefly-left: 70%;
+ --firefly-top: 11%;
+ --firefly-size: 0.2rem;
+ --firefly-duration: 28s;
+ --firefly-delay: -4s;
+ --firefly-opacity: 0.52;
+ --firefly-x-mid: 1.2rem;
+ --firefly-y-mid: 2.8rem;
+ --firefly-x-end: -1.5rem;
+ --firefly-y-end: 4.2rem;
+}
+
+.firefly[data-firefly="8"] {
+ --firefly-left: 86%;
+ --firefly-top: 35%;
+ --firefly-size: 0.28rem;
+ --firefly-duration: 22s;
+ --firefly-delay: -15s;
+ --firefly-opacity: 0.7;
+ --firefly-x-mid: -1.8rem;
+ --firefly-y-mid: -2.6rem;
+ --firefly-x-end: -4.2rem;
+ --firefly-y-end: -0.2rem;
+}
+
+.firefly[data-firefly="9"] {
+ --firefly-left: 96%;
+ --firefly-top: 14%;
+ --firefly-size: 0.23rem;
+ --firefly-duration: 25s;
+ --firefly-delay: -6s;
+ --firefly-opacity: 0.56;
+ --firefly-x-mid: -2.5rem;
+ --firefly-y-mid: 1.8rem;
+ --firefly-x-end: -5rem;
+ --firefly-y-end: 3.3rem;
+}
+
+.firefly[data-firefly="10"] {
+ --firefly-left: 9%;
+ --firefly-top: 64%;
+ --firefly-size: 0.31rem;
+ --firefly-duration: 20s;
+ --firefly-delay: -12s;
+ --firefly-opacity: 0.7;
+ --firefly-x-mid: 2.8rem;
+ --firefly-y-mid: -1rem;
+ --firefly-x-end: 4.8rem;
+ --firefly-y-end: 1.4rem;
+}
+
+.firefly[data-firefly="11"] {
+ --firefly-left: 19%;
+ --firefly-top: 82%;
+ --firefly-size: 0.22rem;
+ --firefly-duration: 29s;
+ --firefly-delay: -3s;
+ --firefly-opacity: 0.5;
+ --firefly-x-mid: 1rem;
+ --firefly-y-mid: -3rem;
+ --firefly-x-end: -1.2rem;
+ --firefly-y-end: -5rem;
+}
+
+.firefly[data-firefly="12"] {
+ --firefly-left: 32%;
+ --firefly-top: 72%;
+ --firefly-size: 0.27rem;
+ --firefly-duration: 23s;
+ --firefly-delay: -17s;
+ --firefly-opacity: 0.62;
+ --firefly-x-mid: -1.2rem;
+ --firefly-y-mid: -2.5rem;
+ --firefly-x-end: 2.2rem;
+ --firefly-y-end: -4.2rem;
+}
+
+.firefly[data-firefly="13"] {
+ --firefly-left: 48%;
+ --firefly-top: 58%;
+ --firefly-size: 0.2rem;
+ --firefly-duration: 26s;
+ --firefly-delay: -8s;
+ --firefly-opacity: 0.54;
+ --firefly-x-mid: 2.5rem;
+ --firefly-y-mid: 2rem;
+ --firefly-x-end: -0.6rem;
+ --firefly-y-end: 4.5rem;
+}
+
+.firefly[data-firefly="14"] {
+ --firefly-left: 63%;
+ --firefly-top: 76%;
+ --firefly-size: 0.33rem;
+ --firefly-duration: 18s;
+ --firefly-delay: -14s;
+ --firefly-opacity: 0.74;
+ --firefly-x-mid: -2.2rem;
+ --firefly-y-mid: -1.8rem;
+ --firefly-x-end: -4rem;
+ --firefly-y-end: 1.1rem;
+}
+
+.firefly[data-firefly="15"] {
+ --firefly-left: 78%;
+ --firefly-top: 62%;
+ --firefly-size: 0.24rem;
+ --firefly-duration: 24s;
+ --firefly-delay: -1s;
+ --firefly-opacity: 0.58;
+ --firefly-x-mid: -1rem;
+ --firefly-y-mid: 2.4rem;
+ --firefly-x-end: 2.4rem;
+ --firefly-y-end: 4.1rem;
+}
+
+.firefly[data-firefly="16"] {
+ --firefly-left: 91%;
+ --firefly-top: 83%;
+ --firefly-size: 0.3rem;
+ --firefly-duration: 21s;
+ --firefly-delay: -16s;
+ --firefly-opacity: 0.68;
+ --firefly-x-mid: -2.8rem;
+ --firefly-y-mid: -2.4rem;
+ --firefly-x-end: -5.3rem;
+ --firefly-y-end: -0.6rem;
+}
+
+.firefly[data-firefly="17"] {
+ --firefly-left: 3%;
+ --firefly-top: 44%;
+ --firefly-size: 0.2rem;
+ --firefly-duration: 31s;
+ --firefly-delay: -18s;
+ --firefly-opacity: 0.46;
+ --firefly-x-mid: 2.6rem;
+ --firefly-y-mid: 1.8rem;
+ --firefly-x-end: 5.1rem;
+ --firefly-y-end: -1.2rem;
+}
+
+.firefly[data-firefly="18"] {
+ --firefly-left: 40%;
+ --firefly-top: 22%;
+ --firefly-size: 0.21rem;
+ --firefly-duration: 30s;
+ --firefly-delay: -20s;
+ --firefly-opacity: 0.5;
+ --firefly-x-mid: -2rem;
+ --firefly-y-mid: 1.4rem;
+ --firefly-x-end: -3.8rem;
+ --firefly-y-end: -2.6rem;
+}
+
+.firefly[data-firefly="19"] {
+ --firefly-left: 53%;
+ --firefly-top: 92%;
+ --firefly-size: 0.26rem;
+ --firefly-duration: 25s;
+ --firefly-delay: -10s;
+ --firefly-opacity: 0.6;
+ --firefly-x-mid: 1.6rem;
+ --firefly-y-mid: -2.7rem;
+ --firefly-x-end: 3.8rem;
+ --firefly-y-end: -5.2rem;
+}
+
+.firefly[data-firefly="20"] {
+ --firefly-left: 67%;
+ --firefly-top: 48%;
+ --firefly-size: 0.22rem;
+ --firefly-duration: 27s;
+ --firefly-delay: -19s;
+ --firefly-opacity: 0.52;
+ --firefly-x-mid: 2.8rem;
+ --firefly-y-mid: -1.5rem;
+ --firefly-x-end: 0.8rem;
+ --firefly-y-end: -4.8rem;
+}
+
+.firefly[data-firefly="21"] {
+ --firefly-left: 84%;
+ --firefly-top: 6%;
+ --firefly-size: 0.2rem;
+ --firefly-duration: 32s;
+ --firefly-delay: -21s;
+ --firefly-opacity: 0.48;
+ --firefly-x-mid: -1.2rem;
+ --firefly-y-mid: 2.8rem;
+ --firefly-x-end: -4.2rem;
+ --firefly-y-end: 4.7rem;
+}
+
+.firefly[data-firefly="22"] {
+ --firefly-left: 15%;
+ --firefly-top: 5%;
+ --firefly-size: 0.18rem;
+ --firefly-duration: 34s;
+ --firefly-delay: -24s;
+ --firefly-opacity: 0.42;
+ --firefly-x-mid: 2.2rem;
+ --firefly-y-mid: 2rem;
+ --firefly-x-end: 4.6rem;
+ --firefly-y-end: 3.8rem;
+}
+
+.firefly[data-firefly="23"] {
+ --firefly-left: 73%;
+ --firefly-top: 91%;
+ --firefly-size: 0.22rem;
+ --firefly-duration: 28s;
+ --firefly-delay: -23s;
+ --firefly-opacity: 0.5;
+ --firefly-x-mid: -2.4rem;
+ --firefly-y-mid: -2.8rem;
+ --firefly-x-end: -0.5rem;
+ --firefly-y-end: -5.5rem;
+}
+
+.firefly[data-firefly="24"] {
+ --firefly-left: 98%;
+ --firefly-top: 58%;
+ --firefly-size: 0.19rem;
+ --firefly-duration: 33s;
+ --firefly-delay: -26s;
+ --firefly-opacity: 0.44;
+ --firefly-x-mid: -2.8rem;
+ --firefly-y-mid: 2.2rem;
+ --firefly-x-end: -5.6rem;
+ --firefly-y-end: 4.6rem;
+}
+
+.wisp,
+.wisp::before,
+.wisp::after {
+ position: absolute;
+}
+
+.wisp::before,
+.wisp::after {
+ content: "";
+}
+
+.wisp {
+ --wisp-size: clamp(3.8rem, 6vw, 7.1rem);
+ --wisp-sprite-opacity: 0.76;
+ --wisp-glow-opacity: 0.34;
+ --wisp-scale: 1;
+ --wisp-scale-mid: 1.06;
+ --wisp-scale-end: 0.98;
+ --wisp-rotate: -8deg;
+ --wisp-rotate-mid: 3.6deg;
+ --wisp-rotate-end: -1deg;
+ --wisp-duration: 14s;
+ --wisp-delay: 0s;
+ --wisp-opacity: 0.86;
+ --wisp-x-start: 0rem;
+ --wisp-y-start: 0rem;
+ --wisp-x-mid: 1rem;
+ --wisp-y-mid: -1.1rem;
+ --wisp-x-end: 2rem;
+ --wisp-y-end: 0.7rem;
+
+ top: var(--wisp-top);
+ left: var(--wisp-left);
+ width: var(--wisp-size);
+ aspect-ratio: 1;
+ opacity: var(--wisp-opacity);
+ filter: drop-shadow(0 0 0.8rem rgb(111 225 255 / 0.34))
+ drop-shadow(0 0 1.8rem rgb(41 184 237 / 0.22));
+ transform: translate3d(var(--wisp-x-start), var(--wisp-y-start), 0)
+ rotate(var(--wisp-rotate)) scale(var(--wisp-scale));
+ animation: pondSpiritWisp var(--wisp-duration) ease-in-out var(--wisp-delay)
+ infinite alternate;
+ isolation: isolate;
+ mix-blend-mode: screen;
+ will-change: opacity, transform;
+}
+
+.wisp::before {
+ inset: -4%;
+ z-index: 1;
+ background: url("https://assets.knighthacks.org/khix/khix-wisp-flame.webp")
+ center / contain no-repeat;
+ opacity: var(--wisp-sprite-opacity);
+ transform: translateZ(0) scale(0.98);
+ animation: pondWispSpriteFlicker 7.2s ease-in-out infinite;
+ image-rendering: auto;
+}
+
+.wisp::after {
+ inset: -18%;
+ z-index: 0;
+ background: url("https://assets.knighthacks.org/khix/khix-wisp-flame.webp")
+ center / contain no-repeat;
+ filter: blur(10px) saturate(1.2);
+ opacity: var(--wisp-glow-opacity);
+ transform: translate3d(0.15rem, 0.12rem, 0) scale(1.22);
+ animation: pondWispSpriteGlow 8.4s ease-in-out infinite;
+}
+
+.wisp[data-wisp="one"] {
+ --wisp-left: 4%;
+ --wisp-top: 18%;
+ --wisp-scale: 0.78;
+ --wisp-scale-mid: 0.84;
+ --wisp-scale-end: 0.76;
+ --wisp-rotate: -12deg;
+ --wisp-rotate-mid: 5deg;
+ --wisp-rotate-end: -5deg;
+ --wisp-duration: 16s;
+ --wisp-delay: -2s;
+ --wisp-opacity: 0.62;
+ --wisp-x-mid: 2.4rem;
+ --wisp-y-mid: -2.2rem;
+ --wisp-x-end: 4rem;
+ --wisp-y-end: 0.5rem;
+}
+
+.wisp[data-wisp="two"] {
+ --wisp-left: 7%;
+ --wisp-top: 91%;
+ --wisp-size: clamp(3.1rem, 4.8vw, 5.1rem);
+ --wisp-scale: 0.68;
+ --wisp-scale-mid: 0.74;
+ --wisp-scale-end: 0.66;
+ --wisp-rotate: 10deg;
+ --wisp-rotate-mid: -4deg;
+ --wisp-rotate-end: 16deg;
+ --wisp-duration: 18s;
+ --wisp-delay: -7s;
+ --wisp-opacity: 0.5;
+ --wisp-x-mid: 1rem;
+ --wisp-y-mid: -1.4rem;
+ --wisp-x-end: 2rem;
+ --wisp-y-end: -0.2rem;
+}
+
+.wisp[data-wisp="three"] {
+ --wisp-left: 56%;
+ --wisp-top: 14%;
+ --wisp-size: clamp(4rem, 6vw, 6.8rem);
+ --wisp-scale: 0.92;
+ --wisp-scale-mid: 1;
+ --wisp-scale-end: 0.9;
+ --wisp-rotate: -18deg;
+ --wisp-rotate-mid: 8deg;
+ --wisp-rotate-end: -12deg;
+ --wisp-duration: 20s;
+ --wisp-delay: -4s;
+ --wisp-x-mid: 1.8rem;
+ --wisp-y-mid: 2.4rem;
+ --wisp-x-end: -1.2rem;
+ --wisp-y-end: 4.4rem;
+}
+
+.wisp[data-wisp="four"] {
+ --wisp-left: 88%;
+ --wisp-top: 43%;
+ --wisp-size: clamp(3.6rem, 5.4vw, 5.8rem);
+ --wisp-scale: 0.78;
+ --wisp-scale-mid: 0.86;
+ --wisp-scale-end: 0.76;
+ --wisp-rotate: 14deg;
+ --wisp-rotate-mid: -6deg;
+ --wisp-rotate-end: 20deg;
+ --wisp-duration: 17s;
+ --wisp-delay: -10s;
+ --wisp-opacity: 0.76;
+ --wisp-x-mid: -1rem;
+ --wisp-y-mid: -1.7rem;
+ --wisp-x-end: -2.5rem;
+ --wisp-y-end: -0.2rem;
+}
+
+.wisp[data-wisp="five"] {
+ --wisp-left: 46%;
+ --wisp-top: 88%;
+ --wisp-size: clamp(3.2rem, 4.8vw, 5.2rem);
+ --wisp-scale: 0.72;
+ --wisp-scale-mid: 0.8;
+ --wisp-scale-end: 0.7;
+ --wisp-rotate: -5deg;
+ --wisp-rotate-mid: 3deg;
+ --wisp-rotate-end: 2deg;
+ --wisp-duration: 21s;
+ --wisp-delay: -13s;
+ --wisp-opacity: 0.68;
+ --wisp-x-mid: 1.8rem;
+ --wisp-y-mid: -1rem;
+ --wisp-x-end: 3.2rem;
+ --wisp-y-end: -2.3rem;
+}
+
+.wisp[data-wisp="six"] {
+ --wisp-left: 75%;
+ --wisp-top: 72%;
+ --wisp-size: clamp(3.4rem, 5vw, 5.6rem);
+ --wisp-scale: 0.72;
+ --wisp-scale-mid: 0.82;
+ --wisp-scale-end: 0.7;
+ --wisp-rotate: 8deg;
+ --wisp-rotate-mid: -8deg;
+ --wisp-rotate-end: 14deg;
+ --wisp-duration: 19s;
+ --wisp-delay: -9s;
+ --wisp-opacity: 0.7;
+ --wisp-x-mid: -2.4rem;
+ --wisp-y-mid: -1.9rem;
+ --wisp-x-end: -4.1rem;
+ --wisp-y-end: 0.8rem;
+}
+
+.wisp[data-wisp="seven"] {
+ --wisp-left: 33%;
+ --wisp-top: 31%;
+ --wisp-size: clamp(3.1rem, 4.5vw, 5rem);
+ --wisp-scale: 0.66;
+ --wisp-scale-mid: 0.73;
+ --wisp-scale-end: 0.64;
+ --wisp-rotate: -20deg;
+ --wisp-rotate-mid: 7deg;
+ --wisp-rotate-end: -14deg;
+ --wisp-duration: 22s;
+ --wisp-delay: -15s;
+ --wisp-opacity: 0.54;
+ --wisp-x-mid: 1.3rem;
+ --wisp-y-mid: -2.8rem;
+ --wisp-x-end: -0.8rem;
+ --wisp-y-end: -4.2rem;
+}
+
+.wisp[data-wisp="eight"] {
+ --wisp-left: 96%;
+ --wisp-top: 20%;
+ --wisp-size: clamp(3.2rem, 4.6vw, 5.2rem);
+ --wisp-scale: 0.62;
+ --wisp-scale-mid: 0.7;
+ --wisp-scale-end: 0.6;
+ --wisp-rotate: 18deg;
+ --wisp-rotate-mid: -5deg;
+ --wisp-rotate-end: 10deg;
+ --wisp-duration: 24s;
+ --wisp-delay: -6s;
+ --wisp-opacity: 0.52;
+ --wisp-x-mid: -2.8rem;
+ --wisp-y-mid: 1.2rem;
+ --wisp-x-end: -5rem;
+ --wisp-y-end: 3.4rem;
+}
+
+@media (max-width: 700px) {
+ .pondAtmosphere {
+ opacity: 0.88;
+ }
+
+ .wisp {
+ --wisp-size: clamp(2.9rem, 12vw, 4.7rem);
+ }
+
+ .wisp[data-wisp="one"] {
+ --wisp-left: -6%;
+ --wisp-top: 18%;
+ --wisp-opacity: 0.5;
+ }
+
+ .wisp[data-wisp="two"] {
+ --wisp-left: 5%;
+ --wisp-top: 90%;
+ --wisp-opacity: 0.44;
+ }
+
+ .wisp[data-wisp="three"] {
+ --wisp-left: 58%;
+ --wisp-top: 24%;
+ }
+
+ .wisp[data-wisp="four"] {
+ --wisp-left: 88%;
+ --wisp-top: 48%;
+ }
+
+ .wisp[data-wisp="five"] {
+ --wisp-left: 42%;
+ --wisp-top: 92%;
+ --wisp-opacity: 0.52;
+ }
+
+ .wisp[data-wisp="six"] {
+ --wisp-left: 72%;
+ --wisp-top: 78%;
+ --wisp-opacity: 0.56;
+ }
+
+ .wisp[data-wisp="seven"] {
+ --wisp-left: 25%;
+ --wisp-top: 40%;
+ --wisp-opacity: 0.46;
+ }
+
+ .wisp[data-wisp="eight"] {
+ --wisp-left: 101%;
+ --wisp-top: 17%;
+ --wisp-opacity: 0.42;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .firefly,
+ .wisp,
+ .wisp::before,
+ .wisp::after {
+ animation: none;
+ }
+}
+
+@keyframes pondFireflyFloat {
+ 0% {
+ transform: translate3d(0, 0, 0);
+ }
+
+ 50% {
+ transform: translate3d(var(--firefly-x-mid), var(--firefly-y-mid), 0);
+ }
+
+ 100% {
+ transform: translate3d(var(--firefly-x-end), var(--firefly-y-end), 0);
+ }
+}
+
+@keyframes pondSpiritWisp {
+ 0% {
+ transform: translate3d(var(--wisp-x-start), var(--wisp-y-start), 0)
+ rotate(var(--wisp-rotate)) scale(var(--wisp-scale));
+ }
+
+ 48% {
+ transform: translate3d(var(--wisp-x-mid), var(--wisp-y-mid), 0)
+ rotate(var(--wisp-rotate-mid)) scale(var(--wisp-scale-mid));
+ }
+
+ 100% {
+ transform: translate3d(var(--wisp-x-end), var(--wisp-y-end), 0)
+ rotate(var(--wisp-rotate-end)) scale(var(--wisp-scale-end));
+ }
+}
+
+@keyframes pondWispSpriteFlicker {
+ 0%,
+ 100% {
+ opacity: 0.74;
+ transform: translate3d(0, 0, 0) scale(0.98) rotate(-0.5deg);
+ }
+
+ 44% {
+ opacity: 0.84;
+ transform: translate3d(0.025rem, -0.035rem, 0) scale(1.015) rotate(0.5deg);
+ }
+}
+
+@keyframes pondWispSpriteGlow {
+ 0%,
+ 100% {
+ opacity: 0.22;
+ transform: translate3d(0.08rem, 0.06rem, 0) scale(1.1) rotate(-1deg);
+ }
+
+ 50% {
+ opacity: 0.36;
+ transform: translate3d(0.18rem, -0.12rem, 0) scale(1.2) rotate(1.5deg);
+ }
+}
diff --git a/apps/khix/src/app/_components/sections/speakers/PondAtmosphere.tsx b/apps/khix/src/app/_components/sections/speakers/PondAtmosphere.tsx
new file mode 100644
index 000000000..163cf13ff
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/speakers/PondAtmosphere.tsx
@@ -0,0 +1,52 @@
+import { AssetCredit } from "../../assets";
+import styles from "./PondAtmosphere.module.css";
+
+const FIREFLY_IDS = Array.from({ length: 24 }, (_, index) => String(index + 1));
+const WISP_IDS = [
+ "one",
+ "two",
+ "three",
+ "four",
+ "five",
+ "six",
+ "seven",
+ "eight",
+] as const;
+
+interface PondAtmosphereProps {
+ className?: string;
+}
+
+export function PondAtmosphere({ className }: PondAtmosphereProps) {
+ const rootClassName = className
+ ? `${styles.pondAssetCredit} ${className}`
+ : styles.pondAssetCredit;
+
+ return (
+
+
+
+ {FIREFLY_IDS.map((fireflyId) => (
+
+ ))}
+
+ {WISP_IDS.map((wispId) => (
+
+ ))}
+
+
+ );
+}
diff --git a/apps/khix/src/app/_components/sections/speakers/SpeakerShowcase.module.css b/apps/khix/src/app/_components/sections/speakers/SpeakerShowcase.module.css
new file mode 100644
index 000000000..28e05a75a
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/speakers/SpeakerShowcase.module.css
@@ -0,0 +1,405 @@
+.speakerShowcase {
+ --speaker-accent: #d7ff76;
+ --speaker-control-color: rgba(249, 255, 246, 0.96);
+ --speaker-ink: rgba(249, 255, 246, 0.96);
+ --speaker-muted: rgba(234, 249, 237, 0.72);
+ --speaker-name-font-size: 2.25rem;
+ --speaker-arrow-width: 2.35rem;
+ --speaker-arrow-offset: clamp(7rem, 8.65vw, 9.25rem);
+ --speaker-stage-gap: clamp(0.55rem, 1.2vw, 0.95rem);
+ --speaker-pair-gap: clamp(1.2rem, 3.8vw, 3.15rem);
+ --speaker-stage-side-width: calc(
+ var(--speaker-arrow-width) + var(--speaker-stage-gap)
+ );
+ --speaker-portrait-width: clamp(13.25rem, 16vw, 16.75rem);
+ --speaker-track-width: var(--speaker-portrait-width);
+ --speaker-stage-width: min(100%, 58rem);
+
+ position: relative;
+ display: grid;
+ width: min(100%, var(--speaker-stage-width));
+ margin: 0 auto;
+ color: var(--speaker-ink);
+ isolation: isolate;
+}
+
+.title {
+ position: relative;
+ z-index: 2;
+ margin: 0 0 clamp(1.5rem, 2.2rem, 2.2rem);
+ color: var(--speaker-ink);
+ font-family: var(--font-cormorant-garamond, Georgia), Georgia, serif;
+ font-size: clamp(3.4rem, 8vw, 6.8rem);
+ font-weight: 400;
+ line-height: 0.84;
+ letter-spacing: 0;
+ text-align: center;
+ text-shadow:
+ 0 0 0.5rem rgba(235, 251, 255, 0.54),
+ 0 0 2.2rem rgba(129, 225, 255, 0.3);
+ transform: translateY(calc(clamp(3rem, 7svh, 5rem) * -1));
+}
+
+.stage {
+ position: relative;
+ z-index: 2;
+ display: grid;
+ grid-template-columns:
+ var(--speaker-arrow-width) var(--speaker-track-width)
+ var(--speaker-arrow-width);
+ width: calc(
+ var(--speaker-track-width) + var(--speaker-stage-side-width) +
+ var(--speaker-stage-side-width)
+ );
+ max-width: 100%;
+ margin: 0 auto;
+ align-items: start;
+ justify-content: center;
+ gap: var(--speaker-stage-gap);
+ overflow: visible;
+}
+
+.stage[data-visible-speaker-count="2"] {
+ --speaker-track-width: calc(
+ var(--speaker-portrait-width) + var(--speaker-portrait-width) +
+ var(--speaker-pair-gap)
+ );
+}
+
+.speakerList {
+ --speaker-swipe-distance: clamp(2rem, 8vw, 4.5rem);
+
+ display: grid;
+ grid-template-columns: minmax(0, var(--speaker-portrait-width));
+ width: var(--speaker-track-width);
+ min-width: 0;
+ gap: var(--speaker-pair-gap);
+ justify-items: center;
+ justify-content: center;
+ overflow: visible;
+ transform: translate3d(0, 0, 0);
+ will-change: opacity, filter, transform;
+}
+
+.speakerList[data-visible-speaker-count="2"] {
+ grid-template-columns:
+ minmax(0, var(--speaker-portrait-width))
+ minmax(0, var(--speaker-portrait-width));
+}
+
+.speaker {
+ display: grid;
+ width: var(--speaker-portrait-width);
+ min-width: 0;
+ justify-items: center;
+ text-align: center;
+ contain: inline-size;
+}
+
+.speakerList[data-transition-state="exiting"][data-transition-direction="next"] {
+ animation: speakerSwipeOutNext 240ms cubic-bezier(0.45, 0, 0.55, 1) both;
+}
+
+.speakerList[data-transition-state="exiting"][data-transition-direction="previous"] {
+ animation: speakerSwipeOutPrevious 240ms cubic-bezier(0.45, 0, 0.55, 1) both;
+}
+
+.speakerList[data-transition-state="entering"][data-transition-direction="next"] {
+ animation: speakerSwipeInNext 300ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
+}
+
+.speakerList[data-transition-state="entering"][data-transition-direction="previous"] {
+ animation: speakerSwipeInPrevious 300ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
+}
+
+.portraitShell {
+ display: block;
+ width: var(--speaker-portrait-width);
+ color: inherit;
+ text-decoration: none;
+ transform: translate3d(0, 0, 0);
+}
+
+.portraitLink {
+ cursor: pointer;
+}
+
+.portraitLink:focus-visible {
+ border-radius: 0.35rem;
+ outline: 2px solid rgba(215, 255, 118, 0.72);
+ outline-offset: 0.35rem;
+}
+
+.portraitLink:hover .frameImage,
+.portraitLink:focus-visible .frameImage {
+ filter: drop-shadow(0 0.85rem 1.6rem rgba(0, 0, 0, 0.34));
+}
+
+.portraitStage {
+ position: relative;
+ display: grid;
+ width: var(--speaker-portrait-width);
+ aspect-ratio: 1607 / 2003;
+ place-items: center;
+ isolation: isolate;
+}
+
+.frameImage {
+ position: absolute;
+ inset: 0;
+ z-index: 3;
+ object-fit: contain;
+ object-position: center;
+ filter: drop-shadow(0 0.8rem 1.6rem rgba(0, 0, 0, 0.32));
+ pointer-events: none;
+ user-select: none;
+ transition: filter 180ms ease;
+}
+
+.imageFrame {
+ position: absolute;
+ top: 17.75%;
+ left: 50%;
+ z-index: 2;
+ display: block;
+ width: 64%;
+ aspect-ratio: 1028 / 1482;
+ overflow: hidden;
+ border-radius: 3px;
+ background: transparent;
+ transform: translateX(-50%);
+}
+
+.image {
+ object-fit: cover;
+ object-position: center;
+ user-select: none;
+}
+
+.speakerMeta {
+ display: grid;
+ justify-items: center;
+ width: min(100%, var(--speaker-portrait-width));
+ min-width: 0;
+ margin: 1.15rem auto 0;
+ text-align: center;
+}
+
+.nameRow {
+ display: grid;
+ width: 100%;
+ min-width: 0;
+ justify-items: center;
+}
+
+.name {
+ max-width: 100%;
+ min-width: 0;
+ margin: 0;
+ color: var(--speaker-ink);
+ font-family: var(--font-cormorant-garamond, Georgia), Georgia, serif;
+ font-size: var(--speaker-name-font-size);
+ font-weight: 600;
+ line-height: 0.95;
+ letter-spacing: 0;
+ overflow-wrap: break-word;
+ text-wrap: balance;
+ white-space: normal;
+ text-shadow:
+ 0 0.1rem 0.75rem rgba(0, 0, 0, 0.54),
+ 0 0 1.2rem rgba(13, 38, 32, 0.48);
+}
+
+.role {
+ max-width: 100%;
+ margin: 0.55rem 0 0;
+ color: var(--speaker-muted);
+ font-family:
+ var(--font-geist-sans, ui-sans-serif),
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ sans-serif;
+ font-size: 0.92rem;
+ font-weight: 800;
+ line-height: 1.4;
+ letter-spacing: 0;
+ text-transform: uppercase;
+ text-wrap: balance;
+}
+
+.arrowButton {
+ display: inline-grid;
+ width: 2.35rem;
+ aspect-ratio: 1;
+ margin-top: var(--speaker-arrow-offset);
+ place-items: center;
+ border: 0;
+ color: var(--speaker-control-color);
+ background: transparent;
+ filter: drop-shadow(0 0.45rem 0.9rem rgba(0, 0, 0, 0.42));
+ cursor: pointer;
+ transition:
+ color 180ms ease,
+ filter 180ms ease,
+ opacity 180ms ease,
+ transform 180ms ease;
+}
+
+.arrowButton:hover,
+.arrowButton:focus-visible {
+ color: #ffffff;
+ outline: none;
+ filter: drop-shadow(0 0 0.45rem rgba(215, 255, 118, 0.28))
+ drop-shadow(0 0.5rem 0.95rem rgba(0, 0, 0, 0.42));
+ transform: translateY(-1px) scale(1.05);
+}
+
+.arrowButton:focus-visible {
+ outline: 2px solid rgba(215, 255, 118, 0.72);
+ outline-offset: 0.15rem;
+}
+
+.arrowButton:disabled {
+ cursor: not-allowed;
+ opacity: 0.42;
+ transform: none;
+}
+
+.arrowButton:disabled:hover,
+.arrowButton:disabled:focus-visible {
+ color: var(--speaker-control-color);
+ filter: drop-shadow(0 0.45rem 0.9rem rgba(0, 0, 0, 0.42));
+ outline: none;
+}
+
+.arrowIcon {
+ width: 2rem;
+ height: 2rem;
+ fill: currentColor;
+ stroke: currentColor;
+ stroke-width: 1.3;
+}
+
+@media (max-width: 760px) {
+ .speakerShowcase {
+ --speaker-name-font-size: 1.9rem;
+ --speaker-arrow-width: 2.15rem;
+ --speaker-arrow-offset: 7.15rem;
+ --speaker-stage-gap: 0.45rem;
+ --speaker-pair-gap: 0;
+ --speaker-portrait-width: 13.25rem;
+ --speaker-stage-width: min(100%, 28rem);
+ }
+
+ .stage {
+ grid-template-columns:
+ var(--speaker-arrow-width) var(--speaker-track-width)
+ var(--speaker-arrow-width);
+ }
+
+ .role {
+ font-size: 0.78rem;
+ }
+
+ .arrowButton {
+ width: 2.15rem;
+ }
+}
+
+@media (max-width: 420px) {
+ .speakerShowcase {
+ --speaker-arrow-width: 1.95rem;
+ --speaker-arrow-offset: 6.35rem;
+ --speaker-stage-gap: 0.35rem;
+ --speaker-portrait-width: 11.75rem;
+ }
+
+ .stage {
+ grid-template-columns:
+ var(--speaker-arrow-width) var(--speaker-track-width)
+ var(--speaker-arrow-width);
+ }
+
+ .arrowButton {
+ width: 1.95rem;
+ }
+
+ .arrowIcon {
+ width: 1.75rem;
+ height: 1.75rem;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .speakerList,
+ .frameImage,
+ .arrowButton {
+ animation: none;
+ transition: none;
+ }
+
+ .arrowButton:hover,
+ .arrowButton:focus-visible {
+ transform: none;
+ }
+}
+
+@keyframes speakerSwipeOutNext {
+ from {
+ opacity: 1;
+ filter: blur(0);
+ transform: translate3d(0, 0, 0) scale(1);
+ }
+
+ to {
+ opacity: 0;
+ filter: blur(1.5px);
+ transform: translate3d(calc(var(--speaker-swipe-distance) * -1), 0, 0)
+ scale(0.98);
+ }
+}
+
+@keyframes speakerSwipeOutPrevious {
+ from {
+ opacity: 1;
+ filter: blur(0);
+ transform: translate3d(0, 0, 0) scale(1);
+ }
+
+ to {
+ opacity: 0;
+ filter: blur(1.5px);
+ transform: translate3d(var(--speaker-swipe-distance), 0, 0) scale(0.98);
+ }
+}
+
+@keyframes speakerSwipeInNext {
+ from {
+ opacity: 0;
+ filter: blur(1.5px);
+ transform: translate3d(var(--speaker-swipe-distance), 0, 0) scale(0.98);
+ }
+
+ to {
+ opacity: 1;
+ filter: blur(0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes speakerSwipeInPrevious {
+ from {
+ opacity: 0;
+ filter: blur(1.5px);
+ transform: translate3d(calc(var(--speaker-swipe-distance) * -1), 0, 0)
+ scale(0.98);
+ }
+
+ to {
+ opacity: 1;
+ filter: blur(0);
+ transform: translate3d(0, 0, 0);
+ }
+}
diff --git a/apps/khix/src/app/_components/sections/speakers/SpeakerShowcase.tsx b/apps/khix/src/app/_components/sections/speakers/SpeakerShowcase.tsx
new file mode 100644
index 000000000..64e840060
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/speakers/SpeakerShowcase.tsx
@@ -0,0 +1,344 @@
+"use client";
+
+import { useCallback, useEffect, useState } from "react";
+import Image from "next/image";
+import { ArrowBigLeft, ArrowBigRight } from "lucide-react";
+
+import type { SpeakerShowcaseSpeaker } from "./speakers";
+import styles from "./SpeakerShowcase.module.css";
+
+const AUTO_SCROLL_DELAY_MS = 4200;
+const SPEAKER_ENTER_MS = 300;
+const SPEAKER_EXIT_MS = 240;
+const MOBILE_VISIBLE_SPEAKER_COUNT = 1;
+const LARGE_DESKTOP_VISIBLE_SPEAKER_COUNT = 2;
+const LARGE_DESKTOP_VISIBLE_SPEAKERS_QUERY = "(min-width: 1800px)";
+
+type SpeakerTransitionDirection = "next" | "previous";
+type SpeakerTransitionState = "idle" | "exiting" | "entering";
+
+interface VisibleSpeaker {
+ index: number;
+ speaker: SpeakerShowcaseSpeaker;
+}
+
+export interface SpeakerShowcaseProps {
+ speakers: readonly SpeakerShowcaseSpeaker[];
+ className?: string;
+ title?: string;
+ titleId?: string;
+}
+
+function getSpeakerKey(speaker: SpeakerShowcaseSpeaker, index: number) {
+ return `${speaker.name}-${speaker.companyRole}-${index}`;
+}
+
+function getSpeakerImageAlt(speaker: SpeakerShowcaseSpeaker) {
+ if (speaker.name.toLowerCase() === "coming soon") {
+ return "";
+ }
+
+ return `${speaker.name} speaker portrait`;
+}
+
+function getLinkedInLabel(speaker: SpeakerShowcaseSpeaker) {
+ return `Open ${speaker.name}'s LinkedIn profile`;
+}
+
+function getPreviousSpeakerIndex(currentIndex: number, speakerCount: number) {
+ return (currentIndex - 1 + speakerCount) % speakerCount;
+}
+
+function getNextSpeakerIndex(currentIndex: number, speakerCount: number) {
+ return (currentIndex + 1) % speakerCount;
+}
+
+function getVisibleSpeakers(
+ speakers: readonly SpeakerShowcaseSpeaker[],
+ activeSpeakerIndex: number,
+ visibleSpeakerCount: number,
+) {
+ const resolvedVisibleSpeakerCount = Math.min(
+ visibleSpeakerCount,
+ speakers.length,
+ );
+ const visibleSpeakers: VisibleSpeaker[] = [];
+
+ for (let offset = 0; offset < resolvedVisibleSpeakerCount; offset += 1) {
+ const index = (activeSpeakerIndex + offset) % speakers.length;
+ const speaker = speakers[index];
+
+ if (speaker) {
+ visibleSpeakers.push({ index, speaker });
+ }
+ }
+
+ return visibleSpeakers;
+}
+
+function SpeakerPortrait({ speaker }: { speaker: SpeakerShowcaseSpeaker }) {
+ const portraitStage = (
+
+ );
+
+ if (!speaker.linkedinUrl) {
+ return {portraitStage}
;
+ }
+
+ return (
+
+ {portraitStage}
+
+ );
+}
+
+export function SpeakerShowcase({
+ className,
+ speakers,
+ title = "Speakers",
+ titleId = "khix-speakers-title",
+}: SpeakerShowcaseProps) {
+ const [activeSpeakerIndex, setActiveSpeakerIndex] = useState(0);
+ const [autoScrollResetKey, setAutoScrollResetKey] = useState(0);
+ const [visibleSpeakerCount, setVisibleSpeakerCount] = useState(
+ MOBILE_VISIBLE_SPEAKER_COUNT,
+ );
+ const [pendingSpeakerIndex, setPendingSpeakerIndex] = useState(
+ null,
+ );
+ const [transitionDirection, setTransitionDirection] =
+ useState("next");
+ const [transitionState, setTransitionState] =
+ useState("idle");
+ const normalizedActiveSpeakerIndex =
+ speakers.length > 0 ? activeSpeakerIndex % speakers.length : 0;
+ const renderedSpeakerCount = Math.min(visibleSpeakerCount, speakers.length);
+ const visibleSpeakers = getVisibleSpeakers(
+ speakers,
+ normalizedActiveSpeakerIndex,
+ visibleSpeakerCount,
+ );
+ const hasScrollableSpeakers = speakers.length > renderedSpeakerCount;
+ const isTransitioning = transitionState !== "idle";
+ const speakerShowcaseClassName = className
+ ? `${styles.speakerShowcase} ${className}`
+ : styles.speakerShowcase;
+
+ const startSpeakerTransition = useCallback(
+ (nextSpeakerIndex: number, direction: SpeakerTransitionDirection) => {
+ if (!hasScrollableSpeakers || isTransitioning) return;
+
+ const normalizedNextSpeakerIndex = nextSpeakerIndex % speakers.length;
+
+ if (normalizedNextSpeakerIndex === normalizedActiveSpeakerIndex) return;
+
+ setPendingSpeakerIndex(normalizedNextSpeakerIndex);
+ setTransitionDirection(direction);
+ setTransitionState("exiting");
+ },
+ [
+ hasScrollableSpeakers,
+ isTransitioning,
+ normalizedActiveSpeakerIndex,
+ speakers.length,
+ ],
+ );
+
+ const showPreviousSpeaker = useCallback(() => {
+ if (!hasScrollableSpeakers) return;
+
+ startSpeakerTransition(
+ getPreviousSpeakerIndex(normalizedActiveSpeakerIndex, speakers.length),
+ "previous",
+ );
+ }, [
+ hasScrollableSpeakers,
+ normalizedActiveSpeakerIndex,
+ speakers.length,
+ startSpeakerTransition,
+ ]);
+
+ const showNextSpeaker = useCallback(() => {
+ if (!hasScrollableSpeakers) return;
+
+ startSpeakerTransition(
+ getNextSpeakerIndex(normalizedActiveSpeakerIndex, speakers.length),
+ "next",
+ );
+ }, [
+ hasScrollableSpeakers,
+ normalizedActiveSpeakerIndex,
+ speakers.length,
+ startSpeakerTransition,
+ ]);
+
+ useEffect(() => {
+ const visibleSpeakersQuery = window.matchMedia(
+ LARGE_DESKTOP_VISIBLE_SPEAKERS_QUERY,
+ );
+ const updateVisibleSpeakerCount = () => {
+ setVisibleSpeakerCount(
+ visibleSpeakersQuery.matches
+ ? LARGE_DESKTOP_VISIBLE_SPEAKER_COUNT
+ : MOBILE_VISIBLE_SPEAKER_COUNT,
+ );
+ };
+
+ updateVisibleSpeakerCount();
+ visibleSpeakersQuery.addEventListener("change", updateVisibleSpeakerCount);
+
+ return () => {
+ visibleSpeakersQuery.removeEventListener(
+ "change",
+ updateVisibleSpeakerCount,
+ );
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!hasScrollableSpeakers || isTransitioning) return;
+
+ const timeoutId = window.setTimeout(showNextSpeaker, AUTO_SCROLL_DELAY_MS);
+
+ return () => window.clearTimeout(timeoutId);
+ }, [
+ autoScrollResetKey,
+ hasScrollableSpeakers,
+ isTransitioning,
+ normalizedActiveSpeakerIndex,
+ showNextSpeaker,
+ ]);
+
+ useEffect(() => {
+ if (transitionState === "exiting" && pendingSpeakerIndex !== null) {
+ const timeoutId = window.setTimeout(() => {
+ setActiveSpeakerIndex(pendingSpeakerIndex);
+ setPendingSpeakerIndex(null);
+ setTransitionState("entering");
+ }, SPEAKER_EXIT_MS);
+
+ return () => window.clearTimeout(timeoutId);
+ }
+
+ if (transitionState === "entering") {
+ const timeoutId = window.setTimeout(() => {
+ setTransitionState("idle");
+ }, SPEAKER_ENTER_MS);
+
+ return () => window.clearTimeout(timeoutId);
+ }
+ }, [pendingSpeakerIndex, transitionState]);
+
+ if (visibleSpeakers.length === 0) {
+ return null;
+ }
+
+ const resetAutoScrollTimer = () => {
+ setAutoScrollResetKey((currentKey) => currentKey + 1);
+ };
+
+ const handlePreviousSpeaker = () => {
+ resetAutoScrollTimer();
+ showPreviousSpeaker();
+ };
+
+ const handleNextSpeaker = () => {
+ resetAutoScrollTimer();
+ showNextSpeaker();
+ };
+
+ return (
+
+
+ {title}
+
+
+
+
+
+
+
+
+ {visibleSpeakers.map(({ index, speaker }) => (
+
+
+
+
+ {speaker.companyRole && (
+
{speaker.companyRole}
+ )}
+
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/khix/src/app/_components/sections/speakers/index.ts b/apps/khix/src/app/_components/sections/speakers/index.ts
new file mode 100644
index 000000000..11c208e1b
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/speakers/index.ts
@@ -0,0 +1,3 @@
+export { PondAtmosphere } from "./PondAtmosphere";
+export { SpeakerShowcase } from "./SpeakerShowcase";
+export { KHIX_SPEAKERS, type SpeakerShowcaseSpeaker } from "./speakers";
diff --git a/apps/khix/src/app/_components/sections/speakers/speakers.ts b/apps/khix/src/app/_components/sections/speakers/speakers.ts
new file mode 100644
index 000000000..48b3ed874
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/speakers/speakers.ts
@@ -0,0 +1,44 @@
+export interface SpeakerShowcaseSpeaker {
+ name: string;
+ imageSrc: string;
+ companyRole?: string;
+ linkedinUrl?: string;
+}
+
+const SPEAKER_PLACEHOLDER_IMAGE =
+ "https://assets.knighthacks.org/khix/speaker-holder.webp";
+
+export const KHIX_SPEAKERS = [
+ {
+ name: "Coming soon",
+ imageSrc: SPEAKER_PLACEHOLDER_IMAGE,
+ },
+ {
+ name: "Coming soon",
+ imageSrc: SPEAKER_PLACEHOLDER_IMAGE,
+ },
+ {
+ name: "Coming soon",
+ imageSrc: SPEAKER_PLACEHOLDER_IMAGE,
+ },
+ {
+ name: "Coming soon",
+ imageSrc: SPEAKER_PLACEHOLDER_IMAGE,
+ },
+ {
+ name: "Coming soon",
+ imageSrc: SPEAKER_PLACEHOLDER_IMAGE,
+ },
+ {
+ name: "Coming soon",
+ imageSrc: SPEAKER_PLACEHOLDER_IMAGE,
+ },
+ {
+ name: "Coming soon",
+ imageSrc: SPEAKER_PLACEHOLDER_IMAGE,
+ },
+ {
+ name: "Coming soon",
+ imageSrc: SPEAKER_PLACEHOLDER_IMAGE,
+ },
+] satisfies readonly SpeakerShowcaseSpeaker[];
diff --git a/apps/khix/src/app/_components/sections/sponsor-team/SponsorTeamSection.module.css b/apps/khix/src/app/_components/sections/sponsor-team/SponsorTeamSection.module.css
new file mode 100644
index 000000000..6c4452ad1
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/sponsor-team/SponsorTeamSection.module.css
@@ -0,0 +1,856 @@
+.waterfallShowcase {
+ --composition-width: min(92vw, 72rem);
+ --water-break-filter: drop-shadow(0 -0.1rem 0.035rem rgb(235 255 253 / 0.62))
+ drop-shadow(0 -0.24rem 0.15rem rgb(157 238 234 / 0.32));
+ --waterfall-width: 100vw;
+ --waterfall-art-height: calc(var(--waterfall-width) * 4.1667);
+ --waterfall-artwork-height: calc(var(--waterfall-width) * 4.1666667);
+ --waterfall-artwork-top: min(
+ 0px,
+ calc(var(--waterfall-art-height) - var(--waterfall-artwork-height))
+ );
+ --waterfall-fallback-color: #2d3336;
+ --waterfall-basin-blend-start: 89.5%;
+ --waterfall-basin-blend-end: 93.5%;
+ --rock-separator-height: 59.9306vw;
+ --team-panel-top: calc(
+ var(--waterfall-scene-top) + var(--waterfall-art-height) * 0.458
+ );
+ --team-separator-height: calc(var(--waterfall-width) * 0.407);
+ --team-separator-top: calc(
+ var(--waterfall-scene-top) + var(--waterfall-art-height) * 0.38
+ );
+ --waterfall-scene-top: 8vw;
+ --waterfall-height: calc(
+ var(--waterfall-scene-top) + var(--waterfall-art-height)
+ );
+
+ position: relative;
+ z-index: 2;
+ min-height: var(--waterfall-height);
+ margin-top: -8vw;
+ background: transparent;
+ isolation: isolate;
+ overflow: hidden;
+}
+
+.waterfallTransition {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: 2;
+ height: var(--rock-separator-height);
+ pointer-events: none;
+}
+
+.sponsorRockSeparator {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: 2;
+ height: var(--rock-separator-height);
+ background: url("https://assets.knighthacks.org/khix/sponsor-rock-separator.webp")
+ center top / 100% auto no-repeat;
+}
+
+.scene {
+ position: absolute;
+ top: var(--waterfall-scene-top);
+ right: 0;
+ left: 0;
+ z-index: 1;
+ width: var(--waterfall-width);
+ height: var(--waterfall-art-height);
+ overflow: hidden;
+ background-color: var(--waterfall-fallback-color);
+ box-shadow:
+ 0 0 0 1px rgba(83, 232, 255, 0.16),
+ 0 0 3rem rgba(20, 238, 231, 0.18);
+}
+
+.scene::after {
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+ content: "";
+ background:
+ radial-gradient(
+ ellipse at 50% 9%,
+ rgba(3, 25, 31, 0.34) 0%,
+ rgba(3, 25, 31, 0.2) 18%,
+ transparent 36%
+ ),
+ linear-gradient(
+ 90deg,
+ rgba(61, 69, 72, 0.34),
+ transparent 12%,
+ transparent 88%,
+ rgba(61, 69, 72, 0.34)
+ );
+ pointer-events: none;
+}
+
+.sceneAsset {
+ position: absolute;
+ left: 0;
+ width: var(--waterfall-width);
+ pointer-events: none;
+}
+
+.rocksTop,
+.rocksBottom {
+ z-index: 0;
+ background-position: center top;
+ background-repeat: no-repeat;
+ background-size: 100% auto;
+}
+
+.rocksTop {
+ top: var(--waterfall-artwork-top);
+ height: calc(var(--waterfall-width) * 2.0854167);
+ background-image: url("https://assets.knighthacks.org/khix/rocks-waterfall-top.webp");
+}
+
+.rocksBottom {
+ top: calc(var(--waterfall-artwork-top) + var(--waterfall-width) * 2.08125);
+ height: calc(var(--waterfall-width) * 2.0854167);
+ background-image: url("https://assets.knighthacks.org/khix/rocks-waterfall-bottom.webp");
+}
+
+.waterfallOne,
+.waterfallTwo,
+.waterfallThree {
+ z-index: 1;
+ background-position: center top;
+ background-repeat: no-repeat;
+ background-size: 100% auto;
+}
+
+.waterfallOne {
+ top: var(--waterfall-artwork-top);
+ height: calc(var(--waterfall-width) * 1.3916667);
+ background-image: url("https://assets.knighthacks.org/khix/waterfall-animated-one.webp");
+}
+
+.waterfallTwo {
+ top: calc(var(--waterfall-artwork-top) + var(--waterfall-width) * 1.3861111);
+ height: calc(var(--waterfall-width) * 1.3944444);
+ background-image: url("https://assets.knighthacks.org/khix/waterfall-animated-two.webp");
+}
+
+.waterfallThree {
+ top: calc(var(--waterfall-artwork-top) + var(--waterfall-width) * 2.775);
+ height: calc(var(--waterfall-width) * 1.3916667);
+ background-image: url("https://assets.knighthacks.org/khix/waterfall-animated-three.webp");
+}
+
+.waterfallAtmosphere,
+.waterfallForegroundAtmosphere {
+ position: absolute;
+ top: var(--waterfall-scene-top);
+ left: 0;
+ width: var(--waterfall-width);
+ height: var(--waterfall-art-height);
+ overflow: hidden;
+ pointer-events: none;
+ contain: layout paint style;
+}
+
+.waterfallAtmosphere {
+ z-index: 2;
+}
+
+.waterfallForegroundAtmosphere {
+ z-index: 5;
+}
+
+.dropletLayer {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+}
+
+.droplet {
+ position: absolute;
+ top: var(--droplet-y);
+ left: var(--droplet-x);
+ width: var(--droplet-width);
+ height: var(--droplet-height);
+ border-radius: 999px;
+ background: linear-gradient(
+ 180deg,
+ rgb(236 255 253 / 0.18),
+ rgb(245 255 254 / 0.78) 34%,
+ rgb(255 255 255 / 0.98) 68%,
+ rgb(116 218 222 / 0.42)
+ );
+ box-shadow:
+ 0 0 0.3rem rgb(234 255 253 / 0.72),
+ 0 0.12rem 0.16rem rgb(8 91 102 / 0.42);
+ opacity: 0;
+ transform: translate3d(0, -0.45rem, 0) scale(0.72);
+ animation: waterfallDropletFall var(--droplet-duration) linear
+ var(--droplet-delay) infinite;
+ animation-play-state: paused;
+}
+
+.droplet[data-shape="bead"] {
+ border-radius: 50%;
+ background: radial-gradient(
+ circle at 35% 30%,
+ rgb(255 255 255),
+ rgb(213 255 252 / 0.94) 42%,
+ rgb(71 183 194 / 0.34) 78%
+ );
+ animation-timing-function: ease-in-out;
+}
+
+.waterfallAtmosphere[data-active="true"] .droplet,
+.waterfallForegroundAtmosphere[data-active="true"] .droplet {
+ animation-play-state: running;
+}
+
+.bridge {
+ position: absolute;
+ top: var(--team-separator-top);
+ left: 50%;
+ z-index: 3;
+ width: 100vw;
+ height: var(--team-separator-height);
+ background: url("https://assets.knighthacks.org/khix/separator-rocks.webp")
+ center top / 100% auto no-repeat;
+ filter: drop-shadow(0 -0.35rem 0.7rem rgba(3, 7, 12, 0.22))
+ drop-shadow(0 0.5rem 0.65rem rgba(2, 4, 8, 0.28));
+ transform: translateX(-50%);
+}
+
+.content {
+ position: relative;
+ z-index: 4;
+ width: min(100%, 58rem);
+ min-height: var(--waterfall-height);
+ margin: 0 auto;
+}
+
+.sponsorPanel {
+ min-height: calc(
+ var(--waterfall-scene-top) + var(--waterfall-art-height) * 0.5
+ );
+ padding: 0 clamp(1rem, calc(var(--composition-width) * 0.06), 4rem);
+}
+
+.sponsorShowcase {
+ --sponsor-showcase-width: calc(var(--composition-width) * 0.86);
+ --sponsor-showcase-padding: 0;
+ --sponsor-showcase-mobile-padding-bottom: 0;
+ --sponsor-showcase-mobile-padding-top: 0;
+ --sponsor-title-color: rgba(255, 252, 245, 0.96);
+ --sponsor-title-font: var(--font-cormorant-garamond, Georgia), Georgia, serif;
+ --sponsor-title-line-height: 0.84;
+ --sponsor-title-height: calc(var(--sponsor-title-rock-width) * 0.219064);
+ --sponsor-title-margin: 0 auto
+ calc(var(--rock-separator-height) - var(--sponsor-title-height));
+ --sponsor-title-mobile-margin-bottom: calc(
+ var(--rock-separator-height) - var(--sponsor-title-height)
+ );
+ --sponsor-title-offset-y: calc(
+ var(--rock-separator-height) * 0.46 - var(--sponsor-title-height) * 0.5
+ );
+ --sponsor-title-rock-display: none;
+ --sponsor-title-rock-ratio: 1196 / 262;
+ --sponsor-title-rock-width: min(
+ 90vw,
+ clamp(20.5rem, calc(var(--composition-width) * 0.54), 38rem)
+ );
+ --sponsor-title-shadow:
+ 0 0 0.5rem rgba(235, 251, 255, 0.54), 0 0 2.2rem rgba(129, 225, 255, 0.3);
+ --sponsor-title-size: clamp(3.4rem, 8vw, 6.8rem);
+ --sponsor-title-transform: none;
+ --sponsor-title-weight: 400;
+ --sponsor-title-text-width: 100%;
+ --sponsor-field-gap: clamp(
+ 2.75rem,
+ calc(var(--composition-width) * 0.052),
+ 4rem
+ );
+ --sponsor-slab-gap: clamp(
+ 0.6rem,
+ calc(var(--composition-width) * 0.014),
+ 1rem
+ );
+ --sponsor-medium-grid-gap: clamp(
+ 0.75rem,
+ calc(var(--composition-width) * 0.018),
+ 1.3rem
+ )
+ clamp(1.2rem, calc(var(--composition-width) * 0.04), 2.6rem);
+ --sponsor-medium-grid-padding-top: 0;
+ --sponsor-medium-grid-width: calc(var(--composition-width) * 0.68);
+ --sponsor-small-grid-gap: clamp(
+ 0.65rem,
+ calc(var(--composition-width) * 0.014),
+ 1.05rem
+ )
+ clamp(0.8rem, calc(var(--composition-width) * 0.025), 1.65rem);
+ --sponsor-small-grid-width: calc(var(--composition-width) * 0.7);
+ --sponsor-stone-center-offset: -0.28rem;
+ --sponsor-stone-edge-offset: 0.2rem;
+ --sponsor-stone-left-offset: -0.38rem;
+ --sponsor-stone-right-offset: 0.38rem;
+ --sponsor-xl-height: clamp(
+ 10rem,
+ calc(var(--composition-width) * 0.205),
+ 14.75rem
+ );
+ --sponsor-xl-logo-height: clamp(
+ 2.7rem,
+ calc(var(--composition-width) * 0.045),
+ 3.25rem
+ );
+ --sponsor-xl-logo-width: clamp(
+ 20rem,
+ calc(var(--composition-width) * 0.4),
+ 28.8rem
+ );
+ --sponsor-xl-width: clamp(
+ 34rem,
+ calc(var(--composition-width) * 0.62),
+ 44.5rem
+ );
+ --sponsor-large-height: clamp(
+ 8.8rem,
+ calc(var(--composition-width) * 0.17),
+ 12.25rem
+ );
+ --sponsor-large-logo-height: clamp(
+ 2.35rem,
+ calc(var(--composition-width) * 0.038),
+ 2.75rem
+ );
+ --sponsor-large-logo-width: clamp(
+ 16rem,
+ calc(var(--composition-width) * 0.32),
+ 23rem
+ );
+ --sponsor-large-width: clamp(
+ 26rem,
+ calc(var(--composition-width) * 0.48),
+ 34.5rem
+ );
+ --sponsor-medium-height: clamp(
+ 8.2rem,
+ calc(var(--composition-width) * 0.158),
+ 11.4rem
+ );
+ --sponsor-medium-logo-height: clamp(
+ 1.75rem,
+ calc(var(--composition-width) * 0.03),
+ 2.15rem
+ );
+ --sponsor-medium-logo-width: clamp(
+ 9.4rem,
+ calc(var(--composition-width) * 0.172),
+ 12.4rem
+ );
+ --sponsor-medium-width: clamp(
+ 12.8rem,
+ calc(var(--composition-width) * 0.225),
+ 16.2rem
+ );
+ --sponsor-medium-mobile-height: 8.15rem;
+ --sponsor-medium-mobile-logo-height: 1.68rem;
+ --sponsor-medium-mobile-logo-width: 5.9rem;
+ --sponsor-medium-mobile-width: min(45vw, 10.6rem);
+ --sponsor-small-height: clamp(
+ 7.4rem,
+ calc(var(--composition-width) * 0.128),
+ 9.2rem
+ );
+ --sponsor-small-logo-height: clamp(
+ 1.7rem,
+ calc(var(--composition-width) * 0.026),
+ 1.9rem
+ );
+ --sponsor-small-logo-width: clamp(
+ 7.2rem,
+ calc(var(--composition-width) * 0.145),
+ 10.4rem
+ );
+ --sponsor-small-width: clamp(
+ 10rem,
+ calc(var(--composition-width) * 0.18),
+ 13rem
+ );
+ --sponsor-xs-height: clamp(
+ 6.6rem,
+ calc(var(--composition-width) * 0.105),
+ 7.55rem
+ );
+ --sponsor-xs-logo-height: clamp(
+ 1.45rem,
+ calc(var(--composition-width) * 0.022),
+ 1.62rem
+ );
+ --sponsor-xs-logo-width: clamp(
+ 6.1rem,
+ calc(var(--composition-width) * 0.12),
+ 8.6rem
+ );
+ --sponsor-xs-width: clamp(
+ 8.5rem,
+ calc(var(--composition-width) * 0.145),
+ 10.4rem
+ );
+ --sponsor-logo-opacity: 0.58;
+ --sponsor-logo-hover-opacity: 0.72;
+ --sponsor-logo-backlight-strength: 0;
+ --sponsor-logo-filter: brightness(0) saturate(100%)
+ drop-shadow(0 -1px 0.2px rgb(3 19 25 / 0.76))
+ drop-shadow(0 1px 0.35px rgb(190 224 218 / 0.38));
+ --sponsor-logo-hover-filter: brightness(0) saturate(100%)
+ drop-shadow(0 -1px 0.2px rgb(3 19 25 / 0.82))
+ drop-shadow(0 1px 0.4px rgb(203 234 228 / 0.5));
+ --sponsor-logo-contrast-filter: grayscale(1) sepia(0.32) saturate(0.68)
+ hue-rotate(126deg) brightness(0.54) contrast(1.08)
+ drop-shadow(0 -1px 0.2px rgb(3 19 25 / 0.76))
+ drop-shadow(0 1px 0.35px rgb(190 224 218 / 0.38));
+ --sponsor-logo-contrast-hover-filter: grayscale(1) sepia(0.32) saturate(0.68)
+ hue-rotate(126deg) brightness(0.58) contrast(1.1)
+ drop-shadow(0 -1px 0.2px rgb(3 19 25 / 0.82))
+ drop-shadow(0 1px 0.4px rgb(203 234 228 / 0.5));
+ --sponsor-logo-mobile-filter: var(--sponsor-logo-filter);
+ --sponsor-logo-hover-animation: none;
+ --sponsor-logo-mobile-animation: none;
+ --sponsor-logo-will-change: auto;
+ --sponsor-rock-hover-animation: none;
+ --sponsor-rock-will-change: auto;
+ --sponsor-rock-surface-filter: saturate(0.58) brightness(0.78) contrast(1.1)
+ hue-rotate(4deg);
+ --sponsor-rock-surface-hover-filter: saturate(0.72) brightness(0.78)
+ contrast(1.14) hue-rotate(4deg);
+ --sponsor-rock-shadow: var(--water-break-filter)
+ drop-shadow(0 0.12rem 0.1rem rgb(2 20 27 / 0.4))
+ drop-shadow(0 0.42rem 0.3rem rgb(2 17 23 / 0.3));
+ --sponsor-rock-focus-shadow: var(--water-break-filter)
+ drop-shadow(0 0.12rem 0.1rem rgb(2 20 27 / 0.42))
+ drop-shadow(0 0 0.14rem rgb(var(--tier-glow-end-rgb) / 0.92))
+ drop-shadow(0 0 0.38rem rgb(var(--tier-glow-start-rgb) / 0.56));
+}
+
+.sectionTitle {
+ position: relative;
+ z-index: 2;
+ margin: 0;
+ color: #081820;
+ font-family: var(--font-cormorant-garamond, Georgia), Georgia, serif;
+ font-size: clamp(1rem, calc(var(--composition-width) * 0.026), 1.55rem);
+ font-weight: 400;
+ line-height: 1;
+ letter-spacing: 0;
+ text-align: center;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.38);
+ text-transform: uppercase;
+}
+
+.partnerGroup {
+ width: calc(var(--composition-width) * 0.54);
+ margin: calc(var(--composition-width) * 0.105) auto 0;
+}
+
+.partnerGroup .sectionTitle {
+ color: rgba(255, 252, 245, 0.96);
+ font-size: clamp(1.7rem, 4vw, 3.4rem);
+ line-height: 0.84;
+ text-shadow:
+ 0 0 0.5rem rgba(235, 251, 255, 0.54),
+ 0 0 2.2rem rgba(129, 225, 255, 0.3);
+ text-transform: none;
+ transform: translateY(clamp(-1.5rem, -2vw, -0.65rem));
+}
+
+.partnerGrid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: calc(var(--composition-width) * 0.018)
+ calc(var(--composition-width) * 0.035);
+ margin-top: calc(var(--composition-width) * 0.03);
+}
+
+.partnerStone {
+ --partner-logo-hover-scale: 1.035;
+ --partner-logo-scale: 1;
+
+ position: relative;
+ display: block;
+ height: calc(var(--composition-width) * 0.125);
+ color: inherit;
+ text-decoration: none;
+ outline: none;
+ isolation: isolate;
+}
+
+.partnerVisual {
+ position: absolute;
+ inset: 0;
+ display: grid;
+ place-items: center;
+ filter: var(--water-break-filter)
+ drop-shadow(0 0.28rem 0.2rem rgb(5 28 35 / 0.28));
+ overflow: visible;
+ transform-origin: 50% 58%;
+}
+
+.partnerRock {
+ position: relative;
+ z-index: 0;
+ display: block;
+ width: 100%;
+ aspect-ratio: 1196 / 318;
+ overflow: hidden;
+ pointer-events: none;
+}
+
+.partnerRock::before {
+ position: absolute;
+ top: -70%;
+ left: -11.96%;
+ display: block;
+ width: 125.42%;
+ aspect-ratio: 1;
+ content: "";
+ background: url("https://assets.knighthacks.org/khix/large-sponsor-rock.webp")
+ 0 0 / 100% auto no-repeat;
+ filter: saturate(0.58) brightness(0.78) contrast(1.1) hue-rotate(4deg);
+ pointer-events: none;
+ transition: filter 180ms ease;
+ user-select: none;
+}
+
+.partnerLogo {
+ position: absolute;
+ top: 44%;
+ left: 50%;
+ z-index: 1;
+ display: block;
+ width: min(78%, 14.5rem);
+ height: clamp(1rem, calc(var(--composition-width) * 0.043), 2rem);
+ opacity: 0.58;
+ transform: translate3d(-50%, -50%, 0) scale(var(--partner-logo-scale));
+ transition:
+ opacity 180ms ease,
+ transform 180ms ease;
+}
+
+.partnerLogoImage {
+ object-fit: contain;
+ object-position: center;
+ filter: brightness(0) saturate(100%)
+ drop-shadow(0 -1px 0.2px rgb(3 19 25 / 0.76))
+ drop-shadow(0 1px 0.35px rgb(190 224 218 / 0.38));
+ pointer-events: none;
+ transition: filter 180ms ease;
+ user-select: none;
+}
+
+.partnerStone[data-preserve-logo-contrast="true"] .partnerLogoImage {
+ filter: grayscale(1) sepia(0.32) saturate(0.68) hue-rotate(126deg)
+ brightness(0.54) contrast(1.08)
+ drop-shadow(0 -1px 0.2px rgb(3 19 25 / 0.76))
+ drop-shadow(0 1px 0.35px rgb(190 224 218 / 0.38));
+}
+
+.partnerStone:hover .partnerVisual,
+.partnerStone:focus-visible .partnerVisual {
+ animation: none;
+}
+
+.partnerStone:hover .partnerRock::before,
+.partnerStone:focus-visible .partnerRock::before {
+ filter: saturate(0.6) brightness(0.82) contrast(1.12) hue-rotate(4deg);
+}
+
+.partnerStone:hover .partnerLogo,
+.partnerStone:focus-visible .partnerLogo {
+ opacity: 0.72;
+ transform: translate3d(-50%, -50%, 0) scale(var(--partner-logo-scale));
+}
+
+.partnerStone:hover .partnerLogoImage,
+.partnerStone:focus-visible .partnerLogoImage {
+ filter: brightness(0) saturate(100%)
+ drop-shadow(0 -1px 0.2px rgb(3 19 25 / 0.82))
+ drop-shadow(0 1px 0.4px rgb(203 234 228 / 0.5));
+}
+
+.partnerStone[data-preserve-logo-contrast="true"]:hover .partnerLogoImage,
+.partnerStone[data-preserve-logo-contrast="true"]:focus-visible
+ .partnerLogoImage {
+ filter: grayscale(1) sepia(0.32) saturate(0.68) hue-rotate(126deg)
+ brightness(0.58) contrast(1.1) drop-shadow(0 -1px 0.2px rgb(3 19 25 / 0.82))
+ drop-shadow(0 1px 0.4px rgb(203 234 228 / 0.5));
+}
+
+.partnerStone:focus-visible .partnerVisual {
+ filter: var(--water-break-filter)
+ drop-shadow(0 0.12rem 0.1rem rgb(2 20 27 / 0.42))
+ drop-shadow(0 0 0.24rem rgb(230 255 249 / 0.94))
+ drop-shadow(0 0 0.7rem rgb(105 224 218 / 0.78));
+}
+
+.teamPanel {
+ position: absolute;
+ top: var(--team-panel-top);
+ left: 50%;
+ width: calc(var(--composition-width) * 0.9);
+ scroll-margin-top: calc(var(--team-panel-top) - var(--team-separator-top));
+ transform: translateX(-50%);
+}
+
+.teamPanel .sectionTitle {
+ margin-bottom: calc(var(--composition-width) * 0.015);
+ color: rgba(255, 252, 245, 0.96);
+ font-family: var(--font-cormorant-garamond, Georgia), Georgia, serif;
+ font-size: clamp(3.4rem, 8vw, 6.8rem);
+ font-weight: 400;
+ line-height: 0.84;
+ letter-spacing: 0;
+ text-shadow:
+ 0 0 0.5rem rgba(235, 251, 255, 0.54),
+ 0 0 2.2rem rgba(129, 225, 255, 0.3);
+ text-transform: none;
+ transform: translateY(
+ calc(
+ var(--team-separator-top) + var(--team-separator-height) * 0.46 -
+ var(--team-panel-top) - 50%
+ )
+ );
+}
+
+.teamCascade {
+ --team-cascade-height: calc(var(--waterfall-art-height) * 0.5);
+ --team-cascade-avatar-size: clamp(
+ 4.75rem,
+ calc(var(--composition-width) * 0.09),
+ 6.25rem
+ );
+ --team-status-color: rgba(235, 255, 255, 0.78);
+
+ width: 100%;
+ margin: 0 auto;
+}
+
+@media (min-width: 761px) {
+ .dropletLayer {
+ clip-path: inset(7% 0 0);
+ }
+}
+
+@media (min-width: 1181px) {
+ .waterfallShowcase {
+ --waterfall-art-height: calc(var(--waterfall-width) * 2.6);
+ --team-panel-top: calc(
+ var(--waterfall-scene-top) + var(--waterfall-art-height) * 0.7
+ );
+ --team-separator-top: calc(
+ var(--waterfall-scene-top) + var(--waterfall-art-height) * 0.62
+ );
+ }
+
+ .sponsorPanel {
+ padding-top: 5vw;
+ }
+
+ .sponsorShowcase {
+ --sponsor-title-margin: 0 auto
+ max(
+ 8rem,
+ calc(var(--rock-separator-height) - var(--sponsor-title-height) - 20vw)
+ );
+ }
+
+ .partnerGroup {
+ margin-top: calc(var(--composition-width) * 0.105 + 3vw);
+ }
+
+ .partnerGroup .sectionTitle {
+ text-shadow:
+ 0 0.22rem 0.32rem rgba(0, 0, 0, 0.88),
+ 0 0 0.5rem rgba(235, 251, 255, 0.54),
+ 0 0 2.2rem rgba(129, 225, 255, 0.3);
+ }
+
+ .partnerGrid {
+ row-gap: calc(var(--composition-width) * 0.01);
+ }
+
+ .partnerStone {
+ height: calc(var(--composition-width) * 0.1);
+ }
+
+ .bridge {
+ top: calc(var(--team-separator-top) - 20vw);
+ }
+
+ .teamPanel {
+ top: calc(var(--team-panel-top) - 25vw);
+ }
+
+ .teamCascade {
+ transform: translateY(12vw);
+ }
+
+ .teamPanel .sectionTitle {
+ translate: 0 6.6vw;
+ }
+}
+
+@media (min-width: 761px) and (max-width: 1024px) {
+ .sponsorShowcase {
+ --sponsor-small-grid-width: calc(var(--composition-width) * 0.78);
+ }
+
+ .partnerGroup {
+ width: calc(var(--composition-width) * 0.78);
+ }
+
+ .partnerGrid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 760px) {
+ .droplet[data-mobile-hidden="true"] {
+ display: none;
+ }
+
+ .sponsorShowcase {
+ --sponsor-stone-center-offset: 0;
+ --sponsor-stone-edge-offset: 0;
+ --sponsor-stone-left-offset: 0;
+ --sponsor-stone-right-offset: 0;
+ }
+
+ .partnerStone:hover .partnerLogo,
+ .partnerStone:focus-visible .partnerLogo {
+ transform: translate3d(-50%, -50%, 0) scale(var(--partner-logo-hover-scale));
+ }
+
+ .teamPanel {
+ width: calc(var(--composition-width) * 0.82);
+ }
+}
+
+@media (max-width: 520px) {
+ .waterfallShowcase {
+ --team-mobile-section-lift: -1.75rem;
+ --team-mobile-panel-lift: 0rem;
+ --team-panel-top: calc(
+ var(--waterfall-scene-top) +
+ min(calc(var(--waterfall-art-height) * 0.69), 70rem) +
+ var(--team-mobile-section-lift) + var(--team-mobile-panel-lift)
+ );
+ --team-separator-top: calc(
+ var(--waterfall-scene-top) +
+ min(calc(var(--waterfall-art-height) * 0.64), 66rem) +
+ var(--team-mobile-section-lift)
+ );
+ }
+
+ .sponsorShowcase {
+ --sponsor-showcase-width: 100%;
+ --sponsor-title-size: clamp(3rem, 16vw, 4.25rem);
+ --sponsor-field-gap: clamp(0.75rem, 3vw, 1rem);
+ --sponsor-field-offset-y: clamp(-1.5rem, -5vw, -1rem);
+ --sponsor-medium-mobile-height: clamp(6.7rem, 31vw, 7.25rem);
+ --sponsor-medium-mobile-logo-height: 1.5rem;
+ --sponsor-medium-mobile-logo-width: 5.4rem;
+ --sponsor-medium-mobile-width: min(47vw, 9.4rem);
+ }
+
+ .partnerGroup {
+ width: calc(var(--composition-width) * 0.82);
+ margin-top: clamp(1.25rem, 5vw, 1.75rem);
+ }
+
+ .partnerGroup .sectionTitle {
+ font-size: clamp(1.55rem, 9.5vw, 2.5rem);
+ }
+
+ .teamPanel .sectionTitle {
+ font-size: clamp(3rem, 16vw, 4.25rem);
+ }
+
+ .teamPanel {
+ width: calc(var(--composition-width) * 0.82);
+ }
+
+ .teamCascade {
+ --team-cascade-height: min(calc(var(--waterfall-art-height) * 0.2), 20rem);
+ --team-cascade-avatar-size: clamp(
+ 3.15rem,
+ calc(var(--composition-width) * 0.16),
+ 3.8rem
+ );
+ }
+}
+
+@media (max-width: 420px) {
+ .sponsorPanel {
+ padding-right: 0.75rem;
+ padding-left: 0.75rem;
+ }
+}
+
+@media (max-width: 390px) {
+ .waterfallShowcase {
+ --team-mobile-panel-lift: -1.75rem;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .droplet {
+ display: none;
+ animation: none;
+ }
+
+ .partnerRock::before,
+ .partnerLogo,
+ .partnerLogoImage {
+ transition: none;
+ }
+
+ .partnerVisual,
+ .partnerStone:hover .partnerVisual,
+ .partnerStone:focus-visible .partnerVisual {
+ animation: none;
+ }
+}
+
+@keyframes waterfallDropletFall {
+ 0% {
+ opacity: 0;
+ transform: translate3d(0, -0.45rem, 0) scale(0.72);
+ }
+
+ 14% {
+ opacity: var(--droplet-opacity);
+ }
+
+ 48% {
+ opacity: var(--droplet-opacity);
+ transform: translate3d(
+ var(--droplet-mid-drift),
+ var(--droplet-mid-travel),
+ 0
+ )
+ scale(0.9);
+ }
+
+ 100% {
+ opacity: 0;
+ transform: translate3d(var(--droplet-drift), var(--droplet-travel), 0)
+ scale(1.04);
+ }
+}
diff --git a/apps/khix/src/app/_components/sections/sponsor-team/SponsorTeamSection.tsx b/apps/khix/src/app/_components/sections/sponsor-team/SponsorTeamSection.tsx
new file mode 100644
index 000000000..fe3e5a0e1
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/sponsor-team/SponsorTeamSection.tsx
@@ -0,0 +1,217 @@
+import Image from "next/image";
+
+import type { SponsorShowcaseSponsor } from "../../sponsor-showcase";
+import { AssetCredit } from "../../assets";
+import { SponsorShowcase } from "../../sponsor-showcase";
+import { TeamCascade } from "../../team-cascade";
+import styles from "./SponsorTeamSection.module.css";
+import { WaterfallAtmosphere } from "./WaterfallAtmosphere";
+
+const HOMEPAGE_SPONSORS = [
+ {
+ name: "OneEthos",
+ websiteUrl: "https://www.oneethos.com/",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-oneethos.svg",
+ logoScale: 1,
+ mobileLogoScale: 1.2,
+ tier: "golden-dawn",
+ },
+ {
+ name: "AMD",
+ websiteUrl: "https://www.amd.com/",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-amd.svg",
+ tier: "silver-moon",
+ },
+ {
+ name: "BNY",
+ websiteUrl: "https://www.bny.com/",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-bny.svg",
+ tier: "silver-moon",
+ },
+ {
+ name: "Databricks",
+ websiteUrl: "https://www.databricks.com/",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-databricks.svg",
+ logoScale: 1.65,
+ mobileLogoScale: 1.65,
+ tier: "silver-moon",
+ },
+ {
+ name: "Morgan & Morgan",
+ websiteUrl: "https://www.forthepeople.com/",
+ logoSrc:
+ "https://assets.knighthacks.org/khix/sponsor-morgan-and-morgan.svg",
+ logoScale: 0.94,
+ mobileLogoScale: 1.15,
+ preserveLogoContrast: true,
+ tier: "silver-moon",
+ },
+ {
+ name: "NextEra",
+ websiteUrl: "https://www.nexteraenergy.com/",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-nextera.svg",
+ logoScale: 1.82,
+ mobileLogoScale: 1.75,
+ tier: "silver-moon",
+ },
+ {
+ name: "Shinies Props",
+ websiteUrl: "https://www.shinies.co/",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-shinies.svg",
+ logoScale: 1.24,
+ mobileLogoScale: 1.2,
+ tier: "silver-moon",
+ },
+ {
+ name: "Codex",
+ websiteUrl: "https://openai.com/codex/",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-codex.svg",
+ logoScale: 2,
+ mobileLogoScale: 1.7,
+ tier: "bronze-ember",
+ },
+ {
+ name: "Impress Ink",
+ websiteUrl: "https://impressink.com/",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-impressink.png",
+ logoScale: 1.25,
+ mobileLogoScale: 1,
+ tier: "bronze-ember",
+ },
+ {
+ name: "RFSmart",
+ websiteUrl: "https://www.rfsmart.com/",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-rfsmart.svg",
+ logoScale: 1.25,
+ mobileLogoScale: 1.2,
+ tier: "bronze-ember",
+ },
+] satisfies SponsorShowcaseSponsor[];
+
+const PARTNERS = [
+ {
+ name: "UCF College of Engineering and Computer Science",
+ logoSrc: "https://assets.knighthacks.org/khix/partner-cecs.svg",
+ websiteUrl: "https://www.cecs.ucf.edu/",
+ },
+ {
+ name: "Game Development Knights",
+ logoSrc: "https://assets.knighthacks.org/khix/partner-gdk.svg",
+ preserveLogoContrast: true,
+ websiteUrl: "https://www.instagram.com/gamedevknights/",
+ },
+ {
+ name: "Girls Who Code",
+ logoSrc: "https://assets.knighthacks.org/khix/partner-gwc.svg",
+ websiteUrl: "https://www.instagram.com/girlswhocodeucf/",
+ },
+ {
+ name: "IEEE at UCF",
+ logoSrc: "https://assets.knighthacks.org/khix/partner-ieee.svg",
+ websiteUrl: "https://www.instagram.com/ieeeucf/",
+ },
+ {
+ name: "Major League Hacking",
+ logoSrc: "https://assets.knighthacks.org/khix/partner-mlh.svg",
+ websiteUrl: "https://mlh.io/",
+ },
+ {
+ name: "Society of Asian Scientists and Engineers",
+ logoSrc: "https://assets.knighthacks.org/khix/partner-sase.svg",
+ websiteUrl: "https://www.instagram.com/saseucf/",
+ },
+] as const;
+
+export function SponsorTeamSection() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/khix/src/app/_components/sections/sponsor-team/WaterfallAtmosphere.tsx b/apps/khix/src/app/_components/sections/sponsor-team/WaterfallAtmosphere.tsx
new file mode 100644
index 000000000..71a801e53
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/sponsor-team/WaterfallAtmosphere.tsx
@@ -0,0 +1,210 @@
+"use client";
+
+import type { CSSProperties } from "react";
+import { useEffect, useRef } from "react";
+
+import styles from "./SponsorTeamSection.module.css";
+
+type DropletStyle = CSSProperties & {
+ "--droplet-delay": string;
+ "--droplet-drift": string;
+ "--droplet-duration": string;
+ "--droplet-height": string;
+ "--droplet-mid-drift": string;
+ "--droplet-mid-travel": string;
+ "--droplet-opacity": string;
+ "--droplet-travel": string;
+ "--droplet-width": string;
+ "--droplet-x": string;
+ "--droplet-y": string;
+};
+
+const BASE_DROPLET_COUNT = 384;
+const BASE_BUBBLE_INTERVAL = 5;
+const EXTRA_BUBBLE_COUNT = Math.ceil(BASE_DROPLET_COUNT / BASE_BUBBLE_INTERVAL);
+const BUBBLE_CURVE_STEP = 0.438447187;
+const BUBBLE_DIRECTION_STEP = 0.414213562;
+const BUBBLE_DISTANCE_STEP = 0.732050808;
+const BUBBLE_LIFESPAN_STEP = 0.569840291;
+const HORIZONTAL_DISTRIBUTION_STEP = 0.754877666;
+const VERTICAL_DISTRIBUTION_STEP = 0.618033989;
+
+function fractionalPart(value: number) {
+ return value - Math.floor(value);
+}
+
+function createDroplet({
+ bubble,
+ id,
+ mobileIndex,
+}: {
+ bubble: boolean;
+ id: number;
+ mobileIndex: number;
+}) {
+ const sequenceIndex = id + 1;
+ const distributionCycle = Math.floor(id / 12);
+ const x =
+ 18 + fractionalPart(sequenceIndex * HORIZONTAL_DISTRIBUTION_STEP) * 64;
+ const y = 3 + fractionalPart(sequenceIndex * VERTICAL_DISTRIBUTION_STEP) * 94;
+ const duration = bubble
+ ? 3.4 + fractionalPart(sequenceIndex * BUBBLE_LIFESPAN_STEP) * 5.6
+ : 2 + ((id * 7) % 13) / 10;
+ const delay = -((id * 0.73 + distributionCycle * 0.41) % duration);
+ const direction =
+ fractionalPart(sequenceIndex * BUBBLE_DIRECTION_STEP) * Math.PI * 2;
+ const bubbleDistance =
+ 80 + fractionalPart(sequenceIndex * BUBBLE_DISTANCE_STEP) * 120;
+ const drift = bubble
+ ? Math.cos(direction) * bubbleDistance
+ : ((id * 23) % 81) - 40;
+ const travel = bubble
+ ? Math.sin(direction) * bubbleDistance
+ : 190 + ((id * 29) % 151);
+ const curveDirection = id % 2 === 0 ? 1 : -1;
+ const curveDistance =
+ 20 + fractionalPart(sequenceIndex * BUBBLE_CURVE_STEP) * 38;
+ const midDrift = bubble
+ ? drift * 0.44 +
+ Math.cos(direction + Math.PI / 2) * curveDistance * curveDirection
+ : drift * 0.28;
+ const midTravel = bubble
+ ? travel * 0.44 +
+ Math.sin(direction + Math.PI / 2) * curveDistance * curveDirection
+ : travel * 0.24;
+ const width = bubble ? 3.6 + (id % 3) * 0.65 : 2.1 + (id % 4) * 0.48;
+ const height = bubble ? width : 17 + ((id * 7) % 18);
+ const opacity = 0.62 + ((id * 7) % 6) * 0.055;
+
+ return {
+ bead: bubble,
+ delay,
+ drift,
+ duration,
+ foreground: (id + distributionCycle) % 4 === 0,
+ height,
+ id,
+ midDrift,
+ midTravel,
+ mobileHidden: mobileIndex % 6 !== 0,
+ opacity,
+ travel,
+ width,
+ x,
+ y,
+ };
+}
+
+const BASE_DROPLETS = Array.from({ length: BASE_DROPLET_COUNT }, (_, index) =>
+ createDroplet({
+ bubble: index % BASE_BUBBLE_INTERVAL === 0,
+ id: index,
+ mobileIndex: index,
+ }),
+);
+
+const EXTRA_BUBBLES = Array.from(
+ { length: EXTRA_BUBBLE_COUNT },
+ (_, bubbleIndex) =>
+ createDroplet({
+ bubble: true,
+ id: BASE_DROPLET_COUNT + bubbleIndex,
+ mobileIndex: bubbleIndex,
+ }),
+);
+
+const DROPLETS = [...BASE_DROPLETS, ...EXTRA_BUBBLES];
+
+type WaterfallDroplet = (typeof DROPLETS)[number];
+
+const BACKGROUND_DROPLETS = DROPLETS.filter((droplet) => !droplet.foreground);
+const FOREGROUND_DROPLETS = DROPLETS.filter((droplet) => droplet.foreground);
+
+function DropletField({ droplets }: { droplets: readonly WaterfallDroplet[] }) {
+ return (
+
+ {droplets.map((droplet) => (
+
+ ))}
+
+ );
+}
+
+export function WaterfallAtmosphere() {
+ const atmosphereRef = useRef(null);
+ const foregroundRef = useRef(null);
+
+ useEffect(() => {
+ const atmosphere = atmosphereRef.current;
+ const foreground = foregroundRef.current;
+
+ if (!atmosphere || !foreground) {
+ return;
+ }
+
+ const setActive = (isActive: boolean) => {
+ const activeValue = isActive ? "true" : "false";
+
+ atmosphere.dataset.active = activeValue;
+ foreground.dataset.active = activeValue;
+ };
+
+ if (!("IntersectionObserver" in window)) {
+ setActive(true);
+ return;
+ }
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ setActive(entry?.isIntersecting ?? false);
+ },
+ { rootMargin: "20% 0px", threshold: 0.01 },
+ );
+
+ observer.observe(atmosphere);
+
+ return () => observer.disconnect();
+ }, []);
+
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/khix/src/app/_components/sections/sponsor-team/index.ts b/apps/khix/src/app/_components/sections/sponsor-team/index.ts
new file mode 100644
index 000000000..f468645d2
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/sponsor-team/index.ts
@@ -0,0 +1 @@
+export { SponsorTeamSection } from "./SponsorTeamSection";
diff --git a/apps/khix/src/app/_components/sections/tracks/TracksSection.module.css b/apps/khix/src/app/_components/sections/tracks/TracksSection.module.css
new file mode 100644
index 000000000..b567f12fe
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/tracks/TracksSection.module.css
@@ -0,0 +1,661 @@
+.tracksSection {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ min-height: 61rem;
+ flex-direction: column;
+ align-items: center;
+ padding: clamp(5.5rem, 10svh, 8rem) clamp(0.75rem, 3vw, 2rem)
+ clamp(2.75rem, 6svh, 5rem);
+ isolation: isolate;
+ overflow-x: clip;
+ overflow-y: visible;
+}
+
+.tracksSection::before {
+ position: absolute;
+ inset: 5% 0 7%;
+ z-index: 0;
+ background:
+ radial-gradient(
+ circle at 50% 28%,
+ rgb(160 105 207 / 0.12),
+ transparent 34%
+ ),
+ radial-gradient(ellipse at 50% 74%, rgb(49 121 99 / 0.12), transparent 48%);
+ content: "";
+ pointer-events: none;
+}
+
+.title {
+ position: relative;
+ z-index: 5;
+ margin: 0 0 clamp(0.65rem, 1.5vw, 1.25rem);
+ color: rgb(255 250 239 / 0.98);
+ font-family: var(--font-cormorant-garamond, Georgia), Georgia, serif;
+ font-size: clamp(3.8rem, 8vw, 7rem);
+ font-weight: 400;
+ line-height: 0.84;
+ text-align: center;
+ text-shadow:
+ 0 0 0.55rem rgb(233 229 255 / 0.44),
+ 0 0.25rem 1.8rem rgb(0 0 0 / 0.58);
+ text-wrap: balance;
+}
+
+.canopyComposition {
+ position: relative;
+ z-index: 4;
+ width: min(96vw, 112rem);
+ height: clamp(27rem, 38vw, 36rem);
+ flex: 0 0 auto;
+ margin: clamp(-1rem, -1vw, -0.4rem) 0 0;
+}
+
+.canopyButton {
+ position: absolute;
+ inset: 0;
+ z-index: 4;
+ display: block;
+ width: 100%;
+ height: 100%;
+ padding: 0;
+ border: 0;
+ border-radius: 45% 55% 42% 58%;
+ background: transparent;
+ color: inherit;
+ cursor: pointer;
+ font: inherit;
+ -webkit-tap-highlight-color: transparent;
+ touch-action: manipulation;
+ user-select: none;
+}
+
+.hangingTracks {
+ position: absolute;
+ inset: 0;
+ z-index: 7;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ pointer-events: none;
+}
+
+.hangingTrack {
+ --track-rotation: 0deg;
+ --track-cord: 4.5rem;
+ --track-delay: 0ms;
+
+ position: absolute;
+ top: 65%;
+ left: 50%;
+ width: clamp(8.5rem, 14vw, 13rem);
+ color: #33291f;
+ transform: translate3d(-50%, 0, 0) rotate(var(--track-rotation));
+ transform-origin: 50% calc(var(--track-cord) * -1);
+}
+
+.trackPaper {
+ display: grid;
+ width: 100%;
+ min-height: clamp(3.25rem, 4.5vw, 4.15rem);
+ place-items: center;
+ padding: clamp(0.55rem, 0.8vw, 0.75rem);
+ border: 1px solid rgb(94 67 34 / 0.34);
+ background:
+ radial-gradient(
+ circle at 24% 18%,
+ rgb(255 255 255 / 0.34),
+ transparent 36%
+ ),
+ linear-gradient(157deg, #f4e5af 0%, #dfc177 66%, #cfa858 100%);
+ box-shadow:
+ 0 0.28rem 0.45rem rgb(29 13 18 / 0.3),
+ inset 0 0 0 1px rgb(255 248 214 / 0.25);
+ color: #33291d;
+ font-family: var(--font-geist-sans, ui-sans-serif), system-ui, sans-serif;
+ font-size: clamp(0.72rem, 0.55vw + 0.45rem, 0.9rem);
+ font-weight: 750;
+ letter-spacing: 0.01em;
+ line-height: 1.12;
+ text-align: center;
+ text-wrap: balance;
+ clip-path: polygon(3% 3%, 97% 0, 100% 91%, 94% 100%, 2% 96%, 0 9%);
+ filter: drop-shadow(0 0.4rem 0.32rem rgb(0 0 0 / 0.2));
+}
+
+.hangingTrack::before {
+ position: absolute;
+ bottom: calc(100% - 0.2rem);
+ left: 50%;
+ width: 3px;
+ height: var(--track-cord);
+ background: linear-gradient(#d8c48e, #80603b 52%, #3a281c);
+ box-shadow:
+ 0 0 0.16rem rgb(0 0 0 / 0.8),
+ inset 0 0 0 1px rgb(255 242 190 / 0.18);
+ content: "";
+ transform: translateX(-50%);
+}
+
+.hangingTrack::after {
+ position: absolute;
+ top: -0.2rem;
+ left: 50%;
+ width: 0.48rem;
+ aspect-ratio: 1;
+ border: 1px solid rgb(76 51 31 / 0.5);
+ border-radius: 50%;
+ background: #aa7a3c;
+ content: "";
+ transform: translateX(-50%);
+}
+
+.hangingTrack:nth-child(1) {
+ --track-rotation: -2deg;
+ --track-cord: 4.4rem;
+ --track-delay: 20ms;
+
+ top: 64%;
+ left: 18%;
+ z-index: 5;
+}
+
+.hangingTrack:nth-child(2) {
+ --track-rotation: 1.2deg;
+ --track-cord: 5.1rem;
+ --track-delay: 85ms;
+
+ top: 67%;
+ left: 36%;
+ z-index: 5;
+}
+
+.hangingTrack:nth-child(3) {
+ --track-rotation: -0.8deg;
+ --track-cord: 4.7rem;
+ --track-delay: 45ms;
+
+ top: 65%;
+ left: 54%;
+ z-index: 4;
+}
+
+.hangingTrack:nth-child(4) {
+ --track-rotation: 1.8deg;
+ --track-cord: 5.4rem;
+ --track-delay: 120ms;
+
+ top: 68%;
+ left: 72%;
+ z-index: 4;
+}
+
+.hangingTrack:nth-child(5) {
+ --track-rotation: -1.5deg;
+ --track-cord: 4.6rem;
+ --track-delay: 65ms;
+
+ top: 64.5%;
+ left: 89%;
+ z-index: 3;
+}
+
+.canopyButton[data-stage="1"] + .hangingTracks .hangingTrack {
+ animation: trackSwaySoft 520ms ease-in-out var(--track-delay) both;
+}
+
+.canopyButton[data-stage="2"] + .hangingTracks .hangingTrack {
+ animation: trackSwayStrong 720ms cubic-bezier(0.3, 0.05, 0.25, 1)
+ var(--track-delay) both;
+}
+
+.canopyButton[data-stage="3"] + .hangingTracks .hangingTrack {
+ animation: trackSwayRelease 880ms cubic-bezier(0.18, 0.72, 0.2, 1)
+ var(--track-delay) both;
+}
+
+.canopyButton[aria-disabled="true"] {
+ cursor: default;
+}
+
+.canopyButton:focus-visible {
+ outline: 2px solid rgb(238 255 218 / 0.92);
+ outline-offset: 0.32rem;
+ box-shadow: 0 0 0.9rem rgb(168 245 195 / 0.28);
+}
+
+.scene {
+ position: absolute;
+ inset: 0;
+ display: block;
+ pointer-events: none;
+}
+
+.branch {
+ position: absolute;
+ top: 54%;
+ left: 50%;
+ z-index: 4;
+ display: block;
+ width: 116%;
+ max-width: none;
+ height: auto;
+ user-select: none;
+ filter: drop-shadow(0 0.8rem 0.8rem rgb(0 0 0 / 0.34));
+ transform: translate3d(-50%, -50%, 0);
+}
+
+.character {
+ position: absolute;
+ bottom: var(--character-bottom);
+ left: var(--character-left);
+ display: block;
+ width: var(--character-width);
+ max-width: none;
+ height: auto;
+ user-select: none;
+ filter: drop-shadow(0 0.65rem 0.5rem rgb(0 0 0 / 0.28));
+ transform: translate3d(-50%, 0, 0) rotate(var(--character-rotation, 0deg));
+ transform-origin: 50% 92%;
+ transition:
+ filter 420ms ease,
+ transform 520ms cubic-bezier(0.18, 0.7, 0.2, 1);
+}
+
+.rearCharacter {
+ z-index: 3;
+}
+
+.frontCharacter {
+ z-index: 5;
+}
+
+.canopyButton[data-stage="3"] .character {
+ filter: drop-shadow(0 0.85rem 0.65rem rgb(0 0 0 / 0.36));
+ transform: translate3d(-50%, -0.2rem, 0)
+ rotate(var(--character-rotation, 0deg));
+}
+
+.leaf {
+ position: absolute;
+ top: var(--leaf-y);
+ left: var(--leaf-x);
+ display: block;
+ width: var(--leaf-width);
+ max-width: none;
+ height: auto;
+ opacity: 1;
+ user-select: none;
+ filter: drop-shadow(0 0.7rem 0.6rem rgb(0 0 0 / 0.2));
+ transform: translate3d(-50%, -50%, 0) rotate(var(--leaf-rotation))
+ scale(var(--leaf-scale));
+ transform-origin: var(--leaf-origin);
+}
+
+.rearLeaf {
+ z-index: 2;
+ opacity: 0.88;
+}
+
+.frontLeaf {
+ z-index: 6;
+}
+
+.canopyButton[data-stage="1"] .leaf {
+ animation: leafRustleSoft var(--leaf-rustle-duration) ease-in-out
+ var(--leaf-rustle-delay) both;
+}
+
+.canopyButton[data-stage="2"] .leaf {
+ animation: leafRustleStrong calc(var(--leaf-rustle-duration) + 160ms)
+ cubic-bezier(0.3, 0.05, 0.25, 1) var(--leaf-rustle-delay) both;
+}
+
+.canopyButton[data-stage="3"] .leaf {
+ animation: leafRelease var(--leaf-fall-duration)
+ cubic-bezier(0.24, 0.02, 0.36, 0.98) var(--leaf-fall-delay) forwards;
+}
+
+.revealHint {
+ position: relative;
+ z-index: 5;
+ min-height: 1.4em;
+ margin: 0 0 clamp(0.25rem, 1vw, 0.65rem);
+ color: rgb(244 248 224 / 0.88);
+ font-family: var(--font-geist-sans, ui-sans-serif), system-ui, sans-serif;
+ font-size: clamp(0.78rem, 0.6vw + 0.65rem, 0.98rem);
+ font-weight: 650;
+ letter-spacing: 0.04em;
+ line-height: 1.35;
+ text-align: center;
+ text-shadow: 0 0.16rem 0.65rem rgb(0 0 0 / 0.78);
+}
+
+.srOnly {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ border: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+}
+
+@keyframes leafRustleSoft {
+ 0%,
+ 100% {
+ transform: translate3d(-50%, -50%, 0) rotate(var(--leaf-rotation))
+ scale(var(--leaf-scale));
+ }
+
+ 28% {
+ transform: translate3d(calc(-50% + 0.28rem), calc(-50% - 0.12rem), 0)
+ rotate(calc(var(--leaf-rotation) + 2.6deg)) scale(var(--leaf-scale));
+ }
+
+ 58% {
+ transform: translate3d(calc(-50% - 0.24rem), calc(-50% + 0.16rem), 0)
+ rotate(calc(var(--leaf-rotation) - 2.1deg)) scale(var(--leaf-scale));
+ }
+
+ 78% {
+ transform: translate3d(calc(-50% + 0.14rem), calc(-50% - 0.08rem), 0)
+ rotate(calc(var(--leaf-rotation) + 1.4deg)) scale(var(--leaf-scale));
+ }
+}
+
+@keyframes leafRustleStrong {
+ 0% {
+ opacity: 1;
+ transform: translate3d(-50%, -50%, 0) rotate(var(--leaf-rotation))
+ scale(var(--leaf-scale));
+ }
+
+ 18% {
+ transform: translate3d(calc(-50% - 0.55rem), calc(-50% + 0.2rem), 0)
+ rotate(calc(var(--leaf-rotation) - 5deg)) scale(var(--leaf-scale));
+ }
+
+ 40% {
+ transform: translate3d(calc(-50% + 0.72rem), calc(-50% - 0.25rem), 0)
+ rotate(calc(var(--leaf-rotation) + 6.5deg)) scale(var(--leaf-scale));
+ }
+
+ 63% {
+ transform: translate3d(calc(-50% - 0.38rem), calc(-50% + 0.18rem), 0)
+ rotate(calc(var(--leaf-rotation) - 3.8deg)) scale(var(--leaf-scale));
+ }
+
+ 100% {
+ opacity: var(--leaf-stage-two-opacity);
+ transform: translate3d(
+ calc(-50% + var(--leaf-shift-x)),
+ calc(-50% + var(--leaf-shift-y)),
+ 0
+ )
+ rotate(calc(var(--leaf-rotation) + var(--leaf-shift-rotation)))
+ scale(var(--leaf-scale));
+ }
+}
+
+@keyframes leafRelease {
+ 0% {
+ opacity: var(--leaf-stage-two-opacity);
+ transform: translate3d(
+ calc(-50% + var(--leaf-shift-x)),
+ calc(-50% + var(--leaf-shift-y)),
+ 0
+ )
+ rotate(calc(var(--leaf-rotation) + var(--leaf-shift-rotation)))
+ scale(var(--leaf-scale));
+ }
+
+ 12% {
+ opacity: 1;
+ transform: translate3d(calc(-50% - 0.8rem), calc(-50% + 0.32rem), 0)
+ rotate(calc(var(--leaf-rotation) - 7deg)) scale(var(--leaf-scale));
+ }
+
+ 24% {
+ transform: translate3d(calc(-50% + 0.95rem), calc(-50% - 0.18rem), 0)
+ rotate(calc(var(--leaf-rotation) + 8deg)) scale(var(--leaf-scale));
+ }
+
+ 72% {
+ opacity: 0.82;
+ }
+
+ 100% {
+ opacity: 0;
+ transform: translate3d(
+ calc(-50% + var(--leaf-fall-x)),
+ calc(-50% + var(--leaf-fall-y)),
+ 0
+ )
+ rotate(calc(var(--leaf-rotation) + var(--leaf-fall-rotation)))
+ scale(var(--leaf-scale)) scale(0.86);
+ }
+}
+
+@keyframes trackSwaySoft {
+ 0%,
+ 100% {
+ transform: translate3d(-50%, 0, 0) rotate(var(--track-rotation));
+ }
+
+ 36% {
+ transform: translate3d(-50%, 0.08rem, 0)
+ rotate(calc(var(--track-rotation) + 1.8deg));
+ }
+
+ 72% {
+ transform: translate3d(-50%, -0.04rem, 0)
+ rotate(calc(var(--track-rotation) - 1.2deg));
+ }
+}
+
+@keyframes trackSwayStrong {
+ 0%,
+ 100% {
+ transform: translate3d(-50%, 0, 0) rotate(var(--track-rotation));
+ }
+
+ 24% {
+ transform: translate3d(calc(-50% - 0.18rem), 0.18rem, 0)
+ rotate(calc(var(--track-rotation) - 3deg));
+ }
+
+ 54% {
+ transform: translate3d(calc(-50% + 0.24rem), -0.08rem, 0)
+ rotate(calc(var(--track-rotation) + 3.8deg));
+ }
+
+ 78% {
+ transform: translate3d(calc(-50% - 0.1rem), 0.06rem, 0)
+ rotate(calc(var(--track-rotation) - 1.6deg));
+ }
+}
+
+@keyframes trackSwayRelease {
+ 0%,
+ 100% {
+ transform: translate3d(-50%, 0, 0) rotate(var(--track-rotation));
+ }
+
+ 25% {
+ transform: translate3d(calc(-50% + 0.32rem), 0.12rem, 0)
+ rotate(calc(var(--track-rotation) + 4.2deg));
+ }
+
+ 58% {
+ transform: translate3d(calc(-50% - 0.2rem), -0.04rem, 0)
+ rotate(calc(var(--track-rotation) - 2.8deg));
+ }
+
+ 80% {
+ transform: translate3d(calc(-50% + 0.08rem), 0.02rem, 0)
+ rotate(calc(var(--track-rotation) + 1.1deg));
+ }
+}
+
+@media (min-width: 761px) and (max-width: 1100px) {
+ .tracksSection {
+ min-height: 57rem;
+ }
+
+ .canopyComposition {
+ width: min(98vw, 72rem);
+ height: clamp(29rem, 54vw, 34rem);
+ }
+}
+
+@media (max-width: 760px) {
+ .tracksSection {
+ min-height: 55rem;
+ padding: clamp(4.75rem, 10svh, 6.5rem) 0.6rem clamp(2.25rem, 5svh, 3.5rem);
+ }
+
+ .title {
+ margin-bottom: 0.75rem;
+ font-size: clamp(3.5rem, 18vw, 5.2rem);
+ }
+
+ .canopyComposition {
+ width: 100%;
+ height: clamp(34rem, 155vw, 40rem);
+ margin-top: 0;
+ }
+
+ .canopyButton {
+ border-radius: 42% 58% 46% 54%;
+ }
+
+ .branch {
+ top: 43%;
+ width: 178%;
+ }
+
+ .character {
+ bottom: calc(var(--character-mobile-bottom, var(--character-bottom)) + 14%);
+ left: var(--character-mobile-left, var(--character-left));
+ width: var(--character-mobile-width, var(--character-width));
+ }
+
+ .leaf {
+ width: var(--leaf-mobile-width);
+ }
+
+ .revealHint {
+ width: min(90%, 24rem);
+ font-size: clamp(0.76rem, 3.2vw, 0.9rem);
+ }
+
+ .hangingTrack {
+ width: min(40%, 10rem);
+ }
+
+ .trackPaper {
+ min-height: 3.4rem;
+ padding: 0.5rem 0.42rem;
+ font-size: clamp(0.7rem, 3vw, 0.84rem);
+ }
+
+ .hangingTrack:nth-child(1) {
+ --track-cord: 4.5rem;
+
+ top: 57%;
+ left: 27%;
+ }
+
+ .hangingTrack:nth-child(2) {
+ --track-cord: 4.8rem;
+
+ top: 57%;
+ left: 73%;
+ }
+
+ .hangingTrack:nth-child(3) {
+ --track-cord: 9.7rem;
+
+ top: 72.5%;
+ left: 27%;
+ }
+
+ .hangingTrack:nth-child(4) {
+ --track-cord: 10rem;
+
+ top: 72.5%;
+ left: 73%;
+ }
+
+ .hangingTrack:nth-child(5) {
+ --track-cord: 14.5rem;
+
+ top: 87%;
+ left: 50%;
+ }
+}
+
+@media (max-width: 360px) {
+ .tracksSection {
+ min-height: 52rem;
+ }
+
+ .canopyComposition {
+ height: clamp(34rem, 155vw, 36rem);
+ }
+
+ .hangingTrack {
+ width: 36%;
+ }
+
+ .hangingTrack:nth-child(3) {
+ left: 39%;
+ }
+
+ .hangingTrack:nth-child(4) {
+ left: 78%;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .character {
+ transition: none;
+ }
+
+ .leaf {
+ transition:
+ transform 120ms ease-out,
+ opacity 120ms linear;
+ animation: none !important;
+ }
+
+ .hangingTrack {
+ animation: none !important;
+ }
+
+ .canopyButton[data-stage="1"] .leaf {
+ transform: translate3d(calc(-50% + 0.18rem), calc(-50% + 0.12rem), 0)
+ rotate(calc(var(--leaf-rotation) + 1deg)) scale(var(--leaf-scale));
+ }
+
+ .canopyButton[data-stage="2"] .leaf {
+ opacity: var(--leaf-stage-two-opacity);
+ transform: translate3d(
+ calc(-50% + var(--leaf-shift-x)),
+ calc(-50% + var(--leaf-shift-y)),
+ 0
+ )
+ rotate(calc(var(--leaf-rotation) + var(--leaf-shift-rotation)))
+ scale(var(--leaf-scale));
+ }
+
+ .canopyButton[data-stage="3"] .leaf {
+ opacity: 0;
+ transform: translate3d(-50%, calc(-50% + 3rem), 0)
+ rotate(var(--leaf-rotation)) scale(var(--leaf-scale));
+ }
+}
diff --git a/apps/khix/src/app/_components/sections/tracks/TracksSection.tsx b/apps/khix/src/app/_components/sections/tracks/TracksSection.tsx
new file mode 100644
index 000000000..44d6cb160
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/tracks/TracksSection.tsx
@@ -0,0 +1,858 @@
+"use client";
+
+import type { CSSProperties, PointerEvent } from "react";
+import { useEffect, useRef, useState } from "react";
+import Image from "next/image";
+
+import styles from "./TracksSection.module.css";
+
+const ASSET_PATH = "https://assets.knighthacks.org/khix";
+
+const TRACKS = [
+ "Hello World",
+ "Artificial Intelligence / Machine Learning",
+ "App Development",
+ "Game Development",
+ "Embedded Software",
+] as const;
+
+type RevealStage = 0 | 1 | 2 | 3;
+type CharacterLayer = "rear" | "front";
+type LeafLayer = "rear" | "front";
+
+const LEAF_ASSETS = {
+ "leaf-1.webp": { width: 495, height: 947 },
+ "leaf-2.webp": { width: 807, height: 918 },
+ "leaf-3.webp": { width: 754, height: 1161 },
+} as const;
+
+type LeafAsset = keyof typeof LEAF_ASSETS;
+
+type CharacterStyle = CSSProperties &
+ Record<`--character-${string}`, string | undefined>;
+type LeafStyle = CSSProperties &
+ Record<`--leaf-${string}`, string | number | undefined>;
+
+interface CharacterConfig {
+ asset: string;
+ layer: CharacterLayer;
+ style: CharacterStyle;
+}
+
+interface LeafConfig {
+ asset: LeafAsset;
+ fallDelay: string;
+ fallDuration: string;
+ fallRotation: string;
+ fallX: string;
+ fallY: string;
+ id: string;
+ layer: LeafLayer;
+ mobileWidth: string;
+ origin: string;
+ rotation: string;
+ rustleDelay: string;
+ rustleDuration: string;
+ scale: number;
+ shiftRotation: string;
+ shiftX: string;
+ shiftY: string;
+ stageTwoOpacity: number;
+ width: string;
+ x: string;
+ y: string;
+}
+
+const NEXT_STAGE: Record = {
+ 0: 1,
+ 1: 2,
+ 2: 3,
+ 3: 3,
+};
+
+const STAGE_SETTLE_MS = {
+ 1: 670,
+ 2: 830,
+} as const;
+
+const REVEAL_COPY = [
+ {
+ hint: "Rustle the leaves three times",
+ label:
+ "Rustle the foliage to begin revealing the forest visitors. Three activations remaining.",
+ status: "The foliage is still. Three activations remain.",
+ },
+ {
+ hint: "The canopy stirred. Two rustles left",
+ label: "Rustle the foliage again. Two activations remaining.",
+ status: "The leaves rustled softly. Two activations remain.",
+ },
+ {
+ hint: "The forest creatures are revealed",
+ label: "Clear the remaining foliage. One activation remaining.",
+ status: "The foliage has started to fall. One activation remains.",
+ },
+ {
+ hint: "The forest visitors are revealed",
+ label: "The forest visitors are revealed.",
+ status: "The leaves have fallen and the forest visitors are revealed.",
+ },
+] as const;
+
+const CHARACTERS: readonly CharacterConfig[] = [
+ {
+ asset: "IMG_7680.webp",
+ layer: "rear",
+ style: {
+ "--character-left": "8%",
+ "--character-bottom": "46%",
+ "--character-width": "clamp(8.5rem, 15%, 13.5rem)",
+ "--character-rotation": "-3deg",
+ "--character-mobile-left": "12%",
+ "--character-mobile-bottom": "45%",
+ "--character-mobile-width": "27%",
+ },
+ },
+ {
+ asset: "IMG_7681.webp",
+ layer: "rear",
+ style: {
+ "--character-left": "18%",
+ "--character-bottom": "48%",
+ "--character-width": "clamp(7.5rem, 13%, 11.5rem)",
+ "--character-rotation": "2deg",
+ "--character-mobile-left": "22%",
+ "--character-mobile-bottom": "49%",
+ "--character-mobile-width": "25%",
+ },
+ },
+ {
+ asset: "IMG_7682.webp",
+ layer: "rear",
+ style: {
+ "--character-left": "30%",
+ "--character-bottom": "44%",
+ "--character-width": "clamp(9rem, 16%, 14rem)",
+ "--character-rotation": "-1deg",
+ "--character-mobile-left": "33%",
+ "--character-mobile-bottom": "44%",
+ "--character-mobile-width": "29%",
+ },
+ },
+ {
+ asset: "IMG_7683.webp",
+ layer: "front",
+ style: {
+ "--character-left": "41%",
+ "--character-bottom": "40%",
+ "--character-width": "clamp(9rem, 16%, 14rem)",
+ "--character-rotation": "2deg",
+ "--character-mobile-left": "43%",
+ "--character-mobile-bottom": "39%",
+ "--character-mobile-width": "31%",
+ },
+ },
+ {
+ asset: "IMG_7684.webp",
+ layer: "rear",
+ style: {
+ "--character-left": "52%",
+ "--character-bottom": "45%",
+ "--character-width": "clamp(8.5rem, 15%, 13rem)",
+ "--character-rotation": "-2deg",
+ "--character-mobile-left": "54%",
+ "--character-mobile-bottom": "46%",
+ "--character-mobile-width": "27%",
+ },
+ },
+ {
+ asset: "IMG_7685.webp",
+ layer: "front",
+ style: {
+ "--character-left": "64%",
+ "--character-bottom": "47%",
+ "--character-width": "clamp(8rem, 14%, 12.5rem)",
+ "--character-rotation": "3deg",
+ "--character-mobile-left": "65%",
+ "--character-mobile-bottom": "49%",
+ "--character-mobile-width": "26%",
+ },
+ },
+ {
+ asset: "IMG_7687.webp",
+ layer: "rear",
+ style: {
+ "--character-left": "76%",
+ "--character-bottom": "45%",
+ "--character-width": "clamp(8.5rem, 15%, 13rem)",
+ "--character-rotation": "-2deg",
+ "--character-mobile-left": "76%",
+ "--character-mobile-bottom": "45%",
+ "--character-mobile-width": "26%",
+ },
+ },
+ {
+ asset: "IMG_7688.webp",
+ layer: "front",
+ style: {
+ "--character-left": "89%",
+ "--character-bottom": "44%",
+ "--character-width": "clamp(8.5rem, 15%, 13rem)",
+ "--character-rotation": "3deg",
+ "--character-mobile-left": "88%",
+ "--character-mobile-bottom": "45%",
+ "--character-mobile-width": "28%",
+ },
+ },
+] as const;
+
+const LEAVES: readonly LeafConfig[] = [
+ {
+ id: "rear-left",
+ asset: "leaf-2.webp",
+ layer: "rear",
+ x: "4%",
+ y: "44%",
+ width: "18%",
+ mobileWidth: "38%",
+ rotation: "-18deg",
+ scale: 1.04,
+ origin: "36% 78%",
+ rustleDelay: "25ms",
+ rustleDuration: "390ms",
+ shiftX: "-0.8rem",
+ shiftY: "1.4rem",
+ shiftRotation: "-9deg",
+ stageTwoOpacity: 0.94,
+ fallX: "-5rem",
+ fallY: "clamp(18rem, 40svh, 29rem)",
+ fallRotation: "-310deg",
+ fallDelay: "40ms",
+ fallDuration: "1180ms",
+ },
+ {
+ id: "rear-quarter",
+ asset: "leaf-1.webp",
+ layer: "rear",
+ x: "30%",
+ y: "35%",
+ width: "13%",
+ mobileWidth: "29%",
+ rotation: "14deg",
+ scale: 0.94,
+ origin: "58% 82%",
+ rustleDelay: "90ms",
+ rustleDuration: "440ms",
+ shiftX: "0.7rem",
+ shiftY: "1.8rem",
+ shiftRotation: "12deg",
+ stageTwoOpacity: 0.9,
+ fallX: "4rem",
+ fallY: "clamp(20rem, 44svh, 31rem)",
+ fallRotation: "340deg",
+ fallDelay: "190ms",
+ fallDuration: "1380ms",
+ },
+ {
+ id: "rear-center",
+ asset: "leaf-3.webp",
+ layer: "rear",
+ x: "55%",
+ y: "39%",
+ width: "14%",
+ mobileWidth: "31%",
+ rotation: "-8deg",
+ scale: 1.08,
+ origin: "44% 84%",
+ rustleDelay: "45ms",
+ rustleDuration: "410ms",
+ shiftX: "-0.4rem",
+ shiftY: "2.2rem",
+ shiftRotation: "-13deg",
+ stageTwoOpacity: 0.88,
+ fallX: "-3rem",
+ fallY: "clamp(19rem, 42svh, 30rem)",
+ fallRotation: "-390deg",
+ fallDelay: "110ms",
+ fallDuration: "1260ms",
+ },
+ {
+ id: "rear-right",
+ asset: "leaf-2.webp",
+ layer: "rear",
+ x: "86%",
+ y: "43%",
+ width: "17%",
+ mobileWidth: "37%",
+ rotation: "20deg",
+ scale: 1,
+ origin: "62% 76%",
+ rustleDelay: "115ms",
+ rustleDuration: "470ms",
+ shiftX: "1.1rem",
+ shiftY: "1.3rem",
+ shiftRotation: "10deg",
+ stageTwoOpacity: 0.92,
+ fallX: "6rem",
+ fallY: "clamp(18rem, 39svh, 28rem)",
+ fallRotation: "370deg",
+ fallDelay: "270ms",
+ fallDuration: "1460ms",
+ },
+ {
+ id: "front-edge-left",
+ asset: "leaf-3.webp",
+ layer: "front",
+ x: "-2%",
+ y: "39%",
+ width: "15%",
+ mobileWidth: "34%",
+ rotation: "23deg",
+ scale: 1.04,
+ origin: "50% 88%",
+ rustleDelay: "70ms",
+ rustleDuration: "430ms",
+ shiftX: "-1.2rem",
+ shiftY: "2.8rem",
+ shiftRotation: "-18deg",
+ stageTwoOpacity: 0.84,
+ fallX: "-7rem",
+ fallY: "clamp(21rem, 46svh, 32rem)",
+ fallRotation: "-460deg",
+ fallDelay: "10ms",
+ fallDuration: "1290ms",
+ },
+ {
+ id: "front-left-a",
+ asset: "leaf-1.webp",
+ layer: "front",
+ x: "8%",
+ y: "31%",
+ width: "12%",
+ mobileWidth: "28%",
+ rotation: "-11deg",
+ scale: 1,
+ origin: "48% 90%",
+ rustleDelay: "10ms",
+ rustleDuration: "380ms",
+ shiftX: "0.8rem",
+ shiftY: "0.9rem",
+ shiftRotation: "9deg",
+ stageTwoOpacity: 0.96,
+ fallX: "3rem",
+ fallY: "clamp(18rem, 41svh, 28rem)",
+ fallRotation: "410deg",
+ fallDelay: "330ms",
+ fallDuration: "1120ms",
+ },
+ {
+ id: "front-left-b",
+ asset: "leaf-2.webp",
+ layer: "front",
+ x: "16%",
+ y: "44%",
+ width: "17%",
+ mobileWidth: "39%",
+ rotation: "8deg",
+ scale: 1.08,
+ origin: "42% 74%",
+ rustleDelay: "85ms",
+ rustleDuration: "460ms",
+ shiftX: "-0.5rem",
+ shiftY: "3.7rem",
+ shiftRotation: "-15deg",
+ stageTwoOpacity: 0.76,
+ fallX: "-4rem",
+ fallY: "clamp(20rem, 45svh, 31rem)",
+ fallRotation: "-520deg",
+ fallDelay: "100ms",
+ fallDuration: "1510ms",
+ },
+ {
+ id: "front-left-c",
+ asset: "leaf-3.webp",
+ layer: "front",
+ x: "24%",
+ y: "34%",
+ width: "13%",
+ mobileWidth: "30%",
+ rotation: "-27deg",
+ scale: 0.96,
+ origin: "57% 86%",
+ rustleDelay: "130ms",
+ rustleDuration: "420ms",
+ shiftX: "1.2rem",
+ shiftY: "1.5rem",
+ shiftRotation: "17deg",
+ stageTwoOpacity: 0.91,
+ fallX: "5rem",
+ fallY: "clamp(19rem, 43svh, 30rem)",
+ fallRotation: "430deg",
+ fallDelay: "230ms",
+ fallDuration: "1220ms",
+ },
+ {
+ id: "front-center-left-a",
+ asset: "leaf-1.webp",
+ layer: "front",
+ x: "34%",
+ y: "39%",
+ width: "14%",
+ mobileWidth: "32%",
+ rotation: "18deg",
+ scale: 1.06,
+ origin: "40% 83%",
+ rustleDelay: "55ms",
+ rustleDuration: "450ms",
+ shiftX: "-1rem",
+ shiftY: "2.6rem",
+ shiftRotation: "-11deg",
+ stageTwoOpacity: 0.82,
+ fallX: "-6rem",
+ fallY: "clamp(22rem, 48svh, 33rem)",
+ fallRotation: "-360deg",
+ fallDelay: "150ms",
+ fallDuration: "1430ms",
+ },
+ {
+ id: "front-center-left-b",
+ asset: "leaf-2.webp",
+ layer: "front",
+ x: "42%",
+ y: "29%",
+ width: "15%",
+ mobileWidth: "35%",
+ rotation: "-5deg",
+ scale: 0.92,
+ origin: "54% 79%",
+ rustleDelay: "5ms",
+ rustleDuration: "400ms",
+ shiftX: "0.6rem",
+ shiftY: "1rem",
+ shiftRotation: "8deg",
+ stageTwoOpacity: 0.95,
+ fallX: "3rem",
+ fallY: "clamp(18rem, 40svh, 29rem)",
+ fallRotation: "480deg",
+ fallDelay: "380ms",
+ fallDuration: "1350ms",
+ },
+ {
+ id: "front-center",
+ asset: "leaf-3.webp",
+ layer: "front",
+ x: "50%",
+ y: "45%",
+ width: "16%",
+ mobileWidth: "37%",
+ rotation: "12deg",
+ scale: 1.09,
+ origin: "47% 87%",
+ rustleDelay: "105ms",
+ rustleDuration: "470ms",
+ shiftX: "-0.3rem",
+ shiftY: "4.4rem",
+ shiftRotation: "-19deg",
+ stageTwoOpacity: 0.7,
+ fallX: "-2rem",
+ fallY: "clamp(21rem, 47svh, 32rem)",
+ fallRotation: "-550deg",
+ fallDelay: "60ms",
+ fallDuration: "1540ms",
+ },
+ {
+ id: "front-center-right-a",
+ asset: "leaf-1.webp",
+ layer: "front",
+ x: "59%",
+ y: "33%",
+ width: "13%",
+ mobileWidth: "30%",
+ rotation: "-21deg",
+ scale: 1.02,
+ origin: "60% 85%",
+ rustleDelay: "35ms",
+ rustleDuration: "410ms",
+ shiftX: "1.1rem",
+ shiftY: "1.9rem",
+ shiftRotation: "14deg",
+ stageTwoOpacity: 0.88,
+ fallX: "6rem",
+ fallY: "clamp(19rem, 43svh, 30rem)",
+ fallRotation: "390deg",
+ fallDelay: "210ms",
+ fallDuration: "1170ms",
+ },
+ {
+ id: "front-center-right-b",
+ asset: "leaf-2.webp",
+ layer: "front",
+ x: "67%",
+ y: "43%",
+ width: "18%",
+ mobileWidth: "40%",
+ rotation: "16deg",
+ scale: 1.04,
+ origin: "39% 78%",
+ rustleDelay: "125ms",
+ rustleDuration: "480ms",
+ shiftX: "-0.9rem",
+ shiftY: "3.3rem",
+ shiftRotation: "-16deg",
+ stageTwoOpacity: 0.78,
+ fallX: "-5rem",
+ fallY: "clamp(22rem, 49svh, 34rem)",
+ fallRotation: "-470deg",
+ fallDelay: "20ms",
+ fallDuration: "1490ms",
+ },
+ {
+ id: "front-right-a",
+ asset: "leaf-3.webp",
+ layer: "front",
+ x: "76%",
+ y: "30%",
+ width: "13%",
+ mobileWidth: "31%",
+ rotation: "27deg",
+ scale: 0.98,
+ origin: "53% 89%",
+ rustleDelay: "65ms",
+ rustleDuration: "390ms",
+ shiftX: "0.7rem",
+ shiftY: "1.2rem",
+ shiftRotation: "10deg",
+ stageTwoOpacity: 0.94,
+ fallX: "4rem",
+ fallY: "clamp(18rem, 39svh, 28rem)",
+ fallRotation: "450deg",
+ fallDelay: "310ms",
+ fallDuration: "1280ms",
+ },
+ {
+ id: "front-right-b",
+ asset: "leaf-1.webp",
+ layer: "front",
+ x: "84%",
+ y: "40%",
+ width: "14%",
+ mobileWidth: "33%",
+ rotation: "-13deg",
+ scale: 1.07,
+ origin: "45% 82%",
+ rustleDelay: "20ms",
+ rustleDuration: "440ms",
+ shiftX: "-1.1rem",
+ shiftY: "2.9rem",
+ shiftRotation: "-17deg",
+ stageTwoOpacity: 0.8,
+ fallX: "-6rem",
+ fallY: "clamp(20rem, 45svh, 31rem)",
+ fallRotation: "-420deg",
+ fallDelay: "130ms",
+ fallDuration: "1370ms",
+ },
+ {
+ id: "front-right-c",
+ asset: "leaf-2.webp",
+ layer: "front",
+ x: "92%",
+ y: "33%",
+ width: "16%",
+ mobileWidth: "37%",
+ rotation: "9deg",
+ scale: 0.96,
+ origin: "61% 80%",
+ rustleDelay: "95ms",
+ rustleDuration: "460ms",
+ shiftX: "0.9rem",
+ shiftY: "2rem",
+ shiftRotation: "12deg",
+ stageTwoOpacity: 0.86,
+ fallX: "5rem",
+ fallY: "clamp(19rem, 42svh, 30rem)",
+ fallRotation: "500deg",
+ fallDelay: "250ms",
+ fallDuration: "1450ms",
+ },
+ {
+ id: "front-edge-right",
+ asset: "leaf-3.webp",
+ layer: "front",
+ x: "101%",
+ y: "43%",
+ width: "15%",
+ mobileWidth: "34%",
+ rotation: "-24deg",
+ scale: 1.05,
+ origin: "38% 86%",
+ rustleDelay: "40ms",
+ rustleDuration: "420ms",
+ shiftX: "1.3rem",
+ shiftY: "3.5rem",
+ shiftRotation: "18deg",
+ stageTwoOpacity: 0.74,
+ fallX: "7rem",
+ fallY: "clamp(21rem, 47svh, 33rem)",
+ fallRotation: "540deg",
+ fallDelay: "80ms",
+ fallDuration: "1520ms",
+ },
+ {
+ id: "front-low-left",
+ asset: "leaf-2.webp",
+ layer: "front",
+ x: "20%",
+ y: "55%",
+ width: "14%",
+ mobileWidth: "34%",
+ rotation: "31deg",
+ scale: 0.88,
+ origin: "42% 73%",
+ rustleDelay: "145ms",
+ rustleDuration: "490ms",
+ shiftX: "-0.6rem",
+ shiftY: "4.8rem",
+ shiftRotation: "-22deg",
+ stageTwoOpacity: 0.68,
+ fallX: "-3rem",
+ fallY: "clamp(18rem, 38svh, 27rem)",
+ fallRotation: "-330deg",
+ fallDelay: "170ms",
+ fallDuration: "1190ms",
+ },
+ {
+ id: "front-low-right",
+ asset: "leaf-1.webp",
+ layer: "front",
+ x: "73%",
+ y: "56%",
+ width: "12%",
+ mobileWidth: "30%",
+ rotation: "-29deg",
+ scale: 0.9,
+ origin: "58% 76%",
+ rustleDelay: "155ms",
+ rustleDuration: "500ms",
+ shiftX: "0.8rem",
+ shiftY: "5.2rem",
+ shiftRotation: "24deg",
+ stageTwoOpacity: 0.65,
+ fallX: "4rem",
+ fallY: "clamp(18rem, 37svh, 27rem)",
+ fallRotation: "360deg",
+ fallDelay: "290ms",
+ fallDuration: "1250ms",
+ },
+] as const;
+
+function getLeafStyle(leaf: LeafConfig): LeafStyle {
+ return {
+ "--leaf-x": leaf.x,
+ "--leaf-y": leaf.y,
+ "--leaf-width": leaf.width,
+ "--leaf-mobile-width": leaf.mobileWidth,
+ "--leaf-rotation": leaf.rotation,
+ "--leaf-scale": leaf.scale,
+ "--leaf-origin": leaf.origin,
+ "--leaf-rustle-delay": leaf.rustleDelay,
+ "--leaf-rustle-duration": leaf.rustleDuration,
+ "--leaf-shift-x": leaf.shiftX,
+ "--leaf-shift-y": leaf.shiftY,
+ "--leaf-shift-rotation": leaf.shiftRotation,
+ "--leaf-stage-two-opacity": leaf.stageTwoOpacity,
+ "--leaf-fall-x": leaf.fallX,
+ "--leaf-fall-y": leaf.fallY,
+ "--leaf-fall-rotation": leaf.fallRotation,
+ "--leaf-fall-delay": leaf.fallDelay,
+ "--leaf-fall-duration": leaf.fallDuration,
+ };
+}
+
+function CanopyLeaf({ leaf }: { leaf: LeafConfig }) {
+ const asset = LEAF_ASSETS[leaf.asset];
+
+ return (
+
+ );
+}
+
+function ForestCharacter({ character }: { character: CharacterConfig }) {
+ return (
+
+ );
+}
+
+export function TracksSection() {
+ const [revealStage, setRevealStage] = useState(0);
+ const revealStageRef = useRef(0);
+ const pendingActivationsRef = useRef(0);
+ const ignoreClickUntilRef = useRef(0);
+ const isSettlingRef = useRef(false);
+ const settleTimerRef = useRef(null);
+ const revealCopy = REVEAL_COPY[revealStage];
+
+ function processNextStage() {
+ if (
+ isSettlingRef.current ||
+ pendingActivationsRef.current === 0 ||
+ revealStageRef.current === 3
+ ) {
+ return;
+ }
+
+ pendingActivationsRef.current -= 1;
+ const nextStage = NEXT_STAGE[revealStageRef.current];
+ revealStageRef.current = nextStage;
+ setRevealStage(nextStage);
+
+ if (nextStage !== 1 && nextStage !== 2) return;
+
+ isSettlingRef.current = true;
+ const settleDelay = window.matchMedia("(prefers-reduced-motion: reduce)")
+ .matches
+ ? 140
+ : STAGE_SETTLE_MS[nextStage];
+ settleTimerRef.current = window.setTimeout(() => {
+ settleTimerRef.current = null;
+ isSettlingRef.current = false;
+ processNextStage();
+ }, settleDelay);
+ }
+
+ function handleReveal() {
+ const queuedStage = revealStageRef.current + pendingActivationsRef.current;
+
+ if (queuedStage >= 3) return;
+
+ pendingActivationsRef.current += 1;
+ processNextStage();
+ }
+
+ function handleCanopyPointerUp(event: PointerEvent) {
+ if (event.pointerType !== "touch" && event.pointerType !== "pen") return;
+
+ ignoreClickUntilRef.current = Date.now() + 700;
+ handleReveal();
+ }
+
+ function handleCanopyClick() {
+ if (Date.now() < ignoreClickUntilRef.current) return;
+
+ handleReveal();
+ }
+
+ useEffect(
+ () => () => {
+ if (settleTimerRef.current !== null) {
+ window.clearTimeout(settleTimerRef.current);
+ }
+ },
+ [],
+ );
+
+ return (
+
+
+ Tracks
+
+
+
+ {revealCopy.hint}
+
+
+
+
+
+ {LEAVES.filter((leaf) => leaf.layer === "rear").map((leaf) => (
+
+ ))}
+
+ {CHARACTERS.filter((character) => character.layer === "rear").map(
+ (character) => (
+
+ ),
+ )}
+
+
+
+ {CHARACTERS.filter((character) => character.layer === "front").map(
+ (character) => (
+
+ ),
+ )}
+
+ {LEAVES.filter((leaf) => leaf.layer === "front").map((leaf) => (
+
+ ))}
+
+
+
+
+ {TRACKS.map((track) => (
+
+ {track}
+
+ ))}
+
+
+
+ {revealCopy.status}
+
+
+ );
+}
diff --git a/apps/khix/src/app/_components/sections/tracks/index.ts b/apps/khix/src/app/_components/sections/tracks/index.ts
new file mode 100644
index 000000000..a57d084a1
--- /dev/null
+++ b/apps/khix/src/app/_components/sections/tracks/index.ts
@@ -0,0 +1 @@
+export { TracksSection } from "./TracksSection";
diff --git a/apps/khix/src/app/_components/sponsor-showcase/SponsorShowcase.module.css b/apps/khix/src/app/_components/sponsor-showcase/SponsorShowcase.module.css
new file mode 100644
index 000000000..55a94bb00
--- /dev/null
+++ b/apps/khix/src/app/_components/sponsor-showcase/SponsorShowcase.module.css
@@ -0,0 +1,828 @@
+.sponsorShowcase {
+ width: min(100%, var(--sponsor-showcase-width, 54rem));
+ margin: 0 auto;
+ padding: var(--sponsor-showcase-padding, 3.25rem 1rem 3.75rem);
+ color: var(--sponsor-showcase-color, #091923);
+ background: transparent;
+ isolation: isolate;
+}
+
+.sponsorTitle {
+ position: relative;
+ display: grid;
+ width: var(--sponsor-title-rock-width, 100%);
+ min-height: var(--sponsor-title-rock-height, auto);
+ aspect-ratio: var(--sponsor-title-rock-ratio, auto);
+ place-items: center;
+ margin: var(--sponsor-title-margin, 0 0 1.9rem);
+ color: var(--sponsor-title-color, #091923);
+ font-family: var(--sponsor-title-font, Georgia, "Times New Roman", serif);
+ font-size: var(--sponsor-title-size, 3.25rem);
+ font-weight: var(--sponsor-title-weight, 400);
+ line-height: var(--sponsor-title-line-height, 0.95);
+ letter-spacing: 0;
+ text-align: center;
+ text-shadow: var(--sponsor-title-shadow, none);
+ text-transform: var(--sponsor-title-transform, none);
+ translate: 0 var(--sponsor-title-offset-y, 0);
+ isolation: isolate;
+}
+
+.sponsorTitleText {
+ position: relative;
+ z-index: 2;
+ display: inline-block;
+ max-width: var(--sponsor-title-text-width, 100%);
+ white-space: nowrap;
+}
+
+.sponsorTitleRock {
+ position: absolute;
+ inset: 0;
+ z-index: 1;
+ display: var(--sponsor-title-rock-display, none);
+ filter: var(
+ --sponsor-rock-shadow,
+ drop-shadow(0 1.05rem 0.92rem rgb(4 24 35 / 0.18))
+ );
+ pointer-events: none;
+}
+
+.sponsorField {
+ display: grid;
+ justify-items: center;
+ gap: var(--sponsor-field-gap, 2.05rem);
+ translate: 0 var(--sponsor-field-offset-y, 0);
+}
+
+.slabStack {
+ display: grid;
+ width: 100%;
+ justify-items: center;
+ gap: var(--sponsor-slab-gap, 1.25rem);
+}
+
+.mediumStoneGrid,
+.smallStoneGrid {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+}
+
+.mediumStoneGrid {
+ width: min(100%, var(--sponsor-medium-grid-width, 42rem));
+ gap: var(--sponsor-medium-grid-gap, 1.85rem 2rem);
+ padding-top: var(--sponsor-medium-grid-padding-top, 0.35rem);
+}
+
+.smallStoneGrid {
+ width: min(100%, var(--sponsor-small-grid-width, 42rem));
+ gap: var(--sponsor-small-grid-gap, 1.75rem 1.8rem);
+}
+
+.smallStoneGrid > .sponsorLink:nth-child(3n + 1) {
+ translate: var(--sponsor-stone-left-offset, 0)
+ var(--sponsor-stone-edge-offset, 0);
+}
+
+.smallStoneGrid > .sponsorLink:nth-child(3n + 2) {
+ translate: 0 var(--sponsor-stone-center-offset, 0);
+}
+
+.smallStoneGrid > .sponsorLink:nth-child(3n) {
+ translate: var(--sponsor-stone-right-offset, 0)
+ var(--sponsor-stone-edge-offset, 0);
+}
+
+.sponsorLink {
+ --logo-height: var(--sponsor-base-logo-height, 3rem);
+ --logo-width: var(--sponsor-base-logo-width, 12rem);
+ --sponsor-logo-hover-scale: 1.035;
+ --sponsor-logo-mobile-hover-scale: var(--sponsor-logo-hover-scale);
+ --sponsor-logo-mobile-scale: var(--sponsor-logo-scale);
+ --sponsor-logo-scale: 1;
+ --sponsor-height: var(--sponsor-base-height, 7rem);
+ --sponsor-rock-bottom-offset: 0px;
+ --sponsor-width: var(--sponsor-base-width, 20rem);
+ --tier-glow-end-rgb: 255 255 255;
+ --tier-glow-hover-end-alpha: 0.62;
+ --tier-glow-hover-start-alpha: 0.9;
+ --tier-glow-pulse-strong-end-alpha: 0.66;
+ --tier-glow-pulse-strong-start-alpha: 0.98;
+ --tier-glow-pulse-soft-end-alpha: 0.4;
+ --tier-glow-pulse-soft-start-alpha: 0.74;
+ --tier-glow-radius-scale: 1;
+ --tier-glow-start-rgb: 255 255 255;
+ --tier-glow-halo-strength: 1;
+ --tier-tint-rgb: 255 255 255;
+ --tier-tint-rest-opacity: 0.68;
+ --tier-tint-opacity: 0.72;
+ --tier-tint-blend: color;
+ --tier-rock-filter: grayscale(0) sepia(0) saturate(1) hue-rotate(0deg)
+ brightness(1) contrast(1);
+ --tier-rock-hover-filter: var(--tier-rock-filter);
+ --sponsor-shine-duration: 2.6s;
+ --sponsor-shine-strength: 0.9;
+ --sponsor-metallic-rest-strength: 0.28;
+ --sponsor-metallic-strength: 0.85;
+ --sponsor-rock-surface-hover-filter: contrast(1.16) saturate(1.12);
+
+ position: relative;
+ display: block;
+ width: var(--sponsor-width);
+ height: var(--sponsor-height);
+ text-decoration: none;
+ outline: none;
+ isolation: isolate;
+}
+
+.sponsorLink[data-size="x-large"] {
+ --logo-height: var(--sponsor-xl-logo-height, 3.65rem);
+ --logo-width: var(--sponsor-xl-logo-width, 15.6rem);
+ --sponsor-height: var(--sponsor-xl-height, 8.6rem);
+ --sponsor-rock-bottom-offset: calc(var(--sponsor-height) * 0.08);
+ --sponsor-width: var(--sponsor-xl-width, clamp(24rem, 76vw, 35rem));
+}
+
+.sponsorLink[data-size="large"] {
+ --logo-height: var(--sponsor-large-logo-height, 3.25rem);
+ --logo-width: var(--sponsor-large-logo-width, 13.8rem);
+ --sponsor-height: var(--sponsor-large-height, 7.9rem);
+ --sponsor-rock-bottom-offset: calc(var(--sponsor-height) * 0.08);
+ --sponsor-width: var(--sponsor-large-width, clamp(21rem, 66vw, 30.5rem));
+}
+
+.sponsorLink[data-size="medium"] {
+ --logo-height: var(--sponsor-medium-logo-height, 2.4rem);
+ --logo-width: var(--sponsor-medium-logo-width, 8.25rem);
+ --sponsor-height: var(--sponsor-medium-height, 12rem);
+ --sponsor-width: var(--sponsor-medium-width, clamp(13.4rem, 36vw, 16rem));
+}
+
+.sponsorLink[data-size="small"] {
+ --logo-height: var(--sponsor-small-logo-height, 2.05rem);
+ --logo-width: var(--sponsor-small-logo-width, 7.25rem);
+ --sponsor-height: var(--sponsor-small-height, 8.75rem);
+ --sponsor-width: var(--sponsor-small-width, clamp(9.4rem, 24vw, 11.2rem));
+}
+
+.sponsorLink[data-size="x-small"] {
+ --logo-height: var(--sponsor-xs-logo-height, 1.85rem);
+ --logo-width: var(--sponsor-xs-logo-width, 6.6rem);
+ --sponsor-height: var(--sponsor-xs-height, 7.45rem);
+ --sponsor-width: var(--sponsor-xs-width, clamp(8.2rem, 20vw, 9.6rem));
+}
+
+/* Color the rock bitmap itself only during interaction. This preserves the
+ asset's transparency and works without a cross-origin CSS mask. */
+.sponsorLink[data-tier="bronze-ember"] {
+ --tier-rock-hover-filter: grayscale(0.34) sepia(1) saturate(2.6)
+ hue-rotate(332deg) brightness(0.9) contrast(1.2);
+}
+
+.sponsorLink[data-tier="silver-moon"] {
+ --tier-rock-hover-filter: grayscale(0.88) sepia(0.1) saturate(0.58)
+ hue-rotate(158deg) brightness(1.04) contrast(1.22);
+}
+
+.sponsorLink[data-tier="golden-dawn"] {
+ --tier-rock-hover-filter: grayscale(0.2) sepia(1) saturate(3.05)
+ hue-rotate(354deg) brightness(0.96) contrast(1.2);
+}
+
+.sponsorLink[data-tier="platinum-crown"] {
+ --tier-rock-hover-filter: grayscale(0.46) sepia(0.46) saturate(1.72)
+ hue-rotate(188deg) brightness(1) contrast(1.22);
+}
+
+.sponsorLink[data-tier="forest-sovereign"] {
+ --tier-rock-hover-filter: grayscale(0.26) sepia(0.9) saturate(2.75)
+ hue-rotate(232deg) brightness(0.84) contrast(1.24);
+}
+
+.sponsorVisual {
+ box-sizing: border-box;
+ position: absolute;
+ inset: 0;
+ display: grid;
+ grid-template-rows: max-content;
+ grid-template-columns: 100%;
+ align-content: end;
+ justify-items: center;
+ filter: var(
+ --sponsor-rock-shadow,
+ drop-shadow(0 1.05rem 0.92rem rgb(4 24 35 / 0.18))
+ );
+ overflow: visible;
+ padding-bottom: var(--sponsor-rock-bottom-offset);
+ transform-origin: 50% 58%;
+}
+
+.sponsorVisual > .rockFrame,
+.sponsorVisual > .sponsorLogo {
+ grid-area: 1 / 1;
+}
+
+.rockFrame {
+ position: relative;
+ z-index: 1;
+ display: block;
+ overflow: hidden;
+ pointer-events: none;
+}
+
+.rockFrame::before {
+ position: absolute;
+ display: block;
+ aspect-ratio: 1;
+ content: "";
+ top: var(--rock-offset-top);
+ left: var(--rock-offset-left);
+ width: var(--rock-sprite-width);
+ background-image: var(--rock-image);
+ background-position: 0 0;
+ background-repeat: no-repeat;
+ background-size: 100% auto;
+ filter: var(--sponsor-rock-surface-filter, brightness(1))
+ var(--tier-rock-filter, brightness(1));
+ transition: filter 100ms ease;
+ user-select: none;
+ pointer-events: none;
+}
+
+/* Tier material layer: the color lives in the rock itself rather than in an
+ exterior halo. `color` preserves the illustrated stone's light and depth. */
+.rockFrame::after {
+ position: absolute;
+ display: block;
+ aspect-ratio: 1;
+ content: "";
+ top: var(--rock-offset-top);
+ left: var(--rock-offset-left);
+ width: var(--rock-sprite-width);
+ background-color: rgb(var(--tier-tint-rgb));
+ mask-image: var(--rock-image);
+ mask-repeat: no-repeat;
+ mask-position: 0 0;
+ mask-size: 100% auto;
+ -webkit-mask-image: var(--rock-image);
+ -webkit-mask-repeat: no-repeat;
+ -webkit-mask-position: 0 0;
+ -webkit-mask-size: 100% auto;
+ mix-blend-mode: var(--tier-tint-blend, multiply);
+ opacity: 0;
+ transition: opacity 200ms ease;
+ user-select: none;
+ pointer-events: none;
+}
+
+/* Chrome-style shading ramp: a bright top highlight, a dark mid-band, a lower
+ secondary highlight and a dark base. Blended with `overlay` over the tinted
+ stone, it fakes a reflective metal surface. Masked to the silhouette. */
+.rockSheen {
+ position: absolute;
+ z-index: 1;
+ display: block;
+ aspect-ratio: 1;
+ top: var(--rock-offset-top);
+ left: var(--rock-offset-left);
+ width: var(--rock-sprite-width);
+ background-image: linear-gradient(
+ 168deg,
+ rgb(var(--tier-glow-end-rgb) / 0.7) 0%,
+ rgb(255 255 255 / 0.14) 20%,
+ rgb(8 12 18 / 0) 42%,
+ rgb(8 12 18 / 0.24) 60%,
+ rgb(var(--tier-glow-end-rgb) / 0.4) 78%,
+ rgb(8 12 18 / 0.3) 100%
+ );
+ mask-image: var(--rock-image);
+ mask-repeat: no-repeat;
+ mask-position: 0 0;
+ mask-size: 100% auto;
+ -webkit-mask-image: var(--rock-image);
+ -webkit-mask-repeat: no-repeat;
+ -webkit-mask-position: 0 0;
+ -webkit-mask-size: 100% auto;
+ mix-blend-mode: overlay;
+ opacity: var(--sponsor-metallic-rest-strength, 0.28);
+ transition: opacity 700ms cubic-bezier(0.16, 1, 0.3, 1);
+ user-select: none;
+ pointer-events: none;
+}
+
+/* Metallic specular sweep — a diagonal highlight that travels across the
+ stone on hover, masked to the silhouette so it stays on the rock. */
+.rockShine {
+ position: absolute;
+ z-index: 2;
+ display: block;
+ aspect-ratio: 1;
+ top: var(--rock-offset-top);
+ left: var(--rock-offset-left);
+ width: var(--rock-sprite-width);
+ background-image: linear-gradient(
+ 115deg,
+ transparent 38%,
+ rgb(
+ var(--tier-glow-end-rgb) /
+ calc(0.62 * var(--sponsor-shine-strength, 0.9))
+ )
+ 46%,
+ rgb(255 255 255 / calc(0.72 * var(--sponsor-shine-strength, 0.9))) 48%,
+ rgb(255 255 255 / var(--sponsor-shine-strength, 0.9)) 50%,
+ rgb(255 255 255 / calc(0.72 * var(--sponsor-shine-strength, 0.9))) 52%,
+ rgb(
+ var(--tier-glow-end-rgb) /
+ calc(0.62 * var(--sponsor-shine-strength, 0.9))
+ )
+ 54%,
+ transparent 62%
+ );
+ background-repeat: no-repeat;
+ background-size: 250% 100%;
+ background-position: 200% 0;
+ mask-image: var(--rock-image);
+ mask-repeat: no-repeat;
+ mask-position: 0 0;
+ mask-size: 100% auto;
+ -webkit-mask-image: var(--rock-image);
+ -webkit-mask-repeat: no-repeat;
+ -webkit-mask-position: 0 0;
+ -webkit-mask-size: 100% auto;
+ mix-blend-mode: screen;
+ opacity: 0;
+ transition: opacity 700ms cubic-bezier(0.16, 1, 0.3, 1);
+ user-select: none;
+ pointer-events: none;
+}
+
+.sponsorLink[data-size="medium"] .rockFrame,
+.sponsorLink[data-size="small"] .rockFrame,
+.sponsorLink[data-size="x-small"] .rockFrame {
+ width: 100%;
+ aspect-ratio: 400 / 292;
+ --rock-image: url("https://assets.knighthacks.org/khix/small-sponsor-rock.webp");
+ --rock-offset-top: -209.59%;
+ --rock-offset-left: -56.5%;
+ --rock-sprite-width: 375%;
+}
+
+.sponsorLink[data-size="x-large"] .rockFrame,
+.sponsorLink[data-size="large"] .rockFrame,
+.sponsorTitleRock .rockFrame {
+ width: 100%;
+ aspect-ratio: 1196 / 262;
+ --rock-image: url("https://assets.knighthacks.org/khix/large-sponsor-rock.webp");
+ --rock-offset-top: -85.12%;
+ --rock-offset-left: -11.96%;
+ --rock-sprite-width: 125.42%;
+}
+
+.sponsorTitleRock .rockFrame {
+ position: relative;
+ inset: auto;
+ margin: 0;
+}
+
+.sponsorLogo {
+ position: relative;
+ z-index: 3;
+ display: block;
+ place-self: center;
+ width: min(var(--logo-width), 76%);
+ height: var(--logo-height);
+ filter: none;
+ opacity: var(--sponsor-logo-opacity, 0.94);
+ transform: scale(var(--sponsor-logo-scale));
+ transition:
+ filter 180ms ease,
+ opacity 180ms ease,
+ transform 180ms ease;
+}
+
+.sponsorLogoImage {
+ z-index: 1;
+ filter: var(--sponsor-logo-filter, drop-shadow(0 1px 0 rgb(4 18 28 / 0.35)));
+ object-fit: contain;
+ object-position: center;
+ pointer-events: none;
+ transition: filter 180ms ease;
+ user-select: none;
+}
+
+.sponsorLink:focus-visible .sponsorLogo {
+ filter: drop-shadow(
+ 0 0 0.14rem
+ rgb(
+ var(--tier-glow-start-rgb) /
+ calc(0.58 * var(--sponsor-logo-backlight-strength, 0))
+ )
+ )
+ drop-shadow(
+ 0 0 0.38rem
+ rgb(
+ var(--tier-glow-end-rgb) /
+ calc(0.42 * var(--sponsor-logo-backlight-strength, 0))
+ )
+ );
+}
+
+@media (hover: hover) and (pointer: fine) {
+ .sponsorLink:hover .sponsorLogo {
+ filter: drop-shadow(
+ 0 0 0.14rem
+ rgb(
+ var(--tier-glow-start-rgb) /
+ calc(0.58 * var(--sponsor-logo-backlight-strength, 0))
+ )
+ )
+ drop-shadow(
+ 0 0 0.38rem
+ rgb(
+ var(--tier-glow-end-rgb) /
+ calc(0.42 * var(--sponsor-logo-backlight-strength, 0))
+ )
+ );
+ }
+}
+
+.sponsorLink[data-size="small"] .sponsorLogo,
+.sponsorLink[data-size="x-small"] .sponsorLogo {
+ width: min(var(--logo-width), 78%);
+}
+
+.sponsorLink:hover .sponsorVisual,
+.sponsorLink:focus-visible .sponsorVisual {
+ animation: var(
+ --sponsor-rock-hover-animation,
+ sponsorHoverFloat 2.4s ease-in-out var(--float-delay) infinite alternate
+ );
+ will-change: var(--sponsor-rock-will-change, transform);
+}
+
+.sponsorLink:hover .rockFrame::before,
+.sponsorLink:focus-visible .rockFrame::before {
+ filter: var(--sponsor-rock-surface-filter, brightness(1))
+ var(--tier-rock-hover-filter, var(--tier-rock-filter, brightness(1)));
+}
+
+.sponsorLink:hover .rockFrame::after,
+.sponsorLink:focus-visible .rockFrame::after {
+ opacity: 0;
+}
+
+.sponsorLink:hover .rockSheen,
+.sponsorLink:focus-visible .rockSheen {
+ opacity: var(--sponsor-metallic-strength, 0.85);
+}
+
+.sponsorLink:hover .rockShine,
+.sponsorLink:focus-visible .rockShine {
+ opacity: 1;
+ animation: sponsorShineSweep var(--sponsor-shine-duration, 2.6s) ease-in-out
+ infinite;
+ will-change: background-position;
+}
+
+.sponsorLink:hover .sponsorLogo,
+.sponsorLink:focus-visible .sponsorLogo {
+ opacity: var(--sponsor-logo-hover-opacity, 1);
+ transform: scale(var(--sponsor-logo-scale));
+}
+
+.sponsorLink:hover .sponsorLogoImage,
+.sponsorLink:focus-visible .sponsorLogoImage {
+ animation: var(
+ --sponsor-logo-hover-animation,
+ sponsorRunePulse 1.65s ease-in-out infinite alternate
+ );
+ filter: var(
+ --sponsor-logo-hover-filter,
+ brightness(1.14) contrast(1.04) saturate(1.12)
+ drop-shadow(0 1px 0 rgb(4 18 28 / 0.35))
+ drop-shadow(0 0 0.04rem rgb(255 255 255 / 0.84))
+ drop-shadow(0 0 0.1rem rgb(255 255 255 / 0.58))
+ drop-shadow(
+ 0 0 calc(0.14rem * var(--tier-glow-radius-scale))
+ rgb(var(--tier-glow-start-rgb) / 0.5)
+ )
+ drop-shadow(
+ 0 0 calc(0.22rem * var(--tier-glow-radius-scale))
+ rgb(var(--tier-glow-start-rgb) / var(--tier-glow-hover-start-alpha))
+ )
+ drop-shadow(
+ 0 0 calc(0.4rem * var(--tier-glow-radius-scale))
+ rgb(
+ var(--tier-glow-end-rgb) /
+ calc(
+ var(--tier-glow-hover-end-alpha) *
+ var(--tier-glow-halo-strength)
+ )
+ )
+ )
+ );
+ will-change: var(--sponsor-logo-will-change, filter);
+}
+
+.sponsorLink[data-preserve-logo-contrast="true"] .sponsorLogoImage {
+ filter: var(
+ --sponsor-logo-contrast-filter,
+ var(--sponsor-logo-filter, drop-shadow(0 1px 0 rgb(4 18 28 / 0.35)))
+ );
+}
+
+.sponsorLink[data-preserve-logo-contrast="true"]:hover .sponsorLogoImage,
+.sponsorLink[data-preserve-logo-contrast="true"]:focus-visible
+ .sponsorLogoImage {
+ filter: var(
+ --sponsor-logo-contrast-hover-filter,
+ var(--sponsor-logo-hover-filter, var(--sponsor-logo-contrast-filter))
+ );
+}
+
+.sponsorLink:focus-visible .sponsorVisual {
+ filter: var(
+ --sponsor-rock-focus-shadow,
+ drop-shadow(0 0 0.16rem rgb(var(--tier-glow-end-rgb) / 0.9))
+ drop-shadow(0 0 0.38rem rgb(var(--tier-glow-start-rgb) / 0.54))
+ );
+}
+
+@media (max-width: 760px) {
+ .sponsorLink {
+ --tier-glow-halo-strength: 0.82;
+ }
+
+ .sponsorShowcase {
+ padding-top: var(--sponsor-showcase-mobile-padding-top, 2.6rem);
+ padding-bottom: var(--sponsor-showcase-mobile-padding-bottom, 3rem);
+ }
+
+ .sponsorTitle {
+ margin-bottom: var(--sponsor-title-mobile-margin-bottom, 1.45rem);
+ font-size: var(--sponsor-title-size, 2.55rem);
+ }
+
+ .sponsorField {
+ gap: clamp(0.8rem, 3vw, 1.15rem);
+ }
+
+ .slabStack {
+ gap: 0.8rem;
+ }
+
+ .mediumStoneGrid {
+ width: min(100%, 24rem);
+ gap: 0.8rem 0.65rem;
+ }
+
+ .smallStoneGrid {
+ width: 100%;
+ gap: 0.65rem 0.55rem;
+ }
+
+ .sponsorLink[data-size="x-large"],
+ .sponsorLink[data-size="large"] {
+ --logo-height: 2.65rem;
+ --logo-width: 11.25rem;
+ --sponsor-height: 6.8rem;
+ --sponsor-width: min(86vw, 24rem);
+ }
+
+ .sponsorLink[data-size="medium"] {
+ --logo-height: var(--sponsor-medium-mobile-logo-height, 2.12rem);
+ --logo-width: var(--sponsor-medium-mobile-logo-width, 7.25rem);
+ --sponsor-height: var(--sponsor-medium-mobile-height, 10.7rem);
+ --sponsor-width: var(--sponsor-medium-mobile-width, min(56vw, 13.8rem));
+ }
+
+ .sponsorLink[data-size="small"] {
+ --logo-height: clamp(1.45rem, 5vw, 1.85rem);
+ --logo-width: clamp(5.8rem, 20vw, 6.6rem);
+ --sponsor-height: clamp(5.3rem, 27vw, 7.45rem);
+ --sponsor-width: clamp(7.2rem, 37vw, 9.6rem);
+ }
+
+ .sponsorLink[data-size="x-small"] {
+ --logo-height: clamp(1.15rem, 4vw, 1.65rem);
+ --logo-width: clamp(4.1rem, 16vw, 5.9rem);
+ --sponsor-height: clamp(4.5rem, 22vw, 6.6rem);
+ --sponsor-width: clamp(4.8rem, 24vw, 7.6rem);
+ }
+
+ .sponsorLogo {
+ transform: scale(var(--sponsor-logo-mobile-scale));
+ }
+
+ .sponsorLink:hover .sponsorLogo,
+ .sponsorLink:focus-visible .sponsorLogo {
+ transform: scale(var(--sponsor-logo-mobile-hover-scale));
+ }
+}
+
+@media (max-width: 420px) {
+ .sponsorTitle {
+ font-size: var(--sponsor-title-size, 2.15rem);
+ }
+}
+
+@media (hover: none) {
+ .sponsorLogo {
+ opacity: var(--sponsor-logo-opacity, 1);
+ }
+
+ .sponsorLogoImage,
+ .sponsorLink:hover:not(:focus-visible) .sponsorLogoImage {
+ animation: var(
+ --sponsor-logo-mobile-animation,
+ sponsorMobileRuneBreath 4.8s ease-in-out var(--rune-delay) infinite
+ );
+ filter: var(
+ --sponsor-logo-mobile-filter,
+ var(--sponsor-logo-filter, drop-shadow(0 1px 0 rgb(4 18 28 / 0.35)))
+ );
+ will-change: var(--sponsor-logo-will-change, filter);
+ }
+
+ .sponsorLink[data-preserve-logo-contrast="true"] .sponsorLogoImage,
+ .sponsorLink[data-preserve-logo-contrast="true"]:hover:not(:focus-visible)
+ .sponsorLogoImage {
+ filter: var(
+ --sponsor-logo-contrast-mobile-filter,
+ var(--sponsor-logo-contrast-filter, var(--sponsor-logo-filter))
+ );
+ }
+
+ .sponsorLink:hover:not(:focus-visible) .sponsorLogo {
+ transform: scale(var(--sponsor-logo-mobile-scale));
+ }
+
+ .sponsorLink:hover:not(:focus-visible) .sponsorVisual {
+ animation: none;
+ will-change: auto;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .sponsorLogo {
+ transition: none;
+ }
+
+ .rockFrame::before,
+ .sponsorLogoImage {
+ animation: none;
+ transition: none;
+ }
+
+ .sponsorLink:hover .sponsorLogoImage,
+ .sponsorLink:focus-visible .sponsorLogoImage {
+ animation: none;
+ }
+
+ .sponsorVisual,
+ .sponsorLink:hover .sponsorVisual,
+ .sponsorLink:focus-visible .sponsorVisual {
+ animation: none;
+ }
+
+ .rockShine,
+ .sponsorLink:hover .rockShine,
+ .sponsorLink:focus-visible .rockShine {
+ animation: none;
+ transition: none;
+ }
+}
+
+@keyframes sponsorRunePulse {
+ 0% {
+ filter: brightness(1.1) contrast(1.03) saturate(1.08)
+ drop-shadow(0 1px 0 rgb(4 18 28 / 0.35))
+ drop-shadow(0 0 0.03rem rgb(255 255 255 / 0.78))
+ drop-shadow(0 0 0.08rem rgb(255 255 255 / 0.5))
+ drop-shadow(
+ 0 0 calc(0.12rem * var(--tier-glow-radius-scale))
+ rgb(var(--tier-glow-start-rgb) / 0.42)
+ )
+ drop-shadow(
+ 0 0 calc(0.18rem * var(--tier-glow-radius-scale))
+ rgb(
+ var(--tier-glow-start-rgb) / var(--tier-glow-pulse-soft-start-alpha)
+ )
+ )
+ drop-shadow(
+ 0 0 calc(0.3rem * var(--tier-glow-radius-scale))
+ rgb(
+ var(--tier-glow-end-rgb) /
+ calc(
+ var(--tier-glow-pulse-soft-end-alpha) *
+ var(--tier-glow-halo-strength)
+ )
+ )
+ );
+ }
+
+ 100% {
+ filter: brightness(1.17) contrast(1.05) saturate(1.16)
+ drop-shadow(0 1px 0 rgb(4 18 28 / 0.35))
+ drop-shadow(0 0 0.045rem rgb(255 255 255 / 0.9))
+ drop-shadow(0 0 0.11rem rgb(255 255 255 / 0.62))
+ drop-shadow(
+ 0 0 calc(0.16rem * var(--tier-glow-radius-scale))
+ rgb(var(--tier-glow-start-rgb) / 0.56)
+ )
+ drop-shadow(
+ 0 0 calc(0.24rem * var(--tier-glow-radius-scale))
+ rgb(
+ var(--tier-glow-start-rgb) /
+ var(--tier-glow-pulse-strong-start-alpha)
+ )
+ )
+ drop-shadow(
+ 0 0 calc(0.46rem * var(--tier-glow-radius-scale))
+ rgb(
+ var(--tier-glow-end-rgb) /
+ calc(
+ var(--tier-glow-pulse-strong-end-alpha) *
+ var(--tier-glow-halo-strength)
+ )
+ )
+ );
+ }
+}
+
+@keyframes sponsorMobileRuneBreath {
+ 0%,
+ 100% {
+ filter: brightness(1.01) contrast(1.01) saturate(1.01)
+ drop-shadow(0 1px 0 rgb(4 18 28 / 0.35))
+ drop-shadow(0 0 0.015rem rgb(255 255 255 / 0.22))
+ drop-shadow(
+ 0 0 calc(0.08rem * var(--tier-glow-radius-scale))
+ rgb(
+ var(--tier-glow-start-rgb) /
+ calc(var(--tier-glow-pulse-soft-start-alpha) * 0.18)
+ )
+ )
+ drop-shadow(
+ 0 0 calc(0.14rem * var(--tier-glow-radius-scale))
+ rgb(
+ var(--tier-glow-end-rgb) /
+ calc(
+ var(--tier-glow-pulse-soft-end-alpha) *
+ var(--tier-glow-halo-strength) * 0.18
+ )
+ )
+ );
+ }
+
+ 50% {
+ filter: brightness(1.08) contrast(1.03) saturate(1.06)
+ drop-shadow(0 1px 0 rgb(4 18 28 / 0.35))
+ drop-shadow(0 0 0.025rem rgb(255 255 255 / 0.55))
+ drop-shadow(0 0 0.06rem rgb(255 255 255 / 0.32))
+ drop-shadow(
+ 0 0 calc(0.1rem * var(--tier-glow-radius-scale))
+ rgb(var(--tier-glow-start-rgb) / 0.3)
+ )
+ drop-shadow(
+ 0 0 calc(0.16rem * var(--tier-glow-radius-scale))
+ rgb(
+ var(--tier-glow-start-rgb) /
+ calc(var(--tier-glow-pulse-soft-start-alpha) * 0.72)
+ )
+ )
+ drop-shadow(
+ 0 0 calc(0.3rem * var(--tier-glow-radius-scale))
+ rgb(
+ var(--tier-glow-end-rgb) /
+ calc(
+ var(--tier-glow-pulse-soft-end-alpha) *
+ var(--tier-glow-halo-strength) * 0.78
+ )
+ )
+ );
+ }
+}
+
+@keyframes sponsorShineSweep {
+ 0% {
+ background-position: 200% 0;
+ }
+
+ 45%,
+ 100% {
+ background-position: -120% 0;
+ }
+}
+
+@keyframes sponsorHoverFloat {
+ 0% {
+ transform: translate3d(0, -0.12rem, 0) rotate(-0.24deg);
+ }
+
+ 100% {
+ transform: translate3d(0, 0.14rem, 0) rotate(0.2deg);
+ }
+}
diff --git a/apps/khix/src/app/_components/sponsor-showcase/SponsorShowcase.tsx b/apps/khix/src/app/_components/sponsor-showcase/SponsorShowcase.tsx
new file mode 100644
index 000000000..fdaf6a8b0
--- /dev/null
+++ b/apps/khix/src/app/_components/sponsor-showcase/SponsorShowcase.tsx
@@ -0,0 +1,343 @@
+import type { CSSProperties } from "react";
+import Image from "next/image";
+
+import styles from "./SponsorShowcase.module.css";
+
+export const SPONSOR_TIER_ORDER = [
+ "bronze-ember",
+ "silver-moon",
+ "golden-dawn",
+ "platinum-crown",
+ "forest-sovereign",
+] as const;
+
+export type SponsorTier = (typeof SPONSOR_TIER_ORDER)[number];
+
+export interface SponsorShowcaseSponsor {
+ name: string;
+ websiteUrl: string;
+ logoSrc: string;
+ logoScale?: number;
+ mobileLogoScale?: number;
+ preserveLogoContrast?: boolean;
+ tier: SponsorTier;
+}
+
+export interface SponsorShowcaseProps {
+ sponsors: readonly SponsorShowcaseSponsor[];
+ className?: string;
+ title?: string;
+ titleId?: string;
+}
+
+type SponsorSize = "x-small" | "small" | "medium" | "large" | "x-large";
+type SponsorGroupName = "slab" | "medium-stone" | "small-stone";
+
+interface SponsorTierConfig {
+ glowBoost?: number;
+ glowEndColor: string;
+ glowStartColor: string;
+ size: SponsorSize;
+ tintColor: string;
+}
+
+type SponsorCardStyle = CSSProperties & {
+ "--float-delay": string;
+ "--rune-delay": string;
+ "--sponsor-logo-hover-scale": string;
+ "--sponsor-logo-mobile-hover-scale": string;
+ "--sponsor-logo-mobile-scale": string;
+ "--sponsor-logo-scale": string;
+ "--tier-glow-end-rgb": string;
+ "--tier-glow-hover-end-alpha": string;
+ "--tier-glow-hover-start-alpha": string;
+ "--tier-glow-pulse-strong-end-alpha": string;
+ "--tier-glow-pulse-strong-start-alpha": string;
+ "--tier-glow-pulse-soft-end-alpha": string;
+ "--tier-glow-pulse-soft-start-alpha": string;
+ "--tier-glow-radius-scale": string;
+ "--tier-glow-start-rgb": string;
+ "--tier-tint-rgb": string;
+};
+
+const SPONSOR_TIER_DISPLAY_ORDER = [
+ "forest-sovereign",
+ "platinum-crown",
+ "golden-dawn",
+ "silver-moon",
+ "bronze-ember",
+] as const satisfies readonly SponsorTier[];
+
+const SPONSOR_TIER_CONFIG = {
+ "bronze-ember": {
+ glowBoost: 1.26,
+ glowEndColor: "#CD7F32D9",
+ glowStartColor: "#5F2A00B2",
+ size: "x-small",
+ tintColor: "#B5561A",
+ },
+ "silver-moon": {
+ glowBoost: 1.48,
+ glowEndColor: "#F7FDFFFF",
+ glowStartColor: "#BECBD1D9",
+ size: "small",
+ tintColor: "#8FA6B5",
+ },
+ "golden-dawn": {
+ glowBoost: 1.26,
+ glowEndColor: "#FFE45CD9",
+ glowStartColor: "#C88A00C7",
+ size: "medium",
+ tintColor: "#FFB400",
+ },
+ "platinum-crown": {
+ glowBoost: 1.34,
+ glowEndColor: "#D7DCFFFF",
+ glowStartColor: "#5748D9E6",
+ size: "large",
+ tintColor: "#8C93E8",
+ },
+ "forest-sovereign": {
+ glowEndColor: "#CE2CFF",
+ glowStartColor: "#4E007B",
+ size: "x-large",
+ tintColor: "#A020F0",
+ },
+} as const satisfies Record;
+
+const SPONSOR_GLOW_ALPHA = {
+ hoverEnd: 0.62,
+ hoverStart: 0.9,
+ pulseSoftEnd: 0.4,
+ pulseSoftStart: 0.74,
+ pulseStrongEnd: 0.66,
+ pulseStrongStart: 0.98,
+} as const;
+
+function getHexColorParts(hexColor: string) {
+ const normalizedHex = hexColor.replace("#", "");
+ const red = Number.parseInt(normalizedHex.slice(0, 2), 16);
+ const green = Number.parseInt(normalizedHex.slice(2, 4), 16);
+ const blue = Number.parseInt(normalizedHex.slice(4, 6), 16);
+ const alpha =
+ normalizedHex.length === 8
+ ? Number.parseInt(normalizedHex.slice(6, 8), 16) / 255
+ : 1;
+
+ return {
+ alpha: alpha.toFixed(3),
+ rgb: `${red} ${green} ${blue}`,
+ };
+}
+
+function getBoostedAlpha(
+ colorAlpha: string,
+ baseAlpha: number,
+ glowBoost: number,
+) {
+ const alpha = Number.parseFloat(colorAlpha);
+
+ return Math.min(alpha * baseAlpha * glowBoost, 1).toFixed(3);
+}
+
+function getOrderedSponsors(sponsors: readonly SponsorShowcaseSponsor[]) {
+ return SPONSOR_TIER_DISPLAY_ORDER.flatMap((tier) =>
+ sponsors.filter((sponsor) => sponsor.tier === tier),
+ );
+}
+
+function getSponsorCardKey(
+ groupName: SponsorGroupName,
+ sponsor: SponsorShowcaseSponsor,
+ index: number,
+) {
+ return `${groupName}-${index}-${sponsor.tier}-${sponsor.name}-${sponsor.websiteUrl}`;
+}
+
+function SponsorRockCard({
+ index,
+ sponsor,
+}: {
+ index: number;
+ sponsor: SponsorShowcaseSponsor;
+}) {
+ const tierConfig = SPONSOR_TIER_CONFIG[sponsor.tier];
+ const glowStartColor = getHexColorParts(tierConfig.glowStartColor);
+ const glowEndColor = getHexColorParts(tierConfig.glowEndColor);
+ const glowBoost = "glowBoost" in tierConfig ? tierConfig.glowBoost : 1;
+ const tintColor = getHexColorParts(tierConfig.tintColor);
+ const logoScale = sponsor.logoScale ?? 1;
+ const mobileLogoScale = sponsor.mobileLogoScale ?? logoScale;
+ const glowRadiusScale = 1 + (glowBoost - 1) * 0.5;
+ const style: SponsorCardStyle = {
+ "--float-delay": `${-(index % 6) * 0.42}s`,
+ "--rune-delay": `${-index * 0.43}s`,
+ "--sponsor-logo-hover-scale": (logoScale * 1.035).toFixed(3),
+ "--sponsor-logo-mobile-hover-scale": (mobileLogoScale * 1.035).toFixed(3),
+ "--sponsor-logo-mobile-scale": mobileLogoScale.toString(),
+ "--sponsor-logo-scale": logoScale.toString(),
+ "--tier-glow-end-rgb": glowEndColor.rgb,
+ "--tier-glow-hover-end-alpha": getBoostedAlpha(
+ glowEndColor.alpha,
+ SPONSOR_GLOW_ALPHA.hoverEnd,
+ glowBoost,
+ ),
+ "--tier-glow-hover-start-alpha": getBoostedAlpha(
+ glowStartColor.alpha,
+ SPONSOR_GLOW_ALPHA.hoverStart,
+ glowBoost,
+ ),
+ "--tier-glow-pulse-strong-end-alpha": getBoostedAlpha(
+ glowEndColor.alpha,
+ SPONSOR_GLOW_ALPHA.pulseStrongEnd,
+ glowBoost,
+ ),
+ "--tier-glow-pulse-strong-start-alpha": getBoostedAlpha(
+ glowStartColor.alpha,
+ SPONSOR_GLOW_ALPHA.pulseStrongStart,
+ glowBoost,
+ ),
+ "--tier-glow-pulse-soft-end-alpha": getBoostedAlpha(
+ glowEndColor.alpha,
+ SPONSOR_GLOW_ALPHA.pulseSoftEnd,
+ glowBoost,
+ ),
+ "--tier-glow-pulse-soft-start-alpha": getBoostedAlpha(
+ glowStartColor.alpha,
+ SPONSOR_GLOW_ALPHA.pulseSoftStart,
+ glowBoost,
+ ),
+ "--tier-glow-radius-scale": glowRadiusScale.toFixed(3),
+ "--tier-glow-start-rgb": glowStartColor.rgb,
+ "--tier-tint-rgb": tintColor.rgb,
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function SponsorShowcase({
+ className,
+ sponsors,
+ title = "Sponsors",
+ titleId = "khix-sponsors-title",
+}: SponsorShowcaseProps) {
+ const orderedSponsors = getOrderedSponsors(sponsors);
+ const slabSponsors = orderedSponsors.filter(
+ (sponsor) =>
+ SPONSOR_TIER_CONFIG[sponsor.tier].size === "large" ||
+ SPONSOR_TIER_CONFIG[sponsor.tier].size === "x-large",
+ );
+ const mediumStoneSponsors = orderedSponsors.filter(
+ (sponsor) => SPONSOR_TIER_CONFIG[sponsor.tier].size === "medium",
+ );
+ const smallStoneSponsors = orderedSponsors.filter(
+ (sponsor) => SPONSOR_TIER_CONFIG[sponsor.tier].size === "small",
+ );
+ const extraSmallStoneSponsors = orderedSponsors.filter(
+ (sponsor) => SPONSOR_TIER_CONFIG[sponsor.tier].size === "x-small",
+ );
+ const sponsorShowcaseClassName = className
+ ? `${styles.sponsorShowcase} ${className}`
+ : styles.sponsorShowcase;
+
+ if (orderedSponsors.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ {title}
+
+
+
+ {slabSponsors.length > 0 ? (
+
+ {slabSponsors.map((sponsor, index) => (
+
+ ))}
+
+ ) : null}
+
+ {mediumStoneSponsors.length > 0 ? (
+
+ {mediumStoneSponsors.map((sponsor, index) => (
+
+ ))}
+
+ ) : null}
+
+ {smallStoneSponsors.length > 0 ? (
+
+ {smallStoneSponsors.map((sponsor, index) => (
+
+ ))}
+
+ ) : null}
+
+ {extraSmallStoneSponsors.length > 0 ? (
+
+ {extraSmallStoneSponsors.map((sponsor, index) => (
+
+ ))}
+
+ ) : null}
+
+
+ );
+}
diff --git a/apps/khix/src/app/_components/sponsor-showcase/index.ts b/apps/khix/src/app/_components/sponsor-showcase/index.ts
new file mode 100644
index 000000000..eb35a17ee
--- /dev/null
+++ b/apps/khix/src/app/_components/sponsor-showcase/index.ts
@@ -0,0 +1,7 @@
+export {
+ SponsorShowcase,
+ SPONSOR_TIER_ORDER,
+ type SponsorShowcaseProps,
+ type SponsorShowcaseSponsor,
+ type SponsorTier,
+} from "./SponsorShowcase";
diff --git a/apps/khix/src/app/_components/team-cascade/TeamCascade.module.css b/apps/khix/src/app/_components/team-cascade/TeamCascade.module.css
new file mode 100644
index 000000000..1365852e5
--- /dev/null
+++ b/apps/khix/src/app/_components/team-cascade/TeamCascade.module.css
@@ -0,0 +1,498 @@
+.teamCascade {
+ --team-cascade-default-avatar-size: clamp(4.75rem, 7vw, 6.25rem);
+ --team-avatar-size: var(
+ --team-cascade-avatar-size,
+ var(--team-cascade-default-avatar-size)
+ );
+
+ position: relative;
+ display: grid;
+ width: 100%;
+ isolation: isolate;
+}
+
+.teamCascadeStatusFrame {
+ position: relative;
+ display: grid;
+ min-height: clamp(14rem, 34vw, 26rem);
+ place-items: center;
+}
+
+.teamRosterSection {
+ display: grid;
+ min-width: 0;
+ justify-items: stretch;
+ animation: teamRosterReveal 420ms ease-out both;
+}
+
+.teamRoster {
+ display: flex;
+ flex-wrap: wrap;
+ width: 100%;
+ align-items: center;
+ justify-content: center;
+ gap: clamp(1.5rem, 2vw, 2rem) clamp(1.5rem, 2.8vw, 2.5rem);
+ padding: clamp(0.5rem, 1vw, 0.75rem) 0;
+}
+
+.teamProfile {
+ position: relative;
+ display: grid;
+ flex: 0 0 var(--team-avatar-size);
+ min-width: 0;
+ justify-items: center;
+}
+
+.teamAvatar {
+ --bubble-glow: rgba(94, 211, 246, 0.42);
+ --splash-color: rgba(114, 223, 255, 0.96);
+
+ position: relative;
+ display: inline-flex;
+ width: var(--team-avatar-size);
+ aspect-ratio: 1;
+ padding: 0;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid rgba(224, 250, 255, 0.96);
+ border-radius: 999px;
+ color: #ffffff;
+ appearance: none;
+ background:
+ linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.72),
+ rgba(67, 186, 232, 0.22)
+ ),
+ linear-gradient(145deg, #125f9d 0%, #269fdb 55%, #bdefff 100%);
+ box-shadow:
+ 0 0 0 0.14rem rgba(82, 192, 235, 0.42),
+ 0 0 0 0.22rem rgba(222, 249, 255, 0.56),
+ 0 0.7rem 1.55rem rgba(17, 86, 133, 0.22),
+ inset 0 0 0 1px rgba(255, 255, 255, 0.36);
+ cursor: pointer;
+ overflow: visible;
+ isolation: isolate;
+ transition:
+ border-color 180ms ease,
+ box-shadow 180ms ease,
+ filter 180ms ease,
+ transform 180ms ease;
+ transform: translate3d(0, 0, 0);
+}
+
+.teamAvatar::before {
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ border-radius: inherit;
+ content: "";
+ background: rgba(255, 255, 255, 0.1);
+ pointer-events: none;
+}
+
+.teamAvatar::after {
+ position: absolute;
+ inset: -1.2rem;
+ z-index: 4;
+ border-radius: 50%;
+ content: "";
+ background:
+ radial-gradient(
+ circle at 50% 3%,
+ var(--splash-color) 0 0.13rem,
+ transparent 0.18rem
+ ),
+ radial-gradient(
+ circle at 82% 13%,
+ var(--splash-color) 0 0.1rem,
+ transparent 0.15rem
+ ),
+ radial-gradient(
+ circle at 96% 43%,
+ var(--splash-color) 0 0.15rem,
+ transparent 0.21rem
+ ),
+ radial-gradient(
+ circle at 83% 82%,
+ var(--splash-color) 0 0.1rem,
+ transparent 0.15rem
+ ),
+ radial-gradient(
+ circle at 52% 97%,
+ var(--splash-color) 0 0.13rem,
+ transparent 0.18rem
+ ),
+ radial-gradient(
+ circle at 16% 83%,
+ var(--splash-color) 0 0.15rem,
+ transparent 0.21rem
+ ),
+ radial-gradient(
+ circle at 3% 45%,
+ var(--splash-color) 0 0.1rem,
+ transparent 0.15rem
+ ),
+ radial-gradient(
+ circle at 19% 13%,
+ var(--splash-color) 0 0.13rem,
+ transparent 0.18rem
+ );
+ opacity: 0;
+ pointer-events: none;
+ transform: scale(0.48) rotate(-8deg);
+}
+
+.teamAvatar:hover,
+.teamAvatar:focus-visible,
+.teamAvatar[data-active="true"] {
+ z-index: 2;
+ border-color: #ffffff;
+ box-shadow:
+ 0 0 0 0.18rem rgba(221, 249, 255, 0.7),
+ 0 0 1.8rem var(--bubble-glow),
+ 0 1rem 2rem rgba(17, 86, 133, 0.24);
+ filter: saturate(1.06) brightness(1.03);
+ outline: none;
+}
+
+.teamAvatar:hover,
+.teamAvatar:focus-visible {
+ transform: translate3d(0, -2px, 0) scale(1.035);
+}
+
+.teamAvatar:focus-visible {
+ outline: 2px solid rgba(224, 250, 255, 0.98);
+ outline-offset: 0.42rem;
+}
+
+.teamAvatar:hover::after,
+.teamAvatar:focus-visible::after {
+ animation: teamAvatarSplash 650ms cubic-bezier(0.18, 0.7, 0.2, 1) both;
+}
+
+.teamAvatarMedia {
+ position: absolute;
+ inset: 0.06rem;
+ z-index: 1;
+ border-radius: inherit;
+ background: #103247;
+ overflow: hidden;
+ transform: translate3d(0, 0, 0);
+}
+
+.teamAvatarImage,
+.teamAvatarInitials {
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+ border-radius: inherit;
+}
+
+.teamAvatarImage {
+ object-fit: cover;
+}
+
+.teamAvatarInitials {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: #ffffff;
+ font-size: clamp(1rem, 2vw, 1.8rem);
+ font-weight: 950;
+}
+
+.teamMemberDetails {
+ display: grid;
+ min-height: 0;
+ margin-bottom: clamp(0.65rem, 1.2vw, 1rem);
+ justify-items: center;
+ align-content: start;
+ text-align: center;
+}
+
+.teamMemberDetailsContent {
+ display: grid;
+ justify-items: center;
+ animation: teamMemberDetailsReveal 180ms ease-out both;
+}
+
+.teamMemberName {
+ max-width: min(100%, 32rem);
+ margin: 0;
+ color: rgba(255, 252, 245, 0.98);
+ font-family: var(--font-cormorant-garamond, Georgia), Georgia, serif;
+ font-size: clamp(1.75rem, 4vw, 2.65rem);
+ font-weight: 600;
+ line-height: 0.95;
+ text-wrap: balance;
+ text-shadow:
+ 0 0.12rem 0.75rem rgba(0, 0, 0, 0.5),
+ 0 0 1.3rem rgba(31, 174, 214, 0.32);
+}
+
+.teamMemberRoleRow {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.35rem;
+ margin-top: 0.55rem;
+}
+
+.teamMemberRole {
+ max-width: min(100%, 26rem);
+ margin: 0;
+ color: rgba(224, 249, 255, 0.82);
+ font-family:
+ var(--font-geist-sans, ui-sans-serif),
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ sans-serif;
+ font-size: clamp(0.72rem, 1.25vw, 0.88rem);
+ font-weight: 850;
+ line-height: 1.35;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ text-wrap: balance;
+ text-shadow: 0 0.1rem 0.65rem rgba(0, 0, 0, 0.46);
+ white-space: nowrap;
+}
+
+.teamLinkedInLink,
+.teamLinkedInUnavailable {
+ margin-top: 0.75rem;
+ font-family:
+ var(--font-geist-sans, ui-sans-serif),
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ sans-serif;
+ font-size: 0.8rem;
+ font-weight: 800;
+ line-height: 1.3;
+}
+
+.teamLinkedInLink {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3rem;
+ color: rgba(238, 253, 255, 0.96);
+ text-shadow: 0 0.1rem 0.65rem rgba(0, 0, 0, 0.46);
+ text-decoration-color: rgba(155, 231, 250, 0.65);
+ text-decoration-thickness: 1px;
+ text-underline-offset: 0.22rem;
+ transition:
+ color 160ms ease,
+ text-decoration-color 160ms ease,
+ transform 160ms ease;
+}
+
+.teamLinkedInLink:hover,
+.teamLinkedInLink:focus-visible {
+ color: #ffffff;
+ text-decoration-color: #ffffff;
+ outline: none;
+ transform: translateY(-1px);
+}
+
+.teamLinkedInLink:focus-visible {
+ border-radius: 0.15rem;
+ outline: 2px solid rgba(224, 250, 255, 0.9);
+ outline-offset: 0.3rem;
+}
+
+.teamLinkedInUnavailable {
+ color: rgba(224, 249, 255, 0.52);
+}
+
+.teamLinkedInIconLink {
+ display: none;
+ color: rgba(238, 253, 255, 0.92);
+ filter: drop-shadow(0 0.1rem 0.65rem rgba(0, 0, 0, 0.46));
+ text-decoration: none;
+}
+
+.srOnly {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ border: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+}
+
+.teamStatus {
+ margin: 0;
+ padding: 1rem;
+ color: var(--team-status-color, rgba(16, 50, 71, 0.74));
+ font-size: 0.95rem;
+ font-weight: 750;
+ line-height: 1.5;
+ text-align: center;
+}
+
+@media (max-width: 920px) {
+ .teamCascade {
+ --team-cascade-default-avatar-size: clamp(4rem, 9vw, 5.5rem);
+ }
+
+ .teamRoster {
+ gap: clamp(1.35rem, 2.6vw, 1.9rem) clamp(0.9rem, 2.2vw, 1.4rem);
+ }
+}
+
+@media (max-width: 760px) {
+ .teamCascade {
+ --team-avatar-size: clamp(2.5rem, 11.5vw, 4.5rem);
+ }
+
+ .teamMemberDetails {
+ min-height: 0;
+ margin-bottom: 0.25rem;
+ padding: 0 0.5rem;
+ }
+
+ .teamMemberName {
+ font-size: clamp(1.15rem, 4.5vw, 1.55rem);
+ }
+
+ .teamMemberRoleRow {
+ gap: 0.25rem;
+ margin-top: 0.25rem;
+ }
+
+ .teamMemberRole {
+ font-size: clamp(0.6rem, 1.6vw, 0.7rem);
+ }
+
+ .teamLinkedInLink,
+ .teamLinkedInUnavailable {
+ display: none;
+ }
+
+ .teamLinkedInIconLink {
+ display: inline-grid;
+ width: 1.15rem;
+ height: 1.15rem;
+ place-items: center;
+ border-radius: 0.18rem;
+ transition:
+ color 160ms ease,
+ transform 160ms ease;
+ }
+
+ .teamLinkedInIconLink:hover,
+ .teamLinkedInIconLink:focus-visible {
+ color: #ffffff;
+ outline: 1px solid rgba(224, 250, 255, 0.9);
+ outline-offset: 0.14rem;
+ transform: translateY(-1px);
+ }
+
+ .teamRoster {
+ --mobile-roster-inset: clamp(0.75rem, 3vw, 1rem);
+
+ display: grid;
+ grid-template-columns: repeat(5, var(--team-avatar-size));
+ width: calc(100% - var(--mobile-roster-inset));
+ max-width: none;
+ margin-left: var(--mobile-roster-inset);
+ justify-self: stretch;
+ gap: clamp(0.7rem, 2.5vw, 1rem) clamp(0.35rem, 1.4vw, 0.55rem);
+ padding-top: 0.15rem;
+ transform: none;
+ }
+
+ .teamProfile:last-child:nth-child(5n + 1) {
+ grid-column: 3;
+ }
+}
+
+@media (max-width: 520px) {
+ .teamCascade {
+ --team-avatar-size: clamp(2.5rem, 12vw, 3.25rem);
+ }
+
+ .teamRoster {
+ gap: clamp(0.55rem, 2.2vw, 0.75rem) clamp(0.3rem, 1.3vw, 0.5rem);
+ }
+
+ .teamMemberDetails {
+ min-height: 0;
+ margin-bottom: 0.2rem;
+ padding: 0 0.25rem;
+ }
+
+ .teamMemberName {
+ font-size: clamp(1.15rem, 5vw, 1.4rem);
+ }
+
+ .teamMemberRole {
+ font-size: 0.6rem;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .teamRosterSection,
+ .teamMemberDetailsContent {
+ animation: none;
+ }
+
+ .teamAvatar,
+ .teamLinkedInLink,
+ .teamLinkedInIconLink {
+ transition: none;
+ }
+
+ .teamAvatar:hover::after,
+ .teamAvatar:focus-visible::after {
+ opacity: 0;
+ animation: none;
+ }
+}
+
+@keyframes teamRosterReveal {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes teamMemberDetailsReveal {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes teamAvatarSplash {
+ 0% {
+ opacity: 0;
+ transform: scale(0.48) rotate(-8deg);
+ }
+
+ 34% {
+ opacity: 1;
+ }
+
+ 72% {
+ opacity: 0.78;
+ }
+
+ 100% {
+ opacity: 0;
+ transform: scale(1.16) rotate(7deg);
+ }
+}
diff --git a/apps/khix/src/app/_components/team-cascade/TeamCascade.tsx b/apps/khix/src/app/_components/team-cascade/TeamCascade.tsx
new file mode 100644
index 000000000..402c424c1
--- /dev/null
+++ b/apps/khix/src/app/_components/team-cascade/TeamCascade.tsx
@@ -0,0 +1,10 @@
+import { env } from "~/env";
+import { TeamCascadeClient } from "./TeamCascadeClient";
+
+interface TeamCascadeProps {
+ className?: string;
+}
+
+export function TeamCascade({ className }: TeamCascadeProps) {
+ return ;
+}
diff --git a/apps/khix/src/app/_components/team-cascade/TeamCascadeClient.tsx b/apps/khix/src/app/_components/team-cascade/TeamCascadeClient.tsx
new file mode 100644
index 000000000..ad201ed6f
--- /dev/null
+++ b/apps/khix/src/app/_components/team-cascade/TeamCascadeClient.tsx
@@ -0,0 +1,334 @@
+"use client";
+
+import type { ImageLoaderProps } from "next/image";
+import type { ReactNode } from "react";
+import { useEffect, useId, useMemo, useState } from "react";
+import Image from "next/image";
+import { FaLinkedin } from "react-icons/fa";
+
+import type {
+ TeamCascadeGroup,
+ TeamCascadeMember,
+ TeamCascadeRole,
+} from "./team-roster";
+import { loadTeamCascadeGroups } from "./team-roster";
+import styles from "./TeamCascade.module.css";
+
+type TeamCascadeStatus = "loading" | "ready" | "error";
+
+interface TeamCascadePerson {
+ member: TeamCascadeMember;
+ profileKey: string;
+ roleLabel: TeamCascadeRole;
+}
+
+const profileImageLoader = ({ src }: ImageLoaderProps) => src;
+const rosterIdPrefixes = [
+ "executive-",
+ "directors-",
+ "hackathon-",
+ "design-",
+] as const;
+
+function getInitials(name: string) {
+ return name
+ .split(" ")
+ .map((part) => part[0])
+ .filter(Boolean)
+ .slice(0, 2)
+ .join("")
+ .toUpperCase();
+}
+
+function getDisplayName(name: string) {
+ return name
+ .trim()
+ .replace(
+ /(^|[\s'-])(\p{L})/gu,
+ (_match, boundary: string, letter: string) =>
+ `${boundary}${letter.toLocaleUpperCase()}`,
+ );
+}
+
+function getDisplayTitle({ member, roleLabel }: TeamCascadePerson) {
+ if (roleLabel === "Organizer" && member.teamRole !== "Hack Lead") {
+ return "Hackathon Organizer";
+ }
+
+ if (roleLabel === "Designer") return "Designer";
+
+ return member.teamRole;
+}
+
+function getMemberProfileKey(member: TeamCascadeMember) {
+ const prefix = rosterIdPrefixes.find((candidate) =>
+ member.id.startsWith(candidate),
+ );
+
+ return prefix ? member.id.slice(prefix.length) : member.id;
+}
+
+function getTeamMembers(groups: TeamCascadeGroup[]) {
+ const membersByProfileId = new Map();
+
+ for (const group of groups) {
+ for (const member of group.members) {
+ const memberProfileKey = getMemberProfileKey(member);
+
+ if (!membersByProfileId.has(memberProfileKey)) {
+ membersByProfileId.set(memberProfileKey, {
+ member,
+ profileKey: `team-${memberProfileKey}`,
+ roleLabel: group.roleLabel,
+ });
+ }
+ }
+ }
+
+ return [...membersByProfileId.values()];
+}
+
+function ProfileImage({ member }: { member: TeamCascadeMember }) {
+ if (member.imageUrl) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {getInitials(member.name)}
+
+ );
+}
+
+function TeamProfile({
+ detailsId,
+ index,
+ isActive,
+ onSelect,
+ person,
+ total,
+}: {
+ detailsId: string;
+ index: number;
+ isActive: boolean;
+ onSelect: () => void;
+ person: TeamCascadePerson;
+ total: number;
+}) {
+ const displayName = getDisplayName(person.member.name);
+ const displayTitle = getDisplayTitle(person);
+
+ return (
+
+
+
+
+
+
+
+ Profile {index + 1} of {total}
+
+
+ );
+}
+
+function TeamMemberDetails({
+ detailsId,
+ person,
+}: {
+ detailsId: string;
+ person: TeamCascadePerson;
+}) {
+ const displayName = getDisplayName(person.member.name);
+ const displayTitle = getDisplayTitle(person);
+
+ return (
+
+
+
{displayName}
+
+
{displayTitle}
+ {person.member.linkedinUrl ? (
+
+
+
+ ) : null}
+
+ {person.member.linkedinUrl ? (
+
+ View LinkedIn profile
+ ↗
+
+ ) : (
+
+ LinkedIn not available
+
+ )}
+
+
+ );
+}
+
+function TeamRoster({ members }: { members: TeamCascadePerson[] }) {
+ const detailsId = useId();
+ const [selectedProfileKey, setSelectedProfileKey] = useState(
+ members[0]?.profileKey ?? "",
+ );
+ const selectedPerson =
+ members.find((person) => person.profileKey === selectedProfileKey) ??
+ members[0];
+
+ if (!selectedPerson) return null;
+
+ return (
+
+
+
+ {members.map((person, index) => (
+ setSelectedProfileKey(person.profileKey)}
+ person={person}
+ total={members.length}
+ />
+ ))}
+
+
+ );
+}
+
+function TeamCascadeStatusMessage({
+ children,
+ className,
+}: {
+ children: ReactNode;
+ className?: string;
+}) {
+ const statusClassName = className
+ ? `${styles.teamCascadeStatusFrame} ${className}`
+ : styles.teamCascadeStatusFrame;
+
+ return (
+
+ );
+}
+
+export function TeamCascadeClient({
+ bladeUrl,
+ className,
+}: {
+ bladeUrl: string;
+ className?: string;
+}) {
+ const [groups, setGroups] = useState([]);
+ const [status, setStatus] = useState(
+ bladeUrl ? "loading" : "error",
+ );
+ const teamMembers = useMemo(() => getTeamMembers(groups), [groups]);
+ const cascadeClassName = className
+ ? `${styles.teamCascade} ${className}`
+ : styles.teamCascade;
+
+ useEffect(() => {
+ if (!bladeUrl) return;
+
+ const abortController = new AbortController();
+
+ async function loadRoster() {
+ setStatus("loading");
+
+ try {
+ setGroups(
+ await loadTeamCascadeGroups(bladeUrl, abortController.signal),
+ );
+ setStatus("ready");
+ } catch {
+ if (abortController.signal.aborted) return;
+
+ setGroups([]);
+ setStatus("error");
+ }
+ }
+
+ void loadRoster();
+
+ return () => abortController.abort();
+ }, [bladeUrl]);
+
+ if (status === "loading") {
+ return (
+
+ Loading public team profiles.
+
+ );
+ }
+
+ if (status === "error") {
+ return (
+
+ Could not load Blade team profiles. Check the configured Blade URL or
+ local Blade server.
+
+ );
+ }
+
+ if (teamMembers.length === 0) {
+ return (
+
+ No visible team profiles found.
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/khix/src/app/_components/team-cascade/blade-trpc.ts b/apps/khix/src/app/_components/team-cascade/blade-trpc.ts
new file mode 100644
index 000000000..183c75453
--- /dev/null
+++ b/apps/khix/src/app/_components/team-cascade/blade-trpc.ts
@@ -0,0 +1,36 @@
+import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
+import SuperJSON from "superjson";
+
+import type { AppRouter } from "@forge/api";
+
+const trpcClients = new Map<
+ string,
+ ReturnType>
+>();
+
+function getBladeTrpcUrl(bladeUrl: string) {
+ return new URL("/api/trpc", bladeUrl).toString();
+}
+
+export function getBladeTrpcClient(bladeUrl: string) {
+ const trpcUrl = getBladeTrpcUrl(bladeUrl);
+ const cachedClient = trpcClients.get(trpcUrl);
+
+ if (cachedClient) return cachedClient;
+
+ const client = createTRPCProxyClient({
+ links: [
+ httpBatchLink({
+ url: trpcUrl,
+ headers: {
+ "x-trpc-source": "khix-site",
+ },
+ transformer: SuperJSON,
+ }),
+ ],
+ });
+
+ trpcClients.set(trpcUrl, client);
+
+ return client;
+}
diff --git a/apps/khix/src/app/_components/team-cascade/index.ts b/apps/khix/src/app/_components/team-cascade/index.ts
new file mode 100644
index 000000000..2348e2abc
--- /dev/null
+++ b/apps/khix/src/app/_components/team-cascade/index.ts
@@ -0,0 +1 @@
+export { TeamCascade } from "./TeamCascade";
diff --git a/apps/khix/src/app/_components/team-cascade/team-roster.ts b/apps/khix/src/app/_components/team-cascade/team-roster.ts
new file mode 100644
index 000000000..8f16bd9ab
--- /dev/null
+++ b/apps/khix/src/app/_components/team-cascade/team-roster.ts
@@ -0,0 +1,63 @@
+import type { inferRouterOutputs } from "@trpc/server";
+
+import type { AppRouter } from "@forge/api";
+
+import { getBladeTrpcClient } from "./blade-trpc";
+
+type PublicClubTeamRoster =
+ inferRouterOutputs["guild"]["getPublicClubTeamRoster"];
+
+export type TeamCascadeMember = PublicClubTeamRoster["executive"][number];
+
+export type TeamCascadeRole = "Officer" | "Director" | "Organizer" | "Designer";
+
+export interface TeamCascadeGroup {
+ roleLabel: TeamCascadeRole;
+ members: TeamCascadeMember[];
+}
+
+const featuredDesignerIds = [
+ "design-f06cbff5-b5f8-49d5-8a3c-5b40a59dfcc6",
+ "design-3a0d6777-2276-4ae8-9281-ace2a26d6c94",
+ "design-f56f4444-7962-4090-b937-f31674a6ac7e",
+] as const;
+
+const teamCascadeRosterGroups = [
+ { roleLabel: "Officer", rosterKey: "executive" },
+ { roleLabel: "Director", rosterKey: "directors" },
+ { roleLabel: "Organizer", rosterKey: "hackathon" },
+ {
+ roleLabel: "Designer",
+ rosterKey: "design",
+ memberIds: featuredDesignerIds,
+ },
+] as const satisfies readonly {
+ roleLabel: TeamCascadeRole;
+ rosterKey: keyof PublicClubTeamRoster;
+ memberIds?: readonly string[];
+}[];
+
+export async function loadTeamCascadeGroups(
+ bladeUrl: string,
+ signal: AbortSignal,
+): Promise {
+ const roster = await getBladeTrpcClient(
+ bladeUrl,
+ ).guild.getPublicClubTeamRoster.query(undefined, {
+ signal,
+ });
+
+ return teamCascadeRosterGroups.map((group) => {
+ const members = roster[group.rosterKey];
+
+ return {
+ roleLabel: group.roleLabel,
+ members:
+ "memberIds" in group
+ ? members.filter((member) =>
+ group.memberIds.some((memberId) => memberId === member.id),
+ )
+ : members,
+ };
+ });
+}
diff --git a/apps/khix/src/app/api/auth/[...all]/route.ts b/apps/khix/src/app/api/auth/[...all]/route.ts
new file mode 100644
index 000000000..eff641b97
--- /dev/null
+++ b/apps/khix/src/app/api/auth/[...all]/route.ts
@@ -0,0 +1,3 @@
+import { handlers } from "~/auth/server";
+
+export const { GET, POST } = handlers;
diff --git a/apps/khix/src/app/api/auth/signin/route.ts b/apps/khix/src/app/api/auth/signin/route.ts
new file mode 100644
index 000000000..06d455b33
--- /dev/null
+++ b/apps/khix/src/app/api/auth/signin/route.ts
@@ -0,0 +1,35 @@
+import { NextResponse } from "next/server";
+
+import { sanitizeCallbackURL } from "@forge/auth/callback-url";
+
+import { KHIX_AUTH_BASE_URL } from "~/auth/server";
+import { env } from "~/env";
+
+export function GET(request: Request) {
+ const requestUrl = new URL(request.url);
+ const provider = requestUrl.searchParams.get("provider");
+ if (provider !== "discord") {
+ return NextResponse.json(
+ { error: "Unsupported provider parameter" },
+ { status: 400 },
+ );
+ }
+
+ const callbackPath = sanitizeCallbackURL(
+ requestUrl.searchParams.get("callbackURL"),
+ KHIX_AUTH_BASE_URL,
+ "/dashboard",
+ );
+ const returnTo = new URL(callbackPath, KHIX_AUTH_BASE_URL);
+ const bridgeUrl = new URL("/auth/khix-return", env.BLADE_URL);
+ bridgeUrl.searchParams.set("returnTo", returnTo.toString());
+
+ const bladeSignInUrl = new URL("/api/auth/signin", env.BLADE_URL);
+ bladeSignInUrl.searchParams.set("provider", "discord");
+ bladeSignInUrl.searchParams.set(
+ "callbackURL",
+ `${bridgeUrl.pathname}${bridgeUrl.search}`,
+ );
+
+ return NextResponse.redirect(bladeSignInUrl);
+}
diff --git a/apps/khix/src/app/api/trpc/[trpc]/route.ts b/apps/khix/src/app/api/trpc/[trpc]/route.ts
new file mode 100644
index 000000000..44ea4f6de
--- /dev/null
+++ b/apps/khix/src/app/api/trpc/[trpc]/route.ts
@@ -0,0 +1,80 @@
+import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
+
+import { createTRPCContext, participantRouter } from "@forge/api/participant";
+
+import { validateToken } from "~/auth/server";
+
+const MAX_REQUEST_SIZE = 8 * 1024 * 1024;
+
+function requestTooLargeResponse() {
+ return Response.json(
+ { error: { message: "Request exceeds the 8MB upload limit." } },
+ { status: 413 },
+ );
+}
+
+function concatChunks(chunks: Uint8Array[], totalLength: number) {
+ if (chunks.length === 0) return undefined;
+
+ const body = new ArrayBuffer(totalLength);
+ const bytes = new Uint8Array(body);
+ let offset = 0;
+ for (const chunk of chunks) {
+ bytes.set(chunk, offset);
+ offset += chunk.byteLength;
+ }
+ return body;
+}
+
+async function readRequestWithLimit(req: Request) {
+ if (req.method === "GET" || req.method === "HEAD" || !req.body) return req;
+
+ const reader = req.body.getReader();
+ const chunks: Uint8Array[] = [];
+ let totalLength = 0;
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ totalLength += value.byteLength;
+ if (totalLength > MAX_REQUEST_SIZE) {
+ await reader.cancel();
+ return null;
+ }
+
+ chunks.push(value);
+ }
+
+ return new Request(req.url, {
+ body: concatChunks(chunks, totalLength),
+ headers: req.headers,
+ method: req.method,
+ signal: req.signal,
+ });
+}
+
+const handler = async (req: Request) => {
+ const contentLength = Number(req.headers.get("content-length") ?? 0);
+ if (contentLength > MAX_REQUEST_SIZE) {
+ return requestTooLargeResponse();
+ }
+
+ const limitedRequest = await readRequestWithLimit(req);
+ if (!limitedRequest) return requestTooLargeResponse();
+
+ const session = await validateToken();
+ return fetchRequestHandler({
+ endpoint: "/api/trpc",
+ router: participantRouter,
+ req: limitedRequest,
+ createContext: () =>
+ createTRPCContext({ headers: limitedRequest.headers, session }),
+ onError({ error, path }) {
+ // eslint-disable-next-line no-console
+ console.error(`KHIX tRPC error on ${path ?? "unknown"}:`, error.message);
+ },
+ });
+};
+
+export { handler as GET, handler as POST };
diff --git a/apps/khix/src/app/fonts/Bagnard-OFL.txt b/apps/khix/src/app/fonts/Bagnard-OFL.txt
new file mode 100644
index 000000000..47ad45ac2
--- /dev/null
+++ b/apps/khix/src/app/fonts/Bagnard-OFL.txt
@@ -0,0 +1,93 @@
+Copyright (c) 2015 Sebastien Sanfilippo (www.love-letters.be)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
\ No newline at end of file
diff --git a/apps/khix/src/app/fonts/Bagnard.otf b/apps/khix/src/app/fonts/Bagnard.otf
new file mode 100644
index 000000000..ddbb6a32c
Binary files /dev/null and b/apps/khix/src/app/fonts/Bagnard.otf differ
diff --git a/apps/khix/src/app/globals.css b/apps/khix/src/app/globals.css
index f02df20d2..f9e183bd3 100644
--- a/apps/khix/src/app/globals.css
+++ b/apps/khix/src/app/globals.css
@@ -1,5 +1,4 @@
@import "tailwindcss";
-@import "tw-animate-css";
@source "../../../../packages/ui/src";
@config "../../tailwind.config.ts";
@custom-variant dark (&:is(.dark *));
@@ -13,6 +12,11 @@
}
:root {
+ --font-khix: var(--font-bagnard);
+ --font-geist-sans: var(--font-khix);
+ --font-cormorant-garamond: var(--font-khix);
+ --font-sans: var(--font-khix);
+ --font-serif: var(--font-khix);
--background: 0 0% 100%;
--foreground: 224 71.4% 4.1%;
--card: 0 0% 100%;
@@ -57,1487 +61,27 @@
--ring: 263.4 70% 50.4%;
}
+ html,
body {
- @apply bg-background text-foreground;
- }
-}
-
-html,
-body {
- width: 100%;
- min-width: 100%;
- min-height: 100%;
- overflow-x: hidden;
- background: #070c10;
-}
-
-.khix-site-background {
- position: relative;
- isolation: isolate;
- width: 100%;
- min-width: 100%;
- min-height: 100vh;
- min-height: 100svh;
- overflow: visible;
- color: #fbfff2;
- background: #070c10;
-}
-
-.khix-experience-stage {
- position: absolute;
- inset: 0;
- z-index: 1;
- overflow: hidden;
- isolation: isolate;
- transition:
- filter 1.35s cubic-bezier(0.16, 1, 0.3, 1),
- opacity 1.15s ease;
- will-change: filter;
-}
-
-.khix-experience-awaiting .khix-experience-stage {
- filter: blur(1.15rem) saturate(1.22) brightness(0.78);
-}
-
-.khix-experience-entered .khix-experience-stage {
- filter: blur(0) saturate(1) brightness(1);
-}
-
-.khix-background-layer {
- position: absolute;
- inset: -1px;
- z-index: -2;
- pointer-events: none;
- background-color: #111827;
- background-image: url("https://assets.knighthacks.org/ixtempbg.webp");
- background-image: image-set(
- url("https://assets.knighthacks.org/ixtempbg.avif") type("image/avif"),
- url("https://assets.knighthacks.org/ixtempbg.webp") type("image/webp")
- );
- background-position: center center;
- background-repeat: no-repeat;
- background-size: cover;
- transform: scale(1.035);
- transform-origin: center;
- transition: transform 1.35s cubic-bezier(0.16, 1, 0.3, 1);
- will-change: transform;
-}
-
-.khix-experience-entered .khix-background-layer {
- transform: scale(1);
-}
-
-.khix-edge-gloom {
- position: absolute;
- inset: -1px;
- z-index: 1;
- pointer-events: none;
- background:
- radial-gradient(
- ellipse at 50% 48%,
- transparent 50%,
- rgba(7, 18, 22, 0.18) 64%,
- rgba(1, 7, 10, 0.55) 82%,
- rgba(0, 2, 5, 0.82) 100%
- ),
- linear-gradient(
- 90deg,
- rgba(2, 11, 15, 0.54),
- transparent 16% 84%,
- rgba(2, 11, 15, 0.58)
- ),
- linear-gradient(
- 0deg,
- rgba(0, 4, 8, 0.64),
- transparent 20% 82%,
- rgba(5, 15, 18, 0.38)
- );
- opacity: 0.75;
-}
-
-.khix-edge-gloom::before {
- position: absolute;
- inset: 0;
- display: block;
- content: "";
- pointer-events: none;
- background:
- radial-gradient(ellipse at 4% 12%, rgba(11, 27, 32, 0.42), transparent 33%),
- radial-gradient(ellipse at 91% 22%, rgba(8, 28, 25, 0.38), transparent 34%),
- radial-gradient(ellipse at 50% 98%, rgba(5, 17, 24, 0.58), transparent 45%);
- opacity: 0.5;
- animation: khix-edge-vignette-pulse 7.4s ease-in-out infinite alternate;
-}
-
-.khix-mlh-badge {
- position: fixed;
- top: max(0px, env(safe-area-inset-top));
- right: 1.25rem;
- z-index: 50;
- display: block;
- width: 74px;
- min-width: 60px;
- max-width: 100px;
- filter: drop-shadow(0 0.7rem 1.1rem rgba(0, 0, 0, 0.44))
- drop-shadow(0 0 0.75rem rgba(255, 255, 255, 0.16));
-}
-
-.khix-mlh-badge img {
- display: block;
- width: 100%;
- height: auto;
-}
-
-.khix-hero {
- position: relative;
- z-index: 2;
- display: flex;
- align-items: flex-start;
- justify-content: center;
- width: 100%;
- height: 100%;
- min-height: 100%;
- overflow: hidden;
- padding: clamp(2.25rem, 4.5vh, 3.25rem) 1.25rem 2rem;
- text-align: center;
-}
-
-.khix-hero-content {
- position: relative;
- z-index: 4;
- display: flex;
- flex-direction: column;
- align-items: center;
- width: min(48rem, 100%);
- max-width: calc(100vw - 2.5rem);
-}
-
-.khix-logo-lockup {
- position: relative;
- isolation: isolate;
- width: min(60vw, 40rem);
-}
-
-.khix-logo-image {
- width: 100%;
- height: auto;
-}
-
-.khix-event-date-lockup {
- width: min(40vw, 26rem);
- margin: -1.5rem auto 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- color: #fffff6;
- font-family: var(--font-khix), system-ui, sans-serif;
- font-weight: 900;
- font-synthesis-weight: auto;
- letter-spacing: 0;
- text-align: center;
- text-transform: uppercase;
-}
-
-.khix-event-date-text {
- margin: 0;
- font-size: clamp(1.45rem, 2.55vw, 2.3rem);
- line-height: 0.94;
- white-space: nowrap;
- -webkit-text-stroke: 0.035em rgba(255, 255, 246, 0.9);
- paint-order: stroke fill;
- text-shadow:
- 0 0 0.2rem rgba(255, 255, 255, 0.8),
- 0 0 0.6rem rgba(214, 255, 86, 0.3),
- 0 0.5rem 1rem rgba(0, 0, 0, 0.5);
-}
-
-.khix-event-location-text {
- margin: clamp(0.25rem, 0.55vh, 0.42rem) 0 0;
- font-size: clamp(0.72rem, 1.28vw, 1.2rem);
- line-height: 1;
- opacity: 1;
- white-space: nowrap;
- -webkit-text-stroke: 0.024em rgba(255, 255, 246, 0.86);
- paint-order: stroke fill;
- text-shadow:
- 0 0 0.16rem rgba(255, 255, 255, 0.7),
- 0 0 0.5rem rgba(214, 255, 86, 0.24),
- 0 0.45rem 0.85rem rgba(0, 0, 0, 0.42);
-}
-
-.khix-gem-aura {
- position: absolute;
- top: 50.8%;
- left: 50.8%;
- z-index: 2;
- width: clamp(10rem, 25vw, 24rem);
- height: clamp(4.7rem, 10.5vw, 10rem);
- pointer-events: none;
- border-radius: 999px;
- background:
- radial-gradient(
- circle at 50% 45%,
- rgba(255, 245, 224, 0.34),
- transparent 14%
- ),
- radial-gradient(
- ellipse at 48% 52%,
- rgba(255, 44, 105, 0.34),
- rgba(127, 46, 184, 0.24) 44%,
- transparent 68%
- );
- filter: blur(0.95rem);
- mix-blend-mode: plus-lighter;
- opacity: 0.78;
- translate: -50% -50%;
-}
-
-.khix-gem-magic {
- position: absolute;
- top: 50.8%;
- left: 50.8%;
- z-index: 3;
- width: clamp(12rem, 27vw, 30rem);
- height: clamp(5rem, 10.8vw, 11rem);
- pointer-events: none;
- mix-blend-mode: normal;
- opacity: 0.92;
- translate: -50% -50%;
-}
-
-.khix-gem-magic::before,
-.khix-gem-magic::after,
-.khix-gem-magic-wisp,
-.khix-gem-magic-thread {
- position: absolute;
- display: block;
- content: "";
- pointer-events: none;
-}
-
-.khix-gem-magic::before {
- inset: 18% 13% 14%;
- border-radius: 999px;
- background: radial-gradient(
- ellipse at 48% 52%,
- rgba(255, 62, 121, 0.26),
- rgba(155, 58, 216, 0.2) 38%,
- transparent 70%
- );
- filter: blur(0.95rem);
- opacity: 0.58;
- animation: khix-gem-magic-breathe 7.4s ease-in-out infinite alternate;
-}
-
-.khix-gem-magic::after {
- top: 38%;
- left: 25%;
- width: 52%;
- height: 20%;
- border-radius: 999px;
- background: linear-gradient(
- 102deg,
- transparent 0%,
- rgba(160, 55, 219, 0.2) 24%,
- rgba(255, 70, 116, 0.22) 50%,
- rgba(171, 68, 232, 0.16) 70%,
- transparent 100%
- );
- filter: blur(0.35rem);
- opacity: 0.7;
- animation: khix-gem-magic-drift-mid 8.6s ease-in-out infinite alternate;
-}
-
-.khix-gem-magic-wisp {
- height: clamp(0.75rem, 1.1vw, 1.3rem);
- border-radius: 999px;
- background: radial-gradient(
- ellipse at 50% 50%,
- rgba(255, 76, 126, 0.34),
- rgba(143, 55, 213, 0.26) 46%,
- transparent 74%
- );
- filter: blur(0.22rem);
-}
-
-.khix-gem-magic-wisp-left {
- top: 53%;
- left: 11%;
- width: 34%;
- opacity: 0.42;
- transform: rotate(-13deg);
- animation: khix-gem-magic-drift-left 8.2s ease-in-out infinite alternate;
-}
-
-.khix-gem-magic-wisp-mid {
- top: 31%;
- left: 35%;
- width: 28%;
- opacity: 0.34;
- transform: rotate(-8deg);
- animation: khix-gem-magic-drift-mid 9.8s ease-in-out -1.8s infinite alternate;
-}
-
-.khix-gem-magic-wisp-right {
- top: 47%;
- right: 12%;
- width: 32%;
- opacity: 0.4;
- transform: rotate(10deg);
- animation: khix-gem-magic-drift-right 7.8s ease-in-out -2.6s infinite
- alternate;
-}
-
-.khix-gem-magic-thread {
- width: clamp(3.2rem, 7vw, 7.4rem);
- height: clamp(0.5rem, 0.85vw, 0.9rem);
- border-radius: 999px;
- background: linear-gradient(
- 90deg,
- transparent,
- rgba(128, 42, 192, 0.34) 24%,
- rgba(255, 58, 119, 0.46) 52%,
- rgba(255, 160, 192, 0.22) 68%,
- transparent
- );
- filter: blur(0.16rem);
- mix-blend-mode: screen;
- opacity: 0.66;
- will-change: opacity, transform;
-}
-
-.khix-gem-magic-thread-left {
- top: 53%;
- left: 20%;
- transform: rotate(-18deg);
- animation: khix-gem-magic-thread-left 4.9s ease-in-out -0.6s infinite
- alternate;
-}
-
-.khix-gem-magic-thread-right {
- top: 43%;
- right: 20%;
- transform: rotate(12deg);
- animation: khix-gem-magic-thread-right 5.6s ease-in-out -1.7s infinite
- alternate;
-}
-
-.khix-gem-shard-shimmer {
- position: absolute;
- inset: 0;
- z-index: 4;
- pointer-events: none;
- background:
- radial-gradient(
- ellipse at 51% 51%,
- rgba(255, 53, 109, 0.24),
- rgba(141, 47, 204, 0.16) 28%,
- transparent 47%
- ),
- linear-gradient(
- 112deg,
- transparent 0 42%,
- rgba(255, 70, 124, 0.16) 46%,
- rgba(255, 235, 211, 0.38) 50%,
- rgba(174, 64, 220, 0.2) 55%,
- transparent 62% 100%
- );
- background-position:
- center center,
- -32vw center;
- background-repeat: no-repeat;
- background-size:
- cover,
- 42vw 100%;
- filter: blur(0.035rem);
- mix-blend-mode: screen;
- opacity: 0.68;
- -webkit-mask-image: url("/khix-gem-shard-mask.png");
- mask-image: url("/khix-gem-shard-mask.png");
- -webkit-mask-position: center center;
- mask-position: center center;
- -webkit-mask-repeat: no-repeat;
- mask-repeat: no-repeat;
- -webkit-mask-size: cover;
- mask-size: cover;
- animation: khix-gem-shard-mask-shimmer 4.8s ease-in-out infinite alternate;
- will-change: background-position, opacity;
-}
-
-.khix-gem-shard-glint {
- position: absolute;
- top: 4%;
- left: 27%;
- z-index: 2;
- width: 56%;
- height: 38%;
- border-radius: 999px;
- background: linear-gradient(
- 111deg,
- transparent 0%,
- rgba(255, 143, 152, 0.12) 23%,
- rgba(255, 245, 219, 0.56) 41%,
- rgba(255, 255, 248, 0.86) 49%,
- rgba(255, 94, 124, 0.2) 58%,
- transparent 76%
- );
- filter: blur(0.06rem);
- mix-blend-mode: screen;
- opacity: 0.72;
- transform: translate3d(0, 0, 0) skewX(-21deg) rotate(-6deg) scaleX(0.96);
- animation: khix-gem-shard-glint 4.6s ease-in-out infinite alternate;
- will-change: opacity, transform;
-}
-
-.khix-gem-shard-spark {
- position: absolute;
- z-index: 3;
- display: block;
- width: clamp(0.28rem, 0.45vw, 0.52rem);
- height: clamp(0.28rem, 0.45vw, 0.52rem);
- border-radius: 999px;
- background: rgba(255, 250, 219, 0.92);
- box-shadow:
- 0 0 0.45rem rgba(255, 236, 184, 0.62),
- 0 0 0.95rem rgba(255, 71, 111, 0.38);
- mix-blend-mode: screen;
-}
-
-.khix-gem-shard-spark-top {
- top: 12%;
- left: 56%;
- opacity: 0.42;
- animation: khix-gem-shard-spark-top 5.2s ease-in-out -1.2s infinite alternate;
-}
-
-.khix-gem-shard-spark-low {
- top: 43%;
- left: 82%;
- width: clamp(0.18rem, 0.3vw, 0.36rem);
- height: clamp(0.18rem, 0.3vw, 0.36rem);
- opacity: 0.32;
- animation: khix-gem-shard-spark-low 6.4s ease-in-out -2.4s infinite alternate;
-}
-
-.khix-hero-actions {
- position: absolute;
- bottom: clamp(4.35rem, 6.3vh, 5.45rem);
- left: 50%;
- z-index: 5;
- display: flex;
- flex-wrap: wrap;
- gap: 0.8rem;
- align-items: center;
- justify-content: center;
- width: min(18.75rem, calc(100vw - 2rem));
- max-width: calc(100vw - 2rem);
- margin-top: clamp(1rem, 2.4vh, 1.5rem);
- opacity: 0;
- translate: -50% 0;
- animation: khix-control-fade 0.58s ease-out 0.42s forwards;
-}
-
-.khix-social-links {
- position: absolute;
- bottom: clamp(0.78rem, 1.35vh, 1.1rem);
- left: 50%;
- z-index: 5;
- display: flex;
- gap: 0.58rem;
- align-items: center;
- justify-content: center;
- width: max-content;
- max-width: calc(100vw - 2rem);
- translate: -50% 0;
- opacity: 0;
- animation: khix-control-fade 0.58s ease-out 0.58s forwards;
-}
-
-.khix-social-link {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 2rem;
- height: 2rem;
- color: rgba(255, 255, 246, 0.76);
- text-decoration: none;
- filter: drop-shadow(0 0.48rem 0.72rem rgba(0, 0, 0, 0.44))
- drop-shadow(0 0 0.5rem rgba(214, 255, 86, 0.12));
- opacity: 0.84;
- transition:
- color 0.18s ease,
- filter 0.18s ease,
- opacity 0.18s ease;
-}
-
-.khix-social-link:hover {
- color: #fff7cf;
- filter: drop-shadow(0 0.5rem 0.8rem rgba(0, 0, 0, 0.44))
- drop-shadow(0 0 0.72rem rgba(214, 255, 86, 0.28));
- opacity: 1;
-}
-
-.khix-button {
- position: relative;
- isolation: isolate;
- display: inline-flex;
- flex: 1 1 0;
- align-items: center;
- justify-content: center;
- min-width: 0;
- min-height: 2.72rem;
- padding: 0.68rem 1.8rem;
- font-family: var(--font-khix), serif;
- font-size: 0.85rem;
- font-weight: 800;
- letter-spacing: 0.05em;
- line-height: 1;
- text-decoration: none;
- text-transform: uppercase;
- white-space: nowrap;
- background: transparent;
- border: none;
- outline: none;
- z-index: 1;
- transition:
- filter 0.2s ease,
- color 0.2s ease,
- transform 0.2s ease;
-}
-
-.khix-button::before {
- content: "";
- position: absolute;
- inset: 0;
- z-index: 0;
- pointer-events: none;
- -webkit-clip-path: polygon(
- 1rem 0,
- calc(100% - 1rem) 0,
- 100% 50%,
- calc(100% - 1rem) 100%,
- 1rem 100%,
- 0 50%
- );
- clip-path: polygon(
- 1rem 0,
- calc(100% - 1rem) 0,
- 100% 50%,
- calc(100% - 1rem) 100%,
- 1rem 100%,
- 0 50%
- );
- transition:
- background 0.2s ease,
- opacity 0.2s ease;
-}
-
-.khix-button:focus-visible {
- outline: 2px solid rgba(255, 242, 163, 0.96);
- outline-offset: 0.35rem;
-}
-
-.khix-button-primary {
- color: #1d090a;
- filter: drop-shadow(0 0.5rem 0.8rem rgba(210, 255, 69, 0.3));
-}
-
-.khix-button-primary::before {
- background: linear-gradient(135deg, #fff2a3, #d8ff5f 62%, #ff4867);
-}
-
-.khix-button-label {
- position: relative;
- z-index: 2;
- display: block;
- width: 100%;
- color: inherit;
- text-align: center;
-}
-
-.khix-button-secondary {
- color: #fff7d2;
- filter: drop-shadow(0 0 0.4rem rgba(255, 211, 106, 0.2));
-}
-
-.khix-button-secondary::before {
- background: rgba(255, 211, 106, 0.48);
-}
-
-.khix-button-secondary::after {
- content: "";
- position: absolute;
- inset: 1px;
- z-index: 1;
- pointer-events: none;
- -webkit-clip-path: polygon(
- calc(1rem - 1px) 0,
- calc(100% - 1rem + 1px) 0,
- 100% 50%,
- calc(100% - 1rem + 1px) 100%,
- calc(1rem - 1px) 100%,
- 0 50%
- );
- clip-path: polygon(
- calc(1rem - 1px) 0,
- calc(100% - 1rem + 1px) 0,
- 100% 50%,
- calc(100% - 1rem + 1px) 100%,
- calc(1rem - 1px) 100%,
- 0 50%
- );
- background: rgba(18, 13, 22, 0.8);
- backdrop-filter: blur(12px);
- transition: background 0.2s ease;
-}
-
-.khix-button-primary:hover {
- filter: drop-shadow(0 0.8rem 1.2rem rgba(255, 83, 108, 0.4));
-}
-
-.khix-button-secondary:hover::before {
- background: rgba(255, 211, 106, 0.78);
-}
-
-.khix-button-secondary:hover::after {
- background: rgba(42, 21, 29, 0.72);
-}
-
-.khix-code-link {
- position: absolute;
- bottom: 2.75rem;
- left: 50%;
- z-index: 5;
- color: rgba(255, 253, 241, 0.72);
- font-family:
- ui-monospace, "SFMono-Regular", "Cascadia Code", "Liberation Mono", Menlo,
- Monaco, Consolas, monospace;
- font-size: 0.68rem;
- font-weight: 800;
- letter-spacing: 0;
- text-decoration: none;
- text-shadow: 0 0.65rem 1.2rem rgba(0, 0, 0, 0.55);
- text-transform: uppercase;
- translate: -50% 0;
- width: max-content;
- max-width: calc(100vw - 2rem);
- opacity: 0;
- animation: khix-control-fade 0.58s ease-out 0.52s forwards;
-}
-
-.khix-code-link:hover {
- color: #fff2a3;
-}
-
-.khix-enter-gate {
- position: fixed;
- inset: 0;
- z-index: 70;
- display: grid;
- place-items: center;
- padding: clamp(1rem, 4vw, 2rem);
- border: 0;
- color: #fffff6;
- cursor: pointer;
- appearance: none;
- -webkit-appearance: none;
- -webkit-tap-highlight-color: transparent;
- touch-action: manipulation;
- background:
- radial-gradient(
- ellipse at 50% 47%,
- rgba(214, 255, 86, 0.24),
- rgba(255, 69, 121, 0.11) 22%,
- rgba(104, 48, 150, 0.08) 39%,
- transparent 58%
- ),
- radial-gradient(
- ellipse at 50% 57%,
- rgba(255, 70, 120, 0.1),
- transparent 46%
- ),
- rgba(5, 8, 10, 0.28);
- text-align: center;
- transition:
- opacity 0.8s ease,
- transform 1s cubic-bezier(0.16, 1, 0.3, 1),
- visibility 0.8s ease;
-}
-
-.khix-enter-gate::before {
- position: absolute;
- inset: 0;
- content: "";
- pointer-events: none;
- background:
- radial-gradient(
- ellipse at 50% 48%,
- rgba(255, 255, 230, 0.16),
- transparent 31%
- ),
- linear-gradient(
- 115deg,
- transparent 0%,
- rgba(216, 255, 86, 0.08) 44%,
- rgba(255, 72, 122, 0.1) 51%,
- transparent 62% 100%
- );
- filter: blur(0.22rem);
- mix-blend-mode: screen;
- opacity: 0.72;
- animation: khix-enter-haze 5.8s ease-in-out infinite alternate;
-}
-
-.khix-experience-entered .khix-enter-gate {
- visibility: hidden;
- pointer-events: none;
- opacity: 0;
- transform: scale(1.04);
-}
-
-.khix-enter-gate-aura {
- position: absolute;
- pointer-events: none;
- width: clamp(18rem, 48vw, 38rem);
- aspect-ratio: 1.75;
- border-radius: 999px;
- background: radial-gradient(
- ellipse at center,
- rgba(213, 255, 82, 0.26),
- rgba(255, 73, 122, 0.16) 33%,
- rgba(117, 48, 160, 0.1) 51%,
- transparent 76%
- );
- filter: blur(2.35rem);
- opacity: 0.7;
- animation: khix-enter-aura 3.4s ease-in-out infinite alternate;
-}
-
-.khix-enter-gate-content {
- position: relative;
- pointer-events: none;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: min(32rem, calc(100vw - 2rem));
- font-family:
- ui-monospace, "SFMono-Regular", "Cascadia Code", "Liberation Mono", Menlo,
- Monaco, Consolas, monospace;
- font-size: clamp(0.76rem, 1.16vw, 1.02rem);
- font-weight: 900;
- letter-spacing: 0;
- line-height: 1.25;
- text-transform: uppercase;
- text-wrap: balance;
- text-shadow:
- 0 0 0.28rem rgba(255, 255, 246, 0.5),
- 0 0 0.82rem rgba(214, 255, 86, 0.52),
- 0 0 1.7rem rgba(255, 71, 119, 0.36),
- 0 0.75rem 1.2rem rgba(0, 0, 0, 0.58);
- filter: drop-shadow(0 0 0.7rem rgba(214, 255, 86, 0.34));
- animation: khix-enter-text-float 2.9s ease-in-out infinite alternate;
-}
-
-.khix-enter-gate-content::after {
- position: absolute;
- inset: -0.75rem -1.5rem;
- z-index: -1;
- content: "";
- border-radius: 999px;
- background: radial-gradient(
- ellipse at center,
- rgba(255, 255, 238, 0.08),
- rgba(214, 255, 86, 0.08) 38%,
- transparent 74%
- );
- filter: blur(0.65rem);
-}
-
-.khix-audio-toggle {
- position: static;
- flex: 0 0 2rem;
- right: auto;
- bottom: auto;
- left: auto;
- z-index: 80;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 2rem;
- height: 2rem;
- padding: 0;
- border: 0 !important;
- border-radius: 999px;
- outline: 0 !important;
- color: rgba(255, 255, 246, 0.76);
- cursor: pointer;
- appearance: none;
- background: transparent !important;
- box-shadow: none !important;
- opacity: 0.84;
- -webkit-tap-highlight-color: transparent;
-}
-
-.khix-audio-toggle svg {
- filter: drop-shadow(0 0.45rem 0.55rem rgba(0, 0, 0, 0.36))
- drop-shadow(0 0 0.42rem rgba(204, 255, 75, 0.16));
-}
-
-.khix-audio-toggle:hover,
-.khix-audio-toggle:focus,
-.khix-audio-toggle:focus-visible,
-.khix-audio-toggle:active {
- opacity: 0.9;
- border: 0 !important;
- outline: 0 !important;
- background: transparent !important;
- box-shadow: none !important;
-}
-
-.khix-social-links .khix-audio-toggle-row {
- position: static !important;
- flex: 0 0 2rem;
- right: auto !important;
- bottom: auto !important;
- left: auto !important;
- width: 2rem;
- height: 2rem;
- color: rgba(255, 255, 246, 0.76);
- line-height: 1;
-}
-
-@keyframes khix-control-fade {
- to {
- opacity: 1;
- }
-}
-
-.khix-cursor-wisp {
- position: fixed;
- z-index: 95;
- display: block;
- width: var(--wisp-width);
- height: var(--wisp-height);
- pointer-events: none;
- user-select: none;
- transform: translate(-50%, -50%);
- transform-origin: center;
- animation: khix-cursor-wisp-drift 1.42s cubic-bezier(0.19, 1, 0.22, 1)
- forwards;
- filter: drop-shadow(0 0 0.7rem var(--wisp-glow))
- drop-shadow(0 0 1.55rem var(--wisp-edge));
- mix-blend-mode: normal;
- will-change: opacity, transform;
-}
-
-.khix-cursor-wisp::before {
- position: absolute;
- inset: -145% -175%;
- content: "";
- border-radius: 999px;
- background: radial-gradient(
- ellipse at 50% 58%,
- var(--wisp-glow) 0%,
- var(--wisp-edge) 24%,
- transparent 66%
- );
- filter: blur(1.35rem);
- opacity: 0.42;
- animation: khix-cursor-wisp-aura 1.42s ease-out forwards;
-}
-
-.khix-cursor-wisp::after {
- position: absolute;
- inset: 18% -68% -18%;
- content: "";
- border-radius: 999px;
- background:
- radial-gradient(ellipse at 35% 55%, var(--wisp-edge) 0%, transparent 48%),
- radial-gradient(ellipse at 70% 44%, var(--wisp-glow) 0%, transparent 42%);
- filter: blur(0.95rem);
- opacity: 0.34;
- transform: rotate(var(--wisp-tail-rotate-end));
- animation: khix-cursor-wisp-fog 1.42s ease-out forwards;
-}
-
-.khix-cursor-wisp-tail {
- position: absolute;
- right: 24%;
- bottom: -4%;
- width: 156%;
- height: 72%;
- border-radius: 999px;
- background:
- linear-gradient(96deg, transparent, var(--wisp-edge) 32%, transparent 74%),
- radial-gradient(ellipse at 76% 50%, var(--wisp-glow), transparent 58%);
- filter: blur(0.62rem);
- opacity: 0.38;
- transform: rotate(var(--wisp-tail-rotate)) scaleX(0.84);
- transform-origin: 88% 50%;
- animation: khix-cursor-wisp-tail 1.42s ease-out forwards;
-}
-
-.khix-cursor-wisp-core {
- position: absolute;
- inset: 0 4% 4%;
- border-radius: 999px;
- background:
- radial-gradient(ellipse at 52% 68%, var(--wisp-glow) 0%, transparent 54%),
- radial-gradient(ellipse at 50% 88%, var(--wisp-edge) 0%, transparent 62%);
- box-shadow: 0 0 1.15rem var(--wisp-glow);
- opacity: 0.9;
- transform: rotate(2deg) skewX(-10deg) scale(0.92, 1);
- animation: khix-cursor-wisp-flicker 0.58s ease-in-out infinite alternate;
-}
-
-.khix-cursor-wisp-core::before,
-.khix-cursor-wisp-core::after {
- position: absolute;
- bottom: 10%;
- content: "";
- border-radius: 999px;
- pointer-events: none;
- transform-origin: 50% 92%;
-}
-
-.khix-cursor-wisp-core::before {
- left: 34%;
- width: 34%;
- height: 88%;
- background:
- radial-gradient(ellipse at 50% 12%, var(--wisp-core) 0%, transparent 20%),
- linear-gradient(
- 180deg,
- transparent 0%,
- var(--wisp-glow) 24%,
- var(--wisp-edge) 63%,
- transparent 100%
- );
- filter: blur(0.12rem);
- opacity: 0.86;
- transform: rotate(8deg) scaleY(0.94);
- animation: khix-cursor-wisp-thread-main 0.68s ease-in-out infinite alternate;
-}
-
-.khix-cursor-wisp-core::after {
- left: 18%;
- width: 54%;
- height: 72%;
- background:
- radial-gradient(ellipse at 42% 20%, var(--wisp-core) 0%, transparent 18%),
- linear-gradient(
- 178deg,
- transparent 0%,
- var(--wisp-glow) 28%,
- var(--wisp-edge) 66%,
- transparent 100%
- );
- filter: blur(0.22rem);
- opacity: 0.56;
- transform: rotate(-14deg) scaleY(0.86);
- animation: khix-cursor-wisp-thread-side 0.78s ease-in-out infinite alternate;
-}
-
-@keyframes khix-cursor-wisp-drift {
- 0% {
- opacity: 0;
- transform: translate(-50%, -50%) scale(0.78) rotate(0deg);
- }
-
- 16% {
- opacity: 0.72;
- }
-
- 54% {
- opacity: 0.48;
- transform: translate(
- calc(-50% + var(--wisp-drift-x-mid)),
- calc(-50% + var(--wisp-drift-y-mid))
- )
- scale(1.05) rotate(var(--wisp-rotate-mid));
- }
-
- 100% {
- opacity: 0;
- transform: translate(
- calc(-50% + var(--wisp-drift-x)),
- calc(-50% + var(--wisp-drift-y))
- )
- scale(0.42) rotate(var(--wisp-rotate));
- }
-}
-
-@keyframes khix-cursor-wisp-aura {
- 0% {
- opacity: 0.28;
- transform: scale(0.8);
- }
-
- 38% {
- opacity: 0.46;
- transform: scale(1.16);
- }
-
- 100% {
- opacity: 0;
- transform: scale(1.62);
- }
-}
-
-@keyframes khix-cursor-wisp-tail {
- 0% {
- opacity: 0.36;
- transform: rotate(var(--wisp-tail-rotate)) scaleX(0.55);
- }
-
- 52% {
- opacity: 0.32;
- transform: rotate(var(--wisp-tail-rotate-mid)) scaleX(1.26);
- }
-
- 100% {
- opacity: 0;
- transform: rotate(var(--wisp-tail-rotate-end)) scaleX(0.2);
- }
-}
-
-@keyframes khix-cursor-wisp-fog {
- 0% {
- opacity: 0.2;
- transform: translate3d(0, 0.12rem, 0) rotate(var(--wisp-tail-rotate));
- }
-
- 45% {
- opacity: 0.34;
- transform: translate3d(var(--wisp-fog-drift-x-mid), -0.18rem, 0)
- rotate(var(--wisp-tail-rotate-mid)) scale(1.18);
- }
-
- 100% {
- opacity: 0;
- transform: translate3d(var(--wisp-fog-drift-x-end), -0.5rem, 0)
- rotate(var(--wisp-tail-rotate-end)) scale(1.42);
- }
-}
-
-@keyframes khix-cursor-wisp-flicker {
- from {
- filter: blur(0.08rem);
- transform: rotate(-3deg) skewX(-12deg) scale(0.86, 0.94);
- }
-
- to {
- filter: blur(0.16rem);
- transform: rotate(5deg) skewX(8deg) scale(1.04, 1.12);
- }
-}
-
-@keyframes khix-cursor-wisp-thread-main {
- from {
- opacity: 0.58;
- transform: translate3d(-0.04rem, 0.08rem, 0) rotate(5deg) scale(0.82, 0.9);
- }
-
- to {
- opacity: 0.9;
- transform: translate3d(0.08rem, -0.08rem, 0) rotate(13deg) scale(1, 1.12);
- }
-}
-
-@keyframes khix-cursor-wisp-thread-side {
- from {
- opacity: 0.36;
- transform: translate3d(0.06rem, 0.08rem, 0) rotate(-18deg) scale(0.84, 0.88);
- }
-
- to {
- opacity: 0.68;
- transform: translate3d(-0.08rem, -0.1rem, 0) rotate(-8deg) scale(1.05, 1.08);
- }
-}
-
-@keyframes khix-enter-haze {
- from {
- opacity: 0.52;
- transform: translate3d(-0.3rem, 0.1rem, 0) scale(0.98);
- }
-
- to {
- opacity: 0.86;
- transform: translate3d(0.35rem, -0.2rem, 0) scale(1.03);
- }
-}
-
-@keyframes khix-enter-aura {
- from {
- opacity: 0.62;
- transform: scale(0.92);
- }
-
- to {
- opacity: 0.98;
- transform: scale(1.07);
- }
-}
-
-@keyframes khix-enter-text-float {
- from {
- opacity: 0.82;
- transform: translate3d(0, 0.12rem, 0);
- }
-
- to {
- opacity: 1;
- transform: translate3d(0, -0.12rem, 0);
- }
-}
-
-@keyframes khix-edge-vignette-pulse {
- from {
- opacity: 0.38;
- }
-
- to {
- opacity: 0.62;
- }
-}
-
-@keyframes khix-gem-magic-breathe {
- from {
- opacity: 0.42;
- transform: translate3d(0, 0, 0) scale(0.96);
- }
-
- to {
- opacity: 0.64;
- transform: translate3d(0.25rem, -0.16rem, 0) scale(1.04);
- }
-}
-
-@keyframes khix-gem-magic-drift-left {
- from {
- opacity: 0.3;
- transform: translate3d(0.2rem, 0.1rem, 0) rotate(-13deg) scaleX(0.92);
- }
-
- to {
- opacity: 0.5;
- transform: translate3d(-1.1rem, -0.45rem, 0) rotate(-18deg) scaleX(1.08);
- }
-}
-
-@keyframes khix-gem-magic-drift-mid {
- from {
- opacity: 0.26;
- transform: translate3d(-0.25rem, 0.08rem, 0) rotate(-8deg) scaleX(0.96);
- }
-
- to {
- opacity: 0.44;
- transform: translate3d(0.45rem, -0.35rem, 0) rotate(-5deg) scaleX(1.08);
- }
-}
-
-@keyframes khix-gem-magic-drift-right {
- from {
- opacity: 0.28;
- transform: translate3d(-0.1rem, 0.08rem, 0) rotate(10deg) scaleX(0.94);
- }
-
- to {
- opacity: 0.48;
- transform: translate3d(1rem, -0.48rem, 0) rotate(15deg) scaleX(1.08);
- }
-}
-
-@keyframes khix-gem-magic-thread-left {
- from {
- opacity: 0.36;
- transform: translate3d(0.35rem, 0.12rem, 0) rotate(-18deg) scaleX(0.78);
- }
-
- to {
- opacity: 0.72;
- transform: translate3d(-0.45rem, -0.26rem, 0) rotate(-24deg) scaleX(1.05);
- }
-}
-
-@keyframes khix-gem-magic-thread-right {
- from {
- opacity: 0.32;
- transform: translate3d(-0.2rem, 0.08rem, 0) rotate(12deg) scaleX(0.82);
- }
-
- to {
- opacity: 0.7;
- transform: translate3d(0.55rem, -0.24rem, 0) rotate(18deg) scaleX(1.08);
- }
-}
-
-@keyframes khix-gem-shard-mask-shimmer {
- from {
- opacity: 0.48;
- background-position:
- center center,
- -28vw center;
- }
-
- to {
- opacity: 0.74;
- background-position:
- center center,
- 28vw center;
- }
-}
-
-@keyframes khix-gem-shard-glint {
- from {
- opacity: 0.52;
- transform: translate3d(-0.18rem, 0.04rem, 0) skewX(-21deg) rotate(-6deg)
- scaleX(0.9);
- }
-
- to {
- opacity: 0.86;
- transform: translate3d(0.22rem, -0.1rem, 0) skewX(-21deg) rotate(-6deg)
- scaleX(1.06);
- }
-}
-
-@keyframes khix-gem-shard-spark-top {
- from {
- opacity: 0.36;
- transform: scale(0.72);
- }
-
- to {
- opacity: 0.74;
- transform: scale(1.08);
+ width: 100%;
+ min-width: 100%;
+ min-height: 100%;
+ background: hsl(var(--background));
}
-}
-@keyframes khix-gem-shard-spark-low {
- from {
- opacity: 0.26;
- transform: scale(0.7);
+ html {
+ scroll-behavior: smooth;
+ scroll-padding-top: 5.5rem;
}
- to {
- opacity: 0.56;
- transform: scale(1.04);
+ body {
+ @apply bg-background text-foreground;
+ font-family: var(--font-khix), Georgia, "Times New Roman", serif;
}
}
-.khix-experience-awaiting .khix-experience-stage *,
-.khix-experience-awaiting .khix-experience-stage *::before,
-.khix-experience-awaiting .khix-experience-stage *::after {
- animation: none !important;
-}
-
-.khix-experience-awaiting .khix-enter-gate::before,
-.khix-experience-awaiting .khix-enter-gate-aura,
-.khix-experience-awaiting .khix-enter-gate-content {
- animation: none !important;
-}
-
@media (prefers-reduced-motion: reduce) {
- .khix-gem-magic::before,
- .khix-gem-magic::after,
- .khix-gem-magic-wisp,
- .khix-gem-magic-thread,
- .khix-gem-shard-shimmer,
- .khix-gem-shard-glint,
- .khix-gem-shard-spark,
- .khix-edge-gloom::before,
- .khix-enter-gate::before,
- .khix-enter-gate-aura,
- .khix-enter-gate-content,
- .khix-cursor-wisp,
- .khix-cursor-wisp::before,
- .khix-cursor-wisp-tail,
- .khix-cursor-wisp-core {
- animation: none;
- }
-
- .khix-cursor-wisp {
- display: none;
- }
-}
-
-@media (min-width: 1024px) {
- .khix-mlh-badge {
- right: 3.125rem;
- width: 92px;
- }
-}
-
-@media (max-width: 900px) {
- .khix-background-layer {
- background-position: center center;
- }
-
- .khix-logo-lockup {
- width: min(75vw, 30rem);
- }
-
- .khix-event-date-lockup {
- width: min(56vw, 20rem);
- }
-
- .khix-event-date-text {
- font-size: clamp(1.25rem, 2.45vw, 1.75rem);
- }
-
- .khix-gem-aura {
- top: 51%;
- width: 15rem;
- height: 8rem;
- }
-}
-
-@media (max-width: 520px) {
- .khix-mlh-badge {
- right: 0.75rem;
- width: 60px;
- }
-
- .khix-audio-toggle {
- position: static;
- flex: 0 0 2.75rem;
- right: auto;
- bottom: auto;
- left: auto;
- width: 2.75rem;
- height: 2.75rem;
- color: rgba(255, 255, 246, 0.76);
- opacity: 0.84;
- line-height: 1;
- }
-
- .khix-social-links .khix-audio-toggle-row {
- position: static !important;
- flex: 0 0 2.75rem;
- right: auto !important;
- bottom: auto !important;
- left: auto !important;
- width: 2.75rem;
- height: 2.75rem;
- color: rgba(255, 255, 246, 0.76);
- line-height: 1;
- }
-
- .khix-hero {
- padding: 5.35rem 1rem 1.25rem;
- }
-
- .khix-background-layer {
- background-position: center center;
- }
-
- .khix-stage-accent-mushroom {
- --khix-mushroom-left: calc(50% - clamp(7.3rem, 31vw, 10.2rem));
- }
-
- .khix-stage-accent-ladybug {
- --khix-ladybug-width: clamp(1.05rem, 3.2vw, 1.35rem);
- }
-
- .khix-logo-lockup {
- width: min(85vw, 24rem);
- }
-
- .khix-event-date-lockup {
- width: min(62vw, 15.5rem);
- margin-top: -0.75rem;
- }
-
- .khix-event-date-text {
- font-size: clamp(0.95rem, 4.2vw, 1.15rem);
- text-shadow:
- 0 0 0.1rem rgba(255, 255, 255, 0.4),
- 0 0 0.3rem rgba(214, 255, 86, 0.15),
- 0 0.5rem 1rem rgba(0, 0, 0, 0.5);
- }
-
- .khix-event-location-text {
- text-shadow:
- 0 0 0.08rem rgba(255, 255, 255, 0.35),
- 0 0 0.25rem rgba(214, 255, 86, 0.12),
- 0 0.45rem 0.85rem rgba(0, 0, 0, 0.42);
- }
-
- .khix-hero-actions {
- bottom: calc(max(0.75rem, env(safe-area-inset-bottom)) + 5.65rem);
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- width: min(18rem, calc(100vw - 2.5rem));
- gap: 0.55rem;
- }
-
- .khix-social-links {
- bottom: max(0.75rem, env(safe-area-inset-bottom));
- gap: clamp(0.25rem, 1.8vw, 0.55rem);
- }
-
- .khix-social-link {
- width: 2.75rem;
- height: 2.75rem;
- }
-
- .khix-code-link {
- bottom: calc(max(0.75rem, env(safe-area-inset-bottom)) + 2.9rem);
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-height: 1.9rem;
- padding: 0 0.45rem;
- line-height: 1;
- }
-
- .khix-button {
- width: 100%;
- min-height: 2.5rem;
- padding: 0.58rem 0.65rem;
- font-size: 0.62rem;
- }
-
- .khix-code-link {
- font-size: 0.62rem;
- }
-}
-
-@media (max-width: 520px) and (max-height: 720px) {
- .khix-hero {
- padding-top: 4.35rem;
- }
-
- .khix-logo-lockup {
- width: min(80vw, 20rem);
- }
-
- .khix-event-date-lockup {
- width: min(56vw, 14.5rem);
- margin-top: -0.5rem;
- }
-
- .khix-event-date-text {
- font-size: clamp(0.9rem, 4vw, 1.08rem);
- }
-
- .khix-hero-actions {
- bottom: calc(max(0.62rem, env(safe-area-inset-bottom)) + 5.35rem);
- gap: 0.5rem;
- }
-
- .khix-social-links {
- bottom: max(0.5rem, env(safe-area-inset-bottom));
- }
-
- .khix-code-link {
- bottom: calc(max(0.5rem, env(safe-area-inset-bottom)) + 2.65rem);
- }
-
- .khix-audio-toggle {
- bottom: auto;
+ html {
+ scroll-behavior: auto;
}
}
diff --git a/apps/khix/src/app/layout.tsx b/apps/khix/src/app/layout.tsx
index 006ca4a26..9a970bce3 100644
--- a/apps/khix/src/app/layout.tsx
+++ b/apps/khix/src/app/layout.tsx
@@ -1,25 +1,24 @@
import "./globals.css";
import type { Metadata, Viewport } from "next";
-import { Faculty_Glyphic } from "next/font/google";
+import localFont from "next/font/local";
-import WispCursor from "./_components/WispCursor";
import {
- eventJsonLd,
OG_IMAGE_ALT,
- OG_IMAGE_HEIGHT,
OG_IMAGE_URL,
- OG_IMAGE_WIDTH,
SEO_DESCRIPTION,
SEO_KEYWORDS,
SEO_TITLE,
SITE_URL,
} from "./seo";
-const font = Faculty_Glyphic({
+const bagnard = localFont({
+ src: "./fonts/Bagnard.otf",
weight: "400",
- subsets: ["latin"],
- variable: "--font-khix",
+ style: "normal",
+ variable: "--font-bagnard",
+ display: "swap",
+ fallback: ["Georgia", "Times New Roman", "serif"],
});
export const metadata: Metadata = {
@@ -43,8 +42,24 @@ export const metadata: Metadata = {
},
},
icons: {
- icon: [{ url: "/khix-sigil.svg", type: "image/svg+xml" }],
- shortcut: [{ url: "/khix-sigil.svg", type: "image/svg+xml" }],
+ icon: [
+ {
+ url: "https://assets.knighthacks.org/khix/favicon.png",
+ type: "image/png",
+ },
+ ],
+ shortcut: [
+ {
+ url: "https://assets.knighthacks.org/khix/favicon.png",
+ type: "image/png",
+ },
+ ],
+ apple: [
+ {
+ url: "https://assets.knighthacks.org/khix/favicon.png",
+ type: "image/png",
+ },
+ ],
},
openGraph: {
type: "website",
@@ -56,8 +71,8 @@ export const metadata: Metadata = {
images: [
{
url: OG_IMAGE_URL,
- width: OG_IMAGE_WIDTH,
- height: OG_IMAGE_HEIGHT,
+ width: 1920,
+ height: 1080,
alt: OG_IMAGE_ALT,
},
],
@@ -78,7 +93,7 @@ export const metadata: Metadata = {
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
- themeColor: "#070c10",
+ themeColor: "#eef6cf",
viewportFit: "cover",
};
@@ -88,17 +103,12 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
-
-
- {children}
-
-
-
+
+ {children}
);
}
diff --git a/apps/khix/src/app/not-found.tsx b/apps/khix/src/app/not-found.tsx
new file mode 100644
index 000000000..d9d5de9a5
--- /dev/null
+++ b/apps/khix/src/app/not-found.tsx
@@ -0,0 +1,5 @@
+import { KhixPublicNotFound } from "./_components/not-found";
+
+export default function NotFound() {
+ return ;
+}
diff --git a/apps/khix/src/app/page.module.css b/apps/khix/src/app/page.module.css
new file mode 100644
index 000000000..6c592523e
--- /dev/null
+++ b/apps/khix/src/app/page.module.css
@@ -0,0 +1,132 @@
+.page {
+ width: 100%;
+ max-width: 100vw;
+ overflow-x: hidden;
+ overflow-x: clip;
+}
+
+.sectionStack {
+ --khix-about-overlap: clamp(2rem, 7svh, 4.75rem);
+ --khix-foreground-join: 65vw;
+ --khix-front-top: calc(
+ var(--khix-about-overlap) - var(--khix-foreground-join)
+ );
+ --khix-front-height: 312.5vw;
+ --khix-front-bottom: calc(var(--khix-front-top) + var(--khix-front-height));
+
+ position: relative;
+ z-index: 2;
+ margin-top: calc(var(--khix-about-overlap) * -1);
+ padding-top: var(--khix-about-overlap);
+ isolation: isolate;
+ overflow-x: clip;
+ overflow-y: visible;
+ background: #07100f;
+}
+
+.foregroundRegion {
+ position: relative;
+}
+
+.speakersSection {
+ --pond-transition-height: 138.8889vw;
+ --pond-atmosphere-height: var(--pond-transition-height);
+
+ position: relative;
+ z-index: 2;
+ display: grid;
+ min-height: 100vh;
+ min-height: 100svh;
+ padding: clamp(4rem, 8svh, 7rem) clamp(0.5rem, 2.5vw, 2rem);
+ isolation: isolate;
+ place-items: center;
+}
+
+.speakersSection::after {
+ position: absolute;
+ top: auto;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1;
+ height: var(--pond-transition-height);
+ pointer-events: none;
+ content: "";
+ background-image: url("https://assets.knighthacks.org/khix/pond-transition.webp");
+ background-repeat: no-repeat;
+ background-position: center bottom;
+ background-size: 100% auto;
+}
+
+.speakersSection > * {
+ position: relative;
+ z-index: 3;
+ transform: translateY(-5svh);
+}
+
+.speakersSection > .pondAtmosphere {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 2;
+ width: 100%;
+ height: var(--pond-atmosphere-height);
+ pointer-events: none;
+ transform: none;
+}
+
+.waterfallFaqSeparator {
+ position: relative;
+ left: 50%;
+ z-index: 5;
+ width: 100vw;
+ height: 40.7vw;
+ min-height: 24rem;
+ margin-top: clamp(-12rem, -10vw, -6rem);
+ margin-bottom: clamp(-19rem, -16vw, -6rem);
+ pointer-events: auto;
+ background-image: var(--faq-separator-image, none);
+ background-position: center top;
+ background-repeat: no-repeat;
+ background-size: 100% auto;
+ filter: drop-shadow(
+ 0 clamp(0.7rem, 1vw, 1.2rem) clamp(0.8rem, 1.3vw, 1.5rem)
+ rgba(2, 4, 15, 0.85)
+ );
+ transform: translateX(-50%);
+}
+
+@media (max-width: 820px) {
+ .waterfallFaqSeparator {
+ height: 40.7vw;
+ min-height: 0;
+ margin-top: -3rem;
+ }
+}
+
+@media (min-width: 761px) and (max-width: 1180px) {
+ .sectionStack {
+ --khix-foreground-join: calc(45rem + 50vw);
+ }
+}
+
+@media (max-width: 760px) {
+ .sectionStack {
+ --khix-about-overlap: clamp(1.5rem, 5svh, 3rem);
+ --khix-foreground-join: 152vw;
+ --khix-front-height: 1006.95vw;
+
+ overflow: visible;
+ }
+
+ .speakersSection {
+ --pond-atmosphere-height: calc(100svh + 55rem);
+ }
+}
+
+@media (max-width: 360px) {
+ .speakersSection {
+ --pond-atmosphere-height: calc(100svh + 52rem);
+ }
+}
diff --git a/apps/khix/src/app/page.tsx b/apps/khix/src/app/page.tsx
index 57d1638b0..2a9515de3 100644
--- a/apps/khix/src/app/page.tsx
+++ b/apps/khix/src/app/page.tsx
@@ -1,499 +1,53 @@
-"use client";
-
-import type { Variants } from "framer-motion";
-import { useCallback, useEffect, useReducer, useRef } from "react";
-import Image from "next/image";
-import { motion } from "framer-motion";
+import { Footer } from "./_components/footer";
+import { Navbar } from "./_components/navbar";
import {
- FaDiscord,
- FaGithub,
- FaInstagram,
- FaLink,
- FaLinkedin,
- FaVolumeMute,
- FaVolumeUp,
-} from "react-icons/fa";
-
+ KHIX_HOME_NAV_LINKS,
+ KHIX_SOCIAL_LINKS,
+} from "./_components/navbar/site-navigation";
+import { AboutCorruption, AboutSection } from "./_components/sections/about";
+import FAQ, { FAQTitle } from "./_components/sections/faq/faq";
+import Hero from "./_components/sections/hero";
import {
- APPLICATION_URL,
- DISCORD_URL,
- GITHUB_URL,
- INSTAGRAM_URL,
- LINKEDIN_URL,
- LINKTREE_URL,
- SEO_DESCRIPTION,
- SPONSOR_URL,
-} from "./seo";
-
-const mlhCodeOfConductUrl = "https://mlh.io/code-of-conduct";
-const mlhTrustBadgeUrl =
- "https://mlh.io/na?utm_source=na-hackathon&utm_medium=TrustBadge&utm_campaign=2027-season&utm_content=white";
-const mlhTrustBadgeImage =
- "https://logged-assets.s3.amazonaws.com/trust-badge/2027/mlh-trust-badge-2027-white.svg";
-
-const reveal: Variants = {
- hidden: { opacity: 0, y: 20 },
- show: {
- opacity: 1,
- y: 0,
- transition: { type: "tween", duration: 0.58, ease: "easeOut" },
- },
-};
-
-const fadeReveal: Variants = {
- hidden: { opacity: 0 },
- show: {
- opacity: 1,
- transition: { type: "tween", duration: 0.5, ease: "easeOut" },
- },
-};
-
-const musicVolume = 0.16;
-const birdsVolume = 0.055;
-
-const socialLinks = [
- {
- label: "Discord",
- href: DISCORD_URL,
- Icon: FaDiscord,
- },
- {
- label: "Instagram",
- href: INSTAGRAM_URL,
- Icon: FaInstagram,
- },
- {
- label: "LinkedIn",
- href: LINKEDIN_URL,
- Icon: FaLinkedin,
- },
- {
- label: "GitHub",
- href: GITHUB_URL,
- Icon: FaGithub,
- },
- {
- label: "Linktree",
- href: LINKTREE_URL,
- Icon: FaLink,
- },
-];
-
-interface ExperienceState {
- hasEntered: boolean;
- music: "idle" | "playing" | "paused";
-}
-
-type ExperienceEvent =
- | { type: "ENTER" }
- | { type: "MUSIC_STARTED" }
- | { type: "MUSIC_PAUSED" };
-
-const initialExperienceState: ExperienceState = {
- hasEntered: false,
- music: "idle",
-};
-
-function prepareAudioTrack(
- audio: HTMLAudioElement | null,
- volume: number,
-): HTMLAudioElement | null {
- if (!audio) {
- return null;
- }
-
- audio.volume = volume;
- audio.loop = true;
- return audio;
-}
-
-async function playAudioTracks(
- primaryTrack: HTMLAudioElement | null,
- secondaryTracks: HTMLAudioElement[] = [],
-) {
- if (!primaryTrack) {
- return false;
- }
-
- const tracks = [primaryTrack, ...secondaryTracks];
- const results = await Promise.allSettled(tracks.map((track) => track.play()));
- const didStartPrimaryTrack =
- results[0]?.status === "fulfilled" && !primaryTrack.paused;
-
- if (!didStartPrimaryTrack) {
- tracks.forEach((track) => track.pause());
- }
-
- return didStartPrimaryTrack;
-}
-
-function experienceReducer(
- state: ExperienceState,
- event: ExperienceEvent,
-): ExperienceState {
- switch (event.type) {
- case "ENTER":
- return { ...state, hasEntered: true };
- case "MUSIC_STARTED":
- return { hasEntered: true, music: "playing" };
- case "MUSIC_PAUSED":
- return { ...state, music: state.hasEntered ? "paused" : "idle" };
- default:
- return state;
- }
-}
+ KHIX_SPEAKERS,
+ PondAtmosphere,
+ SpeakerShowcase,
+} from "./_components/sections/speakers";
+import { SponsorTeamSection } from "./_components/sections/sponsor-team";
+import { TracksSection } from "./_components/sections/tracks";
+import styles from "./page.module.css";
+import { eventJsonLd } from "./seo";
export default function Page() {
- const audioRef = useRef(null);
- const birdsAudioRef = useRef(null);
- const hasStartedExperienceRef = useRef(false);
- const [experienceState, sendExperienceEvent] = useReducer(
- experienceReducer,
- initialExperienceState,
- );
- const hasEntered = experienceState.hasEntered;
- const isMusicPlaying = experienceState.music === "playing";
-
- const startExperience = useCallback(async () => {
- if (hasEntered || hasStartedExperienceRef.current) {
- return;
- }
-
- hasStartedExperienceRef.current = true;
- sendExperienceEvent({ type: "ENTER" });
-
- const musicTrack = prepareAudioTrack(audioRef.current, musicVolume);
- const secondaryTracks = [
- prepareAudioTrack(birdsAudioRef.current, birdsVolume),
- ].filter((track): track is HTMLAudioElement => Boolean(track));
-
- const didStartAudio = await playAudioTracks(musicTrack, secondaryTracks);
- if (didStartAudio) {
- sendExperienceEvent({ type: "MUSIC_STARTED" });
- } else {
- sendExperienceEvent({ type: "MUSIC_PAUSED" });
- }
- }, [hasEntered]);
-
- useEffect(() => {
- if (hasEntered) {
- return;
- }
-
- const handleKeyDown = (event: KeyboardEvent) => {
- if (event.code !== "Space") {
- return;
- }
-
- event.preventDefault();
- void startExperience();
- };
-
- window.addEventListener("keydown", handleKeyDown);
- return () => window.removeEventListener("keydown", handleKeyDown);
- }, [hasEntered, startExperience]);
-
- const toggleMusic = async () => {
- const musicTrack = prepareAudioTrack(audioRef.current, musicVolume);
- const secondaryTracks = [
- prepareAudioTrack(birdsAudioRef.current, birdsVolume),
- ].filter((track): track is HTMLAudioElement => Boolean(track));
- const tracks = musicTrack
- ? [musicTrack, ...secondaryTracks]
- : [...secondaryTracks];
-
- if (tracks.length === 0) {
- return;
- }
-
- if (isMusicPlaying) {
- tracks.forEach((track) => track.pause());
- sendExperienceEvent({ type: "MUSIC_PAUSED" });
- return;
- }
-
- const didStartAudio = await playAudioTracks(musicTrack, secondaryTracks);
- if (didStartAudio) {
- sendExperienceEvent({ type: "MUSIC_STARTED" });
- } else {
- sendExperienceEvent({ type: "MUSIC_PAUSED" });
- }
- };
-
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
Knight Hacks IX
-
{SEO_DESCRIPTION}
-
- October 9-11th, 2026
-
- University of Central Florida
-
-
-
-
-
-
- Join Blade
-
-
- Sponsor
-
+
-
- MLH Code of Conduct
-
-
- {socialLinks.map(({ label, href, Icon }) => (
-
-
-
- ))}
- {hasEntered ? (
-
- {isMusicPlaying ? (
-
- ) : (
-
- )}
-
- ) : null}
-
-
-
-
-
void startExperience()}
- aria-label="Enter Knight Hacks IX"
- >
-
-
- Tap or press Space to enter the Knight Hacks experience
-
-
-
- );
-}
-
-function MlhTrustBadge({ hasEntered }: { hasEntered: boolean }) {
- return (
-
- {/* eslint-disable-next-line @next/next/no-img-element -- MLH serves the required trust badge as a remote SVG. */}
-
-
+
+
+
+
+
+
+ >
);
}
diff --git a/apps/khix/src/app/seo.ts b/apps/khix/src/app/seo.ts
index 2dd1552d3..e8b8a3f67 100644
--- a/apps/khix/src/app/seo.ts
+++ b/apps/khix/src/app/seo.ts
@@ -1,68 +1,38 @@
export const SITE_URL = "https://2026.knighthacks.org";
-export const EVENT_NAME = "Knight Hacks IX";
-export const EVENT_DATE_LABEL = "October 9-11, 2026";
-export const EVENT_START_DATE = "2026-10-09";
-export const EVENT_END_DATE = "2026-10-11";
-export const ORGANIZER_NAME = "Knight Hacks";
-export const ORGANIZER_URL = "https://knighthacks.org";
-export const CONTACT_EMAIL = "hack@knighthacks.org";
-export const APPLICATION_URL = "https://blade.knighthacks.org/";
-export const SPONSOR_URL = "https://blade.knighthacks.org/sponsor";
-export const DISCORD_URL = "https://discord.knighthacks.org/";
-export const INSTAGRAM_URL = "https://www.instagram.com/knighthacks/";
-export const LINKEDIN_URL = "https://www.linkedin.com/company/knight-hacks";
-export const GITHUB_URL = "https://github.com/knighthacks";
-export const LINKTREE_URL = "https://linktr.ee/knighthacks";
-
-export const SEO_TITLE =
- "Knight Hacks IX | 36-Hour UCF Hackathon in Orlando, Florida";
-
+export const SEO_TITLE = "Knight Hacks IX";
export const SEO_DESCRIPTION =
- "Knight Hacks IX is a 36-hour in-person hackathon hosted by Knight Hacks at the University of Central Florida from October 9-11, 2026. Build projects with teams of up to four, attend workshops, meet mentors, join social events, and compete for prizes in Orlando, Florida.";
-
-export const OG_IMAGE_URL = "https://assets.knighthacks.org/IXSEO.webp";
-export const OG_IMAGE_WIDTH = 1200;
-export const OG_IMAGE_HEIGHT = 630;
+ "Knight Hacks IX is UCF's annual student hackathon for building, learning, and creating together.";
+export const OG_IMAGE_URL = "https://assets.knighthacks.org/khix/og-image.webp";
export const OG_IMAGE_ALT =
- "Knight Hacks IX, a 36-hour UCF hackathon in Orlando, Florida";
+ "Knight Hacks IX at the University of Central Florida";
export const SEO_KEYWORDS = [
"Knight Hacks IX",
- "Knight Hacks",
+ "Knight Hacks 2026",
"UCF hackathon",
- "University of Central Florida hackathon",
- "Orlando hackathon",
"Florida hackathon",
- "student hackathon Florida",
- "college hackathon",
- "beginner friendly hackathon",
+ "Orlando hackathon",
+ "student hackathon",
"36 hour hackathon",
- "MLH hackathon",
- "coding competition Orlando",
- "student tech event Orlando",
- "hackathon workshops",
- "hackathon prizes",
- "hackathon mentors",
+ "University of Central Florida hackathon",
];
-export const SOCIAL_PROFILE_URLS = [
- DISCORD_URL,
- INSTAGRAM_URL,
- LINKEDIN_URL,
- GITHUB_URL,
- LINKTREE_URL,
+export const KHIX_SOCIAL_URLS = [
+ "https://www.instagram.com/knighthacks/",
+ "https://www.linkedin.com/company/knight-hacks",
+ "https://github.com/KnightHacks",
+ "https://discord.knighthacks.org/",
];
export const eventJsonLd = {
"@context": "https://schema.org",
"@type": "Event",
- name: EVENT_NAME,
- alternateName: "KH IX",
+ name: SEO_TITLE,
description: SEO_DESCRIPTION,
url: SITE_URL,
image: OG_IMAGE_URL,
- startDate: EVENT_START_DATE,
- endDate: EVENT_END_DATE,
+ startDate: "2026-10-09",
+ endDate: "2026-10-11",
eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode",
eventStatus: "https://schema.org/EventScheduled",
keywords: SEO_KEYWORDS.join(", "),
@@ -78,14 +48,13 @@ export const eventJsonLd = {
},
organizer: {
"@type": "Organization",
- name: ORGANIZER_NAME,
- url: ORGANIZER_URL,
- email: CONTACT_EMAIL,
- sameAs: SOCIAL_PROFILE_URLS,
+ name: "Knight Hacks",
+ url: "https://knighthacks.org",
+ sameAs: KHIX_SOCIAL_URLS,
},
offers: {
"@type": "Offer",
- url: APPLICATION_URL,
+ url: `${SITE_URL}/apply`,
price: "0",
priceCurrency: "USD",
availability: "https://schema.org/InStock",
diff --git a/apps/khix/src/app/test/page.module.css b/apps/khix/src/app/test/page.module.css
new file mode 100644
index 000000000..b0062779d
--- /dev/null
+++ b/apps/khix/src/app/test/page.module.css
@@ -0,0 +1,391 @@
+.testPage {
+ min-height: 100vh;
+ color: #172033;
+ background: #eef6cf;
+}
+
+.section {
+ padding: clamp(4rem, 8vw, 7rem) clamp(1rem, 4vw, 4rem);
+ background:
+ radial-gradient(
+ circle at 18% 12%,
+ rgba(103, 228, 255, 0.2),
+ transparent 28%
+ ),
+ linear-gradient(180deg, #eef6cf 0%, #dff0d8 100%);
+}
+
+.scratchHeader {
+ padding: clamp(5rem, 12vw, 8rem) clamp(1rem, 4vw, 4rem)
+ clamp(2.5rem, 6vw, 4rem);
+ border-bottom: 1px solid rgba(23, 32, 51, 0.12);
+ background:
+ radial-gradient(
+ circle at 72% 16%,
+ rgba(143, 91, 245, 0.22),
+ transparent 28%
+ ),
+ linear-gradient(180deg, #eef6cf 0%, #c9f4d8 100%);
+}
+
+.scratchHeader .sectionHeader {
+ margin-bottom: 0;
+}
+
+.sectionHeader {
+ width: min(100%, 70rem);
+ margin: 0 auto clamp(1.5rem, 3vw, 2.5rem);
+}
+
+.eyebrow {
+ margin: 0 0 0.45rem;
+ color: #5f4779;
+ font-size: 0.78rem;
+ font-weight: 900;
+ letter-spacing: 0;
+ text-transform: uppercase;
+}
+
+.sectionHeader h1,
+.sectionHeader h2 {
+ margin: 0;
+ color: #172033;
+ font-size: clamp(2rem, 5vw, 4.25rem);
+ line-height: 0.95;
+}
+
+.componentGrid,
+.surfaceGrid,
+.stateGrid {
+ display: grid;
+ width: min(100%, 70rem);
+ margin: 0 auto;
+ gap: 1rem;
+}
+
+.componentGrid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.speakerSection {
+ overflow: hidden;
+ background:
+ linear-gradient(
+ 90deg,
+ rgba(255, 255, 255, 0.16),
+ transparent 16%,
+ transparent 84%,
+ rgba(255, 255, 255, 0.1)
+ ),
+ linear-gradient(180deg, #172033 0%, #10251f 58%, #07100f 100%);
+}
+
+.speakerShowcaseBox {
+ --speaker-stage-width: min(100%, 48rem);
+}
+
+.sponsorSection {
+ overflow: hidden;
+ background:
+ linear-gradient(
+ 90deg,
+ rgba(255, 255, 255, 0.22),
+ transparent 18%,
+ transparent 82%,
+ rgba(255, 255, 255, 0.16)
+ ),
+ linear-gradient(180deg, #dff0d8 0%, #c9f4d8 100%);
+}
+
+.sponsorShowcaseBox {
+ --sponsor-showcase-width: 70rem;
+}
+
+.teamSection {
+ background:
+ radial-gradient(
+ circle at 22% 18%,
+ rgba(143, 91, 245, 0.18),
+ transparent 32%
+ ),
+ linear-gradient(180deg, #dff0d8 0%, #eef6cf 100%);
+}
+
+.teamCascadeBox {
+ --team-cascade-height: clamp(25rem, 64svh, 42rem);
+
+ width: min(100%, 70rem);
+ margin: 0 auto;
+}
+
+.previewPanel,
+.typeSpec,
+.swatchPanel {
+ min-width: 0;
+ border: 1px solid rgba(23, 32, 51, 0.12);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.54);
+ box-shadow: 0 1.2rem 3.4rem rgba(23, 32, 51, 0.08);
+ overflow: hidden;
+}
+
+.previewPanel {
+ min-height: 22rem;
+}
+
+.panelHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+ padding: 0.85rem 1rem;
+ border-bottom: 1px solid rgba(23, 32, 51, 0.1);
+ color: rgba(23, 32, 51, 0.72);
+ font-size: 0.74rem;
+ font-weight: 900;
+ letter-spacing: 0;
+ text-transform: uppercase;
+}
+
+.componentMount {
+ display: flex;
+ min-height: 18rem;
+ align-items: center;
+ justify-content: center;
+ gap: 0.8rem;
+ padding: 1.2rem;
+}
+
+.placeholder {
+ display: grid;
+ width: min(100%, 20rem);
+ min-height: 9rem;
+ place-items: center;
+ align-content: center;
+ gap: 0.45rem;
+ border: 1px dashed rgba(95, 71, 121, 0.4);
+ border-radius: 8px;
+ color: #5f4779;
+ text-align: center;
+}
+
+.placeholderKicker {
+ font-size: 0.72rem;
+ font-weight: 900;
+ letter-spacing: 0;
+ text-transform: uppercase;
+}
+
+.placeholder strong {
+ color: #172033;
+ font-size: 1.1rem;
+ line-height: 1.15;
+}
+
+.darkPanel {
+ color: #ffffff;
+ border-color: rgba(255, 255, 255, 0.14);
+ background:
+ radial-gradient(
+ circle at 64% 18%,
+ rgba(143, 91, 245, 0.35),
+ transparent 32%
+ ),
+ linear-gradient(160deg, #172033 0%, #23364a 100%);
+}
+
+.darkPanel .panelHeader {
+ border-bottom-color: rgba(255, 255, 255, 0.14);
+ color: rgba(255, 255, 255, 0.74);
+}
+
+.imagePanel {
+ color: #ffffff;
+ background:
+ linear-gradient(180deg, rgba(23, 32, 51, 0.05), rgba(23, 32, 51, 0.58)),
+ url("https://assets.knighthacks.org/khix/hero-7_bg.webp") center / cover;
+}
+
+.imagePanel .panelHeader {
+ border-bottom-color: rgba(255, 255, 255, 0.18);
+ color: rgba(255, 255, 255, 0.82);
+}
+
+.surfaceSection {
+ background:
+ linear-gradient(
+ 180deg,
+ rgba(238, 246, 207, 0.88),
+ rgba(201, 244, 216, 0.9)
+ ),
+ #c9f4d8;
+}
+
+.surfaceGrid {
+ grid-template-columns: minmax(0, 1.25fr) minmax(18rem, 0.75fr);
+}
+
+.typeSpec {
+ padding: clamp(1.25rem, 3vw, 2rem);
+}
+
+.displayText {
+ margin: 0;
+ color: #172033;
+ font-size: clamp(3rem, 8vw, 7rem);
+ font-weight: 950;
+ line-height: 0.86;
+}
+
+.bodyText {
+ max-width: 42rem;
+ margin: 1.2rem 0 0;
+ color: rgba(23, 32, 51, 0.72);
+ font-size: clamp(1rem, 1.8vw, 1.25rem);
+ line-height: 1.5;
+}
+
+.swatchPanel {
+ display: grid;
+ align-content: start;
+ gap: 0.75rem;
+ padding: 1rem;
+}
+
+.swatchItem {
+ display: grid;
+ grid-template-columns: 2.4rem 1fr;
+ align-items: center;
+ gap: 0.8rem;
+ color: rgba(23, 32, 51, 0.76);
+ font-weight: 850;
+}
+
+.swatch {
+ width: 2.4rem;
+ aspect-ratio: 1;
+ border: 1px solid rgba(23, 32, 51, 0.12);
+ border-radius: 999px;
+ box-shadow: inset 0 0 0 0.25rem rgba(255, 255, 255, 0.24);
+}
+
+.stateGrid {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ align-items: center;
+}
+
+.primaryButton,
+.secondaryButton,
+.textLink {
+ display: inline-flex;
+ min-height: 3rem;
+ align-items: center;
+ justify-content: center;
+ border-radius: 999px;
+ font-weight: 900;
+ letter-spacing: 0;
+ text-decoration: none;
+ transition:
+ background 180ms ease,
+ border-color 180ms ease,
+ color 180ms ease,
+ opacity 180ms ease,
+ transform 180ms ease;
+}
+
+.primaryButton,
+.secondaryButton {
+ border: 1px solid transparent;
+ padding: 0.75rem 1.2rem;
+ cursor: pointer;
+}
+
+.primaryButton {
+ color: #ffffff;
+ background: #5f4779;
+ box-shadow: 0 0.9rem 2rem rgba(95, 71, 121, 0.24);
+}
+
+.primaryButton:hover,
+.primaryButton:focus-visible,
+.primaryButton[data-state="hover"] {
+ background: #7d58b1;
+ outline: none;
+ transform: translateY(-1px);
+}
+
+.primaryButton:disabled {
+ cursor: not-allowed;
+ opacity: 0.46;
+ transform: none;
+}
+
+.secondaryButton {
+ color: #ffffff;
+ border-color: rgba(255, 255, 255, 0.32);
+ background: rgba(255, 255, 255, 0.08);
+}
+
+.secondaryButton:hover,
+.secondaryButton:focus-visible {
+ border-color: rgba(255, 255, 255, 0.74);
+ outline: none;
+ transform: translateY(-1px);
+}
+
+.textLink {
+ color: #5f4779;
+ min-width: 0;
+}
+
+.textLink:hover,
+.textLink:focus-visible {
+ color: #172033;
+ outline: none;
+ transform: translateY(-1px);
+}
+
+@media (max-width: 920px) {
+ .componentGrid,
+ .surfaceGrid,
+ .stateGrid {
+ grid-template-columns: 1fr;
+ }
+
+ .teamCascadeBox {
+ --team-cascade-height: clamp(23rem, 58svh, 34rem);
+ }
+}
+
+@media (max-width: 620px) {
+ .section {
+ padding-right: 0.85rem;
+ padding-left: 0.85rem;
+ }
+
+ .componentMount {
+ min-height: 15rem;
+ }
+
+ .displayText {
+ font-size: clamp(2.5rem, 18vw, 4.4rem);
+ }
+
+ .teamCascadeBox {
+ --team-cascade-height: 29rem;
+ }
+}
+
+@media (max-width: 380px) {
+ .teamCascadeBox {
+ --team-cascade-height: 27rem;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .primaryButton,
+ .secondaryButton,
+ .textLink {
+ transition: none;
+ }
+}
diff --git a/apps/khix/src/app/test/page.tsx b/apps/khix/src/app/test/page.tsx
new file mode 100644
index 000000000..8df653567
--- /dev/null
+++ b/apps/khix/src/app/test/page.tsx
@@ -0,0 +1,254 @@
+import type { Metadata } from "next";
+import Link from "next/link";
+
+import type { SponsorShowcaseSponsor } from "../_components/sponsor-showcase";
+import { AssetCredit } from "../_components/assets";
+import {
+ KHIX_SPEAKERS,
+ SpeakerShowcase,
+} from "../_components/sections/speakers";
+import { SponsorShowcase } from "../_components/sponsor-showcase";
+import { TeamCascade } from "../_components/team-cascade";
+import styles from "./page.module.css";
+
+export const metadata: Metadata = {
+ title: "KHIX Component Test",
+ robots: {
+ index: false,
+ follow: false,
+ },
+};
+
+const SAMPLE_CREDITS = [
+ {
+ name: "Design Team",
+ },
+ {
+ name: "Knight Hacks",
+ href: "https://knighthacks.org",
+ },
+];
+
+const SWATCHES = [
+ { name: "Moss", value: "#eef6cf" },
+ { name: "Mint", value: "#c9f4d8" },
+ { name: "Violet", value: "#8f5bf5" },
+ { name: "Lagoon", value: "#67e4ff" },
+ { name: "Ink", value: "#172033" },
+];
+
+const SAMPLE_SPONSORS = [
+ {
+ name: "NVIDIA",
+ websiteUrl: "https://www.nvidia.com",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-nvidia.svg",
+ tier: "forest-sovereign",
+ },
+ {
+ name: "Google",
+ websiteUrl: "https://google.com",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-google.svg",
+ tier: "platinum-crown",
+ },
+ {
+ name: "AMD",
+ websiteUrl: "https://www.amd.com",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-amd.svg",
+ tier: "golden-dawn",
+ },
+ {
+ name: "Roblox",
+ websiteUrl: "https://www.roblox.com",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-roblox.svg",
+ tier: "silver-moon",
+ },
+ {
+ name: "ServiceNow",
+ websiteUrl: "https://www.servicenow.com",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-servicenow.svg",
+ tier: "silver-moon",
+ },
+ {
+ name: "Synopsys",
+ websiteUrl: "https://www.synopsys.com",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-synopsys.svg",
+ tier: "silver-moon",
+ },
+ {
+ name: "BNY",
+ websiteUrl: "https://www.bny.com",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-bny.svg",
+ tier: "bronze-ember",
+ },
+ {
+ name: "Statsig",
+ websiteUrl: "https://www.statsig.com",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-statsig.svg",
+ tier: "bronze-ember",
+ },
+ {
+ name: "Lockheed Martin",
+ websiteUrl: "https://www.lockheedmartin.com",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-lockheed-martin.svg",
+ tier: "bronze-ember",
+ },
+ {
+ name: "Auritas",
+ websiteUrl: "https://www.auritas.com",
+ logoSrc: "https://assets.knighthacks.org/khix/sponsor-auritas.svg",
+ tier: "bronze-ember",
+ },
+] satisfies SponsorShowcaseSponsor[];
+
+export default function TestPage() {
+ return (
+
+
+
+
KHIX scratch
+
Component test page
+
+
+
+
+
+
+
+
+
+
Public roster
+
Team cascade
+
+
+
+
+
+
+
+
Component bench
+
Responsive slots
+
+
+
+
+
+ Primary canvas
+ Fluid
+
+
+
+ Drop-in slot
+ Import a new component here
+
+
+
+
+
+
+ Dark canvas
+ Contrast
+
+
+ Register
+ Schedule
+
+
+
+
+
+ Scene canvas
+ Overlay
+
+
+
+
+
+
+
+
+
Visual system
+
Surfaces and type
+
+
+
+
+ Knight Hacks IX
+
+ Build against long text, compact spacing, and responsive line
+ breaks before a component reaches the homepage.
+
+
+
+
+ {SWATCHES.map((swatch) => (
+
+
+ {swatch.name}
+
+ ))}
+
+
+
+
+
+
+
Interaction states
+
Controls
+
+
+
+ Default
+
+ Hover
+
+
+ Disabled
+
+
+ Back to site
+
+
+
+
+ );
+}
diff --git a/apps/khix/src/auth/client.ts b/apps/khix/src/auth/client.ts
new file mode 100644
index 000000000..b6791d308
--- /dev/null
+++ b/apps/khix/src/auth/client.ts
@@ -0,0 +1,9 @@
+"use client";
+
+import { createForgeAuthClient } from "@forge/auth/client-factory";
+
+export const khixAuthClient = createForgeAuthClient({
+ defaultRedirectPath: "/dashboard",
+});
+
+export const { auth, authClient, signIn, signOut } = khixAuthClient;
diff --git a/apps/khix/src/auth/server.ts b/apps/khix/src/auth/server.ts
new file mode 100644
index 000000000..d981c306c
--- /dev/null
+++ b/apps/khix/src/auth/server.ts
@@ -0,0 +1,13 @@
+import { createForgeAuthServer } from "@forge/auth/server-factory";
+
+import { env } from "~/env";
+
+export const KHIX_AUTH_BASE_URL =
+ env.NODE_ENV === "production" ? env.KHIX_URL : "http://localhost:3007";
+
+export const khixAuth = createForgeAuthServer({
+ baseURL: KHIX_AUTH_BASE_URL,
+ defaultRedirectPath: "/dashboard",
+});
+
+export const { auth, handlers, signIn, signInRoute, validateToken } = khixAuth;
diff --git a/apps/khix/src/env.ts b/apps/khix/src/env.ts
new file mode 100644
index 000000000..6ba42ba43
--- /dev/null
+++ b/apps/khix/src/env.ts
@@ -0,0 +1,35 @@
+import { createEnv } from "@t3-oss/env-nextjs";
+import { z } from "zod";
+
+import { hackathonPortalOriginSchema } from "@forge/validators";
+
+const bladeUrlSchema =
+ process.env.NODE_ENV === "production"
+ ? hackathonPortalOriginSchema
+ : hackathonPortalOriginSchema.default("http://localhost:3000");
+
+const khixUrlSchema =
+ process.env.NODE_ENV === "production"
+ ? hackathonPortalOriginSchema
+ : hackathonPortalOriginSchema.default("http://localhost:3007");
+
+export const env = createEnv({
+ server: {
+ BLADE_URL: bladeUrlSchema,
+ KHIX_URL: khixUrlSchema,
+ },
+ shared: {
+ NODE_ENV: z
+ .enum(["development", "production", "test"])
+ .default("development"),
+ PORT: z.coerce.number().default(3007),
+ },
+ runtimeEnv: {
+ BLADE_URL: process.env.BLADE_URL,
+ KHIX_URL: process.env.KHIX_URL,
+ NODE_ENV: process.env.NODE_ENV,
+ PORT: process.env.PORT,
+ },
+ skipValidation:
+ !!process.env.CI || process.env.npm_lifecycle_event === "lint",
+});
diff --git a/apps/khix/src/lib/khix-hackathon.ts b/apps/khix/src/lib/khix-hackathon.ts
new file mode 100644
index 000000000..27c5a1f1b
--- /dev/null
+++ b/apps/khix/src/lib/khix-hackathon.ts
@@ -0,0 +1,17 @@
+import { db } from "@forge/db/client";
+
+import { KHIX_HACKATHON_NAMES } from "./portal-config";
+
+export async function getKhixHackathon() {
+ for (const name of KHIX_HACKATHON_NAMES) {
+ const hackathon = await db.query.Hackathon.findFirst({
+ where: (table, { eq }) => eq(table.name, name),
+ });
+
+ if (hackathon) {
+ return { hackathon, name };
+ }
+ }
+
+ return null;
+}
diff --git a/apps/khix/src/lib/portal-config.ts b/apps/khix/src/lib/portal-config.ts
new file mode 100644
index 000000000..ab32912fc
--- /dev/null
+++ b/apps/khix/src/lib/portal-config.ts
@@ -0,0 +1,25 @@
+import type { HackathonPortalConfig } from "@forge/hackathon";
+
+export const KHIX_HACKATHON_NAMES = [
+ "knighthacksix",
+ "khix",
+ "knight-hacks-ix",
+] as const;
+
+export const KHIX_DEFAULT_HACKATHON_NAME = KHIX_HACKATHON_NAMES[0];
+
+export const KHIX_PORTAL_CONFIG = {
+ hackathonName: KHIX_DEFAULT_HACKATHON_NAME,
+ routes: {
+ home: "/",
+ dashboard: "/dashboard",
+ apply: "/apply",
+ profile: "/dashboard/profile",
+ },
+ termsUrl: "https://knight-hacks.notion.site/knight-hacks-26-tos",
+ guideUrl: "https://knight-hacks.notion.site/knighthacksix",
+ copy: {
+ applicationName: "Knight Hacks IX",
+ supportChannelUrl: "https://discord.knighthacks.org/",
+ },
+} satisfies HackathonPortalConfig;
diff --git a/apps/khix/tailwind.config.ts b/apps/khix/tailwind.config.ts
index 721482e66..5100b51d6 100644
--- a/apps/khix/tailwind.config.ts
+++ b/apps/khix/tailwind.config.ts
@@ -1,39 +1,15 @@
import type { Config } from "tailwindcss";
-import defaultTheme from "tailwindcss/defaultTheme";
-
-const { fontFamily } = defaultTheme;
export default {
darkMode: ["class", ".dark"],
theme: {
extend: {
- backgroundImage: {
- "custom-radial": `radial-gradient(121.83% 96.39% at 50.3% 9.28%,
- rgba(216, 179, 254, 0.7) 0%,
- rgba(216, 179, 254, 0.7) 0.01%,
- rgba(216, 179, 254, 0.7) 5.55%,
- rgba(216, 179, 254, 0.7) 14.99%,
- rgba(133, 87, 180, 0.598491) 42.5%,
- rgba(46, 22, 71, 0.7) 86.9%)`,
- },
- filter: {
- "blur-20": "blur(20px)",
- "blur-25": "blur(25px)",
- },
- brightness: {
- 150: "1.5",
- },
- transitionTimingFunction: {
- "minor-spring": "cubic-bezier(0.18,0.89,0.82,1.04)",
- },
colors: {
background: "hsl(var(--background))",
- 950: "#10182B",
- cream: "#F4F4ED",
+ foreground: "hsl(var(--foreground))",
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
- foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
@@ -63,103 +39,6 @@ export default {
foreground: "hsl(var(--card-foreground))",
},
},
- boxShadow: {
- impact: `
- 0px 0px 1879.49px #D8B5FE,
- 0px 0px 1073.99px #D8B5FE,
- 0px 0px 626.495px #D8B5FE,
- 0px 0px 313.248px #D8B5FE,
- 0px 0px 89.4993px #D8B5FE,
- 0px 0px 44.7496px #D8B5FE`,
- glow: "0 0 20px rgba(255, 204, 112, 0.7), 0 0 40px rgba(200, 80, 192, 0.5), 0 0 60px rgba(65, 88, 208, 0.3)",
- glow2:
- "0 0 20px rgba(50, 255, 50, 0.7), 0 0 40px rgba(20, 200, 20, 0.5), 0 0 60px rgba(5, 150, 5, 0.3)",
- },
- fontFamily: {
- sans: ["var(--font-khix)", ...fontFamily.sans],
- mono: [...fontFamily.mono],
- },
- animation: {
- "infinite-scroll": "infinite-scroll 160s linear infinite",
- "accordion-down": "accordion-down 0.5s cubic-bezier(0.87, 0, 0.13, 1)",
- "accordion-up": "accordion-up 0.5s cubic-bezier(0.87, 0, 0.13, 1)",
- "fade-up": "fade-up 2.8s ease-out forwards",
- "fade-up-delay": "fade-up 2.8s ease-out forwards 0.4s",
- first: "moveVertical 30s ease infinite",
- second: "moveInCircle 20s reverse infinite",
- third: "moveInCircle 40s linear infinite",
- fourth: "moveHorizontal 40s ease infinite",
- fifth: "moveInCircle 20s ease infinite",
- },
- keyframes: {
- "infinite-scroll": {
- from: { transform: "translateX(0)" },
- to: { transform: "translateX(-180%)" },
- },
- "reveal-up": {
- "0%": { opacity: "0", transform: "translateY(80%)" },
- "100%": { opacity: "1", transform: "translateY(0)" },
- },
- "reveal-down": {
- "0%": { opacity: "0", transform: "translateY(-80%)" },
- "100%": { opacity: "1", transform: "translateY(0)" },
- },
- "content-blur": {
- "0%": { filter: "blur(0.3rem)" },
- "100%": { filter: "blur(0)" },
- },
- "rotate-full": {
- "0%": { transform: "rotate(0deg)" },
- "100%": { transform: "rotate(360deg)" },
- },
- "fade-up": {
- "0%": { opacity: "0", transform: "translateY(20px)" },
- "100%": { opacity: "1", transform: "translateY(0)" },
- },
- "accordion-down": {
- from: {
- height: "0",
- opacity: "0",
- padding: "0",
- margin: "0",
- },
- to: {
- height: "var(--radix-accordion-content-height)",
- opacity: "1",
- padding: "1rem",
- margin: "0.5rem 0",
- },
- },
- "accordion-up": {
- from: {
- height: "var(--radix-accordion-content-height)",
- opacity: "1",
- padding: "1rem",
- margin: "0.5rem 0",
- },
- to: {
- height: "0",
- opacity: "0",
- padding: "0",
- margin: "0",
- },
- },
- moveHorizontal: {
- "0%": { transform: "translateX(-50%) translateY(-10%)" },
- "50%": { transform: "translateX(50%) translateY(10%)" },
- "100%": { transform: "translateX(-50%) translateY(-10%)" },
- },
- moveInCircle: {
- "0%": { transform: "rotate(0deg)" },
- "50%": { transform: "rotate(180deg)" },
- "100%": { transform: "rotate(360deg)" },
- },
- moveVertical: {
- "0%": { transform: "translateY(-50%)" },
- "50%": { transform: "translateY(50%)" },
- "100%": { transform: "translateY(-50%)" },
- },
- },
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
diff --git a/docs/PARALLAX.md b/docs/PARALLAX.md
new file mode 100644
index 000000000..fdb1b9040
--- /dev/null
+++ b/docs/PARALLAX.md
@@ -0,0 +1,77 @@
+# Parallax Implementation Notes
+
+Temporary handoff for agents working on KHIX or future Forge parallax sections.
+This documents the Chromium fix from Carlos's `39fc6795` commit so it can be
+reused without rediscovering the same failure mode.
+
+## What Broke
+
+The KHIX hero parallax broke when the art stack was treated like a page
+background instead of a normal hero section. The problematic shape was:
+
+- a long or sticky scroll zone that made the hero behave like a background;
+- a large transformed `.art` container using `scale(...)`;
+- per-layer parallax values that did not account for that container scale;
+- unthrottled pointer writes fighting Chromium's rendering pipeline.
+
+The visible symptoms were rapid inline style churn in DevTools, unstable
+Chromium parallax, and the hero reading as the entire page background instead
+of a section that can scroll away.
+
+## Carlos's Fix
+
+Keep the hero itself as a normal section:
+
+- `section#home` owns the hero and stays in document flow.
+- `.stage` is `position: relative`, not sticky, for a normal hero.
+- The hero is `100vh` / `100svh`; extra page scroll should come from following
+ sections, not from a fake hero scroll zone.
+
+Keep the artwork full-bleed inside the hero without scaling the whole `.art`
+container:
+
+- Store the old visual scale as `--khix-art-motion-scale: 0.64`.
+- Use a smaller art inset, currently `inset: -6.32svh -3.76vw`.
+- Do not put `transform: scale(...)` on `.art`.
+- Apply `--khix-art-motion-scale` inside each layer's parallax transform math.
+
+Scope motion variables to the stage and throttle writes:
+
+- Write `--khix-hero-pointer-x`, `--khix-hero-pointer-y`, and
+ `--khix-hero-scroll-progress` on the hero stage.
+- Coalesce pointer movement with `requestAnimationFrame`.
+- Only write motion values when they changed by a meaningful epsilon.
+- Cancel pending pointer frames during cleanup and when reduced motion is
+ enabled.
+
+## Reusable Pattern
+
+For future parallax components:
+
+1. Make the section normal page flow first. Avoid sticky or oversized scroll
+ zones unless the design explicitly calls for pinned storytelling.
+2. Keep the art stack visually oversized with insets, not by scaling the whole
+ container.
+3. If the art needs a visual scale, put that number in a CSS variable and
+ multiply the pointer and scroll offsets by it.
+4. Put all layer movement on `transform: translate3d(...)` and keep depth
+ values as CSS vars on each layer.
+5. Use one local stage for motion vars. Do not inject global `:root` motion
+ state for a component-specific parallax.
+6. Throttle pointer writes with `requestAnimationFrame`; throttle scroll writes
+ too, and skip writes when the value barely changed.
+7. Respect `prefers-reduced-motion` by resetting pointer and scroll progress to
+ `0` and disabling nonessential animated layers.
+
+## Verification Checklist
+
+Before handing off a parallax change:
+
+- The hero/section scrolls away like a normal section when content follows it.
+- The stage is not sticky unless pinned scroll storytelling is the requirement.
+- Chrome/Chromium DevTools does not show unrelated title/glow/art nodes
+ constantly changing inline `style` attributes.
+- Pointer movement still changes layer transforms differently by depth.
+- Scroll changes `--khix-hero-scroll-progress` without causing layout shifts.
+- `pnpm --filter=@forge/khix lint` and
+ `pnpm --filter=@forge/khix typecheck` pass for KHIX changes.
diff --git a/output/playwright/khix-pond-magic-brighter.png b/output/playwright/khix-pond-magic-brighter.png
new file mode 100644
index 000000000..2bec87adf
Binary files /dev/null and b/output/playwright/khix-pond-magic-brighter.png differ
diff --git a/output/playwright/khix-pond-magic-desktop.png b/output/playwright/khix-pond-magic-desktop.png
new file mode 100644
index 000000000..d857f91c8
Binary files /dev/null and b/output/playwright/khix-pond-magic-desktop.png differ
diff --git a/output/playwright/khix-pond-magic-mobile-brighter.png b/output/playwright/khix-pond-magic-mobile-brighter.png
new file mode 100644
index 000000000..b99a8a5c2
Binary files /dev/null and b/output/playwright/khix-pond-magic-mobile-brighter.png differ
diff --git a/output/playwright/khix-pond-magic-mobile-pond.png b/output/playwright/khix-pond-magic-mobile-pond.png
new file mode 100644
index 000000000..888f18d77
Binary files /dev/null and b/output/playwright/khix-pond-magic-mobile-pond.png differ
diff --git a/output/playwright/khix-pond-magic-mobile.png b/output/playwright/khix-pond-magic-mobile.png
new file mode 100644
index 000000000..1e5a0f582
Binary files /dev/null and b/output/playwright/khix-pond-magic-mobile.png differ
diff --git a/packages/api/src/routers/hackers/mutations.ts b/packages/api/src/routers/hackers/mutations.ts
index d6bf25446..df9962188 100644
--- a/packages/api/src/routers/hackers/mutations.ts
+++ b/packages/api/src/routers/hackers/mutations.ts
@@ -24,6 +24,25 @@ import {
} from "../../resume-storage";
import { permProcedure, protectedProcedure } from "../../trpc";
+const CHECK_IN_DISCORD_ROLES = [
+ {
+ label: "BloomKnights",
+ roleId: HACKATHONS.BLOOMKNIGHTS.EVENT_ROLE_ID,
+ names: [HACKATHONS.BLOOMKNIGHTS.HACKATHON_NAME] as const,
+ },
+ {
+ label: "Knight Hacks IX",
+ roleId: HACKATHONS.KHIX.CHECK_IN_DISCORD_ROLE_ID,
+ names: HACKATHONS.KHIX.HACKATHON_NAMES,
+ },
+] as const;
+
+function getCheckInDiscordRole(hackathonName: string) {
+ return CHECK_IN_DISCORD_ROLES.find((role) =>
+ role.names.some((name) => name === hackathonName),
+ );
+}
+
export const hackerMutationRouter = {
createHacker: protectedProcedure
.input(hackerApplicationWireSchema.extend({ hackathonName: z.string() }))
@@ -621,7 +640,9 @@ export const hackerMutationRouter = {
.set({ status: "checkedin" })
.where(eq(HackerAttendee.id, hackerAttendee.id));
- if (hackathon.name === HACKATHONS.BLOOMKNIGHTS.HACKATHON_NAME) {
+ const checkInDiscordRole = getCheckInDiscordRole(hackathon.name);
+
+ if (checkInDiscordRole) {
try {
const discordId = await discord.resolveDiscordUserId(
hacker.discordUser,
@@ -637,7 +658,7 @@ export const hackerMutationRouter = {
});
} catch (logError) {
logger.error(
- "Failed to log skipped BloomKnights role assignment:",
+ `Failed to log skipped ${checkInDiscordRole.label} role assignment:`,
logError instanceof Error
? logError.message
: "Unknown error",
@@ -646,27 +667,27 @@ export const hackerMutationRouter = {
} else {
await discord.addRoleToMember(
discordId,
- HACKATHONS.BLOOMKNIGHTS.EVENT_ROLE_ID,
+ checkInDiscordRole.roleId,
);
logger.log(
- `Assigned role ${HACKATHONS.BLOOMKNIGHTS.EVENT_ROLE_ID} to user ${discordId}`,
+ `Assigned role ${checkInDiscordRole.roleId} to user ${discordId}`,
);
}
} catch (e) {
logger.error(
- "Failed to assign the BloomKnights role:",
+ `Failed to assign the ${checkInDiscordRole.label} role:`,
e instanceof Error ? e.message : "Unknown error",
);
try {
await discord.log({
title: "Discord role assign failed",
- message: `Failed to assign the BloomKnights role for "${hacker.discordUser}".`,
+ message: `Failed to assign the ${checkInDiscordRole.label} role for "${hacker.discordUser}".`,
color: "uhoh_red",
userId: ctx.session.user.discordUserId,
});
} catch (logError) {
logger.error(
- "Failed to log BloomKnights role assignment failure:",
+ `Failed to log ${checkInDiscordRole.label} role assignment failure:`,
logError instanceof Error ? logError.message : "Unknown error",
);
}
diff --git a/packages/api/src/routers/participant-portal.ts b/packages/api/src/routers/participant-portal.ts
index c3c574429..bfa745cef 100644
--- a/packages/api/src/routers/participant-portal.ts
+++ b/packages/api/src/routers/participant-portal.ts
@@ -266,14 +266,23 @@ const participantPortalRouterImplementation = {
const fileBuffer = decodeAndValidateResumeDataUrl(input.fileContent);
const filePath = createResumeObjectName(ctx.session.user.id);
- await ensureResumeBucketExists();
- await resumeStorageClient.putObject(
- RESUME_BUCKET_NAME,
- filePath,
- fileBuffer,
- fileBuffer.length,
- { "Content-Type": "application/pdf" },
- );
+ try {
+ await ensureResumeBucketExists();
+ await resumeStorageClient.putObject(
+ RESUME_BUCKET_NAME,
+ filePath,
+ fileBuffer,
+ fileBuffer.length,
+ { "Content-Type": "application/pdf" },
+ );
+ } catch (error) {
+ logger.error("Unable to upload participant resume:", error);
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message:
+ "Could not upload your resume right now. Please try again or contact the organizers.",
+ });
+ }
return filePath;
}),
diff --git a/packages/consts/src/hackathons/index.ts b/packages/consts/src/hackathons/index.ts
index cb4daeacc..e05be8fbb 100644
--- a/packages/consts/src/hackathons/index.ts
+++ b/packages/consts/src/hackathons/index.ts
@@ -1,4 +1,5 @@
export * as BLOOMKNIGHTS from "./bloomknights";
+export * as KHIX from "./khix";
export const APPLICATION_BACKGROUND_OPTIONS = [
{
diff --git a/packages/consts/src/hackathons/khix.ts b/packages/consts/src/hackathons/khix.ts
new file mode 100644
index 000000000..4ba5514d2
--- /dev/null
+++ b/packages/consts/src/hackathons/khix.ts
@@ -0,0 +1,7 @@
+export const HACKATHON_NAMES = [
+ "knighthacksix",
+ "khix",
+ "knight-hacks-ix",
+] as const;
+
+export const CHECK_IN_DISCORD_ROLE_ID = "1524920854427992074";
diff --git a/packages/email/src/hackathons/templates.ts b/packages/email/src/hackathons/templates.ts
index 90193f3d9..35d610b67 100644
--- a/packages/email/src/hackathons/templates.ts
+++ b/packages/email/src/hackathons/templates.ts
@@ -15,6 +15,10 @@ export const HACKATHON_EMAIL_TEMPLATE_PRESET_OPTIONS = [
key: "knight-hacks-viii",
label: "Knight Hacks VIII",
},
+ {
+ key: "knight-hacks-ix",
+ label: "Knight Hacks IX",
+ },
{
key: "bloomknights",
label: "BloomKnights",
@@ -44,6 +48,15 @@ export const HACKATHON_EMAIL_TEMPLATE_IDS = {
Confirmation: 10,
Waitlist: 11,
},
+ "knight-hacks-ix": {
+ Blacklist: 22,
+ Accepted: 22,
+ AcceptedReminder: 22,
+ Apply: 22,
+ Capacity: 22,
+ Confirmation: 22,
+ Waitlist: 22,
+ },
bloomknights: {
Blacklist: 14,
Accepted: 15,
diff --git a/packages/hackathon/src/client.tsx b/packages/hackathon/src/client.tsx
index d59f787a8..422acf8ca 100644
--- a/packages/hackathon/src/client.tsx
+++ b/packages/hackathon/src/client.tsx
@@ -314,11 +314,20 @@ export function useHackerProfileFlow() {
await queryClient.invalidateQueries({ queryKey: dashboardKey });
},
});
+ const reportIssueMutation = useMutation({
+ mutationFn: (description: string) =>
+ client.portal.reportIssue.mutate({
+ description,
+ hackathonName: config.hackathonName,
+ }),
+ });
return {
participant: dashboardQuery.data?.participant,
dashboardQuery,
profileSchema,
+ reportIssue: reportIssueMutation.mutateAsync,
+ reportIssueMutation,
updateProfile: updateMutation.mutateAsync,
updateMutation,
uploadResume: (fileName: string, fileContent: string) =>
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e154d2b0b..c57c0e042 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -141,7 +141,7 @@ importers:
version: 10.0.2(jiti@2.6.1)
eslint-config-next:
specifier: ^16.2.7
- version: 16.2.7(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
+ version: 16.2.7(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
postcss:
specifier: ^8.5.6
version: 8.5.6
@@ -255,7 +255,7 @@ importers:
version: 6.6.0
geist:
specifier: ^1.7.0
- version: 1.7.0(next@16.2.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
+ version: 1.7.0(next@16.2.7(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
google-auth-library:
specifier: ^10.6.1
version: 10.6.1
@@ -403,7 +403,7 @@ importers:
version: 12.34.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
geist:
specifier: ^1.7.0
- version: 1.7.0(next@16.2.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
+ version: 1.7.0(next@16.2.7(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
gsap:
specifier: ^3.14.2
version: 3.14.2
@@ -670,7 +670,7 @@ importers:
version: 12.34.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
geist:
specifier: ^1.7.0
- version: 1.7.0(next@16.2.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
+ version: 1.7.0(next@16.2.7(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
gsap:
specifier: ^3.14.2
version: 3.14.2
@@ -764,7 +764,7 @@ importers:
version: 12.34.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
geist:
specifier: ^1.7.0
- version: 1.7.0(next@16.2.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
+ version: 1.7.0(next@16.2.7(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
gsap:
specifier: ^3.14.2
version: 3.14.2
@@ -829,9 +829,45 @@ importers:
apps/khix:
dependencies:
+ '@forge/api':
+ specifier: workspace:*
+ version: link:../../packages/api
+ '@forge/auth':
+ specifier: workspace:*
+ version: link:../../packages/auth
+ '@forge/consts':
+ specifier: workspace:*
+ version: link:../../packages/consts
+ '@forge/db':
+ specifier: workspace:*
+ version: link:../../packages/db
+ '@forge/hackathon':
+ specifier: workspace:*
+ version: link:../../packages/hackathon
+ '@forge/ui':
+ specifier: workspace:*
+ version: link:../../packages/ui
+ '@forge/validators':
+ specifier: workspace:*
+ version: link:../../packages/validators
+ '@gsap/react':
+ specifier: ^2.1.2
+ version: 2.1.2(gsap@3.14.2)(react@19.2.4)
+ '@t3-oss/env-nextjs':
+ specifier: ^0.13.10
+ version: 0.13.10(typescript@5.9.3)(zod@4.3.6)
+ '@trpc/client':
+ specifier: 'catalog:'
+ version: 11.10.0(@trpc/server@11.10.0(typescript@5.9.3))(typescript@5.9.3)
+ '@trpc/server':
+ specifier: 'catalog:'
+ version: 11.10.0(typescript@5.9.3)
framer-motion:
specifier: ^12.34.3
version: 12.34.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ gsap:
+ specifier: ^3.14.2
+ version: 3.14.2
lucide-react:
specifier: ^0.575.0
version: 0.575.0(react@19.2.4)
@@ -847,6 +883,12 @@ importers:
react-icons:
specifier: ^5.5.0
version: 5.5.0(react@19.2.4)
+ superjson:
+ specifier: 2.2.6
+ version: 2.2.6
+ zod:
+ specifier: 'catalog:'
+ version: 4.3.6
devDependencies:
'@forge/eslint-config':
specifier: workspace:*
@@ -884,9 +926,6 @@ importers:
tailwindcss:
specifier: 'catalog:'
version: 4.2.1
- tw-animate-css:
- specifier: ^1.4.0
- version: 1.4.0
typescript:
specifier: 'catalog:'
version: 5.9.3
@@ -1505,7 +1544,7 @@ importers:
version: 16.1.6
eslint-plugin-import:
specifier: ^2.32.0
- version: 2.32.0(eslint@10.0.2(jiti@2.6.1))
+ version: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))
eslint-plugin-jsx-a11y:
specifier: ^6.10.2
version: 6.10.2(eslint@10.0.2(jiti@2.6.1))
@@ -13327,13 +13366,13 @@ snapshots:
escape-string-regexp@4.0.0: {}
- eslint-config-next@16.2.7(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3):
+ eslint-config-next@16.2.7(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3):
dependencies:
'@next/eslint-plugin-next': 16.2.7
eslint: 10.0.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@10.0.2(jiti@2.6.1))
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.2(jiti@2.6.1))
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1))
+ eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.2(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@10.0.2(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@10.0.2(jiti@2.6.1))
eslint-plugin-react-hooks: 7.0.1(eslint@10.0.2(jiti@2.6.1))
@@ -13355,7 +13394,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@10.0.2(jiti@2.6.1)):
+ eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@@ -13366,22 +13405,31 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.2(jiti@2.6.1))
+ eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.2(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.2(jiti@2.6.1)):
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.2(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)
eslint: 10.0.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@10.0.2(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.2(jiti@2.6.1)):
+ eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1)):
+ dependencies:
+ debug: 3.2.7
+ optionalDependencies:
+ eslint: 10.0.2(jiti@2.6.1)
+ eslint-import-resolver-node: 0.3.9
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1))
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -13392,7 +13440,7 @@ snapshots:
doctrine: 2.1.0
eslint: 10.0.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.2(jiti@2.6.1))
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.2(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -13410,7 +13458,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
- eslint-plugin-import@2.32.0(eslint@10.0.2(jiti@2.6.1)):
+ eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.2(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -13421,7 +13469,7 @@ snapshots:
doctrine: 2.1.0
eslint: 10.0.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.2(jiti@2.6.1))
+ eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -13730,7 +13778,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- geist@1.7.0(next@16.2.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)):
+ geist@1.7.0(next@16.2.7(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)):
dependencies:
next: 16.2.7(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)