-
+
+
+ {menu.sections.length === 1 && !menu.sections[0].label ? (
+
+ ) : (
+ menu.sections.map((section) => (
+
+ {section.label &&
{section.label}
}
+
+
+ ))
+ )}
) : (
- {menu.name}
+ {menu.label}
)
))}
@@ -172,6 +188,31 @@ nav {
padding: 0;
}
+.nav-dropdown-wide {
+ min-width: 540px;
+}
+
+.nav-dropdown-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 0.5rem;
+}
+
+.nav-dropdown-col {
+ break-inside: avoid;
+}
+
+.nav-dropdown-label {
+ font-size: 0.7rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: var(--color-text-muted);
+ padding: 0.65rem 1.2rem 0.3rem;
+ margin: 0;
+ opacity: 0.7;
+}
+
.nav-dropdown li a {
display: block;
padding: 0.65rem 1.2rem;
@@ -356,6 +397,26 @@ nav {
font-size: 0.95rem;
}
+ /* Mobile: stack Programme sections vertically */
+ .nav-dropdown-grid {
+ display: block;
+ }
+
+ .nav-dropdown-col {
+ margin-bottom: 0.5rem;
+ }
+
+ .nav-dropdown-label {
+ font-size: 0.75rem;
+ padding: 0.6rem 0 0.2rem 0;
+ margin: 0;
+ color: var(--color-text-muted);
+ opacity: 0.7;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ }
+
.nav-item.open .nav-arrow {
transform: rotate(180deg);
}
diff --git a/src/components/JobCard.astro b/src/components/JobCard.astro
index 5c7dd056c..5214f8fd9 100644
--- a/src/components/JobCard.astro
+++ b/src/components/JobCard.astro
@@ -1,7 +1,7 @@
---
import { getEntry } from "astro:content";
import Markdown from "@ui/Markdown.astro";
-const { job:jobId, sponsor:sponsorId } = Astro.props;
+const { job:jobId, sponsor:sponsorId, compact = true } = Astro.props;
const job = await getEntry("jobs", jobId);
if (!job) {
@@ -17,22 +17,30 @@ if (!sponsor) {
// TODO: add tags
const { title, location, type, level, salary, description, responsibilities, min_requirements, requirements, preferred, benefits, apply_link, draft } = job.data;
+const jobUrl = `/sponsor/${jobId}`;
+const descLimit = 500;
+const descFull = description || "";
+const descDisplay = compact && descFull.length > descLimit
+ ? descFull.slice(0, descLimit).replace(/\s+\S*$/, "") + "…"
+ : descFull;
+
---
-
+
{sponsor.data.name}
-
- {title}
+
+ {title}
{([level, type, location].filter(Boolean)).join(" • ")}
{salary}
-
+
+ {compact &&
}
- { responsibilities &&
+ {!compact && responsibilities &&
Responsibilities
{responsibilities.map((item) => {
@@ -53,49 +61,61 @@ const { title, location, type, level, salary, description, responsibilities, min
}
- { min_requirements &&
+ {!compact && min_requirements &&
Minimum requirements
{min_requirements.map((item:string) => - {item}
)}
}
- { requirements &&
+ {!compact && requirements &&
Requirements
{requirements.map((item:string) => - {item}
)}
}
- { preferred &&
+ {!compact && preferred &&
Nice-to-Haves
{preferred.map((item:string) => - {item}
)}
}
- { benefits &&
+ {!compact && benefits &&
Benefits
{benefits.map((item:string) => - {item}
)}
}
- { job.data.description2 &&
+ {!compact && job.data.description2 &&
}
-
- Apply Now
-
+ {!compact && apply_link && (
+
Apply Now
+ )}
diff --git a/src/components/ui/Headline.astro b/src/components/ui/Headline.astro
index 12f0f887f..ce9415118 100644
--- a/src/components/ui/Headline.astro
+++ b/src/components/ui/Headline.astro
@@ -26,7 +26,7 @@ const isCenter = center ? "text-center" : "";
h4,
h5,
h6 {
- font-weight: 500;
+ font-weight: 700;
font-family: var(--font-display);
line-height: 1.25;
diff --git a/src/components/ui/Markdown.astro b/src/components/ui/Markdown.astro
index bb4054d5e..c5d0cce61 100644
--- a/src/components/ui/Markdown.astro
+++ b/src/components/ui/Markdown.astro
@@ -11,6 +11,34 @@ const { content, class: className } = Astro.props;
const defaultClass = className? className: "prose-xl";
const html = marked.parse(await replaceYouTubeLinks(content) );
---
-
+
diff --git a/src/components/ui/Prose.astro b/src/components/ui/Prose.astro
index 4879dd7be..31cd2ca51 100644
--- a/src/components/ui/Prose.astro
+++ b/src/components/ui/Prose.astro
@@ -10,8 +10,9 @@ const { class: className, full=false } = Astro.props;
diff --git a/src/content.config.ts b/src/content.config.ts
index 5e99c17ee..ef63a2497 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -9,7 +9,7 @@ const mode = import.meta.env.MODE;
console.log(`\x1b[35m[EP]\x1b[0m Current MODE: \x1b[1m\x1b[34m${mode}\x1b[0m`);
const pages = defineCollection({
- loader: glob({ pattern: "*.{md,mdx}", base: "./src/content/pages" }),
+ loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/pages" }),
schema: z.object({
title: z.string(),
subtitle: z.string(),
diff --git a/src/data/links.json b/src/data/links.json
deleted file mode 100644
index bcddf3f48..000000000
--- a/src/data/links.json
+++ /dev/null
@@ -1,261 +0,0 @@
-{
- "header": [
- {
- "name": "Programme",
- "items": [
- {
- "name": "Schedule",
- "path": "/schedule/talks"
- },
- {
- "name": "List of Sessions",
- "path": "/sessions"
- },
- {
- "name": "List of Speakers",
- "path": "/speakers"
- },
- {
- "name": "Tracks",
- "path": "/tracks"
- },
- {
- "name": "Speaker Guidelines",
- "path": "/guidelines"
- },
- {
- "name": "Speaker Mentorship",
- "path": "/mentorship"
- }
- ]
- },
- {
- "name": "Events",
- "items": [
- {
- "name": "Language Summit",
- "path": "/language-summit"
- },
- {
- "name": "Packaging Summit",
- "path": "/packaging-summit"
- },
- {
- "name": "Rust Summit",
- "path": "/rust-summit"
- }
- ]
- },
- {
- "name": "Venue",
- "items": [
- {
- "name": "Venue",
- "path": "/venue"
- },
- {
- "name": "Kraków",
- "path": "/krakow"
- },
- {
- "name": "Hotels",
- "path": "/hotels"
- }
- ]
- },
- {
- "name": "Sponsorship",
- "items": [
- {
- "name": "Our Sponsors",
- "path": "/sponsors"
- },
- {
- "name": "Sponsor Packages",
- "path": "/sponsorship/sponsor"
- },
- {
- "name": "Sponsor Information",
- "path": "/sponsorship/information"
- }
- ]
- },
- {
- "name": "Attend",
- "items": [
- {
- "name": "Tickets",
- "path": "/tickets"
- },
- {
- "name": "Financial Aid",
- "path": "/finaid"
- },
- {
- "name": "Visa Information",
- "path": "/visa"
- },
- {
- "name": "Volunteering",
- "path": "/volunteering"
- },
- {
- "name": "FAQ",
- "path": "/faq"
- },
- {
- "name": "Anniversary Challenge",
- "path": "https://ep2026.europython.eu/25anniversary"
- },
- {
- "name": "Code of Conduct",
- "path": "https://www.europython-society.org/coc/"
- }
- ]
- },
- {
- "name": "Community",
- "items": [
- {
- "name": "About Us",
- "path": "/about"
- },
- {
- "name": "EuroPython Society",
- "path": "https://europython-society.org/"
- },
- {
- "name": "Community Partners",
- "path": "/community-partners"
- }
- ]
- },
- {
- "name": "Jobs",
- "path": "/jobs"
- }
- ],
- "footer": [
- {
- "name": "Quick links",
- "items": [
- {
- "name": "Tickets",
- "path": "/tickets"
- },
- {
- "name": "Kraków",
- "path": "/krakow"
- },
- {
- "name": "Visa Information",
- "path": "/visa"
- }
- ]
- },
- {
- "name": "Programme",
- "items": [
- {
- "name": "Schedule",
- "path": "/schedule/talks"
- },
- {
- "name": "List of Sessions",
- "path": "/sessions"
- },
- {
- "name": "List of Speakers",
- "path": "/speakers"
- },
- {
- "name": "Tracks",
- "path": "/tracks"
- },
- {
- "name": "Speaker Guidelines",
- "path": "/guidelines"
- },
- {
- "name": "Speaker Mentorship",
- "path": "/mentorship"
- }
- ]
- },
- {
- "name": "Events",
- "items": [
- {
- "name": "Language Summit",
- "path": "/language-summit"
- },
- {
- "name": "Packaging Summit",
- "path": "/packaging-summit"
- },
- {
- "name": "Rust Summit",
- "path": "/rust-summit"
- }
- ]
- },
- {
- "name": "Sponsorship",
- "items": [
- {
- "name": "Our Sponsors",
- "path": "/sponsors"
- },
- {
- "name": "Sponsor Packages",
- "path": "/sponsorship/sponsor"
- },
- {
- "name": "Sponsor Information",
- "path": "/sponsorship/information"
- }
- ]
- },
- {
- "name": "Sites",
- "items": [
- {
- "name": "EuroPython Society",
- "path": "https://europython-society.org/"
- },
- {
- "name": "EuroPython Blog",
- "path": "https://blog.europython.eu/"
- }
- ]
- }
- ],
- "terms": [
- {
- "name": "Contacts",
- "path": "/contacts"
- },
- {
- "name": "Terms",
- "path": "/terms"
- },
- {
- "name": "Code of Conduct",
- "path": "https://www.europython-society.org/coc/"
- },
- {
- "name": "Privacy Policy",
- "path": "https://www.europython-society.org/privacy/"
- }
- ],
- "socials": {
- "mastodon": "https://fosstodon.org/@europython",
- "linkedin": "https://www.linkedin.com/company/europython",
- "github": "https://github.com/europython",
- "bluesky": "https://bsky.app/profile/europython.eu",
- "twitter": "https://x.com/europython",
- "instagram": "https://www.instagram.com/europython/",
- "youtube": "https://www.youtube.com/channel/UC98CzaYuFNAA_gOINFB0e4Q",
- "tiktok": "https://www.tiktok.com/@europython"
- }
-}
diff --git a/src/data/nav.ts b/src/data/nav.ts
new file mode 100644
index 000000000..48be89813
--- /dev/null
+++ b/src/data/nav.ts
@@ -0,0 +1,262 @@
+/**
+ * Single source of truth for all navigation and footer links.
+ *
+ * Inspired by ep26-draft/src/menu.py — typed, structured, reusable.
+ */
+
+export interface Link {
+ label: string;
+ url: string;
+ external?: boolean;
+}
+
+export interface NavSection {
+ label?: string;
+ items: Link[];
+}
+
+export interface NavMenu {
+ label: string;
+ url: string;
+ sections?: NavSection[];
+ wide?: boolean;
+}
+
+export interface FooterColumn {
+ title: string;
+ items: Link[];
+}
+
+// ── Link registry ────────────────────────────────────────────
+
+const L = {
+ // Programme
+ schedule: { label: "Schedule", url: "/schedule" },
+ talks: { label: "Talks", url: "/talks" },
+ tutorials: { label: "Tutorials", url: "/tutorials" },
+ sessions: { label: "List of Sessions", url: "/sessions" },
+ posters: { label: "Posters", url: "/posters" },
+ speakers: { label: "List of Speakers", url: "/speakers" },
+ tracks: { label: "Tracks", url: "/tracks" },
+ guidelines: { label: "Speaker Guidelines", url: "/guidelines" },
+ mentorship: { label: "Speaker Mentorship", url: "/mentorship" },
+
+ // Summits
+ langSummit: { label: "Language Summit", url: "/language-summit" },
+ rustSummit: { label: "Rust Summit", url: "/rust-summit" },
+
+ // Events & Social
+ sprints: { label: "Sprints Weekend", url: "/sprints" },
+ socialEvent: { label: "Social Event", url: "/social-event" },
+ beginnersDay: { label: "Beginners' Day", url: "/beginners-day" },
+ speakersDinner: { label: "Speakers' Dinner", url: "/speakers-dinner" },
+ openSpaces: { label: "Open Spaces", url: "/open-spaces" },
+
+ // Participate
+ tickets: { label: "Tickets", url: "/tickets" },
+ finaid: { label: "Financial Aid", url: "/finaid" },
+ visa: { label: "Visa Information", url: "/visa" },
+ volunteering: { label: "Volunteering", url: "/volunteering" },
+ faq: { label: "FAQ", url: "/faq" },
+ coc: {
+ label: "Code of Conduct",
+ url: "https://www.europython-society.org/coc/",
+ external: true,
+ },
+
+ // Venue
+ venue: { label: "Venue", url: "/venue" },
+ krakow: { label: "Kraków", url: "/krakow" },
+ hotels: { label: "Hotels", url: "/hotels" },
+
+ // Sponsorship
+ ourSponsors: { label: "Our Sponsors", url: "/sponsors" },
+ sponsorPkg: { label: "Sponsor Packages", url: "/sponsorship/sponsor" },
+ sponsorInfo: {
+ label: "Sponsor Information",
+ url: "/sponsorship/information",
+ },
+
+ // Community
+ about: { label: "About Us", url: "/about" },
+ eps: {
+ label: "EuroPython Society",
+ url: "https://europython-society.org/",
+ external: true,
+ },
+ communityPartners: {
+ label: "Community Partners",
+ url: "/community-partners",
+ },
+
+ // Misc
+ jobs: { label: "Jobs", url: "/jobs" },
+ contacts: { label: "Contacts", url: "/contacts" },
+ terms: { label: "Terms", url: "/terms" },
+ privacy: {
+ label: "Privacy Policy",
+ url: "https://www.europython-society.org/privacy/",
+ external: true,
+ },
+ blog: {
+ label: "EuroPython Blog",
+ url: "https://blog.europython.eu/",
+ external: true,
+ },
+ anniversary: {
+ label: "Anniversary Challenge",
+ url: "https://ep2026.europython.eu/25anniversary",
+ external: true,
+ },
+};
+
+// ── Nav menus ────────────────────────────────────────────────
+
+export const NAV_MENUS: NavMenu[] = [
+ // Programme — rich multi-column with labelled sections
+ {
+ label: "Programme",
+ url: "/sessions",
+ wide: true,
+ sections: [
+ {
+ label: "Talks & Schedule",
+ items: [
+ L.schedule,
+ L.talks,
+ L.tutorials,
+ L.sessions,
+ L.posters,
+ L.speakers,
+ L.tracks,
+ ],
+ },
+ {
+ label: "Summits",
+ items: [L.langSummit, L.rustSummit],
+ },
+ {
+ label: "Events & Social",
+ items: [
+ L.sprints,
+ L.socialEvent,
+ L.beginnersDay,
+ L.speakersDinner,
+ L.openSpaces,
+ ],
+ },
+ {
+ label: "For Speakers",
+ items: [L.guidelines, L.mentorship],
+ },
+ ],
+ },
+
+ // Attend — simple flat list
+ {
+ label: "Attend",
+ url: "/tickets",
+ sections: [
+ { items: [L.tickets, L.finaid, L.visa, L.volunteering, L.faq, L.coc] },
+ ],
+ },
+
+ // Venue — simple flat list
+ {
+ label: "Venue",
+ url: "/venue",
+ sections: [{ items: [L.venue, L.krakow, L.hotels] }],
+ },
+
+ // Sponsorship — simple flat list
+ {
+ label: "Sponsorship",
+ url: "/sponsorship/sponsor",
+ sections: [{ items: [L.ourSponsors, L.sponsorPkg, L.sponsorInfo] }],
+ },
+
+ // Community — simple flat list
+ {
+ label: "Community",
+ url: "/about",
+ sections: [{ items: [L.about, L.eps, L.communityPartners] }],
+ },
+
+ // Jobs — single link, no dropdown
+ {
+ label: "Jobs",
+ url: "/jobs",
+ },
+];
+
+// ── Social links ────────────────────────────────────────────
+
+export const SOCIALS: Record = {
+ mastodon: "https://fosstodon.org/@europython",
+ linkedin: "https://www.linkedin.com/company/europython",
+ github: "https://github.com/europython",
+ bluesky: "https://bsky.app/profile/europython.eu",
+ twitter: "https://x.com/europython",
+ instagram: "https://www.instagram.com/europython/",
+ youtube: "https://www.youtube.com/channel/UC98CzaYuFNAA_gOINFB0e4Q",
+ tiktok: "https://www.tiktok.com/@europython",
+};
+
+export const TERMS: Link[] = [
+ { label: "Contacts", url: "/contacts" },
+ { label: "Terms", url: "/terms" },
+ {
+ label: "Code of Conduct",
+ url: "https://www.europython-society.org/coc/",
+ external: true,
+ },
+ {
+ label: "Privacy Policy",
+ url: "https://www.europython-society.org/privacy/",
+ external: true,
+ },
+];
+
+// ── Footer columns ───────────────────────────────────────────
+
+export const FOOTER_COLUMNS: FooterColumn[] = [
+ {
+ title: "Quick links",
+ items: [L.tickets, L.krakow, L.visa],
+ },
+ {
+ title: "Programme",
+ items: [
+ L.schedule,
+ L.sessions,
+ L.speakers,
+ L.tracks,
+ L.guidelines,
+ L.mentorship,
+ ],
+ },
+ {
+ title: "Events",
+ items: [
+ L.sprints,
+ L.socialEvent,
+ L.beginnersDay,
+ L.speakersDinner,
+ L.openSpaces,
+ L.langSummit,
+ L.rustSummit,
+ ],
+ },
+ {
+ title: "Sponsorship",
+ items: [L.ourSponsors, L.sponsorPkg, L.sponsorInfo, L.jobs],
+ },
+ {
+ title: "Community",
+ items: [L.about, L.eps, L.communityPartners, L.blog, L.contacts],
+ },
+ {
+ title: "Policies",
+ items: [L.coc, L.terms, L.privacy],
+ },
+];
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
index 3c7141ea5..bf9196832 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -5,7 +5,18 @@ import Footer from "@components/Footer.astro";
import Offline from "@components/Offline.astro";
import Breadcrumbs from '@components/Breadcrumbs.astro';
-import linksData from '@src/data/links.json';
+import { NAV_MENUS } from '@src/data/nav';
+
+// Convert NavMenu[] to the shape Breadcrumbs expects: { header: [{ name, path, items? }] }
+const breadcrumbData = {
+ header: NAV_MENUS.map((m) => ({
+ name: m.label,
+ path: m.url,
+ items: m.sections
+ ? m.sections.flatMap((s) => s.items.map((i) => ({ name: i.label, path: i.url })))
+ : undefined,
+ })),
+};
import "@fortawesome/fontawesome-free/css/all.min.css";
import "@styles/tailwind.css";
@@ -56,7 +67,7 @@ const hideFooter = Astro.props.hideFooter ?? false;
{currentPath !== "/" && (
- On this page
+ On this page
diff --git a/src/pages/posters.astro b/src/pages/posters.astro
new file mode 100644
index 000000000..c6f4ca5d3
--- /dev/null
+++ b/src/pages/posters.astro
@@ -0,0 +1,96 @@
+---
+import Layout from "@layouts/Layout.astro";
+import Section2 from "@ui/Section2.astro";
+import Title from "@ui/Title.astro";
+import { getCollection } from "astro:content";
+import ListPosters from "@components/sessions/list-posters.astro";
+
+const allSessions = await getCollection("sessions");
+const posters = allSessions
+ .filter((s) => s.data.session_type?.toLowerCase() === "poster")
+ .sort((a, b) => a.data.title.localeCompare(b.data.title));
+---
+
+
+
+
+
+
+
+
+
+
+ POSTERS
+
+
+ During the main conference
+ Exhibition area
+
+
+
+
+
+ -
+ Visual format
+ Posters present a project, library, or research finding as a visual display.
+ Think of it as a paper you can walk up to and discuss.
+
+ -
+ One-on-one conversations
+ Unlike talks, poster sessions let you have a direct conversation with the
+ author. Ask questions, give feedback, and dive as deep as you like.
+
+ -
+ Great for first-time speakers
+ Posters are an excellent way to share your work without the pressure of a
+ stage presentation. Many speakers start here.
+
+ -
+ Dedicated session time
+ A block of time is reserved in the schedule for poster viewing, so you won't
+ have to choose between posters and talks.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/schedule.astro b/src/pages/schedule.astro
index b74364452..4d86ce1b7 100644
--- a/src/pages/schedule.astro
+++ b/src/pages/schedule.astro
@@ -33,52 +33,64 @@ window.addEventListener('load', function() {
if (!currentAnchor) return;
- const allMatching = document.querySelectorAll(`h2#${currentAnchor}`);
- const targetH2 = allMatching[allMatching.length - 1];
-
- if (targetH2) {
- const parentElement: any = targetH2.parentElement;
-
- if (parentElement) {
- const originalBorder = parentElement.style.border;
- const originalBackground = parentElement.style.background;
-
- let blinkCount = 0;
- const maxBlinks = 5;
- const blinkDuration = 500; // milliseconds
-
- function toggleBorder() {
- if (blinkCount >= maxBlinks * 2) {
- parentElement.style.border = originalBorder;
- parentElement.style.background = originalBackground;
- return;
- }
-
- if (blinkCount % 2 === 0) {
- parentElement.style.border = '1px solid #ff9900';
- parentElement.style.background = 'white';
- } else {
- parentElement.style.border = originalBorder;
- parentElement.style.background = originalBackground;
- }
-
- blinkCount++;
- setTimeout(toggleBorder, blinkDuration);
+ var targetEl = document.getElementById(currentAnchor);
+
+ if (targetEl) {
+ // Find the day this session belongs to
+ var dayEl = targetEl.closest('.ep-sched-day');
+ if (dayEl) {
+ var dayName = dayEl.getAttribute('data-day');
+ var tabsContainer = document.getElementById('sched-tabs');
+ if (tabsContainer) {
+ var tabs = tabsContainer.querySelectorAll('.ep-sched-tab');
+ tabs.forEach(function(t) {
+ if (t.getAttribute('data-day') === dayName) {
+ t.click();
+ }
+ });
}
+ }
- toggleBorder();
+ var sessionEl = targetEl.querySelector('.ep-session') || targetEl;
+ var originalOutline = sessionEl.style.outline;
+ var originalBackground = sessionEl.style.background;
- setTimeout(function() {
- const scrollOffset = 200;
- const elementPosition = parentElement.getBoundingClientRect().top;
- const offsetPosition = elementPosition + window.pageYOffset - scrollOffset;
+ var blinkCount = 0;
+ var maxBlinks = 5;
+ var blinkDuration = 500;
- window.scrollTo({
- top: offsetPosition,
- behavior: 'smooth'
- });
- }, 500);
+ function toggleBorder() {
+ if (blinkCount >= maxBlinks * 2) {
+ sessionEl.style.outline = originalOutline;
+ sessionEl.style.background = originalBackground;
+ return;
+ }
+
+ if (blinkCount % 2 === 0) {
+ sessionEl.style.outline = '2px solid var(--ep-accent, #ff9900)';
+ sessionEl.style.outlineOffset = '-2px';
+ sessionEl.style.background = 'var(--ep-session-hover-bg, rgba(255,255,255,0.05))';
+ } else {
+ sessionEl.style.outline = originalOutline;
+ sessionEl.style.background = originalBackground;
+ }
+
+ blinkCount++;
+ setTimeout(toggleBorder, blinkDuration);
}
+
+ toggleBorder();
+
+ setTimeout(function() {
+ var scrollOffset = 200;
+ var elementPosition = sessionEl.getBoundingClientRect().top;
+ var offsetPosition = elementPosition + window.pageYOffset - scrollOffset;
+
+ window.scrollTo({
+ top: offsetPosition,
+ behavior: 'smooth'
+ });
+ }, 500);
}
}
diff --git a/src/pages/sponsor/[sponsor]/[job].astro b/src/pages/sponsor/[sponsor]/[job].astro
index cb44172fb..2c9e1467c 100644
--- a/src/pages/sponsor/[sponsor]/[job].astro
+++ b/src/pages/sponsor/[sponsor]/[job].astro
@@ -47,7 +47,7 @@ export async function getStaticPaths() {
- {()}
+ {()}
diff --git a/src/pages/talks.astro b/src/pages/talks.astro
new file mode 100644
index 000000000..670a6024a
--- /dev/null
+++ b/src/pages/talks.astro
@@ -0,0 +1,457 @@
+---
+import Layout from "@layouts/Layout.astro";
+import Section2 from "@ui/Section2.astro";
+import Title from "@ui/Title.astro";
+import { getCollection } from "astro:content";
+
+const allSessions = await getCollection("sessions");
+const talks = allSessions
+ .filter((s) => {
+ const t = s.data.session_type?.toLowerCase();
+ const isTalk = t === "talk" || t === "talk (long session)" || t === "panel";
+ return isTalk && s.data.track !== "~ None of these topics";
+ })
+ .sort((a, b) => a.data.title.localeCompare(b.data.title));
+
+// Group talks by track
+const trackOrder = [
+ "Python Core, Internals, Extensions",
+ "Machine Learning: Research & Applications",
+ "Data Engineering and MLOps",
+ "Jupyter and Scientific Python",
+ "Data preparation and visualisation",
+ "Machine Learning, NLP and CV",
+ "Web Development, Web APIs, Front-End Integration",
+ "DevOps, Cloud, Scalable Infrastructure",
+ "Tooling, Packaging, Developer Productivity",
+ "Testing, Quality Assurance, Security",
+ "Community Building, Education, Outreach",
+ "Professional Development, Careers, Leadership",
+ "Ethics, Social Responsibility, Sustainability, Legal",
+ "IoT, Embedded Systems, Hardware Integration",
+ "Python for Games, Art, Play and Expression",
+];
+
+const groups: Record
= {};
+for (const talk of talks) {
+ const track = talk.data.track || "Other";
+ if (!groups[track]) groups[track] = [];
+ groups[track].push(talk);
+}
+
+const sortedTracks = trackOrder.filter((t) => groups[t]);
+const otherTracks = Object.keys(groups).filter((t) => !trackOrder.includes(t)).sort();
+const allDisplayTracks = [...sortedTracks, ...otherTracks];
+---
+
+
+
+
+
+
+
+
+
+
+
+ TALKS
+
+
+ Wednesday – Friday, July 15–17
+ 5 parallel tracks
+
+
+
+
+
+
+
+
{talks.length} talks at EuroPython 2026
+
+
+
+
+
+
+
+
+
+ {allDisplayTracks.map((track) => (
+
+ ))}
+
+
+
No talks match your search.
+
+
+
+
+
+
+
+
diff --git a/src/pages/tutorials.astro b/src/pages/tutorials.astro
new file mode 100644
index 000000000..b1b4afafe
--- /dev/null
+++ b/src/pages/tutorials.astro
@@ -0,0 +1,320 @@
+---
+import Layout from "@layouts/Layout.astro";
+import Section2 from "@ui/Section2.astro";
+import Title from "@ui/Title.astro";
+import { getCollection } from "astro:content";
+
+const allSessions = await getCollection("sessions");
+const tutorials = allSessions
+ .filter((s) => s.data.session_type?.toLowerCase() === "tutorial")
+ .sort((a, b) => a.data.title.localeCompare(b.data.title));
+---
+
+
+
+
+
+
+
+
+
+
+
+ TUTORIALS
+
+
+ Monday & Tuesday, July 13–14
+ Small-group format
+
+
+
+
+
+ -
+ 3-hour or 6-hour sessions
+ Tutorials run for half a day or a full day. Each one is a self-contained workshop with exercises, examples, and hands-on practice.
+
+ -
+ Small groups, personal attention
+ Unlike talks, tutorials are capped in size. You get direct access to the instructor and can ask questions as you go.
+
+ -
+ Wide range of topics
+ From Python basics and web frameworks to data science pipelines, async programming, and testing strategies — there is a tutorial for every interest.
+
+ -
+ Separate ticket required
+ Tutorial participation requires a separate tutorial ticket in addition to your conference pass.
+
+
+
+
+
{tutorials.length} tutorials at EuroPython 2026
+
+
+
+
+
+
+
+
+
+
No tutorials match your search.
+
+
+
+
+
+
+
+
diff --git a/src/styles/markdown.css b/src/styles/markdown.css
index a37ba74b2..9951ddf03 100644
--- a/src/styles/markdown.css
+++ b/src/styles/markdown.css
@@ -1,9 +1,39 @@
+article.prose h1:not([class*="font-normal"]):not([class*="font-light"]) {
+ font-weight: 700 !important;
+}
+
+article.prose h1 {
+ font-weight: 700 !important;
+}
+
+article.prose h2 {
+ font-weight: 700 !important;
+}
+
+article.prose h3 {
+ font-weight: 600 !important;
+}
+
+article.prose h1 a {
+ font-weight: 700 !important;
+}
+
+article.prose h2 a {
+ font-weight: 700 !important;
+}
+
+article.prose h3 a {
+ font-weight: 600 !important;
+}
+
.prose :is(h1, h2, h3, h4, h5, h6) a {
text-decoration: none !important;
+ color: inherit !important;
}
.prose :is(h1, h2, h3, h4, h5, h6) a:hover {
- text-decoration: underline !important;
+ text-decoration: none !important;
+ color: inherit !important;
}
.prose :is(h1, h2, h3, h4, h5, h6) a:focus {