Add splash intro, nuclear games + consulting, Cherenkov hero

- Splash: full-viewport animated SUCH logo cover with four corner nav
  buttons; reveals the site once "entered" (scroll-through or Explore) and
  does not resurrect on scroll-up. Original hero restored beneath it
  (title-anchored monitor pulse + Cherenkov-blue background).
- Logo: traveling Cherenkov glow ring + gentle letter breath (SVG/Framer,
  reduced-motion safe); beveled glass corner buttons.
- Services: new "Nuclear & Radiation Modeling" section + "Consulting"
  umbrella (payments, crypto, nuclear), MCNP Google Scholar link, and a
  not-a-licensed-PE clarification.
- About/founder bio: LLNL credibility + accessibility, art101.io link.
- Products: add Barns & Neutrons, Alpha Doc, Decay Theory (detail pages,
  OG cards, SVG icons); client-side sortable grid with coming-soon at the
  bottom. New Neroswap + Custom Websites icons (de-duplicated).
- SEO: game pages added to sitemap.
- Copy: dial back em-dashes to colons/commas/parens on home + services.
This commit is contained in:
jwinterm
2026-06-05 22:40:14 -04:00
parent 71dec78abe
commit ed07bb5681
26 changed files with 1234 additions and 140 deletions
+4
View File
@@ -56,3 +56,7 @@ test-results/
# AI / Assistant
.claude/
# Private strategy / internal notes (never publish)
STRATEGY.md
docs/internal/
@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<defs>
<filter id="ad-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="2" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="128" height="128" rx="22" fill="#171c26"/>
<!-- atom orbits in the four radiation colors -->
<g fill="none" stroke-width="2.5" transform="translate(64 52)">
<ellipse rx="40" ry="15" stroke="#ff6a00" opacity="0.85"/>
<ellipse rx="40" ry="15" stroke="#00a8ff" opacity="0.85" transform="rotate(60)"/>
<ellipse rx="40" ry="15" stroke="#9b30ff" opacity="0.85" transform="rotate(120)"/>
</g>
<!-- electrons -->
<circle cx="104" cy="52" r="4" fill="#ff6a00"/>
<circle cx="40" cy="24" r="4" fill="#00a8ff"/>
<circle cx="40" cy="80" r="4" fill="#9b30ff"/>
<circle cx="92" cy="78" r="4.5" fill="#39ff14"/>
<!-- nucleus -->
<circle cx="64" cy="52" r="9" fill="#eafff3" filter="url(#ad-glow)"/>
<!-- mortarboard (graduation cap) -->
<g transform="translate(64 100)">
<polygon points="0,-9 30,2 0,13 -30,2" fill="#d7e0ea"/>
<path d="M-17 6 v10 q17 9 34 0 V6" fill="#aeb9c7"/>
<line x1="30" y1="2" x2="30" y2="16" stroke="#39ff14" stroke-width="2"/>
<circle cx="30" cy="17" r="2.5" fill="#39ff14"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<defs>
<filter id="bn-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="2.2" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="128" height="128" rx="22" fill="#18202e"/>
<!-- faint nuclide-chart grid -->
<g stroke="#e6bf3f" stroke-width="1" opacity="0.12" fill="none">
<path d="M20 44h88M20 64h88M20 84h88M44 24v80M64 24v80M84 24v80"/>
</g>
<!-- amber orbit -->
<ellipse cx="60" cy="66" rx="40" ry="15" fill="none" stroke="#e6bf3f" stroke-width="2.5" opacity="0.55" transform="rotate(-18 60 66)"/>
<!-- nucleus: red protons + cream neutrons -->
<g>
<circle cx="54" cy="62" r="8" fill="#e0793c"/>
<circle cx="66" cy="60" r="8" fill="#ece3cf"/>
<circle cx="60" cy="70" r="8" fill="#e0793c"/>
<circle cx="70" cy="70" r="8" fill="#ece3cf"/>
<circle cx="58" cy="56" r="7" fill="#ece3cf"/>
<circle cx="68" cy="64" r="7" fill="#e0793c"/>
</g>
<!-- incoming neutron (radioactive green) with streak -->
<line x1="120" y1="36" x2="92" y2="52" stroke="#93d84f" stroke-width="2.5" stroke-linecap="round" opacity="0.6"/>
<circle cx="90" cy="53" r="7" fill="#93d84f" filter="url(#bn-glow)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<rect width="128" height="128" rx="22" fill="#1e1b2e"/>
<rect x="22" y="30" width="84" height="68" rx="9" fill="none" stroke="#a78bfa" stroke-width="4"/>
<line x1="22" y1="48" x2="106" y2="48" stroke="#a78bfa" stroke-width="4"/>
<circle cx="33" cy="39" r="2.6" fill="#a78bfa"/>
<circle cx="42" cy="39" r="2.6" fill="#a78bfa"/>
<circle cx="51" cy="39" r="2.6" fill="#a78bfa"/>
<path d="M54 62 L40 74 L54 86" fill="none" stroke="#c4b5fd" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M74 62 L88 74 L74 86" fill="none" stroke="#c4b5fd" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 743 B

@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<defs>
<linearGradient id="dt-cat" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#00e5ff"/>
<stop offset="1" stop-color="#1565c0"/>
</linearGradient>
<filter id="dt-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="2.4" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="128" height="128" rx="22" fill="#0d1b1f"/>
<!-- quantum cat -->
<g filter="url(#dt-glow)">
<!-- ears -->
<polygon points="34,40 44,16 56,38" fill="url(#dt-cat)"/>
<polygon points="94,40 84,16 72,38" fill="url(#dt-cat)"/>
<!-- head -->
<circle cx="64" cy="62" r="30" fill="url(#dt-cat)"/>
</g>
<!-- eyes + nose -->
<circle cx="53" cy="58" r="4" fill="#06222b"/>
<circle cx="75" cy="58" r="4" fill="#06222b"/>
<path d="M60 70 h8 l-4 5 z" fill="#06222b"/>
<!-- whiskers -->
<g stroke="#bdf6ff" stroke-width="1.5" opacity="0.7" stroke-linecap="round">
<path d="M78 66 h16M78 71 h14M50 66 h-16M50 71 h-14"/>
</g>
<!-- decay particles drifting off (dissolving) -->
<g fill="#7df9ff">
<circle cx="96" cy="30" r="3" opacity="0.9"/>
<circle cx="106" cy="22" r="2.2" opacity="0.6"/>
<circle cx="114" cy="16" r="1.6" opacity="0.35"/>
<circle cx="90" cy="96" r="2.6" opacity="0.7"/>
<circle cx="100" cy="104" r="1.8" opacity="0.4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<rect width="128" height="128" rx="22" fill="#16a34a"/>
<g transform="scale(2)">
<path fill="#0b0b0c" d="M18 16h8v18l12-18h8v32h-8V30L26 48h-8z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 257 B

+1 -1
View File
@@ -32,7 +32,7 @@ export default function AboutPage() {
<div className="prose dark:prose-invert max-w-none mb-12">
<p className="text-xl leading-relaxed">
Such Software is a software studio in Kennett Square, PA. Founded by <a href="https://jwinterm.com">John Winter Murphy</a>, we build accessible e-commerce websites, crypto infrastructure, and original mobile apps and games.
Such Software is a software studio in Kennett Square, PA, founded by <a href="https://jwinterm.com">John Winter Murphy</a>, whose background is in nuclear and radiation science: a radiation-detector PhD and six years at Lawrence Livermore National Laboratory. We build accessible e-commerce websites, crypto infrastructure, and original mobile apps and games (including a series of nuclear-themed games), and offer nuclear and radiation modeling consulting.
</p>
</div>
+69 -44
View File
@@ -7,60 +7,89 @@ import { ServicesPreview } from "@/components/landing/services-preview";
import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav";
import { DynamicBackground } from "@/components/landing/dynamic-background";
import { HeroSplash } from "@/components/landing/hero-splash";
import { ScrollReveal } from "@/components/scroll-reveal";
import Link from "next/link";
import { Button } from "@repo/ui/components/button";
import { useRef, useState, useEffect } from "react";
import Link from "next/link";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
// Avoid the "useLayoutEffect does nothing on the server" warning during SSR.
const useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
export default function Home() {
// The splash owns the first screen. Once the visitor scrolls through it (or hits
// "Explore"), we "enter" the site: the splash unmounts for good and the real hero,
// header, bottom nav, and background take over. Scrolling back up lands on the hero,
// never the splash again.
const [entered, setEntered] = useState(false);
const splashRef = useRef<HTMLElement>(null);
const restoreScroll = useRef(0);
// Deliberately place the nebula/monitor just to the left of the hero title, at any
// screen width (restored from the original hero).
const titleRef = useRef<HTMLHeadingElement>(null);
const [nebulaPosition, setNebulaPosition] = useState<{ x: number; y: number } | null>(null);
// Dismiss the splash once it has fully scrolled out of view.
useEffect(() => {
if (entered) return;
const el = splashRef.current;
if (!el) return;
const io = new IntersectionObserver(
([entry]) => {
if (!entry.isIntersecting) {
restoreScroll.current = Math.max(0, window.scrollY - el.offsetHeight);
setEntered(true);
}
},
{ threshold: 0 },
);
io.observe(el);
return () => io.disconnect();
}, [entered]);
// After the splash unmounts, keep the visitor's position in the content stable
// (no jump). Runs before paint.
useIsoLayoutEffect(() => {
if (entered) window.scrollTo(0, restoreScroll.current);
}, [entered]);
// Compute the nebula position from the title. Recompute once entered (the title is
// now in its final on-screen position) and on resize / font load.
useEffect(() => {
const updatePosition = () => {
if (titleRef.current) {
if (!titleRef.current) return;
const rect = titleRef.current.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const titleLeftPercent = (rect.left / window.innerWidth) * 100;
const titleTopPercent = (rect.top / window.innerHeight) * 100;
setNebulaPosition({
x: Math.max(5, titleLeftPercent - 8),
y: Math.max(10, titleTopPercent - 2),
});
};
updatePosition();
window.addEventListener("resize", updatePosition);
document.fonts?.ready.then(updatePosition);
return () => window.removeEventListener("resize", updatePosition);
}, [entered]);
// Position nebula to the left of the title with a comfortable margin
// Convert pixel position to percentage of viewport
const titleLeftPercent = (rect.left / viewportWidth) * 100;
const titleTopPercent = (rect.top / viewportHeight) * 100;
// Place the nebula 8% to the left of where the title starts
// and vertically aligned with the title's top area
const nebulaX = Math.max(5, titleLeftPercent - 8);
const nebulaY = Math.max(10, titleTopPercent - 2);
setNebulaPosition({ x: nebulaX, y: nebulaY });
}
const enterFromButton = () => {
restoreScroll.current = 0;
setEntered(true);
};
updatePosition();
// Update on resize
window.addEventListener('resize', updatePosition);
// Also update after fonts load (can affect title width)
document.fonts?.ready.then(updatePosition);
return () => window.removeEventListener('resize', updatePosition);
}, []);
return (
<main className="relative flex flex-col items-center justify-between overflow-hidden">
<DynamicBackground nebulaPosition={nebulaPosition} />
<Header />
<main className="relative flex flex-col items-center overflow-hidden">
<DynamicBackground nebulaPosition={nebulaPosition} visible={entered} />
<Header floating visible={entered} />
<div id="main-content" className="z-10 w-full max-w-7xl mx-auto flex flex-col items-center pt-20 pb-10 px-4 text-center">
{!entered && <HeroSplash sectionRef={splashRef} onEnter={enterFromButton} />}
<div id="main-content" className="z-10 w-full max-w-7xl mx-auto flex flex-col items-center pt-32 pb-10 px-4 text-center scroll-mt-28">
<h1
ref={titleRef}
className="text-4xl sm:text-5xl md:text-7xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-emerald-500 pb-2"
style={{
// Calculated max-width to prevent nebula overlap (only matters on larger screens)
maxWidth: 'min(calc(90vw - 120px), 100%)',
}}
className="text-4xl sm:text-5xl md:text-7xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-cyan-400 to-blue-600 pb-2"
style={{ maxWidth: "min(calc(90vw - 120px), 100%)" }}
>
<span className="block">Precision Engineering</span>
<span className="block">for Everyone</span>
@@ -71,14 +100,10 @@ export default function Home() {
<div className="flex gap-4 mb-10">
<Link href="/products">
<Button size="lg" className="rounded-full">
View Portfolio
</Button>
<Button size="lg" className="rounded-full">View Portfolio</Button>
</Link>
<Link href="#contact">
<Button variant="outline" size="lg" className="rounded-full">
Start a Project
</Button>
<Button variant="outline" size="lg" className="rounded-full">Start a Project</Button>
</Link>
</div>
@@ -115,7 +140,7 @@ export default function Home() {
<ContactForm />
</ScrollReveal>
</div>
<MobileNav />
<MobileNav visible={entered} />
</main>
);
}
@@ -0,0 +1,13 @@
import { gameOgImage, OG_SIZE, OG_CONTENT_TYPE } from "@/lib/og";
export const alt = "Alpha Doc — an academic-horror roguelike";
export const size = OG_SIZE;
export const contentType = OG_CONTENT_TYPE;
export default function Image() {
return gameOgImage({
title: "Alpha Doc",
tagline: "A top-down roguelike where you're the disaster. Master four kinds of radiation.",
accent: "#fb923c",
});
}
@@ -0,0 +1,140 @@
import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav";
import { Card } from "@repo/ui/components/card";
import Link from "next/link";
import Image from "next/image";
import { JsonLd, breadcrumbLd, faqLd } from "@/components/seo/json-ld";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Alpha Doc: An Academic-Horror Roguelike | Such Software",
description: "Alpha Doc is a top-down roguelike where you're the irradiated PhD student. Master four kinds of radiation and descend to your thesis defense. In development.",
alternates: { canonical: "/products/alpha-doc" },
keywords: [
"roguelike",
"academic horror game",
"radiation game",
"indie roguelike",
"physics game",
"alpha doc",
],
openGraph: {
title: "Alpha Doc: An Academic-Horror Roguelike",
description: "A top-down roguelike where you're the disaster. Master four kinds of radiation and decide if the truth matters more than your PhD. In development.",
type: "website",
url: "/products/alpha-doc",
},
};
const faqs = [
{ q: "What is Alpha Doc?", a: "A top-down roguelike set in a physics research facility warped by a containment breach. You play a doctoral candidate who caused the disaster the same night as their thesis defense — now irradiated, you descend through five department wings to face the Committee." },
{ q: "What are the four radiation types?", a: "Alpha, Beta, Gamma, and Neutron, each a distinct playstyle. Neutron is the standout: it's not a weapon but an environmental transmutation tool — you reshape the map itself, destroying cover and creating hazards before a fight even starts." },
{ q: "Is there a morality system?", a: "Yes, but it's never an explicit choice. The aggressive play that wins fights also destroys the evidence of your culpability. Your standing is inferred from the world and reflected in four different endings — you can't quite feel good about winning." },
{ q: "How will it be sold?", a: "Modern shareware: a free, complete first sector (the Grant Office), then a single one-time purchase to unlock the rest. No ads, no microtransactions, no energy timers." },
{ q: "When is it out?", a: "It's in active development — a playable vertical slice exists. Follow Such Software for release news on Steam, itch.io, iOS, and Android." },
];
const jsonLd = {
"@context": "https://schema.org",
"@type": "VideoGame",
name: "Alpha Doc",
url: "https://such.software/products/alpha-doc",
description:
"A top-down academic-horror roguelike. Descend through a warped physics facility, master four types of radiation, and decide if the truth matters more than your PhD.",
applicationCategory: "Game",
genre: ["Roguelike", "Action", "Indie"],
gamePlatform: ["PC", "iOS", "Android"],
author: { "@type": "Organization", name: "Such Software" },
publisher: { "@type": "Organization", name: "Such Software" },
};
const features = [
{ title: "Four-element combat", body: "Alpha, Beta, Gamma, and Neutron are four real playstyles, not reskins. Each rewards a different read of the room." },
{ title: "Neutron reshapes the map", body: "The Neutron element transmutes the environment — destroy cover, open paths, arm dormant hazards. You change the battlefield before the battle." },
{ title: "Instability as a dial", body: "Push instability for harder-hitting shots at a slower fire rate, or dial it back and let it decay. Tension, not a punishment cliff." },
{ title: "Emergent morality", body: "No morality meter. Burning evidence for power quietly shifts dialogue, companion reactions, and which of four endings you reach." },
{ title: "Academic horror", body: "Your enemies are colleagues with real grievances — the reviewer you ignored, the safety inspector you overruled, the adjuncts caught in the blast." },
{ title: "Respectful premium design", body: "Free first sector, one-time unlock, no ads or timers. The systems are built for a complete experience, not a monetization funnel." },
];
export default function AlphaDocPage() {
return (
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground">
<JsonLd data={breadcrumbLd([
{ name: "Home", path: "/" },
{ name: "Products", path: "/products" },
{ name: "Alpha Doc", path: "/products/alpha-doc" },
])} />
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<JsonLd data={faqLd(faqs)} />
<Header />
<div id="main-content" className="z-10 w-full max-w-4xl mx-auto py-20 px-4 pb-24 md:pb-20">
<Link href="/products" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">
← Back to Products
</Link>
<div className="flex items-center gap-4 mb-2">
<Image src="/images/products/alpha-doc.svg" alt="Alpha Doc icon" width={64} height={64} className="rounded-2xl" />
<div>
<span className="inline-flex items-center rounded-full bg-orange-400/15 px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.2em] text-orange-600 dark:text-orange-400 mb-1">
In Development
</span>
<h1 className="text-4xl md:text-5xl font-bold text-orange-600 dark:text-orange-400 leading-none">
Alpha Doc
</h1>
</div>
</div>
<p className="text-base text-muted-foreground mb-6">A top-down roguelike where you&rsquo;re the disaster.</p>
<div className="prose dark:prose-invert max-w-none mb-12">
<p className="text-xl text-muted-foreground leading-relaxed">
It is 2:07 AM. A fifth-year PhD student has just caused a catastrophic accelerator overload,
irradiating the building and everyone in it — and the thesis defense is at 8:00 AM. Descend
through five warped department wings, master four types of radiation, and decide whether to
bury the evidence or face the Committee. Alpha Doc is a systems-driven roguelike with dark
academic teeth.
</p>
</div>
<h2 className="text-2xl font-bold mb-6">What makes it different</h2>
<div className="grid md:grid-cols-2 gap-6 mb-12">
{features.map((f) => (
<Card key={f.title} className="glass-card border-orange-500/20">
<h3 className="text-lg font-bold mb-2">{f.title}</h3>
<p className="text-sm text-muted-foreground">{f.body}</p>
</Card>
))}
</div>
<h2 className="text-2xl font-bold mb-6">Under the Hood</h2>
<div className="section-container border-l-4 border-orange-500 mb-12">
<ul className="space-y-2 text-muted-foreground">
<li><strong>Engine:</strong> Godot 4 (.NET / C#) with a fixed 3/4 orthographic camera.</li>
<li><strong>Procedural generation:</strong> BSP rooms and constraint-solved layouts for endless runs.</li>
<li><strong>Structure:</strong> five sectors, four bosses plus a multi-phase thesis defense, and four endings.</li>
<li><strong>Platforms:</strong> Steam and itch.io on desktop, plus iOS and Android.</li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-6">FAQ</h2>
<div className="space-y-6 mb-12">
{faqs.map((f) => (
<div key={f.q}>
<h3 className="font-bold mb-1 text-orange-600 dark:text-orange-400">{f.q}</h3>
<p className="text-muted-foreground">{f.a}</p>
</div>
))}
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground flex-wrap">
<Link href="/products" className="hover:text-foreground transition-colors">All Products</Link>
<span>•</span>
<Link href="/support" className="hover:text-foreground transition-colors">Get notified / Feedback</Link>
<span>•</span>
<span>Contact: apps@such.software</span>
</div>
</div>
<MobileNav />
</main>
);
}
@@ -0,0 +1,13 @@
import { gameOgImage, OG_SIZE, OG_CONTENT_TYPE } from "@/lib/og";
export const alt = "Barns & Neutrons — a game about the Table of Nuclides";
export const size = OG_SIZE;
export const contentType = OG_CONTENT_TYPE;
export default function Image() {
return gameOgImage({
title: "Barns & Neutrons",
tagline: "A cozy, honest expedition across the real Table of Nuclides.",
accent: "#facc15",
});
}
@@ -0,0 +1,148 @@
import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav";
import { Card } from "@repo/ui/components/card";
import Link from "next/link";
import Image from "next/image";
import { JsonLd, breadcrumbLd, faqLd } from "@/components/seo/json-ld";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Barns & Neutrons: A Game About the Table of Nuclides | Such Software",
description: "Barns & Neutrons is a cozy exploration-puzzle game across the real Table of Nuclides. Capture neutrons, coax decays, and learn honest, cited nuclear physics. In development.",
alternates: { canonical: "/products/barns-and-neutrons" },
keywords: [
"table of nuclides game",
"nuclear physics game",
"edutainment game",
"isotope game",
"neutron capture",
"radiochemistry game",
"barns and neutrons",
],
openGraph: {
title: "Barns & Neutrons: A Game About the Table of Nuclides",
description: "A cozy, honest expedition across the real Table of Nuclides — the anti-spreadsheet. In development by Such Software.",
type: "website",
url: "/products/barns-and-neutrons",
},
};
const faqs = [
{ q: "What is Barns & Neutrons?", a: "A cozy exploration-puzzle game set on the real Table of Nuclides — the grid of every known isotope. You start on a small island of known nuclides and each step into the dark is a small physics puzzle: capture a neutron, coax a decay. The reward is discovery, as a new nuclide lights up and its Compendium entry unfolds." },
{ q: "Is it educational?", a: "Yes, but through play, not lectures. Every animation reflects real physics, and discoveries unlock real-world context (Tc-99m in medical imaging, Am-241 in smoke detectors, C-14 in dating) along with half-lives, Q-values, and cornerstone papers. The data is grounded in open sources like ENDF, IAEA, and NIST." },
{ q: "What does “Barns” mean?", a: "A barn is the real unit of nuclear cross-section (10⁻²⁴ cm²) — a measure of how likely a nucleus is to interact with a neutron. The neutron-capture puzzles are literally about tuning energy to hit that cross-section." },
{ q: "What platforms will it be on?", a: "It is built in Godot for desktop (Windows, macOS, Linux) and mobile (iOS, Android)." },
{ q: "When is it out?", a: "It is in active development. The first playable slice (the Mo-99 → Tc-99m neighborhood) is the current milestone. Follow Such Software for release news." },
];
const jsonLd = {
"@context": "https://schema.org",
"@type": "VideoGame",
name: "Barns & Neutrons",
url: "https://such.software/products/barns-and-neutrons",
description:
"A cozy exploration-puzzle game across the real Table of Nuclides. Capture neutrons, coax decays, and learn honest, cited nuclear physics.",
applicationCategory: "Game",
genre: ["Puzzle", "Educational", "Simulation"],
gamePlatform: ["PC", "iOS", "Android"],
author: { "@type": "Organization", name: "Such Software" },
publisher: { "@type": "Organization", name: "Such Software" },
};
const features = [
{ title: "Discovery is the loop", body: "No linear tech tree. You explore a real, enormous map of ~3,300 nuclides. Completionism and pure curiosity pull you deeper, with no timers and no game-overs." },
{ title: "Honest physics", body: "When reality and a cuter lie compete, reality wins — then we make reality beautiful. Neutron scattering is forward-peaked at fast energies because it actually is." },
{ title: "Failure is serendipity", body: "Miss a neutron capture and the nucleus does what it really would — maybe a decay instead. You discover a different nuclide and learn real behavior. No punishment, only discovery." },
{ title: "The Compendium", body: "Every discovered isotope unlocks a card: real uses, half-lives, Q-values, and a link to a cornerstone paper (Fermi, Chadwick, Hahn & Strassmann). You learn the history, not just the facts." },
{ title: "A 3D tabletop, not a spreadsheet", body: "A tilted, extruded ceramic-tile board with a glowing valley of stability and dramatic discovery vignettes. Cozy, warm, and tactile." },
{ title: "Open core", body: "The physics, data, and code are open; the art, prose, and Compendium illustrations are ours. Facts aren't copyrightable, and we cite them anyway." },
];
export default function BarnsAndNeutronsPage() {
return (
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground">
<JsonLd data={breadcrumbLd([
{ name: "Home", path: "/" },
{ name: "Products", path: "/products" },
{ name: "Barns & Neutrons", path: "/products/barns-and-neutrons" },
])} />
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<JsonLd data={faqLd(faqs)} />
<Header />
<div id="main-content" className="z-10 w-full max-w-4xl mx-auto py-20 px-4 pb-24 md:pb-20">
<Link href="/products" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">
← Back to Products
</Link>
<div className="flex items-center gap-4 mb-2">
<Image src="/images/products/barns-and-neutrons.svg" alt="Barns & Neutrons icon" width={64} height={64} className="rounded-2xl" />
<div>
<span className="inline-flex items-center rounded-full bg-yellow-400/15 px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.2em] text-yellow-600 dark:text-yellow-400 mb-1">
In Development
</span>
<h1 className="text-4xl md:text-5xl font-bold text-yellow-600 dark:text-yellow-400 leading-none">
Barns &amp; Neutrons
</h1>
</div>
</div>
<p className="text-base text-muted-foreground mb-6">A cozy, honest expedition across the real Table of Nuclides — the anti-spreadsheet.</p>
<div className="prose dark:prose-invert max-w-none mb-12">
<p className="text-xl text-muted-foreground leading-relaxed">
Barns &amp; Neutrons is a calm puzzle game about the real Table of Nuclides. You begin on a
tiny lit island of known isotopes, and each step into the dark is a small, elegant physics
puzzle — capture a neutron, coax a decay. The reward is discovery: a new nuclide lights up
and its Compendium entry unfolds. It is our flagship edutainment title, and it shares its
DNA with our founder&rsquo;s open-source{" "}
<a href="https://jwinterm.github.io/geant4-radiation-labs/" target="_blank" rel="noopener noreferrer" className="text-yellow-600 dark:text-yellow-400 hover:underline">GEANT4 radiation labs</a>.
</p>
</div>
<h2 className="text-2xl font-bold mb-6">What makes it different</h2>
<div className="grid md:grid-cols-2 gap-6 mb-12">
{features.map((f) => (
<Card key={f.title} className="glass-card border-yellow-500/20">
<h3 className="text-lg font-bold mb-2">{f.title}</h3>
<p className="text-sm text-muted-foreground">{f.body}</p>
</Card>
))}
</div>
<h2 className="text-2xl font-bold mb-6">Under the Hood</h2>
<div className="section-container border-l-4 border-yellow-500 mb-12">
<ul className="space-y-2 text-muted-foreground">
<li><strong>Engine:</strong> Godot 4, one codebase for desktop and mobile.</li>
<li><strong>The board:</strong> all ~3,300 nuclides rendered in a single 3D instance, with a glowing valley of stability.</li>
<li><strong>Ground truth:</strong> nuclear data from ENDF/B-VIII.0, IAEA, and NIST, with clickable citations in-game.</li>
<li><strong>Honest model:</strong> real cross-sections, real decay modes, real moderation physics.</li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-6">FAQ</h2>
<div className="space-y-6 mb-12">
{faqs.map((f) => (
<div key={f.q}>
<h3 className="font-bold mb-1 text-yellow-600 dark:text-yellow-400">{f.q}</h3>
<p className="text-muted-foreground">{f.a}</p>
</div>
))}
</div>
<p className="text-muted-foreground mb-12">
Curious about the science behind it? See our{" "}
<Link href="/services" className="text-yellow-600 dark:text-yellow-400 hover:underline">nuclear &amp; radiation modeling</Link>
{" "}work.
</p>
<div className="flex items-center gap-4 text-sm text-muted-foreground flex-wrap">
<Link href="/products" className="hover:text-foreground transition-colors">All Products</Link>
<span>•</span>
<Link href="/support" className="hover:text-foreground transition-colors">Get notified / Feedback</Link>
<span>•</span>
<span>Contact: apps@such.software</span>
</div>
</div>
<MobileNav />
</main>
);
}
@@ -0,0 +1,13 @@
import { gameOgImage, OG_SIZE, OG_CONTENT_TYPE } from "@/lib/og";
export const alt = "Decay Theory — Erwin's Atomic Adventure";
export const size = OG_SIZE;
export const contentType = OG_CONTENT_TYPE;
export default function Image() {
return gameOgImage({
title: "Decay Theory",
tagline: "Watch the ground dissolve beneath your feet. Atom by atom.",
accent: "#22d3ee",
});
}
@@ -0,0 +1,140 @@
import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav";
import { Card } from "@repo/ui/components/card";
import Link from "next/link";
import Image from "next/image";
import { JsonLd, breadcrumbLd, faqLd } from "@/components/seo/json-ld";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Decay Theory: Erwin's Atomic Adventure | Such Software",
description: "Decay Theory is a side-scrolling platformer where the world decays atom by atom in real time. Play Erwin, a quantum cat, and learn real nuclear physics. In development.",
alternates: { canonical: "/products/decay-theory" },
keywords: [
"platformer",
"physics platformer",
"nuclear physics game",
"indie game",
"decay theory",
"Erwin atomic adventure",
],
openGraph: {
title: "Decay Theory: Erwin's Atomic Adventure",
description: "Watch the ground dissolve beneath your feet, atom by atom. A platformer that teaches real nuclear physics. In development.",
type: "website",
url: "/products/decay-theory",
},
};
const faqs = [
{ q: "What is Decay Theory?", a: "A 2D side-scrolling platformer where the entire world is made of individual atoms undergoing radioactive decay in real time. You don't see platforms vanish in bulk — you watch the ground dissolve atom by atom beneath your feet." },
{ q: "Who is Erwin?", a: "Erwin is a quantum-coherent cat — a nod to Schrödinger — whose body is itself a particle system. Health is 'quantum coherence,' shown as particle density: a healthy Erwin is a tight cloud; near death, a ghostly shimmer." },
{ q: "Does it actually teach physics?", a: "Yes, through mechanics rather than lectures. Decay modes, half-lives, decay chains, fission, and reactor control are things you feel. Isotopes use real half-lives and color coding." },
{ q: "How is it released?", a: "Episode 1, 'Ground State,' is a free vertical slice — no ads, no IAP. The full game (more zones and powers) unlocks via a single one-time purchase." },
{ q: "When is it out?", a: "Episode 1 is the current milestone, targeting iOS and Android first, with a desktop (Steam) port later. Follow Such Software for the launch." },
];
const jsonLd = {
"@context": "https://schema.org",
"@type": "VideoGame",
name: "Decay Theory: Erwin's Atomic Adventure",
alternateName: "Decay Theory",
url: "https://such.software/products/decay-theory",
description:
"A side-scrolling platformer where the world is simulated atom by atom, decaying in real time. Play Erwin, a quantum cat, and learn real nuclear physics.",
applicationCategory: "Game",
genre: ["Platformer", "Educational", "Indie"],
gamePlatform: ["iOS", "Android", "PC"],
author: { "@type": "Organization", name: "Such Software" },
publisher: { "@type": "Organization", name: "Such Software" },
};
const features = [
{ title: "The world decays as you watch", body: "Every tile is thousands of simulated atoms with a real decay probability. Platforms erode into swiss cheese and crumble under your feet — stochastic, beautiful, and deadly." },
{ title: "Quantum powers", body: "Observation collapses a wave function to stabilize ground; Time Rewind undoes a fatal step. Later powers — superposition, entanglement, tunneling — each teach a real concept." },
{ title: "Erwin, a living particle system", body: "Your cat is rendered as 2,000+ particles. Coherence is health: take damage and Erwin literally comes apart at the seams." },
{ title: "Real isotope data", body: "Isotopes carry authentic half-lives and decay chains, color-coded by family. The Geiger-counter audio tempo tracks the local decay rate." },
{ title: "Tight, skill-based platforming", body: "Precise tilt-and-touch controls, checkpoints, and speedrun-friendly level design. A real platformer, not a quiz with jumps." },
{ title: "Premium, not predatory", body: "Free Episode 1, one-time unlock for the rest. No ads, no loot boxes, no energy systems, no tracking." },
];
export default function DecayTheoryPage() {
return (
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground">
<JsonLd data={breadcrumbLd([
{ name: "Home", path: "/" },
{ name: "Products", path: "/products" },
{ name: "Decay Theory", path: "/products/decay-theory" },
])} />
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<JsonLd data={faqLd(faqs)} />
<Header />
<div id="main-content" className="z-10 w-full max-w-4xl mx-auto py-20 px-4 pb-24 md:pb-20">
<Link href="/products" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">
← Back to Products
</Link>
<div className="flex items-center gap-4 mb-2">
<Image src="/images/products/decay-theory.svg" alt="Decay Theory icon" width={64} height={64} className="rounded-2xl" />
<div>
<span className="inline-flex items-center rounded-full bg-cyan-400/15 px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.2em] text-cyan-600 dark:text-cyan-400 mb-1">
In Development
</span>
<h1 className="text-4xl md:text-5xl font-bold text-cyan-600 dark:text-cyan-400 leading-none">
Decay Theory
</h1>
</div>
</div>
<p className="text-base text-muted-foreground mb-6">Watch the ground dissolve beneath your feet. Atom by atom.</p>
<div className="prose dark:prose-invert max-w-none mb-12">
<p className="text-xl text-muted-foreground leading-relaxed">
Decay Theory is a hard-sci-fi platformer where the entire world is composed of individual
simulated atoms decaying in real time. You play Erwin, a quantum cat navigating a world that
is literally evaporating at the subatomic level — and you learn real nuclear physics by
feeling it, not by reading it.
</p>
</div>
<h2 className="text-2xl font-bold mb-6">What makes it different</h2>
<div className="grid md:grid-cols-2 gap-6 mb-12">
{features.map((f) => (
<Card key={f.title} className="glass-card border-cyan-500/20">
<h3 className="text-lg font-bold mb-2">{f.title}</h3>
<p className="text-sm text-muted-foreground">{f.body}</p>
</Card>
))}
</div>
<h2 className="text-2xl font-bold mb-6">Under the Hood</h2>
<div className="section-container border-l-4 border-cyan-500 mb-12">
<ul className="space-y-2 text-muted-foreground">
<li><strong>Architecture:</strong> Godot renders; a Rust core runs 100% of the atom simulation.</li>
<li><strong>Performance:</strong> tens of thousands of active, decaying atoms at a smooth frame rate on mid-range phones.</li>
<li><strong>Powers:</strong> quantum mechanics turned into verbs — observation, time rewind, and more.</li>
<li><strong>Platforms:</strong> iOS and Android first, desktop (Steam) to follow.</li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-6">FAQ</h2>
<div className="space-y-6 mb-12">
{faqs.map((f) => (
<div key={f.q}>
<h3 className="font-bold mb-1 text-cyan-600 dark:text-cyan-400">{f.q}</h3>
<p className="text-muted-foreground">{f.a}</p>
</div>
))}
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground flex-wrap">
<Link href="/products" className="hover:text-foreground transition-colors">All Products</Link>
<span>•</span>
<Link href="/support" className="hover:text-foreground transition-colors">Get notified / Feedback</Link>
<span>•</span>
<span>Contact: apps@such.software</span>
</div>
</div>
<MobileNav />
</main>
);
}
+47 -59
View File
@@ -1,36 +1,35 @@
import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav";
import { ScrollReveal } from "@/components/scroll-reveal";
import { Card } from "@repo/ui/components/card";
import Image from "next/image";
import Link from "next/link";
import { ProductsGrid, type Product } from "@/components/portfolio/products-grid";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Our Products & Apps: Crypto Tools, Games & Commerce | Such Software",
description: "Explore Such Software's products: Wownerogue Monero roguelike, Neroswap crypto aggregator, Smirk tipping wallet, headless Webshops, and our mobile games.",
title: "Our Products & Apps: Crypto Tools, Nuclear Games & Commerce | Such Software",
description: "Explore Such Software's products: Wownerogue Monero roguelike, Neroswap crypto aggregator, Smirk wallet, headless Webshops, our mobile games, and nuclear games in development.",
alternates: { canonical: "/products" },
openGraph: {
title: "Our Products & Apps: Crypto Tools, Games & Commerce | Such Software",
description: "Explore Such Software's products: Wownerogue Monero roguelike, Neroswap crypto aggregator, Smirk tipping wallet, headless Webshops, and our mobile games.",
title: "Our Products & Apps: Crypto Tools, Nuclear Games & Commerce | Such Software",
description: "Explore Such Software's products: Wownerogue Monero roguelike, Neroswap crypto aggregator, Smirk wallet, headless Webshops, our mobile games, and nuclear games in development.",
type: "website",
},
};
const products = [
const products: Product[] = [
{
slug: "webshops",
title: "Webshops",
description: "Custom online stores on revenue-share pricing. No upfront cost. Built on headless Medusa.",
color: "emerald",
icon: "/images/products/occupy-wallets.png",
releaseDate: "2025-01-01",
},
{
slug: "custom-websites",
title: "Custom Websites",
description: "Real-time web apps, multiplayer games, and live dashboards. Built on Next.js, Socket.io, and WebRTC.",
color: "purple",
icon: "/images/products/neroswap.png",
icon: "/images/products/custom-websites.svg",
releaseDate: "2025-01-01",
},
{
slug: "smirk-wallet",
@@ -38,6 +37,7 @@ const products = [
description: "Non-custodial browser wallet for social tipping. BTC, LTC, XMR, WOW, GRIN.",
color: "amber",
icon: "/images/products/smirk-wallet.png",
releaseDate: "2026-03-01",
},
{
slug: "bauhaus-echo",
@@ -45,6 +45,7 @@ const products = [
description: "Visual memory puzzle game inspired by Bauhaus design. Android, iOS, Web.",
color: "blue",
icon: "/images/products/bauhaus-echo.png",
releaseDate: "2026-04-01",
},
{
slug: "suchoice",
@@ -52,6 +53,7 @@ const products = [
description: "Stop overthinking decisions. Snap a photo, let AI read the options, spin the wheel. Plus coin flip and adventure mode. Android and iOS.",
color: "pink",
icon: "/images/products/suchoice.png",
releaseDate: "2026-04-01",
},
{
slug: "vegan-iq",
@@ -59,6 +61,7 @@ const products = [
description: "Test your plant-based knowledge with 1,000+ trivia questions across 8 categories, plus a daily challenge and weekly quiz. Android and iOS.",
color: "green",
icon: "/images/products/vegan-iq.png",
releaseDate: "2026-04-01",
},
{
slug: "such-moon-launch",
@@ -66,6 +69,7 @@ const products = [
description: "Vry rocket. Much landing. Wow! Wownero-themed pixel rocket arcade. 11 levels, 4 weapons, tilt-to-steer, endless mode. Android, iOS, Web, Desktop.",
color: "amber",
icon: "/images/products/such-moon-launch.png",
releaseDate: "2026-05-01",
},
{
slug: "wownerogue",
@@ -73,38 +77,45 @@ const products = [
description: "A provably-fair multiplayer roguelike synced to Monero and Wownero block times. Play free in the browser, escape the dungeon, earn on-chain payouts.",
color: "orange",
icon: "/images/products/privacy-labs.png",
releaseDate: "2026-06-01",
},
{
slug: "neroswap",
title: "Neroswap",
description: "Aggregated DEX & CEX orderbook data. Compare Monero, Wownero, and Bitcoin prices across 6 exchanges in real time.",
color: "yellow",
icon: "/images/products/neroswap.png",
icon: "/images/products/neroswap.svg",
releaseDate: "2026-03-01",
},
{
slug: "barns-and-neutrons",
title: "Barns & Neutrons",
description: "A cozy exploration-puzzle game across the real Table of Nuclides—the anti-spreadsheet. Capture neutrons, coax decays, and light up the chart while learning honest nuclear physics. Desktop and mobile.",
color: "yellow",
icon: "/images/products/barns-and-neutrons.svg",
releaseDate: null,
comingSoon: true,
},
{
slug: "alpha-doc",
title: "Alpha Doc",
description: "A top-down academic-horror roguelike. You're the irradiated PhD student—master four kinds of radiation and descend through a warped physics facility to your thesis defense. Desktop and mobile.",
color: "orange",
icon: "/images/products/alpha-doc.svg",
releaseDate: null,
comingSoon: true,
},
{
slug: "decay-theory",
title: "Decay Theory",
description: "A side-scrolling platformer where the world is simulated atom by atom, decaying in real time. Play Erwin, a quantum cat, and learn real nuclear physics. Episode 1 coming to iOS and Android.",
color: "cyan",
icon: "/images/products/decay-theory.svg",
releaseDate: null,
comingSoon: true,
},
];
const colorClasses: Record<string, string> = {
emerald: "border-emerald-500/30 hover:border-emerald-500/60",
purple: "border-purple-500/30 hover:border-purple-500/60",
amber: "border-amber-500/30 hover:border-amber-500/60",
blue: "border-blue-500/30 hover:border-blue-500/60",
pink: "border-pink-500/30 hover:border-pink-500/60",
orange: "border-orange-500/30 hover:border-orange-500/60",
yellow: "border-yellow-500/30 hover:border-yellow-500/60",
green: "border-green-500/30 hover:border-green-500/60",
};
const titleColors: Record<string, string> = {
emerald: "text-emerald-600 dark:text-emerald-400",
purple: "text-purple-600 dark:text-purple-400",
amber: "text-amber-600 dark:text-amber-400",
blue: "text-blue-600 dark:text-blue-400",
pink: "text-pink-600 dark:text-pink-400",
orange: "text-orange-600 dark:text-orange-400",
yellow: "text-yellow-600 dark:text-yellow-400",
green: "text-green-600 dark:text-green-400",
};
export default function ProductsPage() {
return (
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground">
@@ -115,35 +126,12 @@ export default function ProductsPage() {
Portfolio
</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Tools we have built, from commerce infrastructure to consumer apps.
Tools we have built, from commerce infrastructure to consumer apps—plus a trio of
nuclear games in development.
</p>
</div>
<div className="grid md:grid-cols-2 gap-8">
{products.map((product, i) => (
<ScrollReveal key={product.slug} delay={(i % 2) * 0.08}>
<Link href={`/products/${product.slug}`}>
<Card className={`glass-card h-full min-h-[200px] ${colorClasses[product.color]} transition-all cursor-pointer`}>
<div className="flex items-center gap-4 mb-3">
<Image
src={product.icon}
alt={`${product.title} icon`}
width={56}
height={56}
className="rounded-xl flex-shrink-0"
/>
<h2 className={`text-2xl font-bold ${titleColors[product.color]}`}>
{product.title}
</h2>
</div>
<p className="text-muted-foreground leading-relaxed">
{product.description}
</p>
</Card>
</Link>
</ScrollReveal>
))}
</div>
<ProductsGrid products={products} />
<div className="flex justify-center mt-16 motion-reduce:hidden">
<img
+106 -10
View File
@@ -10,7 +10,8 @@ import { JsonLd, faqLd } from "@/components/seo/json-ld";
import type { Metadata } from "next";
const faqs = [
{ q: "What does Such Software build?", a: "Custom e-commerce on Medusa v2, real-time web apps and multiplayer experiences, and cryptocurrency and Monero integrations and consulting. We work across the stack, from backend infrastructure to accessible front-ends." },
{ q: "What does Such Software build?", a: "Custom e-commerce on Medusa v2, real-time web apps and multiplayer experiences, cryptocurrency and Monero integrations, and PhD-led nuclear and radiation modeling. We work across the stack, from backend infrastructure to accessible front-ends." },
{ q: "Do you offer nuclear and radiation consulting?", a: "Yes, for select engagements. Our founder holds a PhD in radiation-detector materials and spent six years at Lawrence Livermore National Laboratory, with a specialty in MCNP Monte Carlo transport, plus shielding and spent-fuel/dry-cask work at Holtec. We take on Monte Carlo modeling (MCNP, GEANT4), shielding and dose calculations, detector design, and radiation-science training. Contact us for rates." },
{ q: "Where are you based?", a: "We are a software studio in Kennett Square, Pennsylvania, working with clients remotely across the US and beyond." },
{ q: "Do you build for accessibility?", a: "Yes. We build to WCAG 2.2 and audit for accessibility and Core Web Vitals as part of every project, not as an afterthought." },
{ q: "How does a project start?", a: "Reach out through the contact form with what you need. We scope it, propose an approach, and from there it can run as a fixed project or a performance-based revenue-share partnership." },
@@ -18,14 +19,14 @@ const faqs = [
];
export const metadata: Metadata = {
title: "Medusa, Web App & Crypto Development Services | Such Software",
description: "Custom e-commerce on Medusa v2, real-time web apps, and Monero/crypto consulting. A Kennett Square, PA studio building for scale, accessibility, and performance.",
title: "Web, Crypto & Nuclear Modeling Services | Such Software",
description: "Custom e-commerce on Medusa v2, real-time web apps, Monero/crypto consulting, and PhD-led nuclear & radiation modeling (MCNP, GEANT4). A Kennett Square, PA studio.",
alternates: {
canonical: "/services",
},
openGraph: {
title: "Medusa, Web App & Crypto Development Services | Such Software",
description: "Custom e-commerce on Medusa v2, real-time web apps, and Monero/crypto consulting. A Kennett Square, PA studio building for scale, accessibility, and performance.",
title: "Web, Crypto & Nuclear Modeling Services | Such Software",
description: "Custom e-commerce on Medusa v2, real-time web apps, Monero/crypto consulting, and PhD-led nuclear & radiation modeling (MCNP, GEANT4). A Kennett Square, PA studio.",
type: "website",
},
};
@@ -44,7 +45,8 @@ export default function ServicesPage() {
Specialized Solutions
</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Focused expertise in commerce, crypto, and custom web applications.
Focused expertise in commerce and custom web apps, plus consulting: from payments
and crypto to nuclear and radiation modeling.
</p>
</div>
@@ -73,8 +75,8 @@ export default function ServicesPage() {
<Card className="glass-card hover:border-indigo-500/30">
<h3 className="text-xl font-bold mb-3">Flexible Architecture</h3>
<p className="text-base text-muted-foreground leading-relaxed">
Model your exact business logic—subscriptions, bundles, B2B pricing,
multi-warehouse inventory—without platform constraints.
Model your exact business logic (subscriptions, bundles, B2B pricing,
multi-warehouse inventory) without platform constraints.
</p>
</Card>
<Card className="glass-card hover:border-indigo-500/30">
@@ -87,7 +89,7 @@ export default function ServicesPage() {
<h3 className="text-xl font-bold mb-3">Performance-First Frontend</h3>
<p className="text-base text-muted-foreground leading-relaxed">
Custom Next.js storefronts optimized for Core Web Vitals, accessibility,
and conversion—not template bloat.
and conversion, not template bloat.
</p>
</Card>
</div>
@@ -119,10 +121,26 @@ export default function ServicesPage() {
</div>
</ScrollReveal>
{/* Consulting umbrella */}
<ScrollReveal className="mb-12">
<div className="text-left section-container border-l-4 border-l-teal-500">
<h2 className="text-3xl font-bold mb-2">Consulting</h2>
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
Beyond build work, we consult where specialized, hard-won expertise matters. We
advise on payment architecture and orchestration, and on cryptocurrency and Monero
integration. Most distinctively, we take on nuclear and radiation modeling.
</p>
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed mt-3">
The nuclear work is our standout: PhD-led Monte Carlo transport, shielding analysis,
detector design, and training, grounded in years at a national lab. Details below.
</p>
</div>
</ScrollReveal>
{/* Crypto Section */}
<ScrollReveal className="mb-24">
<div className="text-left mb-10 section-container border-l-4 border-l-cyan-500">
<h2 className="text-3xl font-bold mb-2">Cryptocurrency & Payment Consulting</h2>
<h2 className="text-3xl font-bold mb-2">Cryptocurrency &amp; Payments</h2>
<p className="text-muted-foreground text-lg max-w-2xl leading-relaxed">
Bridge traditional payments and decentralized protocols with secure, auditable infrastructure.
</p>
@@ -145,6 +163,84 @@ export default function ServicesPage() {
</div>
</ScrollReveal>
{/* Nuclear & Radiation Modeling Section */}
<ScrollReveal className="mb-24">
<div className="text-left mb-10 section-container border-l-4 border-l-cyan-400">
<h2 className="text-3xl font-bold mb-2">Nuclear &amp; Radiation Modeling</h2>
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
PhD-led radiation transport and nuclear expertise, available for select engagements.
From Monte Carlo modeling to shielding analysis, we bring lab-grade rigor to hard
problems: modeling, analysis, and consulting.
</p>
</div>
<div className="grid md:grid-cols-2 gap-6 mb-8">
<Card className="glass-card hover:border-cyan-400/30">
<h3 className="text-xl font-bold mb-3">Monte Carlo Transport</h3>
<p className="text-base text-muted-foreground leading-relaxed">
Radiation transport modeling in <span className="font-semibold text-foreground">MCNP</span>{" "}
(<a href="https://scholar.google.com/citations?user=xTyYIKkAAAAJ&hl=en" target="_blank" rel="noopener noreferrer" className="font-semibold text-foreground underline decoration-cyan-400/50 hover:text-primary transition-colors">multiple peer-reviewed papers</a>)
and <span className="font-semibold text-foreground">GEANT4</span>, for
detectors, sources, and complex geometries.
</p>
</Card>
<Card className="glass-card hover:border-cyan-400/30">
<h3 className="text-xl font-bold mb-3">Shielding &amp; Dose</h3>
<p className="text-base text-muted-foreground leading-relaxed">
Shielding calculations and dose analysis, including spent-fuel handling and
dry-cask storage optimization.
</p>
</Card>
<Card className="glass-card hover:border-cyan-400/30">
<h3 className="text-xl font-bold mb-3">Detectors &amp; Isotopes</h3>
<p className="text-base text-muted-foreground leading-relaxed">
Radiation detector design and characterization, plus radioisotope power
(RTG / betavoltaic) modeling.
</p>
</Card>
<Card className="glass-card hover:border-cyan-400/30">
<h3 className="text-xl font-bold mb-3">Training &amp; Curriculum</h3>
<p className="text-base text-muted-foreground leading-relaxed">
Radiation-science training and coursework. See our open-source{" "}
<a
href="https://jwinterm.github.io/geant4-radiation-labs/"
target="_blank"
rel="noopener noreferrer"
className="font-semibold text-foreground underline decoration-cyan-400/50 hover:text-primary transition-colors"
>
GEANT4 radiation labs
</a>.
</p>
</Card>
</div>
<Card className="glass-card border-cyan-400/20">
<h3 className="text-lg font-bold mb-4">Why us</h3>
<ul className="space-y-2 text-base text-muted-foreground leading-relaxed">
<li>
<span className="font-semibold text-foreground">PhD in radiation-detector materials</span>, UT-Dallas (2014)
</li>
<li>
<span className="font-semibold text-foreground">Lawrence Livermore National Laboratory</span> (2015–2021): detector systems, radioisotope battery program, MCNP5 publications
</li>
<li>
<span className="font-semibold text-foreground">Holtec International</span>: radiation shielding and spent-fuel-to-dry-cask optimization
</li>
<li>
<span className="font-semibold text-foreground">College-level instructor</span>: author of open-source GEANT4 radiation labs
</li>
</ul>
<p className="text-sm text-muted-foreground mt-5">
Select engagements, PhD-led.{" "}
<Link href="/#contact" className="font-semibold text-foreground underline decoration-cyan-400/50 hover:text-primary transition-colors">
Contact us for rates
</Link>.
</p>
<p className="text-xs text-muted-foreground/70 mt-3">
We provide modeling, analysis, and consulting. We are not a licensed
professional-engineering (PE) firm and do not provide stamped engineering deliverables.
</p>
</Card>
</ScrollReveal>
{/* Recent Work */}
<ScrollReveal className="mb-24">
<div className="text-left mb-10 section-container border-l-4 border-l-amber-500">
+3
View File
@@ -22,6 +22,9 @@ export default function sitemap(): MetadataRoute.Sitemap {
{ url: `${base}/products/vegan-iq`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
{ url: `${base}/products/suchoice`, lastModified: updated, changeFrequency: "monthly", priority: 0.7 },
{ url: `${base}/products/such-moon-launch`, lastModified: updated, changeFrequency: "monthly", priority: 0.7 },
{ url: `${base}/products/barns-and-neutrons`, lastModified: updated, changeFrequency: "weekly", priority: 0.8 },
{ url: `${base}/products/alpha-doc`, lastModified: updated, changeFrequency: "weekly", priority: 0.7 },
{ url: `${base}/products/decay-theory`, lastModified: updated, changeFrequency: "weekly", priority: 0.7 },
{ url: `${base}/services`, lastModified: updated, changeFrequency: "monthly", priority: 0.9 },
{ url: `${base}/about`, lastModified: updated, changeFrequency: "monthly", priority: 0.7 },
{ url: `${base}/support`, lastModified: updated, changeFrequency: "monthly", priority: 0.5 },
@@ -38,10 +38,10 @@ export const FounderSignature = () => {
"Accessibility isn't bolted on. It's there from the first line of code."
</p>
<p className="text-muted-foreground leading-relaxed mt-6">
John Winter Murphy holds a <span className="text-foreground font-semibold">PhD in Materials Engineering from UT-Dallas (2014)</span>, focused on semiconductor device fabrication for radiation detectors. From <span className="text-foreground font-semibold">2015 to 2021 he was a scientist at Lawrence Livermore National Laboratory</span>, developing radiation detector systems and the lab's nuclear battery program.
John Winter Murphy holds a <span className="text-foreground font-semibold">PhD in Materials Engineering from UT-Dallas (2014)</span>, focused on semiconductor devices for radiation detection. From <span className="text-foreground font-semibold">2015 to 2021 he was a scientist at Lawrence Livermore National Laboratory</span>, developing radiation detector systems and the lab's radioisotope battery program. He brings that same lab-bench rigor to software, and an insistence that it stay <span className="text-foreground font-semibold">accessible to everyone</span>, audited to WCAG 2.2.
</p>
<p className="text-muted-foreground leading-relaxed mt-4">
He previously co-founded <span className="text-foreground font-semibold">Patrn LLC</span> (art101.io), a series of art-history NFT drops, and <span className="text-foreground font-semibold">Tabby Labs</span>, which was developing a cryptocurrency wallet with an interactive world inside.
He previously co-founded <span className="text-foreground font-semibold">Patrn LLC</span> (<a href="https://art101.io" target="_blank" rel="noopener noreferrer" className="text-foreground font-semibold underline decoration-primary/50 hover:text-primary transition-colors">art101.io</a>), a series of art-history NFT drops, and <span className="text-foreground font-semibold">Tabby Labs</span>, which was developing a cryptocurrency wallet with an interactive world inside.
</p>
<p className="text-muted-foreground leading-relaxed mt-4">
He most recently taught engineering at <span className="text-foreground font-semibold">Cecil College</span> and senior-level materials engineering (microfabrication, nanomaterials) at <span className="text-foreground font-semibold">Frostburg State University</span>. Active in cryptocurrency since 2014, with a public track record on <a href="https://github.com/jwinterm" target="_blank" rel="noopener noreferrer" className="text-foreground font-semibold underline decoration-primary/50 hover:text-primary transition-colors">GitHub</a>.
@@ -15,9 +15,11 @@ const NebulaField = dynamic(
interface DynamicBackgroundProps {
nebulaPosition?: { x: number; y: number } | null;
/** When false, the nebula stays hidden (e.g. while the home splash is active). */
visible?: boolean;
}
export function DynamicBackground({ nebulaPosition }: DynamicBackgroundProps) {
export function DynamicBackground({ nebulaPosition, visible = true }: DynamicBackgroundProps) {
const { resolvedTheme, systemTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const [showNebula, setShowNebula] = useState(false);
@@ -54,15 +56,19 @@ export function DynamicBackground({ nebulaPosition }: DynamicBackgroundProps) {
const effectiveTheme = resolvedTheme || systemTheme || 'dark';
const themeMode = effectiveTheme === 'dark' ? 'dark' : 'light';
if (!showNebula) {
// While the splash is active (visible === false) keep the nebula/monitor hidden —
// the splash provides its own Cherenkov glow. Reveal it once the visitor enters.
if (!showNebula || !visible) {
return <div className="fixed inset-0 z-0 h-screen w-screen bg-background" />;
}
return (
<div className="fixed inset-0 z-0 h-screen w-screen animate-in fade-in duration-700">
<NebulaField
className="fixed inset-0 z-0 h-screen w-screen"
className="absolute inset-0 h-full w-full"
themeMode={themeMode}
position={nebulaPosition ?? undefined}
/>
</div>
);
}
@@ -0,0 +1,124 @@
"use client";
import Link from "next/link";
import { motion, useReducedMotion } from "framer-motion";
import { ChevronDown } from "lucide-react";
const navButtons = [
{ label: "Portfolio", href: "/products", corner: "left-4 top-16 sm:left-6 sm:top-24 lg:left-8 lg:top-32" },
{ label: "Apps", href: "/apps", corner: "right-4 top-16 sm:right-6 sm:top-24 lg:right-8 lg:top-32" },
{ label: "Services", href: "/services", corner: "left-4 bottom-24 sm:left-6 lg:left-8 lg:bottom-32" },
{ label: "About", href: "/about", corner: "right-4 bottom-24 sm:right-6 lg:right-8 lg:bottom-32" },
];
/** The "SUCH" glyph mark (2x2 letter grid) with a Cherenkov-blue treatment. */
function SuchMark({ animate }: { animate: boolean }) {
return (
<svg
viewBox="0 0 200 200"
role="img"
aria-label="Such Software"
className="h-auto w-[min(74vw,44vh)] drop-shadow-[0_0_40px_rgba(34,211,238,0.25)]"
>
<defs>
<filter id="splash-glow" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="1.6" result="b" />
<feMerge>
<feMergeNode in="b" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
{/* steady bounding box, Cherenkov cyan */}
<rect
x="14" y="14" width="172" height="172" rx="16"
fill="none" stroke="#22d3ee" strokeWidth="3.5" filter="url(#splash-glow)" opacity="0.45"
/>
{/* a Cherenkov glow segment slowly circling the ring */}
{animate && (
<motion.rect
x="14" y="14" width="172" height="172" rx="16"
fill="none" stroke="#a5f3fc" strokeWidth="4" strokeLinecap="round" filter="url(#splash-glow)"
pathLength={1} strokeDasharray="0.16 0.84"
initial={{ strokeDashoffset: 0 }}
animate={{ strokeDashoffset: [0, -1] }}
transition={{ duration: 5, ease: "linear", repeat: Infinity }}
/>
)}
{/* letters with a gentle breath */}
<motion.g
animate={animate ? { opacity: [0.9, 1, 0.9] } : undefined}
transition={animate ? { duration: 5, ease: "easeInOut", repeat: Infinity } : undefined}
>
{/* S */}
<path d="M 78,36 H 36 V 58 H 78 V 82 H 36" fill="none" stroke="#35c98e" strokeWidth="14" strokeLinecap="round" strokeLinejoin="round" />
{/* U */}
<path d="m 118,36 v 34 c 0,12.15 9.85,14 22,14 12.15,0 22,-1.85 22,-14 V 36" fill="none" stroke="#e8689e" strokeWidth="14" strokeLinecap="round" strokeLinejoin="round" />
{/* C */}
<path d="M 78,118 H 36 V 164 H 78" fill="none" stroke="#4aade0" strokeWidth="14" strokeLinecap="round" strokeLinejoin="round" />
{/* H */}
<path d="M 118,118 V 164 M 162,118 V 164 M 118,141 H 162" fill="none" stroke="#45c99e" strokeWidth="14" strokeLinecap="round" strokeLinejoin="round" />
</motion.g>
</svg>
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function HeroSplash({ onEnter, sectionRef }: { onEnter: () => void; sectionRef?: any }) {
const prefersReduced = useReducedMotion();
const animate = !prefersReduced;
return (
<section
ref={sectionRef}
aria-label="Such Software"
className="relative z-10 flex min-h-[100svh] w-full max-w-7xl flex-col items-center justify-center px-4 py-12 text-center"
>
{/* Logo + pulsing Cherenkov glow */}
<div className="relative flex items-center justify-center">
<div
aria-hidden="true"
className="pointer-events-none absolute h-[120%] w-[120%] rounded-full bg-[radial-gradient(circle,rgba(34,211,238,0.18),transparent_65%)] motion-safe:animate-pulse"
style={{ animationDuration: "5s" }}
/>
<motion.div
initial={{ opacity: 0, scale: 0.94 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, ease: "easeOut" }}
>
<SuchMark animate={animate} />
</motion.div>
</div>
{/* Cover tagline (the real page H1 lives in the hero revealed after entering) */}
<p className="mt-10 bg-gradient-to-r from-cyan-400 to-blue-600 bg-clip-text pb-1 text-3xl font-bold text-transparent sm:text-4xl md:text-5xl">
Precision Engineering for Everyone
</p>
<p className="mt-4 max-w-xl text-base text-muted-foreground sm:text-lg">
A software studio in Kennett Square, PA. Custom e-commerce, apps, games,
payments, and consulting.
</p>
{/* Nav buttons in the four corners (all breakpoints) */}
{navButtons.map((b) => (
<Link
key={b.href}
href={b.href}
className={`absolute flex items-center justify-center rounded-2xl border border-cyan-400/25 bg-foreground/[0.06] backdrop-blur-md shadow-md shadow-black/20 ring-1 ring-inset ring-white/10 px-4 py-3 text-sm font-semibold transition-all hover:scale-[1.03] hover:border-cyan-400/60 hover:bg-foreground/10 hover:text-cyan-600 dark:hover:text-cyan-400 hover:shadow-lg hover:shadow-cyan-500/15 w-28 sm:w-36 sm:px-6 sm:py-4 sm:text-base lg:w-48 ${b.corner}`}
>
{b.label}
</Link>
))}
{/* Enter-the-site affordance */}
<button
type="button"
onClick={onEnter}
className="absolute bottom-6 left-1/2 flex -translate-x-1/2 flex-col items-center gap-1 text-xs uppercase tracking-[0.3em] text-muted-foreground transition-colors hover:text-foreground focus-visible:text-foreground"
>
<span>Explore</span>
<ChevronDown className="h-5 w-5 motion-safe:animate-bounce" aria-hidden="true" />
</button>
</section>
);
}
@@ -6,7 +6,8 @@ export function ServicesPreview() {
<div className="text-left mb-8 section-container border-l-4 border-l-emerald-500">
<h2 className="text-3xl font-bold mb-3 tracking-tight">What We Do</h2>
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
We build high-performance e-commerce stores, develop custom apps, and ship our own titles.
We build high-performance e-commerce stores, develop custom apps, and ship our own
titles, engineered with atomic precision by a studio that sweats the details.
Whether you need a storefront, a bespoke tool, or a full product build, we handle the engineering end to end.
</p>
</div>
+13 -2
View File
@@ -5,9 +5,20 @@ import { usePathname } from "next/navigation";
import { SiteLogo } from "./site-logo";
import { ThemeToggle } from "@/components/theme-toggle";
export function Header() {
/**
* On the home page the splash owns the first screen, so the header is `floating`
* (fixed) and only shown once the visitor has `visible`-entered the site. Other
* pages use the default always-on sticky header.
*/
export function Header({ floating = false, visible = true }: { floating?: boolean; visible?: boolean }) {
const pathname = usePathname();
const positionClasses = floating
? `fixed top-0 inset-x-0 transition-all duration-500 ${
visible ? "translate-y-0 opacity-100" : "-translate-y-full opacity-0 invisible"
}`
: "sticky top-0";
return (
<>
<a
@@ -16,7 +27,7 @@ export function Header() {
>
Skip to main content
</a>
<header className="z-50 w-full max-w-7xl mx-auto flex justify-between items-center p-6 bg-background/50 backdrop-blur-3xl sticky top-0 rounded-b-3xl border-b border-border/50">
<header className={`z-50 w-full max-w-7xl mx-auto flex justify-between items-center p-6 bg-background/50 backdrop-blur-3xl rounded-b-3xl border-b border-border/50 ${positionClasses}`}>
<div className="flex items-center gap-4">
<SiteLogo />
<nav className="hidden md:flex gap-6 text-sm font-medium" aria-label="Main navigation">
+10 -2
View File
@@ -12,7 +12,11 @@ const navItems = [
{ href: "/about", label: "About", icon: FaCircleInfo },
];
export function MobileNav() {
/**
* `visible` is driven by the home-page "entered" state so the bottom bar stays
* hidden under the splash. Defaults to visible everywhere else.
*/
export function MobileNav({ visible = true }: { visible?: boolean }) {
const pathname = usePathname();
const isActive = (href: string) => {
@@ -20,9 +24,13 @@ export function MobileNav() {
return pathname.startsWith(href);
};
const visibilityClasses = visible
? "translate-y-0 opacity-100"
: "translate-y-full opacity-0 invisible";
return (
<nav
className="fixed bottom-0 left-0 right-0 z-50 md:hidden bg-background/95 backdrop-blur-xl border-t border-border/50"
className={`fixed bottom-0 left-0 right-0 z-50 md:hidden bg-background/95 backdrop-blur-xl border-t border-border/50 transition-all duration-300 ${visibilityClasses}`}
aria-label="Mobile navigation"
style={{ paddingBottom: "env(safe-area-inset-bottom)" }}
>
@@ -0,0 +1,156 @@
"use client";
import { ScrollReveal } from "@/components/scroll-reveal";
import { Card } from "@repo/ui/components/card";
import Image from "next/image";
import Link from "next/link";
import { useMemo, useState } from "react";
export type Product = {
slug: string;
title: string;
description: string;
color: string;
icon?: string;
/** ISO date string. Null for ongoing services or unreleased items. */
releaseDate?: string | null;
/** Not yet released. Sorts as "now" by release date and renders without a detail link. */
comingSoon?: boolean;
};
const colorClasses: Record<string, string> = {
emerald: "border-emerald-500/30 hover:border-emerald-500/60",
purple: "border-purple-500/30 hover:border-purple-500/60",
amber: "border-amber-500/30 hover:border-amber-500/60",
blue: "border-blue-500/30 hover:border-blue-500/60",
pink: "border-pink-500/30 hover:border-pink-500/60",
orange: "border-orange-500/30 hover:border-orange-500/60",
yellow: "border-yellow-500/30 hover:border-yellow-500/60",
green: "border-green-500/30 hover:border-green-500/60",
cyan: "border-cyan-400/30 hover:border-cyan-400/60",
};
const titleColors: Record<string, string> = {
emerald: "text-emerald-600 dark:text-emerald-400",
purple: "text-purple-600 dark:text-purple-400",
amber: "text-amber-600 dark:text-amber-400",
blue: "text-blue-600 dark:text-blue-400",
pink: "text-pink-600 dark:text-pink-400",
orange: "text-orange-600 dark:text-orange-400",
yellow: "text-yellow-600 dark:text-yellow-400",
green: "text-green-600 dark:text-green-400",
cyan: "text-cyan-600 dark:text-cyan-400",
};
const glyphBg: Record<string, string> = {
emerald: "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400",
purple: "bg-purple-500/15 text-purple-600 dark:text-purple-400",
amber: "bg-amber-500/15 text-amber-600 dark:text-amber-400",
blue: "bg-blue-500/15 text-blue-600 dark:text-blue-400",
pink: "bg-pink-500/15 text-pink-600 dark:text-pink-400",
orange: "bg-orange-500/15 text-orange-600 dark:text-orange-400",
yellow: "bg-yellow-500/15 text-yellow-600 dark:text-yellow-400",
green: "bg-green-500/15 text-green-600 dark:text-green-400",
cyan: "bg-cyan-400/15 text-cyan-600 dark:text-cyan-400",
};
type SortKey = "release" | "name";
function effectiveTime(p: Product): number {
if (p.releaseDate) return Date.parse(p.releaseDate);
return 0;
}
function ProductIcon({ product }: { product: Product }) {
if (product.icon) {
return (
<Image
src={product.icon}
alt={`${product.title} icon`}
width={56}
height={56}
className="rounded-xl flex-shrink-0"
/>
);
}
return (
<div
aria-hidden="true"
className={`flex h-14 w-14 flex-shrink-0 items-center justify-center rounded-xl text-2xl font-bold ${glyphBg[product.color] ?? glyphBg.cyan}`}
>
{product.title.charAt(0)}
</div>
);
}
function ProductCardInner({ product }: { product: Product }) {
return (
<Card className={`glass-card h-full min-h-[200px] ${colorClasses[product.color]} transition-all`}>
<div className="flex items-center gap-4 mb-3">
<ProductIcon product={product} />
<div className="flex flex-col">
<h2 className={`text-2xl font-bold ${titleColors[product.color]}`}>
{product.title}
</h2>
{product.comingSoon && (
<span className="mt-1 inline-flex w-fit items-center rounded-full bg-cyan-400/15 px-2 py-0.5 font-mono text-[10px] uppercase tracking-[0.2em] text-cyan-600 dark:text-cyan-400">
Coming Soon
</span>
)}
</div>
</div>
<p className="text-muted-foreground leading-relaxed">{product.description}</p>
</Card>
);
}
export function ProductsGrid({ products }: { products: Product[] }) {
const [sort, setSort] = useState<SortKey>("release");
const sorted = useMemo(() => {
const copy = [...products];
if (sort === "name") {
copy.sort((a, b) => a.title.localeCompare(b.title));
} else {
// Released work first (newest first); coming-soon items grouped at the bottom.
copy.sort((a, b) => {
const aSoon = !!a.comingSoon;
const bSoon = !!b.comingSoon;
if (aSoon !== bSoon) return aSoon ? 1 : -1;
return effectiveTime(b) - effectiveTime(a);
});
}
return copy;
}, [products, sort]);
return (
<>
<div className="mb-8 flex items-center justify-end gap-3">
<label htmlFor="product-sort" className="text-sm text-muted-foreground">
Sort by
</label>
<select
id="product-sort"
value={sort}
onChange={(e) => setSort(e.target.value as SortKey)}
className="rounded-lg border border-border bg-background px-3 py-1.5 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-cyan-400/50"
>
<option value="release">Release date (newest)</option>
<option value="name">Name (A–Z)</option>
</select>
</div>
<div className="grid md:grid-cols-2 gap-8">
{sorted.map((product, i) => (
<ScrollReveal key={product.slug} delay={(i % 2) * 0.08}>
<Link href={`/products/${product.slug}`} className="block h-full">
<div className="h-full cursor-pointer">
<ProductCardInner product={product} />
</div>
</Link>
</ScrollReveal>
))}
</div>
</>
);
}
+68
View File
@@ -0,0 +1,68 @@
import { ImageResponse } from "next/og";
export const OG_SIZE = { width: 1200, height: 630 };
export const OG_CONTENT_TYPE = "image/png";
/**
* Build-time Open Graph card for a (pre-release) game product page.
* Themed by accent color, on the Cherenkov-dark studio background.
*/
export function gameOgImage({
title,
tagline,
accent,
badge = "In Development",
}: {
title: string;
tagline: string;
accent: string;
badge?: string;
}) {
return new ImageResponse(
(
<div
style={{
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
background: "#0a1622",
padding: 80,
fontFamily: "sans-serif",
}}
>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div style={{ display: "flex", fontSize: 30, letterSpacing: 6, color: "#94a3b8" }}>
SUCH SOFTWARE
</div>
<div
style={{
display: "flex",
fontSize: 22,
letterSpacing: 3,
color: accent,
border: `2px solid ${accent}`,
borderRadius: 999,
padding: "10px 24px",
}}
>
{badge.toUpperCase()}
</div>
</div>
<div style={{ display: "flex", flexDirection: "column" }}>
<div style={{ display: "flex", fontSize: 92, fontWeight: 800, color: accent, lineHeight: 1.05 }}>
{title}
</div>
<div style={{ display: "flex", fontSize: 40, color: "#cbd5e1", marginTop: 28, maxWidth: 1000 }}>
{tagline}
</div>
</div>
<div style={{ display: "flex", fontSize: 28, color: "#64748b" }}>such.software</div>
</div>
),
{ ...OG_SIZE },
);
}
+37 -9
View File
@@ -73,7 +73,7 @@ const StreamLine = ({ d, themeMode, onHit, width, index }: StreamLineProps) => {
animate={controls}
className={cn(
"transition-colors duration-1000",
themeMode === 'light' ? "text-slate-400" : "text-emerald-500/50"
themeMode === 'light' ? "text-sky-400/80" : "text-cyan-400/50"
)}
/>
);
@@ -82,7 +82,7 @@ const StreamLine = ({ d, themeMode, onHit, width, index }: StreamLineProps) => {
export const NebulaField = ({ className, density = 8, themeMode = 'dark', position }: NebulaProps) => {
// === ALL HOOKS MUST BE CALLED BEFORE ANY CONDITIONAL RETURNS ===
const monitorControls = useAnimation();
const [monitorColor, setMonitorColor] = useState(themeMode === 'light' ? "text-slate-400" : "text-emerald-500/60");
const [monitorColor, setMonitorColor] = useState(themeMode === 'light' ? "text-sky-500" : "text-cyan-400/70");
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
// Parallax Scroll hooks (called unconditionally)
@@ -108,7 +108,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
// Sync color with theme
useEffect(() => {
if (!prefersReducedMotion) {
setMonitorColor(themeMode === 'light' ? "text-slate-300" : "text-emerald-500/40");
setMonitorColor(themeMode === 'light' ? "text-sky-400" : "text-cyan-400/50");
}
}, [themeMode, prefersReducedMotion]);
@@ -135,8 +135,8 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
const handleHit = useCallback(() => {
const colors = themeMode === 'light'
? ["text-slate-400", "text-blue-400", "text-indigo-400", "text-emerald-400"]
: ["text-emerald-500/50", "text-blue-500/50", "text-purple-500/50", "text-cyan-500/50", "text-teal-500/50"];
? ["text-sky-400", "text-cyan-400", "text-blue-400", "text-sky-500"]
: ["text-cyan-400/60", "text-sky-400/50", "text-blue-400/50", "text-cyan-300/50", "text-teal-400/50"];
setMonitorColor(colors[Math.floor(Math.random() * colors.length)]);
@@ -153,22 +153,50 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
// === CONDITIONAL RETURNS AFTER ALL HOOKS ===
// Return static background for reduced motion users
// Cherenkov-blue radial glow ("reactor pool" bloom), positioned near the convergence point.
const glowColor = themeMode === 'light' ? "rgba(56,189,248,0.22)" : "rgba(34,211,238,0.18)";
const glowStyle = {
background: `radial-gradient(circle at center, ${glowColor} 0%, transparent 70%)`,
} as const;
// Return static background for reduced motion users (still tinted Cherenkov blue, no motion).
if (prefersReducedMotion) {
return (
<div className={cn(
"absolute inset-0 overflow-hidden pointer-events-none z-0",
themeMode === 'light'
? "bg-gradient-to-br from-slate-100 via-slate-50 to-white"
: "bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900",
? "bg-gradient-to-br from-sky-50 via-slate-50 to-white"
: "bg-gradient-to-br from-slate-950 via-slate-900 to-[#0a1622]",
className
)} />
)}>
<div
className="absolute h-[70vmax] w-[70vmax] -translate-x-1/2 -translate-y-1/2"
style={{ ...glowStyle, left: `${computerX}%`, top: `${computerY}%` }}
/>
</div>
);
}
return (
<div className={cn("absolute inset-0 overflow-hidden pointer-events-none z-0", className)}>
{/* Base Cherenkov gradient wash */}
<div className={cn(
"absolute inset-0",
themeMode === 'light'
? "bg-gradient-to-br from-sky-50/60 via-transparent to-transparent"
: "bg-gradient-to-br from-slate-950 via-slate-900/80 to-[#0a1622]"
)} />
{/* Slowly-pulsing Cherenkov "reactor pool" bloom */}
<motion.div
className="absolute h-[70vmax] w-[70vmax] -translate-x-1/2 -translate-y-1/2"
style={{ ...glowStyle, left: `${computerX}%`, top: `${computerY}%`, y: yRange }}
initial={{ opacity: 0, scale: 0.85 }}
animate={{ opacity: [0.55, 0.9, 0.55], scale: [0.95, 1.05, 0.95] }}
transition={{ duration: 11, ease: "easeInOut", repeat: Infinity }}
/>
<motion.div
className="absolute -translate-x-1/2 -translate-y-1/2 z-20"
initial={{ opacity: 0, scale: 0.8 }}