Compare commits

..

No commits in common. "product-polish" and "main" have entirely different histories.

38 changed files with 2913 additions and 4508 deletions

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#eaeaea" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z"></path></svg>

Before

Width:  |  Height:  |  Size: 273 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#eaeaea" viewBox="0 0 256 256"><path d="M216,48H40A16,16,0,0,0,24,64V224a15.84,15.84,0,0,0,9.25,14.5A16.05,16.05,0,0,0,40,240a15.89,15.89,0,0,0,10.25-3.78l.09-.07L83,208H216a16,16,0,0,0,16-16V64A16,16,0,0,0,216,48ZM40,224h0ZM216,192H80a8,8,0,0,0-5.23,1.95L40,224V64H216Z"></path></svg>

Before

Width:  |  Height:  |  Size: 355 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#eaeaea" viewBox="0 0 256 256"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"></path></svg>

Before

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

@ -1,11 +1,4 @@
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import path from "node:path";
const viteBin = path.join(
"node_modules",
".bin",
process.platform === "win32" ? "vite.cmd" : "vite"
);
const run = (name, command, args, env = {}) => { const run = (name, command, args, env = {}) => {
const child = spawn(command, args, { const child = spawn(command, args, {
@ -29,7 +22,7 @@ const run = (name, command, args, env = {}) => {
}; };
const api = run("api", "node", ["server/index.js"], { API_PORT: "4174" }); const api = run("api", "node", ["server/index.js"], { API_PORT: "4174" });
const vite = run("vite", viteBin, []); const vite = run("vite", "node_modules/.bin/vite", []);
const stop = () => { const stop = () => {
api.kill("SIGTERM"); api.kill("SIGTERM");

View File

@ -13,67 +13,14 @@ body,
#root { #root {
background: var(--theme-bg); background: var(--theme-bg);
color: var(--theme-text); color: var(--theme-text);
min-width: 0;
} }
body { body {
background: var(--theme-bg); background: var(--theme-bg);
} }
main {
min-width: 0;
}
.shell {
width: var(--container-wide);
margin: 0 auto;
padding: 0 0 clamp(2.2rem, 6vw, 5rem);
background: transparent;
border: 0;
box-shadow: none;
}
.page,
.detail-page,
.discovery-page,
.about-page,
.support-page,
.small-page,
.impressum-page,
.datenschutz-page {
isolation: isolate;
}
.detail-page .navbar--light,
.discovery-page .navbar--light,
.about-page .navbar--light,
.support-page .navbar--light,
.small-page .navbar--light,
.impressum-page .navbar--light,
.datenschutz-page .navbar--light {
position: fixed;
top: clamp(0.75rem, 2.1vw, 1.4rem);
right: 0;
left: 0;
z-index: 998;
margin-bottom: 0;
padding-top: 0;
pointer-events: none;
}
.detail-page .navbar--light .nav-pill,
.discovery-page .navbar--light .nav-pill,
.about-page .navbar--light .nav-pill,
.support-page .navbar--light .nav-pill,
.small-page .navbar--light .nav-pill,
.impressum-page .navbar--light .nav-pill,
.datenschutz-page .navbar--light .nav-pill {
pointer-events: auto;
}
.navbar--light .nav-pill, .navbar--light .nav-pill,
.navbar--light .nav-link, .navbar--light .nav-link,
.shell,
[class*="-page"], [class*="-page"],
[class*="-shell"], [class*="-shell"],
[class*="-card"], [class*="-card"],
@ -82,11 +29,7 @@ main {
button, button,
input, input,
textarea { textarea {
transition: transition: background-color 0.24s ease, border-color 0.24s ease, color 0.24s ease;
background-color var(--duration-med) var(--ease-out),
border-color var(--duration-med) var(--ease-out),
color var(--duration-med) var(--ease-out),
box-shadow var(--duration-med) var(--ease-out);
} }
body.theme-dark .navbar--light .nav-pill { body.theme-dark .navbar--light .nav-pill {

View File

@ -13,7 +13,6 @@ import SupportChatbot from "./components/SupportChatbot";
import ScrollToTop from "./components/ScrollToTop"; import ScrollToTop from "./components/ScrollToTop";
import ShopDrawer from "./components/ShopDrawer"; import ShopDrawer from "./components/ShopDrawer";
import CartToast from "./components/CartToast"; import CartToast from "./components/CartToast";
import { ProductTransitionProvider } from "./components/ProductTransition";
import useScrollTextReveal from "./hooks/useScrollTextReveal"; import useScrollTextReveal from "./hooks/useScrollTextReveal";
import { ThemeProvider } from "./theme/ThemeContext"; import { ThemeProvider } from "./theme/ThemeContext";
import "./style/textReveal.css"; import "./style/textReveal.css";
@ -31,7 +30,7 @@ function App() {
const shouldFlushFooter = const shouldFlushFooter =
location.pathname === "/" || location.pathname.startsWith("/duft/"); location.pathname === "/" || location.pathname.startsWith("/duft/");
useScrollTextReveal(routeContentRef, location.pathname); useScrollTextReveal(routeContentRef, [location.pathname]);
useEffect(() => { useEffect(() => {
if (typeof document === "undefined") return; if (typeof document === "undefined") return;
@ -53,10 +52,10 @@ function App() {
return ( return (
<ThemeProvider value={{ theme, isLight, toggleTheme }}> <ThemeProvider value={{ theme, isLight, toggleTheme }}>
<ProductTransitionProvider> <>
<ScrollToTop /> <ScrollToTop />
<div ref={routeContentRef} data-route-content> <div ref={routeContentRef}>
<Routes> <Routes>
<Route path="/" element={<LandingPage />} /> <Route path="/" element={<LandingPage />} />
<Route path="/duft/:perfumeSlug" element={<ProductDetailPage />} /> <Route path="/duft/:perfumeSlug" element={<ProductDetailPage />} />
@ -73,7 +72,7 @@ function App() {
<CartToast /> <CartToast />
<Footer flushTop={shouldFlushFooter} /> <Footer flushTop={shouldFlushFooter} />
<SupportChatbot /> <SupportChatbot />
</ProductTransitionProvider> </>
</ThemeProvider> </ThemeProvider>
); );
} }

View File

@ -1,10 +1,6 @@
.site-footer { .site-footer {
position: relative; margin-top: 40px;
margin-top: 0; background: #1f1f1f;
overflow: hidden;
background:
radial-gradient(circle at 82% 10%, rgba(var(--theme-accent-rgb) / 0.15), transparent 22rem),
#171717;
color: #f5f5f5; color: #f5f5f5;
border-top: 1px solid rgba(255, 255, 255, 0.08); border-top: 1px solid rgba(255, 255, 255, 0.08);
} }
@ -13,102 +9,76 @@
margin-top: 0; margin-top: 0;
} }
.site-footer::before {
content: "ATMOS";
position: absolute;
right: var(--page-x);
bottom: -0.16em;
color: rgba(255, 255, 255, 0.035);
font-size: clamp(5.5rem, 18vw, 20rem);
line-height: 0.8;
letter-spacing: 0;
pointer-events: none;
}
.site-footer__inner { .site-footer__inner {
position: relative; max-width: 1600px;
z-index: 1;
width: var(--container-wide);
margin: 0 auto; margin: 0 auto;
padding: clamp(2.4rem, 7vw, 6.5rem) 0 clamp(2.2rem, 5vw, 4.8rem); padding: 28px 20px 32px;
display: grid; display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(12rem, 0.65fr) minmax(12rem, 0.75fr); grid-template-columns: 1.4fr 1fr 1fr;
gap: var(--gap-lg); gap: 28px;
align-items: start;
} }
.site-footer__brand { .site-footer__brand {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--gap-sm); gap: 12px;
} }
.site-footer__logo { .site-footer__logo {
width: fit-content;
color: #fff;
font-size: clamp(1rem, 1.5vw, 1.2rem);
letter-spacing: 0.22em;
text-decoration: none; text-decoration: none;
color: #fff;
font-size: 14px;
letter-spacing: 0.22em;
} }
.site-footer__text { .site-footer__text {
max-width: 32rem;
margin: 0; margin: 0;
color: rgba(255, 255, 255, 0.7); max-width: 320px;
font-size: var(--text-base); font-size: 14px;
line-height: 1.65; line-height: 1.6;
color: rgba(255, 255, 255, 0.72);
} }
.site-footer__nav-group { .site-footer__nav-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--gap-sm); gap: 12px;
padding-top: 0.2rem;
} }
.site-footer__heading { .site-footer__heading {
color: rgba(255, 255, 255, 0.52); font-size: 10px;
font-size: var(--text-xs);
letter-spacing: 0.22em; letter-spacing: 0.22em;
text-transform: uppercase; text-transform: uppercase;
color: rgba(255, 255, 255, 0.5);
} }
.site-footer__nav { .site-footer__nav {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.72rem; gap: 10px;
} }
.site-footer__nav a { .site-footer__nav a {
width: fit-content;
color: #f5f5f5;
font-size: var(--text-sm);
line-height: 1.2;
text-decoration: none; text-decoration: none;
transition: color: #f5f5f5;
color var(--duration-med) var(--ease-out), font-size: 14px;
opacity var(--duration-med) var(--ease-out), transition: opacity 0.2s ease, transform 0.2s ease;
transform var(--duration-med) var(--ease-out);
} }
.site-footer__nav a:hover, .site-footer__nav a:hover {
.site-footer__nav a:focus-visible { opacity: 0.7;
color: var(--theme-accent); transform: translateX(2px);
transform: translateX(0.25rem);
} }
@media (max-width: 900px) { @media (max-width: 900px) {
.site-footer__inner { .site-footer__inner {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
.site-footer__brand {
grid-column: 1 / -1;
}
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.site-footer__inner { .site-footer__inner {
grid-template-columns: 1fr; grid-template-columns: 1fr;
padding: 24px 16px 28px;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -2,476 +2,18 @@ import { useEffect, useMemo, useState } from "react";
import { Link, useNavigate, useParams } from "react-router"; import { Link, useNavigate, useParams } from "react-router";
import perfumes from "../data/perfumes"; import perfumes from "../data/perfumes";
import SharedNavbar from "./SharedNavbar"; import SharedNavbar from "./SharedNavbar";
import { useProductTransition } from "../transitions/ProductTransitionContext";
import { formatChf } from "../shop/money"; import { formatChf } from "../shop/money";
import { useShop } from "../shop/useShop"; import { useShop } from "../shop/useShop";
import "./ProductDetailPage.css"; import "./ProductDetailPage.css";
const STORY_PANEL_IMAGE = "/placeholder-character-panel.jpg";
const priceToCents = (price) => { const priceToCents = (price) => {
const match = String(price).match(/(\d+)/); const match = String(price).match(/(\d+)/);
return match ? Number(match[1]) * 100 : 0; return match ? Number(match[1]) * 100 : 0;
}; };
function ProductPurchasePanel({
perfume,
selectedSize,
setSelectedSize,
selectedPriceCents,
discountPreviewCents,
addToCart,
subscribeToProduct,
}) {
const selectedProductId = `${perfume.slug}-${selectedSize === "sample" ? "sample" : "full"}`;
const selectedProductLabel = selectedSize === "sample" ? "Sample" : "Full Size";
const sizeOptions = [
{
key: "sample",
title: "Sample 2ml",
price: perfume.prices.sample,
note: "Zum Testen, ca. 20 Anwendungen",
},
{
key: "full",
title: "Full Size 50ml",
price: perfume.prices.full,
note: "Nachkauf, 500+ Anwendungen",
},
];
return (
<aside className="product-purchase-panel" data-product-transition-reveal>
<div className="purchase-price-row">
<span>Preis</span>
<strong>{perfume.prices[selectedSize]}</strong>
</div>
<div className="purchase-size-group">
<span className="label-title">Größe wählen</span>
<div className="size-grid">
{sizeOptions.map((option) => (
<button
key={option.key}
type="button"
className={`size-card ${selectedSize === option.key ? "active" : ""}`}
onClick={() => setSelectedSize(option.key)}
aria-pressed={selectedSize === option.key}
>
<span className="size-title">{option.title}</span>
<strong>{option.price}</strong>
<small>{option.note}</small>
</button>
))}
</div>
</div>
<div className="purchase-actions">
<button
className="buy-button"
type="button"
onClick={() =>
addToCart(
selectedProductId,
1,
`${perfume.name} ${selectedProductLabel} added.`
).catch(() => {})
}
>
Kaufen
</button>
<button
className="restock-button"
type="button"
onClick={() => subscribeToProduct(selectedProductId, "restock").catch(() => {})}
>
Restock Update abonnieren
</button>
</div>
<div className="purchase-discovery-note">
<div>
<strong>Discovery Set wird angerechnet</strong>
<p>
Sample- und Set-Guthaben werden beim Full-Size-Kauf automatisch
abgezogen.
</p>
{discountPreviewCents > 0 && (
<p className="discount-preview">
Erwarteter Preis mit Rabatt:{" "}
<strong>{formatChf(selectedPriceCents - discountPreviewCents)}</strong>
</p>
)}
</div>
<Link to="/discovery-set">Zum Set</Link>
</div>
</aside>
);
}
function ProductHero({
perfume,
selectedImage,
setSelectedImage,
selectedSize,
setSelectedSize,
selectedPriceCents,
discountPreviewCents,
addToCart,
subscribeToProduct,
}) {
const galleryImages = [...new Set([perfume.image, ...(perfume.gallery || [])])].slice(0, 3);
return (
<section className="product-hero">
<div className="product-media-column">
<div className="product-hero-copy" data-product-transition-reveal>
<span>Edition {perfume.id}</span>
<h1>{perfume.name}</h1>
</div>
<div className="product-hero-visual">
<img
src={selectedImage}
alt={perfume.name}
className="product-hero-image"
data-product-transition-target={perfume.slug}
decoding="async"
/>
</div>
<div className="product-thumbs" data-product-transition-reveal>
{galleryImages.map((img, index) => (
<button
key={`${img}-${index}`}
type="button"
className={`thumb-btn ${selectedImage === img ? "active" : ""}`}
onClick={() => setSelectedImage(img)}
aria-label={`${perfume.name} Ansicht ${index + 1}`}
>
<img src={img} alt="" loading="lazy" decoding="async" />
</button>
))}
</div>
<div className="hero-fact-grid" data-product-transition-reveal>
<div>
<span>Tragehinweis</span>
<p>{perfume.dosage}</p>
</div>
<div>
<span>Konzentration</span>
<p>{perfume.concentration}</p>
</div>
<div>
<span>Studio</span>
<p>{perfume.description}</p>
</div>
</div>
</div>
<ProductPurchasePanel
perfume={perfume}
selectedSize={selectedSize}
setSelectedSize={setSelectedSize}
selectedPriceCents={selectedPriceCents}
discountPreviewCents={discountPreviewCents}
addToCart={addToCart}
subscribeToProduct={subscribeToProduct}
/>
</section>
);
}
function ProductStorySection({ perfume }) {
return (
<section className="product-story-grid" data-reveal-group>
<div className="story-copy">
<span className="eyebrow" data-reveal="fade">Beschreibung / Charakter</span>
<h2 data-reveal="lines">{perfume.text}</h2>
<p data-reveal="fade">{perfume.mood}</p>
<div className="character-facts" data-reveal="fade">
<div>
<span>Haltbarkeit</span>
<p>{perfume.longevity}</p>
</div>
<div>
<span>Anlass</span>
<p>{perfume.occasion}</p>
</div>
<div>
<span>Lieferung</span>
<p>Versand in 1-2 Werktagen. Zustellung in der Regel in 5-6 Tagen.</p>
</div>
</div>
</div>
<div className="story-visual-panel" data-reveal="fade">
<img
className="story-visual-image"
src={STORY_PANEL_IMAGE}
alt=""
loading="lazy"
decoding="async"
/>
<div className="story-visual-content">
<span>Material-Komposition</span>
<div className="material-tags">
{perfume.materialTags.map((tag) => (
<span key={tag}>{tag}</span>
))}
</div>
</div>
</div>
</section>
);
}
function ProductStructureSection({ perfume }) {
const phaseGroups = [
{ label: "Top Notes / 0-1 h", notes: perfume.phases.top },
{ label: "Heart Notes / 1-4 h", notes: perfume.phases.heart },
{ label: "Base Notes / 4 h+", notes: perfume.phases.base },
];
return (
<section className="product-structure-section" data-reveal-group>
<div className="section-intro">
<span className="eyebrow" data-reveal="fade">Duftstruktur</span>
<h2 data-reveal="lines">Die Spur entwickelt sich in Schichten.</h2>
</div>
<div className="structure-timeline">
{phaseGroups.map((phase, index) => (
<article className="structure-phase-card" key={phase.label} data-reveal="fade">
<span className="structure-index">0{index + 1}</span>
<h3>{phase.label}</h3>
<div>
{phase.notes.map((note) => (
<span key={note}>{note}</span>
))}
</div>
</article>
))}
</div>
</section>
);
}
function ProductMetaSection({ perfume }) {
const metaItems = [
["Parfümerie / Studio", perfume.description],
["Herkunft", perfume.origin],
["Konzentration", perfume.concentration],
["Edition", perfume.edition],
];
return (
<section className="product-meta-section" data-reveal-group>
<div className="meta-grid">
{metaItems.map(([label, value]) => (
<div className="meta-card" key={label} data-reveal="fade">
<span>{label}</span>
<p>{value}</p>
</div>
))}
</div>
<div className="delivery-panel" data-reveal="fade">
<div className="delivery-panel-header">
<span className="eyebrow">Lieferung</span>
<span className="delivery-badge">CH</span>
</div>
<div className="delivery-grid">
<div>
<span>Versand</span>
<p>Innerhalb von 1-2 Werktagen</p>
</div>
<div>
<span>Zustellung</span>
<p>In der Regel in 5-6 Tagen bei dir</p>
</div>
<div>
<span>Hinweis</span>
<p>Sorgfältig verpackt und geschützt versendet.</p>
</div>
</div>
</div>
</section>
);
}
function ProductReviews({
reviewSummary,
safeCommentPages,
commentPage,
setCommentPage,
showReviewDetails,
setShowReviewDetails,
}) {
return (
<section className="product-reviews-section" data-reveal-group>
<div className="reviews-heading" data-reveal="fade">
<div>
<span className="eyebrow">Stimmen zum Duft</span>
<h2>Resonanz</h2>
<p>
Verdichtete Wahrnehmung aus bisherigen Stimmen zu Charakter,
Haltbarkeit, Sillage und Originalität.
</p>
</div>
<div className="review-score-block">
<strong>{reviewSummary.score.toFixed(1)}</strong>
<span></span>
<small>{reviewSummary.total} Stimmen</small>
</div>
</div>
<div className="comment-spotlight" data-reveal="fade">
<div className="comment-dots">
{safeCommentPages.map((_, index) => (
<button
key={index}
type="button"
className={`comment-dot ${commentPage === index ? "active" : ""}`}
onClick={() => setCommentPage(index)}
aria-label={`Kommentargruppe ${index + 1}`}
/>
))}
</div>
<div className="comment-spotlight-grid">
{safeCommentPages[commentPage].map((comment) => (
<article className="comment-card" key={comment.id}>
<span>{comment.title}</span>
<p>{comment.text}</p>
<small>{comment.name}</small>
</article>
))}
</div>
</div>
<div className={`review-panel ${showReviewDetails ? "is-open" : ""}`} data-reveal="fade">
<button
type="button"
className="review-toggle"
onClick={() => setShowReviewDetails((prev) => !prev)}
aria-expanded={showReviewDetails}
>
<span>Detailbewertungen</span>
<span className={showReviewDetails ? "review-toggle-icon open" : "review-toggle-icon"}>
+
</span>
</button>
{showReviewDetails && (
<div className="review-details">
{reviewSummary.metrics.map((metric) => (
<div className="review-detail-row" key={metric.label}>
<span>{metric.label}</span>
<div className="review-detail-bar">
<div
className="review-detail-fill"
style={{ width: `${(metric.value / 5) * 100}%` }}
/>
</div>
<strong>{metric.value.toFixed(1)}</strong>
</div>
))}
<button type="button" className="review-write-button" disabled>
Bewertung schreiben
</button>
</div>
)}
</div>
</section>
);
}
function ProductTestingCTA({ perfume, addToCart }) {
return (
<section className="detail-bottom-cta" data-reveal-group>
<div>
<span className="eyebrow" data-reveal="fade">Lieber erst testen?</span>
<h2 data-reveal="lines">Sample oder Discovery Set.</h2>
<p data-reveal="fade">
Bestelle ein 2ml Sample für CHF 12 oder das komplette Discovery Set mit
allen 6 Düften für CHF 48. Beide werden beim späteren Full-Size-Kauf
vollständig angerechnet.
</p>
</div>
<div className="detail-bottom-actions" data-reveal="fade">
<button
type="button"
onClick={() =>
addToCart(`${perfume.slug}-sample`, 1, `${perfume.name} Sample added.`).catch(
() => {}
)
}
>
Sample bestellen - {perfume.prices.sample}
</button>
<button
type="button"
onClick={() => addToCart("discovery-set", 1, "Discovery Set added.").catch(() => {})}
>
Discovery Set - CHF 48
</button>
</div>
</section>
);
}
function ProductRecommendations({ currentSlug, startProductTransition }) {
const recommendations = perfumes
.filter((item) => item.slug !== currentSlug)
.slice(0, 3);
return (
<section className="recommendation-section" data-reveal-group>
<div className="recommendation-heading">
<span className="eyebrow" data-reveal="fade">Empfehlungen</span>
<h2 data-reveal="lines">Weitere Atmosphären</h2>
</div>
<div className="recommendation-grid">
{recommendations.map((item) => (
<Link
to={`/duft/${item.slug}`}
className="recommendation-card"
key={item.slug}
onClick={(event) => startProductTransition(event, item)}
data-reveal="fade"
>
<span>{item.id}</span>
<img
src={item.image}
alt={item.name}
loading="lazy"
decoding="async"
data-product-transition-source
/>
<div>
<h3>{item.name}</h3>
<p>{item.text}</p>
</div>
</Link>
))}
</div>
</section>
);
}
function ProductDetailContent({ perfumeSlug }) { function ProductDetailContent({ perfumeSlug }) {
const navigate = useNavigate(); const navigate = useNavigate();
const { addToCart, subscribeToProduct, user } = useShop(); const { addToCart, subscribeToProduct, user } = useShop();
const { activeSlug, phase, startProductTransition } = useProductTransition();
const perfume = useMemo( const perfume = useMemo(
() => perfumes.find((item) => item.slug === perfumeSlug) || perfumes[0], () => perfumes.find((item) => item.slug === perfumeSlug) || perfumes[0],
@ -484,6 +26,10 @@ function ProductDetailContent({ perfumeSlug }) {
const [selectedSize, setSelectedSize] = useState("sample"); const [selectedSize, setSelectedSize] = useState("sample");
const [showReviewDetails, setShowReviewDetails] = useState(false); const [showReviewDetails, setShowReviewDetails] = useState(false);
const [commentPage, setCommentPage] = useState(0); const [commentPage, setCommentPage] = useState(0);
const [isStructureOpen, setIsStructureOpen] = useState(false);
const [isMoodOpen, setIsMoodOpen] = useState(false);
const selectedProductId = `${perfume.slug}-${selectedSize === "sample" ? "sample" : "full"}`;
const selectedProductLabel = selectedSize === "sample" ? "Sample" : "Full Size";
const selectedPriceCents = priceToCents(perfume.prices[selectedSize]); const selectedPriceCents = priceToCents(perfume.prices[selectedSize]);
const sampleCredit = user?.sampleCredits?.find( const sampleCredit = user?.sampleCredits?.find(
(credit) => credit.slug === perfume.slug && credit.status === "available" (credit) => credit.slug === perfume.slug && credit.status === "available"
@ -497,22 +43,37 @@ function ProductDetailContent({ perfumeSlug }) {
) )
: 0; : 0;
const sizeOptions = [
{
key: "sample",
title: "Sample 2ml",
price: perfume.prices.sample,
note: "Zum Testen · ca. 20 Anwendungen",
},
{
key: "full",
title: "Full Size 50ml",
price: perfume.prices.full,
note: "Nachkauf · 500+ Anwendungen",
},
];
const reviewSummary = perfume.reviews || { const reviewSummary = perfume.reviews || {
score: 0, score: 0,
total: 0, total: 0,
metrics: [], metrics: [],
}; };
const safeCommentPages = useMemo(() => {
const reviewComments = perfume.commentSpotlight || []; const reviewComments = perfume.commentSpotlight || [];
const pages = [];
const commentPages = [];
for (let i = 0; i < reviewComments.length; i += 2) { for (let i = 0; i < reviewComments.length; i += 2) {
pages.push(reviewComments.slice(i, i + 2)); commentPages.push(reviewComments.slice(i, i + 2));
} }
return pages.length > 0 const safeCommentPages =
? pages commentPages.length > 0
? commentPages
: [ : [
[ [
{ {
@ -523,9 +84,6 @@ function ProductDetailContent({ perfumeSlug }) {
}, },
], ],
]; ];
}, [perfume.commentSpotlight]);
const isTransitionArriving = activeSlug === perfume.slug && phase === "entering";
useEffect(() => { useEffect(() => {
const interval = window.setInterval(() => { const interval = window.setInterval(() => {
@ -536,50 +94,407 @@ function ProductDetailContent({ perfumeSlug }) {
}, [safeCommentPages.length]); }, [safeCommentPages.length]);
return ( return (
<div className={`detail-page ${isTransitionArriving ? "is-transition-arriving" : ""}`}> <div className="detail-page">
<SharedNavbar variant="hero" /> <SharedNavbar variant="light" />
<main className="shell"> <main className="detail-shell">
<div className="detail-topbar" data-product-transition-reveal> <div className="detail-topbar">
<button className="back-link" type="button" onClick={() => navigate("/")}> <button className="back-link" type="button" onClick={() => navigate("/")}>
<span className="back-link-arrow" aria-hidden="true" /> <span className="back-link-arrow"></span>
<span>Zurück zur Startseite</span> <span>Zurück zur Startseite</span>
</button> </button>
<div className="detail-topbar-meta"> <div className="detail-topbar-meta">
<span>Duftdetail</span> <span className="detail-topbar-label">DUFTDETAIL</span>
<strong>{perfume.name}</strong> <span className="detail-topbar-name">{perfume.name}</span>
</div> </div>
</div> </div>
<ProductHero <section className="detail-layout">
perfume={perfume} <div className="detail-gallery">
selectedImage={selectedImage} <div className="detail-main-image">
setSelectedImage={setSelectedImage} <img src={selectedImage} alt={perfume.name} />
selectedSize={selectedSize} </div>
setSelectedSize={setSelectedSize}
selectedPriceCents={selectedPriceCents}
discountPreviewCents={discountPreviewCents}
addToCart={addToCart}
subscribeToProduct={subscribeToProduct}
/>
<ProductStorySection perfume={perfume} /> <div className="detail-thumbs">
<ProductStructureSection perfume={perfume} /> {[perfume.image, ...(perfume.gallery || [])]
<ProductMetaSection perfume={perfume} /> .slice(0, 3)
<ProductReviews .map((img, index) => (
reviewSummary={reviewSummary} <button
safeCommentPages={safeCommentPages} key={`${img}-${index}`}
commentPage={commentPage} type="button"
setCommentPage={setCommentPage} className={`thumb-btn ${selectedImage === img ? "active" : ""}`}
showReviewDetails={showReviewDetails} onClick={() => setSelectedImage(img)}
setShowReviewDetails={setShowReviewDetails} >
<img src={img} alt={`${perfume.name} Ansicht ${index + 1}`} />
</button>
))}
</div>
<div className="detail-meta-grid detail-meta-grid--top">
<div>
<span>TRAGEHINWEIS</span>
<p>{perfume.dosage}</p>
</div>
<div>
<span>HALTBARKEIT</span>
<p>{perfume.longevity}</p>
</div>
<div>
<span>ANLASS</span>
<p>{perfume.occasion}</p>
</div>
</div>
{/* --- Accordion Group Start --- */}
<div className="accordion-group">
{/* Dropdown: Duftstruktur */}
<div className={`accordion-item ${isStructureOpen ? "is-open" : ""}`}>
<button
type="button"
className="accordion-toggle"
onClick={() => setIsStructureOpen(!isStructureOpen)}
>
<span>DUFTSTRUKTUR</span>
<span className="accordion-icon">{isStructureOpen ? "" : "+"}</span>
</button>
{isStructureOpen && (
<div className="accordion-content">
<div className="detail-structure-layout">
<div className="detail-structure">
<div className="structure-block">
<span className="structure-phase">PHASE 1: TOP NOTES (01 H)</span>
<div className="structure-tags-grid">
{perfume.phases.top.map((note) => (
<span key={note} className="structure-tag">{note}</span>
))}
</div>
</div>
<div className="structure-block">
<span className="structure-phase">PHASE 2: HEART NOTES (14 H)</span>
<div className="structure-tags-grid">
{perfume.phases.heart.map((note) => (
<span key={note} className="structure-tag">{note}</span>
))}
</div>
</div>
<div className="structure-block">
<span className="structure-phase">PHASE 3: BASE NOTES (4 H+)</span>
<div className="structure-tags-grid">
{perfume.phases.base.map((note) => (
<span key={note} className="structure-tag">{note}</span>
))}
</div>
</div>
</div>
<aside className="structure-info-box">
<span className="structure-info-label">ZUR EINORDNUNG</span>
<p>
Die Duftstruktur zeigt, wie sich der Duft über die Zeit entfaltet:
der erste Eindruck im Auftakt, die eigentliche Signatur im Herzen
und die Spur, die lange auf Haut und Kleidung bleibt.
</p>
<div className="structure-info-legend">
<div>
<span className="structure-info-dot structure-info-dot--light" />
<span>Auftakt</span>
</div>
<div>
<span className="structure-info-dot structure-info-dot--mid" />
<span>Herz</span>
</div>
<div>
<span className="structure-info-dot structure-info-dot--strong" />
<span>Basis</span>
</div>
</div>
</aside>
</div>
</div>
)}
</div>
{/* Dropdown: Moodsetting */}
<div className={`accordion-item ${isMoodOpen ? "is-open" : ""}`}>
<button
type="button"
className="accordion-toggle"
onClick={() => setIsMoodOpen(!isMoodOpen)}
>
<span>MOODSETTING</span>
<span className="accordion-icon">{isMoodOpen ? "" : "+"}</span>
</button>
{isMoodOpen && (
<div className="accordion-content">
<div className="mood-box-content">
<p>{perfume.mood}</p>
</div>
</div>
)}
</div>
</div>
{/* --- Accordion Group End --- */}
</div>
<div className="detail-info">
<div className="detail-heading" data-reveal-group>
<div className="detail-heading-copy">
<span className="detail-kicker" data-reveal="fade">
Edition 04
</span>
<h1 data-reveal="fade">{perfume.name}</h1>
<p data-reveal="fade">{perfume.shortText}</p>
</div>
</div>
<div className="detail-section-block">
<span className="label-title">MATERIAL-KOMPOSITION</span>
<div className="material-tags">
{perfume.materialTags.map((tag) => (
<span key={tag}>{tag}</span>
))}
</div>
</div>
<div className="detail-section-block">
<span className="label-title">GRÖSSE WÄHLEN</span>
<div className="size-grid">
{sizeOptions.map((option) => (
<button
key={option.key}
type="button"
className={`size-card ${selectedSize === option.key ? "active" : ""}`}
onClick={() => setSelectedSize(option.key)}
>
<span className="size-title">{option.title}</span>
<strong>{option.price}</strong>
<small>{option.note}</small>
</button>
))}
</div>
</div>
<div className="discovery-note" data-reveal-group data-reveal-start="top 88%">
<div className="discovery-note-text" data-reveal="fade">
<strong>Discovery Set wird einmalig angerechnet</strong>
<p>
Nur das erste Discovery Set erzeugt CHF 48 Guthaben. Es wird
einmal bei einem späteren Full-Size-Kauf automatisch abgezogen.
</p>
{discountPreviewCents > 0 && (
<p className="discount-preview">
Erwarteter Preis mit Rabatt:{" "}
<strong>{formatChf(selectedPriceCents - discountPreviewCents)}</strong>
</p>
)}
</div>
<Link to="/discovery-set" className="discovery-note-btn" data-reveal="fade">
Zum Set
</Link>
</div>
<button
className="buy-button"
type="button"
onClick={() =>
addToCart(
selectedProductId,
1,
`${perfume.name} ${selectedProductLabel} added.`
).catch(() => {})
}
>
KAUFEN
</button>
<button
className="restock-button"
type="button"
onClick={() => subscribeToProduct(selectedProductId, "restock").catch(() => {})}
>
RESTOCK UPDATE ABONNIEREN
</button>
<div className="detail-description-section">
<span className="label-title">BESCHREIBUNG</span>
<div className="detail-columns">
<div className="detail-copy-block">
<span className="detail-copy-label">PARFÜMERIE / STUDIO</span>
<p>{perfume.description}</p>
</div>
<div className="detail-copy-block">
<span className="detail-copy-label">HERKUNFT</span>
<p>{perfume.origin}</p>
</div>
<div className="detail-copy-block">
<span className="detail-copy-label">KONZENTRATION</span>
<p>{perfume.concentration}</p>
</div>
<div className="detail-copy-block">
<span className="detail-copy-label">EDITION</span>
<p>{perfume.edition}</p>
</div>
</div>
</div>
<div className="delivery-box">
<div className="delivery-box-header">
<span className="label-title">LIEFERUNG</span>
<span className="delivery-badge">CH</span>
</div>
<div className="delivery-grid">
<div className="delivery-item">
<span className="delivery-item-label">VERSAND</span>
<p>Innerhalb von 12 Werktagen</p>
</div>
<div className="delivery-item">
<span className="delivery-item-label">ZUSTELLUNG</span>
<p>In der Regel in 56 Tagen bei dir</p>
</div>
<div className="delivery-item delivery-item--full">
<span className="delivery-item-label">HINWEIS</span>
<p>Sorgfältig verpackt und geschützt versendet.</p>
</div>
</div>
</div>
<div className="comment-spotlight">
<div className="comment-spotlight-header">
<span className="label-title">STIMMEN ZUM DUFT</span>
<div className="comment-dots">
{safeCommentPages.map((_, index) => (
<button
key={index}
type="button"
className={`comment-dot ${commentPage === index ? "active" : ""}`}
onClick={() => setCommentPage(index)}
aria-label={`Kommentargruppe ${index + 1}`}
/> />
<ProductTestingCTA perfume={perfume} addToCart={addToCart} /> ))}
<ProductRecommendations </div>
currentSlug={perfume.slug} </div>
startProductTransition={startProductTransition}
<div className="comment-spotlight-grid">
{safeCommentPages[commentPage].map((comment) => (
<article className="comment-card" key={comment.id}>
<span className="comment-card-title">{comment.title}</span>
<p>{comment.text}</p>
<span className="comment-card-author">{comment.name}</span>
</article>
))}
</div>
</div>
<div className="review-section">
<div className="review-section-header">
<div className="review-section-copy">
<span className="label-title">RESONANZ</span>
<p className="review-section-text">
Verdichtete Wahrnehmung aus bisherigen Stimmen zu Charakter,
Haltbarkeit, Sillage und Originalität.
</p>
</div>
<div className="review-section-main">
<span className="review-score">{reviewSummary.score.toFixed(1)}</span>
<div className="review-summary-copy">
<span className="review-stars"></span>
<span className="review-count">{reviewSummary.total} Stimmen</span>
</div>
</div>
</div>
<div className={`review-panel ${showReviewDetails ? "is-open" : ""}`}>
<button
type="button"
className="review-toggle"
onClick={() => setShowReviewDetails((prev) => !prev)}
aria-expanded={showReviewDetails}
>
<span>Detailbewertungen</span>
<span className={showReviewDetails ? "review-toggle-icon open" : "review-toggle-icon"}>
+
</span>
</button>
{showReviewDetails && (
<div className="review-popover">
<div className="review-details">
{reviewSummary.metrics.map((metric) => (
<div className="review-detail-row" key={metric.label}>
<span className="review-detail-label">{metric.label}</span>
<div className="review-detail-bar">
<div
className="review-detail-fill"
style={{ width: `${(metric.value / 5) * 100}%` }}
/> />
</div>
<span className="review-detail-value">
{metric.value.toFixed(1)}
</span>
</div>
))}
<button
type="button"
className="review-write-button"
disabled
title="Später mit Login verfügbar"
>
Bewertung schreiben
</button>
</div>
</div>
)}
</div>
</div>
</div>
</section>
<section className="detail-bottom-cta" data-reveal-group>
<h2 data-reveal="fade">Lieber erst testen?</h2>
<p data-reveal="fade">
Bestelle ein 2ml Sample für CHF 12 oder das komplette Discovery Set
mit allen 6 Düften für CHF 48. Beide werden beim späteren Full-Size-Kauf
vollständig angerechnet.
</p>
<div className="detail-bottom-actions" data-reveal="fade">
<button
type="button"
onClick={() =>
addToCart(`${perfume.slug}-sample`, 1, `${perfume.name} Sample added.`).catch(() => {})
}
>
SAMPLE BESTELLEN {perfume.prices.sample}
</button>
<button
type="button"
onClick={() => addToCart("discovery-set", 1, "Discovery Set added.").catch(() => {})}
>
DISCOVERY SET CHF 48
</button>
</div>
</section>
</main> </main>
</div> </div>
); );

View File

@ -1,37 +0,0 @@
.product-transition {
position: fixed;
inset: 0;
z-index: 2500;
pointer-events: none;
opacity: 0;
visibility: hidden;
overflow: hidden;
}
.product-transition__wash {
position: absolute;
inset: 0;
background: var(--theme-bg);
}
.product-transition__image {
position: absolute;
top: 0;
left: 0;
display: block;
max-width: none;
object-fit: contain;
transform-origin: 0 0;
will-change: transform, opacity;
filter: drop-shadow(0 34px 82px rgba(0, 0, 0, 0.42));
}
body.product-transition-active {
cursor: progress;
}
@media (prefers-reduced-motion: reduce) {
.product-transition {
display: none;
}
}

View File

@ -1,336 +0,0 @@
import {
useCallback,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { useNavigate } from "react-router";
import { gsap } from "gsap";
import { ProductTransitionContext } from "../transitions/ProductTransitionContext";
import "./ProductTransition.css";
const supportsProductTransition = () => {
if (typeof window === "undefined") return false;
return !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
};
const rectToObject = (rect) => ({
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
});
const isPlainNavigationClick = (event) =>
event.button === 0 &&
!event.metaKey &&
!event.altKey &&
!event.ctrlKey &&
!event.shiftKey;
const getStageRect = (sourceRect) => {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const sourceRatio = sourceRect.height / sourceRect.width || 1;
const stageWidth = Math.min(
Math.max(sourceRect.width * 1.16, 260),
viewportWidth * 0.56,
620
);
const stageHeight = Math.min(stageWidth * sourceRatio, viewportHeight * 0.72);
const normalizedWidth = stageHeight / sourceRatio;
return {
left: (viewportWidth - normalizedWidth) / 2,
top: (viewportHeight - stageHeight) / 2,
width: normalizedWidth,
height: stageHeight,
};
};
const getTransformForRect = (rect, sourceRect) => {
const scale = Math.min(
rect.width / sourceRect.width,
rect.height / sourceRect.height
);
const width = sourceRect.width * scale;
const height = sourceRect.height * scale;
return {
x: rect.left + (rect.width - width) / 2,
y: rect.top + (rect.height - height) / 2,
scaleX: scale,
scaleY: scale,
};
};
export function ProductTransitionProvider({ children }) {
const navigate = useNavigate();
const overlayRef = useRef(null);
const imageRef = useRef(null);
const washRef = useRef(null);
const timelineRef = useRef(null);
const [transition, setTransition] = useState(null);
const clearTransition = useCallback(() => {
timelineRef.current?.kill();
timelineRef.current = null;
document.body.classList.remove("product-transition-active");
setTransition(null);
}, []);
const startProductTransition = useCallback(
(event, perfume) => {
if (!perfume?.slug || !isPlainNavigationClick(event)) {
return false;
}
if (!supportsProductTransition()) {
return false;
}
const card = event.currentTarget;
const image = card.querySelector("[data-product-transition-source]");
if (!image) {
return false;
}
const sourceRect = image.getBoundingClientRect();
if (sourceRect.width <= 0 || sourceRect.height <= 0) {
return false;
}
event.preventDefault();
timelineRef.current?.kill();
document.body.classList.add("product-transition-active");
setTransition({
id: `${perfume.slug}-${Date.now()}`,
slug: perfume.slug,
to: `/duft/${perfume.slug}`,
image: image.currentSrc || image.src || perfume.image,
alt: perfume.name,
sourceRect: rectToObject(sourceRect),
phase: "leaving",
});
return true;
},
[]
);
useLayoutEffect(() => {
if (transition?.phase !== "leaving") {
return undefined;
}
const overlay = overlayRef.current;
const image = imageRef.current;
const wash = washRef.current;
if (!overlay || !image || !wash) {
return undefined;
}
const routeContent = document.querySelector("[data-route-content]");
const sourceRect = transition.sourceRect;
const stageRect = getStageRect(sourceRect);
let completed = false;
gsap.set(overlay, { autoAlpha: 1, pointerEvents: "auto" });
gsap.set(wash, { autoAlpha: 0 });
gsap.set(image, {
x: sourceRect.left,
y: sourceRect.top,
width: sourceRect.width,
height: sourceRect.height,
scaleX: 1,
scaleY: 1,
autoAlpha: 1,
transformOrigin: "0 0",
force3D: true,
});
timelineRef.current = gsap.timeline({
defaults: { ease: "power4.inOut" },
onComplete: () => {
completed = true;
navigate(transition.to, {
state: {
productTransition: true,
transitionId: transition.id,
},
});
setTransition((current) =>
current?.id === transition.id
? { ...current, phase: "entering" }
: current
);
},
});
timelineRef.current
.to(wash, { autoAlpha: 1, duration: 0.42, ease: "power2.out" }, 0)
.to(
routeContent,
{
autoAlpha: 0,
filter: "none",
scale: 1,
duration: 0.62,
ease: "power3.out",
},
0
)
.to(
image,
{
...getTransformForRect(stageRect, sourceRect),
duration: 0.78,
},
0.03
);
return () => {
if (!completed) {
timelineRef.current?.kill();
}
};
}, [navigate, transition]);
useLayoutEffect(() => {
if (transition?.phase !== "entering") {
return undefined;
}
const overlay = overlayRef.current;
const image = imageRef.current;
const wash = washRef.current;
if (!overlay || !image || !wash) {
return undefined;
}
let frame = 0;
let cancelled = false;
let completed = false;
const sourceRect = transition.sourceRect;
const runEnterAnimation = () => {
if (cancelled) return;
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
const target = document.querySelector(
`[data-product-transition-target="${transition.slug}"]`
);
if (!target && frame < 16) {
frame += 1;
window.requestAnimationFrame(runEnterAnimation);
return;
}
const routeContent = document.querySelector("[data-route-content]");
gsap.set(routeContent, {
autoAlpha: 1,
filter: "none",
scale: 1,
clearProps: "opacity,visibility,filter,transform",
});
if (!target) {
gsap.to(overlay, {
autoAlpha: 0,
duration: 0.3,
ease: "power2.out",
onComplete: clearTransition,
});
return;
}
const targetRect = rectToObject(target.getBoundingClientRect());
const revealItems = gsap.utils.toArray("[data-product-transition-reveal]");
gsap.set(target, { autoAlpha: 0 });
gsap.set(revealItems, { y: 28, autoAlpha: 0, force3D: true });
timelineRef.current?.kill();
timelineRef.current = gsap.timeline({
defaults: { ease: "power4.out" },
onComplete: () => {
completed = true;
clearTransition();
},
});
timelineRef.current
.to(image, {
...getTransformForRect(targetRect, sourceRect),
duration: 0.72,
})
.set(target, { autoAlpha: 1 }, ">-0.08")
.to(image, { autoAlpha: 0, duration: 0.16, ease: "power2.out" }, "<")
.to(wash, { autoAlpha: 0, duration: 0.5, ease: "power2.out" }, "<")
.to(
revealItems,
{
y: 0,
autoAlpha: 1,
duration: 0.82,
stagger: 0.07,
ease: "power4.out",
clearProps: "transform",
},
">-0.05"
);
};
window.requestAnimationFrame(runEnterAnimation);
return () => {
cancelled = true;
if (!completed) {
timelineRef.current?.kill();
}
};
}, [clearTransition, transition]);
const value = useMemo(
() => ({
activeSlug: transition?.slug || null,
phase: transition?.phase || "idle",
startProductTransition,
}),
[startProductTransition, transition]
);
return (
<ProductTransitionContext.Provider value={value}>
{children}
{transition && (
<div
className={`product-transition product-transition--${transition.phase}`}
ref={overlayRef}
aria-hidden="true"
>
<div className="product-transition__wash" ref={washRef} />
<img
className="product-transition__image"
src={transition.image}
alt={transition.alt}
ref={imageRef}
decoding="async"
/>
</div>
)}
</ProductTransitionContext.Provider>
);
}

View File

@ -1,9 +1,9 @@
import { Link } from "react-router"; import { Link } from "react-router";
import { useShop } from "../shop/useShop"; import { useShop } from "../shop/useShop";
import { useTheme } from "../theme/useTheme"; import { useTheme } from "../theme/ThemeContext";
import "../style/navbar.css"; import "../style/navbar.css";
function SharedNavbar({ variant = "hero", active = "" }) { function SharedNavbar({ variant = "light", active = "" }) {
const { cart, openCart, openProfile, user } = useShop(); const { cart, openCart, openProfile, user } = useShop();
const { isLight, toggleTheme } = useTheme(); const { isLight, toggleTheme } = useTheme();
const cartLabel = const cartLabel =

View File

@ -495,89 +495,3 @@
} }
/* --- Design System Refinement Start --- */
.shop-drawer {
width: min(560px, 100%);
min-width: 0;
padding: clamp(1rem, 3vw, 1.4rem);
box-shadow: var(--theme-shadow);
}
.drawer-top button,
.cart-toast-close,
.cart-controls button,
.subscription-row button {
min-width: 44px;
min-height: 44px;
}
.drawer-section,
.cart-item,
.order-card,
.read-block,
.status-box,
.requirement-row,
.drawer-error,
.payment-card,
.pref-toggle,
.subscription-row,
.totals-box .discount-explainer,
.cart-toast,
.shop-field input {
border-radius: var(--radius-lg);
}
.drawer-primary,
.drawer-secondary,
.cart-remove,
.cart-controls,
.cart-controls button,
.cart-toast button,
.subscription-row button {
border-radius: 999px;
}
.drawer-primary,
.cart-toast > button:last-child {
min-height: 48px;
}
.drawer-primary,
.drawer-secondary,
.cart-remove,
.payment-card,
.pref-toggle,
.subscription-row button {
transition:
transform var(--duration-med) var(--ease-out),
border-color var(--duration-med) var(--ease-out),
background-color var(--duration-med) var(--ease-out),
box-shadow var(--duration-med) var(--ease-out);
}
.drawer-primary:hover:not(:disabled),
.drawer-secondary:hover,
.cart-remove:hover,
.payment-card:hover,
.pref-toggle:hover,
.subscription-row button:hover {
transform: translateY(-1px);
border-color: rgba(var(--theme-accent-rgb) / 0.55);
}
@media (max-width: 640px) {
.shop-drawer {
width: 100%;
}
.cart-toast {
right: var(--page-x);
bottom: var(--page-x);
width: calc(100vw - (var(--page-x) * 2));
}
}
/* --- Design System Refinement End --- */

View File

@ -274,118 +274,3 @@
} }
} }
/* --- Design System Refinement Start --- */
.chatbot-trigger {
right: max(0.9rem, env(safe-area-inset-right));
bottom: max(0.9rem, env(safe-area-inset-bottom));
width: 52px;
height: 52px;
min-height: 52px;
min-width: 52px;
padding: 0;
display: inline-grid;
place-items: center;
font-size: 0;
box-shadow: var(--theme-shadow-soft);
}
.chatbot-trigger.is-open {
padding: 0;
}
.chatbot-trigger-icon,
.chatbot-close-icon {
display: block;
width: 1.45rem;
height: 1.45rem;
background: currentColor;
-webkit-mask-position: center;
mask-position: center;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: contain;
mask-size: contain;
}
.chatbot-trigger-icon {
-webkit-mask-image: url("/icon-chat.svg");
mask-image: url("/icon-chat.svg");
}
.chatbot-trigger.is-open .chatbot-trigger-icon,
.chatbot-close-icon {
-webkit-mask-image: url("/icon-x.svg");
mask-image: url("/icon-x.svg");
}
.chatbot-trigger:hover,
.chatbot-trigger:focus-visible {
transform: translateY(-2px);
box-shadow: var(--theme-shadow);
}
.chatbot-window {
right: var(--page-x);
bottom: calc(var(--page-x) + 4rem);
border-radius: var(--radius-lg);
box-shadow: var(--theme-shadow);
}
.chatbot-close,
.chatbot-send,
.chatbot-chip,
.chatbot-feedback-btn {
min-height: 44px;
}
.chatbot-close {
min-width: 44px;
}
.chatbot-message,
.chatbot-chip,
.chatbot-feedback-btn,
.chatbot-input,
.chatbot-send {
border-radius: var(--radius-lg);
}
.chatbot-send {
border-radius: 999px;
}
.chatbot-chip,
.chatbot-feedback-btn,
.chatbot-send {
transition:
transform var(--duration-med) var(--ease-out),
border-color var(--duration-med) var(--ease-out),
background-color var(--duration-med) var(--ease-out);
}
.chatbot-chip:hover,
.chatbot-feedback-btn:hover,
.chatbot-send:hover {
transform: translateY(-1px);
border-color: rgba(var(--theme-accent-rgb) / 0.55);
}
@media (max-width: 700px) {
.chatbot-trigger {
right: max(0.75rem, env(safe-area-inset-right));
bottom: max(0.75rem, env(safe-area-inset-bottom));
}
.chatbot-window {
right: var(--page-x);
bottom: calc(var(--page-x) + 4.1rem);
width: calc(100vw - (var(--page-x) * 2));
height: min(72svh, 620px);
max-height: calc(100svh - 6rem);
}
}
/* --- Design System Refinement End --- */

View File

@ -216,7 +216,7 @@ function SupportChatbot() {
onClick={() => setIsOpen((prev) => !prev)} onClick={() => setIsOpen((prev) => !prev)}
aria-label={isOpen ? "Chat schliessen" : "Support Chat öffnen"} aria-label={isOpen ? "Chat schliessen" : "Support Chat öffnen"}
> >
<span className="chatbot-trigger-icon" aria-hidden="true" /> {isOpen ? "×" : "Support"}
</button> </button>
{isOpen && ( {isOpen && (
@ -232,7 +232,7 @@ function SupportChatbot() {
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
aria-label="Chat schliessen" aria-label="Chat schliessen"
> >
<span className="chatbot-close-icon" aria-hidden="true" /> ×
</button> </button>
</div> </div>

View File

@ -1,5 +1,6 @@
import { Link } from "react-router"; import { Link } from "react-router";
import IntroOverlay from "./IntroOverlay"; import IntroOverlay from "./IntroOverlay";
import SharedNavbar from "../SharedNavbar";
function HeroSection({ function HeroSection({
heroImageWrapRef, heroImageWrapRef,
@ -16,15 +17,16 @@ function HeroSection({
<div className="hero-media" ref={heroImageWrapRef}> <div className="hero-media" ref={heroImageWrapRef}>
<img <img
src="/atmos-hero-image.png" src="/atmos-hero-image.png"
alt="Atmos perfume bottle in a material-led campaign scene" alt="Atmos Hero"
className="hero-media__image" className="hero-media__image"
ref={heroImageRef} ref={heroImageRef}
loading="eager" loading="eager"
decoding="async"
fetchPriority="high" fetchPriority="high"
/> />
</div> </div>
<SharedNavbar variant="hero" active="atmos" />
<div className="hero-content"> <div className="hero-content">
<h1 className="hero-title"> <h1 className="hero-title">
<span className="hero-title-line"> <span className="hero-title-line">

View File

@ -57,7 +57,7 @@ const restoreRevealLines = (element) => {
delete element.dataset.revealOriginalHtml; delete element.dataset.revealOriginalHtml;
}; };
function useScrollTextReveal(scopeRef, dependencyKey = "") { function useScrollTextReveal(scopeRef, deps = []) {
useLayoutEffect(() => { useLayoutEffect(() => {
const scope = scopeRef.current; const scope = scopeRef.current;
@ -162,7 +162,7 @@ function useScrollTextReveal(scopeRef, dependencyKey = "") {
ctx.revert(); ctx.revert();
preparedElements.forEach((element) => restoreRevealLines(element)); preparedElements.forEach((element) => restoreRevealLines(element));
}; };
}, [scopeRef, dependencyKey]); }, [scopeRef, ...deps]);
} }
export default useScrollTextReveal; export default useScrollTextReveal;

View File

@ -3,7 +3,6 @@
--theme-black: #262626; --theme-black: #262626;
--theme-white: #eaeaea; --theme-white: #eaeaea;
--theme-accent: #ff6a00; --theme-accent: #ff6a00;
--theme-accent-rgb: 255 106 0;
--theme-bg: #262626; --theme-bg: #262626;
--theme-surface: #2f2f2f; --theme-surface: #2f2f2f;
--theme-surface-soft: #363636; --theme-surface-soft: #363636;
@ -11,47 +10,6 @@
--theme-text: #eaeaea; --theme-text: #eaeaea;
--theme-text-muted: #c8c8c8; --theme-text-muted: #c8c8c8;
--theme-border: #4a4a4a; --theme-border: #4a4a4a;
--theme-border-strong: rgba(234, 234, 234, 0.26);
--theme-shadow: 0 24px 70px rgba(0, 0, 0, 0.28);
--theme-shadow-soft: 0 16px 42px rgba(0, 0, 0, 0.18);
--page-x: clamp(1rem, 4vw, 5rem);
--section-y-xs: clamp(2rem, 5vw, 4.5rem);
--section-y-sm: clamp(3rem, 7vw, 7rem);
--section-y: clamp(4rem, 10vw, 10rem);
--section-y-lg: clamp(5rem, 14vw, 14rem);
--container: min(calc(100% - (var(--page-x) * 2)), 1440px);
--container-narrow: min(calc(100% - (var(--page-x) * 2)), 920px);
--container-wide: min(calc(100% - (var(--page-x) * 2)), 1680px);
--text-measure: 68ch;
--gap-2xs: clamp(0.35rem, 0.7vw, 0.65rem);
--gap-xs: clamp(0.5rem, 1vw, 0.875rem);
--gap-sm: clamp(0.75rem, 1.5vw, 1.25rem);
--gap-md: clamp(1rem, 2vw, 2rem);
--gap-lg: clamp(1.5rem, 4vw, 4rem);
--gap-xl: clamp(2rem, 6vw, 6rem);
--radius-xs: 0;
--radius-sm: 0;
--radius-md: 0;
--radius-lg: 0;
--radius-xl: 0;
--text-xs: clamp(0.75rem, 0.72rem + 0.15vw, 0.875rem);
--text-sm: clamp(0.875rem, 0.83rem + 0.2vw, 1rem);
--text-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--text-lg: clamp(1.125rem, 1.05rem + 0.35vw, 1.375rem);
--text-xl: clamp(1.35rem, 1.15rem + 0.9vw, 2rem);
--text-2xl: clamp(1.75rem, 1.25rem + 2vw, 3.5rem);
--text-display: clamp(3.05rem, 10.5vw, 10.8rem);
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--ease-snap: cubic-bezier(0.16, 1, 0.3, 1);
--duration-fast: 160ms;
--duration-med: 260ms;
--duration-slow: 720ms;
color: var(--theme-text); color: var(--theme-text);
background: var(--theme-bg); background: var(--theme-bg);
@ -88,9 +46,6 @@ body.theme-light {
--theme-text: #262626; --theme-text: #262626;
--theme-text-muted: #5f5f5f; --theme-text-muted: #5f5f5f;
--theme-border: #d6d6d6; --theme-border: #d6d6d6;
--theme-border-strong: rgba(38, 38, 38, 0.22);
--theme-shadow: 0 24px 70px rgba(38, 38, 38, 0.13);
--theme-shadow-soft: 0 16px 42px rgba(38, 38, 38, 0.1);
} }
a { a {
@ -101,65 +56,3 @@ button {
font: inherit; font: inherit;
} }
html {
overflow-x: hidden;
overflow-x: clip;
}
body {
overflow-x: hidden;
overflow-x: clip;
}
img,
picture,
video,
canvas,
svg {
max-width: 100%;
}
img,
video {
height: auto;
}
button,
a,
input,
textarea,
select {
-webkit-tap-highlight-color: transparent;
}
button,
[role="button"],
a {
touch-action: manipulation;
}
:focus-visible {
outline: 2px solid var(--theme-accent);
outline-offset: 4px;
}
::selection {
background: rgba(var(--theme-accent-rgb) / 0.35);
color: var(--theme-white);
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms;
animation-iteration-count: 1;
scroll-behavior: auto;
transition-duration: 0.01ms;
}
}

View File

@ -1,282 +1,208 @@
.about-page { .about-page {
min-height: 100vh; min-height: 100vh;
padding: 0 0 var(--section-y-sm);
color: var(--theme-text); color: var(--theme-text);
padding: 26px 38px 38px;
background: background:
radial-gradient(circle at 86% 8%, rgba(var(--theme-accent-rgb) / 0.13), transparent 28rem), linear-gradient(to right, rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0.1)),
linear-gradient(180deg, var(--theme-bg), color-mix(in srgb, var(--theme-bg) 88%, #000 12%)); linear-gradient(to bottom, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.45));
}
.about-shell {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
padding: 38px;
} }
.about-kicker, .about-kicker,
.about-label, .about-label,
.about-panel-label, .about-panel-label,
.about-origin-box span, .about-origin-box span,
.about-panel-meta span, .about-panel-meta span {
.about-method-points span {
display: block; display: block;
color: var(--theme-text-muted); font-size: 10px;
font-size: var(--text-xs);
letter-spacing: 0.22em; letter-spacing: 0.22em;
text-transform: uppercase; color: var(--theme-text-muted);
} }
.about-hero { .about-hero {
display: grid; display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(18rem, 0.72fr); grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.8fr);
gap: var(--gap-lg); gap: 28px;
align-items: end; align-items: end;
padding: clamp(2rem, 5vw, 5rem) 0 var(--section-y-sm); padding-bottom: 36px;
border-bottom: 1px solid var(--theme-border); border-bottom: 1px solid var(--theme-border);
} }
.about-hero-copy {
min-width: 0;
}
.about-hero-copy h1 { .about-hero-copy h1 {
max-width: 11.4ch; margin: 14px 0 18px;
margin: clamp(0.85rem, 2vw, 1.2rem) 0 clamp(1rem, 2vw, 1.35rem); font-size: 68px;
color: var(--theme-text); line-height: 0.92;
font-size: clamp(3rem, 7.4vw, 8.8rem);
line-height: 0.9;
font-weight: 300; font-weight: 300;
letter-spacing: 0; letter-spacing: -0.05em;
text-transform: uppercase; color: var(--theme-text);
text-wrap: balance;
} }
.about-intro { .about-intro {
max-width: var(--text-measure); max-width: 720px;
margin: 0; margin: 0;
color: var(--theme-text-muted); font-size: 18px;
font-size: var(--text-lg);
line-height: 1.65; line-height: 1.65;
color: var(--theme-text-muted);
} }
.about-hero-panel { .about-hero-panel {
padding: clamp(1.25rem, 3vw, 2rem); padding: 24px;
border: 1px solid rgba(var(--theme-accent-rgb) / 0.2); background: linear-gradient(
background: 180deg,
linear-gradient(135deg, rgba(var(--theme-accent-rgb) / 0.1), transparent 62%), rgba(255, 106, 0, 0.08),
var(--theme-surface-soft); rgba(255, 106, 0, 0.03)
);
border: 1px solid rgba(255, 106, 0, 0.18);
} }
.about-hero-panel p { .about-hero-panel p {
margin: 0.75rem 0 0; margin: 10px 0 0;
font-size: 16px;
line-height: 1.6;
color: var(--theme-text); color: var(--theme-text);
font-size: var(--text-base);
line-height: 1.62;
} }
.about-panel-meta { .about-panel-meta {
display: grid; display: grid;
gap: var(--gap-sm); grid-template-columns: 1fr;
margin-top: clamp(1.2rem, 2.6vw, 2rem); gap: 16px;
padding-top: var(--gap-sm); margin-top: 24px;
border-top: 1px solid rgba(var(--theme-accent-rgb) / 0.2); padding-top: 20px;
border-top: 1px solid rgba(255, 106, 0, 0.14);
} }
.about-panel-meta p, .about-panel-meta p,
.about-origin-box p, .about-origin-box p {
.about-method-points p { margin: 8px 0 0;
margin: 0.45rem 0 0; font-size: 14px;
color: var(--theme-text);
font-size: var(--text-sm);
line-height: 1.55; line-height: 1.55;
color: var(--theme-text);
} }
.about-section { .about-section {
padding-top: var(--section-y-sm); padding-top: 38px;
} }
.about-section--split, .about-section--split {
.about-origin-section,
.about-method-section {
display: grid; display: grid;
grid-template-columns: minmax(16rem, 0.72fr) minmax(0, 1.28fr); grid-template-columns: minmax(260px, 0.7fr) minmax(0, 1.3fr);
gap: var(--gap-lg); gap: 28px;
align-items: start; align-items: start;
} }
.about-section-heading h2, .about-section-heading h2,
.about-origin-copy h2, .about-origin-copy h2,
.about-bottom-copy h2, .about-bottom-copy h2 {
.about-method-copy h2 { margin: 10px 0 0;
margin: 0.75rem 0 0; font-size: 42px;
color: var(--theme-text); line-height: 0.98;
font-size: clamp(2.15rem, 5.2vw, 6rem);
line-height: 0.94;
font-weight: 300; font-weight: 300;
letter-spacing: 0; letter-spacing: -0.04em;
text-transform: uppercase; color: var(--theme-text);
text-wrap: balance;
} }
.about-section-copy, .about-section-copy {
.about-method-points {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--gap-sm); gap: 18px;
} }
.about-section-copy p, .about-section-copy p,
.about-origin-copy p, .about-origin-copy p,
.about-bottom-copy p, .about-bottom-copy p {
.about-method-copy p,
.about-credential-card p,
.about-card p,
.about-proof-item p,
.about-trust-note p {
margin: 0; margin: 0;
color: var(--theme-text-muted); font-size: 16px;
font-size: var(--text-base);
line-height: 1.7; line-height: 1.7;
} color: var(--theme-text-muted);
.about-section-copy p + p,
.about-origin-copy p + p {
margin-top: var(--gap-sm);
}
.about-proof-strip,
.about-grid-section,
.about-credentials-grid {
display: grid;
gap: var(--gap-sm);
margin-top: var(--section-y-sm);
}
.about-proof-strip {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.about-grid-section {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.about-credentials-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.about-card,
.about-proof-item,
.about-credential-card,
.about-origin-box,
.about-method-section,
.about-trust-note {
border: 1px solid var(--theme-border);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.008)),
var(--theme-surface-soft);
}
.about-card,
.about-proof-item,
.about-credential-card {
min-height: 100%;
padding: clamp(1.1rem, 2.4vw, 1.8rem);
transition:
transform var(--duration-med) var(--ease-out),
border-color var(--duration-med) var(--ease-out),
box-shadow var(--duration-med) var(--ease-out);
}
.about-card:hover,
.about-proof-item:hover,
.about-credential-card:hover {
transform: translateY(-4px);
border-color: rgba(var(--theme-accent-rgb) / 0.42);
box-shadow: var(--theme-shadow-soft);
}
.about-card h3,
.about-credential-card h3 {
margin: 0.9rem 0 0.75rem;
color: var(--theme-text);
font-size: var(--text-xl);
line-height: 1.08;
font-weight: 400;
letter-spacing: 0;
}
.about-proof-item {
min-height: 9rem;
}
.about-proof-item p {
margin-top: 0.7rem;
} }
.about-quote-block { .about-quote-block {
margin-top: var(--section-y-sm); margin-top: 38px;
padding: clamp(1.4rem, 4vw, 3rem); padding: 32px 36px;
overflow: hidden; background: #1f1f1f;
border-left: 3px solid var(--theme-accent); border-left: 3px solid #ff6a00;
background:
radial-gradient(circle at 100% 0%, rgba(var(--theme-accent-rgb) / 0.18), transparent 18rem),
#171717;
} }
.about-quote-block p { .about-quote-block p {
max-width: 58rem;
margin: 0; margin: 0;
color: #fff; font-size: 28px;
font-size: clamp(1.7rem, 4vw, 4.2rem); line-height: 1.3;
line-height: 1.08;
font-weight: 300; font-weight: 300;
letter-spacing: 0; letter-spacing: -0.03em;
color: #fff;
max-width: 900px;
}
.about-grid-section {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
margin-top: 38px;
}
.about-card {
padding: 24px;
background: var(--theme-bg);
border: 1px solid var(--theme-border);
min-height: 260px;
}
.about-card h3 {
margin: 14px 0 12px;
font-size: 24px;
line-height: 1.05;
font-weight: 400;
color: var(--theme-text);
}
.about-card p {
margin: 0;
font-size: 15px;
line-height: 1.65;
color: var(--theme-text-muted);
} }
.about-process-section,
.about-origin-section { .about-origin-section {
margin-top: var(--section-y-sm); display: grid;
padding-top: var(--section-y-sm); grid-template-columns: minmax(0, 1.2fr) minmax(280px, 0.8fr);
border-top: 1px solid var(--theme-border); gap: 28px;
} margin-top: 38px;
padding-top: 38px;
.about-method-section {
margin-top: var(--section-y-sm);
padding: clamp(1.25rem, 3vw, 2.4rem);
}
.about-method-points > div,
.about-origin-box > div {
padding-top: 1rem;
border-top: 1px solid var(--theme-border); border-top: 1px solid var(--theme-border);
} }
.about-origin-box { .about-origin-box {
display: grid; display: flex;
gap: var(--gap-sm); flex-direction: column;
padding: clamp(1.1rem, 2.4vw, 1.8rem); gap: 18px;
background: var(--theme-bg);
border: 1px solid var(--theme-border);
padding: 24px;
} }
.about-trust-note { .about-origin-box > div {
margin-top: var(--section-y-sm); padding-bottom: 16px;
padding: clamp(1.1rem, 2.4vw, 1.8rem); border-bottom: 1px solid var(--theme-border);
border-color: rgba(var(--theme-accent-rgb) / 0.24);
background:
linear-gradient(135deg, rgba(var(--theme-accent-rgb) / 0.11), transparent 60%),
var(--theme-surface-soft);
} }
.about-trust-note p { .about-origin-box > div:last-child {
max-width: 72rem; border-bottom: none;
margin-top: 0.75rem; padding-bottom: 0;
color: var(--theme-text);
} }
.about-bottom-cta { .about-bottom-cta {
margin-top: 38px;
padding: 38px;
background: #ff6a00;
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1.2fr) auto;
gap: var(--gap-lg); gap: 24px;
align-items: end; align-items: end;
margin-top: var(--section-y-sm);
padding: clamp(1.5rem, 4vw, 3.5rem);
overflow: hidden;
background:
radial-gradient(circle at 92% 0%, rgba(255, 255, 255, 0.22), transparent 20rem),
var(--theme-accent);
} }
.about-bottom-copy .about-label, .about-bottom-copy .about-label,
@ -285,90 +211,246 @@
color: #fff; color: #fff;
} }
.about-bottom-copy .about-label {
opacity: 0.85;
}
.about-bottom-copy p { .about-bottom-copy p {
max-width: 48rem; margin-top: 16px;
margin-top: 1rem; max-width: 700px;
} }
.about-bottom-actions { .about-bottom-actions {
display: flex; display: flex;
gap: 12px;
flex-wrap: wrap; flex-wrap: wrap;
gap: var(--gap-xs);
justify-content: flex-end;
} }
.about-btn { .about-btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 48px;
padding: 0 1.1rem;
border: 1px solid transparent;
border-radius: 999px;
color: inherit;
font-size: var(--text-sm);
text-decoration: none; text-decoration: none;
transition: padding: 12px 18px;
transform var(--duration-med) var(--ease-out), font-size: 14px;
box-shadow var(--duration-med) var(--ease-out), border-radius: 999px;
background-color var(--duration-med) var(--ease-out); transition: transform 0.2s ease, opacity 0.2s ease, box-shadow 0.2s ease;
} }
.about-btn:hover, .about-btn:hover {
.about-btn:focus-visible { transform: translateY(-1px);
transform: translateY(-2px);
box-shadow: var(--theme-shadow-soft);
} }
.about-btn--primary { .about-btn--primary {
background: #fff; background: var(--theme-paper);
color: var(--theme-accent); color: #ff6a00;
} }
.about-btn--secondary { .about-btn--secondary {
border-color: rgba(255, 255, 255, 0.22);
background: rgba(255, 255, 255, 0.14); background: rgba(255, 255, 255, 0.14);
color: #fff; color: #fff;
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
} }
@media (max-width: 1180px) { .about-proof-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-top: 38px;
}
.about-proof-item {
padding: 18px;
border: 1px solid var(--theme-border);
background: var(--theme-surface-soft);
}
.about-proof-item p {
margin: 10px 0 0;
font-size: 14px;
line-height: 1.55;
color: var(--theme-text-muted);
}
.about-process-section {
margin-top: 38px;
padding-top: 38px;
border-top: 1px solid var(--theme-border);
}
.about-credentials-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 18px;
margin-top: 38px;
}
.about-credential-card {
padding: 24px;
background: linear-gradient(
180deg,
rgba(255, 106, 0, 0.06),
rgba(255, 106, 0, 0.02)
);
border: 1px solid rgba(255, 106, 0, 0.16);
min-height: 220px;
}
.about-credential-card h3 {
margin: 14px 0 12px;
font-size: 24px;
line-height: 1.08;
font-weight: 400;
color: var(--theme-text);
}
.about-credential-card p {
margin: 0;
font-size: 15px;
line-height: 1.65;
color: var(--theme-text-muted);
}
.about-method-section {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(320px, 0.95fr);
gap: 28px;
margin-top: 38px;
padding: 28px;
background: var(--theme-bg);
border: 1px solid var(--theme-border);
}
.about-method-copy h2 {
margin: 10px 0 16px;
font-size: 40px;
line-height: 0.98;
font-weight: 300;
letter-spacing: -0.04em;
color: var(--theme-text);
}
.about-method-copy p {
margin: 0;
font-size: 16px;
line-height: 1.7;
color: var(--theme-text-muted);
}
.about-method-points {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
.about-method-points > div {
padding-bottom: 14px;
border-bottom: 1px solid var(--theme-border);
}
.about-method-points > div:last-child {
border-bottom: none;
padding-bottom: 0;
}
.about-method-points span {
display: block;
font-size: 10px;
letter-spacing: 0.22em;
color: var(--theme-text-muted);
}
.about-method-points p {
margin: 8px 0 0;
font-size: 14px;
line-height: 1.55;
color: var(--theme-text);
}
.about-trust-note {
margin-top: 38px;
padding: 22px;
border: 1px solid rgba(255, 106, 0, 0.18);
background: linear-gradient(
180deg,
rgba(255, 106, 0, 0.08),
rgba(255, 106, 0, 0.03)
);
}
.about-trust-note p {
margin: 10px 0 0;
font-size: 15px;
line-height: 1.65;
color: var(--theme-text);
}
@media (max-width: 1100px) {
.about-hero, .about-hero,
.about-section--split, .about-section--split,
.about-origin-section, .about-origin-section,
.about-method-section, .about-bottom-cta,
.about-bottom-cta { .about-method-section {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.about-proof-strip,
.about-grid-section,
.about-credentials-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 760px) {
.about-hero {
padding-top: clamp(1.4rem, 5vw, 2rem);
}
.about-hero-copy h1 { .about-hero-copy h1 {
font-size: clamp(2.55rem, 13vw, 4.4rem); font-size: 52px;
} }
.about-proof-strip,
.about-grid-section, .about-grid-section,
.about-proof-strip,
.about-credentials-grid { .about-credentials-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.about-bottom-actions { .about-method-copy h2 {
display: grid; font-size: 32px;
justify-content: stretch; }
} }
.about-btn { @media (max-width: 700px) {
width: 100%; .about-page {
padding: 18px;
}
.about-shell {
padding: 24px 18px;
}
.about-hero-copy h1 {
font-size: 38px;
}
.about-section-heading h2,
.about-origin-copy h2,
.about-bottom-copy h2,
.about-method-copy h2 {
font-size: 30px;
}
.about-intro,
.about-section-copy p,
.about-origin-copy p,
.about-bottom-copy p,
.about-method-copy p,
.about-credential-card p,
.about-proof-item p,
.about-trust-note p {
font-size: 15px;
}
.about-quote-block {
padding: 24px 20px;
}
.about-quote-block p {
font-size: 22px;
}
.about-method-section,
.about-bottom-cta {
padding: 26px 20px;
} }
} }

View File

@ -5,9 +5,9 @@ import "./AboutPage.css";
function AboutPage() { function AboutPage() {
return ( return (
<div className="about-page"> <div className="about-page">
<SharedNavbar variant="hero" /> <SharedNavbar variant="light" />
<main className="shell"> <main className="about-shell">
<section className="about-hero" data-reveal-group> <section className="about-hero" data-reveal-group>
<div className="about-hero-copy"> <div className="about-hero-copy">
<span className="about-kicker" data-reveal="fade"> <span className="about-kicker" data-reveal="fade">

View File

@ -1,108 +1,135 @@
.datenschutz-page { .datenschutz-page {
min-height: 100vh; min-height: 100vh;
padding: 0 0 var(--section-y-sm);
color: var(--theme-text); color: var(--theme-text);
padding: 26px 38px 38px;
background: background:
radial-gradient(circle at 86% 8%, rgba(var(--theme-accent-rgb) / 0.12), transparent 28rem), linear-gradient(to right, rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0.1)),
linear-gradient(180deg, var(--theme-bg), color-mix(in srgb, var(--theme-bg) 88%, #000 12%)); linear-gradient(to bottom, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.45));
}
.datenschutz-shell {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
padding: 38px;
} }
.datenschutz-kicker, .datenschutz-kicker,
.datenschutz-label { .datenschutz-label {
display: block; display: block;
color: var(--theme-text-muted); font-size: 10px;
font-size: var(--text-xs);
letter-spacing: 0.22em; letter-spacing: 0.22em;
text-transform: uppercase; color: var(--theme-text-muted);
} }
.datenschutz-hero { .datenschutz-hero {
padding: clamp(2rem, 5vw, 5rem) 0 var(--section-y-sm); padding-bottom: 32px;
border-bottom: 1px solid var(--theme-border); border-bottom: 1px solid var(--theme-border);
} }
.datenschutz-hero h1 { .datenschutz-hero h1 {
margin: clamp(0.85rem, 2vw, 1.2rem) 0 clamp(1rem, 2vw, 1.35rem); margin: 14px 0 16px;
color: var(--theme-text); font-size: 64px;
font-size: clamp(3rem, 8vw, 7.2rem); line-height: 0.92;
line-height: 0.9;
font-weight: 300; font-weight: 300;
letter-spacing: 0; letter-spacing: -0.05em;
text-transform: uppercase; color: var(--theme-text);
} }
.datenschutz-intro { .datenschutz-intro {
max-width: var(--text-measure); max-width: 820px;
margin: 0; margin: 0;
color: var(--theme-text-muted); font-size: 17px;
font-size: var(--text-lg);
line-height: 1.7; line-height: 1.7;
color: var(--theme-text-muted);
} }
.datenschutz-section { .datenschutz-section {
display: grid; display: grid;
grid-template-columns: minmax(14rem, 0.72fr) minmax(0, 1.28fr); grid-template-columns: minmax(260px, 0.7fr) minmax(0, 1.3fr);
gap: var(--gap-lg); gap: 28px;
align-items: start; align-items: start;
margin-top: var(--section-y-sm); margin-top: 38px;
padding-top: var(--section-y-sm); padding-top: 38px;
border-top: 1px solid var(--theme-border); border-top: 1px solid var(--theme-border);
} }
.datenschutz-section-heading h2 { .datenschutz-section-heading h2 {
margin: 0.75rem 0 0; margin: 10px 0 0;
color: var(--theme-text); font-size: 38px;
font-size: clamp(2rem, 4.2vw, 4.5rem); line-height: 0.98;
line-height: 0.96;
font-weight: 300; font-weight: 300;
letter-spacing: 0; letter-spacing: -0.04em;
text-transform: uppercase; color: var(--theme-text);
text-wrap: balance;
} }
.datenschutz-section-copy p { .datenschutz-section-copy p {
margin: 0 0 1rem; margin: 0 0 16px;
color: var(--theme-text-muted); font-size: 16px;
font-size: var(--text-base);
line-height: 1.75; line-height: 1.75;
color: var(--theme-text-muted);
} }
.datenschutz-list { .datenschutz-list {
display: grid;
gap: 0.75rem;
margin: 0; margin: 0;
padding-left: 1.1rem; padding-left: 18px;
display: grid;
gap: 10px;
} }
.datenschutz-list li { .datenschutz-list li {
color: var(--theme-text-muted); font-size: 16px;
font-size: var(--text-base);
line-height: 1.7; line-height: 1.7;
color: var(--theme-text-muted);
} }
.datenschutz-note-box { .datenschutz-note-box {
padding: clamp(1.1rem, 2.4vw, 1.8rem); padding: 22px;
border: 1px solid rgba(var(--theme-accent-rgb) / 0.24); border: 1px solid rgba(255, 106, 0, 0.18);
background: background: linear-gradient(
linear-gradient(135deg, rgba(var(--theme-accent-rgb) / 0.11), transparent 60%), 180deg,
var(--theme-surface-soft); rgba(255, 106, 0, 0.08),
rgba(255, 106, 0, 0.03)
);
} }
.datenschutz-note-box p { .datenschutz-note-box p {
margin: 0; margin: 0;
color: var(--theme-text); font-size: 15px;
font-size: var(--text-base);
line-height: 1.65; line-height: 1.65;
color: var(--theme-text);
} }
@media (max-width: 900px) { @media (max-width: 1100px) {
.datenschutz-section { .datenschutz-section {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.datenschutz-hero h1 {
font-size: 48px;
}
} }
@media (max-width: 700px) { @media (max-width: 700px) {
.datenschutz-hero { .datenschutz-page {
padding-top: clamp(1.4rem, 5vw, 2rem); padding: 18px;
}
.datenschutz-shell {
padding: 24px 18px;
}
.datenschutz-hero h1 {
font-size: 36px;
}
.datenschutz-section-heading h2 {
font-size: 28px;
}
.datenschutz-intro,
.datenschutz-section-copy p,
.datenschutz-list li {
font-size: 15px;
} }
} }

View File

@ -4,9 +4,9 @@ import "./DatenschutzPage.css";
function DatenschutzPage() { function DatenschutzPage() {
return ( return (
<div className="datenschutz-page"> <div className="datenschutz-page">
<SharedNavbar variant="hero" /> <SharedNavbar variant="light" />
<main className="shell"> <main className="datenschutz-shell">
<section className="datenschutz-hero" data-reveal-group> <section className="datenschutz-hero" data-reveal-group>
<span className="datenschutz-kicker" data-reveal="fade">RECHTLICHE ANGABEN</span> <span className="datenschutz-kicker" data-reveal="fade">RECHTLICHE ANGABEN</span>
<h1 data-reveal="lines">DATENSCHUTZ</h1> <h1 data-reveal="lines">DATENSCHUTZ</h1>

File diff suppressed because it is too large Load Diff

View File

@ -4,203 +4,127 @@ import SharedNavbar from "../components/SharedNavbar";
import { useShop } from "../shop/useShop"; import { useShop } from "../shop/useShop";
import "./DiscoverySetPage.css"; import "./DiscoverySetPage.css";
const DISCOVERY_SET_IMAGE = "/atmos-discovery-set-thumbnail.png"; const moodImages = [
"/DISCOVERYSET.png",
const discoveryPanelFacts = [ "/DISCOVERYSET.png",
{ label: "Umfang", value: "6 × 2ml" }, "/DISCOVERYSET.png",
{ label: "Gutschrift", value: "CHF 48 werden beim späteren Full-Size-Kauf berücksichtigt." }, "/DISCOVERYSET.png",
"/DISCOVERYSET.png",
"/DISCOVERYSET.png",
]; ];
const discoveryBenefits = [ function DiscoverySetPage() {
{ const navigate = useNavigate();
title: "6 × 2ml Samples aller Signature-Düfte", const { addToCart } = useShop();
text: "Kalter Beton, Schwarzes Benzin, Verbranntes Chrom, Blasse Seide, Weisse Asche und Nasser Marmor.", const buyDiscoverySet = () =>
}, addToCart("discovery-set", 1, "Discovery Set added.").catch(() => {});
{
title: "CHF 48 Gutschein automatisch im Set",
text: "Nur das erste Discovery Set erstellt den einmaligen Rabatt. Er wird bei einem späteren Full-Size-Kauf automatisch angerechnet.",
},
{
title: "Jedes Sample = ca. 20 Anwendungen",
text: "Genug, um jeden Duft mehrere Tage im Alltag und in unterschiedlichen Situationen zu testen.",
},
{
title: "Hochwertige Mini-Flacons",
text: "Sorgfältig zusammengestellt, reduziert gestaltet und als Teil des atmos Konzepts gedacht.",
},
];
const discoverySteps = [
{
number: "01",
title: "Bestellen",
text: "Discovery Set für CHF 48 bestellen. Nur dein erstes Set erzeugt automatisch einen einmaligen Rabatt.",
},
{
number: "02",
title: "Testen",
text: "Jeden Duft mindestens einige Tage tragen. Im Alltag, zu verschiedenen Anlässen und auf der eigenen Haut.",
},
{
number: "03",
title: "Entscheiden",
text: "Full-Size bestellen. CHF 48 werden automatisch angerechnet, sofern der Rabatt noch nicht genutzt wurde.",
},
];
const discoveryComparison = [
{
icon: "×",
title: "Traditioneller Weg",
text: "CHF 180+ für eine Full Size ausgeben, ohne zu wissen, ob sie wirklich passt. Risiko: Fehlkauf, Überforderung oder ein Duft, der im Regal bleibt.",
},
{
icon: "✓",
title: "Discovery Set Weg",
text: "CHF 48 investieren, alle Düfte testen, bewusst entscheiden. Die erste Investition wird einmalig angerechnet der Einstieg bleibt kontrolliert, nachvollziehbar und fair.",
highlight: true,
},
];
function DiscoveryOrderPanel({ onBuy }) {
return ( return (
<aside className="discovery-order-panel"> <div className="discovery-page">
<div className="discovery-price-row"> <SharedNavbar variant="light" active="testen" />
<span>Preis</span>
<strong>CHF 48.</strong>
</div>
<div className="discovery-panel-facts"> <main className="discovery-shell">
{discoveryPanelFacts.map((item) => ( <div className="discovery-topbar">
<div key={item.label}> <button className="discovery-back-link" type="button" onClick={() => navigate("/")}>
<span>{item.label}</span> <span className="discovery-back-arrow"></span>
<p>{item.value}</p> <span>Zurück zur Startseite</span>
</div>
))}
</div>
<div className="discovery-panel-actions">
<button type="button" className="discovery-primary-btn" onClick={onBuy}>
Discovery Set bestellen CHF 48.
</button> </button>
<p>Nur das erste Set erstellt einen einmaligen CHF 48 Full-Size-Rabatt.</p>
</div> </div>
</aside>
);
}
function DiscoveryHero({ onBuy }) { <section className="discovery-hero" data-reveal-group>
return (
<section className="discovery-hero">
<div className="discovery-hero-stage">
<div className="discovery-hero-copy"> <div className="discovery-hero-copy">
<span className="discovery-kicker">Der Einstieg</span> <span className="discovery-kicker" data-reveal="fade">
<h1>Discovery Set</h1> DISCOVERY SET
</span>
<h1 data-reveal="lines">
DER SICHERSTE EINSTIEG
<br />
IN DIE WELT DER NISCHEN-
<br />
DÜFTE
</h1>
<p className="discovery-intro"> <p className="discovery-intro" data-reveal="fade">
6 Düfte × 2ml. Jeden Duft eine Woche tragen. Verstehen, was 6 Düfte × 2ml. Jeden Duft eine Woche tragen. Verstehen, was
wirklich funktioniert. Ohne Risiko. Der sichere Einstieg in die wirklich funktioniert. Ohne Risiko.
Welt der Nischendüfte, bevor du dich für eine Full Size entscheidest.
</p> </p>
</div>
<figure className="discovery-hero-visual"> <div className="discovery-benefits" data-reveal="fade">
<img <div className="discovery-benefit">
src={DISCOVERY_SET_IMAGE} <span className="discovery-benefit-icon"></span>
alt="Atmos Discovery Set"
loading="eager"
decoding="async"
/>
</figure>
</div>
<DiscoveryOrderPanel onBuy={onBuy} />
</section>
);
}
function DiscoveryStorySection() {
return (
<section className="discovery-story-grid" data-reveal-group>
<div className="discovery-story-copy">
<span className="discovery-label" data-reveal="fade">
Warum Discovery Set
</span>
<h2 data-reveal="lines">Der klügere Einstieg in Nischendüfte.</h2>
<p data-reveal="fade">
Nischen-Parfums sind keine Impulskäufe. Sie brauchen Zeit, um zu
verstehen, wie sie auf deiner Haut funktionieren, wie sie sich im
Alltag entwickeln und ob sie wirklich zu dir passen.
</p>
</div>
<div className="discovery-benefit-panel" data-reveal="fade">
<div className="discovery-benefit-panel-head">
<span>Testlogik</span>
<strong>6 × 2ml</strong>
</div>
{discoveryBenefits.map((benefit) => (
<article className="discovery-benefit" key={benefit.title}>
<span className="discovery-benefit-icon" aria-hidden="true">
</span>
<div> <div>
<strong>{benefit.title}</strong> <strong>6 × 2ml Samples aller Signature-Düfte</strong>
<p>{benefit.text}</p> <p>
Kalter Beton, Schwarzes Benzin, Verbranntes Chrom, Blasse
Seide, Weisse Asche und Nasser Marmor.
</p>
</div>
</div>
<div className="discovery-benefit">
<span className="discovery-benefit-icon"></span>
<div>
<strong>CHF 48 Gutschein automatisch im Set</strong>
<p>
Nur das erste Discovery Set erstellt den einmaligen Rabatt.
Er wird bei einem späteren Full-Size-Kauf automatisch angerechnet.
</p>
</div>
</div>
<div className="discovery-benefit">
<span className="discovery-benefit-icon"></span>
<div>
<strong>Jedes Sample = ca. 20 Anwendungen</strong>
<p>
Genug, um jeden Duft mehrere Tage im Alltag und in
unterschiedlichen Situationen zu testen.
</p>
</div>
</div>
<div className="discovery-benefit">
<span className="discovery-benefit-icon"></span>
<div>
<strong>Hochwertige Mini-Flacons</strong>
<p>
Sorgfältig zusammengestellt, reduziert gestaltet und als
Teil des atmos Konzepts gedacht.
</p>
</div>
</div>
</div>
<div className="discovery-hero-actions" data-reveal="fade">
<button type="button" className="discovery-primary-btn" onClick={buyDiscoverySet}>
DISCOVERY SET BESTELLEN CHF 48.
</button>
<p>Nur das erste Set erstellt einen einmaligen CHF 48 Full-Size-Rabatt</p>
</div>
</div>
<div className="discovery-mood-grid">
{moodImages.map((image, index) => (
<div className="discovery-mood-tile" key={index}>
<img src={image} alt={`Discovery Mood ${index + 1}`} />
</div> </div>
</article>
))} ))}
</div> </div>
</section> </section>
);
}
function DiscoveryProcessSection() {
return (
<section className="discovery-process-section" data-reveal-group>
<div className="discovery-section-intro">
<span className="discovery-label" data-reveal="fade">
Ablauf
</span>
<h2 data-reveal="lines">So funktioniert&apos;s</h2>
</div>
<div className="discovery-steps-grid">
{discoverySteps.map((step) => (
<article className="discovery-step-card" key={step.number} data-reveal="fade">
<span className="discovery-step-number">{step.number}</span>
<h3>{step.title}</h3>
<p>{step.text}</p>
</article>
))}
</div>
</section>
);
}
function DiscoveryIncludedSection() {
return (
<section className="discovery-included" data-reveal-group> <section className="discovery-included" data-reveal-group>
<div className="discovery-section-heading"> <div className="discovery-section-heading">
<span className="discovery-label" data-reveal="fade"> <span className="discovery-label" data-reveal="fade">
Im Set enthalten IM SET ENTHALTEN
</span> </span>
<h2 data-reveal="lines">Alle 6 Signature-Düfte zum Testen.</h2> <h2 data-reveal="lines">ALLE 6 SIGNATURE-DÜFTE ZUM TESTEN.</h2>
</div> </div>
<div className="discovery-products-grid"> <div className="discovery-products-grid">
{perfumes.map((perfume) => ( {perfumes.map((perfume) => (
<article className="discovery-product-card" key={perfume.id} data-reveal="fade"> <article className="discovery-product-card" key={perfume.id}>
<span className="discovery-product-index">{perfume.id}</span>
<div className="discovery-product-image"> <div className="discovery-product-image">
<img <img src={perfume.image} alt={perfume.name} />
src={perfume.image}
alt={perfume.name}
loading="lazy"
decoding="async"
/>
</div> </div>
<div className="discovery-product-copy"> <div className="discovery-product-copy">
@ -217,81 +141,91 @@ function DiscoveryIncludedSection() {
))} ))}
</div> </div>
</section> </section>
);
}
function DiscoveryComparisonSection() { <section className="discovery-steps-section">
return ( <div className="discovery-steps-shell" data-reveal-group>
<section className="discovery-comparison-section" data-reveal-group> <h2 data-reveal="lines">So funktioniert&apos;s</h2>
<div className="discovery-comparison-grid">
{discoveryComparison.map((item) => ( <div className="discovery-steps-grid">
<article <article className="discovery-step-card" data-reveal="fade">
className={`discovery-comparison-card${ <div className="discovery-step-number">1</div>
item.highlight ? " discovery-comparison-card--highlight" : "" <h3>Bestellen</h3>
}`} <p>
key={item.title} Discovery Set für CHF 48 bestellen. Nur dein erstes Set erzeugt
data-reveal="fade" automatisch einen einmaligen Rabatt.
> </p>
<div className="discovery-comparison-head">
<span className="discovery-comparison-icon" aria-hidden="true">
{item.icon}
</span>
<h3>{item.title}</h3>
</div>
<p>{item.text}</p>
</article> </article>
))}
<article className="discovery-step-card" data-reveal="fade">
<div className="discovery-step-number">2</div>
<h3>Testen</h3>
<p>
Jeden Duft mindestens einige Tage tragen. Im Alltag, zu
verschiedenen Anlässen und auf der eigenen Haut.
</p>
</article>
<article className="discovery-step-card" data-reveal="fade">
<div className="discovery-step-number">3</div>
<h3>Entscheiden</h3>
<p>
Full-Size bestellen. CHF 48 werden automatisch angerechnet,
sofern der Rabatt noch nicht genutzt wurde.
</p>
</article>
</div>
</div> </div>
</section> </section>
);
}
function DiscoveryFinalCta({ onBuy }) { <section className="discovery-comparison-section" data-reveal-group>
return ( <div className="discovery-section-heading discovery-section-heading--center">
<section className="discovery-final-cta" data-reveal-group>
<div>
<span className="discovery-label" data-reveal="fade"> <span className="discovery-label" data-reveal="fade">
Discovery Set WARUM DISCOVERY SET
</span> </span>
<h2 data-reveal="lines">Der sichere Einstieg.</h2> <h2 data-reveal="lines">DER KLÜGERE EINSTIEG IN NISCHENDÜFTE.</h2>
<p data-reveal="fade"> <p data-reveal="fade">
Kostenloser Versand · 23 Werktage · Einmalige Anrechnung bei Full-Size Nischen-Parfums sind keine Impulskäufe. Sie brauchen Zeit, um zu
verstehen, wie sie auf deiner Haut funktionieren, wie sie sich im
Alltag entwickeln und ob sie wirklich zu dir passen.
</p> </p>
</div> </div>
<div className="discovery-final-actions" data-reveal="fade"> <div className="discovery-comparison-grid">
<button type="button" className="discovery-primary-btn" onClick={onBuy}> <div className="discovery-comparison-card" data-reveal="fade">
Discovery Set bestellen CHF 48. <div className="discovery-comparison-head">
<span className="discovery-comparison-icon">×</span>
<h3>Traditioneller Weg</h3>
</div>
<p>
CHF 180+ für eine Full Size ausgeben, ohne zu wissen, ob sie
wirklich passt. Risiko: Fehlkauf, Überforderung oder ein Duft,
der im Regal bleibt.
</p>
</div>
<div
className="discovery-comparison-card discovery-comparison-card--highlight"
data-reveal="fade"
>
<div className="discovery-comparison-head">
<span className="discovery-comparison-icon"></span>
<h3>Discovery Set Weg</h3>
</div>
<p>
CHF 48 investieren, alle Düfte testen, bewusst entscheiden. Die
erste Investition wird einmalig angerechnet der Einstieg bleibt
kontrolliert, nachvollziehbar und fair.
</p>
</div>
</div>
<div className="discovery-bottom-cta" data-reveal="fade">
<button type="button" className="discovery-primary-btn" onClick={buyDiscoverySet}>
DISCOVERY SET BESTELLEN CHF 48.
</button> </button>
<p>Kostenloser Versand · 23 Werktage · Einmalige Anrechnung bei Full-Size</p>
</div> </div>
</section> </section>
);
}
function DiscoverySetPage() {
const navigate = useNavigate();
const { addToCart } = useShop();
const buyDiscoverySet = () =>
addToCart("discovery-set", 1, "Discovery Set added.").catch(() => {});
return (
<div className="discovery-page">
<SharedNavbar variant="hero" active="testen" />
<main className="shell">
<div className="discovery-topbar">
<button className="discovery-back-link" type="button" onClick={() => navigate("/")}>
<span className="discovery-back-arrow" aria-hidden="true" />
<span>Zurück zur Startseite</span>
</button>
</div>
<DiscoveryHero onBuy={buyDiscoverySet} />
<DiscoveryStorySection />
<DiscoveryIncludedSection />
<DiscoveryProcessSection />
<DiscoveryComparisonSection />
<DiscoveryFinalCta onBuy={buyDiscoverySet} />
</main> </main>
</div> </div>
); );

View File

@ -1,142 +1,152 @@
.impressum-page { .impressum-page {
min-height: 100vh; min-height: 100vh;
padding: 0 0 var(--section-y-sm);
color: var(--theme-text); color: var(--theme-text);
padding: 26px 38px 38px;
background: background:
radial-gradient(circle at 86% 8%, rgba(var(--theme-accent-rgb) / 0.12), transparent 28rem), linear-gradient(to right, rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0.1)),
linear-gradient(180deg, var(--theme-bg), color-mix(in srgb, var(--theme-bg) 88%, #000 12%)); linear-gradient(to bottom, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.45));
}
.impressum-shell {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
padding: 38px;
} }
.impressum-kicker, .impressum-kicker,
.impressum-label { .impressum-label {
display: block; display: block;
color: var(--theme-text-muted); font-size: 10px;
font-size: var(--text-xs);
letter-spacing: 0.22em; letter-spacing: 0.22em;
text-transform: uppercase; color: var(--theme-text-muted);
} }
.impressum-hero { .impressum-hero {
padding: clamp(2rem, 5vw, 5rem) 0 var(--section-y-sm); padding-bottom: 32px;
border-bottom: 1px solid var(--theme-border); border-bottom: 1px solid var(--theme-border);
} }
.impressum-hero h1 { .impressum-hero h1 {
margin: clamp(0.85rem, 2vw, 1.2rem) 0 clamp(1rem, 2vw, 1.35rem); margin: 14px 0 16px;
color: var(--theme-text); font-size: 64px;
font-size: clamp(3rem, 8vw, 7.2rem); line-height: 0.92;
line-height: 0.9;
font-weight: 300; font-weight: 300;
letter-spacing: 0; letter-spacing: -0.05em;
text-transform: uppercase; color: var(--theme-text);
} }
.impressum-intro { .impressum-intro {
max-width: var(--text-measure); max-width: 760px;
margin: 0; margin: 0;
color: var(--theme-text-muted); font-size: 17px;
font-size: var(--text-lg);
line-height: 1.65; line-height: 1.65;
color: var(--theme-text-muted);
} }
.impressum-grid { .impressum-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, 1fr);
gap: var(--gap-sm); gap: 18px;
margin-top: var(--section-y-sm); margin-top: 38px;
}
.impressum-card,
.impressum-note-box {
border: 1px solid var(--theme-border);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.008)),
var(--theme-surface-soft);
} }
.impressum-card { .impressum-card {
padding: 24px;
background: var(--theme-bg);
border: 1px solid var(--theme-border);
min-height: 210px; min-height: 210px;
padding: clamp(1.1rem, 2.4vw, 1.8rem);
transition:
transform var(--duration-med) var(--ease-out),
border-color var(--duration-med) var(--ease-out),
box-shadow var(--duration-med) var(--ease-out);
}
.impressum-card:hover {
transform: translateY(-4px);
border-color: rgba(var(--theme-accent-rgb) / 0.42);
box-shadow: var(--theme-shadow-soft);
} }
.impressum-card h2 { .impressum-card h2 {
margin: 0.9rem 0 0.75rem; margin: 14px 0 12px;
color: var(--theme-text); font-size: 26px;
font-size: var(--text-xl); line-height: 1.05;
line-height: 1.08;
font-weight: 400; font-weight: 400;
letter-spacing: 0; color: var(--theme-text);
} }
.impressum-card p { .impressum-card p {
margin: 0; margin: 0;
color: var(--theme-text-muted); font-size: 15px;
font-size: var(--text-base);
line-height: 1.7; line-height: 1.7;
color: var(--theme-text-muted);
} }
.impressum-section { .impressum-section {
display: grid; display: grid;
grid-template-columns: minmax(14rem, 0.72fr) minmax(0, 1.28fr); grid-template-columns: minmax(260px, 0.7fr) minmax(0, 1.3fr);
gap: var(--gap-lg); gap: 28px;
align-items: start; align-items: start;
margin-top: var(--section-y-sm); margin-top: 38px;
padding-top: var(--section-y-sm); padding-top: 38px;
border-top: 1px solid var(--theme-border); border-top: 1px solid var(--theme-border);
} }
.impressum-section-heading h2 { .impressum-section-heading h2 {
margin: 0.75rem 0 0; margin: 10px 0 0;
color: var(--theme-text); font-size: 38px;
font-size: clamp(2rem, 4.2vw, 4.5rem); line-height: 0.98;
line-height: 0.96;
font-weight: 300; font-weight: 300;
letter-spacing: 0; letter-spacing: -0.04em;
text-transform: uppercase; color: var(--theme-text);
text-wrap: balance;
} }
.impressum-section-copy p { .impressum-section-copy p {
margin: 0 0 1rem; margin: 0 0 16px;
color: var(--theme-text-muted); font-size: 16px;
font-size: var(--text-base);
line-height: 1.75; line-height: 1.75;
color: var(--theme-text-muted);
} }
.impressum-note-box { .impressum-note-box {
padding: clamp(1.1rem, 2.4vw, 1.8rem); padding: 22px;
border-color: rgba(var(--theme-accent-rgb) / 0.24); border: 1px solid rgba(255, 106, 0, 0.18);
background: background: linear-gradient(
linear-gradient(135deg, rgba(var(--theme-accent-rgb) / 0.11), transparent 60%), 180deg,
var(--theme-surface-soft); rgba(255, 106, 0, 0.08),
rgba(255, 106, 0, 0.03)
);
} }
.impressum-note-box p { .impressum-note-box p {
margin: 0; margin: 0;
color: var(--theme-text); font-size: 15px;
font-size: var(--text-base);
line-height: 1.65; line-height: 1.65;
color: var(--theme-text);
} }
@media (max-width: 900px) { @media (max-width: 1100px) {
.impressum-grid, .impressum-grid,
.impressum-section { .impressum-section {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.impressum-hero h1 {
font-size: 48px;
}
} }
@media (max-width: 700px) { @media (max-width: 700px) {
.impressum-hero { .impressum-page {
padding-top: clamp(1.4rem, 5vw, 2rem); padding: 18px;
}
.impressum-shell {
padding: 24px 18px;
}
.impressum-hero h1 {
font-size: 36px;
}
.impressum-section-heading h2 {
font-size: 28px;
}
.impressum-intro,
.impressum-card p,
.impressum-section-copy p {
font-size: 15px;
} }
} }

View File

@ -4,9 +4,9 @@ import "./ImpressumPage.css";
function ImpressumPage() { function ImpressumPage() {
return ( return (
<div className="impressum-page"> <div className="impressum-page">
<SharedNavbar variant="hero" /> <SharedNavbar variant="light" />
<main className="shell"> <main className="impressum-shell">
<section className="impressum-hero" data-reveal-group> <section className="impressum-hero" data-reveal-group>
<span className="impressum-kicker" data-reveal="fade"> <span className="impressum-kicker" data-reveal="fade">
RECHTLICHE ANGABEN RECHTLICHE ANGABEN

View File

@ -1,9 +1,6 @@
.page { .page {
position: relative;
min-height: 100vh; min-height: 100vh;
background: background: var(--theme-bg);
radial-gradient(circle at 82% 12%, rgba(var(--theme-accent-rgb) / 0.13), transparent 28rem),
linear-gradient(180deg, var(--theme-bg), color-mix(in srgb, var(--theme-bg) 88%, #000 12%));
color: var(--theme-text); color: var(--theme-text);
} }
@ -19,37 +16,20 @@
border: 0; border: 0;
} }
.page main {
padding-bottom: var(--section-y-sm);
}
/* HERO */ /* HERO */
.hero { .hero {
position: relative; position: relative;
width: 100%; width: 100%;
min-height: clamp(680px, 100svh, 980px); min-height: 100vh;
min-height: 100svh;
min-height: 100dvh;
overflow: hidden; overflow: hidden;
display: grid; display: flex;
align-items: center; align-items: center;
isolation: isolate; isolation: isolate;
background: #111; background: #111;
} }
.hero::before {
content: "";
position: absolute;
inset: 0;
z-index: 2;
pointer-events: none;
}
.hero::before {
background:
radial-gradient(circle at 72% 42%, rgba(255, 255, 255, 0.1), transparent 24rem),
linear-gradient(90deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.42) 44%, rgba(0, 0, 0, 0.08) 100%),
linear-gradient(0deg, rgba(0, 0, 0, 0.44), transparent 46%);
}
.hero-media { .hero-media {
position: absolute; position: absolute;
inset: 0; inset: 0;
@ -62,50 +42,44 @@
height: 100%; height: 100%;
display: block; display: block;
object-fit: cover; object-fit: cover;
object-position: 60% center; object-position: center;
will-change: transform; will-change: transform;
} }
.hero .navbar--hero { .hero .navbar--hero {
position: fixed; position: absolute;
top: clamp(0.75rem, 2.1vw, 1.4rem); top: 22px;
right: 0;
left: 0; left: 0;
z-index: 998; right: 0;
z-index: 12;
padding-top: 0; padding-top: 0;
} }
.hero-content { .hero-content {
position: relative; position: relative;
z-index: 6; z-index: 6;
width: var(--container-wide); width: min(760px, 100%);
margin: 0 auto; padding: clamp(6rem, 11vh, 9rem) clamp(1.2rem, 3.4vw, 3rem)
padding: clamp(7rem, 14vh, 11rem) 0 clamp(3rem, 8vh, 6rem); clamp(2.6rem, 7vh, 4rem);
display: grid; display: flex;
grid-template-columns: repeat(12, minmax(0, 1fr)); flex-direction: column;
gap: var(--gap-md); justify-content: center;
align-items: center;
} }
.hero-title { .hero-title {
grid-column: 1 / span 7;
max-width: 10.8ch;
margin: 0; margin: 0;
font-size: clamp(3.2rem, 8.4vw, 8.8rem); font-size: clamp(2.8rem, 8.5vw, 6.4rem);
line-height: 0.9; line-height: 0.88;
font-weight: 300; font-weight: 300;
letter-spacing: 0; letter-spacing: -0.045em;
text-transform: uppercase; text-transform: uppercase;
color: #fff; color: #fff;
text-wrap: balance;
} }
.hero-title-line { .hero-title-line {
display: block; display: block;
overflow: hidden; overflow: hidden;
padding-right: 0.12em;
padding-bottom: 0.08em; padding-bottom: 0.08em;
margin-right: -0.12em;
margin-bottom: -0.08em; margin-bottom: -0.08em;
} }
@ -114,75 +88,63 @@
} }
.hero-title-line + .hero-title-line { .hero-title-line + .hero-title-line {
margin-top: 0.02em; margin-top: 0.1em;
} }
.hero-text { .hero-text {
grid-column: 1 / span 5; margin-top: 1.25rem;
max-width: 31rem; max-width: 29rem;
margin: 0; font-size: 0.99rem;
font-size: var(--text-base); line-height: 1.58;
line-height: 1.62; color: rgba(255, 255, 255, 0.86);
color: rgba(255, 255, 255, 0.84);
will-change: transform, opacity; will-change: transform, opacity;
} }
.hero-actions { .hero-actions {
grid-column: 1 / span 5;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: var(--gap-xs); gap: 12px;
margin-top: clamp(0.2rem, 1vw, 0.7rem); margin-top: 1.9rem;
will-change: transform, opacity; will-change: transform, opacity;
} }
.btn, .btn {
.discovery-btn {
min-height: 48px;
border: none; border: none;
border-radius: 999px; border-radius: 999px;
padding: 0 clamp(1rem, 2vw, 1.35rem); padding: 12px 20px;
font-size: var(--text-sm); font-size: 0.9rem;
cursor: pointer; cursor: pointer;
transition: transition: transform 0.24s ease, opacity 0.24s ease;
transform var(--duration-med) var(--ease-out),
opacity var(--duration-med) var(--ease-out),
box-shadow var(--duration-med) var(--ease-out),
background-color var(--duration-med) var(--ease-out),
border-color var(--duration-med) var(--ease-out);
text-decoration: none; text-decoration: none;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.btn:hover, .hero .btn {
.discovery-btn:hover { border-radius: 999px;
transform: translateY(-2px);
} }
.btn:active, .btn:hover {
.discovery-btn:active { transform: translateY(-1px);
transform: translateY(0) scale(0.98);
} }
.btn-primary { .btn-primary {
background: var(--theme-accent); background: #ff6a00;
color: #fff; color: #fff;
box-shadow: 0 18px 42px rgba(var(--theme-accent-rgb) / 0.26);
} }
.btn-secondary { .btn-secondary {
background: rgba(255, 255, 255, 0.13); background: rgba(255, 255, 255, 0.16);
color: #fff; color: #fff;
border: 1px solid rgba(255, 255, 255, 0.24); border: 1px solid rgba(255, 255, 255, 0.22);
backdrop-filter: blur(12px); backdrop-filter: blur(8px);
} }
.intro-overlay { .intro-overlay {
position: absolute; position: absolute;
inset: 0; inset: 0;
z-index: 999; z-index: 26;
background: var(--theme-paper); background: var(--theme-paper);
display: grid; display: grid;
place-items: center; place-items: center;
@ -194,7 +156,7 @@
height: 100%; height: 100%;
display: grid; display: grid;
place-items: center; place-items: center;
padding: var(--page-x); padding: clamp(1rem, 4vw, 2.2rem);
} }
.intro-overlay__text-mask { .intro-overlay__text-mask {
@ -217,107 +179,51 @@
/* SECTIONS */ /* SECTIONS */
.section { .section {
width: var(--container-wide); padding: 42px 20px 10px;
margin: 0 auto;
padding: var(--section-y-sm) 0 var(--section-y-xs);
} }
.section-heading { .section-heading {
display: grid; margin-bottom: 28px;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: var(--gap-md);
align-items: end;
margin-bottom: clamp(1.6rem, 4vw, 4.8rem);
}
.section-heading::after {
content: "01 / Kollektion";
grid-column: 9 / span 3;
align-self: start;
padding-top: 0.3rem;
border-top: 1px solid var(--theme-border);
color: var(--theme-text-muted);
font-size: var(--text-xs);
line-height: 1.4;
letter-spacing: 0.16em;
text-transform: uppercase;
} }
.section-heading h2, .section-heading h2,
.discovery-copy h2 { .discovery-copy h2 {
margin: 0; margin: 0;
font-size: clamp(2.6rem, 7vw, 7.4rem); font-size: 52px;
line-height: 0.92; line-height: 0.95;
font-weight: 300; font-weight: 300;
letter-spacing: 0; letter-spacing: -0.04em;
color: var(--theme-text); color: var(--theme-text);
text-wrap: balance;
}
.section-heading h2 {
grid-column: 1 / span 8;
} }
/* GRID */ /* GRID */
.product-grid { .product-grid {
container-type: inline-size;
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, 1fr);
gap: var(--gap-sm); gap: 18px;
} }
.product-card { .product-card {
position: relative; position: relative;
container-type: inline-size;
isolation: isolate; isolation: isolate;
overflow: hidden; overflow: hidden;
min-height: clamp(360px, 36vw, 560px); background: var(--theme-surface);
display: grid;
grid-template-rows: auto minmax(14rem, 1fr) auto;
padding: clamp(1rem, 2vw, 1.55rem);
color: inherit;
text-decoration: none;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0)),
var(--theme-surface);
border: 1px solid var(--theme-border); border: 1px solid var(--theme-border);
border-radius: var(--radius-lg); border-radius: 0;
padding: 18px;
min-height: 360px;
display: flex;
flex-direction: column;
justify-content: space-between;
cursor: pointer; cursor: pointer;
transition: transition: transform 0.15s ease, border-color 0.15s ease;
transform var(--duration-med) var(--ease-out), text-decoration: none;
border-color var(--duration-med) var(--ease-out), color: inherit;
background-color var(--duration-med) var(--ease-out),
box-shadow var(--duration-med) var(--ease-out);
}
.product-card::before {
content: "";
position: absolute;
inset: 0;
z-index: 3;
pointer-events: none;
background:
linear-gradient(180deg, rgba(0, 0, 0, 0.18), transparent 28%),
linear-gradient(0deg, rgba(0, 0, 0, 0.22), transparent 42%);
opacity: 0;
transition: opacity var(--duration-med) var(--ease-out);
}
.product-card:hover,
.product-card:focus-visible {
transform: translateY(-4px);
border-color: rgba(var(--theme-accent-rgb) / 0.48);
box-shadow: var(--theme-shadow-soft);
}
.product-card:hover::before,
.product-card:focus-visible::before {
opacity: 1;
} }
.product-card:focus-visible { .product-card:focus-visible {
outline: 2px solid var(--theme-accent); outline: 2px solid #ff6a00;
outline-offset: 4px; outline-offset: 3px;
} }
.product-hover-fill { .product-hover-fill {
@ -353,14 +259,14 @@
z-index: 1; z-index: 1;
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.08),
rgba(0, 0, 0, 0.34) rgba(0, 0, 0, 0.18)
); );
} }
.product-card:active { .product-card:active {
transform: translateY(-1px) scale(0.985); transform: scale(0.97);
border-color: var(--theme-accent); border-color: #ff6a00;
} }
.product-top { .product-top {
@ -369,23 +275,20 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
gap: var(--gap-sm); gap: 12px;
} }
.product-id { .product-id {
font-size: var(--text-sm); font-size: 18px;
color: var(--theme-text-muted); color: var(--theme-text-muted);
} }
.product-top h3 { .product-top h3 {
max-width: 12ch;
margin: 0; margin: 0;
font-size: var(--text-sm); font-size: 18px;
line-height: 1.15;
font-weight: 400; font-weight: 400;
text-align: right; text-align: right;
letter-spacing: 0.02em; letter-spacing: 0.02em;
text-transform: uppercase;
} }
.product-image-wrap { .product-image-wrap {
@ -394,35 +297,25 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-height: 180px;
padding: 20px 0;
width: 100%; width: 100%;
min-height: 0;
padding: clamp(1.2rem, 5vw, 4.8rem) 0;
overflow: hidden; overflow: hidden;
} }
.product-image-wrap::before {
content: "";
position: absolute;
width: min(76%, 26rem);
aspect-ratio: 1;
border-radius: 50%;
background: rgba(var(--theme-accent-rgb) / 0.08);
filter: blur(28px);
transform: translateY(10%);
}
.product-image { .product-image {
position: relative; position: relative;
z-index: 1; z-index: 1;
width: min(92%, 520px); width: 100%;
max-width: 600px;
height: auto; height: auto;
object-fit: contain; object-fit: contain;
border-radius: 0; border-radius: 0;
transition: transform var(--duration-slow) var(--ease-out); transition: transform 0.4s ease;
} }
.product-card:hover .product-image { .product-card:hover .product-image {
transform: scale(1.045) rotate(-0.6deg); transform: scale(1.05);
} }
.product-bottom { .product-bottom {
@ -431,47 +324,28 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-end; align-items: flex-end;
gap: var(--gap-sm); gap: 12px;
} }
.product-bottom p { .product-bottom p {
max-width: 18rem;
margin: 0; margin: 0;
max-width: 170px;
font-size: 15px;
line-height: 1.35;
color: var(--theme-text-muted); color: var(--theme-text-muted);
font-size: var(--text-sm);
line-height: 1.45;
} }
.arrow { .arrow {
display: inline-flex; font-size: 26px;
align-items: center; color: #ff6a00;
justify-content: center; line-height: 1;
min-width: 2rem;
width: 2rem;
height: 2rem;
color: var(--theme-accent);
font-size: 0;
line-height: 0;
}
.arrow::before {
content: "";
display: block;
width: clamp(1.35rem, 2.2vw, 1.75rem);
height: clamp(1.35rem, 2.2vw, 1.75rem);
background: currentColor;
transform: rotate(180deg);
-webkit-mask: url("/icon-arrow-left.svg") center / contain no-repeat;
mask: url("/icon-arrow-left.svg") center / contain no-repeat;
} }
.product-id, .product-id,
.product-top h3, .product-top h3,
.product-bottom p, .product-bottom p,
.arrow { .arrow {
transition: transition: color 0.25s ease;
color var(--duration-med) var(--ease-out),
transform var(--duration-med) var(--ease-out);
} }
.product-card:hover .product-id, .product-card:hover .product-id,
@ -483,232 +357,156 @@
.product-card:focus-within .product-bottom p, .product-card:focus-within .product-bottom p,
.product-card:focus-within .arrow { .product-card:focus-within .arrow {
color: #fff; color: #fff;
mix-blend-mode: difference;
} }
.product-card:hover .arrow, .product-card:active .product-id,
.product-card:focus-within .arrow { .product-card:active .product-top h3,
transform: translateX(0.35rem); .product-card:active .product-bottom p,
.product-card:active .arrow {
color: #ff6a00;
mix-blend-mode: normal;
transform: scale(1.02);
transition: all 0.1s ease;
} }
/* DISCOVERY */ /* DISCOVERY */
.discovery-section { .discovery-section {
position: relative;
width: var(--container-wide);
min-height: clamp(520px, 62vw, 780px);
margin: var(--section-y-sm) auto 0;
padding: clamp(1.25rem, 4vw, 4rem);
display: grid; display: grid;
grid-template-columns: minmax(17rem, 0.85fr) minmax(0, 1.35fr); grid-template-columns: 600px 1fr;
gap: var(--gap-lg); gap: 28px;
align-items: center; align-items: center;
overflow: hidden; background: #ff6a00;
background: margin: 5vw 0px 0;
radial-gradient(circle at 14% 18%, rgba(255, 255, 255, 0.22), transparent 16rem), border-radius: 0;
linear-gradient(135deg, #ff6a00, #d84f00); padding: 00px 0px 0px 40px;
border-radius: var(--radius-lg);
}
.discovery-section::after {
content: "";
position: absolute;
inset: 1px;
border: 1px solid rgba(255, 255, 255, 0.26);
border-radius: inherit;
pointer-events: none;
}
.discovery-copy {
position: relative;
z-index: 2;
max-width: 36rem;
} }
.discovery-copy h2 { .discovery-copy h2 {
margin: 0;
font-size: 42px;
line-height: 0.95;
font-weight: 300;
letter-spacing: -0.04em;
color: #fff; color: #fff;
font-size: clamp(2.2rem, 5.8vw, 6rem);
} }
.discovery-copy p { .discovery-copy p {
max-width: 29rem; margin-top: 18px;
margin: clamp(1rem, 2vw, 1.4rem) 0 0; font-size: 15px;
color: rgba(255, 255, 255, 0.86); line-height: 1.5;
font-size: var(--text-base); color: #fff;
line-height: 1.62;
} }
.discovery-btn { .discovery-btn {
margin-top: clamp(1.3rem, 3vw, 2.1rem); border: none;
background: #fff; border-radius: 999px;
color: #d64f00; padding: 12px 18px;
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.18); font-size: 14px;
cursor: pointer;
transition: transform 0.2s ease, opacity 0.2s ease;
background: var(--theme-paper);
color: #ff6a00;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
}
.discovery-btn:hover {
transform: translateY(-1px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.2);
}
.discovery-btn:active {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
background: rgba(255, 255, 255, 0.8);
} }
.discovery-banner { .discovery-banner {
position: relative; position: relative;
z-index: 1; width: 100%;
width: min(100%, 1080px); max-width: 1300px;
aspect-ratio: 16 / 10; height: 50vh;
min-height: 320px; border-radius: 0;
justify-self: end;
overflow: hidden; overflow: hidden;
border-radius: var(--radius-lg); justify-self: end;
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.24);
}
.discovery-banner::before {
content: "";
position: absolute;
inset: 0;
z-index: 1;
border: 1px solid rgba(255, 255, 255, 0.24);
border-radius: inherit;
pointer-events: none;
} }
.discovery-banner img { .discovery-banner img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
object-position: center;
display: block; display: block;
will-change: transform; will-change: transform;
} }
/* RESPONSIVE */ /* RESPONSIVE */
@media (max-width: 1180px) { @media (max-width: 900px) {
.hero-content { .hero-content {
grid-template-columns: repeat(8, minmax(0, 1fr)); width: min(640px, 100%);
} padding-top: 7rem;
.hero-title {
grid-column: 1 / span 6;
}
.hero-text,
.hero-actions {
grid-column: 1 / span 4;
}
.section-heading {
grid-template-columns: 1fr;
} }
.hero-title,
.section-heading h2, .section-heading h2,
.section-heading::after { .discovery-copy h2 {
grid-column: 1; font-size: clamp(2.45rem, 9vw, 3.2rem);
} }
.section-heading::after { .hero-text {
max-width: 18rem; font-size: 0.94rem;
} }
.product-grid { .product-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, 1fr);
} }
.discovery-section { .discovery-section {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.discovery-banner {
justify-self: stretch;
}
} }
@media (max-width: 760px) { @media (max-width: 640px) {
.hero { .hero .navbar--hero {
min-height: clamp(620px, 91svh, 760px); top: 14px;
}
.hero::before {
background:
linear-gradient(90deg, rgba(0, 0, 0, 0.74), rgba(0, 0, 0, 0.28)),
linear-gradient(0deg, rgba(0, 0, 0, 0.62), transparent 48%);
}
.hero-media__image {
object-position: 64% center;
} }
.hero-content { .hero-content {
display: flex; padding: 6.2rem 1rem 2.3rem;
flex-direction: column;
align-items: flex-start;
justify-content: flex-end;
gap: var(--gap-sm);
padding-top: clamp(6.5rem, 18svh, 8.5rem);
padding-bottom: clamp(2.4rem, 8svh, 4rem);
} }
.hero-title { .hero-title,
max-width: 9.6ch; .section-heading h2,
font-size: clamp(3rem, 17.5vw, 5.1rem); .discovery-copy h2 {
} font-size: clamp(2.05rem, 13vw, 2.7rem);
.hero-text {
max-width: 25rem;
font-size: var(--text-sm);
} }
.hero-actions { .hero-actions {
width: min(100%, 22rem); flex-direction: column;
align-items: flex-start;
width: min(300px, 100%);
} }
.hero-actions .btn { .hero-actions .btn {
width: 100%; width: 100%;
} }
.section {
padding: 34px 12px 10px;
}
.product-grid { .product-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.product-card { .product-card {
min-height: clamp(340px, 118vw, 520px); min-height: 320px;
}
.product-top h3 {
max-width: 11ch;
} }
.discovery-section { .discovery-section {
width: calc(100% - (var(--page-x) * 2)); margin: 12px 12px 0;
min-height: 0; padding: 28px 20px;
padding: clamp(1rem, 5vw, 1.5rem);
}
.discovery-banner {
min-height: 0;
aspect-ratio: 1 / 1;
}
}
@media (max-width: 430px) {
.hero-title {
max-width: 9.2ch;
}
.section-heading h2,
.discovery-copy h2 {
font-size: clamp(2.25rem, 13vw, 3.45rem);
}
.product-card {
padding: 1rem;
}
.product-bottom {
align-items: flex-start;
}
}
@container (max-width: 360px) {
.product-bottom {
flex-direction: column;
align-items: flex-start;
}
.arrow {
align-self: flex-end;
} }
} }
@ -718,12 +516,9 @@
.hero-text, .hero-text,
.hero-actions, .hero-actions,
.hero-brand, .hero-brand,
.intro-overlay, .intro-overlay {
.product-card, transition: none !important;
.product-image, animation: none !important;
.discovery-banner img {
transition: none;
animation: none;
will-change: auto;
} }
} }

View File

@ -9,8 +9,6 @@ import { Link } from "react-router";
import { gsap } from "gsap"; import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger"; import { ScrollTrigger } from "gsap/ScrollTrigger";
import HeroSection from "../components/landing/HeroSection"; import HeroSection from "../components/landing/HeroSection";
import SharedNavbar from "../components/SharedNavbar";
import { useProductTransition } from "../transitions/ProductTransitionContext";
import perfumes from "../data/perfumes"; import perfumes from "../data/perfumes";
import "../pages/LandingPage.css"; import "../pages/LandingPage.css";
import "../style/navbar.css"; import "../style/navbar.css";
@ -29,7 +27,6 @@ function LandingPage() {
const headlineLineRefs = useRef([]); const headlineLineRefs = useRef([]);
const heroMetaRefs = useRef([]); const heroMetaRefs = useRef([]);
const cardRefs = useRef([]); const cardRefs = useRef([]);
const { startProductTransition } = useProductTransition();
const [introSettings] = useState(() => { const [introSettings] = useState(() => {
if (typeof window === "undefined") { if (typeof window === "undefined") {
@ -378,8 +375,6 @@ function LandingPage() {
return ( return (
<div className="page" ref={pageRef}> <div className="page" ref={pageRef}>
<SharedNavbar variant="hero" active="atmos" />
<HeroSection <HeroSection
heroImageWrapRef={heroImageWrapRef} heroImageWrapRef={heroImageWrapRef}
heroImageRef={heroImageRef} heroImageRef={heroImageRef}
@ -407,7 +402,6 @@ function LandingPage() {
to={`/duft/${item.slug}`} to={`/duft/${item.slug}`}
className="product-card" className="product-card"
key={item.id} key={item.id}
onClick={(event) => startProductTransition(event, item)}
ref={(element) => { ref={(element) => {
cardRefs.current[index] = element; cardRefs.current[index] = element;
}} }}
@ -438,19 +432,12 @@ function LandingPage() {
</div> </div>
<div className="product-image-wrap"> <div className="product-image-wrap">
<img <img src={item.image} alt={item.name} className="product-image" />
src={item.image}
alt={item.name}
className="product-image"
data-product-transition-source
loading={index < 3 ? "eager" : "lazy"}
decoding="async"
/>
</div> </div>
<div className="product-bottom"> <div className="product-bottom">
<p>{item.text}</p> <p>{item.text}</p>
<span className="arrow" aria-hidden="true" /> <span className="arrow">&rarr;</span>
</div> </div>
</Link> </Link>
))} ))}
@ -486,7 +473,6 @@ function LandingPage() {
src="/atmos-discovery-set-thumbnail.png" src="/atmos-discovery-set-thumbnail.png"
alt="Discovery Set" alt="Discovery Set"
loading="lazy" loading="lazy"
decoding="async"
ref={discoveryImageRef} ref={discoveryImageRef}
/> />
</div> </div>

View File

@ -1,207 +1,127 @@
.small-page { .small-page {
min-height: 100vh; min-height: 100vh;
padding: 0 0 var(--section-y-sm); padding: 26px 38px 38px;
background: var(--theme-bg);
color: var(--theme-text); color: var(--theme-text);
background: }
radial-gradient(circle at 84% 8%, rgba(var(--theme-accent-rgb) / 0.13), transparent 28rem),
linear-gradient(180deg, var(--theme-bg), color-mix(in srgb, var(--theme-bg) 88%, #000 12%)); .small-shell {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
padding: 38px;
} }
.small-hero { .small-hero {
max-width: 64rem; max-width: 780px;
padding: clamp(2rem, 5vw, 5rem) 0 var(--section-y-sm); padding-bottom: 34px;
border-bottom: 1px solid var(--theme-border); border-bottom: 1px solid var(--theme-border);
margin-bottom: 28px;
} }
.small-kicker, .small-kicker {
.small-requirement span,
.release-card span {
display: block; display: block;
margin-bottom: 0.75rem; margin-bottom: 12px;
color: var(--theme-text-muted); color: var(--theme-text-muted);
font-size: var(--text-xs); font-size: 10px;
letter-spacing: 0.22em; letter-spacing: 0.22em;
text-transform: uppercase;
} }
.small-hero h1, .small-hero h1,
.small-panel h2 { .small-panel h2 {
margin: 0 0 clamp(0.85rem, 2vw, 1.2rem); margin: 0 0 14px;
color: var(--theme-text);
font-weight: 300;
letter-spacing: 0; letter-spacing: 0;
text-transform: uppercase;
} }
.small-hero h1 { .small-hero h1 {
font-size: clamp(3rem, 8.6vw, 9rem); font-size: clamp(42px, 8vw, 92px);
line-height: 0.88; line-height: 0.92;
text-wrap: balance;
}
.small-panel h2 {
font-size: clamp(2.2rem, 5vw, 5.4rem);
line-height: 0.94;
} }
.small-hero p, .small-hero p,
.small-panel p, .small-panel p,
.release-card p { .release-card p {
max-width: var(--text-measure);
margin: 0; margin: 0;
color: var(--theme-text-muted); color: var(--theme-text-muted);
font-size: var(--text-base); line-height: 1.55;
line-height: 1.65;
} }
.small-panel, .small-panel,
.release-card, .release-card,
.small-error { .small-error {
background: var(--theme-surface-soft);
border: 1px solid var(--theme-border); border: 1px solid var(--theme-border);
background: padding: 22px;
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.008)),
var(--theme-surface-soft);
}
.small-panel {
max-width: 68rem;
margin-top: var(--section-y-sm);
padding: clamp(1.25rem, 3vw, 2.2rem);
} }
.small-panel button, .small-panel button,
.release-card button { .release-card button {
display: inline-flex; min-height: 44px;
align-items: center; margin-top: 18px;
justify-content: center; border: 1px solid #1f1f1f;
min-height: 48px; border-radius: 0;
margin-top: 1.2rem; background: #1f1f1f;
padding: 0 1.1rem;
border: 1px solid #111;
border-radius: 999px;
background: #111;
color: #fff; color: #fff;
padding: 0 18px;
cursor: pointer; cursor: pointer;
font-size: var(--text-sm);
text-transform: uppercase;
letter-spacing: 0.1em;
transition:
transform var(--duration-med) var(--ease-out),
box-shadow var(--duration-med) var(--ease-out),
background-color var(--duration-med) var(--ease-out);
}
.small-panel button:hover,
.release-card button:hover,
.small-panel button:focus-visible,
.release-card button:focus-visible {
transform: translateY(-2px);
box-shadow: var(--theme-shadow-soft);
} }
.small-requirements { .small-requirements {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--gap-sm); gap: 12px;
margin-top: clamp(1.4rem, 3vw, 2.4rem); margin-top: 18px;
} }
.small-requirement { .small-requirement {
min-height: 7rem;
display: flex; display: flex;
flex-direction: column;
justify-content: space-between; justify-content: space-between;
gap: var(--gap-xs); gap: 12px;
padding: clamp(1rem, 2vw, 1.4rem); padding: 14px;
background: var(--theme-surface-soft);
border: 1px solid var(--theme-border); border: 1px solid var(--theme-border);
background: var(--theme-paper);
} }
.small-requirement span { .small-requirement span,
margin-bottom: 0; .release-card span {
} color: var(--theme-text-muted);
font-size: 10px;
.small-requirement strong { letter-spacing: 0.18em;
color: var(--theme-text); text-transform: uppercase;
font-size: var(--text-base);
font-weight: 400;
line-height: 1.3;
} }
.small-requirement strong.met { .small-requirement strong.met {
color: var(--theme-accent); color: #ff6a00;
}
.small-error {
max-width: 68rem;
margin: var(--gap-sm) 0 0;
padding: clamp(1rem, 2vw, 1.4rem);
border-color: rgba(var(--theme-accent-rgb) / 0.45);
color: var(--theme-text);
} }
.release-grid { .release-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--gap-sm); gap: 16px;
margin-top: var(--section-y-sm); margin-top: 22px;
}
.release-card {
min-height: 280px;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: var(--gap-sm);
padding: clamp(1.1rem, 2.4vw, 1.8rem);
transition:
transform var(--duration-med) var(--ease-out),
border-color var(--duration-med) var(--ease-out),
box-shadow var(--duration-med) var(--ease-out);
}
.release-card:hover {
transform: translateY(-4px);
border-color: rgba(var(--theme-accent-rgb) / 0.42);
box-shadow: var(--theme-shadow-soft);
}
.release-card span {
margin-bottom: 0;
} }
.release-card h3 { .release-card h3 {
margin: 0; margin: 10px 0 12px;
color: var(--theme-text);
font-size: var(--text-xl);
line-height: 1.08;
font-weight: 400;
letter-spacing: 0;
} }
@media (max-width: 1180px) { .small-error {
.small-requirements, margin: 16px 0 0;
.release-grid { border-color: #ff6a00;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
} }
@media (max-width: 760px) { @media (max-width: 760px) {
.small-hero { .small-page {
padding-top: clamp(1.4rem, 5vw, 2rem); padding: 18px;
} }
.small-hero h1 { .small-shell {
font-size: clamp(2.55rem, 13vw, 4.4rem); padding: 24px 18px;
} }
.small-requirements, .small-requirements,
.release-grid { .release-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
}
.small-panel button,
.release-card button {
width: 100%;
}
}

View File

@ -60,9 +60,9 @@ function SmallBatchPage() {
return ( return (
<div className="small-page"> <div className="small-page">
<SharedNavbar variant="hero" /> <SharedNavbar variant="light" />
<main className="shell"> <main className="small-shell">
<section className="small-hero" data-reveal-group> <section className="small-hero" data-reveal-group>
<span className="small-kicker" data-reveal="fade"> <span className="small-kicker" data-reveal="fade">
SMALL BATCH / ARCHIVE / PROTOTYPE SMALL BATCH / ARCHIVE / PROTOTYPE

View File

@ -1,10 +1,16 @@
.support-page { .support-page {
min-height: 100vh; min-height: 100vh;
padding: 0 0 var(--section-y-sm);
color: var(--theme-text); color: var(--theme-text);
padding: 26px 38px 38px;
background: background:
radial-gradient(circle at 86% 8%, rgba(var(--theme-accent-rgb) / 0.13), transparent 28rem), linear-gradient(to right, rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0.1)),
linear-gradient(180deg, var(--theme-bg), color-mix(in srgb, var(--theme-bg) 88%, #000 12%)); linear-gradient(to bottom, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.45));
}
.support-shell {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
padding: 38px;
} }
.support-kicker, .support-kicker,
@ -12,173 +18,165 @@
.support-panel-label, .support-panel-label,
.support-panel-meta span { .support-panel-meta span {
display: block; display: block;
color: var(--theme-text-muted); font-size: 10px;
font-size: var(--text-xs);
letter-spacing: 0.22em; letter-spacing: 0.22em;
text-transform: uppercase; color: var(--theme-text-muted);
} }
.support-hero { .support-hero {
display: grid; display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(18rem, 0.72fr); grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.8fr);
gap: var(--gap-lg); gap: 28px;
align-items: end; align-items: end;
padding: clamp(2rem, 5vw, 5rem) 0 var(--section-y-sm); padding-bottom: 36px;
border-bottom: 1px solid var(--theme-border); border-bottom: 1px solid var(--theme-border);
} }
.support-hero-copy {
min-width: 0;
}
.support-hero-copy h1 { .support-hero-copy h1 {
max-width: 10.8ch; margin: 14px 0 18px;
margin: clamp(0.85rem, 2vw, 1.2rem) 0 clamp(1rem, 2vw, 1.35rem); font-size: 68px;
color: var(--theme-text); line-height: 0.92;
font-size: clamp(3rem, 7.4vw, 8.8rem);
line-height: 0.9;
font-weight: 300; font-weight: 300;
letter-spacing: 0; letter-spacing: -0.05em;
text-transform: uppercase; color: var(--theme-text);
text-wrap: balance;
} }
.support-intro { .support-intro {
max-width: var(--text-measure); max-width: 720px;
margin: 0; margin: 0;
color: var(--theme-text-muted); font-size: 18px;
font-size: var(--text-lg);
line-height: 1.65; line-height: 1.65;
color: var(--theme-text-muted);
} }
.support-hero-panel { .support-hero-panel {
padding: clamp(1.25rem, 3vw, 2rem); padding: 24px;
border: 1px solid rgba(var(--theme-accent-rgb) / 0.2); background: linear-gradient(
background: 180deg,
linear-gradient(135deg, rgba(var(--theme-accent-rgb) / 0.1), transparent 62%), rgba(255, 106, 0, 0.08),
var(--theme-surface-soft); rgba(255, 106, 0, 0.03)
);
border: 1px solid rgba(255, 106, 0, 0.18);
} }
.support-hero-panel p { .support-hero-panel p {
margin: 0.75rem 0 0; margin: 10px 0 0;
font-size: 16px;
line-height: 1.6;
color: var(--theme-text); color: var(--theme-text);
font-size: var(--text-base);
line-height: 1.62;
} }
.support-panel-meta { .support-panel-meta {
display: grid; display: grid;
gap: var(--gap-sm); grid-template-columns: 1fr;
margin-top: clamp(1.2rem, 2.6vw, 2rem); gap: 16px;
padding-top: var(--gap-sm); margin-top: 24px;
border-top: 1px solid rgba(var(--theme-accent-rgb) / 0.2); padding-top: 20px;
border-top: 1px solid rgba(255, 106, 0, 0.14);
} }
.support-panel-meta p { .support-panel-meta p {
margin: 0.45rem 0 0; margin: 8px 0 0;
color: var(--theme-text); font-size: 14px;
font-size: var(--text-sm);
line-height: 1.55; line-height: 1.55;
} color: var(--theme-text);
.support-quick-grid,
.support-info-grid,
.support-faq-grid {
display: grid;
gap: var(--gap-sm);
} }
.support-quick-grid { .support-quick-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: var(--section-y-sm);
}
.support-section,
.support-faq-section {
padding-top: var(--section-y-sm);
}
.support-section--split,
.support-info-grid {
display: grid; display: grid;
grid-template-columns: minmax(16rem, 0.72fr) minmax(0, 1.28fr); grid-template-columns: repeat(3, 1fr);
gap: var(--gap-lg); gap: 18px;
align-items: start; margin-top: 38px;
} }
.support-info-grid { .support-quick-card {
margin-top: var(--section-y-sm); padding: 24px;
background: var(--theme-bg);
border: 1px solid var(--theme-border);
min-height: 240px;
}
.support-quick-card h3 {
margin: 14px 0 12px;
font-size: 24px;
line-height: 1.08;
font-weight: 400;
color: var(--theme-text);
}
.support-quick-card p {
margin: 0;
font-size: 15px;
line-height: 1.65;
color: var(--theme-text-muted);
}
.support-section {
padding-top: 38px;
}
.support-section--split {
display: grid;
grid-template-columns: minmax(260px, 0.7fr) minmax(0, 1.3fr);
gap: 28px;
align-items: start;
} }
.support-section-heading h2, .support-section-heading h2,
.support-bottom-copy h2 { .support-bottom-copy h2 {
margin: 0.75rem 0 0; margin: 10px 0 0;
color: var(--theme-text); font-size: 42px;
font-size: clamp(2.15rem, 5.2vw, 6rem); line-height: 0.98;
line-height: 0.94;
font-weight: 300; font-weight: 300;
letter-spacing: 0; letter-spacing: -0.04em;
text-transform: uppercase; color: var(--theme-text);
text-wrap: balance;
} }
.support-section-copy { .support-section-copy {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--gap-sm); gap: 18px;
} }
.support-section-copy p, .support-section-copy p,
.support-bottom-copy p, .support-bottom-copy p {
.support-quick-card p,
.support-faq-card p,
.support-info-box p,
.support-list li {
margin: 0; margin: 0;
color: var(--theme-text-muted); font-size: 16px;
font-size: var(--text-base);
line-height: 1.7; line-height: 1.7;
color: var(--theme-text-muted);
}
.support-info-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(320px, 0.9fr);
gap: 18px;
margin-top: 38px;
} }
.support-quick-card,
.support-faq-card,
.support-info-box { .support-info-box {
min-height: 100%; padding: 24px;
padding: clamp(1.1rem, 2.4vw, 1.8rem);
border: 1px solid var(--theme-border); border: 1px solid var(--theme-border);
background: background: var(--theme-surface-soft);
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.008)),
var(--theme-surface-soft);
transition:
transform var(--duration-med) var(--ease-out),
border-color var(--duration-med) var(--ease-out),
box-shadow var(--duration-med) var(--ease-out);
} }
.support-quick-card:hover,
.support-faq-card:hover,
.support-info-box:hover {
transform: translateY(-4px);
border-color: rgba(var(--theme-accent-rgb) / 0.42);
box-shadow: var(--theme-shadow-soft);
}
.support-quick-card h3,
.support-faq-card h3,
.support-info-box h3 { .support-info-box h3 {
margin: 0.9rem 0 0.75rem; margin: 14px 0 12px;
color: var(--theme-text); font-size: 28px;
font-size: var(--text-xl); line-height: 1.02;
line-height: 1.08;
font-weight: 400; font-weight: 400;
letter-spacing: 0; color: #fff;
}
.support-info-box p {
margin: 0;
font-size: 15px;
line-height: 1.65;
color: var(--theme-text-muted);
} }
.support-info-box--dark { .support-info-box--dark {
border-color: rgba(255, 255, 255, 0.16); background: #1f1f1f;
background: border-color: var(--theme-text);
radial-gradient(circle at 100% 0%, rgba(var(--theme-accent-rgb) / 0.18), transparent 18rem),
#171717;
} }
.support-info-box--dark .support-label, .support-info-box--dark .support-label,
@ -186,61 +184,80 @@
color: rgba(255, 255, 255, 0.78); color: rgba(255, 255, 255, 0.78);
} }
.support-info-box--dark h3 { .support-list {
color: #fff; margin: 14px 0 0;
padding-left: 18px;
display: grid;
gap: 10px;
} }
.support-list { .support-list li {
display: grid; font-size: 15px;
gap: 0.75rem; line-height: 1.6;
margin: 1rem 0 0; color: var(--theme-text);
padding-left: 1.1rem;
} }
.support-mail-btn { .support-mail-btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 48px; margin-top: 18px;
margin-top: 1.15rem;
padding: 0 1.1rem;
border-radius: 999px;
background: var(--theme-accent);
color: #fff;
font-size: var(--text-sm);
text-decoration: none; text-decoration: none;
transition: padding: 12px 18px;
transform var(--duration-med) var(--ease-out), border-radius: 999px;
box-shadow var(--duration-med) var(--ease-out); background: #ff6a00;
color: #fff;
font-size: 14px;
transition: transform 0.2s ease, opacity 0.2s ease, box-shadow 0.2s ease;
} }
.support-mail-btn:hover, .support-mail-btn:hover {
.support-mail-btn:focus-visible { transform: translateY(-1px);
transform: translateY(-2px);
box-shadow: var(--theme-shadow-soft);
} }
.support-faq-section { .support-faq-section {
margin-top: var(--section-y-sm); margin-top: 38px;
padding-top: 38px;
border-top: 1px solid var(--theme-border); border-top: 1px solid var(--theme-border);
} }
.support-faq-grid { .support-faq-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); display: grid;
margin-top: clamp(1.6rem, 4vw, 3rem); grid-template-columns: repeat(2, 1fr);
gap: 18px;
margin-top: 28px;
}
.support-faq-card {
padding: 24px;
border: 1px solid var(--theme-border);
background: var(--theme-bg);
min-height: 210px;
}
.support-faq-card h3 {
margin: 0 0 12px;
font-size: 22px;
line-height: 1.08;
font-weight: 400;
color: var(--theme-text);
}
.support-faq-card p {
margin: 0;
font-size: 15px;
line-height: 1.65;
color: var(--theme-text-muted);
} }
.support-bottom-cta { .support-bottom-cta {
margin-top: 38px;
padding: 38px;
background: #ff6a00;
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1.2fr) auto;
gap: var(--gap-lg); gap: 24px;
align-items: end; align-items: end;
margin-top: var(--section-y-sm);
padding: clamp(1.5rem, 4vw, 3.5rem);
overflow: hidden;
background:
radial-gradient(circle at 92% 0%, rgba(255, 255, 255, 0.22), transparent 20rem),
var(--theme-accent);
} }
.support-bottom-copy .support-label, .support-bottom-copy .support-label,
@ -249,54 +266,48 @@
color: #fff; color: #fff;
} }
.support-bottom-copy .support-label {
opacity: 0.85;
}
.support-bottom-copy p { .support-bottom-copy p {
max-width: 48rem; margin-top: 16px;
margin-top: 1rem; max-width: 700px;
} }
.support-bottom-actions { .support-bottom-actions {
display: flex; display: flex;
gap: 12px;
flex-wrap: wrap; flex-wrap: wrap;
gap: var(--gap-xs);
justify-content: flex-end;
} }
.support-btn { .support-btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 48px;
padding: 0 1.1rem;
border: 1px solid transparent;
border-radius: 999px;
color: inherit;
font-size: var(--text-sm);
text-decoration: none; text-decoration: none;
transition: padding: 12px 18px;
transform var(--duration-med) var(--ease-out), font-size: 14px;
box-shadow var(--duration-med) var(--ease-out), border-radius: 999px;
background-color var(--duration-med) var(--ease-out); transition: transform 0.2s ease, opacity 0.2s ease, box-shadow 0.2s ease;
} }
.support-btn:hover, .support-btn:hover {
.support-btn:focus-visible { transform: translateY(-1px);
transform: translateY(-2px);
box-shadow: var(--theme-shadow-soft);
} }
.support-btn--primary { .support-btn--primary {
background: #fff; background: var(--theme-paper);
color: var(--theme-accent); color: #ff6a00;
} }
.support-btn--secondary { .support-btn--secondary {
border-color: rgba(255, 255, 255, 0.22);
background: rgba(255, 255, 255, 0.14); background: rgba(255, 255, 255, 0.14);
color: #fff; color: #fff;
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
} }
@media (max-width: 1180px) { @media (max-width: 1100px) {
.support-hero, .support-hero,
.support-section--split, .support-section--split,
.support-info-grid, .support-info-grid,
@ -304,32 +315,44 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.support-quick-grid,
.support-faq-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 760px) {
.support-hero {
padding-top: clamp(1.4rem, 5vw, 2rem);
}
.support-hero-copy h1 { .support-hero-copy h1 {
font-size: clamp(2.55rem, 13vw, 4.4rem); font-size: 52px;
} }
.support-quick-grid, .support-quick-grid,
.support-faq-grid { .support-faq-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.support-bottom-actions {
display: grid;
justify-content: stretch;
} }
.support-btn { @media (max-width: 700px) {
width: 100%; .support-page {
padding: 18px;
}
.support-shell {
padding: 24px 18px;
}
.support-hero-copy h1 {
font-size: 38px;
}
.support-section-heading h2,
.support-bottom-copy h2 {
font-size: 30px;
}
.support-intro,
.support-section-copy p,
.support-bottom-copy p,
.support-quick-card p,
.support-faq-card p {
font-size: 15px;
}
.support-bottom-cta {
padding: 26px 20px;
} }
} }

View File

@ -5,9 +5,9 @@ import "./SupportPage.css";
function SupportPage() { function SupportPage() {
return ( return (
<div className="support-page"> <div className="support-page">
<SharedNavbar variant="hero" /> <SharedNavbar variant="light" />
<main className="shell"> <main className="support-shell">
<section className="support-hero" data-reveal-group> <section className="support-hero" data-reveal-group>
<div className="support-hero-copy"> <div className="support-hero-copy">
<span className="support-kicker" data-reveal="fade"> <span className="support-kicker" data-reveal="fade">

View File

@ -1,86 +1,39 @@
/* --- Shared Navbar Start --- */ /* --- Shared Navbar Start --- */
.navbar { .navbar {
position: sticky; position: relative;
top: clamp(0.75rem, 2vw, 1.25rem); z-index: 20;
z-index: 998;
display: flex; display: flex;
justify-content: center; justify-content: center;
width: 100%;
padding-inline: var(--page-x);
} }
.nav-pill { .nav-pill {
display: flex; display: flex;
align-items: center; gap: 10px;
justify-content: center; padding: 8px 10px;
gap: clamp(0.25rem, 0.8vw, 0.65rem);
width: fit-content;
max-width: 100%;
min-height: clamp(3rem, 5.4vw, 3.5rem);
padding: clamp(0.32rem, 0.8vw, 0.55rem);
border: 1px solid transparent;
border-radius: 999px; border-radius: 999px;
backdrop-filter: blur(18px) saturate(1.2); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(18px) saturate(1.2); -webkit-backdrop-filter: blur(10px);
box-shadow: 0 16px 50px rgba(0, 0, 0, 0.16);
} }
.nav-link { .nav-link {
position: relative;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 42px;
min-width: 42px;
padding: 0 clamp(0.72rem, 1.5vw, 1rem);
border-radius: 999px;
color: inherit;
font-size: var(--text-xs);
line-height: 1;
text-decoration: none; text-decoration: none;
white-space: nowrap; font-size: 13px;
transition: padding: 8px 14px;
background-color var(--duration-med) var(--ease-out), border-radius: 999px;
color var(--duration-med) var(--ease-out), transition: 0.2s ease;
opacity var(--duration-med) var(--ease-out),
transform var(--duration-med) var(--ease-out);
}
.nav-link::after {
content: "";
position: absolute;
right: 0.8rem;
bottom: 0.45rem;
left: 0.8rem;
height: 1px;
background: currentColor;
opacity: 0;
transform: scaleX(0.35);
transform-origin: center;
transition:
opacity var(--duration-med) var(--ease-out),
transform var(--duration-med) var(--ease-out);
}
.nav-link:hover::after,
.nav-link.active::after {
opacity: 0.5;
transform: scaleX(1);
} }
.nav-link--brand { .nav-link--brand {
padding-inline: clamp(0.75rem, 1.4vw, 1rem); padding: 8px 12px;
}
.nav-link--brand::after,
.nav-theme-switch::after {
display: none;
} }
.nav-brand-logo { .nav-brand-logo {
display: block; display: block;
width: clamp(58px, 5.2vw, 82px); width: clamp(56px, 5.4vw, 78px);
height: auto; height: auto;
} }
@ -91,32 +44,28 @@
} }
.nav-theme-switch { .nav-theme-switch {
min-width: 46px; min-width: auto;
padding-inline: 0.55rem; padding: 8px;
} }
.nav-theme-switch__track { .nav-theme-switch__track {
position: relative; position: relative;
width: 40px; width: 38px;
height: 22px; height: 20px;
border-radius: 999px;
border: 1px solid;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
padding: 2px; padding: 2px;
border: 1px solid; transition: background-color 0.2s ease, border-color 0.2s ease;
border-radius: 999px;
transition:
background-color var(--duration-med) var(--ease-out),
border-color var(--duration-med) var(--ease-out);
} }
.nav-theme-switch__thumb { .nav-theme-switch__thumb {
width: 16px; width: 14px;
height: 16px; height: 14px;
border-radius: 50%; border-radius: 50%;
transform: translateX(0); transform: translateX(0);
transition: transition: transform 0.2s ease, background-color 0.2s ease;
transform var(--duration-med) var(--ease-snap),
background-color var(--duration-med) var(--ease-out);
} }
.nav-theme-switch.is-light .nav-theme-switch__thumb { .nav-theme-switch.is-light .nav-theme-switch__thumb {
@ -125,21 +74,15 @@
/* Hero variant */ /* Hero variant */
.navbar--hero { .navbar--hero {
position: fixed; padding-top: 22px;
top: clamp(0.75rem, 2.1vw, 1.4rem);
left: 0;
right: 0;
z-index: 998;
padding-top: 0;
} }
.navbar--hero .nav-pill { .navbar--hero .nav-pill {
background: rgba(15, 15, 15, 0.58); background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.22);
} }
.navbar--hero .nav-link { .navbar--hero .nav-link {
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.88);
} }
.navbar--hero .nav-button { .navbar--hero .nav-button {
@ -157,18 +100,17 @@
.navbar--hero .nav-link:hover, .navbar--hero .nav-link:hover,
.navbar--hero .nav-link.active { .navbar--hero .nav-link.active {
background: rgba(255, 255, 255, 0.16); background: rgba(255, 255, 255, 0.22);
} }
/* Detail page variant */ /* Detail page variant */
.navbar--light { .navbar--light {
margin-bottom: clamp(1rem, 2.5vw, 1.8rem); margin-bottom: 18px;
padding-top: clamp(0.35rem, 1vw, 0.7rem);
} }
.navbar--light .nav-pill { .navbar--light .nav-pill {
background: color-mix(in srgb, var(--theme-paper) 86%, transparent); background: rgba(255, 255, 255, 0.88);
border-color: var(--theme-border); border: 1px solid var(--theme-border);
} }
.navbar--light .nav-link { .navbar--light .nav-link {
@ -180,7 +122,7 @@
} }
.navbar--light .nav-theme-switch__track { .navbar--light .nav-theme-switch__track {
border-color: var(--theme-border-strong); border-color: rgba(38, 38, 38, 0.24);
background: rgba(38, 38, 38, 0.08); background: rgba(38, 38, 38, 0.08);
} }
@ -195,31 +137,19 @@
/* --- Shared Navbar End --- */ /* --- Shared Navbar End --- */
@media (max-width: 700px) { @media (max-width: 640px) {
.navbar {
padding-inline: clamp(0.75rem, 4vw, 1rem);
}
.nav-pill { .nav-pill {
justify-content: space-between; gap: 4px;
width: 100%; padding: 6px;
overflow-x: auto;
scrollbar-width: none;
}
.nav-pill::-webkit-scrollbar {
display: none;
} }
.nav-link { .nav-link {
min-height: 40px; padding: 8px 10px;
min-width: 40px; font-size: 12px;
padding-inline: 0.68rem;
font-size: 0.75rem;
} }
.nav-link--brand { .nav-link--brand {
padding-inline: 0.72rem; padding: 8px 10px;
} }
.nav-brand-logo { .nav-brand-logo {
@ -227,16 +157,8 @@
} }
.nav-theme-switch { .nav-theme-switch {
padding-inline: 0.45rem; min-width: auto;
padding: 8px;
} }
} }
@media (max-width: 390px) {
.nav-link {
padding-inline: 0.58rem;
}
.nav-link--brand {
padding-inline: 0.62rem;
}
}

View File

@ -1,7 +1,17 @@
import { ThemeContext } from "./ThemeContextBase"; import { createContext, useContext } from "react";
const ThemeContext = createContext(null);
function ThemeProvider({ value, children }) { function ThemeProvider({ value, children }) {
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>; return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
} }
export { ThemeProvider }; function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider.");
}
return context;
}
export { ThemeProvider, useTheme };

View File

@ -1,5 +0,0 @@
import { createContext } from "react";
const ThemeContext = createContext(null);
export { ThemeContext };

View File

@ -1,12 +0,0 @@
import { useContext } from "react";
import { ThemeContext } from "./ThemeContextBase";
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider.");
}
return context;
}
export { useTheme };

View File

@ -1,9 +0,0 @@
import { createContext, useContext } from "react";
export const ProductTransitionContext = createContext({
activeSlug: null,
phase: "idle",
startProductTransition: () => false,
});
export const useProductTransition = () => useContext(ProductTransitionContext);