diff --git a/apps/web/package.json b/apps/web/package.json
index 8f5f460a..81446c82 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -17,9 +17,12 @@
"dependencies": {
"@gonext/blocks-core": "workspace:*",
"@gonext/blocks-sdk": "workspace:*",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.469.0",
"next": "^15.0.0",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "tailwind-merge": "^2.5.5"
},
"devDependencies": {
"@gonext/test-config": "workspace:*",
@@ -29,9 +32,12 @@
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitest/coverage-v8": "^1.6.0",
+ "autoprefixer": "^10.4.20",
"eslint": "^8.57.0",
"eslint-config-next": "^15.0.0",
"jsdom": "^24.0.0",
+ "postcss": "^8.4.49",
+ "tailwindcss": "^3.4.17",
"typescript": "^5.6.0",
"vitest": "^1.6.0"
}
diff --git a/apps/web/postcss.config.mjs b/apps/web/postcss.config.mjs
new file mode 100644
index 00000000..1314e4a5
--- /dev/null
+++ b/apps/web/postcss.config.mjs
@@ -0,0 +1,16 @@
+/**
+ * PostCSS config — required for Next.js to pick up Tailwind v3.
+ *
+ * Next.js auto-detects `postcss.config.{js,mjs,cjs}` at the app root
+ * and runs the listed plugins over every imported `.css` file (we
+ * import `./globals.css` from the root layout). Without this file
+ * Next would fall through to its default postcss preset and silently
+ * skip the Tailwind processing pass, so the `@tailwind base/components/utilities`
+ * directives at the top of globals.css would never expand.
+ */
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/apps/web/src/app/PublicShell.test.tsx b/apps/web/src/app/PublicShell.test.tsx
index e77d12a8..47f6347a 100644
--- a/apps/web/src/app/PublicShell.test.tsx
+++ b/apps/web/src/app/PublicShell.test.tsx
@@ -16,6 +16,7 @@ describe('PublicShell', () => {
bodyHtml="HM"
cssCustomProperties=":root{--x:1}"
templateBasename="single.html"
+ withChrome={false}
/>,
);
expect(container.querySelector('header')).not.toBeNull();
@@ -29,6 +30,7 @@ describe('PublicShell', () => {
bodyHtml=""
cssCustomProperties=":root{--y:2}"
templateBasename="index.html"
+ withChrome={false}
/>,
);
const style = container.querySelector('style[data-gn-theme]');
@@ -41,9 +43,47 @@ describe('PublicShell', () => {
bodyHtml="
hi
"
cssCustomProperties=""
templateBasename="archive-book.tsx"
+ withChrome={false}
/>,
);
const site = container.querySelector('.gn-site');
expect(site?.getAttribute('data-gn-template')).toBe('archive-book.tsx');
});
+
+ it('renders the brand chrome (nav + footer) by default', () => {
+ const { container } = render(
+ ,
+ );
+ // The marketing nav uses a sticky pill on the forest surface;
+ // checking for the aria-label keeps the assertion forward-
+ // compatible with class-name changes.
+ expect(
+ container.querySelector('nav[aria-label="Primary"]'),
+ ).not.toBeNull();
+ // The footer is a real