diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 7d7a7147..7cec89f0 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,12 +1,15 @@
import type { Metadata } from "next";
+import type { CSSProperties } from "react";
import { Analytics } from "@vercel/analytics/react";
import "./globals.css";
+import { ApplyBar, APPLY_BAR_HEIGHT } from "@/components/layout/ApplyBar";
import Footer from "@/components/layout/Footer";
import MainContent from "@/components/layout/MainContent";
import { NavMenuProvider } from "@/components/layout/NavMenuContext";
import Navbar from "@/components/layout/Navbar";
import ScrollToTopButton from "@/components/layout/ScrollToTopButton";
import { Geist } from "next/font/google";
+import { APPLICATION_IS_LIVE } from "@constants/applications";
import { cn } from "@/lib/utils";
const geist = Geist({ subsets: ["latin"], variable: "--font-sans" });
@@ -23,9 +26,20 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
+ // When applications are live, the sticky ApplyBar occupies the top of the
+ // viewport; `--apply-banner-h` offsets the navbar (see Navbar) and pushes all
+ // page content down by exactly the bar's height.
+ const bannerStyle = APPLICATION_IS_LIVE
+ ? ({ "--apply-banner-h": APPLY_BAR_HEIGHT } as CSSProperties)
+ : undefined;
+
return (
-
+
+ {children}
diff --git a/src/components/layout/ApplyBar.tsx b/src/components/layout/ApplyBar.tsx
new file mode 100644
index 00000000..13411979
--- /dev/null
+++ b/src/components/layout/ApplyBar.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import Link from "next/link";
+
+import {
+ APPLICATION_CLOSE_DATETIME,
+ APPLICATION_IS_LIVE,
+ APPLICATION_LINK,
+} from "@constants/applications";
+
+/** Height of the bar. Kept in sync with `--apply-banner-h` set in the layout. */
+export const APPLY_BAR_HEIGHT = "3.5rem";
+
+/**
+ * Site-wide sticky announcement bar pinned above the navbar. Renders only while
+ * applications are open; the layout mirrors this with `--apply-banner-h` so the
+ * navbar and page content offset by exactly this height (0 when hidden).
+ */
+export function ApplyBar() {
+ if (!APPLICATION_IS_LIVE) return null;
+
+ const closeDate = APPLICATION_CLOSE_DATETIME.format("MMMM D, YYYY");
+
+ return (
+
+
+
+ Applications to join Blueprint next term are open, and close{" "}
+ {closeDate}.
+
+ Applications close {closeDate}.
+