Merge pull request #97 from mcgill-ecsess/antoine/preview-seo-changes-and-query-optimization

Weekend overhaul: SEO changes, query optimization, landing page, animations
This commit is contained in:
Antoine Phan
2026-01-26 23:51:05 -05:00
committed by GitHub
15 changed files with 258 additions and 207 deletions

View File

@@ -6,21 +6,29 @@
/* Light shades - for backgrounds and cards */
--color-ecsess-50: #e8ffd9;
--color-ecsess-100: #cce7ba;
--color-ecsess-150: #bae9a5;
--color-ecsess-200: #a9d0a0;
/* Mid-light shades - for borders and hover states */
--color-ecsess-250: #9cc295;
--color-ecsess-300: #8fb98a;
--color-ecsess-350: #7daa7a;
--color-ecsess-400: #6a9a6a;
/* Mid shades - for accents and interactive elements */
--color-ecsess-450: #62925a;
--color-ecsess-500: #5a8b5a;
--color-ecsess-550: #4c7a4f;
--color-ecsess-600: #3f6a3f;
--color-ecsess-650: #306032;
/* Mid-dark shades - for text on light backgrounds */
--color-ecsess-700: #2d5a2d;
--color-ecsess-700: #2f4d29;
--color-ecsess-750: #1c4a1e;
--color-ecsess-800: #0a3d2a;
/* Dark shades - for text and backgrounds */
--color-ecsess-850: #083525;
--color-ecsess-900: #062c20;
--color-ecsess-950: #031c15;
@@ -28,14 +36,14 @@
--color-ecsess-black: #1f1f1f;
--color-ecsess-black-hover: #161917;
--animate-wiggle: wiggle 0.3s ease-in-out infinite;
--animate-wiggle: wiggle 0.5s ease-in-out 1;
@keyframes wiggle {
0%,
100% {
transform: rotateY(-3deg);
transform: rotateY(-2.4deg);
}
50% {
transform: rotateY(3deg);
transform: rotateY(2.4deg);
}
}
}

View File

@@ -1,11 +1,12 @@
<script lang="ts">
let { href, external = false, children } = $props();
let { href, external = false, children, class: className = '' } = $props();
</script>
<a
{href}
target={external ? '_blank' : undefined}
rel={external ? 'noopener noreferrer' : undefined}
class={className}
>
{@render children()}
</a>

View File

@@ -4,6 +4,6 @@
let { value } = $props();
</script>
<div class="flex flex-col justify-center-safe">
<div class="typography flex flex-col justify-center-safe">
<PortableText {value} />
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { Globe, Instagram, Wrench, Zap, CodeXml, Podcast } from '@lucide/svelte';
import { Globe, Instagram, Wrench, Users, CodeXml, Cpu } from '@lucide/svelte';
// All icons from @lucide/svelte share the same component type; reuse one for typing
type IconComponent = typeof Wrench;
@@ -17,16 +17,11 @@
{
name: 'Code.Jam()',
description:
"McGill Engineering's largest annual hackathon. A 48-hour programming competition where students create innovative solutions to real-world problems.",
"McGill Engineering's largest annual hackathon, a 36-hour programming competition where students create innovative projects!",
website: 'https://codejam.mcgilleus.ca/',
instagram: 'https://www.instagram.com/mcgillcodejam/',
icon: CodeXml,
features: [
'Biggest Hackathon in Engineering',
'Great prizes',
'Cool swags and merch',
'Networking opportunities'
]
features: ['Biggest Hackathon in Engineering', 'Great prizes', 'Networking opportunities']
},
{
name: 'The Factory',
@@ -35,7 +30,16 @@
website: 'https://factory.mcgilleus.ca/',
instagram: 'https://www.instagram.com/thefactory_mcgill/',
icon: Wrench,
features: ['Student-run Lab Space', '3D Printing', 'Equipment Rental', 'Hardware Workshops']
features: ['Student-run Lab Space', '3D Printing', 'Hardware Workshops']
},
{
name: 'ECSESSBits',
description:
'First Year Council of the McGill Electrical, Computer, Software Engineering Student Society.',
website: '',
instagram: 'https://www.instagram.com/ecsessbits/',
icon: Users,
features: ['First Year Council', 'Fun Events', 'Study Sessions']
},
{
name: 'IEEE McGill',
@@ -43,49 +47,40 @@
'One of the largest IEEE student branches in Eastern Canada, offering professional development, networking, and industry connections.',
website: 'https://ieee.mcgilleus.ca/',
instagram: 'https://www.instagram.com/ieeemcgill/',
icon: Zap,
features: [
'Technical Talks',
'Arduino Workshops',
'IEEEXtreme Competition',
'Networking Events'
]
icon: Cpu,
features: ['Technical Talks', 'Arduino Workshops', 'Networking Events']
}
];
</script>
<div class="container mx-auto px-4">
<!-- Section Header -->
<div class="mb-12 text-center">
<h2 id="affiliated-clubs-title" class="text-ecsess-100 mb-4 text-4xl font-bold md:text-5xl">
<div class="my-12 text-center">
<h2 id="affiliated-clubs-title" class="text-ecsess-100 mb-2 text-4xl font-bold md:text-5xl">
Subcommittees & Affiliated Groups
</h2>
<p class="text-ecsess-200 mx-auto max-w-2xl text-lg">
<p class="text-ecsess-200 mx-auto max-w-2xl text-base">
Explore opportunities to enhance your skills, build innovative projects, and connect with the
engineering community through our subcommittees and affiliated groups.
</p>
</div>
<!-- Clubs Grid -->
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
<!-- Clubs Grid: 2x2 on large screens -->
<div class="grid grid-cols-1 gap-8 md:grid-cols-2">
{#each groups as group, i (group.name)}
{@const Icon = group.icon}
<article
class="group bg-ecsess-950 shadow-ecsess-800 relative flex flex-col overflow-hidden rounded-2xl shadow-xl transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
class="bg-ecsess-950 border-ecsess-800 flex flex-col overflow-hidden rounded-lg border text-left"
aria-labelledby={`group-${i}-title`}
>
<!-- Decorative gradient bar -->
<div class="from-ecsess-400 via-ecsess-500 to-ecsess-600 h-2 bg-gradient-to-r"></div>
<div class="flex flex-1 flex-col p-6">
<!-- Icon and Name -->
<div class="mb-4 flex items-center justify-between gap-4">
<div class="flex items-center gap-4">
<div class="flex flex-1 flex-col p-7 md:p-8">
<!-- Header: icon + name -->
<header class="mb-5 flex items-center justify-start gap-4">
<div
class="group-hover:bg-ecsess-500 bg-ecsess-800 flex h-14 w-14 items-center justify-center rounded-xl shadow-md transition-all duration-300 group-hover:scale-110"
class="bg-ecsess-800 flex h-14 w-14 shrink-0 items-center justify-center rounded-xl"
>
<Icon
class="text-ecsess-300 h-7 w-7 transition-colors group-hover:text-white"
class="text-ecsess-300 size-7"
strokeWidth={2.5}
aria-hidden="true"
focusable="false"
@@ -94,63 +89,53 @@
<h3 id={`group-${i}-title`} class="text-ecsess-50 text-2xl font-bold">
{group.name}
</h3>
</div>
<!-- Action Buttons -->
<div class="flex gap-2">
<a
href={group.website}
target="_blank"
rel="noopener noreferrer external"
aria-label={`Visit ${group.name} website`}
class="bg-ecsess-800 hover:bg-ecsess-500 text-ecsess-100 hover:text-ecsess-50 flex h-10 w-10 items-center justify-center rounded-lg shadow-md transition-all hover:shadow-lg active:scale-95"
>
<Globe class="h-5 w-5" strokeWidth={2.5} aria-hidden="true" focusable="false" />
</a>
</header>
<!-- Description -->
<p class="text-ecsess-200 mb-5 text-base leading-relaxed md:text-lg">
{group.description}
</p>
<!-- Features -->
<ul class="mb-5 list-none space-y-2 ps-0 text-base md:text-lg" role="list">
{#each group.features as feature (feature)}
<li class="flex items-center gap-2">
<span class="bg-ecsess-500 h-1.5 w-1.5 shrink-0 rounded-full" aria-hidden="true"
></span>
<span class="text-ecsess-100 font-medium">{feature}</span>
</li>
{/each}
</ul>
<!-- Links -->
<div class="border-ecsess-800 mt-auto flex flex-wrap items-center gap-3 border-t pt-5">
{#if group.instagram}
<a
href={group.instagram}
target="_blank"
rel="noopener noreferrer external"
aria-label={`Follow ${group.name} on Instagram`}
class="bg-ecsess-800 hover:bg-ecsess-500 text-ecsess-100 hover:text-ecsess-50 flex h-10 w-10 items-center justify-center rounded-lg shadow-md transition-all hover:shadow-lg active:scale-95"
class="text-ecsess-300 hover:text-ecsess-100 border-ecsess-700 bg-ecsess-900/50 hover:bg-ecsess-800/80 inline-flex items-center gap-2 rounded-md border px-4 py-2 text-base"
>
<Instagram
class="h-5 w-5"
strokeWidth={2.5}
aria-hidden="true"
focusable="false"
/>
<Instagram class="size-5" strokeWidth={2.5} aria-hidden="true" focusable="false" />
<span>Instagram</span>
</a>
{/if}
{#if group.website}
<a
href={group.website}
target="_blank"
rel="noopener noreferrer external"
aria-label={`Visit ${group.name} website`}
class="text-ecsess-300 hover:text-ecsess-100 border-ecsess-700 bg-ecsess-900/50 hover:bg-ecsess-800/80 inline-flex items-center gap-2 rounded-md border px-4 py-2 text-base"
>
<Globe class="size-5" strokeWidth={2.5} aria-hidden="true" focusable="false" />
<span>Website</span>
</a>
{/if}
</div>
</div>
<!-- Description -->
<p class="text-ecsess-100 mb-6 flex-1 text-base leading-relaxed">
{group.description}
</p>
<!-- Features -->
<ul class="mb-6 space-y-2" role="list">
{#each group.features as feature (feature)}
<li class="flex items-center gap-2 pl-3">
<div class="bg-ecsess-500 h-1.5 w-1.5 rounded-full" aria-hidden="true"></div>
<span class="text-ecsess-100 text-base font-semibold">
{feature}
</span>
</li>
{/each}
</ul>
</div>
</article>
{/each}
</div>
<!-- Bottom CTA -->
<div class="mt-12 text-center">
<p class="text-ecsess-300 text-sm">
Want to get involved? Visit their websites and social media pages to learn about upcoming
events and how to join!
</p>
</div>
</div>

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import type { Sponsors } from '$lib/schemas';
import Link from 'components/Link.svelte';
import Button from 'components/Button.svelte';
let { sponsors } = $props<{ sponsors: Sponsors[] }>();
</script>
<div class="container mx-auto px-4">
<!-- Section Header -->
<div class="my-12 text-center">
<h2 id="sponsors-title" class="text-ecsess-100 mb-2 text-4xl font-bold md:text-5xl">
Our Sponsors
</h2>
<p class="text-ecsess-200/90 mx-auto max-w-2xl text-base leading-relaxed md:text-lg">
We're grateful to our sponsors for their continued support of ECSESS, our events, activities,
and our community.
</p>
<div
class="via-ecsess-150/40 mx-auto mt-6 h-px w-32 bg-linear-to-r from-transparent to-transparent"
aria-hidden="true"
></div>
<div class="mt-6">
<Link href="/sponsor">
<Button>Become a Sponsor</Button>
</Link>
</div>
</div>
<!-- Sponsors -->
{#if sponsors && sponsors.length > 0}
<div class="mx-auto max-w-6xl">
<div class="flex flex-wrap justify-center gap-4 sm:gap-6" aria-labelledby="sponsors-title">
{#each sponsors as sponsor}
<Link
href={sponsor.url}
external
class="group flex w-full max-w-[342px] justify-center rounded-xl focus-visible:outline-none"
>
<div
class="border-ecsess-150/15 bg-ecsess-950 group-hover:border-ecsess-150/30 group-hover:bg-ecsess-900 flex h-28 w-full items-center justify-center overflow-hidden rounded-xl border p-6 shadow-md transition-colors duration-150"
>
<img
src={sponsor.logo}
alt={sponsor.name}
class="max-h-16 w-full object-contain opacity-90 transition-opacity duration-150 group-hover:opacity-100"
/>
</div>
</Link>
{/each}
</div>
</div>
{:else}
<div class="text-ecsess-300 py-12 text-center">
<p>You can be our next sponsor!</p>
</div>
{/if}
</div>

View File

@@ -0,0 +1,10 @@
<script>
let { thumbnail } = $props();
</script>
<svelte:head>
{#if thumbnail}
<meta property="og:image" content={thumbnail} />
<meta property="twitter:image" content={thumbnail} />
{/if}
</svelte:head>

View File

@@ -4,9 +4,6 @@
description = 'Meet the student council, get access to academic and technical resources, registration for events, and much more!',
canonical = 'https://ecsess.mcgilleus.ca'
} = $props();
let thumbnail =
'https://cdn.sanity.io/images/vmtsvpe2/production/5d68504038cc692805dc5e51af83adedfefde442-5304x3443.jpg?h=628&fm=webp';
</script>
<svelte:head>
@@ -22,12 +19,10 @@
<meta property="og:url" content={canonical} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={thumbnail} />
<!-- X (Twitter) -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={canonical} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={thumbnail} />
</svelte:head>

View File

@@ -9,14 +9,14 @@
</script>
<div
class="bg-ecsess-100 text-ecsess-900 hover:bg-ecsess-200 border-ecsess-300 grid h-full place-content-center rounded-md border text-center shadow-md transition-all hover:shadow-lg"
class="bg-ecsess-100 text-ecsess-900 hover:bg-ecsess-200 grid h-full place-content-center rounded-md text-center shadow-md transition-all hover:shadow-lg"
>
<p class="text-base font-extrabold lg:text-lg">
<p class="text-base leading-tight font-semibold">
{officeHour.member.name.split(' ')[0]}
</p>
{#if !isShortBlock}
<p class="text-ecsess-700 text-xs italic">
<p class="text-ecsess-700 mt-0.5 text-[11px] leading-tight opacity-90">
{shortenPosition(officeHour.member.position)}
</p>
{/if}

View File

@@ -114,10 +114,14 @@
</script>
<div class="overflow-x-auto">
<div class="mx-auto max-w-7xl min-w-[800px]">
<div class="border-ecsess-500 bg-ecsess-900 mx-auto max-w-7xl min-w-[800px] border-t pt-2">
<!-- Header row -->
<div class="mb-2 grid gap-0" style:grid-template-columns="80px repeat(5, 1fr)">
<div class="text-ecsess-50 px-2 text-center text-base font-semibold">Time</div>
<div
class="text-ecsess-50 bg-ecsess-900 sticky left-0 z-20 px-2 text-center text-base font-semibold"
>
Time
</div>
{#each DAYS as day}
<div class="text-ecsess-50 px-2 text-center text-base font-semibold md:text-lg">
{day}
@@ -135,7 +139,7 @@
<!-- Time column (only for first day) -->
{#if dayIndex === 0}
<div class="border-ecsess-500 relative border-b-2">
<div class="border-ecsess-500 bg-ecsess-900 sticky left-0 z-20 border-b-2">
{#each timeSlots as timeSlot}
{@const isHourMark = timeSlot % 60 === 0}
<div

View File

@@ -36,15 +36,6 @@ export type FAQ = {
answer: string;
};
export type HomepageCMSResponse = {
description: InputValue;
councilPhoto: string;
faqs: {
question: string;
answer: string;
}[];
};
export type OfficeHour = {
day: string;
startTime: string;

View File

@@ -18,8 +18,8 @@
</Link>
<p class="text-ecsess-200 text-lg leading-relaxed md:text-xl">
The page you are looking for is not implemented because we are too lazy to do it. But if you
really want to see it, you can reboot to the homepage and try again.
The page you are looking for is not implemented because we are too lazy to do it. <br />
But if you really want to see it, you can reboot to the homepage and try again.
<br />
<br />
Or even better, you can join us and help us implement it.

View File

@@ -0,0 +1,14 @@
import { getFromCMS } from '$lib/utils.js';
const thumbnailQuery = `*[_type == "homepage"]{
"thumbnail": councilPhoto.asset->url+"?h=800&fm=webp",
}[0]`;
export const load = async () => {
try {
return { thumbnail: (await getFromCMS(thumbnailQuery)).thumbnail };
} catch (err) {
console.error('Failed to fetch homepage thumbnail from CMS:', err);
return { thumbnail: null };
}
};

View File

@@ -4,9 +4,9 @@
import { onMount } from 'svelte';
import Navbar from 'components/layout/NavBar.svelte';
import Footer from 'components/layout/Footer.svelte';
import PageThumbnail from 'components/layout/PageThumbnail.svelte';
let { children } = $props();
let { children, data } = $props();
// Lazy load analytics only in production for faster dev startup
onMount(async () => {
if (!dev) {
@@ -17,5 +17,6 @@
</script>
<Navbar />
<PageThumbnail thumbnail={data.thumbnail} />
{@render children()}
<Footer />

View File

@@ -1,13 +1,12 @@
import { getFromCMS } from '$lib/utils.js';
import type { HomepageCMSResponse, OfficeHour, Sponsors } from '$lib/schemas';
import type { OfficeHour, Sponsors } from '$lib/schemas';
const homepageQuery = `*[_type == "homepage"]{
"description": description[],
const homepageQuery = `{
"homepage": *[_type == "homepage"]{
"councilPhoto": councilPhoto.asset->url+"?h=1200&fm=webp",
"faqs": faqs[]{ question, answer },
}[0]`;
const ohQuery = `*[_type=="officeHours"]{
}[0],
"officeHours": *[_type=="officeHours"]{
day,
startTime,
endTime,
@@ -15,27 +14,26 @@ const ohQuery = `*[_type=="officeHours"]{
"name": member->name,
"position": member->position
}
}`;
const sponsorQuery = `*[_type=="sponsors"]{
},
"sponsors": *[_type=="sponsors"]{
name,
url,
"logo": logo.asset->url+"?h=100&fm=webp"
}
}`;
export const load = async ({ url }) => {
/**
* @description Response data type based on the `homepageQuery` above.
* @description Response data type based on the combined query above.
* Note that `description` is a rich/portable text type
*/
let homepageResp: HomepageCMSResponse = await getFromCMS(homepageQuery);
let officeHourResp: OfficeHour[] = await getFromCMS(ohQuery);
let sponsorsResp: Sponsors[] = await getFromCMS(sponsorQuery);
let homePageResp = await getFromCMS(homepageQuery);
let councilPhotoUrl: string = homePageResp.homepage.councilPhoto;
let officeHourResp: OfficeHour[] = homePageResp.officeHours;
let sponsorsResp: Sponsors[] = homePageResp.sponsors;
return {
description: homepageResp.description,
councilPhoto: homepageResp.councilPhoto,
faqs: homepageResp.faqs,
councilPhoto: councilPhotoUrl,
allOHs: officeHourResp,
sponsors: sponsorsResp,
canonical: url.href

View File

@@ -1,13 +1,11 @@
<script>
import FaqAccordion from 'components/homepage/FAQAccordion.svelte';
import Section from 'components/layout/Section.svelte';
import RichText from 'components/RichText.svelte';
import OhSchedule from 'components/officehour/OHSchedule.svelte';
import Link from 'components/Link.svelte';
import SeoMetaTags from 'components/layout/SeoMetaTags.svelte';
import AffiliatedGroups from 'components/homepage/AffiliatedGroups.svelte';
import { fade } from 'svelte/transition';
import Sponsors from 'components/homepage/Sponsors.svelte';
import QuickLinks from 'components/QuickLinks.svelte';
import { fade } from 'svelte/transition';
/** loading things from the server side */
let { data } = $props();
@@ -17,86 +15,74 @@
<SeoMetaTags canonical={data.canonical} />
<!-- ECSESS Introduction -->
<Section from="from-ecsess-black" to="to-ecsess-900">
<Section from="from-ecsess-black" to="to-ecsess-900" via="via-ecsess-950">
<div
class="grid grid-cols-1 gap-2 place-self-center sm:gap-4 md:gap-6 lg:h-[70vh] lg:grid-cols-3 lg:grid-rows-3 lg:items-center lg:gap-6"
>
<!-- Title -->
<div
class="order-1 mb-2 flex items-center justify-center lg:col-start-1 lg:row-start-1 lg:mb-6 lg:place-self-center"
>
<div class="flex flex-col text-center">
<p>
{#each 'We are ECSESS!' as char, i}
<span class="page-title" in:fade|global={{ delay: 200 + i * 100, duration: 800 }}
>{char}</span
class="mx-auto grid w-full max-w-[84dvw] grid-cols-1 place-items-center gap-16 py-6 lg:min-h-[75vh] lg:grid-cols-[1fr_2fr]"
>
<!-- Left: Description and Quick Links -->
<div class="ml-4 flex flex-col items-center gap-2 text-center lg:items-start lg:text-left">
<h1 class="mb-2">
{#each 'We are ECSESS!'.split('') as char, i}
<span class="page-title" in:fade|global={{ delay: 150 + i * 60, duration: 800 }}>
{char}
</span>
{/each}
</p>
</div>
</div>
<!-- Description -->
<div
class="order-2 mb-2 flex items-center justify-center p-4 lg:col-start-1 lg:row-start-2 lg:mb-6 lg:place-self-center"
</h1>
<p
class="text-ecsess-200/90 max-w-xl text-base leading-relaxed md:text-lg lg:max-w-lg lg:leading-8"
>
<div class="max-w-xl text-center lg:text-center">
<RichText value={data.description} />
<span class="text-ecsess-50 font-bold"
>Electrical, Computer & Software Engineering Students' Society at McGill (ECSESS)</span
>
is the student council which helps McGill ECSE students in their
<span class="text-ecsess-50 font-bold">academic</span>,
<span class="text-ecsess-50 font-bold">technical</span>,
<span class="text-ecsess-50 font-bold">social</span> and
<span class="text-ecsess-50 font-bold">professional</span> progression.
</p>
<div class="mt-8 w-full max-w-xl lg:max-w-none">
<QuickLinks />
</div>
</div>
<!-- Image -->
<div class="order-3 m-0 sm:m-2 lg:col-span-2 lg:col-start-2 lg:row-span-3 lg:row-start-1">
<div class="flex h-auto w-full items-center justify-center sm:h-full">
<!-- Right: Council Photo -->
<div class="relative flex w-full items-center justify-center lg:max-w-none">
<div
class="ring-ecsess-400/20 ring-offset-ecsess-900/50 relative flex items-center justify-center overflow-hidden rounded-2xl shadow-2xl ring-1 ring-offset-2"
>
<div
class="from-ecsess-500/10 absolute inset-0 rounded-2xl bg-linear-to-br to-transparent"
aria-hidden="true"
></div>
<img
src={data.councilPhoto}
alt="ECSESS Council"
class="ring-ecsess-500 max-h-[35vh] max-w-full rounded-md object-contain shadow-md ring-4 transition-all sm:max-h-[45vh] md:max-h-[60vh] lg:max-h-full"
class="relative h-full w-full object-contain object-center"
/>
</div>
</div>
<!-- Quick Links -->
<div class="order-4 p-3 lg:col-start-1 lg:row-start-3 lg:flex lg:place-self-center lg:p-4">
<QuickLinks />
</div>
</div>
</Section>
<!-- Office Hours Calendar -->
<Section from="from-ecsess-900" to="to-ecsess-800">
<Section from="from-ecsess-900" to="to-ecsess-700" via="via-ecsess-650">
<div class="w-full">
<h1 id="office-hours">Office Hours</h1>
<h2 class="text-2xl font-bold" id="office-hours">Lounge Office Hours</h2>
<p class="text-ecsess-200 mb-8">
Come visit us in our student lounge at ENGTR 1060 to grab a coffee (free), play Mario Kart, or
just chat about anything!
</p>
<OhSchedule allOhs={data.allOHs} />
</div>
</Section>
<!-- Sponsors -->
<Section from="from-ecsess-700" to="to-ecsess-800" via="via-ecsess-750">
<Sponsors sponsors={data.sponsors} />
</Section>
<!-- Affiliated Clubs -->
<Section from="from-ecsess-800" to="to-ecsess-950">
<Section from="from-ecsess-800" to="to-ecsess-black" via="via-ecsess-850">
<AffiliatedGroups />
</Section>
<!-- FAQs and Sponsors -->
<Section from="from-ecsess-950" to="to-ecsess-black">
<div class="grid w-full max-w-[80vw] grid-cols-1 gap-12 lg:grid-cols-2 lg:gap-24">
<div>
<h1>FAQs</h1>
<hr class="hr w-full border-dashed py-4" />
<FaqAccordion entries={data.faqs} />
</div>
<div id="sponsors" class="mb-24">
<h1>Sponsors</h1>
<hr class="hr w-full border-dashed py-4" />
<div class="flex gap-12">
{#each data.sponsors as sponsor}
<div class="max-h-20">
<Link href={sponsor.url}>
<img src={sponsor.logo} alt="{sponsor.name} Logo" class="max-h-24" />
</Link>
</div>
{/each}
</div>
</div>
</div>
</Section>