diff --git a/components/ui/ButtonGroup.tsx b/components/ui/ButtonGroup.tsx
index 83e0642..60caac6 100644
--- a/components/ui/ButtonGroup.tsx
+++ b/components/ui/ButtonGroup.tsx
@@ -15,7 +15,8 @@ export default function ButtonGroup({ className, children, ...rest }: Props) {
*]:flex-1 [&>*]:rounded-none",
// keep focus rings visible inside the clipped container
From 9f97ce9666701b415623f9411d6cdf909dc41be1 Mon Sep 17 00:00:00 2001
From: Gilles <43683714+corp-0@users.noreply.github.com>
Date: Thu, 9 Jul 2026 13:50:12 -0400
Subject: [PATCH 2/5] fix: dead buttons on local mobile test
---
next.config.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/next.config.js b/next.config.js
index a365f31..cf9e4b0 100644
--- a/next.config.js
+++ b/next.config.js
@@ -2,6 +2,10 @@
module.exports = {
reactCompiler: true,
+ // Dev only: lets phones on the home network open the dev server via the
+ // machine's LAN IP. Without this Next blocks its /_next dev resources for
+ // non-localhost origins and pages render but never hydrate (dead buttons).
+ allowedDevOrigins: ["192.168.1.*"],
images: {
remotePatterns: [
{
From 0779b70f9176def080f2c56e40d69f13c9f89672 Mon Sep 17 00:00:00 2001
From: Gilles <43683714+corp-0@users.noreply.github.com>
Date: Thu, 9 Jul 2026 13:50:31 -0400
Subject: [PATCH 3/5] chore: new test for responsiveness
---
cypress/e2e/responsive.cy.ts | 123 +++++++++++++++++++++++++++++++++++
1 file changed, 123 insertions(+)
create mode 100644 cypress/e2e/responsive.cy.ts
diff --git a/cypress/e2e/responsive.cy.ts b/cypress/e2e/responsive.cy.ts
new file mode 100644
index 0000000..fe2d92e
--- /dev/null
+++ b/cypress/e2e/responsive.cy.ts
@@ -0,0 +1,123 @@
+/*
+ * Test if any route has unintended horizontal overflow at most common screens
+ */
+const WIDTHS = [320, 393, 640, 768, 1024];
+
+type Offender = { tag: string; cls: string; left: number; right: number };
+
+/** Elements sticking out horizontally at the current viewport width. */
+function findOverflowingElements(win: Window): Offender[] {
+ const doc = win.document;
+ const vw = doc.documentElement.clientWidth;
+ const offenders: Offender[] = [];
+
+ for (const el of Array.from(doc.querySelectorAll("body *"))) {
+ const rect = el.getBoundingClientRect();
+ if (rect.width === 0 && rect.height === 0) continue;
+ if (rect.right <= vw + 1 && rect.left >= -1) continue;
+
+ // Fixed overlays (parallax space, the clown's cage) clip themselves.
+ if (win.getComputedStyle(el).position === "fixed") continue;
+
+ // Skip elements an ancestor already clips or scrolls horizontally
+ let ancestor = el.parentElement;
+ let clipped = false;
+ while (ancestor && ancestor !== doc.body) {
+ if (/(hidden|clip|auto|scroll)/.test(win.getComputedStyle(ancestor).overflowX)) {
+ clipped = true;
+ break;
+ }
+ ancestor = ancestor.parentElement;
+ }
+ if (clipped) continue;
+
+ const cls = typeof el.className === "string" ? el.className : "";
+ offenders.push({
+ tag: el.tagName.toLowerCase(),
+ cls: cls.slice(0, 100),
+ left: Math.round(rect.left),
+ right: Math.round(rect.right),
+ });
+ }
+ return offenders;
+}
+
+function assertNoHorizontalOverflow(route: string) {
+ for (const width of WIDTHS) {
+ cy.viewport(width, 850);
+ // let the resize propagate and media queries re-evaluate
+ cy.wait(100);
+ cy.window().then((win) => {
+ const offenders = findOverflowingElements(win);
+ expect(
+ offenders,
+ `${route} @ ${width}px elements past the viewport edge:\n` +
+ JSON.stringify(offenders, null, 2),
+ ).to.have.length(0);
+ expect(
+ win.document.documentElement.scrollWidth,
+ `${route} @ ${width}px page scroll width`,
+ ).to.be.at.most(width + 1);
+ });
+ }
+}
+
+describe("Responsive layout", () => {
+ it("home has no horizontal overflow at any width", () => {
+ cy.visit("/");
+ cy.contains("h1", "Unitystation!").should("be.visible");
+ assertNoHorizontalOverflow("/");
+ });
+
+ it("download has no horizontal overflow at any width", () => {
+ cy.visit("/download");
+ cy.contains("h1", "Download Pudu Launcher").should("be.visible");
+ assertNoHorizontalOverflow("/download");
+ });
+
+ it("blog has no horizontal overflow at any width", () => {
+ cy.intercept("GET", /changelog\.unitystation\.org\/posts\/\?page=1$/, {
+ fixture: "blogPosts-page1.json",
+ }).as("postsPage1");
+ cy.intercept("GET", /changelog\.unitystation\.org\/posts\/\?page=2$/, {
+ fixture: "blogPosts-page2.json",
+ }).as("postsPage2");
+
+ cy.visit("/blog");
+ cy.wait("@postsPage1");
+ // materialise the infinite-scroll page before scanning
+ cy.scrollTo("bottom");
+ cy.wait("@postsPage2");
+ assertNoHorizontalOverflow("/blog");
+ });
+
+ it("changelog has no horizontal overflow at any width", () => {
+ cy.intercept("GET", /changelog\.unitystation\.org\/all-changes/, {
+ fixture: "changelog-page1.json",
+ }).as("changelog");
+
+ cy.visit("/changelog");
+ cy.wait("@changelog");
+ assertNoHorizontalOverflow("/changelog");
+ });
+
+ it("ledger has no horizontal overflow at any width", () => {
+ cy.intercept("GET", /ledger\.unitystation\.org\/movements\/$/, {
+ fixture: "ledger-page1.json",
+ }).as("ledgerPage1");
+
+ cy.visit("/ledger");
+ cy.wait("@ledgerPage1");
+ assertNoHorizontalOverflow("/ledger");
+ });
+
+ for (const route of ["/login", "/register", "/reset-password"]) {
+ it(`${route} has no horizontal overflow at any width`, () => {
+ cy.visit(route);
+ cy.get("h1").should("be.visible");
+ assertNoHorizontalOverflow(route);
+ });
+ }
+});
+
+export {};
From 92eb833c1e1548ac40ee7e194f59b0a29d4a15d3 Mon Sep 17 00:00:00 2001
From: Gilles <43683714+corp-0@users.noreply.github.com>
Date: Thu, 9 Jul 2026 13:51:16 -0400
Subject: [PATCH 4/5] fix: more mobile only fixes
---
components/home/Hero.tsx | 3 +--
components/home/RotatingTagline.tsx | 2 +-
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/components/home/Hero.tsx b/components/home/Hero.tsx
index 89a89f8..e368278 100644
--- a/components/home/Hero.tsx
+++ b/components/home/Hero.tsx
@@ -40,8 +40,7 @@ export default function Hero() {
diff --git a/components/home/RotatingTagline.tsx b/components/home/RotatingTagline.tsx
index 429bab2..985f45c 100644
--- a/components/home/RotatingTagline.tsx
+++ b/components/home/RotatingTagline.tsx
@@ -32,7 +32,7 @@ export default function RotatingTagline() {
}, []);
return (
-
);
From 616bf6e54988e7cb32f12d4ed9e5297ca0829a7e Mon Sep 17 00:00:00 2001
From: Gilles <43683714+corp-0@users.noreply.github.com>
Date: Thu, 9 Jul 2026 14:11:25 -0400
Subject: [PATCH 5/5] fix: test on ci failing
---
cypress/e2e/responsive.cy.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/cypress/e2e/responsive.cy.ts b/cypress/e2e/responsive.cy.ts
index fe2d92e..9e8e793 100644
--- a/cypress/e2e/responsive.cy.ts
+++ b/cypress/e2e/responsive.cy.ts
@@ -85,6 +85,7 @@ describe("Responsive layout", () => {
cy.visit("/blog");
cy.wait("@postsPage1");
+ cy.contains("h2", "Station reactor now explodes properly").should("be.visible");
// materialise the infinite-scroll page before scanning
cy.scrollTo("bottom");
cy.wait("@postsPage2");