diff --git a/apps/web/src/app/blog/page.tsx b/apps/web/src/app/blog/page.tsx
index c2114e4..30dbfb8 100644
--- a/apps/web/src/app/blog/page.tsx
+++ b/apps/web/src/app/blog/page.tsx
@@ -30,18 +30,8 @@ export default function BlogPage() {
{/* Animated accent */}
-

-

+
+
{posts.length === 0 ? (
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index ecfcd96..81841b6 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -286,14 +286,29 @@
/* Hero title: an animated gradient sheen. LCP-safe — the text paints immediately with the
gradient; only the gradient POSITION animates, which never delays the largest paint. The
global reduced-motion rule freezes it to the base gradient. */
+/* The brand's four-colour system (S green, U magenta, C blue, H teal), one per glyph.
+ Light theme uses the light palette; .dark swaps in the dark palette, which is the
+ same hues lifted for contrast on near-black -- from packs/such-software/pack.json. */
.hero-title {
- background-image: linear-gradient(110deg, #22d3ee 0%, #3b82f6 30%, #7dd3fc 50%, #3b82f6 70%, #22d3ee 100%);
+ background-image: linear-gradient(110deg, #1a5c38 0%, #b83670 30%, #176a92 55%, #12857e 80%, #1a5c38 100%);
background-size: 220% auto;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: hero-sheen 9s ease-in-out infinite;
}
+.dark .hero-title {
+ background-image: linear-gradient(110deg, #35925c 0%, #e8689e 30%, #4aade0 55%, #3ad4c6 80%, #35925c 100%);
+}
+.brand-gradient-text {
+ background-image: linear-gradient(90deg, #1a5c38, #b83670, #176a92, #12857e);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+.dark .brand-gradient-text {
+ background-image: linear-gradient(90deg, #35925c, #e8689e, #4aade0, #3ad4c6);
+}
@keyframes hero-sheen {
0%, 100% { background-position: 0% center; }
50% { background-position: 100% center; }
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 2645aca..d06f0de 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -58,8 +58,8 @@ const structuredData = {
"@type": "Organization",
name: "Such Software LLC",
url: "https://such.software",
- logo: "https://such.software/images/branding/clean_dark_w_text.svg",
- description: "E-commerce infrastructure, custom app development, and accessible web applications. Engineered for scale, compliance, and performance.",
+ logo: "https://such.software/images/branding/icon-512.png",
+ description: "A Kennett Square, PA software studio building custom online stores, web apps, mobile games, and Monero & crypto tools.",
address: {
"@type": "PostalAddress",
streetAddress: "110 E State St, Suite 300",
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index bebe568..64dbe6c 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -21,23 +21,23 @@ export default function Home() {
{/* Decorative aura behind the title (aria-hidden, LCP-safe, motion-safe). */}
- Precision Engineering
- for Everyone
+ Custom E-commerce,
+ Apps & Crypto Tools
- Custom e-commerce, websites, apps, and consulting. Plus our own mobile apps and games. A software studio in Kennett Square, PA.
+ Websites, games, payments, and consulting too — plus our own mobile apps. A software studio in Kennett Square, PA. Precision engineering for everyone.
-
+
diff --git a/apps/web/src/app/products/page.tsx b/apps/web/src/app/products/page.tsx
index 07e2eda..1e528bd 100644
--- a/apps/web/src/app/products/page.tsx
+++ b/apps/web/src/app/products/page.tsx
@@ -4,11 +4,11 @@ import { ProductsGrid, type Product } from "@/components/portfolio/products-grid
import type { Metadata } from "next";
export const metadata: Metadata = {
- title: "Products: Crypto Tools, Games & Commerce | Such Software",
+ title: "Portfolio: Products & Projects | 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: "Products: Crypto Tools, Games & Commerce | Such Software",
+ title: "Portfolio: Products & Projects | 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",
},
@@ -146,31 +146,21 @@ export default function ProductsPage() {
return (
-
+
-
+
Portfolio
- The tools and apps we have shipped, from commerce backends to consumer games, plus three nuclear-physics games in development.
+ Products & projects — from commerce backends to consumer games, plus three nuclear-physics games in development.
-

-

+
+
diff --git a/apps/web/src/app/services/page.tsx b/apps/web/src/app/services/page.tsx
index 7258f77..0d0a5bc 100644
--- a/apps/web/src/app/services/page.tsx
+++ b/apps/web/src/app/services/page.tsx
@@ -33,7 +33,7 @@ export default function ServicesPage() {
{/* Page header */}
-
+
What we build
diff --git a/apps/web/src/components/landing/hero-splash.tsx b/apps/web/src/components/landing/hero-splash.tsx
index 160150e..4657afa 100644
--- a/apps/web/src/components/landing/hero-splash.tsx
+++ b/apps/web/src/components/landing/hero-splash.tsx
@@ -3,7 +3,7 @@
import Link from "next/link";
import { ChevronDown, LayoutGrid, Smartphone, Wrench, Info } from "lucide-react";
import { useEffect, useRef, useState } from "react";
-import { SPLASH_LOGO_SRC } from "./splash-logo-data";
+import { SPLASH_LOGO_SRC_DARK, SPLASH_LOGO_SRC_LIGHT } from "./splash-logo-data";
const navButtons = [
{ label: "Portfolio", href: "/products", Icon: LayoutGrid, corner: "left-4 top-6 sm:left-6 sm:top-24 lg:left-8 lg:top-32" },
@@ -179,17 +179,24 @@ export function HeroSplash({ onEnter, sectionRef, leaving }: { onEnter: () => vo
{/* Data URI (no network request) so the logo paints with FCP; it is the
mobile LCP element. */}
+
{/* Cover text hidden on mobile (logo + corner icons carry it there); fades out on enter */}
Precision Engineering for Everyone
diff --git a/apps/web/src/components/landing/home-shell.tsx b/apps/web/src/components/landing/home-shell.tsx
index feac3ed..0dc8f92 100644
--- a/apps/web/src/components/landing/home-shell.tsx
+++ b/apps/web/src/components/landing/home-shell.tsx
@@ -49,16 +49,18 @@ export function HomeShell({ children }: { children: ReactNode }) {
if (entered) return;
const el = splashRef.current;
if (!el) return;
+ // Enter once 40% of the splash has scrolled by: requiring the whole screen to
+ // leave the viewport made the reveal feel like a toll booth.
const io = new IntersectionObserver(
([entry]) => {
- if (!entry.isIntersecting) {
+ if (entry.intersectionRatio < 0.6) {
restoreScroll.current = Math.max(0, window.scrollY - el.offsetHeight);
sessionStorage.setItem("such:entered", "1");
setEntered(true);
setSplashUp(false); // scrolled past: just unmount, no overlay fade needed
}
},
- { threshold: 0 },
+ { threshold: [0, 0.6] },
);
io.observe(el);
return () => io.disconnect();
diff --git a/apps/web/src/components/landing/splash-logo-data.ts b/apps/web/src/components/landing/splash-logo-data.ts
index 68626e0..3a6f733 100644
--- a/apps/web/src/components/landing/splash-logo-data.ts
+++ b/apps/web/src/components/landing/splash-logo-data.ts
@@ -1,6 +1,11 @@
-// AUTO-GENERATED from public/images/branding/splash-such.svg.
-// Inlined as a data URI so the splash logo (the homepage LCP element) needs no
-// network request and paints with first contentful paint. Regenerate if the
-// mark changes.
-export const SPLASH_LOGO_SRC =
- "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20200%20200%22%20role%3D%22img%22%20aria-label%3D%22Such%20Software%22%3E%3Cdefs%3E%3Cfilter%20id%3D%22splash-glow%22%20x%3D%22-30%25%22%20y%3D%22-30%25%22%20width%3D%22160%25%22%20height%3D%22160%25%22%3E%3CfeGaussianBlur%20stdDeviation%3D%221.6%22%20result%3D%22b%22%2F%3E%3CfeMerge%3E%3CfeMergeNode%20in%3D%22b%22%2F%3E%3CfeMergeNode%20in%3D%22SourceGraphic%22%2F%3E%3C%2FfeMerge%3E%3C%2Ffilter%3E%3C%2Fdefs%3E%3Crect%20x%3D%2214%22%20y%3D%2214%22%20width%3D%22172%22%20height%3D%22172%22%20rx%3D%2216%22%20fill%3D%22none%22%20stroke%3D%22%2322d3ee%22%20stroke-width%3D%223.5%22%20filter%3D%22url%28%23splash-glow%29%22%20opacity%3D%220.45%22%2F%3E%3Cg%20fill%3D%22none%22%20stroke-width%3D%2214%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M%2078%2C36%20H%2036%20V%2058%20H%2078%20V%2082%20H%2036%22%20stroke%3D%22%2335c98e%22%2F%3E%3Cpath%20d%3D%22m%20118%2C36%20v%2034%20c%200%2C12.15%209.85%2C14%2022%2C14%2012.15%2C0%2022%2C-1.85%2022%2C-14%20V%2036%22%20stroke%3D%22%23e8689e%22%2F%3E%3Cpath%20d%3D%22M%2078%2C118%20H%2036%20V%20164%20H%2078%22%20stroke%3D%22%234aade0%22%2F%3E%3Cpath%20d%3D%22M%20118%2C118%20V%20164%20M%20162%2C118%20V%20164%20M%20118%2C141%20H%20162%22%20stroke%3D%22%2345c99e%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E";
+// AUTO-GENERATED from packs/such-software/pack.json (such-graphics).
+// Inlined as data URIs so the splash logo (the homepage LCP element) needs no
+// network request and paints with first contentful paint. Two theme variants:
+// the pack's dark hues are the light hues lifted for contrast on near-black,
+// so each ground gets the palette that was tuned for it. Regenerate from the
+// pack if the mark changes; public/images/branding/splash-such.svg mirrors
+// the dark variant for anything that hotlinks it.
+export const SPLASH_LOGO_SRC_DARK =
+ "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20200%20200%22%20role%3D%22img%22%20aria-label%3D%22Such%20Software%22%3E%3Cdefs%3E%3Cfilter%20id%3D%22splash-glow%22%20x%3D%22-30%25%22%20y%3D%22-30%25%22%20width%3D%22160%25%22%20height%3D%22160%25%22%3E%3CfeGaussianBlur%20stdDeviation%3D%221.6%22%20result%3D%22b%22%2F%3E%3CfeMerge%3E%3CfeMergeNode%20in%3D%22b%22%2F%3E%3CfeMergeNode%20in%3D%22SourceGraphic%22%2F%3E%3C%2FfeMerge%3E%3C%2Ffilter%3E%3C%2Fdefs%3E%3Crect%20x%3D%2214%22%20y%3D%2214%22%20width%3D%22172%22%20height%3D%22172%22%20rx%3D%2212%22%20fill%3D%22none%22%20stroke%3D%22%2345c99e%22%20stroke-width%3D%223.5%22%20filter%3D%22url%28%23splash-glow%29%22%20opacity%3D%220.45%22%2F%3E%3Cg%20fill%3D%22none%22%20stroke-width%3D%2214%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M%2078%2C36%20H%2036%20V%2058%20H%2078%20V%2082%20H%2036%22%20stroke%3D%22%2335925c%22%20transform%3D%22translate%283%2C0%29%22%2F%3E%3Cpath%20d%3D%22m%20118%2C36%20v%2034%20c%200%2C12.15%209.85%2C13%2022%2C13%2012.15%2C0%2022%2C-3.1%2022%2C-13%20V%2036%22%20stroke%3D%22%23e8689e%22%20transform%3D%22translate%28-3%2C0%29%22%2F%3E%3Cpath%20d%3D%22M%2078%2C118%20H%2036%20V%20164%20H%2078%22%20stroke%3D%22%234aade0%22%20transform%3D%22translate%283%2C0%29%22%2F%3E%3Cpath%20d%3D%22M%20118%2C118%20V%20164%20M%20162%2C118%20V%20164%20M%20118%2C141%20H%20162%22%20stroke%3D%22%233ad4c6%22%20transform%3D%22translate%28-3%2C0%29%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E";
+export const SPLASH_LOGO_SRC_LIGHT =
+ "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20200%20200%22%20role%3D%22img%22%20aria-label%3D%22Such%20Software%22%3E%3Cdefs%3E%3Cfilter%20id%3D%22splash-glow%22%20x%3D%22-30%25%22%20y%3D%22-30%25%22%20width%3D%22160%25%22%20height%3D%22160%25%22%3E%3CfeGaussianBlur%20stdDeviation%3D%221.6%22%20result%3D%22b%22%2F%3E%3CfeMerge%3E%3CfeMergeNode%20in%3D%22b%22%2F%3E%3CfeMergeNode%20in%3D%22SourceGraphic%22%2F%3E%3C%2FfeMerge%3E%3C%2Ffilter%3E%3C%2Fdefs%3E%3Crect%20x%3D%2214%22%20y%3D%2214%22%20width%3D%22172%22%20height%3D%22172%22%20rx%3D%2212%22%20fill%3D%22none%22%20stroke%3D%22%231a5c38%22%20stroke-width%3D%223.5%22%20filter%3D%22url%28%23splash-glow%29%22%20opacity%3D%220.45%22%2F%3E%3Cg%20fill%3D%22none%22%20stroke-width%3D%2214%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M%2078%2C36%20H%2036%20V%2058%20H%2078%20V%2082%20H%2036%22%20stroke%3D%22%231a5c38%22%20transform%3D%22translate%283%2C0%29%22%2F%3E%3Cpath%20d%3D%22m%20118%2C36%20v%2034%20c%200%2C12.15%209.85%2C13%2022%2C13%2012.15%2C0%2022%2C-3.1%2022%2C-13%20V%2036%22%20stroke%3D%22%23b83670%22%20transform%3D%22translate%28-3%2C0%29%22%2F%3E%3Cpath%20d%3D%22M%2078%2C118%20H%2036%20V%20164%20H%2078%22%20stroke%3D%22%23176a92%22%20transform%3D%22translate%283%2C0%29%22%2F%3E%3Cpath%20d%3D%22M%20118%2C118%20V%20164%20M%20162%2C118%20V%20164%20M%20118%2C141%20H%20162%22%20stroke%3D%22%2312857e%22%20transform%3D%22translate%28-3%2C0%29%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E";
diff --git a/apps/web/src/components/portfolio/products-grid.tsx b/apps/web/src/components/portfolio/products-grid.tsx
index 9c092bd..949f60c 100644
--- a/apps/web/src/components/portfolio/products-grid.tsx
+++ b/apps/web/src/components/portfolio/products-grid.tsx
@@ -133,16 +133,16 @@ export function ProductsGrid({ products }: { products: Product[] }) {
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"
+ 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-[#3ad4c6]/50"
>
-
+
{sorted.map((product, i) => (
-
+
diff --git a/apps/web/src/components/seo/json-ld.tsx b/apps/web/src/components/seo/json-ld.tsx
index 42c43cc..59ae0ef 100644
--- a/apps/web/src/components/seo/json-ld.tsx
+++ b/apps/web/src/components/seo/json-ld.tsx
@@ -99,7 +99,7 @@ export function articleLd(opts: { title: string; description: string; slug: stri
publisher: {
"@type": "Organization",
name: "Such Software",
- logo: { "@type": "ImageObject", url: `${SITE}/images/branding/clean_dark_w_text.svg` },
+ logo: { "@type": "ImageObject", url: `${SITE}/images/branding/icon-512.png` },
},
};
}