diff --git a/README.md b/README.md
index 5e24075..9f3fc8f 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@ CI rejects JavaScript or CSS growth beyond the limits in
the current production build and should be raised only with an explanation in
the pull request.
-Lighthouse runs three times against both generated pages. Accessibility, SEO,
+Lighthouse runs three times against all five generated pages. Accessibility, SEO,
layout stability, blocking time, and best-practice regressions fail CI;
performance-score and largest-contentful-paint regressions initially warn while
the project establishes a stable baseline.
@@ -72,3 +72,13 @@ The Cloudflare Pages project should use:
Do not set `SKIP_DEPENDENCY_INSTALL`. Cloudflare copies `public/_headers` into
the production build and applies those response headers to static assets.
+
+## Competition editions
+
+The homepage is the current 2026 competition. Past editions live at
+`/competitions/2024/` and `/competitions/2025/`; organization content lives at
+`/about/`. Competition-specific sponsors, photos, and results are assigned in
+`src/data/competitions.ts`, with shared logo definitions in `src/data/sponsors.ts`.
+Archives only display available historical content, never current registration
+information. New editions need a static HTML entry, Vite input, sitemap entry,
+and Lighthouse URL in addition to their competition data.
diff --git a/about/index.html b/about/index.html
new file mode 100644
index 0000000..ec26ede
--- /dev/null
+++ b/about/index.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+ About MACSO | Massachusetts Computer Science Olympiad
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/competitions/2024/index.html b/competitions/2024/index.html
new file mode 100644
index 0000000..742b062
--- /dev/null
+++ b/competitions/2024/index.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+ MACSO 2024 Archive | Massachusetts Computer Science Olympiad
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/competitions/2025/index.html b/competitions/2025/index.html
new file mode 100644
index 0000000..de750e8
--- /dev/null
+++ b/competitions/2025/index.html
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+ MACSO Fall 2025 Archive | Massachusetts Computer Science Olympiad
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/lighthouserc.json b/config/lighthouserc.json
index 5d1255e..50a9254 100644
--- a/config/lighthouserc.json
+++ b/config/lighthouserc.json
@@ -2,18 +2,59 @@
"ci": {
"collect": {
"staticDistDir": "./dist",
- "url": ["http://localhost/", "http://localhost/careers/"],
+ "url": [
+ "http://localhost/",
+ "http://localhost/careers/",
+ "http://localhost/about/",
+ "http://localhost/competitions/2024/",
+ "http://localhost/competitions/2025/"
+ ],
"numberOfRuns": 3
},
"assert": {
"assertions": {
- "categories:performance": ["warn", { "minScore": 0.9 }],
- "categories:accessibility": ["error", { "minScore": 0.95 }],
- "categories:best-practices": ["error", { "minScore": 0.9 }],
- "categories:seo": ["error", { "minScore": 0.95 }],
- "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
- "total-blocking-time": ["error", { "maxNumericValue": 300 }],
- "largest-contentful-paint": ["warn", { "maxNumericValue": 2500 }]
+ "categories:performance": [
+ "warn",
+ {
+ "minScore": 0.9
+ }
+ ],
+ "categories:accessibility": [
+ "error",
+ {
+ "minScore": 0.95
+ }
+ ],
+ "categories:best-practices": [
+ "error",
+ {
+ "minScore": 0.9
+ }
+ ],
+ "categories:seo": [
+ "error",
+ {
+ "minScore": 0.95
+ }
+ ],
+ "cumulative-layout-shift": [
+ "error",
+ {
+ "maxNumericValue": 0.1
+ }
+ ],
+ "total-blocking-time": [
+ "error",
+ {
+ "maxNumericValue": 300
+ }
+ ],
+ "largest-contentful-paint": [
+ "warn",
+ {
+ "maxNumericValue": 2500
+ }
+ ]
}
}
}
diff --git a/index.html b/index.html
index ae81620..ae44992 100644
--- a/index.html
+++ b/index.html
@@ -6,7 +6,7 @@
Massachusetts Computer Science Olympiad | MACSO
@@ -60,7 +60,7 @@
/>
+
+
+ logo_webTop
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/sitemap.xml b/public/sitemap.xml
index 6b0e860..15984a0 100644
--- a/public/sitemap.xml
+++ b/public/sitemap.xml
@@ -2,7 +2,7 @@
https://macso.org/
- 2026-09-14
+ 2026-10-11
monthly
1.0
@@ -12,4 +12,7 @@
monthly
0.8
+ https://macso.org/about/ yearly 0.6
+ https://macso.org/competitions/2024/ yearly 0.6
+ https://macso.org/competitions/2025/ yearly 0.6
diff --git a/src/components/layout/Footer.module.css b/src/components/layout/Footer.module.css
index 94929ee..4a89287 100644
--- a/src/components/layout/Footer.module.css
+++ b/src/components/layout/Footer.module.css
@@ -47,3 +47,14 @@
transition: none;
}
}
+
+.links {
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 12px 24px;
+ margin: 16px 0;
+}
+.links a {
+ color: var(--accent-text);
+}
diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx
index c5acd75..555c696 100644
--- a/src/components/layout/Footer.tsx
+++ b/src/components/layout/Footer.tsx
@@ -17,16 +17,12 @@ export function Footer({ page }: FooterProps) {
Brookline Computer Science Initiative (BCSI), a 501(c)(3) non-profit
organization.
-
- Directed by{' '}
-
- Mikhail Zhernevskii
-
- , Jun 2024 - October 2024
-
+
+ About
+ Careers
+ Discord
+ Contact
+
Website is made by{' '}
diff --git a/src/components/layout/Header.module.css b/src/components/layout/Header.module.css
index f7d11c4..ac03133 100644
--- a/src/components/layout/Header.module.css
+++ b/src/components/layout/Header.module.css
@@ -143,7 +143,9 @@
}
@media (prefers-reduced-motion: reduce) {
- .nav a.hire,
+ .nav a.hire {
+ transition: none;
+ }
.themeButton,
.themeButton svg,
.brand,
@@ -152,3 +154,57 @@
transition: none;
}
}
+
+.archives {
+ position: relative;
+ font-size: 0.88rem;
+ font-weight: 700;
+}
+.archives summary {
+ cursor: pointer;
+ padding: 8px 0;
+}
+.archives summary:focus-visible {
+ outline: 3px solid var(--blue-deep);
+ outline-offset: 3px;
+}
+.archiveLinks {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ min-width: 180px;
+ display: grid;
+ gap: 4px;
+ padding: 10px;
+ background: var(--paper);
+ border: 1px solid var(--rule-strong);
+ border-radius: 8px;
+ box-shadow: var(--shadow);
+}
+.archiveLinks a {
+ padding: 10px;
+}
+.nav a[aria-current='page'] {
+ color: var(--blue-deep);
+ text-decoration: underline;
+ text-underline-offset: 5px;
+}
+@media (max-width: 520px) {
+ .header {
+ gap: 8px;
+ padding: 10px 18px;
+ }
+ .nav {
+ width: 100%;
+ gap: 4px 14px;
+ }
+ .nav a,
+ .archives {
+ font-size: 0.8rem;
+ }
+ .themeButton {
+ margin-left: auto;
+ width: 30px;
+ height: 30px;
+ }
+}
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
index adf3694..003fcd4 100644
--- a/src/components/layout/Header.tsx
+++ b/src/components/layout/Header.tsx
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
-import { navLinks } from '~/data/site'
+import { pastCompetitions } from '~/data/competitions'
import { applyTheme, getPreferredTheme, saveTheme } from '~/lib/theme'
import { type PageId, pageMeta } from './pages'
import styles from './Header.module.css'
@@ -12,7 +12,6 @@ export function Header({ page }: HeaderProps) {
const [theme, setTheme] = useState(getPreferredTheme)
const { topId } = pageMeta[page]
const isHome = page === 'home'
- const hashBase = isHome ? '' : '/'
useEffect(() => {
applyTheme(theme)
@@ -30,11 +29,36 @@ export function Header({ page }: HeaderProps) {
macso.
- {navLinks.map(({ label, hash }) => (
-
- {label}
-
- ))}
+
+ 2026 Competition
+
+ {
+ if (event.key === 'Escape') {
+ event.currentTarget.open = false
+ event.currentTarget.querySelector('summary')?.focus()
+ }
+ }}
+ >
+ Past Competitions
+
+
+
+ About
+
= {
home: { topId: 'notebook-top' },
careers: { topId: 'careers-top' },
+ about: { topId: 'about-top' },
+ archive2024: { topId: 'archive-2024-top' },
+ archive2025: { topId: 'archive-2025-top' },
}
diff --git a/src/data/competitions.ts b/src/data/competitions.ts
new file mode 100644
index 0000000..0f8fe7c
--- /dev/null
+++ b/src/data/competitions.ts
@@ -0,0 +1,63 @@
+import { fall25Photos, macso24Photos, type Photo } from './photos'
+import { scoreboards, type Scoreboard } from './results'
+import type { SponsorId } from './sponsors'
+
+export type CompetitionYear = 2024 | 2025 | 2026
+export type Competition = {
+ readonly year: CompetitionYear
+ readonly title: string
+ readonly href: string
+ readonly status: 'current' | 'completed'
+ readonly sponsors: readonly SponsorId[]
+ readonly photos?: readonly Photo[]
+ readonly results?: readonly Scoreboard[]
+}
+
+export const competitions: Record = {
+ 2024: {
+ year: 2024,
+ title: 'MACSO 2024',
+ href: '/competitions/2024/',
+ status: 'completed',
+ sponsors: [
+ 'ktbyte',
+ 'rsm',
+ 'dataIntensity',
+ 'algoverse',
+ 'bhs',
+ 'hackClub',
+ 'btc',
+ 'aops',
+ ],
+ photos: macso24Photos,
+ results: scoreboards,
+ },
+ 2025: {
+ year: 2025,
+ title: 'MACSO Fall 2025',
+ href: '/competitions/2025/',
+ status: 'completed',
+ sponsors: [
+ 'google',
+ 'wolfram',
+ 'jetbrains',
+ 'bhs',
+ 'hackClub',
+ 'btc',
+ 'aops',
+ ],
+ photos: fall25Photos,
+ },
+ 2026: {
+ year: 2026,
+ title: 'MACSO 2026',
+ href: '/',
+ status: 'current',
+ sponsors: ['janeStreet', 'xcamp', 'wolfram', 'bhs', 'hackClub', 'btc'],
+ },
+}
+export const currentCompetition = competitions[2026]
+export const pastCompetitions = [
+ competitions[2025],
+ competitions[2024],
+] as const
diff --git a/src/data/faq.tsx b/src/data/faq.tsx
index d715206..9653333 100644
--- a/src/data/faq.tsx
+++ b/src/data/faq.tsx
@@ -1,117 +1,61 @@
import type { ReactNode } from 'react'
import { TextLink } from '~/components/ui/TextLink'
import { site } from './site'
-
-export type FaqItem = {
- readonly question: string
- readonly answer: ReactNode
-}
-
+export type FaqItem = { readonly question: string; readonly answer: ReactNode }
export const faqItems: readonly FaqItem[] = [
{
- question: 'Who can participate in the competition?',
+ question: 'Who can participate?',
answer: (
<>
- Anyone is welcome to enter the online qualification round. There is no
- qualification requirement to compete in Round 1. Whether you're brand
- new to competitive programming or have competed before, we'd love to
- have you join us!
+ Anyone, anywhere can enter the online qualification round, including
+ beginners. Both rounds are individual. If you advance, you must attend
+ the final in person in Brookline.
>
),
},
{
- question: 'How does the competition work?',
+ question: 'How do I register? What if I registered earlier?',
answer: (
<>
- MACSO 2026 runs in two rounds. Round 1 is an online qualification round
- of 3–4 problems, open from {site.round.onlineOpens} until{' '}
- {site.round.onlineClosesTime} on {site.round.onlineCloses}, hosted at{' '}
- grader.macso.org . Competitors
- who advance are invited to Round 2, an in-person final on{' '}
- {site.round.finalDates}. Both rounds are competed individually.
- >
- ),
- },
- {
- question: 'I already registered earlier. Do I need to register again?',
- answer: (
- <>
- Yes. Everyone must complete the new{' '}
+ Complete the{' '}
qualification round registration form
- , even if you registered through the earlier event form. We know this is
- a change, but it's what lets us issue everyone grader accounts for the
- online round.
- >
- ),
- },
- {
- question: 'How do I get into the online round once I register?',
- answer: (
- <>
- After you register, we email you a username and password for the contest
- grader. Sign in with those credentials at{' '}
- grader.macso.org and you can
- start submitting. If your credentials haven't arrived, check your spam
- folder, then email{' '}
- {site.email} and
- we'll get you set up.
- >
- ),
- },
- {
- question: 'How many problems are in the online round?',
- answer: (
- <>
- The round starts with 3–4 programming problems, and we may add a few
- more while the round is open, depending on how the round goes. There's
- no fixed sitting — solve them at your own pace any time before the
- deadline.
+ . Everyone must submit this form, even if you registered through the
+ earlier event form, so we can issue your grader account.
>
),
},
{
- question: 'What is the scoring system like?',
+ question: 'How do I access the grader?',
answer: (
<>
- The contest uses the ICPC system: each problem is either solved or not,
- and the time penalty serves as tie breaker. Your Round 1 results
- determine advancement to the in-person final.
+ After registration, we email your username and password. Sign in at{' '}
+ grader.macso.org to start
+ submitting. Missing credentials? Check your spam folder, then email{' '}
+ {site.email} .
>
),
},
{
- question: 'When and where is the in-person final?',
+ question: 'How many problems are there, and when do I solve them?',
answer: (
<>
- The final is scheduled for {site.round.finalDates} at {site.venue.name},{' '}
- {site.venue.address}. Qualifiers will be notified by email with check-in
- time and the remaining event details once they're confirmed. Prizes will
- be awarded to our top competitors, just like last year!
- >
- ),
- },
- {
- question: 'How can I register for the competition?',
- answer: (
- <>
- You can register by clicking the Register for Round 1 button at the top
- of the page, or directly through the{' '}
-
- qualification round registration form
- {' '}
- where you can fill in your details.
+ The online round starts with 3–4 problems; more may be added. Solve at
+ your own pace while the round is open, starting {site.round.onlineOpens}
+ . There is no fixed sitting for Round 1.
>
),
},
{
- question: 'Can I participate if I’m not in Massachusetts?',
+ question: 'How are scoring and advancement decided?',
answer: (
<>
- Yes! The online qualification round is open to anyone, anywhere, with no
- qualification requirement. The final is in person, so if you advance
- you’ll need to make it to Brookline on {site.round.finalDates}.
+ The contest uses ICPC-style scoring: each problem is solved or not, with
+ time penalty as the tiebreaker. Round 1 performance determines
+ advancement to the timed in-person final in {site.round.finalDates}.
+ Qualifiers receive check-in details by email; prizes go to top
+ competitors.
>
),
},
diff --git a/src/data/site.ts b/src/data/site.ts
index b71c236..1c94bbd 100644
--- a/src/data/site.ts
+++ b/src/data/site.ts
@@ -11,10 +11,7 @@ export const site = {
instagramHandle: '@bhs.computerscience',
round: {
onlineOpens: 'Monday, September 7, 2026',
- onlineCloses: 'Saturday, October 10, 2026',
- onlineClosesShort: 'Oct 10',
- onlineClosesTime: '11:59 PM ET',
- finalDates: 'October 11, 2026',
+ finalDates: 'late October 2026',
},
venue: {
name: 'Brookline Teen Center',
@@ -28,18 +25,3 @@ export const site = {
'https://www.linkedin.com/in/mikhail-zhernevskii-117477282',
},
} as const
-
-export type NavLink = {
- readonly label: string
- readonly hash: string
-}
-
-export const navLinks: readonly NavLink[] = [
- { label: 'Format', hash: '#format' },
- { label: 'Highlights', hash: '#highlights' },
- { label: 'Results', hash: '#results' },
- { label: 'FAQ', hash: '#faq' },
- { label: 'Team', hash: '#team' },
- { label: 'Sponsors', hash: '#sponsors' },
- { label: 'Location', hash: '#location' },
-]
diff --git a/src/data/sponsors.ts b/src/data/sponsors.ts
index 4c6ce5d..046248d 100644
--- a/src/data/sponsors.ts
+++ b/src/data/sponsors.ts
@@ -4,68 +4,75 @@ export type Tile = {
readonly logo: string
}
-export const sponsors: readonly Tile[] = [
- {
+export const sponsorDirectory = {
+ xcamp: {
+ name: 'X-Camp',
+ href: 'https://x-camp.academy/',
+ logo: '/images/logos/x-camp.svg',
+ },
+ janeStreet: {
name: 'Jane Street',
href: 'https://www.janestreet.com/',
logo: '/images/logos/jane-street.png',
},
- {
+ google: {
name: 'Google',
href: 'https://edu.google.com/',
logo: '/images/logos/google.webp',
},
- {
+ wolfram: {
name: 'Wolfram',
href: 'https://www.wolfram.com/',
logo: '/images/logos/wolfram.webp',
},
- {
+ jetbrains: {
name: 'JetBrains',
href: 'https://www.jetbrains.com/',
logo: '/images/logos/jetbrains_logo.webp',
},
- {
+ btc: {
name: 'Brookline Teen Center',
href: 'https://brooklineteencenter.org/',
logo: '/images/logos/btc.webp',
},
- {
+ rsm: {
name: 'Russian School of Mathematics',
href: 'https://www.mathschool.com/',
logo: '/images/logos/rsm.webp',
},
- {
+ ktbyte: {
name: 'KTBYTE',
href: 'https://www.ktbyte.com/',
logo: '/images/logos/ktbyte.webp',
},
- {
+ dataIntensity: {
name: 'Data Intensity',
href: 'https://www.dataintensity.com/',
logo: '/images/logos/dataintensity.webp',
},
- {
+ hackClub: {
name: 'Hack Club',
href: 'https://hackclub.com/',
logo: 'https://assets.hackclub.com/icon-rounded.svg',
},
- {
+ aops: {
name: 'Art of Problem Solving',
href: 'https://artofproblemsolving.com/',
logo: '/images/logos/AoPS.webp',
},
- {
+ algoverse: {
name: 'Algoverse',
href: 'https://algoverseairesearch.org/',
logo: '/images/logos/algoverse_logo.webp',
},
- {
+ bhs: {
name: 'Brookline High School',
href: 'https://bhs.brookline.k12.ma.us/',
logo: '/images/logos/bhs1.webp',
},
-]
+} as const satisfies Record
+
+export type SponsorId = keyof typeof sponsorDirectory
export const partners: readonly Tile[] = [
{
diff --git a/src/entries/about.tsx b/src/entries/about.tsx
new file mode 100644
index 0000000..3bc9857
--- /dev/null
+++ b/src/entries/about.tsx
@@ -0,0 +1,5 @@
+import { AboutPage } from '~/pages/AboutPage'
+import '~/styles/tokens.css'
+import '~/styles/base.css'
+import { renderPage } from './renderPage'
+renderPage( )
diff --git a/src/entries/archive2024.tsx b/src/entries/archive2024.tsx
new file mode 100644
index 0000000..2bcec08
--- /dev/null
+++ b/src/entries/archive2024.tsx
@@ -0,0 +1,5 @@
+import { ArchivePage } from '~/pages/ArchivePage'
+import '~/styles/tokens.css'
+import '~/styles/base.css'
+import { renderPage } from './renderPage'
+renderPage( )
diff --git a/src/entries/archive2025.tsx b/src/entries/archive2025.tsx
new file mode 100644
index 0000000..bbec85c
--- /dev/null
+++ b/src/entries/archive2025.tsx
@@ -0,0 +1,5 @@
+import { ArchivePage } from '~/pages/ArchivePage'
+import '~/styles/tokens.css'
+import '~/styles/base.css'
+import { renderPage } from './renderPage'
+renderPage( )
diff --git a/src/features/home/FormatSection.module.css b/src/features/home/FormatSection.module.css
index aa3133f..9863575 100644
--- a/src/features/home/FormatSection.module.css
+++ b/src/features/home/FormatSection.module.css
@@ -5,7 +5,7 @@
.rounds {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
gap: 22px;
margin: 28px 0 0;
padding: 0;
diff --git a/src/features/home/FormatSection.tsx b/src/features/home/FormatSection.tsx
index 7be1186..6bf8b5e 100644
--- a/src/features/home/FormatSection.tsx
+++ b/src/features/home/FormatSection.tsx
@@ -9,7 +9,11 @@ export function FormatSection() {
const { round } = site
return (
-