diff --git a/src/app/api/register/route.ts b/src/app/api/register/route.ts index 8d89ae0..6d8f103 100644 --- a/src/app/api/register/route.ts +++ b/src/app/api/register/route.ts @@ -4,13 +4,7 @@ import { getSupabaseAdmin } from '@/lib/supabase'; import { getAuthenticatedUser } from '@/lib/auth-server'; import { paymentProofExists } from '@/lib/payment-proofs'; import { registrationsClosedResponse, registrationsOpen } from '@/lib/registration-gate'; -import { - isDuplicatePhoneError, - isIterSoaCollege, - isIterSoaEmail, - ITER_SOA_ERROR_MESSAGE, - PHONE_IN_USE_ERROR_MESSAGE, -} from '@/lib/validation'; +import { isDuplicatePhoneError, PHONE_IN_USE_ERROR_MESSAGE } from '@/lib/validation'; import { classifyStudentType, createRateLimiter, @@ -153,14 +147,6 @@ export async function POST(req: NextRequest) { const input = parsed.data; const college = studentType === 'internal' ? NIT_ROURKELA_COLLEGE : input.college; - // Exclusion rule: Students from ITER - SOA are not allowed to register - if (isIterSoaCollege(college) || isIterSoaEmail(verifiedEmail)) { - return NextResponse.json( - { error: ITER_SOA_ERROR_MESSAGE, field: 'college' }, - { status: 403 } - ); - } - const adminClient = getSupabaseAdmin(token); // 3. Prevent duplicate registrations for the same authenticated user diff --git a/src/app/globals.css b/src/app/globals.css index 4fdca86..249a222 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -128,20 +128,18 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o @media (max-width:719px){.wh-card{gap:12px !important;padding:10px 12px 10px 14px !important}.wh-glyph{display:none !important}.wh-main{font-size:14px !important}.wh-sub{font-size:12px !important}} /* ---------- sponsors ---------- */ -/* Tickers: each strip holds its logos twice and slides by one copy, forever; it pauses while hovered, pressed - or holding keyboard focus, and the edges fade so logos drift in and out of view. */ -@keyframes iv-sp{to{transform:translateX(-50%)}} -.sp-row{overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);padding:4px 0} -.sp-strip{display:flex;width:max-content;animation:iv-sp var(--sp-dur,30s) linear infinite} -.sp-strip-rev{animation-direction:reverse} -.sp-row:hover .sp-strip,.sp-row:active .sp-strip,.sp-row:focus-within .sp-strip{animation-play-state:paused} -.sp-card{transition:border-color .3s,background-color .3s} -.sp-card:hover{border-color:rgba(20,19,18,.5) !important;background:rgba(20,19,18,.06) !important} -.sp-logo{width:100%;height:100%;object-fit:contain;filter:grayscale(1);opacity:.78;transition:filter .4s,opacity .4s} -.sp-card:hover .sp-logo,.sp-card:focus-visible .sp-logo{filter:none;opacity:1} -/* Without motion the rows stand still and scroll by hand instead, so every logo stays reachable. */ -@media (prefers-reduced-motion:reduce){.sp-row{overflow-x:auto;scrollbar-width:none;-webkit-mask-image:none;mask-image:none}.sp-row::-webkit-scrollbar{display:none}.sp-strip{animation:none}} -@media (max-width:719px){.sp-title{grid-template-columns:minmax(0,1fr) !important}.sp-title-art{display:none}} +/* Past sponsors: a wall of logo cards, five across on wide screens and two on phones. A card lifts while hovered or + holding keyboard focus. */ +.ps-grid{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px} +.ps-card{position:relative;aspect-ratio:3 / 2;border:1px solid rgba(20,19,18,.14);transition:border-color .3s,transform .4s cubic-bezier(.25,1,.1,1),box-shadow .4s} +.ps-card:hover,.ps-card:focus-within{border-color:rgba(20,19,18,.5);transform:translateY(-3px);box-shadow:0 14px 26px -18px rgba(20,19,18,.5)} +.ps-in{position:absolute;inset:0;display:grid;place-items:center} +.ps-in:focus-visible{outline:2px solid #141312;outline-offset:3px} +.ps-logo{height:auto;max-height:64%;object-fit:contain} +@media (max-width:959px){.ps-grid{grid-template-columns:repeat(4,minmax(0,1fr))}} +@media (max-width:719px){.ps-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}} +@media (max-width:479px){.ps-grid{grid-template-columns:repeat(2,minmax(0,1fr))}} +@media (prefers-reduced-motion:reduce){.ps-card:hover,.ps-card:focus-within{transform:none}} /* ---------- home hero ---------- */ /* --d is the disc's diameter and --cy its centre. Rings, disc, orbiters, copy and astronaut all hang off the two, @@ -316,6 +314,8 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o .to-planet{left:-8vw;top:calc(43% - var(--s) * 1.14)} .to-rocket{right:5vw;top:calc(43% - var(--s) * .6 - min(9.5vh,85px));height:min(19vh,170px)} .to-tagline{top:74%} + /* The closing fade into the mission list stays under the tagline, so the ink text never lands on the dark. */ + .to-fade{height:17% !important} } /* ---------- worlds slider decoration (WorldsView.tsx) ---------- */ @@ -391,6 +391,53 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o /* Every other spark and gallery dust mote, and two of the five curtain cloud layers, sit out. */ [data-lowpower] [data-lp-skip]{display:none} +/* ---------- home gallery tunnel (HomeView.tsx) ---------- */ +.t-scrim{display:none} +/* Phones: photos are capped by height too (--ar is the photo's width/height; --tx/--ty its offset from the centre), so a tall one can't outgrow the room + between the heading and the counter; the tunnel's centre and vanishing point drop into that room; and dark fades sit + behind the heading and the counter, so a photo passing beneath never covers the text. */ +@media (max-width:719px){ + .t-space{perspective-origin:50% 56% !important} + .t-frame{width:min(54vw,calc(28vh * var(--ar))) !important;top:calc(56% + var(--ty) * .5) !important} + .t-scrim{display:block;position:absolute;left:0;right:0;pointer-events:none} + .t-scrim-top{top:0;height:calc(4vh + 290px);background:linear-gradient(180deg,#141312 0,rgba(20,19,18,.94) 62%,rgba(20,19,18,0))} + .t-scrim-bot{bottom:0;height:200px;background:linear-gradient(0deg,#141312 0,rgba(20,19,18,.9) 45%,rgba(20,19,18,0))} +} +/* Landscape phones: too short to stack the photos between the heading and the counter, so they fly through the open + right-hand side instead, sized off the height. */ +@media (max-height:499px) and (orientation:landscape){ + .t-space{perspective-origin:76% 42% !important} + .t-frame{width:calc(38vh * var(--ar)) !important;left:calc(76% + var(--tx) * .3) !important;top:calc(42% + var(--ty) * .3) !important} +} + +/* ---------- gallery (GalleryView.tsx) ---------- */ +/* Phones: a frame sized off the width alone came out about 90-125px wide in portrait, and ran off the bottom of a + landscape screen. These fill most of the screen once the photo in focus is scaled up by the perspective (1.25x), + with Innovision#galleryTick keeping the spiral tight. Landscape phones size off the height instead. */ +@media (max-width:719px){ + .g-fr-l{width:min(64vw,340px) !important;height:min(42.2vw,224px) !important} + .g-fr-p{width:min(48vw,240px) !important;height:min(64vw,320px) !important} +} +@media (max-height:499px) and (orientation:landscape){ + .g-fr-l{width:70vh !important;height:46vh !important} + .g-fr-p{width:42vh !important;height:56vh !important} +} + +/* ---------- touch devices ---------- */ +/* Phones and tablets: drop the effects that cost the most per frame while a page scrolls. Scene art's drop shadows go + (each is an extra blur pass over a big layer, every frame it moves): inline filters write theirs as + var(--no-ds, drop-shadow(...)), and opacity(1) is a no-op in its place. Low-power mode drops them everywhere. The film + grain, a full-screen layer blended over the whole page, goes too. The HUD bar and the world switcher, always on screen, + trade the frosted glass (a blur of whatever scrolls beneath, redone every frame) for a near-solid fill. */ +[data-lowpower]{--no-ds:opacity(1)} +@media (pointer:coarse){ + :root{--no-ds:opacity(1)} + body::after{display:none} + .hud-frost,.wn-pill{-webkit-backdrop-filter:none !important;backdrop-filter:none !important} + .hud-frost{background:rgba(236,232,223,.97) !important} + .wn-pill{background:rgba(236,232,223,.96) !important} +} + /* ---------- schedule ---------- */ /* --gut lines the rows up with the 1240px column while letting each carousel run to the screen edge. */ .sc-hero,.sc-body{--gut:max(clamp(16px,4vw,64px),(100% - 1240px) / 2)} diff --git a/src/components/coming-soon/ComingSoon.tsx b/src/components/coming-soon/ComingSoon.tsx index d29fdeb..d125296 100644 --- a/src/components/coming-soon/ComingSoon.tsx +++ b/src/components/coming-soon/ComingSoon.tsx @@ -5,13 +5,13 @@ import styles from './ComingSoon.module.css'; type ComingSoonProps = { title: ReactNode; - /** Second call to action, next to BACK TO INNOVISION. */ - secondary: { label: string; href: string }; + /** Optional second call to action, next to BACK TO INNOVISION. */ + secondary?: { label: string; href: string }; }; /** * Shared full-page teaser for flows that are built but not yet live (see src/config/launch.ts): a centred - * headline and two buttons. Used by RegistrationsOpeningSoon (/register) and MerchDroppingSoon (/merch). + * headline, a BACK TO INNOVISION button and an optional second one. Used by RegistrationsOpeningSoon (/register) and MerchDroppingSoon (/merch). */ export default function ComingSoon({ title, secondary }: ComingSoonProps) { return ( @@ -20,7 +20,7 @@ export default function ComingSoon({ title, secondary }: ComingSoonProps) {

{title}

BACK TO INNOVISION - {secondary.label} + {secondary && {secondary.label}}
); diff --git a/src/components/coming-soon/MerchDroppingSoon.tsx b/src/components/coming-soon/MerchDroppingSoon.tsx index 06e13e4..03cb792 100644 --- a/src/components/coming-soon/MerchDroppingSoon.tsx +++ b/src/components/coming-soon/MerchDroppingSoon.tsx @@ -3,9 +3,6 @@ import ComingSoon from './ComingSoon'; /** /merch while MERCH_STORE_OPEN is false (src/config/launch.ts). */ export default function MerchDroppingSoon() { return ( - Exclusive merch
dropping soon.} - secondary={{ label: 'VIEW THE SCHEDULE', href: '/#/schedule' }} - /> + Exclusive merch
dropping soon.} /> ); } diff --git a/src/components/innovision/AuthOverlay.tsx b/src/components/innovision/AuthOverlay.tsx index 002dec2..db289f7 100644 --- a/src/components/innovision/AuthOverlay.tsx +++ b/src/components/innovision/AuthOverlay.tsx @@ -5,12 +5,10 @@ import React, { useState } from 'react'; import type { V } from './types'; import { Logo } from './icons'; -import { isIterSoaCollege, isIterSoaEmail } from '@/lib/validation'; export default function AuthOverlay({ v }: { v: V }) { const [userCollege, setUserCollege] = useState(null); const collegeValue = userCollege !== null ? userCollege : (v.regVals?.college || ''); - const isBlockedCollege = !v.isInternal && (isIterSoaCollege(collegeValue) || isIterSoaEmail(v.regVals?.email)); return (
@@ -105,14 +103,7 @@ export default function AuthOverlay({ v }: { v: V }) { {v.err.gender ? ({v.err.gender}) : null}
diff --git a/src/components/innovision/DetailView.tsx b/src/components/innovision/DetailView.tsx index f6b06e0..256b483 100644 --- a/src/components/innovision/DetailView.tsx +++ b/src/components/innovision/DetailView.tsx @@ -157,7 +157,7 @@ function TouchdownFront() { {/* Low-gravity hop: the shadow shrinks while the astronaut is up and a dust ring puffs out on landing. */} - Astronaut making a low-gravity hop on a cratered ledge + Astronaut making a low-gravity hop on a cratered ledge @@ -191,7 +191,7 @@ function SpotlightBack() {
- +
@@ -237,7 +237,7 @@ export default function DetailView({ v }: { v: V }) { @@ -292,7 +292,7 @@ export default function DetailView({ v }: { v: V }) { {v.isTakeoff && (
- +
@@ -308,7 +308,7 @@ export default function DetailView({ v }: { v: V }) {
- Lander on the launch pad + Lander on the launch pad
@@ -325,8 +325,10 @@ export default function DetailView({ v }: { v: V }) { )} {/* ---- copy ---- */} + {/* The compact tagline is solid ink, not a blend mode: phone browsers can drop mix-blend-mode on text whose words + animate, which left white text on the light sky. A glow in the sky's tint keeps it legible over the art. */} {v.compact && ( -

+

{cw.words.map((wd, wdI) => ( {wd.t} ))} @@ -394,7 +396,7 @@ export default function DetailView({ v }: { v: V }) { )} -
+
@@ -545,7 +547,7 @@ export default function DetailView({ v }: { v: V }) { {m.no} - + diff --git a/src/components/innovision/GalleryView.tsx b/src/components/innovision/GalleryView.tsx index ee9c02b..44ecdf8 100644 --- a/src/components/innovision/GalleryView.tsx +++ b/src/components/innovision/GalleryView.tsx @@ -14,7 +14,7 @@ export default function GalleryView({ v }: { v: V }) { ))} {v.gallery.map((g, gI) => ( -
+
{g.imageUrl ? ( {g.title} diff --git a/src/components/innovision/HomeView.tsx b/src/components/innovision/HomeView.tsx index 0b60e53..cc5ba57 100644 --- a/src/components/innovision/HomeView.tsx +++ b/src/components/innovision/HomeView.tsx @@ -10,7 +10,7 @@ import ImageSlot from './ImageSlot'; import { LiveFooter } from './SiteFooter'; import type { V } from './types'; -/** Landing page: sticky hero, briefing, odyssey map, gallery tunnel, sponsors, merch teaser and closing call to action. */ +/** Landing page: sticky hero, briefing, odyssey map, gallery tunnel, sponsors and closing call to action. */ export default function HomeView({ v }: { v: V }) { return (
@@ -104,7 +104,7 @@ export default function HomeView({ v }: { v: V }) {
{/* Same box and tilt as the image, so the bob moves it exactly as before while the filter stays static. */}
- Astronaut drifting beside the celestial disc + Astronaut drifting beside the celestial disc
@@ -179,10 +179,10 @@ export default function HomeView({ v }: { v: V }) {
-
+
{v.tunnel.map((g, gI) => ( -
+
{g.imageUrl ? ( {g.ph} @@ -198,6 +198,9 @@ export default function HomeView({ v }: { v: V }) { ))}
+ {/* Phones only (globals.css): dark fades behind the heading and the counter, so a photo flying past never covers the text. */} + +

GALLERY

Into the archive

@@ -214,47 +217,23 @@ export default function HomeView({ v }: { v: V }) {
-
+
-

Sponsors & partners

+

Past sponsors

-

The brands fuelling Innovision 2026. Full line-up announced closer to launch.

+

{v.pastSponsors.length} brands have backed Innovision over the years. The 2026 line-up is announced closer to launch.

Want your brand in orbit?

- + BECOME A SPONSOR
-
- -
-
-

OFFICIAL MERCH

-

Wear the odyssey.

-

Limited-run tees, hoodies and keepsakes. Pre-order online, collect on campus during the fest.

- - VISIT THE STORE - -
-
- {v.merchTeaser.map((t, tI) => ( - -
- -
- {t.name} - {t.priceL} -
- ))} -
-
-
{/* The footer slides up over the closing section: the section is sticky (its top is set by Innovision#homeScroll so it pins once its bottom reaches the bottom of the screen) across the 40vh spacer below it, and the wrapper's negative margin lets the footer overlap that spacer. Sticky runs with the browser's own scrolling, so it stays smooth on diff --git a/src/components/innovision/Hud.tsx b/src/components/innovision/Hud.tsx index baad092..c113a21 100644 --- a/src/components/innovision/Hud.tsx +++ b/src/components/innovision/Hud.tsx @@ -24,7 +24,7 @@ export default function Hud({ v }: { v: V }) { return ( <>
- +
{v.isDetail && ( <> diff --git a/src/components/innovision/Innovision.tsx b/src/components/innovision/Innovision.tsx index 6f47c92..4ea4637 100644 --- a/src/components/innovision/Innovision.tsx +++ b/src/components/innovision/Innovision.tsx @@ -13,7 +13,7 @@ import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { ScrambleTextPlugin } from 'gsap/ScrambleTextPlugin'; import { A, WORLDS, PRELOAD_CRITICAL, PRELOAD_DEFERRED, HERO_SPARKS, LOADER_SPARKS, STATUS, SCRAMBLE, GAP, GALLERY, ARCHIVE, ikWidth, gFrame, - LINKS, NAVBAR_HIDDEN, TUNNEL, TUNNEL_C, PRODUCTS, BRIEF, SCHED, SCHED_DAYS, SCHED_BLOCKS, SPONSOR_TIERS, TITLE_SPONSOR, inr, type Product, type WorldKey, + LINKS, NAVBAR_HIDDEN, TUNNEL, TUNNEL_C, PRODUCTS, BRIEF, SCHED, SCHED_DAYS, SCHED_BLOCKS, PAST_SPONSORS, inr, type Product, type WorldKey, } from './data'; import HomeView from './HomeView'; import WorldsView from './WorldsView'; @@ -52,10 +52,12 @@ import { import { REGISTRATIONS_OPEN, REGISTRATIONS_SOON_PATH, MERCH_STORE_OPEN, MERCH_SOON_PATH } from '@/config/launch'; import { REGISTRATION_FEE, UPI_ID, UPI_PAY_LINK, UPI_QR_SRC } from '@/config/payment'; import { SKIP_INTRO_KEY } from '@/components/coming-soon/SkipIntroOnReturn'; -import { isIterSoaCollege, isIterSoaEmail, ITER_SOA_ERROR_MESSAGE, isValidGoogleDriveUrl, GENDER_OPTIONS as GENDERS } from '@/lib/validation'; +import { isValidGoogleDriveUrl, GENDER_OPTIONS as GENDERS } from '@/lib/validation'; import { httpErrorMessage, isTransientFailure } from '@/lib/api-errors'; gsap.registerPlugin(ScrollTrigger, ScrambleTextPlugin); +// A phone's address bar sliding in or out resizes the window; re-measuring every trigger then makes scrolling hitch. +ScrollTrigger.config({ ignoreMobileResize: true }); // Views and overlays the first screen never shows: each is its own chunk and mounts the first time it is // needed (Innovision#mount), hidden, under the curtain, loader or a closed overlay. Once mounted it stays. @@ -879,13 +881,17 @@ export class Innovision extends Component { if (this.state.view !== 'gallery' || !this.gEls.length) return; const dz = this.gTarget - this.gZ; this.gZ = Math.abs(dz) < .05 ? this.gTarget : this.gZ + dz * .07; - const W = innerWidth, H = innerHeight, RX = Math.min(W * .3, 520), RY = Math.min(H * .25, 250); + const W = innerWidth, H = innerHeight, narrow = this.state.narrow, short = H < 500 && W > H; + // Phones (narrow, or short in landscape) swing the spiral far less, so the photo in focus stays on screen at their + // larger frame size (globals.css); portrait ones also lift it clear of the title and counter at the bottom. + const tight = narrow || short; + const RX = tight ? W * .05 : Math.min(W * .3, 520), RY = tight ? H * .04 : Math.min(H * .25, 250), lift = narrow ? -H * .07 : short ? -H * .03 : 0; // Once the camera has settled the frame is identical to the last one, so skip the 58 style writes. const key = this.gZ + '|' + W + '|' + H; if (key === this.gDrawn) return; this.gDrawn = key; this.gEls.forEach((el, i) => { - const a = i * 2.4 + .7, x = Math.cos(a) * RX, y = Math.sin(a) * RY, z = -i * GAP + this.gZ; + const a = i * 2.4 + .7, x = Math.cos(a) * RX, y = Math.sin(a) * RY + lift, z = -i * GAP + this.gZ; const o = z < -6200 ? 0 : z < -4000 ? (z + 6200) / 2200 : z < 300 ? 1 : Math.max(0, 1 - (z - 300) / 500); el.style.transform = 'translate(-50%,-50%) translate3d(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px,' + z.toFixed(1) + 'px) rotateY(' + (-x / RX * 12).toFixed(2) + 'deg)'; el.style.opacity = o.toFixed(3); @@ -907,7 +913,8 @@ export class Innovision extends Component { this.gGlow!.style.transform = 'scale(' + (1 + Math.pow(p, 3) * 14).toFixed(3) + ')'; this.gGlow!.style.opacity = p.toFixed(3); this.gStars!.style.transform = 'scale(' + (1 + p * .25).toFixed(4) + ')'; - const end = p > .94; + // The end card shows once the camera has passed the last photo, so it never lands on top of it. + const end = this.gZ > (gLen - 1) * GAP + 450; this.gEnd!.style.opacity = end ? '1' : '0'; this.gEnd!.style.pointerEvents = end ? 'auto' : 'none'; const idx = Math.max(0, Math.min(gLen - 1, Math.round((this.gZ - 200) / GAP))); @@ -926,8 +933,9 @@ export class Innovision extends Component { } gStep(d: number) { const gLen = this.gEls?.length || (GALLERY.length + this.state.dbGallery.length); - const i = Math.max(0, Math.min(gLen - 1, Math.round((this.gTarget - 200) / GAP) + d)); - this.gTarget = i * GAP + 200; + // One step past the last photo flies by it to the end card. + const i = Math.max(0, Math.min(gLen, Math.round((this.gTarget - 200) / GAP) + d)); + this.gTarget = Math.min(this.getGMax(), i * GAP + 200); } gWheel = (e: WheelEvent) => { this.gTarget = Math.max(0, Math.min(this.getGMax(), this.gTarget + (e.deltaY + e.deltaX) * 1.6)); @@ -996,9 +1004,11 @@ export class Innovision extends Component { // Only write what changed: replacing the counter's text every scroll update invalidates layout. const upd = (p: number) => { const travel = p * (N * TGAP - 200); + // Phones fade a photo out sooner as it passes the camera, before it grows past the room between the heading and the counter. + const near = innerWidth < 720 || (innerHeight < 500 && innerWidth > innerHeight) ? 120 : 250; fr.forEach((f, i) => { const z = -(i + 1) * TGAP + travel + 300; - const o = String(z > 250 ? Math.max(0, 1 - (z - 250) / 450) : z < -TGAP * 3 ? Math.max(0, 1 - (-TGAP * 3 - z) / TGAP) : 1), pe = +o > .6 ? 'auto' : 'none'; + const o = String(z > near ? Math.max(0, 1 - (z - near) / 450) : z < -TGAP * 3 ? Math.max(0, 1 - (-TGAP * 3 - z) / TGAP) : 1), pe = +o > .6 ? 'auto' : 'none'; f.style.transform = 'translate(-50%,-50%) translate3d(0,0,' + z + 'px)'; if (f.style.opacity !== o) f.style.opacity = o; if (f.style.pointerEvents !== pe) f.style.pointerEvents = pe; @@ -1131,7 +1141,9 @@ export class Innovision extends Component { tl.to(root.querySelector('[data-d-titleblock]'), { scale: 1.25, autoAlpha: 0, y: () => -H() * .08, duration: .3 }, 0); // One tween per word (equivalent to stagger: .03): with GSAP 3.13+, a staggered fromTo inside a // scrubbed timeline that is invalidated on refresh reverts not-yet-started targets to visible. - q('[data-d-word]').forEach((el, k) => tl.fromTo(el, { autoAlpha: 0, y: 40, filter: 'blur(10px)' }, { autoAlpha: 1, y: 0, filter: 'blur(0px)', duration: .12 }, .24 + k * .03)); + // Touch screens skip the blur: a filter redrawn on every scroll frame is costly on a phone. + const blur = !matchMedia('(pointer: coarse)').matches; + q('[data-d-word]').forEach((el, k) => tl.fromTo(el, { autoAlpha: 0, y: 40, ...(blur && { filter: 'blur(10px)' }) }, { autoAlpha: 1, y: 0, ...(blur && { filter: 'blur(0px)' }), duration: .12 }, .24 + k * .03)); const intro = root.querySelector('[data-d-intro]'), spec = root.querySelector('[data-d-spec]'); if (intro) tl.fromTo(intro, { autoAlpha: 0, x: -60 }, { autoAlpha: 1, x: 0, duration: .18 }, .5); if (spec) tl.fromTo(spec, { autoAlpha: 0, x: 60 }, { autoAlpha: 1, x: 0, duration: .18 }, .56); @@ -2298,12 +2310,6 @@ export class Innovision extends Component { return; } - if (!isInternal && isIterSoaEmail(user.email)) { - this.toast(ITER_SOA_ERROR_MESSAGE); - this.fail({ college: ITER_SOA_ERROR_MESSAGE }); - return; - } - if (s.step === 0) { const name = v('name').replace(/\s+/g, ' '); const college = isInternal ? 'National Institute of Technology, Rourkela' : v('college').replace(/\s+/g, ' '); @@ -2319,8 +2325,6 @@ export class Innovision extends Component { err.college = 'Which college are you from?'; } else if (!isInternal && college.length > 200) { err.college = 'Keep the college name under 200 characters.'; - } else if (!isInternal && (isIterSoaCollege(college) || isIterSoaEmail(email))) { - err.college = ITER_SOA_ERROR_MESSAGE; } if (!/^[6-9]\d{9}$/.test(phone)) err.phone = 'Use a 10-digit Indian mobile number.'; if (isInternal && !enrollment_no) err.enrollment_no = 'Enter your NIT Rourkela Roll / Enrollment number.'; @@ -2363,12 +2367,6 @@ export class Innovision extends Component { status: 'pending' as const, }; - if (!isInternal && (isIterSoaCollege(r.college) || isIterSoaEmail(user.email))) { - this.toast(ITER_SOA_ERROR_MESSAGE); - this.setState({ err: { college: ITER_SOA_ERROR_MESSAGE }, step: 0, busyLbl: '' }); - return; - } - const saveRes = await createRegistration(regPayload); const savedReg = saveRes.registration || (saveRes.success ? regPayload : null); if (!savedReg) { @@ -2543,6 +2541,9 @@ export class Innovision extends Component { // Handlers below read this.state when they run: memoised views may hold an older v. const s = this.state, i = s.index, w = WORLDS[i], dw = WORLDS[s.dIndex], nx = WORLDS[(s.dIndex + 1) % WORLDS.length]; const navOn = s.view === 'worlds' || s.view === 'detail'; + // On a world's page the world switcher shows only at the top, where you land after Enter; once the page scrolls + // (hudSolid) it gives the scene and the mission list the whole screen. + const navShown = navOn && !(s.view === 'detail' && s.hudSolid); const act = s.view === 'merch' ? 'merch' : s.view === 'gallery' ? 'gallery' : s.view === 'schedule' ? 'schedule' : navOn ? 'events' : 'home'; // Store closed (src/config/launch.ts): MERCH links leave for the "dropping soon" page. const merchHref = MERCH_STORE_OPEN ? '#/merch' : MERCH_SOON_PATH; @@ -2581,22 +2582,14 @@ export class Innovision extends Component { x, y, ar: photo.width + ' / ' + photo.height, + arN: (photo.width / photo.height).toFixed(3), c: TUNNEL_C[k % TUNNEL_C.length], imageUrl: ikWidth(photo.url, 960), }; }), tunnelTotal: String(TUNNEL.length).padStart(2, '0'), - titleSponsor: TITLE_SPONSOR, - sponsorTiers: SPONSOR_TIERS.map((t) => ({ - key: t.key, title: t.title, lg: t.size === 'lg', countL: t.items.length + (t.items.length === 1 ? ' partner' : ' partners'), - cards: t.items.map((it, k) => ({ ...it, slot: 'sponsor-' + t.key + '-' + (k + 1), ph: t.key === 'main' ? 'Sponsor logo' : t.key === 'media' ? 'Media partner logo' : 'Food partner logo' })), - })).map((t) => { - // The ticker repeats a short tier until one pass is wider than any screen, so the loop never shows a gap; - // its duration grows with the pass so every row drifts at the same gentle speed. - const loop = t.cards.length ? Array.from({ length: Math.max(8, t.cards.length) }, (_, k) => t.cards[k % t.cards.length]) : []; - return { ...t, loop, dur: (loop.length * (t.lg ? 3.6 : 3)).toFixed(1) + 's' }; - }), - merchTeaser: PRODUCTS.slice(0, 3).map((p) => ({ slot: 'merch-' + p.id, ph: p.name, name: p.name, priceL: '₹' + p.price })), + // Logos are sized by the square root of their aspect ratio, so wide wordmarks and square marks carry similar weight. + pastSponsors: PAST_SPONSORS.map((s) => ({ ...s, src: ikWidth(s.logo, 400), w: Math.min(78, 36 * Math.sqrt(s.width / s.height)).toFixed(1) + '%' })), products: PRODUCTS.map((p) => { const sl = s.sel[p.id] || {}, size = sl.size || (p.sizes ? 'M' : null), color = sl.color || 0; return { @@ -2622,7 +2615,6 @@ export class Innovision extends Component { if (!email || !email.value) return; f.reset(); this.toast("You're on the mission list."); }, - sponsorCta: (e: MouseEvent) => { e.preventDefault(); this.toast('Partnership deck drops soon. Reach us on Instagram.'); }, toastMsg: s.toastMsg, heroChars: 'INNOVISION'.split('').map((ch) => ({ ch })), briefWords: BRIEF.split(' '), @@ -2715,7 +2707,7 @@ export class Innovision extends Component { navX: (i * 100) + '%', navW: (100 / WORLDS.length) + '%', navGlow: '#141312', - navO: navOn ? 1 : 0, navY: navOn ? '0px' : '30px', navPE: (navOn ? 'auto' : 'none') as 'auto' | 'none', + navO: navShown ? 1 : 0, navY: navShown ? '0px' : '30px', navPE: (navShown ? 'auto' : 'none') as 'auto' | 'none', navBottom: s.narrow ? 'calc(clamp(16px,2.6vw,44px) + 40px)' : 'clamp(16px,2.6vw,44px)', hudSolid: s.hudSolid, hintOn: s.hint && s.view === 'worlds' && !s.auth && !s.menu, diff --git a/src/components/innovision/SignalLink.tsx b/src/components/innovision/SignalLink.tsx index 6f5da59..641cca0 100644 --- a/src/components/innovision/SignalLink.tsx +++ b/src/components/innovision/SignalLink.tsx @@ -51,12 +51,12 @@ function Scene({ fit, accent, id, className }: { fit: Fit; accent: string; id: s return (
- +
{/* The satellite turns about its horn tip, so the waves always leave from the tip as it sways. */}
- +
diff --git a/src/components/innovision/SiteFooter.tsx b/src/components/innovision/SiteFooter.tsx index 74bb3c7..75df9ce 100644 --- a/src/components/innovision/SiteFooter.tsx +++ b/src/components/innovision/SiteFooter.tsx @@ -69,7 +69,7 @@ export default function SiteFooter({ v }: { v: V }) {
© 2026 Innovision · NIT Rourkela, Odisha - Crafted with ♥ by OpenCode NIT Rourkela + Crafted with ♥ by OpenCode NIT Rourkela BACK TO ORBIT ↑ diff --git a/src/components/innovision/Sponsors.tsx b/src/components/innovision/Sponsors.tsx index 820a145..53efb65 100644 --- a/src/components/innovision/Sponsors.tsx +++ b/src/components/innovision/Sponsors.tsx @@ -1,60 +1,23 @@ /* eslint-disable @next/next/no-img-element -- logos keep their own proportions inside fixed cards */ -import { Sparkle } from './icons'; -import ImageSlot from './ImageSlot'; -import { A } from './data'; import type { V } from './types'; -const CUT = (n: number) => `polygon(${n}px 0,100% 0,100% calc(100% - ${n}px),calc(100% - ${n}px) 100%,0 100%,0 ${n}px)`; -const SLOT_FILL = { width: "100%", height: "100%", aspectRatio: "auto" }; - /** - * The sponsors line-up: the title sponsor in a featured card, then one endlessly scrolling logo ticker - * per tier, alternating direction; a ticker pauses while hovered, pressed or holding keyboard focus - * (globals.css, .sp-row). Tiers and logos come from SPONSOR_TIERS in data.ts. + * Past sponsors: a wall of logo cards, one per brand, in brochure order (PAST_SPONSORS in data.ts). Each card is + * filled with its logo's own background colour; cards for brands with a known site open it in a new tab. */ export default function Sponsors({ v }: { v: V }) { - const t = v.titleSponsor; return ( - <> -
- - -
- TITLE SPONSOR -

Innovision 2026, presented by

- - {t.logo ? {t.name : -
- -
- - {v.sponsorTiers.map((tier, ti) => ( -
-
-

{tier.title}

- {tier.countL} -
- {/* Endless ticker: the strip holds the logos twice and slides by one copy, so it loops without a seam. */} -
-
- {[0, 1].map((copy) => tier.loop.map((c, k) => { - const style = { flex: "none", display: "grid", placeItems: "center", width: tier.lg ? "clamp(210px, 22vw, 290px)" : "clamp(160px, 16vw, 220px)", aspectRatio: "16 / 10", marginRight: "14px", padding: c.logo ? "13%" : "0", border: "1px solid rgba(20,19,18,.14)", background: "rgba(20,19,18,.03)", color: "#141312", textDecoration: "none" } as const; - // The second copy and any repeats are there only to keep the loop full: hidden from screen readers and Tab. - const extra = copy === 1 || k >= tier.cards.length; - const inner = c.logo ? {extra : ; - return c.url - ? {inner} - :
{inner}
; - }))} -
-
-
- ))} - +
    + {v.pastSponsors.map((s) => { + const logo = {s.name}; + return ( +
  • + {s.url + ? {logo} + : {logo}} +
  • + ); + })} +
); } diff --git a/src/components/innovision/WorldNav.tsx b/src/components/innovision/WorldNav.tsx index e36cb2d..69001d9 100644 --- a/src/components/innovision/WorldNav.tsx +++ b/src/components/innovision/WorldNav.tsx @@ -4,7 +4,7 @@ import type { V } from './types'; export default function WorldNav({ v }: { v: V }) { return (