diff --git a/app/globals.css b/app/globals.css
index ecb8987..7e191e6 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -169,6 +169,20 @@ body {
transform: translateY(-14px) scale(1.02);
}
+/* Mobile: the pinned scene is replaced by a stacked list, so the tile sizes to
+ the screen instead of being scaled down. Height follows content — the icon is
+ smaller here, so a fixed 560px would leave a big empty gap. */
+@media (max-width: 767px) {
+ .app-card {
+ width: min(320px, 86vw);
+ height: auto;
+ min-height: 420px;
+ padding: 40px 24px 32px;
+ /* the Reveal wrapper is full-width, so centre the tile inside it */
+ margin-inline: auto;
+ }
+}
+
@media (prefers-reduced-motion: reduce) {
.app-card {
transition: none;
@@ -210,6 +224,15 @@ body {
background: color-mix(in oklab, var(--acc) 18%, rgba(255, 255, 255, 0.02));
}
+/* Mobile shows chips at 1:1 (no scene scaling), so nudge them up to a
+ comfortable reading size. */
+@media (max-width: 767px) {
+ .cap-chip {
+ font-size: 12.5px;
+ padding: 7px 13px;
+ }
+}
+
@media (prefers-reduced-motion: reduce) {
.cap-caret {
animation: none;
diff --git a/components/sections/AppCards.tsx b/components/sections/AppCards.tsx
index 7526b77..c80a79e 100644
--- a/components/sections/AppCards.tsx
+++ b/components/sections/AppCards.tsx
@@ -2,6 +2,8 @@
import { useEffect, useRef } from "react";
import { useTranslations } from "next-intl";
+import { useIsMobile } from "@/lib/useIsMobile";
+import Reveal from "@/components/Reveal";
/** Per-card layout + scroll-entrance constants (from the source scene). */
const CFG = [
@@ -97,11 +99,68 @@ function AppIcon({
return
;
}
+/** The card face itself — identical markup in both layouts, so the mobile
+ * fallback keeps the exact look, just at a size that stays readable. */
+function CardFace({
+ card,
+ copy,
+ iconSize,
+}: {
+ card: (typeof CARDS)[number];
+ copy: { title: string; sub: string } | undefined;
+ iconSize: number;
+}) {
+ return (
+
+
+ {copy?.sub}
+
+
+ {copy?.title}
+
+
+
+ );
+}
+
export default function AppCards() {
const t = useTranslations("AppCards");
const cards = t.raw("cards") as { title: string; sub: string }[];
const word1 = t("word1");
const word2 = t("word2");
+ const isMobile = useIsMobile();
const sceneRef = useRef(null);
const w1Ref = useRef(null);
@@ -221,6 +280,27 @@ export default function AppCards() {
whiteSpace: "nowrap",
};
+ // ── Mobile: the pinned scene scales everything to ~26%, which drops body
+ // copy to ~5px. Fall back to a plain stacked list at full type size.
+ if (isMobile) {
+ return (
+
+
+
+ // {word1} {word2}
+
+
+
+ {CARDS.map((card, i) => (
+
+
+
+ ))}
+
+
+ );
+ }
+
return (
-
-
- {cards[i]?.sub}
-
-
- {cards[i]?.title}
-
-
-
+
))}
diff --git a/components/sections/CapabilitiesScene.tsx b/components/sections/CapabilitiesScene.tsx
index cd89cf6..5125da3 100644
--- a/components/sections/CapabilitiesScene.tsx
+++ b/components/sections/CapabilitiesScene.tsx
@@ -2,6 +2,8 @@
import { useEffect, useRef } from "react";
import { useTranslations } from "next-intl";
+import { useIsMobile } from "@/lib/useIsMobile";
+import Reveal from "@/components/Reveal";
type Group = { no: string; title: string; items: string[]; tools: string[] };
@@ -35,6 +37,7 @@ export default function CapabilitiesScene() {
const label = t("label");
const title = t("title");
const lead = t("lead");
+ const isMobile = useIsMobile();
const rootRef = useRef(null);
const spotRef = useRef(null);
@@ -52,7 +55,7 @@ export default function CapabilitiesScene() {
useEffect(() => {
const root = rootRef.current;
- if (!root) return;
+ if (!root || isMobile) return;
const mouse = { x: -1, y: -1 };
const spotPos = { x: 0, y: 0 };
@@ -329,7 +332,62 @@ export default function CapabilitiesScene() {
document.removeEventListener("visibilitychange", onVis);
window.removeEventListener("mousemove", onMouse);
};
- }, [groups.length]);
+ }, [groups.length, isMobile]);
+
+ // ── Mobile: the pinned scene scales the 1280px grid down to ~58%, dropping
+ // list text to 9px and chips to 7px. Fall back to a single column at full size.
+ if (isMobile) {
+ return (
+
+
+
+ // {label}
+
+
+ {title}
+
+ {lead}
+
+
+
+ {groups.slice(0, 4).map((group, c) => (
+
+
+
+
+ {group.no}
+
+
{group.title}
+
+
+ {group.items.map((item) => (
+ -
+
+ {item}
+
+ ))}
+
+
+ {group.tools.map((tool) => (
+ -
+ {tool}
+
+ ))}
+
+
+
+ ))}
+
+
+ );
+ }
return (
{
+ const mq = window.matchMedia(MOBILE_QUERY);
+ const update = () => setIsMobile(mq.matches);
+ update();
+ mq.addEventListener("change", update);
+ return () => mq.removeEventListener("change", update);
+ }, []);
+
+ return isMobile;
+}