diff --git a/src/README.md b/src/README.md
index df9e5ad1..ec0dc937 100644
--- a/src/README.md
+++ b/src/README.md
@@ -1,6 +1,10 @@
{%
laika.title = Typelevel
laika.html.template = templates/home.template.html
+ homepage-announcement {
+ enabled = true
+ template = "/announcements/traverses-europe-2026.template.html"
+ }
%}
This is a placeholder for the landing page. No content from this markdown file is rendered.
diff --git a/src/announcements/announcement.css b/src/announcements/announcement.css
new file mode 100644
index 00000000..94696873
--- /dev/null
+++ b/src/announcements/announcement.css
@@ -0,0 +1,236 @@
+.homepage-announcement {
+ background:
+ linear-gradient(115deg, hsl(273deg 39% 29%), hsl(273deg 36% 47%) 58%, hsl(288deg 42% 34%));
+ border-bottom: 1px solid hsl(0deg 0% 0% / 12%);
+ color: var(--bulma-white);
+ isolation: isolate;
+ overflow: hidden;
+ position: relative;
+}
+
+.homepage-announcement::before {
+ background: url("../img/favicon.svg") center / contain no-repeat;
+ content: "";
+ height: 34rem;
+ left: 52%;
+ opacity: 0.13;
+ pointer-events: none;
+ position: absolute;
+ top: 50%;
+ transform: translate(-50%, -50%) rotate(-7deg);
+ width: 34rem;
+ z-index: -1;
+}
+
+.homepage-announcement::after {
+ background: hsl(0deg 0% 100% / 5%);
+ clip-path: polygon(34% 0, 100% 0, 66% 100%, 0 100%);
+ content: "";
+ inset: 0 18% 0 38%;
+ pointer-events: none;
+ position: absolute;
+ z-index: -1;
+}
+
+.homepage-announcement [hidden] {
+ display: none !important;
+}
+
+.homepage-announcement__inner {
+ align-items: center;
+ display: grid;
+ gap: 1.75rem;
+ grid-template-areas: "action copy countdown";
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ min-height: 9.25rem;
+ padding: 1.15rem 1.5rem;
+ width: 100%;
+}
+
+.homepage-announcement__copy {
+ grid-area: copy;
+ min-width: 0;
+}
+
+.homepage-announcement__eyebrow,
+.homepage-announcement__title,
+.homepage-announcement__countdown-label {
+ color: inherit;
+ margin: 0;
+}
+
+.homepage-announcement__eyebrow {
+ font-size: 0.68rem;
+ font-weight: var(--bulma-weight-semibold);
+ letter-spacing: 0.08em;
+ margin-bottom: 0.25rem;
+ text-transform: uppercase;
+}
+
+.homepage-announcement__title {
+ font-size: 1.35rem;
+ font-weight: var(--bulma-weight-bold);
+ line-height: 1.2;
+}
+
+.homepage-announcement__events {
+ display: flex;
+ flex-wrap: wrap;
+ font-size: 0.82rem;
+ gap: 0.2rem 0.65rem;
+ list-style: none;
+ margin: 0.35rem 0 0;
+ padding: 0;
+}
+
+.homepage-announcement__events li {
+ align-items: baseline;
+ display: flex;
+ white-space: nowrap;
+}
+
+.homepage-announcement__events a {
+ color: inherit;
+ display: flex;
+ gap: 0.3rem;
+ text-decoration-color: hsl(0deg 0% 100% / 45%);
+ text-decoration-line: underline;
+ text-underline-offset: 0.18em;
+}
+
+.homepage-announcement__events a:hover {
+ color: inherit;
+ text-decoration-color: currentColor;
+}
+
+.homepage-announcement__events a:focus-visible {
+ border-radius: 2px;
+ outline: 2px solid var(--bulma-white);
+ outline-offset: 3px;
+}
+
+.homepage-announcement__events li:not(:last-child)::after {
+ content: "·";
+ margin-left: 0.35rem;
+ opacity: 0.65;
+}
+
+.homepage-announcement__events time {
+ opacity: 0.78;
+}
+
+.homepage-announcement__action {
+ align-items: center;
+ background: hsl(0deg 0% 100% / 6%);
+ border: 2px solid hsl(0deg 0% 100% / 88%);
+ border-radius: var(--bulma-radius);
+ color: inherit;
+ display: inline-flex;
+ font-size: 0.9rem;
+ font-weight: var(--bulma-weight-semibold);
+ gap: 0.45rem;
+ grid-area: action;
+ justify-content: center;
+ min-height: 2.75rem;
+ padding: 0.55rem 0.9rem;
+ text-decoration: none;
+ transition: background-color 150ms ease, color 150ms ease;
+ white-space: nowrap;
+}
+
+.homepage-announcement__action:hover {
+ background: var(--bulma-white);
+ color: hsl(273deg 39% 29%);
+ text-decoration: none;
+}
+
+.homepage-announcement__action:focus-visible {
+ outline: 3px solid var(--bulma-white);
+ outline-offset: 3px;
+}
+
+.homepage-announcement__countdown {
+ grid-area: countdown;
+ min-width: 10.75rem;
+ text-align: center;
+}
+
+.homepage-announcement__countdown-label {
+ font-size: 0.62rem;
+ font-weight: var(--bulma-weight-semibold);
+ letter-spacing: 0.08em;
+ margin-bottom: 0.4rem;
+ opacity: 0.82;
+ text-transform: uppercase;
+}
+
+.homepage-announcement__countdown-units {
+ display: flex;
+ font-variant-numeric: tabular-nums;
+ gap: 0.9rem;
+ justify-content: center;
+}
+
+.homepage-announcement__countdown-units span {
+ min-width: 2.5rem;
+}
+
+.homepage-announcement__countdown-units strong,
+.homepage-announcement__countdown-units small {
+ color: inherit;
+ display: block;
+}
+
+.homepage-announcement__countdown-units strong {
+ font-size: 1.85rem;
+ line-height: 1;
+}
+
+.homepage-announcement__countdown-units small {
+ font-size: 0.56rem;
+ letter-spacing: 0.05em;
+ margin-top: 0.25rem;
+ opacity: 0.76;
+ text-transform: uppercase;
+}
+
+@media screen and (max-width: 768px) {
+ .homepage-announcement::before {
+ height: 30rem;
+ left: 78%;
+ opacity: 0.11;
+ width: 30rem;
+ }
+
+ .homepage-announcement::after {
+ inset-inline: 8% 42%;
+ }
+
+ .homepage-announcement__inner {
+ gap: 0.9rem;
+ grid-template-areas:
+ "copy"
+ "action"
+ "countdown";
+ grid-template-columns: minmax(0, 1fr);
+ justify-items: center;
+ min-height: 0;
+ padding-block: 1.35rem;
+ text-align: center;
+ }
+
+ .homepage-announcement__events {
+ justify-content: center;
+ }
+
+ .homepage-announcement__action {
+ min-height: 2.5rem;
+ padding-block: 0.4rem;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .homepage-announcement__action {
+ transition: none;
+ }
+}
diff --git a/src/announcements/announcement.js b/src/announcements/announcement.js
new file mode 100644
index 00000000..16243bc2
--- /dev/null
+++ b/src/announcements/announcement.js
@@ -0,0 +1,60 @@
+function initAnnouncementCountdowns() {
+ document.querySelectorAll("[data-announcement-countdown]").forEach((countdown) => {
+ const announcement = countdown.closest(".homepage-announcement") || countdown;
+ const location = countdown.querySelector("[data-announcement-countdown-location]");
+ const daysElement = countdown.querySelector("[data-announcement-countdown-days]");
+ const hoursElement = countdown.querySelector("[data-announcement-countdown-hours]");
+ const minutesElement = countdown.querySelector("[data-announcement-countdown-minutes]");
+ const description = countdown.querySelector("[data-announcement-countdown-description]");
+
+ if (
+ [location, daysElement, hoursElement, minutesElement, description].some(
+ (element) => !element,
+ )
+ ) {
+ return;
+ }
+
+ const events = Array.from(announcement.querySelectorAll("[data-announcement-event]"))
+ .map((event) => ({
+ location: event.dataset.eventLocation || "the next event",
+ start: Date.parse(event.dateTime),
+ }))
+ .filter((event) => Number.isFinite(event.start))
+ .sort((first, second) => first.start - second.start);
+ let interval;
+
+ const formatUnit = (value, unit) =>
+ `${value} ${unit}${value === 1 ? "" : "s"}`;
+
+ function updateCountdown() {
+ const now = Date.now();
+ const nextEvent = events.find((event) => event.start >= now);
+
+ if (!nextEvent) {
+ countdown.hidden = true;
+ if (interval) window.clearInterval(interval);
+ return;
+ }
+
+ const totalMinutes = Math.max(0, Math.floor((nextEvent.start - now) / 60000));
+ const days = Math.floor(totalMinutes / 1440);
+ const hours = Math.floor((totalMinutes % 1440) / 60);
+ const minutes = totalMinutes % 60;
+
+ location.textContent = nextEvent.location;
+ daysElement.textContent = days;
+ hoursElement.textContent = hours;
+ minutesElement.textContent = minutes;
+ description.textContent =
+ `${formatUnit(days, "day")}, ${formatUnit(hours, "hour")}, and ` +
+ `${formatUnit(minutes, "minute")} until the ${nextEvent.location} event.`;
+ countdown.hidden = false;
+ }
+
+ updateCountdown();
+ if (!countdown.hidden) interval = window.setInterval(updateCountdown, 60000);
+ });
+}
+
+initAnnouncementCountdowns();
diff --git a/src/announcements/traverses-europe-2026.template.html b/src/announcements/traverses-europe-2026.template.html
new file mode 100644
index 00000000..6aa30373
--- /dev/null
+++ b/src/announcements/traverses-europe-2026.template.html
@@ -0,0 +1,86 @@
+ Three meetups · October 2026
+ Typelevel Traverses Europe
+
+
+