feat(site): one voice, brand gradients, a lighter splash gate, denser portfolio

- Voice unified on the title-tag register: the homepage h1 now says what we
  sell (Custom E-commerce, Apps & Crypto Tools); 'Precision engineering for
  everyone' is demoted to the supporting line. The structured-data and
  manifest descriptions now match the OG description instead of carrying
  their own registers.
- The off-palette cyan/blue gradients are gone: .hero-title and the new
  .brand-gradient-text use the four-colour system (light palette on light,
  dark palette on dark, per the pack). Splash cover text, services and
  portfolio headings, the hero aura, and button shadows all follow.
- The splash now dismisses once 40% scrolled past instead of requiring the
  whole screen to leave the viewport (it was already once-per-session and
  reduced-motion-skipping).
- Portfolio: heading 'Portfolio', subtitle 'Products & projects', meta title
  to match; grid grows to three columns on xl in a max-w-6xl container.
- anim3_matrix slimmed: single-layer glow, unfiltered crackle and orbs, and
  the border cycles stroke colour instead of animating filter:hue-rotate --
  the filter animation forced a full filter re-render per frame and pegged a
  core on the page hosting it.
This commit is contained in:
jwinterm
2026-08-16 00:04:28 -04:00
parent c73b9f1c04
commit 261cb04667
11 changed files with 47 additions and 30 deletions
@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs><filter id="sg-mglow" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter><clipPath id="sg-clip"><rect width="200" height="200" rx="26"/></clipPath><radialGradient id="sg-shock-s"><stop offset="0" stop-color="#35925c" stop-opacity="0.6"/><stop offset="1" stop-color="#35925c" stop-opacity="0"/></radialGradient><radialGradient id="sg-shock-u"><stop offset="0" stop-color="#e8689e" stop-opacity="0.6"/><stop offset="1" stop-color="#e8689e" stop-opacity="0"/></radialGradient><radialGradient id="sg-shock-c"><stop offset="0" stop-color="#4aade0" stop-opacity="0.6"/><stop offset="1" stop-color="#4aade0" stop-opacity="0"/></radialGradient><radialGradient id="sg-shock-h"><stop offset="0" stop-color="#3ad4c6" stop-opacity="0.6"/><stop offset="1" stop-color="#3ad4c6" stop-opacity="0"/></radialGradient></defs> <defs><filter id="sg-mglow" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="2.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter><clipPath id="sg-clip"><rect width="200" height="200" rx="26"/></clipPath><radialGradient id="sg-shock-s"><stop offset="0" stop-color="#35925c" stop-opacity="0.6"/><stop offset="1" stop-color="#35925c" stop-opacity="0"/></radialGradient><radialGradient id="sg-shock-u"><stop offset="0" stop-color="#e8689e" stop-opacity="0.6"/><stop offset="1" stop-color="#e8689e" stop-opacity="0"/></radialGradient><radialGradient id="sg-shock-c"><stop offset="0" stop-color="#4aade0" stop-opacity="0.6"/><stop offset="1" stop-color="#4aade0" stop-opacity="0"/></radialGradient><radialGradient id="sg-shock-h"><stop offset="0" stop-color="#3ad4c6" stop-opacity="0.6"/><stop offset="1" stop-color="#3ad4c6" stop-opacity="0"/></radialGradient></defs>
<style> <style>
.rn { animation: sg-rain linear infinite; } .rn { animation: sg-rain linear infinite; }
@keyframes sg-rain { from { transform: translateY(-100px); } to { transform: translateY(320px); } } @keyframes sg-rain { from { transform: translateY(-100px); } to { transform: translateY(320px); } }
@@ -26,8 +26,8 @@
@keyframes sg-slam { 0%, 2% { transform: scale(0); } 5% { transform: scale(1.15); } 8% { transform: scale(1); } 85% { transform: scale(1); } 95% { transform: scale(1); opacity: 1; } 100% { transform: scale(0); opacity: 0; } } @keyframes sg-slam { 0%, 2% { transform: scale(0); } 5% { transform: scale(1.15); } 8% { transform: scale(1); } 85% { transform: scale(1); } 95% { transform: scale(1); opacity: 1; } 100% { transform: scale(0); opacity: 0; } }
.orb { transform-box: fill-box; transform-origin: center; animation: sg-orb 1.5s ease-in-out infinite; } .orb { transform-box: fill-box; transform-origin: center; animation: sg-orb 1.5s ease-in-out infinite; }
@keyframes sg-orb { 0%,100% { transform: scale(1); opacity: 0.6; } 50% { transform: scale(2); opacity: 1; } } @keyframes sg-orb { 0%,100% { transform: scale(1); opacity: 0.6; } 50% { transform: scale(2); opacity: 1; } }
.bdr { animation: sg-hue 4s linear infinite, sg-breathe-o 2s ease-in-out infinite; } .bdr { animation: sg-bcycle 4s linear infinite, sg-breathe-o 2s ease-in-out infinite; }
@keyframes sg-hue { from { filter: hue-rotate(0deg); } to { filter: hue-rotate(360deg); } } @keyframes sg-bcycle { 0%,100% { stroke: #7ddfbe; } 25% { stroke: #e8689e; } 50% { stroke: #4aade0; } 75% { stroke: #35925c; } }
@keyframes sg-breathe-o { 0%,100% { opacity: 0.7; } 50% { opacity: 1; } } @keyframes sg-breathe-o { 0%,100% { opacity: 0.7; } 50% { opacity: 1; } }
.slam-s { animation-delay: 0s; } .slam-s { animation-delay: 0s; }
.shock-s { animation-delay: 0s; } .shock-s { animation-delay: 0s; }
@@ -66,8 +66,8 @@
<path d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#45c99e" stroke-width="2" opacity="0.25" stroke-linecap="round" stroke-linejoin="round" fill="none"/> <path d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#45c99e" stroke-width="2" opacity="0.25" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<path class="zap" d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#7ddfbe" stroke-width="3" opacity="0.8" stroke-linecap="round" stroke-linejoin="round" fill="none" filter="url(#sg-mglow)" stroke-dasharray="15 588.9"/> <path class="zap" d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#7ddfbe" stroke-width="3" opacity="0.8" stroke-linecap="round" stroke-linejoin="round" fill="none" filter="url(#sg-mglow)" stroke-dasharray="15 588.9"/>
<path class="ctr" d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#ffffff" stroke-width="1.5" opacity="0.5" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke-dasharray="5 598.9"/> <path class="ctr" d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#ffffff" stroke-width="1.5" opacity="0.5" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke-dasharray="5 598.9"/>
<path class="crk" d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#45c99e" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none" filter="url(#sg-mglow)" stroke-dasharray="3 40 8 60 2 80 5 400"/> <path class="crk" d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#45c99e" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke-dasharray="3 40 8 60 2 80 5 400"/>
<g filter="url(#sg-mglow)"> <g>
<g transform="translate(20,28)"><g class="orb" style="animation-delay:0s"><circle r="2.5" fill="#45c99e"/></g></g> <g transform="translate(20,28)"><g class="orb" style="animation-delay:0s"><circle r="2.5" fill="#45c99e"/></g></g>
<g transform="translate(104,100)"><g class="orb" style="animation-delay:0.5s"><circle r="2.5" fill="#45c99e"/></g></g> <g transform="translate(104,100)"><g class="orb" style="animation-delay:0.5s"><circle r="2.5" fill="#45c99e"/></g></g>
<g transform="translate(172,172)"><g class="orb" style="animation-delay:1s"><circle r="2.5" fill="#45c99e"/></g></g> <g transform="translate(172,172)"><g class="orb" style="animation-delay:1s"><circle r="2.5" fill="#45c99e"/></g></g>

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs><filter id="sg-mglow" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter><clipPath id="sg-clip"><rect width="200" height="200" rx="26"/></clipPath><radialGradient id="sg-shock-s"><stop offset="0" stop-color="#1a5c38" stop-opacity="0.6"/><stop offset="1" stop-color="#1a5c38" stop-opacity="0"/></radialGradient><radialGradient id="sg-shock-u"><stop offset="0" stop-color="#b83670" stop-opacity="0.6"/><stop offset="1" stop-color="#b83670" stop-opacity="0"/></radialGradient><radialGradient id="sg-shock-c"><stop offset="0" stop-color="#176a92" stop-opacity="0.6"/><stop offset="1" stop-color="#176a92" stop-opacity="0"/></radialGradient><radialGradient id="sg-shock-h"><stop offset="0" stop-color="#12857e" stop-opacity="0.6"/><stop offset="1" stop-color="#12857e" stop-opacity="0"/></radialGradient></defs> <defs><filter id="sg-mglow" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="2.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter><clipPath id="sg-clip"><rect width="200" height="200" rx="26"/></clipPath><radialGradient id="sg-shock-s"><stop offset="0" stop-color="#1a5c38" stop-opacity="0.6"/><stop offset="1" stop-color="#1a5c38" stop-opacity="0"/></radialGradient><radialGradient id="sg-shock-u"><stop offset="0" stop-color="#b83670" stop-opacity="0.6"/><stop offset="1" stop-color="#b83670" stop-opacity="0"/></radialGradient><radialGradient id="sg-shock-c"><stop offset="0" stop-color="#176a92" stop-opacity="0.6"/><stop offset="1" stop-color="#176a92" stop-opacity="0"/></radialGradient><radialGradient id="sg-shock-h"><stop offset="0" stop-color="#12857e" stop-opacity="0.6"/><stop offset="1" stop-color="#12857e" stop-opacity="0"/></radialGradient></defs>
<style> <style>
.rn { animation: sg-rain linear infinite; } .rn { animation: sg-rain linear infinite; }
@keyframes sg-rain { from { transform: translateY(-100px); } to { transform: translateY(320px); } } @keyframes sg-rain { from { transform: translateY(-100px); } to { transform: translateY(320px); } }
@@ -26,8 +26,8 @@
@keyframes sg-slam { 0%, 2% { transform: scale(0); } 5% { transform: scale(1.15); } 8% { transform: scale(1); } 85% { transform: scale(1); } 95% { transform: scale(1); opacity: 1; } 100% { transform: scale(0); opacity: 0; } } @keyframes sg-slam { 0%, 2% { transform: scale(0); } 5% { transform: scale(1.15); } 8% { transform: scale(1); } 85% { transform: scale(1); } 95% { transform: scale(1); opacity: 1; } 100% { transform: scale(0); opacity: 0; } }
.orb { transform-box: fill-box; transform-origin: center; animation: sg-orb 1.5s ease-in-out infinite; } .orb { transform-box: fill-box; transform-origin: center; animation: sg-orb 1.5s ease-in-out infinite; }
@keyframes sg-orb { 0%,100% { transform: scale(1); opacity: 0.6; } 50% { transform: scale(2); opacity: 1; } } @keyframes sg-orb { 0%,100% { transform: scale(1); opacity: 0.6; } 50% { transform: scale(2); opacity: 1; } }
.bdr { animation: sg-hue 4s linear infinite, sg-breathe-o 2s ease-in-out infinite; } .bdr { animation: sg-bcycle 4s linear infinite, sg-breathe-o 2s ease-in-out infinite; }
@keyframes sg-hue { from { filter: hue-rotate(0deg); } to { filter: hue-rotate(360deg); } } @keyframes sg-bcycle { 0%,100% { stroke: #2f8f5e; } 25% { stroke: #b83670; } 50% { stroke: #176a92; } 75% { stroke: #1a5c38; } }
@keyframes sg-breathe-o { 0%,100% { opacity: 0.7; } 50% { opacity: 1; } } @keyframes sg-breathe-o { 0%,100% { opacity: 0.7; } 50% { opacity: 1; } }
.slam-s { animation-delay: 0s; } .slam-s { animation-delay: 0s; }
.shock-s { animation-delay: 0s; } .shock-s { animation-delay: 0s; }
@@ -66,8 +66,8 @@
<path d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#1a5c38" stroke-width="2" opacity="0.25" stroke-linecap="round" stroke-linejoin="round" fill="none"/> <path d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#1a5c38" stroke-width="2" opacity="0.25" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<path class="zap" d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#2f8f5e" stroke-width="3" opacity="0.8" stroke-linecap="round" stroke-linejoin="round" fill="none" filter="url(#sg-mglow)" stroke-dasharray="15 588.9"/> <path class="zap" d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#2f8f5e" stroke-width="3" opacity="0.8" stroke-linecap="round" stroke-linejoin="round" fill="none" filter="url(#sg-mglow)" stroke-dasharray="15 588.9"/>
<path class="ctr" d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#0a0a14" stroke-width="1.5" opacity="0.5" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke-dasharray="5 598.9"/> <path class="ctr" d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#0a0a14" stroke-width="1.5" opacity="0.5" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke-dasharray="5 598.9"/>
<path class="crk" d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#1a5c38" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none" filter="url(#sg-mglow)" stroke-dasharray="3 40 8 60 2 80 5 400"/> <path class="crk" d="m 20,28 h 66 l 6,6 h 16 l 6,-6 h 58 l 6,6 v 60 l -6,6 H 28 l -6,6 v 60 l 6,6 h 58 l 6,-6 h 16 l 6,6 h 58" stroke="#1a5c38" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke-dasharray="3 40 8 60 2 80 5 400"/>
<g filter="url(#sg-mglow)"> <g>
<g transform="translate(20,28)"><g class="orb" style="animation-delay:0s"><circle r="2.5" fill="#1a5c38"/></g></g> <g transform="translate(20,28)"><g class="orb" style="animation-delay:0s"><circle r="2.5" fill="#1a5c38"/></g></g>
<g transform="translate(104,100)"><g class="orb" style="animation-delay:0.5s"><circle r="2.5" fill="#1a5c38"/></g></g> <g transform="translate(104,100)"><g class="orb" style="animation-delay:0.5s"><circle r="2.5" fill="#1a5c38"/></g></g>
<g transform="translate(172,172)"><g class="orb" style="animation-delay:1s"><circle r="2.5" fill="#1a5c38"/></g></g> <g transform="translate(172,172)"><g class="orb" style="animation-delay:1s"><circle r="2.5" fill="#1a5c38"/></g></g>

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "Such Software", "name": "Such Software",
"short_name": "Such Software", "short_name": "Such Software",
"description": "Next-generation web applications engineered for scale, compliance, and performance.", "description": "A Kennett Square, PA software studio building custom online stores, web apps, mobile games, and Monero & crypto tools.",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"background_color": "#0a0a0a", "background_color": "#0a0a0a",
+16 -1
View File
@@ -286,14 +286,29 @@
/* Hero title: an animated gradient sheen. LCP-safe — the text paints immediately with the /* 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 gradient; only the gradient POSITION animates, which never delays the largest paint. The
global reduced-motion rule freezes it to the base gradient. */ 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 { .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; background-size: 220% auto;
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
color: transparent; color: transparent;
animation: hero-sheen 9s ease-in-out infinite; 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 { @keyframes hero-sheen {
0%, 100% { background-position: 0% center; } 0%, 100% { background-position: 0% center; }
50% { background-position: 100% center; } 50% { background-position: 100% center; }
+1 -1
View File
@@ -59,7 +59,7 @@ const structuredData = {
name: "Such Software LLC", name: "Such Software LLC",
url: "https://such.software", url: "https://such.software",
logo: "https://such.software/images/branding/icon-512.png", logo: "https://such.software/images/branding/icon-512.png",
description: "E-commerce infrastructure, custom app development, and accessible web applications. Engineered for scale, compliance, and performance.", description: "A Kennett Square, PA software studio building custom online stores, web apps, mobile games, and Monero & crypto tools.",
address: { address: {
"@type": "PostalAddress", "@type": "PostalAddress",
streetAddress: "110 E State St, Suite 300", streetAddress: "110 E State St, Suite 300",
+5 -5
View File
@@ -21,23 +21,23 @@ export default function Home() {
{/* Decorative aura behind the title (aria-hidden, LCP-safe, motion-safe). */} {/* Decorative aura behind the title (aria-hidden, LCP-safe, motion-safe). */}
<div <div
aria-hidden="true" aria-hidden="true"
className="hero-aura pointer-events-none absolute left-1/2 top-24 sm:top-16 -z-10 h-64 w-[46rem] max-w-[92vw] rounded-full bg-cyan-400/25 blur-3xl" className="hero-aura pointer-events-none absolute left-1/2 top-24 sm:top-16 -z-10 h-64 w-[46rem] max-w-[92vw] rounded-full bg-[#12857e]/25 blur-3xl"
/> />
<h1 <h1
id="hero-title" id="hero-title"
className="hero-title text-4xl sm:text-5xl md:text-7xl font-bold mb-6 pb-2" className="hero-title text-4xl sm:text-5xl md:text-7xl font-bold mb-6 pb-2"
style={{ maxWidth: "min(calc(90vw - 120px), 100%)" }} style={{ maxWidth: "min(calc(90vw - 120px), 100%)" }}
> >
<span className="block">Precision Engineering</span> <span className="block">Custom E-commerce,</span>
<span className="block">for Everyone</span> <span className="block">Apps &amp; Crypto Tools</span>
</h1> </h1>
<p className="max-w-2xl text-xl text-muted-foreground mb-8"> <p className="max-w-2xl text-xl text-muted-foreground mb-8">
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.
</p> </p>
<div className="flex flex-wrap justify-center gap-4 mb-14"> <div className="flex flex-wrap justify-center gap-4 mb-14">
<Link href="/products"> <Link href="/products">
<Button size="lg" className="rounded-full shadow-lg shadow-cyan-500/25 hover:shadow-cyan-400/40 hover:scale-[1.03] transition-all">View Portfolio</Button> <Button size="lg" className="rounded-full shadow-lg shadow-[#12857e]/25 hover:shadow-[#3ad4c6]/40 hover:scale-[1.03] transition-all">View Portfolio</Button>
</Link> </Link>
<Link href="#contact"> <Link href="#contact">
<Button variant="outline" size="lg" className="rounded-full hover:scale-[1.03] transition-all">Start a Project</Button> <Button variant="outline" size="lg" className="rounded-full hover:scale-[1.03] transition-all">Start a Project</Button>
+5 -5
View File
@@ -4,11 +4,11 @@ import { ProductsGrid, type Product } from "@/components/portfolio/products-grid
import type { Metadata } from "next"; import type { Metadata } from "next";
export const metadata: Metadata = { 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.", 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" }, alternates: { canonical: "/products" },
openGraph: { 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.", 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", type: "website",
}, },
@@ -146,13 +146,13 @@ export default function ProductsPage() {
return ( return (
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground"> <main className="relative min-h-screen flex flex-col items-center bg-background text-foreground">
<Header /> <Header />
<div id="main-content" className="z-10 w-full max-w-5xl mx-auto py-20 px-4 pb-24 md:pb-20"> <div id="main-content" className="z-10 w-full max-w-6xl mx-auto py-20 px-4 pb-24 md:pb-20">
<div className="text-center mb-16"> <div className="text-center mb-16">
<h1 className="text-4xl md:text-6xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-amber-400 to-purple-500"> <h1 className="text-4xl md:text-6xl font-bold mb-6 brand-gradient-text">
Portfolio Portfolio
</h1> </h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto"> <p className="text-xl text-muted-foreground max-w-2xl mx-auto">
The tools and apps we have shipped, from commerce backends to consumer games, plus three nuclear-physics games in development. Products &amp; projects — from commerce backends to consumer games, plus three nuclear-physics games in development.
</p> </p>
</div> </div>
+1 -1
View File
@@ -33,7 +33,7 @@ export default function ServicesPage() {
<div id="main-content" className="z-10 w-full max-w-5xl mx-auto py-20 px-4"> <div id="main-content" className="z-10 w-full max-w-5xl mx-auto py-20 px-4">
{/* Page header */} {/* Page header */}
<div className="text-center mb-12"> <div className="text-center mb-12">
<h1 className="text-4xl md:text-6xl font-bold mb-6 pb-3 bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 to-blue-500"> <h1 className="text-4xl md:text-6xl font-bold mb-6 pb-3 brand-gradient-text">
What we build What we build
</h1> </h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto"> <p className="text-xl text-muted-foreground max-w-2xl mx-auto">
@@ -196,7 +196,7 @@ export function HeroSplash({ onEnter, sectionRef, leaving }: { onEnter: () => vo
{/* Cover text hidden on mobile (logo + corner icons carry it there); fades out on enter */} {/* Cover text hidden on mobile (logo + corner icons carry it there); fades out on enter */}
<p <p
className={`mt-10 hidden sm:block 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 transition-opacity duration-700 motion-safe:animate-in motion-safe:fade-in ${entering ? "opacity-0" : "opacity-100"}`} className={`mt-10 hidden sm:block brand-gradient-text pb-1 text-3xl font-bold text-transparent sm:text-4xl md:text-5xl transition-opacity duration-700 motion-safe:animate-in motion-safe:fade-in ${entering ? "opacity-0" : "opacity-100"}`}
style={{ animationDuration: "700ms", animationDelay: "120ms", animationFillMode: "backwards" }} style={{ animationDuration: "700ms", animationDelay: "120ms", animationFillMode: "backwards" }}
> >
Precision Engineering for Everyone Precision Engineering for Everyone
@@ -49,16 +49,18 @@ export function HomeShell({ children }: { children: ReactNode }) {
if (entered) return; if (entered) return;
const el = splashRef.current; const el = splashRef.current;
if (!el) return; 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( const io = new IntersectionObserver(
([entry]) => { ([entry]) => {
if (!entry.isIntersecting) { if (entry.intersectionRatio < 0.6) {
restoreScroll.current = Math.max(0, window.scrollY - el.offsetHeight); restoreScroll.current = Math.max(0, window.scrollY - el.offsetHeight);
sessionStorage.setItem("such:entered", "1"); sessionStorage.setItem("such:entered", "1");
setEntered(true); setEntered(true);
setSplashUp(false); // scrolled past: just unmount, no overlay fade needed setSplashUp(false); // scrolled past: just unmount, no overlay fade needed
} }
}, },
{ threshold: 0 }, { threshold: [0, 0.6] },
); );
io.observe(el); io.observe(el);
return () => io.disconnect(); return () => io.disconnect();
@@ -133,16 +133,16 @@ export function ProductsGrid({ products }: { products: Product[] }) {
id="product-sort" id="product-sort"
value={sort} value={sort}
onChange={(e) => setSort(e.target.value as SortKey)} 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"
> >
<option value="release">Release date (newest)</option> <option value="release">Release date (newest)</option>
<option value="name">Name (A–Z)</option> <option value="name">Name (A–Z)</option>
</select> </select>
</div> </div>
<div className="grid md:grid-cols-2 gap-8"> <div className="grid md:grid-cols-2 xl:grid-cols-3 gap-8">
{sorted.map((product, i) => ( {sorted.map((product, i) => (
<ScrollReveal key={product.slug} delay={(i % 2) * 0.08}> <ScrollReveal key={product.slug} delay={(i % 3) * 0.08}>
<Link href={`/products/${product.slug}`} className="block h-full"> <Link href={`/products/${product.slug}`} className="block h-full">
<div className="h-full cursor-pointer"> <div className="h-full cursor-pointer">
<ProductCardInner product={product} /> <ProductCardInner product={product} />