fix: consent mode v2 compliance + visual enhancements across sections
Some checks failed
Build & Push / build-and-push (push) Failing after 52s

Google Consent Mode v2: region-specific defaults (granted globally,
denied for EEA/UK), update all 4 consent types on accept/decline,
add url_passthrough and ads_data_redaction for better measurement.

Visual: unified 48px grid texture across all light sections, animated
constellation SVG in Process section, radial glow on Philosophy,
removed broken SVG connector lines and unused imports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 16:19:24 -04:00
parent 1b09059467
commit 09b91b1292
11 changed files with 233 additions and 87 deletions

3
.gitignore vendored
View File

@@ -31,3 +31,6 @@ public/media/
# superpowers # superpowers
.superpowers/ .superpowers/
# private credentials
private/

27
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@calcom/embed-react": "^1.5.3", "@calcom/embed-react": "^1.5.3",
"@google/genai": "^1.46.0", "@google/genai": "^1.48.0",
"@payloadcms/db-postgres": "^3.80.0", "@payloadcms/db-postgres": "^3.80.0",
"@payloadcms/next": "^3.80.0", "@payloadcms/next": "^3.80.0",
"@payloadcms/richtext-lexical": "^3.80.0", "@payloadcms/richtext-lexical": "^3.80.0",
@@ -1435,9 +1435,9 @@
} }
}, },
"node_modules/@google/genai": { "node_modules/@google/genai": {
"version": "1.46.0", "version": "1.48.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.46.0.tgz", "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.48.0.tgz",
"integrity": "sha512-ewPMN5JkKfgU5/kdco9ZhXBHDPhVqZpMQqIFQhwsHLf8kyZfx1cNpw1pHo1eV6PGEW7EhIBFi3aYZraFndAXqg==", "integrity": "sha512-plonYK4ML2PrxsRD9SeqmFt76eREWkQdPCglOA6aYDzL1AAbE+7PUnT54SvpWGfws13L0AZEqGSpL7+1IPnTxQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"google-auth-library": "^10.3.0", "google-auth-library": "^10.3.0",
@@ -3633,14 +3633,6 @@
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@types/unist": { "node_modules/@types/unist": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -6714,17 +6706,6 @@
} }
} }
}, },
"node_modules/next-intl/node_modules/@swc/helpers": {
"version": "0.5.20",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.20.tgz",
"integrity": "sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",

View File

@@ -11,7 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@calcom/embed-react": "^1.5.3", "@calcom/embed-react": "^1.5.3",
"@google/genai": "^1.46.0", "@google/genai": "^1.48.0",
"@payloadcms/db-postgres": "^3.80.0", "@payloadcms/db-postgres": "^3.80.0",
"@payloadcms/next": "^3.80.0", "@payloadcms/next": "^3.80.0",
"@payloadcms/richtext-lexical": "^3.80.0", "@payloadcms/richtext-lexical": "^3.80.0",

View File

@@ -10,6 +10,9 @@ function updateConsent(state: ConsentState) {
if (typeof window !== 'undefined' && typeof window.gtag === 'function') { if (typeof window !== 'undefined' && typeof window.gtag === 'function') {
window.gtag('consent', 'update', { window.gtag('consent', 'update', {
analytics_storage: state, analytics_storage: state,
ad_storage: state,
ad_user_data: state,
ad_personalization: state,
}) })
} }
} }

View File

@@ -7,18 +7,33 @@ export default function GoogleAnalytics() {
return ( return (
<> <>
{/* Consent Mode v2 — default to denied for EEA compliance */} {/* Consent Mode v2 — region-specific defaults */}
<Script id="gtag-consent" strategy="beforeInteractive"> <Script id="gtag-consent" strategy="beforeInteractive">
{` {`
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);} function gtag(){dataLayer.push(arguments);}
// Default: granted for regions without consent requirements
gtag('consent', 'default', {
analytics_storage: 'granted',
ad_storage: 'granted',
ad_user_data: 'granted',
ad_personalization: 'granted',
});
// EEA + UK: denied until user consents (GDPR)
gtag('consent', 'default', { gtag('consent', 'default', {
analytics_storage: 'denied', analytics_storage: 'denied',
ad_storage: 'denied', ad_storage: 'denied',
ad_user_data: 'denied', ad_user_data: 'denied',
ad_personalization: 'denied', ad_personalization: 'denied',
wait_for_update: 500, wait_for_update: 500,
region: ['AT','BE','BG','CY','CZ','DE','DK','EE','ES','FI','FR','GR','HR','HU','IE','IT','LT','LU','LV','MT','NL','PL','PT','RO','SE','SI','SK','IS','LI','NO','GB'],
}); });
// Improve measurement when consent is denied
gtag('set', 'url_passthrough', true);
gtag('set', 'ads_data_redaction', true);
`} `}
</Script> </Script>

View File

@@ -36,7 +36,16 @@ export default function Configurator() {
]; ];
return ( return (
<section id="configure" className="relative bg-surface py-24 overflow-hidden"> <section
id="configure"
className="relative bg-surface py-24 overflow-hidden"
style={{
backgroundImage: [
'repeating-linear-gradient(0deg, rgba(25,28,29,0.02) 0px, rgba(25,28,29,0.02) 1px, transparent 1px, transparent 48px)',
'repeating-linear-gradient(90deg, rgba(25,28,29,0.02) 0px, rgba(25,28,29,0.02) 1px, transparent 1px, transparent 48px)',
].join(', '),
}}
>
{/* Subtle diagonal accent line */} {/* Subtle diagonal accent line */}
<div <div
className="absolute top-0 left-0 right-0 h-px pointer-events-none" className="absolute top-0 left-0 right-0 h-px pointer-events-none"

View File

@@ -57,7 +57,16 @@ export default function Discovery() {
if (!voiceSupported) return null; if (!voiceSupported) return null;
return ( return (
<section id="discover" className="relative bg-surface-high py-24 overflow-hidden"> <section
id="discover"
className="relative bg-surface-high py-24 overflow-hidden"
style={{
backgroundImage: [
'repeating-linear-gradient(0deg, rgba(25,28,29,0.02) 0px, rgba(25,28,29,0.02) 1px, transparent 1px, transparent 48px)',
'repeating-linear-gradient(90deg, rgba(25,28,29,0.02) 0px, rgba(25,28,29,0.02) 1px, transparent 1px, transparent 48px)',
].join(', '),
}}
>
{/* Top accent line */} {/* Top accent line */}
<div <div
className="absolute top-0 left-0 right-0 h-px pointer-events-none" className="absolute top-0 left-0 right-0 h-px pointer-events-none"

View File

@@ -131,7 +131,24 @@ export default function Philosophy() {
const t = useTranslations(); const t = useTranslations();
return ( return (
<section id="about" className="bg-surface py-20"> <section
id="about"
className="relative bg-surface py-20 overflow-hidden"
style={{
backgroundImage: [
'repeating-linear-gradient(0deg, rgba(25,28,29,0.02) 0px, rgba(25,28,29,0.02) 1px, transparent 1px, transparent 48px)',
'repeating-linear-gradient(90deg, rgba(25,28,29,0.02) 0px, rgba(25,28,29,0.02) 1px, transparent 1px, transparent 48px)',
].join(', '),
}}
>
{/* Ambient radial glow */}
<div
className="absolute inset-0 pointer-events-none"
style={{
background: 'radial-gradient(ellipse 80% 60% at 20% 50%, rgba(91,164,217,0.04) 0%, transparent 70%)',
}}
aria-hidden="true"
/>
<div className="container mx-auto px-6"> <div className="container mx-auto px-6">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-start"> <div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-start">

View File

@@ -2,8 +2,6 @@
import { motion, type Variants } from 'framer-motion'; import { motion, type Variants } from 'framer-motion';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import type { LucideIcon } from 'lucide-react';
import { Search, LayoutDashboard, PenTool, Rocket } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import {
staggerContainerWide, staggerContainerWide,
@@ -17,16 +15,15 @@ import SectionHeader from '@/components/ui/SectionHeader';
interface Step { interface Step {
numeral: string; numeral: string;
key: 'discovery' | 'strategy' | 'build' | 'launch'; key: 'discovery' | 'strategy' | 'build' | 'launch';
Icon: LucideIcon;
} }
// ─── Data ───────────────────────────────────────────────────────────────────── // ─── Data ─────────────────────────────────────────────────────────────────────
const STEPS: Step[] = [ const STEPS: Step[] = [
{ numeral: '01', key: 'discovery', Icon: Search }, { numeral: '01', key: 'discovery' },
{ numeral: '02', key: 'strategy', Icon: LayoutDashboard }, { numeral: '02', key: 'strategy' },
{ numeral: '03', key: 'build', Icon: PenTool }, { numeral: '03', key: 'build' },
{ numeral: '04', key: 'launch', Icon: Rocket }, { numeral: '04', key: 'launch' },
]; ];
// ─── Variants ───────────────────────────────────────────────────────────────── // ─── Variants ─────────────────────────────────────────────────────────────────
@@ -44,9 +41,134 @@ const numeralScaleVariants: Variants = {
}, },
}; };
// ─── Animated blueprint network (desktop only) ───────────────────────────────
function FlowNetwork() {
// Radial constellation — center hub with curved spokes to outer nodes
const cx = 130, cy = 100; // center
const nodes = [
{ x: 50, y: 30 }, // top-left
{ x: 210, y: 25 }, // top-right
{ x: 240, y: 110 }, // right
{ x: 195, y: 180 }, // bottom-right
{ x: 55, y: 170 }, // bottom-left
{ x: 20, y: 95 }, // left
];
return (
<div className="hidden lg:block relative mt-8 w-full h-52" aria-hidden="true">
<style>{`
@keyframes flow-dash { to { stroke-dashoffset: -40; } }
@keyframes flow-dash-slow { to { stroke-dashoffset: -40; } }
@keyframes node-pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
@keyframes orbit-a {
from { transform: rotate(0deg) translateX(28px) rotate(0deg); }
to { transform: rotate(360deg) translateX(28px) rotate(-360deg); }
}
@keyframes orbit-b {
from { transform: rotate(120deg) translateX(22px) rotate(-120deg); }
to { transform: rotate(480deg) translateX(22px) rotate(-480deg); }
}
@keyframes orbit-c {
from { transform: rotate(240deg) translateX(35px) rotate(-240deg); }
to { transform: rotate(-120deg) translateX(35px) rotate(120deg); }
}
@keyframes ring-breathe {
0%, 100% { opacity: 0.06; }
50% { opacity: 0.14; }
}
.process-flow { animation: flow-dash 2.2s linear infinite; }
.process-flow-slow { animation: flow-dash-slow 3.5s linear infinite; }
.process-pulse { animation: node-pulse 2.8s ease-in-out infinite; }
.process-orbit-a { animation: orbit-a 8s linear infinite; }
.process-orbit-b { animation: orbit-b 11s linear infinite; }
.process-orbit-c { animation: orbit-c 14s linear infinite; }
.process-ring { animation: ring-breathe 4s ease-in-out infinite; }
.process-ring-lg { animation: ring-breathe 5.5s ease-in-out infinite; }
`}</style>
<svg className="w-full h-full" viewBox="0 0 260 200" fill="none" xmlns="http://www.w3.org/2000/svg">
{/* Breathing rings */}
<circle cx={cx} cy={cy} r="24" stroke="rgba(91,164,217,0.1)" strokeWidth="1" fill="none" className="process-ring" />
<circle cx={cx} cy={cy} r="42" stroke="rgba(91,164,217,0.07)" strokeWidth="1" fill="none" className="process-ring-lg" />
<circle cx={cx} cy={cy} r="65" stroke="rgba(91,164,217,0.04)" strokeWidth="0.8" fill="none" strokeDasharray="3 6" />
{/* Curved spokes from center to each outer node */}
{nodes.map((n, i) => {
const mx = cx + (n.x - cx) * 0.5 + (i % 2 === 0 ? 15 : -15);
const my = cy + (n.y - cy) * 0.5 + (i % 2 === 0 ? -12 : 12);
return (
<path
key={i}
d={`M ${cx} ${cy} Q ${mx} ${my} ${n.x} ${n.y}`}
stroke="rgba(91,164,217,0.18)"
strokeWidth="1.2"
strokeDasharray="5 4"
strokeLinecap="round"
className={i % 2 === 0 ? 'process-flow' : 'process-flow-slow'}
style={{ animationDelay: `${i * 0.3}s` }}
/>
);
})}
{/* Faint arcs connecting adjacent outer nodes */}
{nodes.map((n, i) => {
const next = nodes[(i + 1) % nodes.length];
const mx = cx + (n.x + next.x - 2 * cx) * 0.15;
const my = cy + (n.y + next.y - 2 * cy) * 0.15;
return (
<path
key={`arc-${i}`}
d={`M ${n.x} ${n.y} Q ${mx} ${my} ${next.x} ${next.y}`}
stroke="rgba(91,164,217,0.08)"
strokeWidth="0.8"
strokeDasharray="3 5"
strokeLinecap="round"
/>
);
})}
{/* Outer nodes — pulsing */}
{nodes.map((n, i) => (
<circle
key={`node-${i}`}
cx={n.x}
cy={n.y}
r="3"
fill="#5BA4D9"
className="process-pulse"
style={{ animationDelay: `${i * 0.45}s` }}
/>
))}
{/* Center hub */}
<circle cx={cx} cy={cy} r="5" fill="#5BA4D9" opacity="0.85" />
<circle cx={cx} cy={cy} r="2" fill="#fff" opacity="0.9" />
{/* Orbiting particles */}
<g style={{ transformOrigin: `${cx}px ${cy}px` }}>
<circle cx={cx} cy={cy} r="2" fill="#5BA4D9" opacity="0.7" className="process-orbit-a" />
</g>
<g style={{ transformOrigin: `${cx}px ${cy}px` }}>
<circle cx={cx} cy={cy} r="1.5" fill="rgba(91,164,217,0.5)" className="process-orbit-b" />
</g>
<g style={{ transformOrigin: `${cx}px ${cy}px` }}>
<circle cx={cx} cy={cy} r="1.5" fill="rgba(91,164,217,0.35)" className="process-orbit-c" />
</g>
{/* Corner brackets — architectural detail */}
<path d="M 14 25 L 14 20 L 19 20" stroke="rgba(28,43,58,0.12)" strokeWidth="0.8" fill="none" />
<path d="M 246 175 L 246 180 L 241 180" stroke="rgba(28,43,58,0.12)" strokeWidth="0.8" fill="none" />
</svg>
</div>
);
}
// ─── Sub-components ─────────────────────────────────────────────────────────── // ─── Sub-components ───────────────────────────────────────────────────────────
function StepCard({ numeral, stepKey, Icon }: { numeral: string; stepKey: string; Icon: LucideIcon }) { function StepCard({ numeral, stepKey }: { numeral: string; stepKey: string }) {
const t = useTranslations(); const t = useTranslations();
const title = t(`process.steps.${stepKey}.title`); const title = t(`process.steps.${stepKey}.title`);
const description = t(`process.steps.${stepKey}.description`); const description = t(`process.steps.${stepKey}.description`);
@@ -94,7 +216,16 @@ export default function Process() {
const t = useTranslations(); const t = useTranslations();
return ( return (
<section id="process" className="bg-surface-low py-20"> <section
id="process"
className="relative bg-surface py-20"
style={{
backgroundImage: [
'repeating-linear-gradient(0deg, rgba(25,28,29,0.02) 0px, rgba(25,28,29,0.02) 1px, transparent 1px, transparent 48px)',
'repeating-linear-gradient(90deg, rgba(25,28,29,0.02) 0px, rgba(25,28,29,0.02) 1px, transparent 1px, transparent 48px)',
].join(', '),
}}
>
<div className="container mx-auto px-6"> <div className="container mx-auto px-6">
{/* {/*
@@ -121,47 +252,11 @@ export default function Process() {
align="left" align="left"
/> />
</div> </div>
<FlowNetwork />
</div> </div>
{/* ── Steps column ── */} {/* ── Steps column ── */}
<div className="lg:col-span-3"> <div className="lg:col-span-3">
<div className="relative">
{/* Dashed connector line — visible on sm+ grid layouts only */}
<div
className="hidden sm:block absolute inset-0 pointer-events-none"
aria-hidden="true"
>
<svg
className="w-full h-full"
preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* Horizontal dashes across the middle gap */}
<line
x1="50%" y1="50%"
x2="50%" y2="50%"
stroke="rgba(91,164,217,0.18)"
strokeWidth="1.5"
strokeDasharray="4 6"
/>
{/* Vertical dashes down the centre gap */}
<line
x1="0%" y1="50%"
x2="100%" y2="50%"
stroke="rgba(91,164,217,0.18)"
strokeWidth="1.5"
strokeDasharray="4 6"
/>
<line
x1="50%" y1="0%"
x2="50%" y2="100%"
stroke="rgba(91,164,217,0.18)"
strokeWidth="1.5"
strokeDasharray="4 6"
/>
</svg>
</div>
<motion.div <motion.div
variants={staggerContainerWide} variants={staggerContainerWide}
initial="hidden" initial="hidden"
@@ -170,11 +265,10 @@ export default function Process() {
className="grid grid-cols-1 sm:grid-cols-2 gap-5" className="grid grid-cols-1 sm:grid-cols-2 gap-5"
> >
{STEPS.map((step) => ( {STEPS.map((step) => (
<StepCard key={step.key} numeral={step.numeral} stepKey={step.key} Icon={step.Icon} /> <StepCard key={step.key} numeral={step.numeral} stepKey={step.key} />
))} ))}
</motion.div> </motion.div>
</div> </div>
</div>
</div> </div>
</div> </div>

View File

@@ -286,7 +286,16 @@ export default function SelectedWorks() {
const secondaryProjects = PROJECTS.filter((p) => !p.featured); const secondaryProjects = PROJECTS.filter((p) => !p.featured);
return ( return (
<section id="work" className="bg-surface-low py-20"> <section
id="work"
className="bg-surface-low py-20"
style={{
backgroundImage: [
'repeating-linear-gradient(0deg, rgba(25,28,29,0.02) 0px, rgba(25,28,29,0.02) 1px, transparent 1px, transparent 48px)',
'repeating-linear-gradient(90deg, rgba(25,28,29,0.02) 0px, rgba(25,28,29,0.02) 1px, transparent 1px, transparent 48px)',
].join(', '),
}}
>
<style>{` <style>{`
@keyframes dashed-drift { @keyframes dashed-drift {
0% { background-position: 0 0, 100% 0, 100% 100%, 0 100%; } 0% { background-position: 0 0, 100% 0, 100% 100%, 0 100%; }

View File

@@ -112,6 +112,12 @@ export default function TrustBar() {
<section <section
aria-label="Trust indicators" aria-label="Trust indicators"
className="relative bg-surface-low py-12" className="relative bg-surface-low py-12"
style={{
backgroundImage: [
'repeating-linear-gradient(0deg, rgba(25,28,29,0.02) 0px, rgba(25,28,29,0.02) 1px, transparent 1px, transparent 48px)',
'repeating-linear-gradient(90deg, rgba(25,28,29,0.02) 0px, rgba(25,28,29,0.02) 1px, transparent 1px, transparent 48px)',
].join(', '),
}}
> >
{/* Gradient bridge — blends hero into this section */} {/* Gradient bridge — blends hero into this section */}
<div <div