add styling and media cleanup
@ -1,16 +1,59 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Questrial&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>parfum-shop</title>
|
||||
|
||||
<title>atmos · Konzeptionelle Düfte aus der Schweiz</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="atmos — konzeptionelle Nischendüfte zwischen Materialität, Raum und Charakter. Sechs Düfte als Discovery Set oder 50 ml Flakon. Made in Switzerland."
|
||||
/>
|
||||
<meta name="theme-color" content="#262626" />
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
|
||||
<!-- TODO: replace https://atmos.example with the real production domain -->
|
||||
<link rel="canonical" href="https://atmos.example/" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="atmos" />
|
||||
<meta property="og:title" content="atmos · Konzeptionelle Düfte" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Konzeptionelle Nischendüfte zwischen Materialität, Raum und Charakter. Made in Switzerland."
|
||||
/>
|
||||
<meta property="og:url" content="https://atmos.example/" />
|
||||
<meta property="og:image" content="https://atmos.example/og-image.jpg" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:locale" content="de_CH" />
|
||||
|
||||
<!-- Twitter / X -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="atmos · Konzeptionelle Düfte" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Konzeptionelle Nischendüfte zwischen Materialität, Raum und Charakter."
|
||||
/>
|
||||
<meta name="twitter:image" content="https://atmos.example/og-image.jpg" />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/questrial/questrial-latin.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
/>
|
||||
|
||||
<!-- Preload LCP image of the landing hero -->
|
||||
<link
|
||||
rel="preload"
|
||||
as="image"
|
||||
href="/blasse-seide-hero-product.webp"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
|
Before Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 341 KiB |
|
Before Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 446 KiB |
|
Before Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 1.8 MiB |
BIN
parfum-shop/public/atmos-discovery-set-thumbnail.webp
Normal file
|
After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 952 KiB |
BIN
parfum-shop/public/blasse-seide-product-image.webp
Normal file
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 179 KiB |
BIN
parfum-shop/public/blasse-seide-product-sample-image.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
93
parfum-shop/public/fonts/questrial/OFL.txt
Normal file
@ -0,0 +1,93 @@
|
||||
Copyright 2011 The Questrial Project Authors (https://github.com/googlefonts/questrial)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
parfum-shop/public/fonts/questrial/questrial-latin-ext.woff2
Normal file
BIN
parfum-shop/public/fonts/questrial/questrial-latin.woff2
Normal file
BIN
parfum-shop/public/fonts/questrial/questrial-vietnamese.woff2
Normal file
|
Before Width: | Height: | Size: 957 KiB |
BIN
parfum-shop/public/kalter-beton-product-image.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 138 KiB |
BIN
parfum-shop/public/kalter-beton-product-sample-image.webp
Normal file
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 365 KiB |
|
Before Width: | Height: | Size: 952 KiB |
BIN
parfum-shop/public/nasser-marmor-product-image.webp
Normal file
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 169 KiB |
BIN
parfum-shop/public/nasser-marmor-product-sample-image.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
8
parfum-shop/public/robots.txt
Normal file
@ -0,0 +1,8 @@
|
||||
# robots.txt — atmos parfum-shop
|
||||
# TODO: replace https://atmos.example with the real production domain
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /api/
|
||||
|
||||
Sitemap: https://atmos.example/sitemap.xml
|
||||
|
Before Width: | Height: | Size: 933 KiB |
BIN
parfum-shop/public/schwarzes-benzin-product-image.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 154 KiB |
BIN
parfum-shop/public/schwarzes-benzin-product-sample-image.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
68
parfum-shop/public/sitemap.xml
Normal file
@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://atmos.ch/</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/about</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/discovery-set</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/small-batch</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/support</loc>
|
||||
<changefreq>yearly</changefreq>
|
||||
<priority>0.4</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/impressum</loc>
|
||||
<changefreq>yearly</changefreq>
|
||||
<priority>0.2</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/datenschutz</loc>
|
||||
<changefreq>yearly</changefreq>
|
||||
<priority>0.2</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/duft/kalter-beton</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/duft/nasser-marmor</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/duft/blasse-seide</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/duft/weisse-asche</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/duft/verbranntes-chrom</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/duft/schwarzes-benzin</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
|
Before Width: | Height: | Size: 951 KiB |
BIN
parfum-shop/public/verbranntes-chrom-product-image.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 138 KiB |
BIN
parfum-shop/public/verbrannteschrom-product-sample-image.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 932 KiB |
BIN
parfum-shop/public/weisse-asche-product-image.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 175 KiB |
BIN
parfum-shop/public/weisse-asche-product-sample-image.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
@ -59,6 +59,10 @@ function App() {
|
||||
<ProductTransitionProvider>
|
||||
<ScrollToTop />
|
||||
|
||||
<a href="#main-content" className="skip-link">
|
||||
Zum Inhalt springen
|
||||
</a>
|
||||
|
||||
<div ref={routeContentRef} data-route-content>
|
||||
<Routes>
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 82% 10%, rgba(var(--theme-accent-rgb) / 0.15), transparent 22rem),
|
||||
#171717;
|
||||
color: #f5f5f5;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
var(--footer-bg);
|
||||
color: var(--footer-text);
|
||||
border-top: 1px solid var(--footer-border);
|
||||
}
|
||||
|
||||
.site-footer--flush {
|
||||
@ -18,7 +18,7 @@
|
||||
position: absolute;
|
||||
right: var(--page-x);
|
||||
bottom: -0.16em;
|
||||
color: rgba(255, 255, 255, 0.035);
|
||||
color: var(--footer-watermark);
|
||||
font-size: clamp(5.5rem, 18vw, 20rem);
|
||||
line-height: 0.8;
|
||||
letter-spacing: 0;
|
||||
@ -54,7 +54,7 @@
|
||||
.site-footer__text {
|
||||
max-width: 32rem;
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
color: var(--footer-text-muted);
|
||||
font-size: var(--text-base);
|
||||
line-height: 1.65;
|
||||
}
|
||||
@ -67,7 +67,7 @@
|
||||
}
|
||||
|
||||
.site-footer__heading {
|
||||
color: rgba(255, 255, 255, 0.52);
|
||||
color: var(--footer-text-faint);
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
@ -81,7 +81,7 @@
|
||||
|
||||
.site-footer__nav a {
|
||||
width: fit-content;
|
||||
color: #f5f5f5;
|
||||
color: var(--footer-text);
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.2;
|
||||
text-decoration: none;
|
||||
|
||||
@ -94,7 +94,7 @@
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
filter: drop-shadow(0 34px 72px rgba(0, 0, 0, 0.42));
|
||||
filter: var(--shadow-product);
|
||||
}
|
||||
|
||||
.is-transition-arriving .product-hero-image,
|
||||
@ -250,46 +250,75 @@
|
||||
}
|
||||
|
||||
.size-card {
|
||||
min-height: 112px;
|
||||
padding: clamp(0.85rem, 1.4vw, 1.05rem);
|
||||
position: relative;
|
||||
min-height: 116px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
padding: clamp(0.95rem, 1.6vw, 1.2rem);
|
||||
border: 1px solid var(--theme-border);
|
||||
background: var(--theme-paper);
|
||||
background: transparent;
|
||||
color: var(--theme-text);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
isolation: isolate;
|
||||
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);
|
||||
color var(--duration-med) var(--ease-out);
|
||||
}
|
||||
|
||||
/* Top accent line — appears on hover, stays on active. */
|
||||
.size-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: var(--theme-accent);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left center;
|
||||
transition: transform var(--duration-med) var(--ease-out);
|
||||
}
|
||||
|
||||
.size-card:hover,
|
||||
.size-card:focus-visible {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(var(--theme-accent-rgb) / 0.58);
|
||||
box-shadow: var(--theme-shadow-soft);
|
||||
border-color: var(--theme-border-strong);
|
||||
}
|
||||
|
||||
.size-card:hover::before,
|
||||
.size-card:focus-visible::before {
|
||||
transform: scaleX(0.45);
|
||||
}
|
||||
|
||||
.size-card.active {
|
||||
border-color: var(--theme-accent);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(var(--theme-accent-rgb) / 0.12), transparent 62%),
|
||||
var(--theme-paper);
|
||||
background: rgba(var(--theme-accent-rgb) / 0.06);
|
||||
}
|
||||
|
||||
.size-card.active::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.size-title {
|
||||
display: block;
|
||||
margin-bottom: 0.65rem;
|
||||
color: var(--theme-text);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--theme-text-muted);
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
transition: color var(--duration-med) var(--ease-out);
|
||||
}
|
||||
|
||||
.size-card.active .size-title {
|
||||
color: var(--theme-accent);
|
||||
}
|
||||
|
||||
.size-card strong {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
margin-top: auto;
|
||||
color: var(--theme-text);
|
||||
font-size: clamp(1.05rem, 1.4vw, 1.38rem);
|
||||
font-size: clamp(1.1rem, 1.55vw, 1.45rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.05;
|
||||
@ -931,7 +960,7 @@
|
||||
width: min(86%, 430px);
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 28px 58px rgba(0, 0, 0, 0.34));
|
||||
filter: var(--shadow-product-card);
|
||||
transition: transform var(--duration-slow) var(--ease-out);
|
||||
}
|
||||
|
||||
@ -1170,18 +1199,18 @@
|
||||
}
|
||||
|
||||
.size-card {
|
||||
min-height: 62px;
|
||||
padding: 0.55rem 0.6rem;
|
||||
min-height: 70px;
|
||||
gap: 0.2rem;
|
||||
padding: 0.6rem 0.7rem;
|
||||
}
|
||||
|
||||
.size-title {
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.76rem;
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.14em;
|
||||
}
|
||||
|
||||
.size-card strong {
|
||||
margin-bottom: 0;
|
||||
font-size: 0.95rem;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.size-card small,
|
||||
|
||||
@ -2,12 +2,53 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useParams } from "react-router";
|
||||
import perfumes from "../data/perfumes";
|
||||
import SharedNavbar from "./SharedNavbar";
|
||||
import PageMeta from "./seo/PageMeta";
|
||||
import { useProductTransition } from "../transitions/ProductTransitionContext";
|
||||
import { formatChf } from "../shop/money";
|
||||
import { useShop } from "../shop/useShop";
|
||||
import { formatChf } from "../shop/money";
|
||||
import "./ProductDetailPage.css";
|
||||
|
||||
const STORY_PANEL_IMAGE = "/placeholder-character-panel.jpg";
|
||||
// TODO: replace with the real production origin once available.
|
||||
const SITE_ORIGIN = "https://atmos.example";
|
||||
|
||||
function buildProductJsonLd(perfume) {
|
||||
const priceMatch = String(perfume.prices?.full || "").match(/(\d+)/);
|
||||
const price = priceMatch ? Number(priceMatch[1]) : undefined;
|
||||
const reviews = perfume.reviews;
|
||||
|
||||
const data = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Product",
|
||||
name: perfume.name,
|
||||
sku: `${perfume.id}-${perfume.slug}`,
|
||||
description: `${perfume.text} ${perfume.mood}`.trim(),
|
||||
image: `${SITE_ORIGIN}${perfume.image}`,
|
||||
brand: { "@type": "Brand", name: "atmos" },
|
||||
category: "Parfum",
|
||||
};
|
||||
|
||||
if (price !== undefined) {
|
||||
data.offers = {
|
||||
"@type": "Offer",
|
||||
priceCurrency: "CHF",
|
||||
price,
|
||||
availability: "https://schema.org/InStock",
|
||||
url: `${SITE_ORIGIN}/duft/${perfume.slug}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (reviews?.score && reviews?.total) {
|
||||
data.aggregateRating = {
|
||||
"@type": "AggregateRating",
|
||||
ratingValue: reviews.score,
|
||||
reviewCount: reviews.total,
|
||||
bestRating: 5,
|
||||
worstRating: 1,
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
const priceToCents = (price) => {
|
||||
const match = String(price).match(/(\d+)/);
|
||||
@ -24,6 +65,8 @@ const getSampleImage = (perfume) =>
|
||||
const getImageForSize = (perfume, size) =>
|
||||
size === "sample" ? getSampleImage(perfume) : getFullSizeImage(perfume);
|
||||
|
||||
const STORY_PANEL_IMAGE = "/placeholder-character-panel.jpg";
|
||||
|
||||
function ProductPurchasePanel({
|
||||
perfume,
|
||||
selectedSize,
|
||||
@ -144,10 +187,12 @@ function ProductHero({
|
||||
<div className="product-hero-visual">
|
||||
<img
|
||||
src={selectedImage}
|
||||
alt={perfume.name}
|
||||
alt={`${perfume.name} Parfumflakon`}
|
||||
className="product-hero-image"
|
||||
data-product-transition-target={perfume.slug}
|
||||
decoding="async"
|
||||
loading="eager"
|
||||
fetchPriority="high"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -218,7 +263,7 @@ function ProductStorySection({ perfume }) {
|
||||
<img
|
||||
className="story-visual-image"
|
||||
src={STORY_PANEL_IMAGE}
|
||||
alt=""
|
||||
alt={`Material- und Stimmungsstudie zu ${perfume.name}`}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
@ -545,11 +590,23 @@ function ProductDetailContent({ perfumeSlug }) {
|
||||
return () => window.clearInterval(interval);
|
||||
}, [safeCommentPages.length]);
|
||||
|
||||
const productJsonLd = useMemo(() => buildProductJsonLd(perfume), [perfume]);
|
||||
|
||||
return (
|
||||
<div className={`detail-page ${isTransitionArriving ? "is-transition-arriving" : ""}`}>
|
||||
<PageMeta
|
||||
title={`${perfume.name} · Edition ${perfume.id}`}
|
||||
description={`${perfume.text} ${perfume.mood} ${perfume.concentration}.`}
|
||||
path={`/duft/${perfume.slug}`}
|
||||
image={perfume.image}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }}
|
||||
/>
|
||||
<SharedNavbar variant="hero" brandMode="back" />
|
||||
|
||||
<main className="shell">
|
||||
<main id="main-content" className="shell">
|
||||
<ProductHero
|
||||
perfume={perfume}
|
||||
selectedImage={selectedImage}
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
object-fit: contain;
|
||||
transform-origin: 0 0;
|
||||
will-change: transform, opacity;
|
||||
filter: drop-shadow(0 34px 82px rgba(0, 0, 0, 0.42));
|
||||
filter: var(--shadow-product);
|
||||
}
|
||||
|
||||
body.product-transition-active {
|
||||
|
||||
@ -6,56 +6,74 @@ import "../style/navbar.css";
|
||||
function SharedNavbar({ variant = "hero", active = "", brandMode = "logo" }) {
|
||||
const { cart, openCart, openProfile, user } = useShop();
|
||||
const { isLight, toggleTheme } = useTheme();
|
||||
const cartLabel =
|
||||
cart.total_quantity > 0 ? `Cart ${cart.total_quantity}` : "Cart";
|
||||
const cartCount = cart.total_quantity || 0;
|
||||
const cartLabel = cartCount > 0 ? `Cart ${cartCount}` : "Cart";
|
||||
const cartAriaLabel =
|
||||
cartCount > 0
|
||||
? `Warenkorb mit ${cartCount} ${cartCount === 1 ? "Artikel" : "Artikeln"} öffnen`
|
||||
: "Warenkorb öffnen";
|
||||
const logoSrc =
|
||||
variant === "hero" ? "/atmos-logo-light.svg" : "/atmos-logo-dark.svg";
|
||||
const brandIsBack = brandMode === "back";
|
||||
|
||||
return (
|
||||
<nav className={`navbar navbar--${variant}`} aria-label="Hauptnavigation">
|
||||
<div className="nav-pill">
|
||||
<Link
|
||||
to="/"
|
||||
className={`nav-link nav-link--brand ${brandIsBack ? "nav-link--back" : ""} ${
|
||||
active === "atmos" ? "active" : ""
|
||||
}`}
|
||||
aria-label={brandIsBack ? "Zur Startseite" : "Atmos Startseite"}
|
||||
>
|
||||
{brandIsBack ? (
|
||||
<>
|
||||
<span className="nav-back-icon" aria-hidden="true" />
|
||||
<span>Zurück</span>
|
||||
</>
|
||||
) : (
|
||||
<img src={logoSrc} alt="" className="nav-brand-logo" />
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/discovery-set"
|
||||
className={`nav-link ${active === "testen" ? "active" : ""}`}
|
||||
>
|
||||
Testen
|
||||
</Link>
|
||||
<button type="button" className="nav-link nav-button" onClick={openCart}>
|
||||
{cartLabel}
|
||||
</button>
|
||||
<button type="button" className="nav-link nav-button" onClick={openProfile}>
|
||||
{user ? "Profile" : "Profile"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`nav-link nav-button nav-theme-switch ${isLight ? "is-light" : ""}`}
|
||||
onClick={toggleTheme}
|
||||
aria-label={isLight ? "Switch to dark mode" : "Switch to light mode"}
|
||||
aria-pressed={isLight}
|
||||
>
|
||||
<span className="nav-theme-switch__track" aria-hidden="true">
|
||||
<span className="nav-theme-switch__thumb" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<header className={`site-header site-header--${variant}`}>
|
||||
<nav className={`navbar navbar--${variant}`} aria-label="Hauptnavigation">
|
||||
<div className="nav-pill">
|
||||
<Link
|
||||
to="/"
|
||||
className={`nav-link nav-link--brand ${brandIsBack ? "nav-link--back" : ""} ${
|
||||
active === "atmos" ? "active" : ""
|
||||
}`}
|
||||
aria-label={brandIsBack ? "Zur Startseite" : "Atmos Startseite"}
|
||||
>
|
||||
{brandIsBack ? (
|
||||
<>
|
||||
<span className="nav-back-icon" aria-hidden="true" />
|
||||
<span>Zurück</span>
|
||||
</>
|
||||
) : (
|
||||
<img src={logoSrc} alt="" className="nav-brand-logo" />
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/discovery-set"
|
||||
className={`nav-link ${active === "testen" ? "active" : ""}`}
|
||||
>
|
||||
Testen
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="nav-link nav-button"
|
||||
onClick={openCart}
|
||||
aria-haspopup="dialog"
|
||||
aria-label={cartAriaLabel}
|
||||
>
|
||||
{cartLabel}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="nav-link nav-button"
|
||||
onClick={openProfile}
|
||||
aria-haspopup="dialog"
|
||||
aria-label={user ? "Profil öffnen" : "Anmelden oder Profil öffnen"}
|
||||
>
|
||||
Profile
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`nav-link nav-button nav-theme-switch ${isLight ? "is-light" : ""}`}
|
||||
onClick={toggleTheme}
|
||||
aria-label={isLight ? "Zum Dark Mode wechseln" : "Zum Light Mode wechseln"}
|
||||
aria-pressed={isLight}
|
||||
>
|
||||
<span className="nav-theme-switch__track" aria-hidden="true">
|
||||
<span className="nav-theme-switch__thumb" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -319,7 +319,11 @@ function SupportChatbot() {
|
||||
|
||||
<div className="chatbot-footer">
|
||||
<form className="chatbot-form" onSubmit={handleSubmit}>
|
||||
<label htmlFor="chatbot-input" className="visually-hidden">
|
||||
Deine Frage an den Support
|
||||
</label>
|
||||
<input
|
||||
id="chatbot-input"
|
||||
type="text"
|
||||
className="chatbot-input"
|
||||
placeholder="Deine Frage eingeben..."
|
||||
|
||||
@ -6,6 +6,7 @@ function HeroSection({
|
||||
heroImageRef,
|
||||
setHeadlinePrimaryRef,
|
||||
setHeadlineSecondaryRef,
|
||||
setHeadlineTertiaryRef,
|
||||
setWordmarkRef,
|
||||
setDescriptionRef,
|
||||
setActionsRef,
|
||||
@ -56,7 +57,9 @@ function HeroSection({
|
||||
</span>
|
||||
</span>
|
||||
<span className="hero-copy-line">
|
||||
<span className="reveal-line">Raum und Charakter.</span>
|
||||
<span className="reveal-line" ref={setHeadlineTertiaryRef}>
|
||||
Raum und Charakter.
|
||||
</span>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
|
||||
31
parfum-shop/src/components/layout/Container.jsx
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Container — horizontally constrained content wrapper.
|
||||
*
|
||||
* Sizes map to design tokens in src/style/tokens.css:
|
||||
* default → --container (max 1440px)
|
||||
* narrow → --container-narrow (max 920px, for prose)
|
||||
* wide → --container-wide (max 1680px, for hero/grid pages)
|
||||
*/
|
||||
function Container({
|
||||
as = "div",
|
||||
size = "default",
|
||||
className = "",
|
||||
children,
|
||||
...rest
|
||||
}) {
|
||||
const Tag = as;
|
||||
const sizeClass =
|
||||
size === "narrow"
|
||||
? "container-narrow"
|
||||
: size === "wide"
|
||||
? "container-wide"
|
||||
: "container";
|
||||
|
||||
return (
|
||||
<Tag className={`${sizeClass}${className ? ` ${className}` : ""}`} {...rest}>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
export default Container;
|
||||
67
parfum-shop/src/components/layout/Grid.jsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { forwardRef } from "react";
|
||||
|
||||
/**
|
||||
* 12-column grid wrapper. Mobile-first: children span the full width by
|
||||
* default and narrow at md/lg breakpoints via the Col `md`/`lg` props.
|
||||
*
|
||||
* <Grid gap="md">
|
||||
* <Col span={12} md={6} lg={4}>…</Col>
|
||||
* </Grid>
|
||||
*/
|
||||
const Grid = forwardRef(function Grid(
|
||||
{ gap = "md", className = "", children, as = "div", ...rest },
|
||||
ref
|
||||
) {
|
||||
const Tag = as;
|
||||
const gapClass = `grid-gap-${gap}`;
|
||||
return (
|
||||
<Tag
|
||||
ref={ref}
|
||||
className={`grid-12 ${gapClass}${className ? ` ${className}` : ""}`}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Grid column. Pass `span` for the mobile-first base, then `md`/`lg`
|
||||
* for tablet/desktop overrides. Optional `start` / `mdStart` / `lgStart`
|
||||
* for absolute placement.
|
||||
*/
|
||||
export const Col = forwardRef(function Col(
|
||||
{
|
||||
span = 12,
|
||||
md,
|
||||
lg,
|
||||
start,
|
||||
mdStart,
|
||||
lgStart,
|
||||
className = "",
|
||||
children,
|
||||
as = "div",
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const Tag = as;
|
||||
const classes = [`col-span-${span}`];
|
||||
if (md) classes.push(`md:col-span-${md}`);
|
||||
if (lg) classes.push(`lg:col-span-${lg}`);
|
||||
if (start) classes.push(`col-start-${start}`);
|
||||
if (mdStart) classes.push(`md:col-start-${mdStart}`);
|
||||
if (lgStart) classes.push(`lg:col-start-${lgStart}`);
|
||||
|
||||
return (
|
||||
<Tag
|
||||
ref={ref}
|
||||
className={`${classes.join(" ")}${className ? ` ${className}` : ""}`}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
});
|
||||
|
||||
export default Grid;
|
||||
38
parfum-shop/src/components/layout/Section.jsx
Normal file
@ -0,0 +1,38 @@
|
||||
import Container from "./Container";
|
||||
|
||||
/**
|
||||
* Section — semantic <section> with vertical rhythm and an inner Container.
|
||||
*
|
||||
* spacing: xs | sm | md | lg (maps to --section-y-* tokens)
|
||||
* container: default | narrow | wide | none
|
||||
*/
|
||||
function Section({
|
||||
as = "section",
|
||||
spacing = "sm",
|
||||
container = "default",
|
||||
id,
|
||||
className = "",
|
||||
children,
|
||||
...rest
|
||||
}) {
|
||||
const Tag = as;
|
||||
const spacingClass = `section-pad-${spacing}`;
|
||||
const inner =
|
||||
container === "none" ? (
|
||||
children
|
||||
) : (
|
||||
<Container size={container}>{children}</Container>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tag
|
||||
id={id}
|
||||
className={`${spacingClass}${className ? ` ${className}` : ""}`}
|
||||
{...rest}
|
||||
>
|
||||
{inner}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
export default Section;
|
||||
58
parfum-shop/src/components/seo/PageMeta.jsx
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* PageMeta — per-page SEO metadata.
|
||||
*
|
||||
* Relies on React 19's native document metadata hoisting: any <title>,
|
||||
* <meta>, or <link> rendered inside a component is moved to <head> at
|
||||
* mount time, so no third-party helmet library is required.
|
||||
*
|
||||
* <PageMeta
|
||||
* title="…"
|
||||
* description="…"
|
||||
* path="/discovery-set"
|
||||
* image="/og-image-discovery.jpg"
|
||||
* />
|
||||
*
|
||||
* Falls back to the brand suffix " · atmos" unless `bareTitle` is true.
|
||||
*/
|
||||
|
||||
const SITE_NAME = "atmos";
|
||||
// TODO: replace with the real production origin once available.
|
||||
const SITE_ORIGIN = "https://atmos.example";
|
||||
const DEFAULT_OG_IMAGE = `${SITE_ORIGIN}/og-image.jpg`;
|
||||
|
||||
function PageMeta({
|
||||
title,
|
||||
description,
|
||||
path = "/",
|
||||
image = DEFAULT_OG_IMAGE,
|
||||
bareTitle = false,
|
||||
}) {
|
||||
const fullTitle = bareTitle ? title : `${title} · ${SITE_NAME}`;
|
||||
const canonical = path.startsWith("http")
|
||||
? path
|
||||
: `${SITE_ORIGIN}${path.startsWith("/") ? path : `/${path}`}`;
|
||||
const ogImage = image?.startsWith("http") ? image : `${SITE_ORIGIN}${image}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{fullTitle}</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href={canonical} />
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content={SITE_NAME} />
|
||||
<meta property="og:title" content={fullTitle} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:locale" content="de_CH" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={fullTitle} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={ogImage} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageMeta;
|
||||
@ -3,7 +3,7 @@ const perfumes = [
|
||||
id: "01",
|
||||
slug: "kalter-beton",
|
||||
name: "KALTER BETON",
|
||||
image: "/kalter-beton-product-image.png",
|
||||
image: "/kalter-beton-product-image.webp",
|
||||
fillImage: "/platzhalter.png",
|
||||
fillVideo: "/kalter-beton-hover.webm",
|
||||
text: "Mineralisch. Roh. Unberührt.",
|
||||
@ -26,8 +26,8 @@ const perfumes = [
|
||||
concentration: "Eau de Parfum (18%)",
|
||||
edition: "Batch 04/24",
|
||||
gallery: [
|
||||
"/kalter-beton-product-image.png",
|
||||
"/kalter-beton-product-sample-image.png",
|
||||
"/kalter-beton-product-image.webp",
|
||||
"/kalter-beton-product-sample-webp",
|
||||
"/kalter-beton-product-image.png",
|
||||
],
|
||||
reviews: {
|
||||
@ -83,7 +83,7 @@ const perfumes = [
|
||||
id: "02",
|
||||
slug: "nasser-marmor",
|
||||
name: "NASSER MARMOR",
|
||||
image: "/nasser-marmor-product-image.png",
|
||||
image: "/nasser-marmor-product-image.webp",
|
||||
fillImage: "/platzhalter.png",
|
||||
fillVideo: "/nasser-marmor-hover.webm",
|
||||
text: "Kühl. Glatt. Sinnlich.",
|
||||
@ -106,8 +106,8 @@ const perfumes = [
|
||||
concentration: "Eau de Parfum (17%)",
|
||||
edition: "Batch 02/24",
|
||||
gallery: [
|
||||
"/nasser-marmor-product-image.png",
|
||||
"/nasser-marmor-product-sample-image.png",
|
||||
"/nasser-marmor-product-image.webp",
|
||||
"/nasser-marmor-product-sample-image.webp",
|
||||
"/nasser-marmor-product-image.png",
|
||||
],
|
||||
reviews: {
|
||||
@ -163,7 +163,7 @@ const perfumes = [
|
||||
id: "03",
|
||||
slug: "blasse-seide",
|
||||
name: "BLASSE SEIDE",
|
||||
image: "/blasse-seide-product-image.png",
|
||||
image: "/blasse-seide-product-image.webp",
|
||||
fillImage: "/platzhalter.png",
|
||||
fillVideo: "/blasse-seide-hover.webm",
|
||||
text: "Blass. Sanft. Kostbar.",
|
||||
@ -186,8 +186,8 @@ const perfumes = [
|
||||
concentration: "Eau de Parfum (16%)",
|
||||
edition: "Batch 03/24",
|
||||
gallery: [
|
||||
"/blasse-seide-product-image.png",
|
||||
"/blasse-seide-product-sample-image.png",
|
||||
"/blasse-seide-product-image.webp",
|
||||
"/blasse-seide-product-sample-image.webp",
|
||||
"/blasse-seide-product-image.png",
|
||||
],
|
||||
reviews: {
|
||||
@ -243,7 +243,7 @@ const perfumes = [
|
||||
id: "04",
|
||||
slug: "weisse-asche",
|
||||
name: "WEISSE ASCHE",
|
||||
image: "/weisse-asche-product-image.png",
|
||||
image: "/weisse-asche-product-image.webp",
|
||||
fillImage: "/platzhalter.png",
|
||||
fillVideo: "/weisse-asche-hover.webm",
|
||||
text: "Still. Staubig. Erhaben.",
|
||||
@ -266,8 +266,8 @@ const perfumes = [
|
||||
concentration: "Eau de Parfum (19%)",
|
||||
edition: "Batch 01/24",
|
||||
gallery: [
|
||||
"/weisse-asche-product-image.png",
|
||||
"/weisse-asche-product-sample-image.png",
|
||||
"/weisse-asche-product-image.webp",
|
||||
"/weisse-asche-product-sample-image.webp",
|
||||
"/weisse-asche-product-image.png",
|
||||
],
|
||||
reviews: {
|
||||
@ -323,7 +323,7 @@ const perfumes = [
|
||||
id: "05",
|
||||
slug: "verbranntes-chrom",
|
||||
name: "VERBRANNTES CHROM",
|
||||
image: "/verbranntes-chrom-product-image.png",
|
||||
image: "/verbranntes-chrom-product-image.webp",
|
||||
fillImage: "/platzhalter.png",
|
||||
fillVideo: "/verbranntes-chrom-hover.webm",
|
||||
text: "Metallisch. Verzehrt. Edel.",
|
||||
@ -346,8 +346,8 @@ const perfumes = [
|
||||
concentration: "Extrait de Parfum (24%)",
|
||||
edition: "Batch 05/24",
|
||||
gallery: [
|
||||
"/verbranntes-chrom-product-image.png",
|
||||
"/verbranntes-chrom-product-sample-image.png",
|
||||
"/verbranntes-chrom-product-image.webp",
|
||||
"/verbrannteschrom-product-sample-image.webp",
|
||||
"/verbranntes-chrom-product-image.png",
|
||||
],
|
||||
reviews: {
|
||||
@ -403,7 +403,7 @@ const perfumes = [
|
||||
id: "06",
|
||||
slug: "schwarzes-benzin",
|
||||
name: "SCHWARZES BENZIN",
|
||||
image: "/schwarzes-benzin-product-image.png",
|
||||
image: "/schwarzes-benzin-product-image.webp",
|
||||
fillImage: "/platzhalter.png",
|
||||
fillVideo: "/schwarzes-benzin-hover.webm",
|
||||
text: "Dunkel. Glänzend. Verboten.",
|
||||
@ -426,8 +426,8 @@ const perfumes = [
|
||||
concentration: "Eau de Parfum (20%)",
|
||||
edition: "Batch 06/24",
|
||||
gallery: [
|
||||
"/schwarzes-benzin-product-image.png",
|
||||
"/schwarzes-benzin-product-sample-image.png",
|
||||
"/schwarzes-benzin-product-image.webp",
|
||||
"/schwarzes-benzin-product-sample-image.webp",
|
||||
"/schwarzes-benzin-product-image.png",
|
||||
],
|
||||
reviews: {
|
||||
|
||||
@ -1,57 +1,51 @@
|
||||
/**
|
||||
* Global stylesheet
|
||||
* ----------------------------------------------------------------------------
|
||||
* Imports the design-token layer first, then the grid + breakpoint utilities,
|
||||
* and finally the global resets and base element styles. Page- and
|
||||
* component-level styles live next to their components.
|
||||
*/
|
||||
|
||||
@import "./style/tokens.css";
|
||||
@import "./style/grid.css";
|
||||
@import "./style/breakpoints.css";
|
||||
|
||||
@font-face {
|
||||
font-family: "Questrial";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("/fonts/questrial/questrial-vietnamese.woff2") format("woff2");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169,
|
||||
U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309,
|
||||
U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Questrial";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("/fonts/questrial/questrial-latin-ext.woff2") format("woff2");
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
|
||||
U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
|
||||
U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
|
||||
U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Questrial";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("/fonts/questrial/questrial-latin.woff2") format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
|
||||
U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
|
||||
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: "Questrial", Arial, Helvetica, sans-serif;
|
||||
--theme-black: #262626;
|
||||
--theme-white: #eaeaea;
|
||||
--theme-accent: #ff6a00;
|
||||
--theme-accent-rgb: 255 106 0;
|
||||
--theme-bg: #262626;
|
||||
--theme-surface: #2f2f2f;
|
||||
--theme-surface-soft: #363636;
|
||||
--theme-paper: #404040;
|
||||
--theme-text: #eaeaea;
|
||||
--theme-text-muted: #c8c8c8;
|
||||
--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);
|
||||
background: var(--theme-bg);
|
||||
@ -70,6 +64,8 @@
|
||||
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
body {
|
||||
@ -77,20 +73,11 @@ body {
|
||||
background: var(--theme-bg);
|
||||
color: var(--theme-text);
|
||||
font-family: inherit;
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
}
|
||||
|
||||
body.theme-light {
|
||||
--theme-bg: #eaeaea;
|
||||
--theme-surface: #f5f5f5;
|
||||
--theme-surface-soft: #f0f0f0;
|
||||
--theme-paper: #ffffff;
|
||||
--theme-text: #262626;
|
||||
--theme-text-muted: #5f5f5f;
|
||||
--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);
|
||||
overflow-x: hidden;
|
||||
overflow-x: clip;
|
||||
transition:
|
||||
background-color 0.25s ease,
|
||||
color 0.25s ease;
|
||||
}
|
||||
|
||||
a {
|
||||
@ -101,16 +88,6 @@ button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
img,
|
||||
picture,
|
||||
video,
|
||||
@ -148,6 +125,41 @@ a {
|
||||
color: var(--theme-white);
|
||||
}
|
||||
|
||||
/* Visually hidden but available to assistive tech */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Skip link — hidden until keyboard focus moves onto it */
|
||||
.skip-link {
|
||||
position: fixed;
|
||||
top: 0.75rem;
|
||||
left: 0.75rem;
|
||||
z-index: 9999;
|
||||
padding: 0.6rem 1rem;
|
||||
background: var(--theme-accent);
|
||||
color: #fff;
|
||||
font-size: var(--text-sm);
|
||||
text-decoration: none;
|
||||
transform: translateY(-150%);
|
||||
transition: transform var(--duration-med) var(--ease-out);
|
||||
}
|
||||
|
||||
.skip-link:focus,
|
||||
.skip-link:focus-visible {
|
||||
transform: translateY(0);
|
||||
outline: 2px solid #ffffff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
@ -162,4 +174,3 @@ a {
|
||||
transition-duration: 0.01ms;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -138,26 +138,14 @@
|
||||
margin-top: var(--gap-sm);
|
||||
}
|
||||
|
||||
/* These three sections use the global Grid12 system (see Grid.jsx).
|
||||
We keep only the section-level spacing here. */
|
||||
.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,
|
||||
@ -340,12 +328,6 @@
|
||||
.about-bottom-cta {
|
||||
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) {
|
||||
@ -357,12 +339,6 @@
|
||||
font-size: clamp(2.55rem, 13vw, 4.4rem);
|
||||
}
|
||||
|
||||
.about-proof-strip,
|
||||
.about-grid-section,
|
||||
.about-credentials-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.about-bottom-actions {
|
||||
display: grid;
|
||||
justify-content: stretch;
|
||||
|
||||
@ -1,13 +1,68 @@
|
||||
import { Link } from "react-router";
|
||||
import SharedNavbar from "../components/SharedNavbar";
|
||||
import Grid, { Col } from "../components/layout/Grid";
|
||||
import PageMeta from "../components/seo/PageMeta";
|
||||
import "./AboutPage.css";
|
||||
|
||||
const PROOF_ITEMS = [
|
||||
{ label: "ATELIER", text: "Entwicklung aus einem kuratierten Duft- und Materialkontext" },
|
||||
{ label: "KLEINSERIEN", text: "Chargenbasiert gedacht statt massenmarktfähig optimiert" },
|
||||
{ label: "KOMPOSITION", text: "Materiallogik vor Trendformel und Lautstärke" },
|
||||
{ label: "HERKUNFT", text: "Creative Direction und Qualitätsanspruch aus der Schweiz" },
|
||||
];
|
||||
|
||||
const APPROACH_CARDS = [
|
||||
{
|
||||
label: "01 / DEKODIEREN",
|
||||
title: "Atmosphäre lesen",
|
||||
text: "Wir beobachten Licht, Materialität, Temperatur, Distanz und Spannung. Nicht als Moodboard allein, sondern als System von Eindrücken, das eine bestimmte Wirkung erzeugt.",
|
||||
},
|
||||
{
|
||||
label: "02 / VERDICHTEN",
|
||||
title: "In Duft übersetzen",
|
||||
text: "Diese Eindrücke werden in eine olfaktorische Sprache übertragen: mineralisch, staubig, metallisch, glatt, rau, still oder warm. So entsteht keine Kopie eines Objekts, sondern seine Atmosphäre.",
|
||||
},
|
||||
{
|
||||
label: "03 / REDUZIEREN",
|
||||
title: "Wirkung schärfen",
|
||||
text: "Wir lassen weg, was beliebig ist. Übrig bleibt eine klare Signatur: charaktervoll, hochwertig und bewusst nischig. Ein Duft, der Haltung zeigt, statt nur zu gefallen.",
|
||||
},
|
||||
];
|
||||
|
||||
const CREDENTIAL_CARDS = [
|
||||
{
|
||||
label: "ATELIER / ENTWICKLUNG",
|
||||
title: "Komposition aus einem klaren Duftverständnis",
|
||||
text: "Jede Arbeit basiert auf einem kuratierten Konzept, einer definierten Materialwelt und mehreren internen Entwicklungsstufen statt auf kurzfristigen Trendbriefings.",
|
||||
},
|
||||
{
|
||||
label: "MATERIALLOGIK",
|
||||
title: "Noten folgen einer Idee, nicht bloss einer Wirkung",
|
||||
text: "Rohstoffe und Akkorde werden danach gewählt, welche Oberfläche, Temperatur oder Spannung sie transportieren – nicht nur danach, ob sie möglichst sofort gefallen.",
|
||||
},
|
||||
{
|
||||
label: "CHARGENPRINZIP",
|
||||
title: "Kleinserie statt gesichtsloser Massenästhetik",
|
||||
text: "atmos denkt in kontrollierten Batches und klaren Editionen. Das stärkt Nachvollziehbarkeit, Qualitätsfokus und die Eigenständigkeit jeder Komposition.",
|
||||
},
|
||||
{
|
||||
label: "QUALITÄTSPRÜFUNG",
|
||||
title: "Komposition, Haltbarkeit und Verlauf werden bewusst geprüft",
|
||||
text: "Bewertet werden nicht nur Auftakt und Präsenz, sondern auch Übergänge, Balance, Textur, Wiedererkennbarkeit und das Verhalten auf Haut über mehrere Stunden.",
|
||||
},
|
||||
];
|
||||
|
||||
function AboutPage() {
|
||||
return (
|
||||
<div className="about-page">
|
||||
<PageMeta
|
||||
title="About"
|
||||
description="atmos dekodiert Atmosphären und übersetzt Materialität, Raum und Charakter in konzeptionelle Düfte. Nischig. Hochwertig. Made in Switzerland."
|
||||
path="/about"
|
||||
/>
|
||||
<SharedNavbar variant="hero" />
|
||||
|
||||
<main className="shell">
|
||||
<main id="main-content" className="shell">
|
||||
<section className="about-hero" data-reveal-group>
|
||||
<div className="about-hero-copy">
|
||||
<span className="about-kicker" data-reveal="fade">
|
||||
@ -80,24 +135,27 @@ function AboutPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="about-proof-strip" data-reveal-group data-reveal-start="top 90%">
|
||||
<div className="about-proof-item" data-reveal="fade">
|
||||
<span className="about-label">ATELIER</span>
|
||||
<p>Entwicklung aus einem kuratierten Duft- und Materialkontext</p>
|
||||
</div>
|
||||
<div className="about-proof-item" data-reveal="fade">
|
||||
<span className="about-label">KLEINSERIEN</span>
|
||||
<p>Chargenbasiert gedacht statt massenmarktfähig optimiert</p>
|
||||
</div>
|
||||
<div className="about-proof-item" data-reveal="fade">
|
||||
<span className="about-label">KOMPOSITION</span>
|
||||
<p>Materiallogik vor Trendformel und Lautstärke</p>
|
||||
</div>
|
||||
<div className="about-proof-item" data-reveal="fade">
|
||||
<span className="about-label">HERKUNFT</span>
|
||||
<p>Creative Direction und Qualitätsanspruch aus der Schweiz</p>
|
||||
</div>
|
||||
</section>
|
||||
<Grid
|
||||
as="section"
|
||||
gap="sm"
|
||||
className="about-proof-strip"
|
||||
data-reveal-group
|
||||
data-reveal-start="top 90%"
|
||||
>
|
||||
{PROOF_ITEMS.map((item) => (
|
||||
<Col
|
||||
key={item.label}
|
||||
span={12}
|
||||
md={6}
|
||||
lg={3}
|
||||
className="about-proof-item"
|
||||
data-reveal="fade"
|
||||
>
|
||||
<span className="about-label">{item.label}</span>
|
||||
<p>{item.text}</p>
|
||||
</Col>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<section className="about-quote-block" data-reveal-group>
|
||||
<p data-reveal="fade">
|
||||
@ -106,37 +164,28 @@ function AboutPage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="about-grid-section" data-reveal-group data-reveal-start="top 84%">
|
||||
<div className="about-card" data-reveal="fade">
|
||||
<span className="about-label">01 / DEKODIEREN</span>
|
||||
<h3>Atmosphäre lesen</h3>
|
||||
<p>
|
||||
Wir beobachten Licht, Materialität, Temperatur, Distanz und Spannung.
|
||||
Nicht als Moodboard allein, sondern als System von Eindrücken, das
|
||||
eine bestimmte Wirkung erzeugt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="about-card" data-reveal="fade">
|
||||
<span className="about-label">02 / VERDICHTEN</span>
|
||||
<h3>In Duft übersetzen</h3>
|
||||
<p>
|
||||
Diese Eindrücke werden in eine olfaktorische Sprache übertragen:
|
||||
mineralisch, staubig, metallisch, glatt, rau, still oder warm. So
|
||||
entsteht keine Kopie eines Objekts, sondern seine Atmosphäre.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="about-card" data-reveal="fade">
|
||||
<span className="about-label">03 / REDUZIEREN</span>
|
||||
<h3>Wirkung schärfen</h3>
|
||||
<p>
|
||||
Wir lassen weg, was beliebig ist. Übrig bleibt eine klare Signatur:
|
||||
charaktervoll, hochwertig und bewusst nischig. Ein Duft, der Haltung
|
||||
zeigt, statt nur zu gefallen.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<Grid
|
||||
as="section"
|
||||
gap="sm"
|
||||
className="about-grid-section"
|
||||
data-reveal-group
|
||||
data-reveal-start="top 84%"
|
||||
>
|
||||
{APPROACH_CARDS.map((card) => (
|
||||
<Col
|
||||
key={card.label}
|
||||
span={12}
|
||||
md={6}
|
||||
lg={4}
|
||||
className="about-card"
|
||||
data-reveal="fade"
|
||||
>
|
||||
<span className="about-label">{card.label}</span>
|
||||
<h3>{card.title}</h3>
|
||||
<p>{card.text}</p>
|
||||
</Col>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<section
|
||||
className="about-section about-section--split about-process-section"
|
||||
@ -168,51 +217,28 @@ function AboutPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
<Grid
|
||||
as="section"
|
||||
gap="sm"
|
||||
className="about-credentials-grid"
|
||||
data-reveal-group
|
||||
data-reveal-start="top 84%"
|
||||
>
|
||||
<article className="about-credential-card" data-reveal="fade">
|
||||
<span className="about-label">ATELIER / ENTWICKLUNG</span>
|
||||
<h3>Komposition aus einem klaren Duftverständnis</h3>
|
||||
<p>
|
||||
Jede Arbeit basiert auf einem kuratierten Konzept, einer definierten
|
||||
Materialwelt und mehreren internen Entwicklungsstufen statt auf
|
||||
kurzfristigen Trendbriefings.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="about-credential-card" data-reveal="fade">
|
||||
<span className="about-label">MATERIALLOGIK</span>
|
||||
<h3>Noten folgen einer Idee, nicht bloss einer Wirkung</h3>
|
||||
<p>
|
||||
Rohstoffe und Akkorde werden danach gewählt, welche Oberfläche,
|
||||
Temperatur oder Spannung sie transportieren – nicht nur danach, ob sie
|
||||
möglichst sofort gefallen.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="about-credential-card" data-reveal="fade">
|
||||
<span className="about-label">CHARGENPRINZIP</span>
|
||||
<h3>Kleinserie statt gesichtsloser Massenästhetik</h3>
|
||||
<p>
|
||||
atmos denkt in kontrollierten Batches und klaren Editionen. Das stärkt
|
||||
Nachvollziehbarkeit, Qualitätsfokus und die Eigenständigkeit jeder
|
||||
Komposition.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="about-credential-card" data-reveal="fade">
|
||||
<span className="about-label">QUALITÄTSPRÜFUNG</span>
|
||||
<h3>Komposition, Haltbarkeit und Verlauf werden bewusst geprüft</h3>
|
||||
<p>
|
||||
Bewertet werden nicht nur Auftakt und Präsenz, sondern auch Übergänge,
|
||||
Balance, Textur, Wiedererkennbarkeit und das Verhalten auf Haut über
|
||||
mehrere Stunden.
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
{CREDENTIAL_CARDS.map((card) => (
|
||||
<Col
|
||||
key={card.label}
|
||||
as="article"
|
||||
span={12}
|
||||
md={6}
|
||||
className="about-credential-card"
|
||||
data-reveal="fade"
|
||||
>
|
||||
<span className="about-label">{card.label}</span>
|
||||
<h3>{card.title}</h3>
|
||||
<p>{card.text}</p>
|
||||
</Col>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<section className="about-method-section" data-reveal-group>
|
||||
<div className="about-method-copy">
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
import SharedNavbar from "../components/SharedNavbar";
|
||||
import PageMeta from "../components/seo/PageMeta";
|
||||
import "./DatenschutzPage.css";
|
||||
|
||||
function DatenschutzPage() {
|
||||
return (
|
||||
<div className="datenschutz-page">
|
||||
<PageMeta
|
||||
title="Datenschutz"
|
||||
description="Datenschutzerklärung von atmos: Erhebung, Speicherung und Verarbeitung personenbezogener Daten."
|
||||
path="/datenschutz"
|
||||
/>
|
||||
<SharedNavbar variant="hero" />
|
||||
|
||||
<main className="shell">
|
||||
<main id="main-content" className="shell">
|
||||
<section className="datenschutz-hero" data-reveal-group>
|
||||
<span className="datenschutz-kicker" data-reveal="fade">RECHTLICHE ANGABEN</span>
|
||||
<h1 data-reveal="lines">DATENSCHUTZ</h1>
|
||||
|
||||
@ -95,7 +95,7 @@
|
||||
border: 0;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
filter: saturate(0.92) contrast(1.04) drop-shadow(0 34px 72px rgba(0, 0, 0, 0.42));
|
||||
filter: saturate(0.92) contrast(1.04) var(--shadow-product);
|
||||
}
|
||||
|
||||
.discovery-panel-facts div,
|
||||
@ -383,15 +383,8 @@
|
||||
max-width: 11ch;
|
||||
}
|
||||
|
||||
.discovery-steps-grid,
|
||||
.discovery-products-grid,
|
||||
.discovery-comparison-grid {
|
||||
display: grid;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
/* Layout columns are provided by the global Grid12 system (see Grid.jsx). */
|
||||
.discovery-steps-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
margin-top: clamp(1.6rem, 4vw, 4rem);
|
||||
}
|
||||
|
||||
@ -438,9 +431,7 @@
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.discovery-products-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
/* .discovery-products-grid columns provided by Grid12 (see Grid.jsx). */
|
||||
|
||||
.discovery-product-card {
|
||||
min-height: clamp(390px, 40vw, 560px);
|
||||
@ -465,7 +456,6 @@
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@ -474,7 +464,7 @@
|
||||
width: min(86%, 360px);
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 24px 48px rgba(0, 0, 0, 0.24));
|
||||
filter: var(--shadow-product-card);
|
||||
transition: transform var(--duration-slow) var(--ease-out);
|
||||
}
|
||||
|
||||
@ -509,7 +499,6 @@
|
||||
|
||||
.discovery-comparison-grid {
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
@ -636,10 +625,7 @@
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.discovery-products-grid,
|
||||
.discovery-steps-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
/* products & steps grids: handled by Grid12 md:6 col-spans */
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
@ -687,12 +673,10 @@
|
||||
}
|
||||
|
||||
.discovery-panel-facts,
|
||||
.discovery-section-heading,
|
||||
.discovery-comparison-grid,
|
||||
.discovery-products-grid,
|
||||
.discovery-steps-grid {
|
||||
.discovery-section-heading {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
/* comparison-, products- & steps-grid: Grid12 collapses to 1 col by default */
|
||||
|
||||
.discovery-price-row {
|
||||
order: 1;
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import perfumes from "../data/perfumes";
|
||||
import SharedNavbar from "../components/SharedNavbar";
|
||||
import Grid, { Col } from "../components/layout/Grid";
|
||||
import PageMeta from "../components/seo/PageMeta";
|
||||
import { useShop } from "../shop/useShop";
|
||||
import "./DiscoverySetPage.css";
|
||||
|
||||
@ -162,15 +164,23 @@ function DiscoveryProcessSection() {
|
||||
<h2 data-reveal="lines">So funktioniert's</h2>
|
||||
</div>
|
||||
|
||||
<div className="discovery-steps-grid">
|
||||
<Grid gap="sm" className="discovery-steps-grid">
|
||||
{discoverySteps.map((step) => (
|
||||
<article className="discovery-step-card" key={step.number} data-reveal="fade">
|
||||
<Col
|
||||
key={step.number}
|
||||
as="article"
|
||||
span={12}
|
||||
md={6}
|
||||
lg={4}
|
||||
className="discovery-step-card"
|
||||
data-reveal="fade"
|
||||
>
|
||||
<span className="discovery-step-number">{step.number}</span>
|
||||
<h3>{step.title}</h3>
|
||||
<p>{step.text}</p>
|
||||
</article>
|
||||
</Col>
|
||||
))}
|
||||
</div>
|
||||
</Grid>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -185,15 +195,23 @@ function DiscoveryIncludedSection() {
|
||||
<h2 data-reveal="lines">Alle 6 Signature-Düfte zum Testen.</h2>
|
||||
</div>
|
||||
|
||||
<div className="discovery-products-grid">
|
||||
<Grid gap="sm" className="discovery-products-grid">
|
||||
{perfumes.map((perfume) => (
|
||||
<article className="discovery-product-card" key={perfume.id} data-reveal="fade">
|
||||
<Col
|
||||
key={perfume.id}
|
||||
as="article"
|
||||
span={12}
|
||||
md={6}
|
||||
lg={4}
|
||||
className="discovery-product-card"
|
||||
data-reveal="fade"
|
||||
>
|
||||
<span className="discovery-product-index">{perfume.id}</span>
|
||||
|
||||
<div className="discovery-product-image">
|
||||
<img
|
||||
src={perfume.image}
|
||||
alt={perfume.name}
|
||||
alt={`Atmos ${perfume.name} Sample im Discovery Set`}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
@ -209,9 +227,9 @@ function DiscoveryIncludedSection() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</Col>
|
||||
))}
|
||||
</div>
|
||||
</Grid>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -219,13 +237,16 @@ function DiscoveryIncludedSection() {
|
||||
function DiscoveryComparisonSection() {
|
||||
return (
|
||||
<section className="discovery-comparison-section" data-reveal-group>
|
||||
<div className="discovery-comparison-grid">
|
||||
<Grid gap="sm" className="discovery-comparison-grid">
|
||||
{discoveryComparison.map((item) => (
|
||||
<article
|
||||
<Col
|
||||
key={item.title}
|
||||
as="article"
|
||||
span={12}
|
||||
md={6}
|
||||
className={`discovery-comparison-card${
|
||||
item.highlight ? " discovery-comparison-card--highlight" : ""
|
||||
}`}
|
||||
key={item.title}
|
||||
data-reveal="fade"
|
||||
>
|
||||
<div className="discovery-comparison-head">
|
||||
@ -235,9 +256,9 @@ function DiscoveryComparisonSection() {
|
||||
<h3>{item.title}</h3>
|
||||
</div>
|
||||
<p>{item.text}</p>
|
||||
</article>
|
||||
</Col>
|
||||
))}
|
||||
</div>
|
||||
</Grid>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -271,9 +292,14 @@ function DiscoverySetPage() {
|
||||
|
||||
return (
|
||||
<div className="discovery-page">
|
||||
<PageMeta
|
||||
title="Discovery Set"
|
||||
description="6 × 2ml Samples aller atmos-Düfte. Eine Woche pro Duft tragen, verstehen, welcher passt. CHF 48, anrechenbar beim Full-Size-Kauf."
|
||||
path="/discovery-set"
|
||||
/>
|
||||
<SharedNavbar variant="hero" active="testen" />
|
||||
|
||||
<main className="shell">
|
||||
<main id="main-content" className="shell">
|
||||
<DiscoveryHero onBuy={buyDiscoverySet} />
|
||||
<DiscoveryStorySection />
|
||||
<DiscoveryIncludedSection />
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
import SharedNavbar from "../components/SharedNavbar";
|
||||
import PageMeta from "../components/seo/PageMeta";
|
||||
import "./ImpressumPage.css";
|
||||
|
||||
function ImpressumPage() {
|
||||
return (
|
||||
<div className="impressum-page">
|
||||
<PageMeta
|
||||
title="Impressum"
|
||||
description="Rechtliche Angaben, Verantwortlichkeit und Erreichbarkeit von atmos."
|
||||
path="/impressum"
|
||||
/>
|
||||
<SharedNavbar variant="hero" />
|
||||
|
||||
<main className="shell">
|
||||
<main id="main-content" className="shell">
|
||||
<section className="impressum-hero" data-reveal-group>
|
||||
<span className="impressum-kicker" data-reveal="fade">
|
||||
RECHTLICHE ANGABEN
|
||||
|
||||
@ -89,10 +89,10 @@ body.theme-light .hero-wordmark__image {
|
||||
|
||||
.hero-product {
|
||||
position: absolute;
|
||||
top: clamp(22rem, 58vh, 44rem);
|
||||
top: clamp(20rem, 52vh, 40rem);
|
||||
left: 50%;
|
||||
z-index: 5;
|
||||
width: clamp(19rem, 29vw, 38rem);
|
||||
width: clamp(20rem, 31vw, 42rem);
|
||||
aspect-ratio: 1078 / 1284;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
@ -117,6 +117,7 @@ body.theme-light .hero-wordmark__image {
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
filter: var(--shadow-product);
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
@ -193,7 +194,6 @@ body.theme-light .hero-wordmark__image {
|
||||
.btn-primary {
|
||||
background: var(--theme-accent);
|
||||
color: #fff;
|
||||
box-shadow: 0 18px 42px rgba(var(--theme-accent-rgb) / 0.26);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@ -309,12 +309,10 @@ body.theme-light .hero-wordmark__image {
|
||||
grid-column: 1 / span 8;
|
||||
}
|
||||
|
||||
/* GRID */
|
||||
/* GRID — columns provided by the global Grid12 system (see Grid.jsx).
|
||||
.product-grid only contributes container queries here. */
|
||||
.product-grid {
|
||||
container-type: inline-size;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.product-card {
|
||||
@ -451,17 +449,6 @@ body.theme-light .hero-wordmark__image {
|
||||
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 {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@ -469,6 +456,7 @@ body.theme-light .hero-wordmark__image {
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
border-radius: 0;
|
||||
filter: var(--shadow-product-card);
|
||||
transition: transform var(--duration-slow) var(--ease-out);
|
||||
}
|
||||
|
||||
@ -589,9 +577,9 @@ body.theme-light .hero-wordmark__image {
|
||||
|
||||
.discovery-btn {
|
||||
margin-top: clamp(1.3rem, 3vw, 2.1rem);
|
||||
background: #fff;
|
||||
color: #d64f00;
|
||||
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.18);
|
||||
background: var(--theme-surface);
|
||||
color: var(--theme-text);
|
||||
|
||||
}
|
||||
|
||||
.discovery-banner {
|
||||
@ -632,6 +620,9 @@ body.theme-light .hero-wordmark__image {
|
||||
}
|
||||
|
||||
.hero-product {
|
||||
/* Tablet range (≤ 1180 px): viewports are taller relative to width,
|
||||
so we re-center the product instead of inheriting the desktop 52vh. */
|
||||
top: clamp(17rem, 44vh, 28rem);
|
||||
width: clamp(21rem, 45vw, 34rem);
|
||||
}
|
||||
|
||||
@ -657,9 +648,7 @@ body.theme-light .hero-wordmark__image {
|
||||
max-width: 18rem;
|
||||
}
|
||||
|
||||
.product-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
/* .product-grid columns handled by Grid12 (md:6 / lg:4) */
|
||||
|
||||
.discovery-section {
|
||||
grid-template-columns: 1fr;
|
||||
@ -688,10 +677,17 @@ body.theme-light .hero-wordmark__image {
|
||||
}
|
||||
|
||||
.hero-product {
|
||||
top: clamp(20.75rem, 49svh, 25.5rem);
|
||||
left: 44%;
|
||||
width: min(72vw, 19rem);
|
||||
transform: translate(-50%, -58%);
|
||||
/* Phone: slot spans the page width minus the standard
|
||||
--page-x gutter on each side. The img inside uses
|
||||
object-fit: contain, so the bottle stays naturally
|
||||
centered and proportional within the box. */
|
||||
aspect-ratio: auto;
|
||||
top: clamp(11rem, 24svh, 14rem);
|
||||
left: var(--page-x);
|
||||
right: var(--page-x);
|
||||
width: auto;
|
||||
height: clamp(18rem, 44svh, 24rem);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
@ -723,9 +719,7 @@ body.theme-light .hero-wordmark__image {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.product-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
/* .product-grid mobile: Grid12 base = col-span-12 = single column */
|
||||
|
||||
.section-heading::after {
|
||||
right: auto;
|
||||
@ -761,9 +755,10 @@ body.theme-light .hero-wordmark__image {
|
||||
}
|
||||
|
||||
.hero-product {
|
||||
top: clamp(20.25rem, 48svh, 24.5rem);
|
||||
left: 43%;
|
||||
width: min(74vw, 18.25rem);
|
||||
/* Slightly tighter slot for compact phones; centering and
|
||||
full-width box come from the .hero-product base above. */
|
||||
top: clamp(10rem, 22svh, 13rem);
|
||||
height: clamp(17rem, 42svh, 22rem);
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
|
||||
@ -8,10 +8,12 @@ import {
|
||||
import { Link } from "react-router";
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import Grid from "../components/layout/Grid";
|
||||
import HeroSection from "../components/landing/HeroSection";
|
||||
import SharedNavbar from "../components/SharedNavbar";
|
||||
import { useProductTransition } from "../transitions/ProductTransitionContext";
|
||||
import PageMeta from "../components/seo/PageMeta";
|
||||
import perfumes from "../data/perfumes";
|
||||
import { useProductTransition } from "../transitions/ProductTransitionContext";
|
||||
import "../pages/LandingPage.css";
|
||||
import "../style/navbar.css";
|
||||
|
||||
@ -57,6 +59,10 @@ function LandingPage() {
|
||||
headlineLineRefs.current[1] = element;
|
||||
}, []);
|
||||
|
||||
const setHeadlineTertiaryRef = useCallback((element) => {
|
||||
headlineLineRefs.current[2] = element;
|
||||
}, []);
|
||||
|
||||
const setWordmarkRef = useCallback((element) => {
|
||||
heroWordmarkRef.current = element;
|
||||
}, []);
|
||||
@ -427,6 +433,12 @@ function LandingPage() {
|
||||
|
||||
return (
|
||||
<div className="page" ref={pageRef}>
|
||||
<PageMeta
|
||||
title="atmos · Konzeptionelle Düfte aus der Schweiz"
|
||||
description="atmos — konzeptionelle Nischendüfte zwischen Materialität, Raum und Charakter. Sechs Düfte als Discovery Set oder 50 ml Flakon. Made in Switzerland."
|
||||
path="/"
|
||||
bareTitle
|
||||
/>
|
||||
<SharedNavbar variant="hero" active="atmos" />
|
||||
|
||||
<HeroSection
|
||||
@ -434,6 +446,7 @@ function LandingPage() {
|
||||
heroImageRef={heroImageRef}
|
||||
setHeadlinePrimaryRef={setHeadlinePrimaryRef}
|
||||
setHeadlineSecondaryRef={setHeadlineSecondaryRef}
|
||||
setHeadlineTertiaryRef={setHeadlineTertiaryRef}
|
||||
setWordmarkRef={setWordmarkRef}
|
||||
setDescriptionRef={setDescriptionRef}
|
||||
setActionsRef={setActionsRef}
|
||||
@ -442,7 +455,7 @@ function LandingPage() {
|
||||
overlayTextRef={overlayTextRef}
|
||||
/>
|
||||
|
||||
<main>
|
||||
<main id="main-content">
|
||||
<section className="section" id="dufte" data-reveal-group>
|
||||
<div className="section-heading">
|
||||
<h2 data-reveal="lines">
|
||||
@ -452,11 +465,11 @@ function LandingPage() {
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="product-grid">
|
||||
<Grid gap="sm" className="product-grid">
|
||||
{perfumes.map((item, index) => (
|
||||
<Link
|
||||
to={`/duft/${item.slug}`}
|
||||
className="product-card"
|
||||
className="product-card col-span-12 md:col-span-6 lg:col-span-4"
|
||||
key={item.id}
|
||||
onClick={(event) => startProductTransition(event, item)}
|
||||
ref={(element) => {
|
||||
@ -505,7 +518,7 @@ function LandingPage() {
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</Grid>
|
||||
</section>
|
||||
|
||||
<section
|
||||
@ -534,7 +547,7 @@ function LandingPage() {
|
||||
|
||||
<div className="discovery-banner">
|
||||
<img
|
||||
src="/atmos-discovery-set-thumbnail.png"
|
||||
src="/atmos-discovery-set-thumbnail.webp"
|
||||
alt="Discovery Set"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import SharedNavbar from "../components/SharedNavbar";
|
||||
import PageMeta from "../components/seo/PageMeta";
|
||||
import { formatChf } from "../shop/money";
|
||||
import { useShop } from "../shop/useShop";
|
||||
import "./SmallBatchPage.css";
|
||||
@ -60,9 +61,14 @@ function SmallBatchPage() {
|
||||
|
||||
return (
|
||||
<div className="small-page">
|
||||
<PageMeta
|
||||
title="Small Batch · Early Access"
|
||||
description="atmos Small Batch Programm: limitierte Prototypen, Archivstücke und Early-Access-Düfte für aktive Kund:innen."
|
||||
path="/small-batch"
|
||||
/>
|
||||
<SharedNavbar variant="hero" />
|
||||
|
||||
<main className="shell">
|
||||
<main id="main-content" className="shell">
|
||||
<section className="small-hero" data-reveal-group>
|
||||
<span className="small-kicker" data-reveal="fade">
|
||||
SMALL BATCH / ARCHIVE / PROTOTYPE
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
import { Link } from "react-router";
|
||||
import SharedNavbar from "../components/SharedNavbar";
|
||||
import PageMeta from "../components/seo/PageMeta";
|
||||
import "./SupportPage.css";
|
||||
|
||||
function SupportPage() {
|
||||
return (
|
||||
<div className="support-page">
|
||||
<PageMeta
|
||||
title="Support"
|
||||
description="Kontakt, Beratung und Hilfe rund um atmos: Bestellung, Versand, Düfte und Discovery Set."
|
||||
path="/support"
|
||||
/>
|
||||
<SharedNavbar variant="hero" />
|
||||
|
||||
<main className="shell">
|
||||
<main id="main-content" className="shell">
|
||||
<section className="support-hero" data-reveal-group>
|
||||
<div className="support-hero-copy">
|
||||
<span className="support-kicker" data-reveal="fade">
|
||||
|
||||
27
parfum-shop/src/style/breakpoints.css
Normal file
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Breakpoints
|
||||
* ----------------------------------------------------------------------------
|
||||
* Mobile-first standard. New layout code must use min-width queries below.
|
||||
*
|
||||
* sm >= 480px Phone landscape
|
||||
* md >= 768px Tablet
|
||||
* lg >= 1024px Tablet landscape / small desktop
|
||||
* xl >= 1280px Desktop
|
||||
*
|
||||
* Existing legacy max-width queries in page-level CSS files remain in place
|
||||
* and are migrated incrementally. See plan, Phase E.
|
||||
*/
|
||||
|
||||
/* Visibility utilities */
|
||||
.is-hidden-md-down {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.is-hidden-md-down {
|
||||
display: revert;
|
||||
}
|
||||
.is-hidden-md-up {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
131
parfum-shop/src/style/grid.css
Normal file
@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 12-Column Grid System
|
||||
* ----------------------------------------------------------------------------
|
||||
* Mobile-first. All columns span 12 by default; `md:` and `lg:` modifiers
|
||||
* narrow the span on larger viewports.
|
||||
*
|
||||
* <Container size="default|narrow|wide">
|
||||
* <Section spacing="xs|sm|md|lg">
|
||||
* <Grid cols={12} gap="sm|md|lg">
|
||||
* <Col span={12} md={6} lg={4} />
|
||||
*/
|
||||
|
||||
/* Containers */
|
||||
.container,
|
||||
.container-narrow,
|
||||
.container-wide {
|
||||
width: var(--container);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.container-narrow {
|
||||
width: var(--container-narrow);
|
||||
}
|
||||
|
||||
.container-wide {
|
||||
width: var(--container-wide);
|
||||
}
|
||||
|
||||
/* Section spacing */
|
||||
.section-pad-xs {
|
||||
padding-block: var(--section-y-xs);
|
||||
}
|
||||
|
||||
.section-pad-sm {
|
||||
padding-block: var(--section-y-sm);
|
||||
}
|
||||
|
||||
.section-pad-md {
|
||||
padding-block: var(--section-y);
|
||||
}
|
||||
|
||||
.section-pad-lg {
|
||||
padding-block: var(--section-y-lg);
|
||||
}
|
||||
|
||||
/* Grid */
|
||||
.grid-12 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
gap: var(--grid-gap);
|
||||
}
|
||||
|
||||
.grid-gap-sm {
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.grid-gap-md {
|
||||
gap: var(--gap-md);
|
||||
}
|
||||
|
||||
.grid-gap-lg {
|
||||
gap: var(--gap-lg);
|
||||
}
|
||||
|
||||
/* Base column spans (mobile-first; default = full width) */
|
||||
.col-span-1 { grid-column: span 1; }
|
||||
.col-span-2 { grid-column: span 2; }
|
||||
.col-span-3 { grid-column: span 3; }
|
||||
.col-span-4 { grid-column: span 4; }
|
||||
.col-span-5 { grid-column: span 5; }
|
||||
.col-span-6 { grid-column: span 6; }
|
||||
.col-span-7 { grid-column: span 7; }
|
||||
.col-span-8 { grid-column: span 8; }
|
||||
.col-span-9 { grid-column: span 9; }
|
||||
.col-span-10 { grid-column: span 10; }
|
||||
.col-span-11 { grid-column: span 11; }
|
||||
.col-span-12 { grid-column: span 12; }
|
||||
|
||||
.col-start-1 { grid-column-start: 1; }
|
||||
.col-start-2 { grid-column-start: 2; }
|
||||
.col-start-3 { grid-column-start: 3; }
|
||||
.col-start-4 { grid-column-start: 4; }
|
||||
.col-start-5 { grid-column-start: 5; }
|
||||
.col-start-6 { grid-column-start: 6; }
|
||||
.col-start-7 { grid-column-start: 7; }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:col-span-1 { grid-column: span 1; }
|
||||
.md\:col-span-2 { grid-column: span 2; }
|
||||
.md\:col-span-3 { grid-column: span 3; }
|
||||
.md\:col-span-4 { grid-column: span 4; }
|
||||
.md\:col-span-5 { grid-column: span 5; }
|
||||
.md\:col-span-6 { grid-column: span 6; }
|
||||
.md\:col-span-7 { grid-column: span 7; }
|
||||
.md\:col-span-8 { grid-column: span 8; }
|
||||
.md\:col-span-9 { grid-column: span 9; }
|
||||
.md\:col-span-10 { grid-column: span 10; }
|
||||
.md\:col-span-11 { grid-column: span 11; }
|
||||
.md\:col-span-12 { grid-column: span 12; }
|
||||
|
||||
.md\:col-start-1 { grid-column-start: 1; }
|
||||
.md\:col-start-2 { grid-column-start: 2; }
|
||||
.md\:col-start-3 { grid-column-start: 3; }
|
||||
.md\:col-start-4 { grid-column-start: 4; }
|
||||
.md\:col-start-5 { grid-column-start: 5; }
|
||||
.md\:col-start-6 { grid-column-start: 6; }
|
||||
.md\:col-start-7 { grid-column-start: 7; }
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.lg\:col-span-1 { grid-column: span 1; }
|
||||
.lg\:col-span-2 { grid-column: span 2; }
|
||||
.lg\:col-span-3 { grid-column: span 3; }
|
||||
.lg\:col-span-4 { grid-column: span 4; }
|
||||
.lg\:col-span-5 { grid-column: span 5; }
|
||||
.lg\:col-span-6 { grid-column: span 6; }
|
||||
.lg\:col-span-7 { grid-column: span 7; }
|
||||
.lg\:col-span-8 { grid-column: span 8; }
|
||||
.lg\:col-span-9 { grid-column: span 9; }
|
||||
.lg\:col-span-10 { grid-column: span 10; }
|
||||
.lg\:col-span-11 { grid-column: span 11; }
|
||||
.lg\:col-span-12 { grid-column: span 12; }
|
||||
|
||||
.lg\:col-start-1 { grid-column-start: 1; }
|
||||
.lg\:col-start-2 { grid-column-start: 2; }
|
||||
.lg\:col-start-3 { grid-column-start: 3; }
|
||||
.lg\:col-start-4 { grid-column-start: 4; }
|
||||
.lg\:col-start-5 { grid-column-start: 5; }
|
||||
.lg\:col-start-6 { grid-column-start: 6; }
|
||||
.lg\:col-start-7 { grid-column-start: 7; }
|
||||
}
|
||||
126
parfum-shop/src/style/tokens.css
Normal file
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Design Tokens
|
||||
* ----------------------------------------------------------------------------
|
||||
* Single source of truth for theme colors, spacing, typography, breakpoints,
|
||||
* grid and motion. Imported once via index.css.
|
||||
*
|
||||
* Tokens follow a BEM-like prefix scheme:
|
||||
* --theme-* colors and surfaces (theme-aware via body.theme-light)
|
||||
* --gap-* spacing primitives
|
||||
* --section-* vertical section rhythm
|
||||
* --container-* horizontal content widths
|
||||
* --text-* typographic scale
|
||||
* --bp-* breakpoint values (mobile-first)
|
||||
* --grid-* layout grid system
|
||||
* --ease-* motion easing
|
||||
* --duration-* motion duration
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Brand */
|
||||
--theme-black: #262626;
|
||||
--theme-white: #eaeaea;
|
||||
--theme-accent: #ff6a00;
|
||||
--theme-accent-rgb: 255 106 0;
|
||||
|
||||
/* Surfaces (dark theme — default) */
|
||||
--theme-bg: #262626;
|
||||
--theme-surface: #2f2f2f;
|
||||
--theme-surface-soft: #363636;
|
||||
--theme-paper: #404040;
|
||||
--theme-text: #eaeaea;
|
||||
--theme-text-muted: #c8c8c8;
|
||||
--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);
|
||||
|
||||
/* Canonical drop-shadow for product bottle artwork. Used wherever the
|
||||
flacon has room (hero, recommendation banner, product transition). */
|
||||
--shadow-product: drop-shadow(0 34px 72px rgba(0, 0, 0, 0.42));
|
||||
|
||||
/* Tighter version for small grid contexts (landing product cards,
|
||||
discovery product thumbnails, recommendation cards) — fits inside
|
||||
the card bounds even with overflow: hidden. */
|
||||
--shadow-product-card: drop-shadow(0 14px 26px rgba(0, 0, 0, 0.3));
|
||||
|
||||
/* Footer keeps its dark identity in both themes */
|
||||
--footer-bg: #171717;
|
||||
--footer-text: #f5f5f5;
|
||||
--footer-text-muted: rgba(255, 255, 255, 0.7);
|
||||
--footer-text-faint: rgba(255, 255, 255, 0.52);
|
||||
--footer-border: rgba(255, 255, 255, 0.08);
|
||||
--footer-watermark: rgba(255, 255, 255, 0.035);
|
||||
|
||||
/* Spacing — horizontal page padding */
|
||||
--page-x: clamp(1rem, 4vw, 5rem);
|
||||
|
||||
/* Spacing — vertical section rhythm */
|
||||
--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);
|
||||
|
||||
/* Containers */
|
||||
--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;
|
||||
|
||||
/* Spacing — reusable gaps */
|
||||
--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 — design intent: hard edges everywhere */
|
||||
--radius-xs: 0;
|
||||
--radius-sm: 0;
|
||||
--radius-md: 0;
|
||||
--radius-lg: 0;
|
||||
--radius-xl: 0;
|
||||
|
||||
/* Typography scale */
|
||||
--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);
|
||||
|
||||
/* Breakpoints (mobile-first, used in JS via matchMedia and as docs) */
|
||||
--bp-sm: 480px;
|
||||
--bp-md: 768px;
|
||||
--bp-lg: 1024px;
|
||||
--bp-xl: 1280px;
|
||||
|
||||
/* Grid system */
|
||||
--grid-cols: 12;
|
||||
--grid-gap: var(--gap-md);
|
||||
|
||||
/* Accessibility */
|
||||
--touch-target-min: 44px;
|
||||
|
||||
/* Motion */
|
||||
--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;
|
||||
}
|
||||
|
||||
body.theme-light {
|
||||
--theme-bg: #eaeaea;
|
||||
--theme-surface: #f5f5f5;
|
||||
--theme-surface-soft: #f0f0f0;
|
||||
--theme-paper: #ffffff;
|
||||
--theme-text: #262626;
|
||||
--theme-text-muted: #5f5f5f;
|
||||
--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);
|
||||
}
|
||||