diff --git a/apps/web/messages/it.json b/apps/web/messages/it.json
index a3188a5..30c4c3a 100644
--- a/apps/web/messages/it.json
+++ b/apps/web/messages/it.json
@@ -1,9 +1,12 @@
{
"Home": {
"title": "Una keto italiana, sostenibile",
+ "titleAccent": "sostenibile",
"tagline": "Il sentiero che ti accompagna dalla prima settimana di adattamento al mantenimento — settimana dopo settimana, piatto dopo piatto.",
"ctaPrimary": "Iscriviti",
"ctaSecondary": "Accedi",
+ "ctaTryFree": "Inizia gratis",
+ "ctaTryFreeHint": "Niente carta, niente spam. Disinstalla quando vuoi.",
"disclaimer": "KetoPath è uno strumento di stile di vita: suggerisce, non prescrive. Le indicazioni nutrizionali non sostituiscono il parere del tuo medico.",
"signedInAs": "Accesso effettuato come {name}",
"signOut": "Esci",
@@ -17,7 +20,42 @@
"greeting": "Buongiorno, {name} .",
"principle1": "Personalizzazione totale: ciò che vedi è solo ciò che ti riguarda.",
"principle2": "Cucina italiana e mediterranea: ingredienti veri, di stagione, reperibili.",
- "principle3": "Progressività: dalla fase intensiva al mantenimento, senza strappi."
+ "principle3": "Progressività: dalla fase intensiva al mantenimento, senza strappi.",
+ "stat1Label": "Fasi del percorso",
+ "stat1Value": "3",
+ "stat1Hint": "Intensiva · Transizione · Mantenimento",
+ "stat2Label": "Ricette mediterranee",
+ "stat2Value": "96+",
+ "stat2Hint": "Tutte stagionali, italiane",
+ "stat3Label": "Costo del piano",
+ "stat3Value": "0€",
+ "stat3Hint": "Gratis durante la beta",
+ "featureSectionEyebrow": "Cosa fa per te",
+ "featureSectionTitle": "Tre strumenti, un solo ritmo.",
+ "feature1Eyebrow": "Cucina",
+ "feature1Title": "Piano settimanale che si adatta a te",
+ "feature1Body": "Ogni domenica un menù di sette giorni costruito sui tuoi macros, le tue esclusioni e il tempo che hai in cucina. La lista della spesa si genera da sola.",
+ "feature2Eyebrow": "Digiuno",
+ "feature2Title": "Digiuno intermittente, senza ansia",
+ "feature2Body": "Sei protocolli, dal 14:10 al 5:2. Il timer ti accompagna con micro-promemoria di idratazione, elettroliti, e una proposta morbida per rompere il digiuno.",
+ "feature3Eyebrow": "Tracking",
+ "feature3Title": "Numeri che parlano, non urlano",
+ "feature3Body": "Una pesata a settimana, qualche misura quando vuoi, una foto se ti va. Trend lineare, soglia d'allarme, esportazione PDF per il medico.",
+ "previewSectionEyebrow": "Sotto il cofano",
+ "previewSectionTitle": "L'app, in tre schermate.",
+ "previewSectionBody": "Niente dashboard infinite. Tre superfici essenziali: il piano di oggi, la sessione di digiuno, la pesata della settimana.",
+ "previewPlanLabel": "Mercoledì · giorno III",
+ "previewPlanRecipe1": "Frittata di spinaci",
+ "previewPlanRecipe2": "Tagliata con rucola",
+ "previewPlanRecipe3": "Branzino al cartoccio",
+ "previewFastLabel": "16:8 · in corso",
+ "previewFastTime": "08:42",
+ "previewFastHint": "Ancora 7h 18m alla finestra alimentare.",
+ "previewWeightLabel": "−4,2 kg in 8 settimane",
+ "ctaSectionTitle": "Pronti a partire?",
+ "ctaSectionBody": "Sette passaggi, dieci minuti, e il tuo primo piano è pronto. Senza carta di credito.",
+ "principlesEyebrow": "Tre principi non negoziabili",
+ "footerCopyright": "KetoPath — anno I, Italia."
},
"Welcome": {
"title": "Prima di iniziare",
diff --git a/apps/web/src/app/[locale]/fasting/page.tsx b/apps/web/src/app/[locale]/fasting/page.tsx
index 5e8a8f5..54acabe 100644
--- a/apps/web/src/app/[locale]/fasting/page.tsx
+++ b/apps/web/src/app/[locale]/fasting/page.tsx
@@ -3,6 +3,7 @@ import { redirect } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { setRequestLocale } from 'next-intl/server';
+import { CursorGlow } from '@/components/cursor-glow';
import { Masthead } from '@/components/masthead';
import { getServerSession } from '@/lib/auth';
@@ -41,29 +42,58 @@ function FastingPageContent({
const t = useTranslations('Fasting');
return (
-
-
-
- Capitolo VII
-
- {t('title')}
- .
-
-
- {t('subtitle')}
-
+
+
+
+
+
+
+
+
+ VII
+
+
+
+ Capitolo VII — Digiuno
+
+
+
+
+ {t('title')}
+ .
+
+
+
+ {t('subtitle')}
+
-
+
-
+
+
-
+
-
-
{t('historyTitle')}
-
-
-
+
+
{t('historyTitle')}
+
+
+
+
+
);
}
diff --git a/apps/web/src/app/[locale]/onboarding/page.tsx b/apps/web/src/app/[locale]/onboarding/page.tsx
index 37433d2..c77c974 100644
--- a/apps/web/src/app/[locale]/onboarding/page.tsx
+++ b/apps/web/src/app/[locale]/onboarding/page.tsx
@@ -3,6 +3,7 @@ import { redirect } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { setRequestLocale } from 'next-intl/server';
+import { CursorGlow } from '@/components/cursor-glow';
import { Masthead } from '@/components/masthead';
import { getServerSession } from '@/lib/auth';
@@ -47,24 +48,57 @@ function OnboardingPageContent({
const t = useTranslations('Onboarding');
return (
-
-
-
- Capitolo III
-
- {t('title')}
- .
-
-
- {t('subtitle')}
-
+
+
+
+
+
+
+
-
+ {/* Chapter "III" gigante in stroke */}
+
+ III
+
-
-
+ {/* Eyebrow verticale destra */}
+
+
+ Capitolo III — Inizio
+
+
-
+ {/* Titolo bleed-left */}
+
+ {t('title')}
+
+
+
+ {t('subtitle')}
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx
index 780e4ce..ec2e5b7 100644
--- a/apps/web/src/app/[locale]/page.tsx
+++ b/apps/web/src/app/[locale]/page.tsx
@@ -2,8 +2,8 @@ import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { setRequestLocale } from 'next-intl/server';
+import { CursorGlow } from '@/components/cursor-glow';
import { Masthead } from '@/components/masthead';
-import { Button } from '@/components/ui/button';
import { getServerSession } from '@/lib/auth';
import { SignOutButton } from './sign-out-button';
@@ -16,114 +16,936 @@ export default async function HomePage({ params: { locale } }: { params: { local
}
function HomeContent({ userName }: { userName: string | null }) {
+ return (
+
+
+
+ {userName ? : }
+
+
+ );
+}
+
+/* ─────────────────────────────────────────────────────────────────────────
+ * Marketing landing — Asymmetric, drastico.
+ * Mosse: chapter markers giganti come art di sfondo, titoli che sbordano
+ * il viewport (bleed-left), mockup ruotato che si sovrappone al titolo,
+ * eyebrow verticali, card ruotate ad angoli diversi.
+ * ───────────────────────────────────────────────────────────────────────── */
+function MarketingLanding() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+/* HERO — title rompe il bordo sinistro del viewport, mockup absolute
+ * sovrappone parte del titolo come un timbro. Eyebrow verticale a destra. */
+function Hero() {
const t = useTranslations('Home');
+ const title = t('title');
+ const accent = t('titleAccent');
+ const accentIdx = title.toLowerCase().indexOf(accent.toLowerCase());
+ const before = accentIdx >= 0 ? title.slice(0, accentIdx).trim() : title;
+ const accentText = accentIdx >= 0 ? title.slice(accentIdx, accentIdx + accent.length) : '';
return (
-
-
-
-
-
N. 01 — Manifesto
-
- {t('title')}
- .
-
-
- {t('tagline')}
-
-
+
-
-
-
-
-
-
- {t('disclaimer')}
-
-
-
-
Tre principi
-
-
- I
- {t('principle1')}
-
-
- II
- {t('principle2')}
-
-
- III
- {t('principle3')}
-
-
-
-
-
-
-
- );
-}
-
-function SignedOutBlock({ t }: { t: ReturnType> }) {
- return (
-
-
- {t('ctaPrimary')}
-
-
- {t('ctaSecondary')}
-
-
- );
-}
-
-function SignedInBlock({
- userName,
- t,
-}: {
- userName: string;
- t: ReturnType>;
-}) {
- return (
-
-
-
- {t.rich('greeting', {
- name: () => {userName} ,
- })}
+ {/* Eyebrow VERTICALE sul bordo destro */}
+
+
+ N. 01 — Manifesto · Anno I — 2026
-
-
+
+ {/* Titolo che sborda il bordo sinistro del viewport */}
+
+ {before}
+ italiana,
+
+ {accentText}
+ .
+
+
+
+ {/* Mockup-ring assoluto: si sovrappone al titolo da destra,
+ ruotato leggermente come un timbro. */}
+
+
+ {/* Tagline + CTA: posizioni completamente sganciate, indentate diverse */}
+
+
+ {t('tagline')}
+
+
+
+ {t('ctaTryFree')}
+
+ →
+
+
+
+ {t('ctaSecondary')}
+
+
+ {t('ctaTryFreeHint')}
+
+
+
+
+ );
+}
+
+function PhaseRingsArt() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {[
+ { angle: -90, label: 'I' },
+ { angle: 30, label: 'II' },
+ { angle: 150, label: 'III' },
+ ].map(({ angle, label }) => {
+ const rad = (angle * Math.PI) / 180;
+ const cx = 300 + Math.cos(rad) * 280;
+ const cy = 300 + Math.sin(rad) * 280;
+ return (
+
+
+
+ {label}
+
+
+ );
+ })}
+
+ );
+}
+
+function Marquee() {
+ const tokens = [
+ 'KETO ITALIANA',
+ 'MEDITERRANEA',
+ 'PROGRESSIVA',
+ 'PERSONALIZZATA',
+ 'NIENTE FAME',
+ 'ZERO CARTE',
+ '96+ RICETTE',
+ '3 FASI',
+ 'DIGIUNO INTERMITTENTE',
+ 'GDPR-FIRST',
+ ];
+ const doubled = [...tokens, ...tokens];
+ return (
+
+
+ {doubled.map((tok, i) => (
+
+
+ {tok}
+
+
+ ✦
+
+
+ ))}
+
+
+ );
+}
+
+/* STATS — Una stat occupa 60% della larghezza con numero a 28vw, le altre
+ * due flottano in absolute a posizioni diverse. Sezione anche bleed a destra. */
+function StatsScattered() {
+ const t = useTranslations('Home');
+ return (
+
+ {/* Chapter marker di sfondo: "II" gigante, stroke trasparente */}
+
+ II
+
+
+ {/* Stat 1 — gigante, sborda a sinistra */}
+
+
{t('stat1Label')}
+
+ {t('stat1Value')}
+
+
+ {t('stat1Hint')}
+
+
+
+ {/* Stat 2 — assoluta in alto a destra */}
+
+
{t('stat2Label')}
+
+ {t('stat2Value')}
+
+
+ {t('stat2Hint')}
+
+
+
+ {/* Stat 3 — assoluta in basso a destra, sotto la stat 2 */}
+
+
{t('stat3Label')}
+
+ {t('stat3Value')}
+
+
+ {t('stat3Hint')}
+
+
+
+ );
+}
+
+/* FEATURES — ognuna è una composizione diversa con un chapter marker
+ * gigante in stroke come art di sfondo. Card ruotate a -2°, 0°, +2°. */
+function FeaturesAsymmetric() {
+ const t = useTranslations('Home');
+ return (
+
+
+
+ {/* Feature 1 — chapter "01" gigante a sinistra, card a destra */}
+ }
+ />
+
+ {/* Feature 2 — chapter "02" a destra, card a sinistra (flippata) */}
+ }
+ />
+
+ {/* Feature 3 — chapter "03" centrale, card al centro (rompe il pattern) */}
+ }
+ />
+
+ );
+}
+
+function FeatureBlock({
+ chapter,
+ eyebrow,
+ title,
+ body,
+ accent,
+ align,
+ rotate,
+ icon,
+}: {
+ chapter: string;
+ eyebrow: string;
+ title: string;
+ body: string;
+ accent: 'oliva' | 'pomodoro' | 'oro';
+ align: 'left' | 'right' | 'center';
+ rotate: string;
+ icon: React.ReactNode;
+}) {
+ const accentClass =
+ accent === 'oliva' ? 'text-oliva' : accent === 'pomodoro' ? 'text-pomodoro' : 'text-oro';
+ const tintGrad =
+ accent === 'oliva'
+ ? 'linear-gradient(135deg, hsl(var(--oliva) / 0.18) 0%, hsl(var(--carta-light)) 60%, transparent 100%)'
+ : accent === 'pomodoro'
+ ? 'linear-gradient(135deg, hsl(var(--pomodoro) / 0.18) 0%, hsl(var(--carta-light)) 60%, transparent 100%)'
+ : 'linear-gradient(135deg, hsl(var(--oro) / 0.18) 0%, hsl(var(--carta-light)) 60%, transparent 100%)';
+
+ // Posizionamento del chapter marker per ogni align
+ const chapterPosition =
+ align === 'right'
+ ? '-left-4 top-[-2rem]'
+ : align === 'left'
+ ? '-right-4 top-[-3rem]'
+ : 'left-1/2 top-[-4rem] -translate-x-1/2';
+
+ // Posizionamento della card per ogni align
+ const cardCols =
+ align === 'right'
+ ? 'md:col-start-6 md:col-span-7'
+ : align === 'left'
+ ? 'md:col-start-1 md:col-span-7'
+ : 'md:col-start-3 md:col-span-8';
+
+ return (
+
+ {/* Chapter marker gigante in stroke, fa da art di sfondo */}
+
+ {chapter}
+
+
+ {/* Card della feature, ruotata */}
+
+
+
+
+
{eyebrow}
+
+ {title}
+
+
+ {body}
+
+
+
+ {icon}
+
+
+
);
}
-function NavItem({ href, label, eyebrow }: { href: string; label: string; eyebrow: string }) {
+function PlateIcon({ size = 60 }: { size?: number }) {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+function TimerIcon({ size = 60 }: { size?: number }) {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+function TrendIcon({ size = 60 }: { size?: number }) {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+/* APP SHOWCASE — mockup ruotato che sborda a destra del viewport. */
+function AppShowcaseOffset() {
+ const t = useTranslations('Home');
+ return (
+
+
+
+
+ {/* Chapter marker "IV" gigante */}
+
+ IV
+
+
+
+
+
{t('previewSectionEyebrow')}
+
+ {t('previewSectionTitle')}
+
+
+ {t('previewSectionBody')}
+
+
+
+ {/* Mockup sborda DESTRA del viewport, ruotato +3° */}
+
+
+
+ );
+}
+
+function BrowserMockup({ t }: { t: ReturnType
> }) {
+ return (
+
+
+
+
+
+ ketopath.app/plan
+
+
+
+
+
+
+
+ );
+}
+
+function ShowcasePlanCard({ t }: { t: ReturnType> }) {
+ return (
+
+
+
III
+
+ {t('previewPlanLabel')}
+
+
+
+
+ kcal
+ 1.510
+
+
+ );
+}
+
+function ShowcaseRow({ numeral, label, kcal }: { numeral: string; label: string; kcal: string }) {
+ return (
+
+
+ {numeral}
+ {label}
+
+ {kcal}
+
+ );
+}
+
+function ShowcaseFastCard({ t }: { t: ReturnType> }) {
+ return (
+
+
+
II
+
+ {t('previewFastLabel')}
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('previewFastTime')}
+
+
+
+ {t('previewFastHint')}
+
+
+ );
+}
+
+function ShowcaseWeightCard({ t }: { t: ReturnType> }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {[0, 25, 50, 75, 100, 125, 150, 175, 200].map((x, i) => {
+ const ys = [30, 38, 36, 50, 56, 70, 76, 84, 92];
+ return ;
+ })}
+
+
+ {t('previewWeightLabel')}
+
+
+ );
+}
+
+/* CTA finale — full-bleed dark, "?" gigante che sborda in alto a destra,
+ * titolo ancorato in basso a sinistra. */
+function FinalCta() {
+ const t = useTranslations('Home');
+ const titleNoMark = t('ctaSectionTitle').replace(/\?$/, '');
+ return (
+
+
+
+
+ {/* "?" GIGANTE che sborda fuori dal lato destro e in alto */}
+
+ ?
+
+
+
+ {/* Pull-quote in alto, indentata */}
+
+ {t('ctaSectionBody')}
+
+
+ {/* Titolo gigante in basso, ancorato a sinistra */}
+
+ {titleNoMark}
+ .
+
+
+ {/* CTA in basso a destra, defilato */}
+
+
+ {t('ctaTryFree')}
+
+ →
+
+
+
+ {t('ctaTryFreeHint')}
+
+
+
+
+ );
+}
+
+function Principles() {
+ const t = useTranslations('Home');
+ return (
+
+
+ V
+
+
+ {t('principlesEyebrow')}
+
+
+ I
+ {t('principle1')}
+
+
+ II
+ {t('principle2')}
+
+
+ III
+ {t('principle3')}
+
+
+
+ );
+}
+
+function Disclaimer() {
+ const t = useTranslations('Home');
+ return (
+
+ );
+}
+
+function SignedInDashboard({ userName }: { userName: string }) {
+ const t = useTranslations('Home');
+ return (
+
+
+
+
+
+
+ {/* Chapter marker gigante "I" come art di sfondo */}
+
+ I
+
+
+ {/* Eyebrow verticale a destra */}
+
+
+ {/* Greeting bleed-left, sborda fuori dal container */}
+
+ Buongiorno,
+
+ {userName}
+ .
+
+
+
+
+ {t('tagline')}
+
+
+ {/* Nav asimmetrica: card grande "/plan" a sinistra, le altre 4 in colonna a destra */}
+
+
+
+
+ );
+}
+
+function DashboardHero({
+ href,
+ eyebrow,
+ title,
+ accent,
+}: {
+ href: string;
+ eyebrow: string;
+ title: string;
+ accent: 'oliva' | 'pomodoro' | 'oro';
+}) {
+ const accentClass =
+ accent === 'oliva' ? 'text-oliva' : accent === 'pomodoro' ? 'text-pomodoro' : 'text-oro';
+ const tintGrad =
+ accent === 'oliva'
+ ? 'linear-gradient(135deg, hsl(var(--oliva) / 0.18) 0%, hsl(var(--carta-light)) 70%)'
+ : accent === 'pomodoro'
+ ? 'linear-gradient(135deg, hsl(var(--pomodoro) / 0.18) 0%, hsl(var(--carta-light)) 70%)'
+ : 'linear-gradient(135deg, hsl(var(--oro) / 0.18) 0%, hsl(var(--carta-light)) 70%)';
+ return (
+
+
+
+ {eyebrow}
+
+
+
+ );
+}
+
+function NavItem({
+ href,
+ label,
+ eyebrow,
+ chapter,
+}: {
+ href: string;
+ label: string;
+ eyebrow: string;
+ chapter?: string;
+}) {
return (
-
- {eyebrow}
- {label}
+
+ {chapter ? (
+
+ {chapter}
+
+ ) : null}
+
+ {eyebrow}
+ {label}
+
-
-
-
-
-
Capitolo V
-
+
+
+
+
+
+
+
+
+ {/* Chapter "V" gigante in stroke */}
+
+ V
+
+
+ {/* Eyebrow verticale destra */}
+
+
+ Capitolo V — Cucina
+
+
+
+ {/* Titolo che sborda */}
+
+
{t('title')}
.
-
-
- {plan ? formatWeek(plan.weekStart) : t('noPlan')}
-
- {plan?.fastingProtocol ? (
-
- {t('window')} {' '}
-
- {t(`protocolLabel.${plan.fastingProtocol}`)}
-
-
- ) : null}
- {plan?.currentPhase === 'TRANSITION' && plan.phase2Week ? (
-
- {t('phase2Eyebrow')} {' '}
-
- {t('phase2Week', { n: plan.phase2Week })}
-
-
- ) : plan?.currentPhase === 'MAINTENANCE' ? (
-
- {t('phase3Eyebrow')}
-
- ) : null}
-
-
-
+
+
-
-
- {plan ? (
-
-
+
+
+
+ {plan ? formatWeek(plan.weekStart) : t('noPlan')}
+
+
+ {plan?.fastingProtocol ? (
+
+ {t('window')}
+
+ {t(`protocolLabel.${plan.fastingProtocol}`)}
+
+
+ ) : null}
+ {plan?.currentPhase === 'TRANSITION' && plan.phase2Week ? (
+
+ {t('phase2Eyebrow')}
+
+ {t('phase2Week', { n: plan.phase2Week })}
+
+
+ ) : plan?.currentPhase === 'MAINTENANCE' ? (
+ {t('phase3Eyebrow')}
+ ) : null}
+
+
+
- ) : null}
-
+
+
+
+ {plan ? (
+
+ ) : null}
+
+
);
}
diff --git a/apps/web/src/app/[locale]/profile/page.tsx b/apps/web/src/app/[locale]/profile/page.tsx
index 43830a1..a125b2b 100644
--- a/apps/web/src/app/[locale]/profile/page.tsx
+++ b/apps/web/src/app/[locale]/profile/page.tsx
@@ -3,6 +3,7 @@ import { redirect } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { setRequestLocale } from 'next-intl/server';
+import { CursorGlow } from '@/components/cursor-glow';
import { Masthead } from '@/components/masthead';
import { getServerSession } from '@/lib/auth';
@@ -56,48 +57,75 @@ function ProfilePageContent({
const t = useTranslations('Profile');
return (
-
-
-
- Capitolo IV
-
- {t('title')}
- .
-
-
- {t('subtitle')}
-
+
+
+
+
+
+
+
+
+ IV
+
+
+
+ Capitolo IV — Profilo
+
+
+
+
+ {t('title')}
+ .
+
+
+
+ {t('subtitle')}
+
-
+
-
+
-
+
-
+
-
+
-
-
-
+
+
+
-
+
-
+
-
+
-
-
-
-
+
+
+
+
+
);
}
diff --git a/apps/web/src/app/[locale]/shopping/page.tsx b/apps/web/src/app/[locale]/shopping/page.tsx
index ba3bf43..96f60f7 100644
--- a/apps/web/src/app/[locale]/shopping/page.tsx
+++ b/apps/web/src/app/[locale]/shopping/page.tsx
@@ -4,6 +4,7 @@ import { redirect } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { setRequestLocale } from 'next-intl/server';
+import { CursorGlow } from '@/components/cursor-glow';
import { Masthead } from '@/components/masthead';
import { Button } from '@/components/ui/button';
import { getServerSession } from '@/lib/auth';
@@ -51,52 +52,78 @@ function ShoppingPageContent({ list }: { list: ShoppingList | null }) {
);
return (
-
-
-
-
-
-
Capitolo IX
-
- {t('title')}
- .
-
-
- {list ? formatWeek(list.plan.weekStart) : t('noPlan')}
+
+
+
+
+
+
+
+
+ IX
+
+
+
+ Capitolo IX — La spesa
- {list ? (
-
- {t('totalLabel')}
-
- {totalItems}
-
- {typeof estimatedCost === 'number' && estimatedCost > 0 ? (
-
- {t('estimatedCost')}{' '}
- € {estimatedCost.toFixed(2)}
+
+
+ {t('title')}
+ .
+
+
+
+
+
+ {list ? formatWeek(list.plan.weekStart) : t('noPlan')}
+
+ {list ? (
+
+ {t('totalLabel')}
+
+ {totalItems}
- ) : null}
-
- ) : (
-
-
- {t('viewPlan')}
-
-
- )}
-
-
-
-
- {list ? (
-
-
+ {typeof estimatedCost === 'number' && estimatedCost > 0 ? (
+
+ {t('estimatedCost')}{' '}
+ € {estimatedCost.toFixed(2)}
+
+ ) : null}
+
+ ) : (
+
+
+ {t('viewPlan')}
+
+
+ )}
- ) : null}
-
-
+
+
+ {list ? (
+
+
+
+ ) : null}
+
+
+
+
);
}
diff --git a/apps/web/src/app/[locale]/tracking/page.tsx b/apps/web/src/app/[locale]/tracking/page.tsx
index 199a7fd..7d8794a 100644
--- a/apps/web/src/app/[locale]/tracking/page.tsx
+++ b/apps/web/src/app/[locale]/tracking/page.tsx
@@ -3,6 +3,7 @@ import { redirect } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { setRequestLocale } from 'next-intl/server';
+import { CursorGlow } from '@/components/cursor-glow';
import { Masthead } from '@/components/masthead';
import { getServerSession } from '@/lib/auth';
@@ -63,59 +64,86 @@ function TrackingPageContent({
const alertActive = alertKg != null && latest != null && latest.weightKg >= alertKg;
return (
-
);
}
diff --git a/apps/web/src/components/cursor-glow.tsx b/apps/web/src/components/cursor-glow.tsx
new file mode 100644
index 0000000..25c5b29
--- /dev/null
+++ b/apps/web/src/components/cursor-glow.tsx
@@ -0,0 +1,76 @@
+'use client';
+
+import { useEffect, useRef } from 'react';
+
+/**
+ * Cursor-tracking glow — un alone radiale che segue il puntatore.
+ * Si appiccica al parent posizionato relative/absolute. Rispetta
+ * `prefers-reduced-motion` (in quel caso resta centrato).
+ */
+export function CursorGlow({
+ className = '',
+ color = 'hsl(var(--pomodoro))',
+ size = 520,
+}: {
+ className?: string;
+ color?: string;
+ size?: number;
+}) {
+ const ref = useRef
(null);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ if (reduce) return;
+
+ const parent = el.parentElement;
+ if (!parent) return;
+
+ let raf = 0;
+ let targetX = parent.clientWidth / 2;
+ let targetY = parent.clientHeight / 3;
+ let currentX = targetX;
+ let currentY = targetY;
+
+ function onMove(e: PointerEvent): void {
+ const rect = parent!.getBoundingClientRect();
+ targetX = e.clientX - rect.left;
+ targetY = e.clientY - rect.top;
+ }
+
+ function loop(): void {
+ // Easing: avvicino la posizione corrente al target con un delta morbido.
+ currentX += (targetX - currentX) * 0.12;
+ currentY += (targetY - currentY) * 0.12;
+ if (el) {
+ el.style.transform = `translate3d(${currentX - size / 2}px, ${currentY - size / 2}px, 0)`;
+ }
+ raf = requestAnimationFrame(loop);
+ }
+
+ parent.addEventListener('pointermove', onMove);
+ raf = requestAnimationFrame(loop);
+ return () => {
+ cancelAnimationFrame(raf);
+ parent.removeEventListener('pointermove', onMove);
+ };
+ }, [size]);
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/editorial-atmosphere.tsx b/apps/web/src/components/editorial-atmosphere.tsx
new file mode 100644
index 0000000..c0b63c9
--- /dev/null
+++ b/apps/web/src/components/editorial-atmosphere.tsx
@@ -0,0 +1,91 @@
+import { CursorGlow } from './cursor-glow';
+
+/**
+ * Atmosfera di pagina riusabile: 2 mesh-blob fluttuanti + cursor glow
+ * opzionale. Va inserita come primo figlio di un `relative overflow-hidden`,
+ * sotto al contenuto principale.
+ */
+export function EditorialAtmosphere({
+ variant = 'default',
+ withCursor = false,
+}: {
+ variant?: 'default' | 'pomodoro' | 'oliva' | 'oro';
+ withCursor?: boolean;
+}) {
+ // Combinazione di blob per ogni variante.
+ const blobs =
+ variant === 'pomodoro'
+ ? [
+ {
+ kind: 'pomodoro',
+ pos: '-left-32 top-12',
+ size: 'h-[28rem] w-[28rem]',
+ anim: 'animate-float-x',
+ },
+ { kind: 'oro', pos: '-right-24 top-64', size: 'h-80 w-80', anim: 'animate-float-y' },
+ ]
+ : variant === 'oliva'
+ ? [
+ {
+ kind: 'oliva',
+ pos: '-right-32 top-12',
+ size: 'h-[28rem] w-[28rem]',
+ anim: 'animate-float-y',
+ },
+ {
+ kind: 'pomodoro',
+ pos: '-left-24 top-64',
+ size: 'h-72 w-72',
+ anim: 'animate-float-x',
+ },
+ ]
+ : variant === 'oro'
+ ? [
+ {
+ kind: 'oro',
+ pos: 'right-1/4 top-12',
+ size: 'h-[24rem] w-[24rem]',
+ anim: 'animate-float-z',
+ },
+ {
+ kind: 'oliva',
+ pos: '-left-24 bottom-12',
+ size: 'h-72 w-72',
+ anim: 'animate-float-y',
+ },
+ ]
+ : [
+ {
+ kind: 'oliva',
+ pos: '-right-32 top-12',
+ size: 'h-80 w-80',
+ anim: 'animate-float-y',
+ },
+ {
+ kind: 'pomodoro',
+ pos: '-left-24 top-64',
+ size: 'h-72 w-72',
+ anim: 'animate-float-x',
+ },
+ ];
+
+ const cursorColor =
+ variant === 'pomodoro'
+ ? 'hsl(var(--pomodoro))'
+ : variant === 'oro'
+ ? 'hsl(var(--oro))'
+ : 'hsl(var(--oliva))';
+
+ return (
+ <>
+ {withCursor ? : null}
+ {blobs.map((b, i) => (
+
+ ))}
+ >
+ );
+}
diff --git a/apps/web/src/components/editorial-header.tsx b/apps/web/src/components/editorial-header.tsx
new file mode 100644
index 0000000..f8b77c2
--- /dev/null
+++ b/apps/web/src/components/editorial-header.tsx
@@ -0,0 +1,76 @@
+/**
+ * Header tipografico riusabile in stile "asimmetrico off-grid".
+ * Chapter marker gigante (numero romano) come art di sfondo,
+ * eyebrow defilata, titolo che può sbordare dal bordo sinistro,
+ * sottotitolo indentato in colonna stretta.
+ *
+ * Va wrappato in un padre `relative overflow-hidden` (la pagina lo è
+ * già di norma). Il chapter marker è in stroke trasparente, decorativo.
+ */
+export function EditorialHeader({
+ chapter,
+ eyebrow,
+ title,
+ accent,
+ subtitle,
+ rightSlot,
+}: {
+ /** Numero romano grande (es. "II", "IV", "VII") usato come art di sfondo. */
+ chapter: string;
+ /** Eyebrow in alto. */
+ eyebrow: string;
+ /** Titolo principale. */
+ title: string;
+ /** Parola/parole da evidenziare in gradient italico (sostituisce nel titolo). */
+ accent?: string;
+ /** Sottotitolo italic, indentato. */
+ subtitle?: string;
+ /** Slot opzionale a destra del titolo (es. CTA "rigenera", date pickers). */
+ rightSlot?: React.ReactNode;
+}) {
+ let titleNode: React.ReactNode = title;
+ if (accent) {
+ const idx = title.toLowerCase().indexOf(accent.toLowerCase());
+ if (idx >= 0) {
+ titleNode = (
+ <>
+ {title.slice(0, idx)}
+ {title.slice(idx, idx + accent.length)}
+ {title.slice(idx + accent.length)}
+ >
+ );
+ }
+ }
+
+ return (
+
+ {/* Chapter marker decorativo */}
+
+ {chapter}
+
+
+
+
+
{eyebrow}
+
+ {titleNode}
+ .
+
+ {subtitle ? (
+
+ {subtitle}
+
+ ) : null}
+
+ {rightSlot ? (
+
+ {rightSlot}
+
+ ) : null}
+
+
+ );
+}
diff --git a/apps/web/src/styles/globals.css b/apps/web/src/styles/globals.css
index fda9533..58e3e12 100644
--- a/apps/web/src/styles/globals.css
+++ b/apps/web/src/styles/globals.css
@@ -160,4 +160,258 @@
.monogram {
@apply font-display text-2xl italic leading-none tracking-tight;
}
+
+ /* ─────────────────────────────────────────────────────────────────────
+ * Atmosfera moderna: blob, gradient, motion utilities.
+ * Usate dalla landing rifatta — pensate per essere applicate con
+ * parsimonia (tutta la palette è "warm cream" di base).
+ * ─────────────────────────────────────────────────────────────────── */
+
+ .mesh-blob {
+ position: absolute;
+ border-radius: 9999px;
+ filter: blur(64px);
+ opacity: 0.55;
+ pointer-events: none;
+ will-change: transform;
+ }
+
+ .mesh-blob--oliva {
+ background: radial-gradient(closest-side, hsl(var(--oliva) / 0.7), transparent);
+ }
+ .mesh-blob--pomodoro {
+ background: radial-gradient(closest-side, hsl(var(--pomodoro) / 0.6), transparent);
+ }
+ .mesh-blob--oro {
+ background: radial-gradient(closest-side, hsl(var(--oro) / 0.55), transparent);
+ }
+
+ .text-gradient {
+ background: linear-gradient(
+ 120deg,
+ hsl(var(--pomodoro)) 0%,
+ hsl(var(--oro)) 45%,
+ hsl(var(--oliva)) 100%
+ );
+ background-size: 200% 200%;
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ animation: gradient-pan 9s ease-in-out infinite;
+ }
+
+ .hover-lift {
+ transition:
+ transform 320ms cubic-bezier(0.2, 0.7, 0.2, 1),
+ box-shadow 320ms cubic-bezier(0.2, 0.7, 0.2, 1),
+ border-color 320ms ease;
+ }
+ .hover-lift:hover {
+ transform: translateY(-4px);
+ box-shadow:
+ 0 14px 30px -18px hsl(var(--ink) / 0.25),
+ 0 2px 4px -2px hsl(var(--ink) / 0.06);
+ }
+
+ .glow-ring {
+ box-shadow:
+ 0 0 0 1px hsl(var(--ink) / 0.06),
+ 0 18px 40px -22px hsl(var(--pomodoro) / 0.45),
+ 0 6px 18px -10px hsl(var(--oliva) / 0.25);
+ }
+
+ .btn-gradient {
+ background: linear-gradient(
+ 120deg,
+ hsl(var(--pomodoro)) 0%,
+ hsl(var(--oro)) 50%,
+ hsl(var(--oliva)) 100%
+ );
+ background-size: 180% 180%;
+ color: hsl(var(--carta-light));
+ transition:
+ background-position 600ms ease,
+ transform 200ms ease,
+ box-shadow 320ms ease;
+ box-shadow:
+ 0 12px 30px -14px hsl(var(--pomodoro) / 0.55),
+ 0 4px 10px -4px hsl(var(--oliva) / 0.3);
+ }
+ .btn-gradient:hover {
+ background-position: 100% 50%;
+ transform: translateY(-1px);
+ }
+ .btn-gradient:active {
+ transform: translateY(1px);
+ }
+
+ /* Display tipografico WOW: stroke outline (text-only border). Funziona
+ * con -webkit-text-stroke che ha buon supporto su tutti i browser
+ * moderni; il fill resta trasparente così si vede solo il contorno. */
+ .text-stroke {
+ -webkit-text-stroke: 2px hsl(var(--ink));
+ color: transparent;
+ }
+ .text-stroke-thick {
+ -webkit-text-stroke: 3px hsl(var(--ink));
+ color: transparent;
+ }
+
+ /* Sezione dark per il contrasto col cream — usata nel CTA finale.
+ * Niente paper grain qui, palette inverted. */
+ .section-dark {
+ background: linear-gradient(135deg, hsl(28 18% 8%) 0%, hsl(8 30% 14%) 100%);
+ color: hsl(var(--carta));
+ }
+
+ /* Mockup card 3D — leggera rotazione X+Y per dare profondità.
+ * Il container va settato perspective-1000 per attivare la prospettiva. */
+ .perspective-1000 {
+ perspective: 1400px;
+ }
+ .tilt-3d {
+ transform: rotateX(8deg) rotateY(-12deg) rotateZ(0.5deg);
+ transform-style: preserve-3d;
+ transition: transform 600ms cubic-bezier(0.2, 0.7, 0.2, 1);
+ }
+ .tilt-3d:hover {
+ transform: rotateX(2deg) rotateY(-3deg) rotateZ(0deg);
+ }
+}
+
+@layer utilities {
+ /* Marquee: scroll orizzontale infinito per keyword strip.
+ * Il contenuto deve essere duplicato due volte per evitare il salto. */
+ @keyframes marquee {
+ from {
+ transform: translateX(0);
+ }
+ to {
+ transform: translateX(-50%);
+ }
+ }
+ .animate-marquee {
+ animation: marquee 38s linear infinite;
+ }
+ .animate-marquee--fast {
+ animation: marquee 22s linear infinite;
+ }
+ .animate-marquee--reverse {
+ animation: marquee 38s linear infinite reverse;
+ }
+
+ /* Display gigante WOW per il titolo hero — fino a 16vw. */
+ .text-mega {
+ font-size: clamp(3.5rem, 16vw, 12rem);
+ line-height: 0.85;
+ letter-spacing: -0.045em;
+ }
+
+ /* Chapter marker gigante usato come art di sfondo per le sezioni. */
+ .text-chapter {
+ font-size: clamp(8rem, 28vw, 22rem);
+ line-height: 0.8;
+ letter-spacing: -0.06em;
+ }
+
+ /* Stroke fine, per chapter markers di sfondo. */
+ .text-stroke-thin {
+ -webkit-text-stroke: 1.5px hsl(var(--ink) / 0.18);
+ color: transparent;
+ }
+
+ /* Bleed: rompe il container per attaccarsi al bordo del viewport.
+ * Usato sui titoli mega che devono "uscire dalla pagina". */
+ .bleed-left {
+ margin-left: calc(50% - 50vw);
+ padding-left: max(1.5rem, calc(50vw - 50%));
+ }
+ .bleed-right {
+ margin-right: calc(50% - 50vw);
+ padding-right: max(1.5rem, calc(50vw - 50%));
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .animate-marquee,
+ .animate-marquee--fast,
+ .animate-marquee--reverse {
+ animation: none;
+ }
+ }
+}
+
+@layer utilities {
+ @keyframes gradient-pan {
+ 0%,
+ 100% {
+ background-position: 0% 50%;
+ }
+ 50% {
+ background-position: 100% 50%;
+ }
+ }
+
+ @keyframes float-x {
+ 0%,
+ 100% {
+ transform: translate3d(0, 0, 0);
+ }
+ 50% {
+ transform: translate3d(2.5rem, -1.5rem, 0);
+ }
+ }
+
+ @keyframes float-y {
+ 0%,
+ 100% {
+ transform: translate3d(0, 0, 0);
+ }
+ 50% {
+ transform: translate3d(-1.5rem, 2.5rem, 0);
+ }
+ }
+
+ @keyframes float-z {
+ 0%,
+ 100% {
+ transform: translate3d(0, 0, 0) scale(1);
+ }
+ 50% {
+ transform: translate3d(1.5rem, 1.5rem, 0) scale(1.08);
+ }
+ }
+
+ .animate-float-x {
+ animation: float-x 16s ease-in-out infinite;
+ }
+ .animate-float-y {
+ animation: float-y 20s ease-in-out infinite;
+ }
+ .animate-float-z {
+ animation: float-z 24s ease-in-out infinite;
+ }
+
+ @keyframes ring-trace {
+ 0% {
+ stroke-dashoffset: 360;
+ }
+ 100% {
+ stroke-dashoffset: 0;
+ }
+ }
+
+ .animate-ring-trace {
+ animation: ring-trace 2.6s cubic-bezier(0.7, 0, 0.3, 1) both;
+ }
+
+ /* Reduce motion: stoppa i drift in sottofondo. */
+ @media (prefers-reduced-motion: reduce) {
+ .animate-float-x,
+ .animate-float-y,
+ .animate-float-z,
+ .text-gradient,
+ .animate-ring-trace {
+ animation: none;
+ }
+ }
}